如何用 Unity 编写像炸弹人一样的游戏

原文:How To Make A Game Like Bomberman With Unity
作者:Brian Broom
译者:kmyhy

更新说明:本教程由 Brian Broom 更新至 Unity 2017.1。原教程作者是 Eric Van de Kerckhove。

炸弹游戏很好玩。和朋友一起玩炸弹游戏就更好玩了。将你的朋友炸死了吗?赢家出现了!

但是,在保证 C4 炸弹不炸到自己的同时让小伙伴们心甘情愿地去死貌似有点困难。幸好,事情出现了一点转机。

欢迎来到这篇炸弹人教程。炸弹人是一个 4 人对战游戏,在这个游戏中,需要在战场上通过放置炸弹炸到对方是需要一定技巧的。

每枚炸弹都需要几秒钟的延迟才会爆炸,同时会向 4 个方向喷出火焰。更刺激的是,爆炸会引发连锁反应。

最早的炸弹人诞生在 80 年代早期,并出现了大量衍生作品。这是一个永恒的游戏分类,去玩和编写它仍然很有意思。

原来的版本是 2D 的,但你将用 Unity 编写一个简单的 3D 版本。

在这本教程中,你将学到:

  • 如何放置炸弹并将它们固定到方格中
  • 通过射线检查有效的方格以引爆
  • 处理爆炸是否炸到玩家
  • 处理爆炸是否炸到了炸弹
  • 处理玩家死亡事件以判断输赢

伸出你的手指,大喊一声“fire in the hole”( cs 术语,即“注意隐蔽,我要扔手雷了”)。爆炸即将在 Unity 中发生。:]

注意:这篇炸弹人教程假设你知道如何使用 Unity 编辑器以及用文本编辑器写代码。如果你不是太熟悉的话,建议你先阅读我们的其它 Unity 教程。

开始

从这里下载开始项目并解压缩。

用 Unity 打开开始项目。资源被放在这了几个文件夹中:

如何用 Unity 编写像炸弹人一样的游戏_第1张图片

  • Animation Controllers: 包含了玩家的动画控制器,包含了玩家移动时的肢体逻辑。如果你想复习一下关于动画的课程,请参考我们的 Unity 动画入门课程。
  • Materials: 包含了关卡中用到的材料
  • Models: 包含了玩家、关卡和炸弹的模型,以及它们的材质
  • Music: 包含了音乐
  • Physics Materials: 包含了玩家的物理材质 —— 一种特殊材质,用于在物体表面添加物理属性。在本教程中,将允许玩家毫不费力地移动,不会发生任何摩擦。
  • Prefabs: 包含炸弹和爆炸的预制件。
  • Scenes: 包含游戏场景
  • Scripts: 包含了开始项目的脚本,请打开它们看一看里面的代码,它们的注释非常完善,能很容易被理解。
  • Sound Effects: 包含了炸弹和爆炸产生的音效。
  • Textures: 包含了两个玩家的贴图。

扔炸弹

打开游戏场景,运行游戏。

如何用 Unity 编写像炸弹人一样的游戏_第2张图片

两个玩家可以通过 WASD 键和箭头键在地图上溜达。

通常玩家 1(红色)可以用空格键在脚下安装炸弹,而玩家 2(蓝色)则使用回车键安装炸弹。

当然,现在还不行。你必须实现安装炸弹的逻辑,因此请打开 Player.cs 脚本。

这个脚本负责处理玩家的所有动作和动画逻辑。它有一个方法名叫 DropBomb,它简单地判断了一下 bombPrefab 游戏对象是否被绑定:

private void DropBomb() 
{
  if (bombPrefab) 
  { //Check if bomb prefab is assigned first

  }
}

要将炸弹扔到玩家脚下,在 if 语句中添加一句:

Instantiate(bombPrefab, myTransform.position, bombPrefab.transform.rotation);  

这将在玩家脚下生成一颗炸弹。保存修改,运行游戏场景:

如何用 Unity 编写像炸弹人一样的游戏_第3张图片

