原文: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 打开开始项目。资源被放在这了几个文件夹中:

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

扔炸弹

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

两个玩家可以通过 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,将小数变成证书,然后将炸弹限制到瓦片的位置:

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

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

制造爆炸

首先,创建一个新脚本:

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

将 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); //1GetComponent<MeshRenderer>().enabled = false; //2
transform.Find("Collider").gameObject.SetActive(false); //3
Destroy(gameObject, .3f); //4

这块代码主要做了:

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

保存 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++) { //2RaycastHit hit; //3Physics.Raycast(transform.position + new Vector3(0,.5f,0), direction, out hit, i, levelMask); //4if (!hit.collider) { Instantiate(explosionPrefab, transform.position + (i * direction),//5 explosionPrefab.transform.rotation); //6} else { //7break; }//8yield return new WaitForSeconds(.05f);
}

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

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

实际效果是这个样子:

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

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

最后,在项目视图中,选择 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"); // 2Explode(); // 3
}  

这段代码进行了:

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

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

...
GetComponent<MeshRenderer>().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

这段代码做了些什么:

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

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

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

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

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

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

宣布赢家

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

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

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

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

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

这段代码做了这些事情:

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

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

胜、负、平

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

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

void CheckPlayersDeath()
{// 1if (deadPlayers == 1) { // 2if (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 开发一个简单的炸弹人游戏。

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

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

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

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

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

如何用 Unity 编写像炸弹人一样的游戏相关推荐

  1. unity编写一个简单的小游戏

    unity编写一个简单的小游戏 简易fly bird的制作 关于flybird 场景的搭建和素材的导入 使用素材搭建game和scene 制作柱体 bird的script column的sprite ...

  2. java入门教程:如何用java编写一款王者荣耀游戏?

    Java是一种编程语言,被特意设计用于互联网的分布式环境.Java具有类似于C++语言的"形式和感觉",但它要比C++语言更易于使用,而且在编程时彻底采用了一种"以对象为 ...

  3. python写乘法口诀-如何用python编写乘法口诀表

    如何用python编写乘法口诀表?首先要明确一下思路,我们可以确定x,y两个变量,弄清楚其变化的规律,再使用循环嵌套实现.下面是如何用Python编写乘法口诀表的具体方法. 第一种:使用for遍历循环 ...

  4. python rest api 测试_如何用Python编写REST API的单元测试

    在过去的几个月中,正在从事一个名为B的项目.它是带有简单Web UI的徽章生成器,用于添加数据并生成PDF可打印徽章.B后端现在已转移到REST-API并测试REST-API中使用的功能,我们需要一些 ...

  5. 如何用Unity和Cardboard做一款VR游戏

    随着Oculus宣布1月6日开启预售,2016年很可能成为VR游戏元年,但很多的调研显示,手游设备才是市场增长的关键,SuperData发布的报告显示,2016年全球VR游戏市场规模预计在51亿美元左 ...

  6. c语言switch写值班表,如何用asp编写按周轮换的值班表?例,1月份的值班领导有4位,怎样写可以让4位领导的名字自动到时间显示...

    如何用asp编写按周轮换的值班表?例,1月份的值班领导有4位,怎样写可以让4位领导的名字自动到时间显示以下文字资料是由(历史新知网www.lishixinzhi.com)小编为大家搜集整理后发布的内容 ...

  7. python写乘法口诀表好记方法_如何用python编写乘法口诀表

    如何用python编写乘法口诀表?首先要明确一下思路,我们可以确定x,y两个变量,弄清楚其变化的规律,再使用循环嵌套实现.下面是如何用Python编写乘法口诀表的具体方法. 第一种:使用for遍历循环 ...

  8. 如何用FFmpeg编写一个简单播放器详细步骤介绍

    如何用FFmpeg编写一个简单播放器详细步骤介绍(转载) FFMPEG是一个很好的库,可以用来创建视频应用或者生成特定的工具.FFMPEG几乎为你把所有的繁重工作都做了,比如解码.编码.复用和解复用. ...

  9. 如何用Python编写一个求 1到n阶乘之和的程序

    各位许久不见了,甚是想念! 前段时间我进入高中阶段学习,一直还没有适应,现在好些了就继续写博客了. 看到很多人关注我,点赞或是评论,我感觉太高兴了! C语言这块我暂时先放下了,我想自己学学Python ...

  10. 如何用PHP编写简单的api数据接口

    一.编写接口所需几样工具或软件(均是win7+64位) 1.phpStudy.SQLyog和编码工具的安装(sublime text/webStorm/vs code均可,按自己习惯来): 2.启动p ...

最新文章

  1. curl模拟多线程抓取网页(优化)
  2. WebService_Unity
  3. CvBlobDetector 新目标检测算法简析
  4. Codeforces Round #619 (Div. 2) E. Nanosoft 思维 + 二维前缀和
  5. docker 查看日志_8个优秀Docker容器监控工具,收藏了
  6. Hibernate-Criteria
  7. SQL Server 2008 R2:error 26 开启远程连接详解
  8. 文件后缀名查询(全)
  9. java fps计算_帧率(FPS)计算的六种方法总结
  10. Go 原生插件使用问题全解析
  11. 卡瓦莱斯的世界杯往事
  12. 高中计算机应用面试教资真题,2019下半年高中信息技术教师资格证面试试题(精选)第四批...
  13. Angular学习笔记第三章——创建组件
  14. tableau的骑行路线地理数据可视化
  15. 怎么在计算机上搭建远程桌面,创建远程桌面连接的方法
  16. 接口自动化测试框架介绍
  17. Windows打印机驱动开发
  18. 新年寄语+从业感受+祝大家新年快乐~
  19. 分享一下杭州医院的看病流程(我去的杭州市第三人民医院)
  20. 【C语言】你还不会指针吗?不妨来一起攻克指针这个难点

热门文章

  1. 线性代数05 齐次/非齐次线性方程组的具体解集
  2. 黑帽SEO强势技术大纲
  3. [实战]黑帽SEO的RayFile排名做法
  4. 央视财经采访:康晓阳投资分享
  5. 工业级高精度电磁流量计解决方案
  6. SpringBoot yml文件命名规则
  7. 什么是云计算,云计算的三种类型
  8. android 系统app切换,安卓应用转换器(安装应用转为系统应用)app
  9. 《第一行代码——Android》封面诞生记
  10. threejs学习之透视相机与正交相机