从零开始做一款Unity3D游戏<三>——编写游戏机制

news2024/11/16 7:25:46

添加跳跃

了解枚举

使用层遮罩

发射投射物

实例化对象

管理游戏对象的创建

游戏管理器

维护玩家属性

get和set属性

精益求精

图形用户界面

胜败条件

使用预编译指定和命令空间

总结


前面一章,我们专注于通过代码来移动玩家和相机,同时了解了与 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 ..
}
}

(2)拖动 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 方法,可以确保开火时重力不会使弹道下坠为弧形。

管理游戏对象的创建

无论是编写应用还是 3D 游戏,都需要确保定期删除未使用的对象以避免程序过载。子弹在射出后就不那么重要了,但此类对象仍存在于关卡中与其发生碰撞的对象和墙体的附近。这种射击机制会导致场景中存在成百上千颗子弹,这不是我们想要的效果。

实践:销毁子弹
为了达到目的,我们可以想办法让子弹执行自身的销毁行为。
(1)在Scripts文件夹中创建一个新的C#脚本,命名为 BulletBehavior。
(2)拖放 BulletBehavior 脚本至 Prefabs 文件夹中的Bullet预制体上。
(3)在BulletBehavior 脚本中添加如下代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class BulletBehavior : MonoBehaviour 
{
    public float onscreenDelay = 5f;

	void Update () 
    {
        Destroy(this.gameObject, onscreenDelay);
	}
}

 下面对步骤(3)中的代码进行解释

声明一个 float 变量来保存 Bullet 预制体实例化之后要在场景中保留的时间

使用 Destroy 方法删除GameObject。

  • Destroy 方法始终需要一个对象作为参数,在本例中,可使用 this 关键字指定脚本被附加到的对象
  • Destroy 方法还能使用可选的 float 参数来表示延迟时间,从而使子弹在屏幕上保留一小段时间。

刚刚发生了什么
Bullet 预制体会在指定的延迟时间过后从场景中销毁自身。这意味着子弹会执行自身定义的行为,无须其他脚本干预,这是对“组件”设计模式的理想应用。第 12章将讨论更多相关内容。

游戏管理器

学习编程的常见误区是把所有变量都设置为公共的。根据经验,首先应考虑将变量设为受保护的或私有的,仅当必要时再设为公共的。有经验的程序员会通过管理类来保护数据,为了养成好习惯,我们也会这样做。可以将管理类理解为安全访问重要变量和方法的通道。
pub
在编程中讨论安全性听起来有些奇怪。然而,当不同的类互相访问并更新数据时,事情会变得一团糟。只保留单个诸如管理类的联系点,可使影响变得最小。

维护玩家属性

Hero Bom 是一款十分简单的游戏,需要维护的数据只有两项:一是玩家收集了多少物品:二是玩家还剩多少生命值。可将这些变量设为私有的,使它们只能由管理类修改以保证受控且安全。

实践:创建游戏管理器

游戏管理类对于将来开发任何项目都是必需的,我们先来学习如何合适地创建游戏管理类。

(1)在 Scripts 文件夹中创建一个新的 C#脚本,命名为 GameBehavior。通常来说这个脚本应该命名为GameManager,但是 Unity 保留了这个名称供自己使用。
(2)在 Hicrarchy 面板中使用 Create  Create Empty 创建一个空对象并命名为GameManager。然后向 GameManager 空对象附加GameBehavior 脚本,

(3)添加如下代码至GameBehavior 脚本中:

public class GameBehavior : MonoBehaviour
{
 private intitemsCollected = 0;
private int_playerHP = 10;
}

 上述代码添加了两个私有变量来保存拾取的物品数量以及玩家剩余的生命值,设置为私有的是因为它们只能由 GameBehavior 类修改。如果设为公共的,其他类可能会修改它们,导致其中存储的数据不正确。

get和set属性