真的可以耶✌️!

有个小问题,炸弹是可以生成了,你可以到处乱扔,这样在你计算爆炸应当在哪里发生时会出现一些问题。

当进入到如何产生爆炸时,你将会学习为什么要特别指出这一点。

限制炸弹位置

第二个任务是固定炸弹放置的位置,以便它们刚好放在一个格子中。因为每个方格的瓦片图刚好是 1x1 的,所以修改起来非常容易。

在 Player.cs,修改 DropBomb() 方法中的 Instantiate() 一句:

    Instantiate(bombPrefab, new Vector3(Mathf.RoundToInt(myTransform.position.x), 
    bombPrefab.transform.position.y, Mathf.RoundToInt(myTransform.position.z)),
  bombPrefab.transform.rotation); 

Mathf.RoundToInt 调用了玩家位置的 x 和 z,将小数变成证书,然后将炸弹限制到瓦片的位置:

如何用 Unity 编写像炸弹人一样的游戏_第4张图片

保存修改,运行游戏,扔几颗炸弹。炸弹的位置现在被固定到了方格中:

如何用 Unity 编写像炸弹人一样的游戏_第5张图片

尽管在地图上扔炸弹很好玩,但它要会炸才行啊!接下来给炸弹添加一点威力。:]

制造爆炸

首先,创建一个新脚本:

  • 在项目视图中选中 Scripts 文件夹。
  • 按 Create 按钮。
  • 选择 C# 脚本。
  • 将脚本命名为 Bomb。

如何用 Unity 编写像炸弹人一样的游戏_第6张图片

将 Bomb 脚本绑定到 Bomb 预制件:

  • 在 Prefabs 文件夹中,选择 Bomb 游戏对象。
  • 在 Inspector 窗口中点击 Add Component 按钮。
  • 在搜索框中输入 bomb。
  • 选择 Bomb 脚本。

最后,打开 Bomb script。在 Start() 方法中,添加:

Invoke("Explode", 3f);

Invoke() 方法有 2 个参数,第一个是你想调用的方法名,第二个是延迟调用的时间。在这里,你将让炸弹在 3 秒后引爆,因此调用了方法 Explode() —— 这个方法后面添加。

在 Update() 方法下面添加:

void Explode() 
{

}  

在创建任何 Explosion 游戏对象前,必须声明一个 GameObject 的公有变量,这样你可以在编辑器中将一个 Explosionprefab 赋给它。在 Start() 上面添加:

public GameObject explosionPrefab;

保存文件,回到编辑器。从 Prefabs 文件夹中选择 Bomb 预制件,然后拖一个 Explosion 预制件到 Explosion Prefab 的空格中:

然后,回到代码编辑器。你最后还是得编写爆炸的代码!

在 Explode() 中,添加:

Instantiate(explosionPrefab, transform.position, Quaternion.identity); //1

GetComponent().enabled = false; //2
transform.Find("Collider").gameObject.SetActive(false); //3
Destroy(gameObject, .3f); //4

这块代码主要做了:

  1. 在炸弹的位置上产生一个爆炸。
  2. 关闭网格渲染,让 bomb 隐藏。
  3. 关闭碰撞体,允许玩家通过并在爆炸中移动。
  4. 在 0.3 秒后销毁炸弹,这确保在这个游戏对象被销毁之前,所有的爆炸都能够创建出来。

保存 Bomb 脚本,回到编辑器,运行游戏。放几颗炸弹,然后享受爆炸带来的快感吧!

如何用 Unity 编写像炸弹人一样的游戏_第7张图片

添加图层掩码

值得庆幸的是,游戏中有墙,能够抵挡住炸弹。炸弹不能防住爆炸,玩家肯定也防不住爆炸。你必须有一种方法声明某个对象是不是墙。有一个方法就是使用 LayerMask 图层掩码。

LayerMask 会有选择地过滤掉某些图层,它通常用于射线检测。在这个例子中,你需要过滤的只有砖墙,这样射线将不能射中任何东西。

