×

实例分析使用Unity3D开发VR游戏

消耗积分:1 | 格式:rar | 大小:0.16 MB | 2017-10-10

分享资料个

使用Unity3D开发一款VR弹球游戏2016-07-05 17:06
  开发VR游戏首先要选择一个合适的平台。目前可供选择的平台不多, Google的Cardboard和Oculus Rift等头戴式显示设备都是可供选择的对象。但Cardboard的价格低廉,市场占有率更高,用它来作为移动端进行开发更为划算一些。当然,选择其一并不代表我们舍弃了其它开发平台的优点,同时Unity3D游戏引擎拥有跨平台的特性,对项目经过简单的修改便可将VR项目移植到PC或者其他平台中。
  使用Unity3D开发VR游戏十分便捷:
  首先在于如场景、模型等丰富的素材资源,开发者们无需再为美工建模这些相对耗时间的工作而困扰。
  其次在于便捷的引擎支持,Oculus Rift、Google Cardboard和HTC Vive等产品均提供了对于Unity3D引擎的支持,只需将官方提供的插件包导入到目标项目中,便可使用相应的VR产品了。
  随着Unity5.3版本的发布,Unity增强了对VR开发的兼容性,在项目设置中我们可以直接选择使用Oculus或是Open VR进行开发。
  下面就以一款简单的VR弹球游戏为例子,谈谈使用Unity3D游戏引擎开发VR游戏的流程。
  先说说VR弹球游戏的规则。和传统的弹球游戏一样,在VR弹球游戏中有两个玩家,一个是用户,另一个则是AI。玩家分别控制己方的一块弹球板,当弹球碰到弹球板的时候,球则会反弹回去。如果一方没有成功反弹弹球的话则该方失败。
  那么对于这款VR弹球游戏,具体应该怎样开发呢?
  在确定了开发平台之后,我们需要在Unity中导入相关的SDK。如选用Oculus Rift则导入Oculus Rift的SDK,选用Google Cardboard则导入Cardboard的SDK。这些SDK的使用方法大同小异,每个SDK中均包含着一个角色控制器,而对角色控制器的使用也近乎相同。我们将相应的SDK导入Unity,随后将SDK中的角色控制器拖放到游戏场景中,进行测试。在使用Oculus Rift等设备进行调试的时候,往往需要将Oculus Rift配置好,连接电脑才能调试。而Cardboard的调试,可以使用Alt键+拖动鼠标来模拟用户头部的晃动。
  在正式开发游戏之前,采用一个良好的目录结构是项目管理的有效方式。这个项目中,在根目录下有如下的目录:Model(存放OBJ或是FBX格式的模型)、Texture(存放模型的贴图,PNG等格式的图片)、Material(存放通过贴图制作的Unity中的PBR贴图)、Prefab(存放Unity预设物体)、Scene(存放游戏的场景文件)。在这种目录结构下,我们还可以对Model、Texture、Prefab和Material等目录进行细分,在一些大的项目中拥有大量的模型的时候,通过建立Environment、Building和Player等目录以细化Model目录结构,以方便对于模型的寻找,加快开发速度。在对目录中的文件命名的时候,模型文件以M_模型名称进行命名,Material以Mat_贴图名称进行命名也不失为一种良好的习惯。
  开发游戏的第一步便是搭建场景。在该VR游戏中,我们选用的风格是对程序员友好的像素3D风格。像素3D建模较为简单,所有的模型都是我们团队中的程序员制作的。在像素建模的软件选用方面,我们使用了Magica Voxel这款免费的软件。Magica Voxel的界面和操作十分简单,即使没有基础,跟着网上的教程半个小时左右就能入门,并做出自己心仪的3D模型。在建模阶段完成之后,将模型和材质等资源按照约定的目录结构导入Unity3D中。
  在搭建场景时,先将FBX或是OBJ格式的模型拖动到Scene(场景)面板中,如果在建模的时候使用的单位和Unity默认单位(米)不同的话,便需要进行相应的缩放。通常如果是场景类的模型如Terrain(地形),我们选择将模型的Position属性设为(0,0,0),这样将会对后面的脚本编写和场景的搭建工作剩下时间。在简单地对模型进行拖动之后,得到了如下效果的游戏场景。
  
  此时搭建好的场景是静态的,无法进行互动,我们需要进行相应脚本的编写,以实现游戏的可操控性。如下图所示,我们需要编写脚本的地方有3个,一个是蓝色的击球平台,一个是粉色的击球平台,一个是蓝色的球。
  
  在这里,我们选择使用简单的AI来控制粉色的击球平台,玩家控制蓝色的击球平台。从这些物体我们可以引申出以下三个问题:
  如何移动玩家控制一方的击球平台;
  如何控制电脑一方的击球平台;
  如何对弹球进行反弹。
  首先是如何移动玩家的击球平台。要解决这个问题,就需要确定一个输入方式。在VR游戏中有传统游戏中的使用手柄、键鼠输入,同时还有HTC Vive和Oculus Rift的动作捕捉。但我们决定使用的输入方式是Gaze Input(凝视输入)。凝视输入的原理是通过在角色控制器的摄像头射出一条射线,该射线代表玩家的视线,射线碰撞到的位置便是玩家目前凝视的位置。
  在实现方面,需要在场景中新建一个Quad物体,与场景中的地面垂直,其宽度为玩家击球平台可以移动的范围,其高度可以设置为1000甚至更大。最后将该Quad的Mesh Renderer关闭,并调整其位置到玩家的击球平台的位置。然后,我们需要对VR中的SDK中的角色控制器进行相应的修改,由于该弹球游戏中采用的是固定视角,因此我们需要删除SDK中的角色控制器的RigidBody等组件,仅需要能跟随玩家头部运动的Camera即可。找到角色控制器中的Camera,在其上添加如下脚本RayCaster.cs:
  float hitPos;
  void FixedUpdate()
  {
  Ray ray = new Ray(transform.position, transform.forward);
  RaycastHit hit;
  if (Physics.Raycast(transform.position, transform.forward, out hit))
  {
  if (hit.collider.gameObject.tag == “Quad”)
  {
  hitPos = hit.point.x;
  }
  }
  }
  public float getHisPos()
  {
  return hitPos;
  }
  在上述脚本中,定义了一个射线和射线投影碰撞。当射线投影碰撞的对象是Quad的平面的时候获取碰撞点的x坐标(因只需移动弹球平台的X坐标,所以只获取碰撞点的X坐标),并保存到hitPos变量中,以便其他脚本调用。
  接着在Hierarchy(阶层面板中)找到玩家控制的弹球平台,在其上编写脚本playerMovement.cs。
  GameObject mainCamera;
  RayCaster rc;
  float thisX;
  void Start()
  {
  mainCamera = GameObject.FindGameObjectWithTag(“MainCamera”);
  rc = mainCamera.GetComponent《RayCaster》();
  thisX = this.transform.position.x;
  }
  void FixedUpdate()
  {
  this.transform.position=new Vector3(rc.getHisPos()+thisX,this.transform.position.y,this.transform.position.z);
  }
  在上述脚本中,实现了通过获取MainCamera的RayCaster组件的碰撞点坐标来相应的移动弹球平台。
  至此,通过视线来控制玩家击球平台的功能已经完成。对于电脑控制的击球平台,可以通过设定其平台的移动速度来决定难度,通过预测弹球到达电脑方的位置来决定电脑移动平台的目标点。在如何反弹弹球的问题上,我们使用Unity3D自带的物理引擎,在弹球物体上增加RigidBody组件,将RigidBody的Use Gravity选项取消选择,同时将Drag和Angular Drag设为0和Constraint中的Freeze Position的Y选项勾选上。这样,在游戏开始的时候,通过给弹球一个力来发球,当弹球碰到场景和击球平台的Collider的时候便会进行相应的反弹。至此,上面提出的三个问题解决完毕。
  但以上三个问题都是显性的问题,解决完这三个问题后便可以对游戏进行简单的操作,并不能完成真正意义上的游戏。因为在游戏设计中,还有一些对象是隐性的,没有实际的物体形态。这些对象有 Game Manager(游戏管理器)、 State Machine(状态机)和 User Interface(用户界面即UI)等。
  在游戏管理方面,Game Manager下面有如下子Manager:Audio Manager(负责游戏中音效的管理)、Game Status Manager(负责游戏的阶段状态管理)、Score Manager(负责游戏计分的管理)和UI Manager(负责游戏UI的管理)等。以Game Status Manager为例,在不同的游戏阶段中,可以进行的动作将会不同。如在游戏开始之前的准备阶段和游戏结束阶段,弹球平台和弹球不可以移动。在游戏开始之前的准备阶段,需要对弹球和击球平台进行复位操作。
  通过Game Manager进行游戏管理来编写游戏,可以使程序更具有面向对象的特性,对于程序的修改、调试更为直观和方便,从而减少BUG出现的几率。
  关于VR游戏中的UI,大部分Unity开发者在Unity4.6之后会倾向于选择官方的UGUI。在UGUI中,UI的TEXT、Button和Image等组件均是Canvas的子物体时才能正常工作。Canvas的默认渲染模式是以屏幕空间为基准的,然而在VR模式下,左右眼的视角均在一块屏幕中渲染,由此会造成UI在整块屏幕上只渲染一次而非分别在左右眼的视角分别渲染。因此,我们需要将Canvas的Render Mode(渲染模式)调整为World Space。这样,整块UI画布便成为了游戏3D世界中的物体,由此可缩放,拖动以放置到游戏中相应的位置。
  

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