我们已经设置好了管理类脚本与私有变量,如何从其他类访问这些私有变量呢?我们可以通过向 GameBehavior 类添加不同的公共方法来向私有变量传递新值,但是还有没有更好的办法呢?

在这种情况下,C#为所有变量提供了 get 和 set 属性,从而完美地满足了现在的需求。可以将这些属性理解为由 C#编译器自动触发的方法,而无论是否显式地调用它们就像场景刚开始时 Unity 自动执行的 Start和Update方法一样。
get 和 set 属性能被添加至任何变量,包含或不包含初始值皆可:

public string firstName {get; set;};

public string lastName {get; set;}= "Smith";

然而,仅仅这样使用没有任何附加效果;为此,需要让每个属性包含一个代码块

public string FirstName
{
 get 
{
  // Code block executes when variable is accessed
}
set 
{
  // Code block executes when variable is updated
}
}

 现在,根据变量使用的位置,get 和 set 属性会执行附加逻辑。由于还没有完成全部工作,因此仍然需要处理这些新的逻辑。
每个get代码块都需要返回一个值,而每个set代码块则需要赋予一个值,这里正是结合使用私有变量(称为后备变量)和具有 get 及 set 属性的公共变量的好地方。私有变量将受到保护,其他类则可以受控访问公共变量。

private string firstName
public string FirstName 
{
  get 
  {
   return _firstName;
  } 
 set
  {
   _firstName=value;
  }
}
}

 下面对上述代码进行解释。

  • 任何时候,当其他类需要时,可以使用 get 属性返回存储在私有变量中的值下面对上述代码进行解释。而不需要实际将变量暴露给外部类。
  • 任何时候,当使用外部类给公共变量赋值时,可以更新私有变量,使二者同步

不进行实际应用,上述解释阅读起来会有点深奥。可利用已有的私有变量,GameBehavior脚本,添加具有 get 和 set 属性访问器的公共变量。

实践:添加后备变量

你已经理解了 get 和 set 属性访问器的语法,下面在管理类中实现它们,从而使代码更高效、更具可读性。
按如下所示修改GameBehavior脚本中的代码:

public class GameBehavior : MonoBehaviour
{
  private int _itemsCollected = 0;
  public int Items
  {
   get { return _itemsCollected;}
   set 
   {
    _itemsCollected = value ;
    Debug.LogFormat("Items:{0}",_itemsCollected);
   }
  }
   private int_playerHP = 3;
   public int HP
 {
  get { return _playerHP; }
  set{
   _playerHP = value;
   Debug.LogFormat("Lives:{0}",_playerHP);
   }
  }

}

 下面对上述代码进行解释。

声明名为 Items 的公共变量,其中包含 get 和 set 属性。

 外部类访问 Items 变量时,使用 get 属性返回存储于 itemsCollected 中的值。

使用sct 属性在 lcms 变量被更新时为 itemCollected 赋新值,同时添加Debug.LogFormat 方法以打印修改后的 itemsCollected 的值。

设置具有get和set 属性的公共变量HP,从而对后备变量 playerHP 进行补充

刚刚发生了什么
GameBehavior脚本的两个私有变量现在都可以访问了,但是仅允许访问公开的部分。这确保了私有变量只能在特定的位置进行访问和修改。

实践:更新物品集合
我们已经设置好了 GameBehavior 脚本中的变量,每次在场景中收集 Pickup_Item时都可以更新Items变量。

(1)在ItemBehavior脚本中添加如下代码:

public class ItemBehavior : MonoBehaviour

{

void Start()
{
  gameManager = GameObject.Find("GameManager").GetComponent<GameBehavior>();
}
void OnCollisionEnter(Collision collision)
{
  if (collision.gameObject.name == "Player")
  {
   Destroy(this.transform.parent.gameObject);
   Debug.Log("Item collected!");
   gameManager.Items += 1;
}
}
}

(2)运行游戏并收集物品,查看管理类脚本输出到控制台中的信息