在 Unity 编辑器中点击右上角 Layer 按钮,然后选择 Edit Layer …

如何用 Unity 编写像炸弹人一样的游戏_第8张图片

如果看不见图层列表,你可以点击 Layers 前边的三角形,展开图层列表。

点击 User Layer 8 旁边的文本框,输入 Blocks,这会定义一个新的图层,后面会用到。

在结构视图中,选择 Map 对象下边的 Blocks 游戏对象。

如何用 Unity 编写像炸弹人一样的游戏_第9张图片

将 layer 修改成你刚刚创建的 Blocks 图层:

如何用 Unity 编写像炸弹人一样的游戏_第10张图片

当修改图层对话框弹出,点击 “Yes, change children” 按钮,将修改应用到地图中所有的黄色砖块。

如何用 Unity 编写像炸弹人一样的游戏_第11张图片

最后,为 LayerMask 添加一个公有的引用,以便 Bomb 脚本能够访问这个图层,在 explosionPrefab 之后添加:

public LayerMask levelMask;

别忘了保存代码。

大一点!让爆炸来的更大一点!

下一步是添加爆炸的波及范围。这需要创建一个协程。

备注:协程本质上是允许你暂停执行并返回控制权给 Unity 的函数。在之后的某个时刻,这个函数的执行从它最后一次暂停的地方恢复。

程序员经常将协程和多线程混淆起来。它们是不一样的:协程在同一个线程中执行,它们从中间恢复。

要了解更多关于协程的知识,以及如何定义协程,请阅读 Unity 文档。

回到代码编辑器,编辑 Bomb 脚本。在 Explode() 下面,添加一个 IEnumerator 叫做 CreateExplosions:

private IEnumerator CreateExplosions(Vector3 direction) 
{
  return null; // 暂时占位的代码
}

创建协程

在 Explode() 的 Instantiate 一句和禁用 MeshRenderer 之间添加代码:

StartCoroutine(CreateExplosions(Vector3.forward));
StartCoroutine(CreateExplosions(Vector3.right));
StartCoroutine(CreateExplosions(Vector3.back));
StartCoroutine(CreateExplosions(Vector3.left));  

StartCoroutine 会从四个方向分别调用 CreateExplosions 这个 IEnumerator 一次。

现在有趣的部分来了。在 CreateExplosions() 中,将 return null 一句替换为:

//1
for (int i = 1; i < 3; i++) 
  { 
  //2
  RaycastHit hit; 
  //3
  Physics.Raycast(transform.position + new Vector3(0,.5f,0), direction, out hit, 
    i, levelMask); 

  //4
  if (!hit.collider) 
  { 
    Instantiate(explosionPrefab, transform.position + (i * direction),
    //5 
      explosionPrefab.transform.rotation); 
    //6
  } 
  else 
  { //7
    break; 
  }

  //8
  yield return new WaitForSeconds(.05f); 
}

看起来很复杂,但实际上非常简单。它分成了以下几个步骤:

  1. 一个 for 循环,对你想让爆炸波及的范围进行遍历。在这里,这个爆炸将波及 2 米范围。
  2. 一个 RaycastHit 对象,用于保存所有关于哪个位置被射线射中——或不射中的信息。
  3. 关键的一句,发射一条射线,从炸弹的中心向 StartCoroutine 调用时指定方向发射。结果将放到 RaycastHit 对象中。i 参数指定了射线的射程。最后用一个 LayerMask 参数(lavelMask 变量)表明射线只需要检测砖块,忽略玩家和其它碰撞体。
  4. 如果射线没有射到任何东西,说明这是一个空的瓦片。
  5. 在这个位置产生爆炸。
  6. 射线击中砖块。
  7. 如果射中的是砖块,中断 for 循环。这使得爆炸会被墙所阻断。
  8. 等待 0.5 秒在进行下一次循环。这使得爆炸会以向外蔓延的方式进行。

实际效果是这个样子:

如何用 Unity 编写像炸弹人一样的游戏_第12张图片