评论(0)
发评论

下载排行榜

全部0条评论

快来发表一下你的评论吧 !

'+ '

'+ '

'+ ''+ '
'+ ''+ ''+ '
'+ ''+ '' ); $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code ==5){ $(pop_this).attr('href',"/login/index.html"); return false } if(data.code == 2){ //跳转到VIP升级页面 window.location.href="//m.obk20.com/vip/index?aid=" + webid return false } //是会员 if (data.code > 0) { $('body').append(htmlSetNormalDownload); var getWidth=$("#poplayer").width(); $("#poplayer").css("margin-left","-"+getWidth/2+"px"); $('#tips').html(data.msg) $('.download_confirm').click(function(){ $('#dialog').remove(); }) } else { var down_url = $('#vipdownload').attr('data-url'); isBindAnalysisForm(pop_this, down_url, 1) } }); }); //是否开通VIP $.get('/article/vipdownload/aid/'+webid,function(data){ if(data.code == 2 || data.code ==5){ //跳转到VIP升级页面 $('#vipdownload>span').text("开通VIP 免费下载") return false }else{ // 待续费 if(data.code == 3) { vipExpiredInfo.ifVipExpired = true vipExpiredInfo.vipExpiredDate = data.data.endoftime } $('#vipdownload .icon-vip-tips').remove() $('#vipdownload>span').text("VIP免积分下载") } }); }).on("click",".download_cancel",function(){ $('#dialog').remove(); }) var setWeixinShare={};//定义默认的微信分享信息,页面如果要自定义分享,直接更改此变量即可 if(window.navigator.userAgent.toLowerCase().match(/MicroMessenger/i) == 'micromessenger'){ var d={ title:'实例分析使用Unity3D开发VR游戏',//标题 desc:$('[name=description]').attr("content"), //描述 imgUrl:'https://'+location.host+'/static/images/ele-logo.png',// 分享图标,默认是logo link:'',//链接 type:'',// 分享类型,music、video或link,不填默认为link dataUrl:'',//如果type是music或video,则要提供数据链接,默认为空 success:'', // 用户确认分享后执行的回调函数 cancel:''// 用户取消分享后执行的回调函数 } setWeixinShare=$.extend(d,setWeixinShare); $.ajax({ url:"//www.obk20.com/app/wechat/index.php?s=Home/ShareConfig/index", data:"share_url="+encodeURIComponent(location.href)+"&format=jsonp&domain=m", type:'get', dataType:'jsonp', success:function(res){ if(res.status!="successed"){ return false; } $.getScript('https://res.wx.qq.com/open/js/jweixin-1.0.0.js',function(result,status){ if(status!="success"){ return false; } var getWxCfg=res.data; wx.config({ //debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。 appId:getWxCfg.appId, // 必填,公众号的唯一标识 timestamp:getWxCfg.timestamp, // 必填,生成签名的时间戳 nonceStr:getWxCfg.nonceStr, // 必填,生成签名的随机串 signature:getWxCfg.signature,// 必填,签名,见附录1 jsApiList:['onMenuShareTimeline','onMenuShareAppMessage','onMenuShareQQ','onMenuShareWeibo','onMenuShareQZone'] // 必填,需要使用的JS接口列表,所有JS接口列表见附录2 }); wx.ready(function(){ //获取“分享到朋友圈”按钮点击状态及自定义分享内容接口 wx.onMenuShareTimeline({ title: setWeixinShare.title, // 分享标题 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享给朋友”按钮点击状态及自定义分享内容接口 wx.onMenuShareAppMessage({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 type: setWeixinShare.type, // 分享类型,music、video或link,不填默认为link dataUrl: setWeixinShare.dataUrl, // 如果type是music或video,则要提供数据链接,默认为空 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ”按钮点击状态及自定义分享内容接口 wx.onMenuShareQQ({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到腾讯微博”按钮点击状态及自定义分享内容接口 wx.onMenuShareWeibo({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); //获取“分享到QQ空间”按钮点击状态及自定义分享内容接口 wx.onMenuShareQZone({ title: setWeixinShare.title, // 分享标题 desc: setWeixinShare.desc, // 分享描述 link: setWeixinShare.link, // 分享链接 imgUrl: setWeixinShare.imgUrl, // 分享图标 success: function () { setWeixinShare.success; // 用户确认分享后执行的回调函数 }, cancel: function () { setWeixinShare.cancel; // 用户取消分享后执行的回调函数 } }); }); }); } }); } function openX_ad(posterid, htmlid, width, height) { if ($(htmlid).length > 0) { var randomnumber = Math.random(); var now_url = encodeURIComponent(window.location.href); var ga = document.createElement('iframe'); ga.src = 'https://www1.elecfans.com/www/delivery/myafr.php?target=_blank&cb=' + randomnumber + '&zoneid=' + posterid+'&prefer='+now_url; ga.width = width; ga.height = height; ga.frameBorder = 0; ga.scrolling = 'no'; var s = $(htmlid).append(ga); } } openX_ad(828, '#berry-300', 300, 250);