下面对步骤(1)中的代码进行解释。

创建一个GameBehavior类型的变量来保存对脚本的引用。

在Start 方法中,使用 Find 方法查找对象并添加 GetComponent 方法以初始化gameManager。

当Pickup_Item 对象被销毁后,就在 gameManager 中增加Items 属性的值

刚刚发生了什么
由于已经在 ItemBehavior 类中处理好了碰撞逻辑,因此我们可以很容易地修改onolisionEnter 方法,从而在玩家拾取物品时与管理类进行沟通。将功能分离能使代召更具弹性,在开发期间进行修改时出错的可能性也会降低。

精益求精

目前,多个脚本共同配合,进而实现了玩家的移动、跳跃、收集、射击等机制。但是,现在仍然缺少用来展示玩家状态的显示内容或视觉提示,并且缺少游戏的胜败条件。本节将重点关注这两个主题。

图形用户界面

用户界面是任何计算机系统都有的可视组件,通常称为 UI。鼠标指针、文件夹以及桌面上的程序图标都是 UI元素。我们的游戏需要拥有简单的UI以使玩家知道已经集了多少物品以及当前的生命值,还需要一个能在发生特定事件时进行更新的文本框在Unity 中添加UI元素有两种方式:

  • 直接使用 Hierarchy 面板中的 Create 菜单进行创建,就像创建其他游戏对象一样。
  • 在代码中使用内置的 GUI类。

我们将一直使用代码方式,这么做并非因为代码方式优于另一种,而是为了与之前保持一致。

GUI类提供了一系列方法来创建和摆放组件,所有 GU方法都可在 MonoBehaviour脚本的OnGUI方法中进行调用。可以将 OnGUI方法理解为用于UI的 Update 方法.

实践:添加 UI 元素

目前还不需要向玩家显示很多信息,但是我们应该将需要显示的信息以令人愉悦引人注目的方式显示在屏幕上。
(1)按如下代码修改GameBehavior 脚本并收集物品:

public class GameBehavior : MonoBehaviour
{
  public string labelText = "Collect all 4 items and winyour freedom!";public int maxItems = 4;
  private int _itemsCollected = 0;
  public int Items
{
   get { return _itemsCollected; }
   set{_itemsCollected = value;
   if(_itemsCollected >= maxItems)
   {
     labelText = "You've found all theitems!";
   }
   else
   {
    labelText = "Item found, only " + (maxItems-_itemsCollected) + " more to go!";
   }
}
}
 private int _playerLives =3;
 public int Lives
{
 get { return _playerLives;}
 set {
     _playerLives = value;
Debug.LogFormat("Lives:(0),_playerLives);
}
}
void OnGUI()
{
  GUI.Box(new Rect(20,20,150,25),"Player
Health:"+_playerLives);
   GUIBox(new Rect(20,50,150,25),nItemsCollected: + _itemsCollected);
  GUI.Label(new Rect(Screen.width / 2 - 100,Screen.height - 50,300,50),labelText);
}
}

(2) 运行游戏,用户界面 

 下面对步骤(1)中的代码进行解释。

创建两个公共变量:一个表示要在屏幕底部显示的文本;另一个表示关卡中品的最大数量。

在itemsCollected变量的set 属性中声明一条if语句。

  • 如果玩家收集的物品的数量大于或等于 maxItems,那么玩家赢得游戏并目回顾第6新 labelText。
  • 否则,使用 labelText 显示还需要收集多少物体。

声明OnGUI方法以包含UI代码。

通过指定位置、大小与字符串信息来创建 GUIBox 方法

  • Rect 类的构造函数将接收宽度和高度值作为参数。
  • Rect对象的起始位置始终为屏幕的左上角。
  • 使用new Rect(20.20.150.25)可创建一个位于场景左上角的2D方框距离场景的左侧边界 20 像素,距离顶部边界也 20 像素,宽度为 150像素,高度为25像素。

在生命值方框的下面创建另一个方框以显示当前的物品数量

在屏幕的底部创建一个标签以显示 labelText

  • 因为OnGUI方法每帧至少会执行一次,所以在任何时候当abelText的值发生变化时,都会在屏幕上进行更新。
  • 这里使用Screen类的width和height属性获取绝对位置而不是手动计赛展幕的中心位置。

刚刚发生了什么

当我们运行游戏时,三个UI元素都显示了正确的值。每当收集一个Pickup llem时,lableText和 itemsCollected 都会得到更新

胜败条件

游戏的核心机制与简易UI都已实现,Hero Bom游戏还缺少如下重要的射击元素胜败条件。胜败条件用于管理玩家赢得游戏还是失败,并根据情况执行不同的代码

  • 收集关卡中的所有道具且生命值至少为1时胜利。
  • 受到敌人伤害且直到生命值变为0时失败。

以上条件会影响 UI以及游戏机制,但这些都已在 GameBehavior 脚本中高效处理过了。get和 set 属性会处理任何游戏相关逻辑,而GUI方法则会在玩家胜利或失败时改变UI。

实践:赢得游戏

为了给玩家带来清晰且即时的反馈,下面从添加胜利条件的逻辑开始。

(1)按如下代码修改GameBehavior 脚本

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using CustomExtensions;

public class GameBehavior : MonoBehaviour, IManager
{
    public string labelText = "Collect all 4 items and win your freedom!";
    public readonly int maxItems = 4;
    public bool showWinScreen = false;
    public bool showLossScreen = false;

    public delegate void DebugDelegate(string newText);
    public DebugDelegate debug = Print;

    private string _state;
    public string State 
    {
        get { return _state; }
        set { _state = value; }
    }

    private int _itemsCollected = 0;
    public int Items
    {
        get { return _itemsCollected; }
        set { 
            _itemsCollected = value;

            if (_itemsCollected >= maxItems)
            {
                labelText = "You've found all the items!";
                showWinScreen = true;
                Time.timeScale = 0;
            }
            else
            {
                labelText = "Item found, only " + (maxItems - _itemsCollected) + " more to go!";
            }
        }
    }

    private int _playerLives = 3;
    public int Lives 
    {
        get { return _playerLives; }
        set { 
            _playerLives = value; 

            if(_playerLives <= 0)
            {
                labelText = "You want another life with that?";
                showLossScreen = true;
                Time.timeScale = 0;
            }
            else
            {
                labelText = "Ouch... that's got hurt.";
            }
        }
    }

    void Start()
    {
        Initialize();

        InventoryList<string> inventoryList = new InventoryList<string>();
        inventoryList.SetItem("Potion");
        Debug.Log(inventoryList.item);
    }

    public void Initialize() 
    {
        _state = "Manager initialized..";
        _state.FancyDebug();

        debug(_state);
        LogWithDelegate(debug);

        PlayerBehavior playerBehavior = GameObject.Find("Player").GetComponent<PlayerBehavior>();
        playerBehavior.playerJump += HandlePlayerJump;
    }

    public void HandlePlayerJump(bool isGrounded)
    {
        if(isGrounded)
        {
            debug("Player has jumped...");
        }
    }

    public static void Print(string newText)
    {
        Debug.Log(newText);
    }

    public void LogWithDelegate(DebugDelegate debug)
    {
        debug("Delegating the debug task...");
    }

	void OnGUI()
	{
        GUI.Box(new Rect(20, 20, 150, 25), "Player Health: " + _playerLives);
        GUI.Box(new Rect(20, 50, 150, 25), "Items Collected: " + _itemsCollected);
        GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height - 50, 300, 50), labelText);

        if (showWinScreen)
        {
            if (GUI.Button(new Rect(Screen.width/2 - 100, Screen.height/2 - 50, 200, 100), "YOU WON!"))
            {
                Utilities.RestartLevel();
            }
        }

        if(showLossScreen)
        {
            if (GUI.Button(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 50, 200, 100), "You lose..."))
            {
                try
                {
                    Utilities.RestartLevel(-1);
                    debug("Level restarted successfully...");
                }
                catch (System.ArgumentException e)
                {
                    Utilities.RestartLevel(0);
                    debug("Reverting to scene 0: " + e.ToString());
                }
                finally
                {
                    Utilities.RestartLevel(0);
                    debug("Restart handled...");
                }
            }
        }
	}
}

 (2)在Inpsector 面板中将Max Items 修改为1,然后进行测试,

 下面对步骤(1)中的代码进行解释。