红色的线表示了射线。它检查周围是否有空瓦片,如果有,生成一个爆炸。如果照射到砖块,则不会产生爆炸,并停止该方向上的检查。

现在你明白,为什么炸弹需要限制在瓦片的中央了吧?如果炸弹可以放在任意位置,那么在某种边缘情况下射线可能会碰到墙砖而不爆炸,因为它没有正确的对齐:

如何用 Unity 编写像炸弹人一样的游戏_第13张图片

最后,在项目视图中,选择 Prefabs 文件夹下的 Bomb 预制件,修改 Level Mask 为 Blocks。

如何用 Unity 编写像炸弹人一样的游戏_第14张图片

运行游戏,扔炸弹。观察爆炸是否会向外波及以及被墙阻断:

如何用 Unity 编写像炸弹人一样的游戏_第15张图片

恭喜你,你完成了最艰巨的任务!

想想你刚才所做的,来杯清爽的饮料或者点心犒劳一下自己,然后再去放几颗炸弹。

连锁反应

当一个爆炸波及到另一颗炸弹时,第二颗炸弹会爆炸——这种特性能让游戏更加具有技巧性、刺激性和火爆。

幸好这做起来也不难。

打开 Bomb.cs 脚本。在 CreateExplosions() 下面添加一个新方法 OnTriggerEnter:

public void OnTriggerEnter(Collider other) 
{

}

OnTriggerEnter 是 MonoBehaviour 的一个内置方法,当一个触发碰撞体和一个刚体发生碰撞时调用。其中的 Collider 参数 other,指的是进入这个触发器的游戏对象的碰撞体。

在这里,你需要检查这个碰撞体,如果它是一个爆炸则引发这个炸弹。

首先,你需要知道这个炸弹是否已经爆炸过了。先申明一个 exploded 属性,在 levelMask 变量下声明:

private bool exploded = false;

在 OnTriggerEnter() 中加入:

if (!exploded && other.CompareTag("Explosion"))
{ // 1 & 2  
  CancelInvoke("Explode"); // 2
  Explode(); // 3
}  

这段代码进行了:

  1. 检查炸弹有没有爆炸过。
  2. 检查这个触发碰撞体的 Explosion 标签有没有赋值。
  3. 取消先前对 Explode 的调用——如果你不想让这个炸弹爆炸两次的话。
  4. 引爆。

你声明了一个变量,但还没有在任何地方对它赋值。最理想的地方是在 Explode() 方法的禁用 MeshRenderer 组件之后:

...
GetComponent().enabled = false;
exploded = true;        
...  

活干完了,保存文件,运行游戏场景。接连扔几颗炸弹,观察会发生什么:

如何用 Unity 编写像炸弹人一样的游戏_第16张图片

现在,你可以让巨大的破坏力得以持续。利用超酷的链式反应,只需要一次爆炸并波及到其它炸弹就能让整个游戏世界陷入火海。

最后一件事情是处理玩家被炸到的反应(提示:他们没玩好),以及游戏如何将这种反应转换成输赢状态。

玩家死亡以及如何处理

打开 the Player.cs 脚本。

现在,还没有表示玩家是死是活的变量,可以加一个布尔变量,在 canMove 变量下面添加:

public bool dead = false;

这个变量用于记录玩家是否被炸死了。

然后,在所有变量声明之前添加:

public GlobalStateManager globalManager;

这是一个 GlobalStateManager 对象,这是一个通知所有玩家死亡状态并判断那个玩家获胜的脚本。

在 OnTriggerEnter() 中,已经对玩家是否被炸到进行了判断,但它所采取的处理仅仅是在控制台输出而已。

在 Debug.log 语句后面添加:

dead = true; // 1
globalManager.PlayerDied(playerNumber); // 2
Destroy(gameObject); // 3  

这段代码做了些什么:

  1. 设置 dead 变量,记录下玩家的死亡状态。
  2. 通知全局状态管理器,该玩家已死。
  3. 销毁代表这个玩家的游戏对象。

