游戏开发论坛

 找回密码
 立即注册
搜索
查看: 2232|回复: 0

独立游戏开发中的物理系统

[复制链接]

1万

主题

1万

帖子

3万

积分

论坛元老

Rank: 8Rank: 8

积分
36572
发表于 2020-6-10 09:27:06 | 显示全部楼层 |阅读模式
作者:王寅寅

注:本文选自机械工业出版社出版的《独立游戏开发:基础、实践与创收》一书的小节,略有改动。经出版社授权刊登于此。

1 (2)_副本.jpg

Unity物理系统更准确的叫法应该是物理引擎(Physics Engine),该引擎是采用NVIDIA的PhysX物理引擎实现的,为避免与游戏引擎本身的名字冲突,本书还是称其为物理系统。所谓物理系统,是指在游戏对象上实现加速度、碰撞、重力、摩擦力及各种外力作用的一系列功能集合。Unity物理系统又分为2D和3D两种类型,两者在使用上大体相似,主要区别是3D物理系统多了一个维度。

Unity物理系统没有总开关,只要在游戏对象上附加并正确设置了物理组件(如Rigidbody、Collider、Joint、Effector等组件),即使用了物理系统功能。下面我们继续开发案例游戏,并基于物理系统实现主角的移动、跳跃、自由落体及更复杂的碰撞检测等功能。

游戏对象调整

对于Road,我们需要将其调整为一个有一定距离且主角可以站立的路面。首先将Road的Sprite Renderer组件Draw Mode属性选择为Tiled(在该属性下,图像会根据游戏对象尺寸自动填充,就像连续的瓦片一样),然后在场景中拖曳Road的左右边框(需要确保工具栏中的变换工具为Rect Tool状态),适当增大其宽度后即可得到一个连续的路面。接下来调整碰撞器范围,在Box Collider 2D组件中单击Edit Collider按钮后,碰撞器范围即进入可编辑状态,调整完后再次单击Edit Collider按钮即可。我们还需要取消碰撞器的Is Trigger属性,以保证主角与路面的碰撞不可穿透,此时尽管Road并未附加Rigidbody 2D组件,但它相当于一个Static状态的刚体。另外,之前的Road脚本已经不适用了,我们将其对应脚本组件从检视窗口移除,并将该脚本文件从项目窗口删除即可。如图1和图2所示:图1展示了检视窗口中Road的相关组件情况,标注框中为相关的调整项;图2展示了Road在场景视图中的情况,注意其碰撞器范围是一个极细的矩形绿色框(图中可能不容易看出来,请读者结合实际操作查看),我们将该范围上边框调整在Road高度二分之一的位置,对应马路中央,也是游戏角色的水平落脚点。

2_副本.jpg
图1    Road游戏对象相关组件情况

3_副本.jpg
图2    调整后的Road游戏对象

注:在图1中,有一个三角形警告符号,其内容提示我们:当前本Sprite图像资源的导入设置可能会造成Tiled模式下的绘制错误。但很明显,我们这里并未出现绘制错误,笔者在实际工作中也尚未遇到过此类错误,忽略该警告即可。或者,可在该Sprite的图像资源导入设置中,将Mesh Type属性设置为Full Rect以消除该警告。

对于Player,我们需要让其拥有重力以及合适的碰撞范围。首先将Rigidbody 2D组件的Gravity Scale属性设置为4,以接受该值大小的重力等级。接着重新选择碰撞器,由于主角有一个近似圆形的外观,因此可用圆形的Circle Collider 2D组件替换Box Collider 2D组件,并适当调整其范围大小,如图3所示。

4_副本.jpg
图3  调整后的Player游戏对象

对于RoadBlock,可用类似方法调整其碰撞范围并删除RoadBlock脚本即可,具体步骤这里不再赘述。

渲染顺序修正

我们先运行游戏,可以看到主角会因重力向路面下落,最终被错误地显示在马路后面,如图4所示。要修正此问题,我们需要了解下Sprite Renderer组件的Sorting Layer与Order in Layer属性:Sorting Layer属性中可添加一系列特定名称的排序分组,Unity将按照组顺序依次渲染其中的Sprite;当多个Sprite同属一个Sorting Layer分组时,则可通过Order in Layer属性的值大小来决定它们的渲染顺序。

值得注意的是,Soring Layer与Layer虽然只差一个单词,但在Unity中它们是两个不同的概念,可阅读书中第5章中有关Layer的简要介绍。另外读者应知晓,Sprite Renderer组件功能不属于物理系统功能。

下面,我们开始调整Sorting Layer与Order in Layer。首先,在Sorting Layer中添加一个Player分组:任意选择一个游戏对象,在检视窗口中单击Sorting Layer属性右侧的Default按钮,并在展开的下拉列表中选择Add Sorting Layer选项,即可打开标签与层的有关设置,其中,Sorting Layers栏下默认仅有一个Default分组,右下角的加减号(“+ -”)可增减分组,拖曳左侧的等号(“=”)则可调整组顺序,这里我们添加一个Player分组,并保持现有顺序即可。然后为Player的Sprite选择该分组:单击Player游戏对象,直接将其Sorting Layer属性右侧的选项选择为刚才添加的Player分组即可。接下来,Road与RoadBlock之间同样需要调整渲染顺序:将RoadBlock拖曳到Road上,可看到错误的前后关系,此时保持两者的Sorting Layer同属默认Default分组,我们保持Road的Order in Layer属性为0,再将RoadBlock的Order in Layer属性设为1,即可修正渲染顺序(RoadBlock为0,Road为-1也可以)。图5展示了调整后的运行效果。

