简介
炸弹人游戏是上世纪80年代广泛流行的一个2D游戏,本文创建的是一个基本型的此游戏的Unity3D版本。
通过本游戏,你可以实现如下功能:
- 投掷炸弹并把它放到特定位置
- 通过光线跟踪技术激活炸弹
- 处理与玩家的爆炸碰撞
- 处理与炸弹的爆炸碰撞
- 游戏结局处理
准备工作
首先,请下载一个我为本文游戏建立的初始示例项目,然后把它放到一个你指定的位置。
然后,使用Unity3D打开这个项目,注意到Assets文件夹下包含了好多的子文件夹,如图所示。
这里具体说一下各个文件夹的主要功能:
- Animation Controllers:存储着游戏控制器部分,包括的逻辑部分。
- Materials:包含构建各关卡场景所需要的块(Block)材质。
- Models:存储玩家、关卡及炸弹模型,及其相关材质。
- Music:存储游戏的音效文件。
- Physics Materials:存储玩家的物理材质数据,它们是一些特殊类型的材质,用于实现特定的物理属性。在本教程中,用于使玩家在无摩擦情况下轻松地在关卡中穿越。
- Prefabs:包含炸弹及爆炸的预制数据。
- Scenes:对应于游戏场景数据。
- Scripts:包含游戏的启动脚本,其中添加的大量注释将有利于读者阅读源码。
- Sound Effects:包含炸弹及爆炸效果相关的声效文件。
- Textures:包含两个玩家的纹理数据。
投掷炸弹
如果你还没有打开游戏工程,请抓紧打开,然后试着运行一下此程序。没有其他问题的话,你会观察到如图所示的情形:
你会注意到,游戏中的两个玩家可以通过键盘上的WASD四个字符键或者四个箭头键驱动,使其沿着游戏地图运动。
通常,当按下空格键时红色玩家会在其脚下安置一枚炸弹,而另一个玩家也能够做同样的事情——只是通过按回车键实现。
然而,目前我们还没有实现这一功能。为此,你需要先编写放置炸弹的代码。现在,请你使用自己喜欢的代码编辑器打开脚本文件Player.cs。
此脚本负责处理所有的玩家运动及动画逻辑,还包含一个方法DropBomb,当关联游戏对象(GameObject)bombPrefab时,它用于检测目的。
- private void DropBomb() {
- if (bombPrefab) { //Check if bomb prefab is assigned first
- }
- }
为了实现一个炸弹掉落在玩家下面的效果,在if语句中添加下面的代码:
- Instantiate(bombPrefab, myTransform.position, bombPrefab.transform.rotation);
上述代码将在玩家脚下生成炸弹(随着玩家的运动路径的变化,将生成成串的炸弹)。现在,运行一下游戏工程,你会观察到如下图所示效果:
目前,效果不错吧!
但是,还有一个小问题:炸弹投掷的方式如何?如果是无论在哪里你都能放炸弹的话,当你需要计算爆炸应该发生的位置时就会带来一些问题。
接下来,本教程将向你具体介绍如何实现爆炸的所有细节。
炸弹定位
下一步任务是确保炸弹在丢掉时能够附着到相应位置,从而实现炸弹很好地与地板上的网格对齐。由于我们的设计中网格上的每个图块大小是 1 × 1,所以进行此更改是相当容易的。
打开文件Player.cs,编辑一下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两个参数值,每一个浮点类型值被转换为一个整型值,这就可以实现炸弹很好地与地板上的网格对齐的效果:
现在,你可以再次启动工程来运行一下,你会观察到当投掷炸弹时,这些炸弹恰好能够对齐网格:
虽然把炸弹投掷到地图上是很有趣的,但你知道真正有趣的事是如何实现爆炸!为此,我们再来添加一些功能。
创建爆炸效果
首先,我们要创建一个新的脚本文件:
(1)从Project视图下选择Scripts文件夹;
(2)按下Create按钮;
(3)选择“C# Script”;
(4)把脚本文件命名为Bomb即可。
现在,把Bomb.cs脚本关联到预制Bomb上:
(1)在Prefabs文件夹中选择GameObject Bomb;
(2)点击按钮“Add Component”;
(3)在搜索框中输入“bomb”;
(4)选择你刚刚创建的脚本Bomb.cs;
(5)打开此脚本文件,然后在其Start()方法中输入如下代码:
- Invoke("Explode", 3f);
此方法使用了两个参数,第一个是将要调用的方法名称,第二个是在调用此方法时需要延迟的时间数。在本例中,想实现炸弹在3秒内爆炸的效果。我们将在后面添加这个Explode方法的具体内容。
现在,只是在Update()方法下面添加这个方法占位符形式(目前为空):
- void Explode() {
- }
在生成任何GameObject Explosion之前,还需要创建一个公共类型的GameObjet对象,以便进行预制Explosion的赋值。恰好在Start()方法上面定义如下代码:
- public GameObjectexplosionPrefab;
保存此文件,然后从Prefabs文件夹下选择预制Bomb,然后把预制Explosion拖动到“Explosion Prefab”选项后面空白处。
完成这一操作后,返回到编辑器中。现在开始编写更有意思的代码。
在方法Explode()中,添加如下代码行:
- Instantiate(explosionPrefab, transform.position, Quaternion.identity); //1
- GetComponent<MeshRenderer>().enabled = false; //2
- transform.FindChild("Collider").gameObject.SetActive(false); //3
- Destroy(gameObject, .3f); //4
上述代码实现如下功能:
1.在炸弹位置触发爆炸;
2.禁用网络渲染器(mesh render),使炸弹不可见;
3.禁用碰撞器,从而允许玩家在爆炸中移动与行走;
4.在0.3秒后拆除炸弹;这可以确保在删除GameObject之前所有爆炸都会触发。
现在,保存脚本Bomb.cs,返回到编辑器尝试再玩一下游戏。放下一些炸弹并观察一下它们爆炸时良好的效果,参考下图。
设置爆炸音效
为了创建理想的爆炸效果,你需要创建一个协程。
「补充」协程本质上是一个函数,允许你暂停执行并将控制返回到Unity3D。在以后的某个时间点处该函数将从上次离开的位置恢复执行。
人们经常混淆协程与多线程。其实,它们是不同的:协程运行在同一个线程中,并能够在某中间点处及时恢复执行。若要了解更多的关于协程及其定义相关信息,请查阅相关的Unity文档(http://docs.unity3d.com/Manual/Coroutines.html)。
现在,返回到代码编辑器中修改脚本Bomb.cs,在函数Explode()下面添加一个名字为CreateExplosions的IEnumberator:
- private IEnumeratorCreateExplosions(Vector3 direction) {
- return null // placeholder for now
- }
创建协程
现在,请把下面四行代码添加到函数Explode()内部的Instantiate调用与MeshRender禁用之间:
- StartCoroutine(CreateExplosions(Vector3.forward));
- StartCoroutine(CreateExplosions(Vector3.right));
- StartCoroutine(CreateExplosions(Vector3.back));
- StartCoroutine(CreateExplosions(Vector3.left));
这里的StartCoroutine调用将针对游戏场景中的每个方向触发CreateExplosions。
现在,更有趣的时刻到了。在方法CreateExplosions()内部加入如下代码:
- //1
- for (inti = 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.RaycastHit对象包含有关Raycast击中的是什么对象及击中位置的所有信息,当然也可能没有击中。
3.上述代码中非常重要的代码行是StartCoroutine调用,这个调用中实现从炸弹中心朝你通过的方向发出raycast。然后,它将结果输出到RaycastHit对象。I 参数指示射线走过的距离。最后,代码中使用命名为levelMask的层蒙版(LayerMask)来确保射线只检查当前关卡中的块而忽略检查玩家及其他的碰撞对象。
4.如果raycast没有撞到任何东西,那么说明这个块(Block)是一个自由块。
5.在raycast检查的位置产生爆炸。
6.Raycast击中块。
7.一旦raycast击中一个块,它就跳出for循环。这将确保爆炸不会跨越墙。
8.在进行下一个for循环迭代前等待0.05秒。这将使爆炸呈现向外扩展的效果而更具有说服力。
下图给出的是上面添加代码后的动画效果:
注意,下图中的红线是raycast。它围绕炸弹检查一段自由空间距离;如果发现存在碰撞,那么将产生爆炸。当它击中块时,它并不产生任何东西并停止在那个方向上的检查。
现在,你该明白了为什么炸弹需要附着到网格中各小格子中心了吧。如果炸弹能去任何地方,那么在一些边缘情况下,raycast会击中块却并不产生任何爆炸,因为它没有正确地进行水平对齐。
添加遮罩层
在Bomb代码中还存在一个错误:事实上LayerMask并不存在。因此,现在就在explosionPrefab变量声明的下面,再添加一行代码,如下:
- public GameObjectexplosionPrefab;
- public LayerMasklevelMask;
LayerMask通常使用raycasts技术有选择地筛选出特定图层。在本例情况下,你只需要筛选出块部分,所以,raycasts技术并没有做什么事。
保存Bomb脚本并返回到Unity编辑器。单击右上角的Layers按钮并选择Edit Layers...
现在,请点击User Layer 8旁边的文本框并输入“Blocks”。这样就定义了你可以使用的新层。
在层次视图中,选择GameObject Blocks,如下图所示:
把图层改变为你刚刚创建的图层Blocks,如下图所示:
当出现“Change Layer”对话框时点击“Yes,change children”按钮,以便应用于地图上所有散布的黄色长方体块。
最后,从Prefabs文件夹下选择预制Bomb,并把遮罩层改成Blocks。
现在,再次运行一下游戏场景并投掷几枚炸弹。你会观察到爆炸效果比以前好多了:良好地分布于各长方体块之间!
恭喜你,你已经攻克了本游戏中最困难的编码部分。剩下的是为游戏添加一些附加效果。
链式反应
当一枚炸弹爆炸时会接触到另一个炸弹,这枚相邻的炸弹也应该爆炸,这将产生一种更令人惊喜的效果。
值得欣喜的是,上述效果并不难实现。
现在打开脚本文件Bomb.cs,然后在方法CreateExplosions()下面添加一个新的方法OnTriggerEnter:
- public void OnTriggerEnter(Collider other) {
- }
OnTriggerEnter方法是MonoBehaviour中一个预定义的方法,在触发器碰撞器与刚体碰撞时激活执行。碰撞器参数是other,对应于进入触发器的游戏物体(GameObject)的碰撞器。
在本例情况下,你需要检查碰撞对象,并确定当该对象是一个爆炸对象时使之爆炸。
首先,你需要知道是否发生炸弹爆炸。需要首先声明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<MeshRenderer>().enabled = false;
- exploded = true;
- ...
现在准备好了一切,请保存一下刚才的文件修改,然后再次运行一下工程。再次投掷一枚炸弹,并连续在其周围投掷炸弹,观察效果:
最后剩下的事情是处理玩家对于爆炸的反应情况,以及游戏结局处理逻辑。
游戏结局处理
打开文件Player.cs。目前还没有定义变量来表示玩家的死活;因此,在脚本顶部添加一个布尔变量,如下所示:
- public cool dead=false;
这个变量用于跟踪是否玩家在爆炸以后死亡。
接下来,在其他变量声明后面添加如下变量声明:
- public GlobalStateManagerGlobalManager;
注意,到现在在方法OnTriggerEnter()内部已经能够检查是否玩家被炸弹击中,但目前实现的仅仅是通过控制台窗口输出这一消息。因此,现在请将如下代码添加到Debug.Log调用后面:
- dead = true; // 1
- GlobalManager.PlayerDied(playerNumber); // 2
- Destroy(gameObject); // 3
这段代码实现如下功能:
1.修改变量dead,以便跟踪玩家死亡的消息;
2.通知全局状态管理器玩家已经死亡;
3. 销毁玩家对象GameObject。
现在,保存一下文件并返回到Unity编辑器中。你需要把GlobalStateManager连接到两个玩家:
(1)在层次窗口内,选择两个Player GameObject。
(2)把全局状态管理器GameObject拖动到它们的Global Manager选项处。
再次运行游戏场景,确保至少有一个玩家被炸弹击中,参考下图。
每一个遭遇到爆炸的玩家都会立即死亡。
但是,目前为止游戏并不知道谁赢了,因为GlobalStateManager还没有使用它收到的信息。下面来讨论这件事情。
定义赢家
打开文件GlobalStateManager.cs。为了使GlobalStateManager能够跟踪玩家的死亡,还需要定义两个变量。在函数PlayerDied()上面加上下面的定义:
- private intdeadPlayers = 0;
- private intdeadPlayerNumber = -1;
首先,变量deadPlayers会存储死亡的玩家数量。一旦第一个玩家死亡,变量deadPlayerNumber即被修改,此变量也表示了是哪一位玩家这种额外信息。
准备好了上面变量后,现在加入实际逻辑。在函数PlayerDied()中加入如下代码:
- deadPlayers++; // 1
- if (deadPlayers == 1) { // 2
- deadPlayerNumber = playerNumber; // 3
- Invoke("CheckPlayersDeath", .3f); // 4
- }
这段代码的功能是:
1.添加一个死亡玩家;
2.进一步判断是否这是第一个死亡玩家…
3.把死亡玩家数设置为首先死亡的玩家;
4.检查是否另一个玩家也死亡了,还是在0.3秒后仅起了一些爆炸尘埃而没有死亡。
最后的一点时间延迟对于绘制检查来说很重要。如果立即进行检查,你可能发现不了有人死亡,而0.3秒对于判断是否每一个人都死亡了已经足够了。
输赢判定
现在,请在GlobalStateManager脚本中添加一个新方法CheckPlayersDeath:
- 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!");
- }
- }
上述条件语句的功能列举如下:
1.只有一个玩家死亡,则判定他是输家;
2.玩家1死亡了,那么玩家2是赢家;
3.玩家2死亡了,那么玩家1是赢家;
4.两个玩家都死亡了,那么这是一场平局。
现在,再保存并运行一下你的工程试试吧,参考下图:
剩下的话
请下载工程代码并进行详细研究吧!
你通过本文了解了如何使用Unity3D创建像炸弹人这样的基本类型的游戏。
本文使用了一些粒子系统用于炸弹与爆炸效果。更多的有关信息,请参考Unity3D官方文档。
最后,强烈建议你做如下增强性修改:
(1)可以使炸弹能够被推动,这样当炸弹靠近你时你可以逃跑,而把炸弹推到你的对手身上;
(2)限制可以投掷的炸弹数量;
(3)加入重新启动游戏功能;
(4)伴随爆炸加入可破裂的场景中的块(Blocks);
(5)你可以增加一些有趣的装备;
(6)多加几条命,以及使用某种方式来进行购买;
(7)创建漂亮的UI元素来显示玩家赢了什么东西;
(8)探讨某种方法来添加更多的玩家,等等。