创建一个新的布尔变量来维护胜利界面出现的时机。
当玩家收集完所有物品时,在 Items 对象的set 属性中将showWinScreen 设置为true。

在OnGU方法的内部使用 if 语句检查胜利界面是否应该显示

在屏幕的中央创建一个可单击的按钮。

  • GULBumon 方法将返回一个布尔值,当这个按钮被单击时返回 true,否则返回
    false.
  • 在i语句中调用GULButton 方法,从而当这个按钮被单击时执行f语句的语句体

刚刚发生了什么
maxItems 被设置为1,胜利按钮会在收集完场景中唯一的 Pickup Item 后出现
但是现在单击这个按钮不起任何作用,

使用预编译指定和命令空间

胜利条件可以按预期方式运行了,但是胜利后,玩家仍然可以控制胶囊,而且游戏一旦结束,尚没有办法重新开始。Unity 的 Time 类提供了 timeScale 属性,当这属性被设置为 0 时就会暂停整个游戏。为了重新开始游戏,我们需要访问命名空间SceneManagement。默认情况下,这个命名空间还无法从我们的类中直接访问。
命名空间可以将一系列类包含在某个特定的名称下,进而组织大型项目并避免共用相同名称的脚本间产生冲突。可通过向类中添加 using 指令以访问另一个命名空间中的类。
所有通过 Unity创建的C#脚本都包含如下三条默认的 using 指令:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

 这样就可以访问常用的命名空间了。Unity 和 C#提供了非常多的功能,可以通过在关键字 using之后加上命名空间的名称来进行添加。

实践:暂停与重启游戏
我们的游戏需要在玩家胜利或失败时能够暂停和重启。为此,我们需要引入新建的C#脚本默认都不会包含的命名空间。
在GameBehavior脚本中添加如下代码并运行游戏。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using CustomExtensions;

public class GameBehavior : MonoBehaviour, IManager
{
    public string labelText = "Collect all 4 items and win your freedom!";
    public readonly int maxItems = 4;
    public bool showWinScreen = false;
    public bool showLossScreen = false;

    public delegate void DebugDelegate(string newText);
    public DebugDelegate debug = Print;

    private string _state;
    public string State 
    {
        get { return _state; }
        set { _state = value; }
    }

    private int _itemsCollected = 0;
    public int Items
    {
        get { return _itemsCollected; }
        set { 
            _itemsCollected = value;

            if (_itemsCollected >= maxItems)
            {
                labelText = "You've found all the items!";
                showWinScreen = true;
                Time.timeScale = 0;
            }
            else
            {
                labelText = "Item found, only " + (maxItems - _itemsCollected) + " more to go!";
            }
        }
    }

    private int _playerLives = 3;
    public int Lives 
    {
        get { return _playerLives; }
        set { 
            _playerLives = value; 

            if(_playerLives <= 0)
            {
                labelText = "You want another life with that?";
                showLossScreen = true;
                Time.timeScale = 0;
            }
            else
            {
                labelText = "Ouch... that's got hurt.";
            }
        }
    }

    void Start()
    {
        Initialize();

        InventoryList<string> inventoryList = new InventoryList<string>();
        inventoryList.SetItem("Potion");
        Debug.Log(inventoryList.item);
    }

