前面一章,我们专注于通过代码来移动玩家和相机,同时了解了与 Unity 的物理系统相关的一些知识。然而,仅仅控制角色并不足以制作出具有竞争力的游戏:事实上,这只是各种不同游戏中都会存在的主题之一。
游戏的独特性来自游戏的核心机制以及这些机制赋予玩家的力量感与代入感。虚拟环境若不具有任何乐趣和可玩性,游戏便不值得重复玩耍,更不用说带来趣味了。当尝试实现游戏机制时,我们还会进一步学习 C#的编程知识以及一些中级特性
本章将完成 Hero Bor 游戏原型的制作,其中包含如下主题:
- 通过施加力来添加跳跃。
- 理解层遮罩。
- 初始化对象和预制体
- 理解游戏管理器。
- 理解get和 set 属性。
- 计算分数。
- 编写UI。
添加跳跃
使用 Rigidbody 组件控制玩家移动带来的好处是,添加依赖于施加力的游戏机将变得很容易,例如跳跃。为了使玩家能够跳跃,本节将使用称为枚举的数据类型并且编写第一个工具函数。
提示:
工具函数是用来执行一些杂事的类方法,能使游戏代码不那么混乱。例如,检查玩家是否接触地面,从而进行跳跃(或提示)。
了解枚举
根据定义,枚举是属于同一变量的具名常量的集合。当需要使用一系列不同的值而这些值又属于相同的父类型时,枚举十分有用。
与进行描述相比,直接进行展示能让枚举理解起来更为容易。枚举的语法如下
enum PlayerAction { Attack, Defend,Flee ;
下面分步解释枚举是如何起作用的。
- 关键字enum声明了后面变量的类型
- 枚举包含的值位于花括号中,使用逗号分隔(最后一个值除外)
- 枚举必须以分号结尾,就像之前使用的所有其他类型一样。
例如,使用如下语法就可以声明一个枚举变量:
PlayerAction currentAction = PlayerAction.Defend;
- 解释如下:
- 类型是PlayerAction。
- 枚举变量包含名称并等价于 PlayerAction 的某个值。
- 每个枚举常量都可以通过点符号来访问。
底层类型
枚举关联着底层类型,这意味着花括号内的每个常量值都有关联值。默认的底层类型是 int,初始值为0,就像数组一样,各个枚举常量按顺序获得下一个更大的值
注意:
并非所有类型都相同。枚举可以使用的底层类型已被限制为 byte、sbyte.short、ushort、int、uint、long 和 ulong.这些类型被称为整型,用来指定变量可以存储的数值的大小。这些内容超出了本书的讨论范围,大部分情况下使用 mt 类型即可。
例如,假设 PlayerAction 枚举的值现在如下所示:
enum PlayerAction f Attack = 0,Defend = 1,Flee = 2 i
这里并无规则限制底层类型的值必须起始于 0;实际上,只需要指定第一个值,C#就会自动递增其余的值:
enumPlayerAction { Attack = 5,Defend,Flee} ;
在以上示例中,Defend自动等于6,Flee自动等于7。但是,如果需要使PlayerAction枚举包含不连续的值,那么需要显式地添加它们:
enum PlayerAction { Attack = 10,Defend = 5,Flee = 0};
你甚至可以改变 PlayerAction 的底层类型至任何支持的类型,只需要在枚举名的后面添加一个冒号即可:
enum PlayerAction : byte {Attack, Defend,Flee };
为了获取枚举的底层类型,需要执行显式的类型转换,我们已经介绍过这些内容因此下面的语法不足为奇
enum PlayerAction {Attack = 10,Defend = 5,Flee = 0}; PlayerAction currentAction = PlayerAction.Attack; int actionCost = (int)currentAction;
枚举是编程领域中功能极为强大的工具,请一定熟练掌握。
实践:按空格键使玩家跳跃
你现在已经对枚举有了基本了解,下面使用枚举 KeyCode 来获取键盘输入。按如下代码修改 PlayerBehavior脚本,保存并单击 Play 按钮:
public class PlayerBehavior : MonoBehaviour { public float moveSpeed = 10f; public float rotateSpeed = 75f; public float jumpVelocity = 5f; private float vInput; private float hInput; private Rigidbody rb; void Start() { _rb = GetComponent<Rigidbody>(); } void Update() { vInput - Input.GetAxis("Vertical") * moveSpeed; hInput = Input.GetAxis("Horizontal") * rotateSpeed; } if(Input.GetKeyDown (KeyCode .Space)) { _rb.AddForce(Vector3.up * jumpVelocity!ForceMode.Impulse); } //this.transform.Translate(Vector3.forward * vInputTime.deltaTime); //this.transform.Rotate(Vector3.up * hInputTime.deltaTime); } void FixedUpdate() { //No changes needed ... } }
下面对上述代码进行解释。
创建一个变量来保存施加的跳跃力的大小,可以在Inspector 面板中进行调整
指定的键位被按下后,Input.GetKeyDown 方法将返回一个布尔值
- GetKeyDown方法接收一个键位参数,可以是字符串或 KeyCode,其中KeyCode 是枚举类型。可使用 KeyCode.Space 方法对指定的键位进行检测。
- 使用if语句检查 GetKeyDow 方法的返回值。如果返回tue,则执行i语句的语句体。
由于已经保存了 Rigidbody 组件,因此可以将 Vector3 和 ForceMode 参数传RigidbodyAddForce 方法以使玩家跳跃。
- 向量(或施加的力)应该沿着up 方向并乘以jumpVelocity。
- ForceMode 参数也是枚举类型,它决定了力是如何施加的。Impulse 表示给对象传递考虑了物体质量的即时力,这对跳跃机制来说很完美。
刚刚发生了什么
如果运行游戏,现在就可以向四周移动并且按下空格键来使玩家跳跃。但是,现在的机制会让玩家无限次地进行跳跃,这不是我们想要的结果。8.1.2 节将使用层遮罩来限制跳跃次数为单次。
使用层遮罩
层遮罩可以理解为用来归类游戏对象的不可见分组,Unity 的物理系统将使用这些分组来决定从寻路到碰撞体相交的一切表现。关于层遮罩的更多使用方式超出了本书的讨论范围,我们将创建并使用一个层级来执行简单的检查一一检查玩家是否触地。
实践:设置对象层级
在检查玩家是否触地前,首先把关卡中的所有对象添加到自定义的层遮罩中。这样就可以利用玩家对象上已有的Capsule Collider 来执行碰撞计算。
()选中Hicrarchy面板中的任意对象并选择 LayerAdd Layer,
(2)向可用的第一个位置添加一个新的层级,命名为Ground,
(3)在 Hierarchy 面板中选中父对象 Enviroment,选择 Layer|Ground,当弹出提示框询问是否应用至所有子对象时,单击 Yes 按钮。
刚刚发生了什么
默认情况下,Unity 引擎使用了层级 07,在剩下的 24 个位置可以自定义层级。这里定义了一个新的名为Ground 的层级并将 Enviroment 对象的所有子对象添加到了这个层级中。之后就可以检查处于 Ground 层级的所有对象是否与某个指定的物体相交了。
实践:限制重复跳跃
由于不想使 Update 方法变得混乱不堪,因此我们将层遮罩的相关计算写到一个工具函数中,并根据结果返回 true 或false。
(1)添加如下代码至PlayerBehavior 脚本并运行游戏:
public class PlayerBehavior : MonoBehaviour { public float moveSpeed = 10f; public float rotateSpeed = 75f; public float jumpVelocity = 5f; public float distanceToGround = 0.1f; public LayerMask groundlayer; private float vInput; private float hInput; private Rigidbody _rb; private CapsuleCollider _col; void Start() { _rb = GetComponent<Rigidbody>(); _col = GetComponent<CapsuleCollider>(); } void Update() { _vInput = Input.GetAxis("Vertical")*moveSpeed; _hInput = Input.GetAxis("Horizontal") * rotateSpeed; if(IsGrounded() & Input .GetKeyDown (KeyCode.Space)) { _rb.AddForce(Vector3.up * jumpVelocity,ForceMode.Impulse); } } void FixedUpdate() { //... No changes needed .. } private bool IsGrounded() { Vector3 capsuleBottom = new } Vector3( _col.bounds.center.x,_col.bounds .min.y,_col.bounds.center.z); Bool grounded =Physics.CheckCapsule(_col.bounds.center,capsuleBottom,distanceToGround,groundlayer,QueryTriggerInteraction.Ignore); return grounded; } }
2)在Inspector 面板中设置 Ground Layer 为 Ground.
下面对步骤(2)中的代码进行解释。
创建一个 float 变量来保存任意处于 Ground 层级的对象与 Player 对象的CapsuleCollider 组件之间的距离。
创建一个LayerMask 变量来进行碰撞检测,可以在Inspector 面板中进行设置
创建一个私有变量来保存玩家的 CapsuleCollider 组件
使用GetComponent0方法查找并返回 Player 对象上挂载的 CapsuleCollider组件
修改if语句,在执行跳跃之前检查IsGrounded 方法是否返回 te 以及空格键是否被按下。
声明将会返回一个布尔值的IsGrounded方法。
创建一个 Vector3 局部变量来保存 Player 对象的 CapsuleCollider 组件的底部置,我们将使用该位置判定与 Ground 层级中的对象发生的碰撞。
- 所有 Collider 组件都包含 bounds 属性,可以通过 min、max 和 center 子属性来
访问最小点、最大点和中心位置。 - 碰撞体的底部是指三维空间中的点坐标(center.x,min.y,center.z)。
- 创建一个布尔局部变量来保存从Physics 类调用的 CheckCapsule 方法的结果该方法接收如下5 个参数:
- 胶囊的起始位置,可设置为碰撞体的中心位置,因为我们只关心胶囊的底部是否接触地面。
胶囊的结束位置,可传入已经计算好的 capsuleBottom。 - 胶囊的半径,可传入 distanceToGround。
- 想要用来检查碰撞的层遮罩,可传入 Inspector 面板中已经设置好的groundLayer。
- 触发器的查询行为决定了 CheckCapsule 方法是否忽略设置为触发器的碰体。因为不需要检查触发器,所以使用枚举QueryTriggerInteraction.Ignore
计算结束,返回 grounded 中存储的结果。
刚刚发生了什么
添加至 PlayerBehavior 脚本的方法有些涩难懂,但分解后,我们发现要做的事情只是使用一个来自 Physics类的方法。用简单的语言解释就是,我们向 CheckCapsule方法提供了起点和终点、碰撞半径以及层遮罩。如果终点位置与 Ground 层级中的某个物体之间的距离小于碰撞半径,CheckCapsule 方法就返回 ue,这意味着玩家触地了。若玩家正处于跳跃过程中,CheckCapsule 方法就返回 false。因为每一帧都将在Update方法中使用if语句检查IsGround,因此只有当玩家触地时,才允许进行跳跃
发射投射物
射击机制在游戏中十分常见,第一人称射击游戏中必然包含射击机制的某些变种Hero Bom游戏也不例外。本节将讨论如何在游戏运行时从预制体实例化游戏对象以及利用Unity的物理系统将这些对象向前射出。
实例化对象
在游戏中实例化游戏对象的概念与实例化类相同一一都需要某个初始值,这样C#才知道需要创建什么对象以及在何处创建。在场景中实例化游戏对象时,可以使用Instantiate 方法简化整个流程,只需要提供预制体对象、起始位置以及朝向即可。实际上,也可以使用 Unity 创建包含所需脚本和组件的对象,使之朝向指定的方向,然后在3D空间中按需进行调整。
实践:创建投射物预制体
在射击任何投射物之前,首先需要创建预制体。
1)在Hierarchy 面板中使用 Createl3D Obiect Sphere 创建一个球体,命名为Bullet。然后修改Transform组件的各个轴的缩放值均为0.15
(2)单击Add Component 按钮,查找并添加 Rigidbody 组件,保留默认设置即可。
(3)使用 Create|Material 在 Materials 文件夹中创建一个新的材质,命名为Orb Mat。
- 修改AIbedo 属性为深黄色。
- 将Orb Mat 材质拖曳至 Bullet 对象上。
(4)拖放 Bullet对象至 Prefabs 文件夹
刚刚发生了什么
我们创建并配置了 Bullet 预制体,这个预制体在游戏中可以实例化任意多次,并且可按需进行修改。
实践:添加射击机制
现在已经有可用的预制体了,在任何时候,当按下鼠标左键进行射击时,都可实例化并移动预制体的副本。
(1)按如下代码修改 PlayerBehavior 脚本:
public class PlayerBehavior : MonoBehaviour { public float moveSpeed = 10f; public float rotateSpeed = 75; public float jumpVelocity = 5f; public float distanceToGround=0.1f; public LayerMask groundLayer; public GameObject bullet; public float bulletSpeed = 100f; private float _vInput; private float _hInput; private Rigidbody _rb; private CapsuleCollider _col; void Start() { // ... No changes needed. } void Update() { // ... No changes needed ... } void FixedUpdate() { Vector3 rotation = Vector3.up * _hInput * Time.fixedDeltaTime; Quaternion deltaRotation = Quaternion.Euler(rotation); _rb.MovePosition(this.transform.position +this.transform.forward *_vInput * Time.fixedDeltaTime); _rb.MoveRotation( rb.rotation * deltaRotation); if (Input.GetMouseButtonDown(0)) { GameObject newBullet = Instantiate(bullet,this.transform.position,this.transform.rotation) as GameObject; Rigidbody bulletRB = newBullet.GetComponent<Rigidbody>(); bulletRB.velocity = this.transform.forward * bulletSpeed; } } private bool IsGrounded() { // .. No changes needed .. } }
)拖动 Bullet 预制体到 PlayerBehavior 脚本的 Inspector 面板中的 Bullet 属性上
(3)运行游戏并使用鼠标左键向玩家开火!
下面对步骤2)中的代码进行解释。
创建两个公共变量:一个用来保存 Bullet 预制体;另一个用来保存子弹的速度
使用f语句检查 Input.GetMouseButtonDown 方法是否返回 true,就像之前查InputGetKeyDown方法一样。GetMouseButtonDown方法接收一个int类型的参这个参数的值决定了想要检测的鼠标按键: 0 表示左键,1表示右键,2 表示中滚轮。
每当鼠标左键被按下时,就创建一个 GameObiect 局部变量。
使用Instantiate 方法为 newBullet 变量赋值,向该方法传入 Bullet预制体,并以胶囊的位置和旋转作为起始值。
添加as GameObiect 以显式地转换所返回对象的类型,从而与 newBullet 的类型一致。
调用GetComponent 方法以返回 newBullet 上的 Rigidbody 组件并保存。
设置 Rigidbody 组件的 velocity 属性为玩家的 tranform.forward 万向乘以bulletSpeed。通过直接修改 velocity 而不是使用 AddForce 方法,可以确保开火时重力不会使弹道下坠为弧形。