5_副本.jpg
图4   错误的渲染顺序

6_副本.jpg
图5    修正的渲染顺序

基于物理系统的移动

这里我们修改PlayerController脚本,将当前基于Transform组件的移动替换为基于物理系统功能的移动,如代码1所示。

代码1   PlayerController.cs

  1. using UnityEngine;

  2. public class PlayerController : MonoBehaviour
  3. {
  4.     // 用于引用Player的Rigidbody 2D组件
  5.     private Rigidbody2D body;
  6.     // 表示主角的移动速度
  7.     public float speed;

  8.     private void Start()
  9.     {
  10.         // 获取Player的Rigidbody 2D组件
  11.         body = GetComponent<Rigidbody2D>();
  12.     }

  13.     private void FixedUpdate()
  14.     {
  15.         KeyboardControl();
  16.     }

  17.     private void KeyboardControl()
  18.     {
  19.         // 通过键盘左右键输入乘以速度变量得出水平速度
  20.         float sp = speed * Input.GetAxis("Horizontal");
  21.         // 根据水平速度和应有的垂直速度影响刚体速度
  22.         body.velocity = new Vector2(sp, body.velocity.y);
  23.     }
  24. }
复制代码

上述代码中的GetComponent是一个泛型方法,用于获取已附加组件,尖括号内为该组件的具体类型,这里我们获取Player的Rigidbody 2D组件,并由body变量引用。velocity是Rigidbody 2D组件中一个属性,代表当前刚体的移动速度,我们把一个匿名二维矢量赋值给它,即可实现刚体速度驱动游戏对象的移动。在这个匿名二维矢量中,X维度值即左右方向键输入值与speed变量的积,Y维度值则对应刚体当前在该维度应有的速度(也就是说,我们仅控制水平速度,而不直接控制垂直速度,垂直速度将由外力实现,例如,下落时由物理系统重力产生向下的速度;跳跃时由人物跳跃力产生向上的速度)。

注:对精准名词解释一下,velocity表示速度,speed表示速率。速度是有方向和大小的矢量,而速率是没有方向的值。在没有特别说明时,本书把两者都称为速度。

基于物理系统的跳跃与碰撞

我们继续编写PlayerController脚本代码,为主角添加跳跃能力,并使用更复杂的碰撞检测来判定其是否站立在地面上,如代码2所示。

代码2    PlayerController.cs

  1. using UnityEngine;

  2. public class PlayerController : MonoBehaviour
  3. {
  4.     // 用于引用Player的Rigidbody 2D组件
  5.     private Rigidbody2D body;
  6.     // 表示主角的移动速度
  7.     public float speed;

  8.     private void Start()
  9.     {
  10.         // 获取Player的Rigidbody 2D组件
  11.         body = GetComponent<Rigidbody2D>();
  12.     }

  13.     private void FixedUpdate()
  14.     {
  15.         KeyboardControl();
  16.     }

  17.     private void KeyboardControl()
  18.     {
  19.         // 通过键盘左右键输入乘以速度变量得出水平速度
  20.         float sp = speed * Input.GetAxis("Horizontal");
  21.         // 根据水平速度和应有的垂直速度影响刚体速度
  22.         body.velocity = new Vector2(sp, body.velocity.y);
  23.     }
  24. }
复制代码


在上述代码中,我们新增了onGround与jumpPower变量,并使用了OnCollisionStay2D与OnCollisionExit2D方法。onGround变量是一个布尔值,用于说明主角是否站立在地面上(或其他可站立物体上),当该值为真时,我们使用GetAxis("Vertical")获取上下方向键输入值,并在输入值大于0时(向上的方向)调用AddForce方法在刚体上增加一个瞬时的力,该力是一个匿名二维矢量,作为参数传递给AddForce方法,我们这里只需要一个向上的力以产生向上的速度,因此该二维矢量的X维度值设为0,Y维度值则设为代表跳跃力大小的jumpPower变量。

OnCollisionStay2D方法会在Player始终与碰撞对象接触时连续执行,我们使用它检测主角是否站立在地面上,其中,contactCount属性保存了Player与某个碰撞对象之间碰撞点的数量(多数情况下为1),我们用cnum变量保存该数量,并结合for循环与GetContact方法遍历所有的碰撞点;contact变量则用于依次保存遍历结果,每一个碰撞点都有一个normal属性,该属性是一个法线向量(这里该向量是一个长度为1,并与Player碰撞点切线垂直的二维向量),当该向量Y维度值为1时,Player的碰撞点必然在正下方,即站立在地面上。这里我们将该值的判定标准设为0.8,以考虑碰撞点稍稍偏离正下方的情况。OnCollisionExit2D方法会在Player脱离任意碰撞时执行,我们直接在其中将onGround变量赋值为假,即说明Player处于悬空状态。

若此时运行游戏,主角将会以滚动方式移动,这不是我们想要的效果。可在Rigidbody 2D组件中展开Constraints选项并勾选Freeze Rotation Z属性以解决此问题,如图6所示。最后在检视窗口的Player Controller脚本组件中,为Speed与Jump Power属性分别设定一个合适的值(这里我们设定为3和150),即可运行游戏测试最终效果。

7_副本.jpg
图6  在Rigidbody 2D组件中锁定Z轴旋转

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2024-3-29 19:17

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表