    public void Initialize() 
    {
        _state = "Manager initialized..";
        _state.FancyDebug();

        debug(_state);
        LogWithDelegate(debug);

        PlayerBehavior playerBehavior = GameObject.Find("Player").GetComponent<PlayerBehavior>();
        playerBehavior.playerJump += HandlePlayerJump;
    }

    public void HandlePlayerJump(bool isGrounded)
    {
        if(isGrounded)
        {
            debug("Player has jumped...");
        }
    }

    public static void Print(string newText)
    {
        Debug.Log(newText);
    }

    public void LogWithDelegate(DebugDelegate debug)
    {
        debug("Delegating the debug task...");
    }

	void OnGUI()
	{
        GUI.Box(new Rect(20, 20, 150, 25), "Player Health: " + _playerLives);
        GUI.Box(new Rect(20, 50, 150, 25), "Items Collected: " + _itemsCollected);
        GUI.Label(new Rect(Screen.width / 2 - 100, Screen.height - 50, 300, 50), labelText);

        if (showWinScreen)
        {
            if (GUI.Button(new Rect(Screen.width/2 - 100, Screen.height/2 - 50, 200, 100), "YOU WON!"))
            {
                Utilities.RestartLevel();
            }
        }

        if(showLossScreen)
        {
            if (GUI.Button(new Rect(Screen.width / 2 - 100, Screen.height / 2 - 50, 200, 100), "You lose..."))
            {
                try
                {
                    Utilities.RestartLevel(-1);
                    debug("Level restarted successfully...");
                }
                catch (System.ArgumentException e)
                {
                    Utilities.RestartLevel(0);
                    debug("Reverting to scene 0: " + e.ToString());
                }
                finally
                {
                    Utilities.RestartLevel(0);
                    debug("Restart handled...");
                }
            }
        }
	}
}

 下面对上述代码进行解释。

使用 using 关键字添加 SceneManagement 命名空间,Unity 提供的这个命名空间会处理所有场景相关的逻辑。

当胜利界面出现时,把 Time.timeScale 设置为0以暂停游戏,从而禁止任何输入和移动。

当单击胜利界面中的按钮时,调用 LoadScene 方法。

  •  LoadScene 方法接收一个int 类型的参数来表示场景的索引。
  • 因为项目中只有一个场景,所以使用索引0来重新开始游戏。

重新打开场景后,把Time.timeScale 重置为默认值 1,这样所有控件和行为就可以再次执行了。

刚刚发生了什么
现在,当玩家收集物品并单击胜利界面中的按钮时,关卡会重启,所有脚本和组件都会被重置为原始值并为下一轮游戏做准备。

总结

恭喜!从玩家的视角看,Hero Bor 游戏现在已处于可玩状态。我们实现了跳跃和射击机制,对物理碰撞进行了管理并生成了对象,还添加了少量的基础性 I 元素来给予反馈。你甚至可以在玩家胜利时重置关卡!
本章介绍了大量新的主题,一定要回顾并确保自己真的理解所写代码中发生了什么。尤其要掌握枚举、get 和 set 属性以及命名空间方面的知识。从本章开始,随着进步探究 C#语言,代码只会变得越来越复杂。
在第9章,我们将使敌人在与玩家距离过近时能够注意到玩家,从而执行跟随射击行为,以此增大玩家收集物品时的风险。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/23027.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

深入理解计算机系统——Midterm Exam 2012

深入理解计算机系统——Midterm Exam 2012Problem 4 疑问Problem 6 疑问Problem 7 疑问Problem 8 疑问试卷&#xff1a;Midterm Exam 2012 答案&#xff1a;Exam Solutions Problem 4 疑问 第 4 题为第三章内容&#xff0c;这里觉得题目有一步骤写错了&#xff0c;<13> …

让Python可视化事半功倍!

