注:本文选自机械工业出版社出版的《独立游戏开发:基础、实践与创收》一书的小节,略有改动。经出版社授权刊登于此。文末还有赠书福利哦!!!
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高度二分之一的位置,对应马路中央,也是游戏角色的水平落脚点。
图1 Road游戏对象相关组件情况
图2 调整后的Road游戏对象
注:在图1中,有一个三角形警告符号,其内容提示我们:当前本Sprite图像资源的导入设置可能会造成Tiled模式下的绘制错误。但很明显,我们这里并未出现绘制错误,笔者在实际工作中也尚未遇到过此类错误,忽略该警告即可。或者,可在该Sprite的图像资源导入设置中,将Mesh Type属性设置为Full Rect以消除该警告。
对于Player,我们需要让其拥有重力以及合适的碰撞范围。首先将Rigidbody 2D组件的Gravity Scale属性设置为4,以接受该值大小的重力等级。接着重新选择碰撞器,由于主角有一个近似圆形的外观,因此可用圆形的Circle Collider 2D组件替换Box Collider 2D组件,并适当调整其范围大小,如图3所示。
图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展示了调整后的运行效果。
图4 错误的渲染顺序
图5 修正的渲染顺序
基于物理系统的移动
这里我们修改PlayerController脚本,将当前基于Transform组件的移动替换为基于物理系统功能的移动,如代码1所示。
代码1 PlayerController.cs
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// 用于引用Player的Rigidbody 2D组件
private Rigidbody2D body;
// 表示主角的移动速度
public float speed;
private void Start()
{
// 获取Player的Rigidbody 2D组件
body = GetComponent
(); }
private void FixedUpdate()
{
KeyboardControl();
}
private void KeyboardControl()
{
// 通过键盘左右键输入乘以速度变量得出水平速度
float sp = speed * Input.GetAxis("Horizontal");
// 根据水平速度和应有的垂直速度影响刚体速度
body.velocity = new Vector2(sp, body.velocity.y);
}
}
上述代码中的GetComponent是一个泛型方法,用于获取已附加组件,尖括号内为该组件的具体类型,这里我们获取Player的Rigidbody 2D组件,并由body变量引用。velocity是Rigidbody 2D组件中一个属性,代表当前刚体的移动速度,我们把一个匿名二维矢量赋值给它,即可实现刚体速度驱动游戏对象的移动。在这个匿名二维矢量中,X维度值即左右方向键输入值与speed变量的积,Y维度值则对应刚体当前在该维度应有的速度(也就是说,我们仅控制水平速度,而不直接控制垂直速度,垂直速度将由外力实现,例如,下落时由物理系统重力产生向下的速度;跳跃时由人物跳跃力产生向上的速度)。
注:对精准名词解释一下,velocity表示速度,speed表示速率。速度是有方向和大小的矢量,而速率是没有方向的值。在没有特别说明时,本书把两者都称为速度。
基于物理系统的跳跃与碰撞
我们继续编写PlayerController脚本代码,为主角添加跳跃能力,并使用更复杂的碰撞检测来判定其是否站立在地面上,如代码2所示。
代码2 PlayerController.cs
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// 用于引用Player的Rigidbody 2D组件
private Rigidbody2D body;
// 表示主角的移动速度
public float speed;
private void Start()
{
// 获取Player的Rigidbody 2D组件
body = GetComponent
(); }
private void FixedUpdate()
{
KeyboardControl();
}
private void KeyboardControl()
{
// 通过键盘左右键输入乘以速度变量得出水平速度
float sp = speed * Input.GetAxis("Horizontal");
// 根据水平速度和应有的垂直速度影响刚体速度
body.velocity = new Vector2(sp, body.velocity.y);
}
}
在上述代码中,我们新增了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),即可运行游戏测试最终效果。
图6 在Rigidbody 2D组件中锁定Z轴旋转
赠书福利
点亮“在看”按钮
1.GameRes游资网粉丝们包邮免费送5本
2.获得方式:在留言区留言,说出关于“独立游戏开发”的相关话题,我们将选出5名最走心的读者留言,各送出《独立游戏开发:基础、实践与创收》一本。
3.活动时间结束后我们将联系中奖用户!
4.活动截止时间:2020-6-12 16:00。
如果有更多粉丝想更多了解此书,请点击:
投稿邮箱:[email protected]
商务合作:Amber(微信:lcxk6876767)
其他合作:老林(微信:sea_bug)