保存文件,回到 Unity 编辑器。你需要将 GlobalStateManager 连接到两个玩家上:

  • 在结构窗口中,选择两个 Player 的游戏对象。
  • 将 Global State Manager 游戏对象拖到它们两个的 Global Manager 空格中。

如何用 Unity 编写像炸弹人一样的游戏_第17张图片

运行游戏,让其中一个玩家被炸死。

如何用 Unity 编写像炸弹人一样的游戏_第18张图片

任何敢于挡在爆炸面前的玩家都立马灰飞烟灭。

然而游戏并不知道谁赢了,因为 GlobalStateManager 还没有使用它收到的信息。让我们继续。

宣布赢家

打开 GlobalStateManager.cs。因为 GlobalStateManager 记录了玩家的死亡状态,你需要两个变量。在这个脚本的 PlayerDied() 方法上面添加:

private int deadPlayers = 0;
private int deadPlayerNumber = -1;  

deadPlayers 记录玩家的死亡次数。deadPlayerNumber 会在第一个玩家死亡时被设置,用于表示死的是哪一个。

然后,添加真正的逻辑。在 PlayerDied() 中。添加这段代码:

deadPlayers++; // 1

if (deadPlayers == 1) 
{ // 2
    deadPlayerNumber = playerNumber; // 3
    Invoke("CheckPlayersDeath", .3f); // 4
}  

这段代码做了这些事情:

  1. deadPlayers 加 1。
  2. 如果是第一个死亡的玩家…
  3. 设置 deadPlayerNumber 为第一个死亡的玩家。
  4. 判断是否另外一个玩家也死了,或者在 0.3 秒之内也死了。

最后的延迟很关键,因为允许平局。如果你立即检查,你无法知道任何人已死。要判断人是否死了,0.3 秒足矣。

胜、负、平

这已经是最后一部分了!这里你需要创建决定胜负平的逻辑。

在 GlobalStateManager 脚本中增加一个新方法:

void CheckPlayersDeath() 
{
  // 1
  if (deadPlayers == 1) 
  { 
    // 2
    if (deadPlayerNumber == 1) 
    { 
      Debug.Log("Player 2 is the winner!");
    // 3
    } 
    else 
    { 
      Debug.Log("Player 1 is the winner!");
    }
     // 4
  } 
  else 
  { 
    Debug.Log("The game ended in a draw!");
  }
}  

这个方法中的每个 if 语句的逻辑分别是:

  1. 只有一个玩家死了,那么它是输家。
  2. 如果死的是玩家 1,那么玩家 2 获胜。
  3. 如果死的是玩家 2,那么玩家 1 获胜。
  4. 两个都死了,平局。

保存代码,最后一次运行游戏,查看控制台窗口,看看是哪个玩家获胜或者是平局。

如何用 Unity 编写像炸弹人一样的游戏_第19张图片

本教程就到此结束了!现在可以邀请好友来一起玩并把他们炸飞吧!

接下来做什么?

如果你遇到困难,可以从这里下载最终完成的项目。

现在你已经知道如何用 Unity 开发一个简单的炸弹人游戏。

这篇炸弹人教程使用了粒子系统,用于炸弹和爆炸效果,如果你想学习更多关于粒子系统的内容,请阅读我的 Unity 入门:粒子系统教程。

我强烈建议你继续在这个游戏上花功夫——添加新的功能!这里有一些建议:

  1. 让炸弹可以被“推开”,你可以从炸弹身边逃走并推向对手
  2. 限制能够放的炸弹数
  3. 能够更快地开始游戏
  4. 添加一些能够被炸开的砖块
  5. 创建有趣的道具
  6. 加命,或者某种可以获取它的方法
  7. 添加用于表示玩家获胜的 UI
  8. 允许更多玩家

请将你的作品分享给我,我想看看你能够做到哪一步!再次希望你喜欢本教程!

如果你有任何问题或评论,请在评论区留言。

你可能感兴趣的:(游戏开发)