分享一款不错的Matplotlib可视化查询手册&#xff0c;让Python Matplotlib事半功倍&#xff01; 当可视化时&#xff0c;记不清楚图形Marker名称、colormap名称、坐标轴刻度设置、图例设置等等时&#xff0c;稍微扫描一眼该手册&#xff0c;直接YYDS。 下面简单介绍一下这个手册…

SpringBoot SpringBoot 开发实用篇 5 整合第三方技术 5.5 变更缓存供应商 Ehcache

SpringBoot 【黑马程序员SpringBoot2全套视频教程&#xff0c;springboot零基础到项目实战&#xff08;spring boot2完整版&#xff09;】 SpringBoot 开发实用篇 文章目录SpringBootSpringBoot 开发实用篇5 整合第三方技术5.5 变更缓存供应商 Ehcache5.5.1 缓存实现方案5.5.…

部署支持使用Redis哨兵模式,支持纳管ClickHouse数据库,JumpServer堡垒机v2.28.0发布

2022年11月21日&#xff0c;JumpServer开源堡垒机正式发布v2.28.0版本。在这一版本中&#xff0c;JumpServer的部署支持使用Redis哨兵集群作为后端缓存数据库&#xff0c;从而使系统更加健壮和高可用。操作日志审计方面&#xff0c;新增支持查看资源变更信息。当资源有新增、更…

今天给大家介绍一篇基基于SSM超市管理系统的设计与实现

项目描述 临近学期结束&#xff0c;还是毕业设计&#xff0c;你还在做java程序网络编程&#xff0c;期末作业&#xff0c;老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下&#xff0c;你想解决的问…

【迁移学习】分布差异的度量以及迁移学习的统一表征方法

在文本分类中&#xff0c;由于文本数据有其领域特殊性&#xff0c;因此&#xff0c;在一个领域上训练的分类器&#xff0c;不能直接拿来作用到另一个领域上&#xff0c;这就需要用到迁移学习。 迁移学习是机器学习中重要的研究领域&#xff0c;ICML、NIPS、AAAI、ICIR等国际人工…

号码认证平台有哪些?号码认证平台费用?

随着互联网的发展&#xff0c;电话经常接到骚扰电话、买房、保险等电话&#xff0c;让用户不胜其扰。目前提供电话标记的软件个数达到几十款之多&#xff0c;可以有效预防诈骗发生&#xff0c;也暴露出“号码标记随意性强”“错误标记取消难”等一系列问题。如果被恶意标记&…

Rancher 全球化部署最佳实践

作者 万绍远&#xff0c;CNCF 基金会官方认证 Kubernetes CKA&CKS 工程师&#xff0c;云原生解决方案架构师。对 ceph、Openstack、Kubernetes、prometheus 技术和其他云原生相关技术有较深入的研究。参与设计并实施过多个金融、保险、制造业等多个行业 IaaS 和 PaaS 平台设…

美欧之后,台湾地区推出芯片法案:搞研发、买设备最高抵减50%税额

前言 在全球科技博弈加剧的背景下&#xff0c;芯片作为资本与技术双密集型产业&#xff0c;在全球产业链的重要地位已经不言而喻。 今年以来&#xff0c;欧洲、美国、日本、韩国、印度等国家/地区陆续出台了扶持半导体产业发展的法案&#xff0c;以加强本土半导体实力。而台湾…

肝不完这份HTTP八股文的你,再强大也是假的

关于网络模型 OSI 网络模型 应用层&#xff0c;给用户提供应用功能&#xff1b;表示层&#xff0c;负责把数据转换成兼容另一个系统能识别的格式&#xff1b;会话层&#xff0c;负责建立、维持、同步会话&#xff1b;传输层&#xff0c;负责端到端的数据传输&#xff1b;网络…

创新型中小企业认定

目前申报省级专精特新之前需要先认定创新型中小企业。创新型中小企业需要满足哪些条件才能进行认定呢&#xff1f;下面我就来详细的介绍一下&#xff1a; 一、评价方式&#xff1a; 由企业按属地原则自愿登录培育平台参与自评&#xff0c;省级中小企业主管部门根据评价标准&a…

