原文: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 打开开始项目。资源被放在这了几个文件夹中:
打开游戏场景,运行游戏。
两个玩家可以通过 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);
这将在玩家脚下生成一颗炸弹。保存修改,运行游戏场景:
真的可以耶✌️!
有个小问题,炸弹是可以生成了,你可以到处乱扔,这样在你计算爆炸应当在哪里发生时会出现一些问题。
当进入到如何产生爆炸时,你将会学习为什么要特别指出这一点。
第二个任务是固定炸弹放置的位置,以便它们刚好放在一个格子中。因为每个方格的瓦片图刚好是 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,将小数变成证书,然后将炸弹限制到瓦片的位置:
保存修改,运行游戏,扔几颗炸弹。炸弹的位置现在被固定到了方格中:
尽管在地图上扔炸弹很好玩,但它要会炸才行啊!接下来给炸弹添加一点威力。:]
首先,创建一个新脚本:
将 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
这块代码主要做了:
保存 Bomb 脚本,回到编辑器,运行游戏。放几颗炸弹,然后享受爆炸带来的快感吧!
值得庆幸的是,游戏中有墙,能够抵挡住炸弹。炸弹不能防住爆炸,玩家肯定也防不住爆炸。你必须有一种方法声明某个对象是不是墙。有一个方法就是使用 LayerMask 图层掩码。
LayerMask 会有选择地过滤掉某些图层,它通常用于射线检测。在这个例子中,你需要过滤的只有砖墙,这样射线将不能射中任何东西。
在 Unity 编辑器中点击右上角 Layer 按钮,然后选择 Edit Layer …
如果看不见图层列表,你可以点击 Layers 前边的三角形,展开图层列表。
点击 User Layer 8 旁边的文本框,输入 Blocks,这会定义一个新的图层,后面会用到。
在结构视图中,选择 Map 对象下边的 Blocks 游戏对象。
将 layer 修改成你刚刚创建的 Blocks 图层:
当修改图层对话框弹出,点击 “Yes, change children” 按钮,将修改应用到地图中所有的黄色砖块。
最后,为 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);
}
看起来很复杂,但实际上非常简单。它分成了以下几个步骤:
实际效果是这个样子:
红色的线表示了射线。它检查周围是否有空瓦片,如果有,生成一个爆炸。如果照射到砖块,则不会产生爆炸,并停止该方向上的检查。
现在你明白,为什么炸弹需要限制在瓦片的中央了吧?如果炸弹可以放在任意位置,那么在某种边缘情况下射线可能会碰到墙砖而不爆炸,因为它没有正确的对齐:
最后,在项目视图中,选择 Prefabs 文件夹下的 Bomb 预制件,修改 Level Mask 为 Blocks。
运行游戏,扔炸弹。观察爆炸是否会向外波及以及被墙阻断:
恭喜你,你完成了最艰巨的任务!
想想你刚才所做的,来杯清爽的饮料或者点心犒劳一下自己,然后再去放几颗炸弹。
当一个爆炸波及到另一颗炸弹时,第二颗炸弹会爆炸——这种特性能让游戏更加具有技巧性、刺激性和火爆。
幸好这做起来也不难。
打开 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
}
这段代码进行了:
你声明了一个变量,但还没有在任何地方对它赋值。最理想的地方是在 Explode() 方法的禁用 MeshRenderer 组件之后:
...
GetComponent().enabled = false;
exploded = true;
...
活干完了,保存文件,运行游戏场景。接连扔几颗炸弹,观察会发生什么:
现在,你可以让巨大的破坏力得以持续。利用超酷的链式反应,只需要一次爆炸并波及到其它炸弹就能让整个游戏世界陷入火海。
最后一件事情是处理玩家被炸到的反应(提示:他们没玩好),以及游戏如何将这种反应转换成输赢状态。
打开 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
这段代码做了些什么:
保存文件,回到 Unity 编辑器。你需要将 GlobalStateManager 连接到两个玩家上:
运行游戏,让其中一个玩家被炸死。
任何敢于挡在爆炸面前的玩家都立马灰飞烟灭。
然而游戏并不知道谁赢了,因为 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
}
这段代码做了这些事情:
最后的延迟很关键,因为允许平局。如果你立即检查,你无法知道任何人已死。要判断人是否死了,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 语句的逻辑分别是:
保存代码,最后一次运行游戏,查看控制台窗口,看看是哪个玩家获胜或者是平局。
本教程就到此结束了!现在可以邀请好友来一起玩并把他们炸飞吧!
如果你遇到困难,可以从这里下载最终完成的项目。
现在你已经知道如何用 Unity 开发一个简单的炸弹人游戏。
这篇炸弹人教程使用了粒子系统,用于炸弹和爆炸效果,如果你想学习更多关于粒子系统的内容,请阅读我的 Unity 入门:粒子系统教程。
我强烈建议你继续在这个游戏上花功夫——添加新的功能!这里有一些建议:
请将你的作品分享给我,我想看看你能够做到哪一步!再次希望你喜欢本教程!
如果你有任何问题或评论,请在评论区留言。