目标检测算法——YOLOv5/YOLOv7改进之结合​ASPP(空洞空间卷积池化金字塔)

>>>深度学习Tricks&#xff0c;第一时间送达<<< 目录 一、前沿介绍 1.空洞卷积&#xff08;Atrous Convolution&#xff09; 2.空洞空间卷积池化金字塔&#xff08;Atrous Spatial Pyramid Pooling&#xff09; 二、YOLOv5/YOLOv7改进之结合​​ASPP 1.…

CANoe-Ethernet IG和Ethernet Packet Builder的使用和区别

Ethernet IG和Ethernet Packet Builder都是CANoe仿真以太网报文发送的模块,但是在用途上各有不同 1. Ethernet IG 以太网IG模块允许用户以多种方式发送以太网帧、UDP报文和TCP报文。可以发送一次,也可以按照指定的周期时间循环发送 创建以太网IG的步骤如下: 在Simulation …

2.5A强驱动能力,舞台灯光驱动TMI8263锻造“中国芯”

舞台艺术&#xff0c;自古以来就是人们享受生活不可或缺的艺术形式&#xff0c;一段赏心悦目的舞台表演能为观众带来从视听感官到心灵的艺术洗礼。 现在的舞台艺术&#xff0c;越来越追求极致的视觉效果&#xff0c;舞台灯光起着非常关键的作用。 一场舞台表演&#xff0c;需要…

Linux 之 Linux/Ubuntu 中开发操作中常用的命令整理

Linux 之 Linux/Ubuntu 中开发操作中常用的命令整理 目录 Linux 之 Linux/Ubuntu 中开发操作中常用的命令整理 一、简单介绍 二、常用命令 1、 打开终端 &#xff1a;Ctrl Alt T 2、退出终端&#xff1a;exit 3、查看安装 Ubuntu 版本/显示系统等信息&#xff1a;uname…

豆瓣评分9.0,《Java核心技术与面试》神作,已帮助1374人拿到Offer

Java的知识点实在太多&#xff0c;如果什么都去学&#xff0c;而不是精学工作和面试中常用的知识点&#xff0c;那不仅会造成学习效率低下&#xff0c;而且会让大家迷失在海量的知识中。结果是&#xff0c;大家投入了大量时间&#xff0c;也学了不少知识点&#xff0c;但是无法…

汇编逆向-入门

Qt源码解析 索引 汇编逆向— 授权破解示例分析 问题模拟 运行环境 x64dbgWindows 10serial.exe 运行效果 输入注册信息不对提示错误。 [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tV3bdLGO-1669017713175)(D:\Work\Code\luxiang\CSDN\ASM-…

element 表格字段过长

element 表格 - 表头太长换行 解决方案一&#xff1a;show-overflow-tooltip Table表格&#xff0c;如果每列显示的内容过长则可以使用该属性&#xff0c;即 :show-overflow-tooltip“true”&#xff0c;该属性可以让内容在一行显示&#xff0c;如果显示不下时&#xff0c;显示…

智能晾衣架(一)--设计

本文素材来源于红河学院 工学院 作者&#xff1a;赵德森 张艺锦 潘志慧 曹紫康 指导老师&#xff1a;江洁 张龙超 一、简介 本作品设计的晾衣系统从人们日常生活研究设计&#xff0c;体积小&#xff0c;收纳方便&#xff0c;不占空间可以在日常生活中极大的便利用户。我们…

MyBatis 源码分析之 Select 语句执行(上)

三哥 内容来自【自学星球】 欢迎大家来了解我的星球&#xff0c;和星主&#xff08;也就是我&#xff09;一起学习 Java &#xff0c;深入 Java 体系中的所有技术。我给自己定的时间是一年&#xff0c;无论结果如何&#xff0c;必定能给星球中的各位带来点东西。 想要了解更多&…