2.4 程序实现
2.4.1 前期准备
[1] 新建文件。新建一个名为WhacAMole的2D工程。把3D/2D选项修改为2D,如图2-3所示。点击“Create Project”按钮,完成新建工程。
图2-3 新建工程
[2] 导入素材包。在Assets标签上单击鼠标右键,在弹出菜单中选择“Import Package/custom Package…”,将本书资源包中WhacAMole文件导入。资源包中有完整的游戏案例和完成游戏所需的一切素材。在【_Complete-Game】文件夹中双击【Done_Mole】文件可以运行游戏。运行结果如图2-4所示。
图2-4 资源包导入
2.4.2 设置洞口
[1] 新建场景。在Assets标签上单击鼠标右键,在弹出的菜单中点击【Create/scene】,新建名为【Game】的游戏场景后保存。我们的游戏将在这个场景里制作完成。
[2] 在Sprites文件夹中选择【ground】图片,将其拖入场景中。在界面右侧Inspector面板中将图片的x与y的坐标值定为0。将Order in Layer的值设定为-1,让这张图像一直在最低层,如图2-5所示。
图2-5 【ground】图像设置
注:Unity会根据Order的值从低到高绘制图像。值越低,说明该物体渲染时间越早,显示在越低层,会被遮盖;值越高,说明该物体渲染时间越晚,显示在越高层,可以覆盖低层的图片,如图2-6所示。
图2-6 Order in layer的值与画面显示效果的关系示意图
[3] 摄像机视野调整。调整摄像机的视野范围,使背景图片占据整个视野。在【Hierarchy】内单击选择Main Camera后,我们有两种方式调整摄像机视野,一种是直接在Scene面板内调整白色线框的大小,另一种则是在【Inspector】面板内调整Size值,如图2-7所示。
图2-7 摄像机视野调整
[4] 洞口设置。选择【Sprites】文件夹内的【Hole】图片拖入场景中,将其x、y坐标值设置为0和-1。九个洞口的坐标和序号对应如图2-8所示。
图2-8 洞口位置设置和坐标示意图
以上的坐标是我根据当前的画面自己决定的,当然你也可以进行适当的改变。资源包中存有单个地洞的图像,如果你选择自己定义洞口坐标,可以利用单个地洞自行安置,不要忘记在后续的函数中也改变相应的坐标值。
[5] 洞口归类。如果你是利用单个地洞图像放置地洞的,为了画面的简洁,在【Hierarchy】中新建一个名为【Hole】的空物体,将上述的九个洞口都拖入成为其子物体,如图2-9所示。
图2-9 洞口归类
2.4.3 单只地鼠的出现与消失
打地鼠这个游戏最核心最主要的就是地鼠的出现和消失,下面我们要完成的就是单只地鼠生成和击打后消失的流程。
[1] 在Project面板中,新建一个名为【Scripts】的文件夹,我们之后要写的所有脚本,都将存储在这个文件夹中。
[2] 流程控制脚本。首先我们需要一个可以对整个游戏流程进行管理控制的脚本,一般选择将这种类型的脚本绑定在主摄像机上。选择Main Camera,在【Inspector】中选择【Add Component】-【New Script】,新建一个名为【GameControl】的C#脚本,如图2-10所示,将其放入Scripts文件夹中,双击打开。
图2-10 新建c#脚本步骤
注意:本次游戏的脚本都会使用这种方法创建,下文将不再复述。
[3] 地鼠出现程序。因为我们需要生成一个地鼠,所以在函数中我们需要定义一个GameObject类型的变量“Gophers”来存放地鼠对象,并在Start函数中生成它。
1 using System.Collections; 2 using System.Collections.Generic; 3 using UnityEngine; 4 public class GameControl : MonoBehaviour { 5 public GameObject Gophers; 6 // Use this for initialization 7 void Start() { 8 //在(0,0+0.4f)上生成地鼠,0.4f为地鼠的高度 9 Instantiate(Gophers,new Vector3(0, 0+ 0.4F, -0.1F), Quaternion.identity); 10 } 11 // Update is called once per frame 12 void Update () { 13 } 14 } GameControl脚本
[4] 预制体拖入。保存代码以后,返回Unity界面,选择Main Camera,在【Inspector】中找到GameControl的脚本,将【Prefabs】文件夹下名为【Mole】的预制体拖入,如图2-11所示。
图2-11 预制体拖入
完成上述步骤以后,点击运行游戏,单个地鼠就会出现了,效果如图2-12所示。
图2-12 效果示意图
[5] 地鼠脚本。在【Prefabs】文件夹中,我们找到地鼠预制体【Mole】,为其新建并绑定C#脚本【Gophers】,如图2-13所示,将脚本放入Scripts文件夹后双击打开。
图2-13 脚本放置
思考一下地鼠出现以后的状态:未被击打时,地鼠在一定时间后会自动消失;被击打后则会转化为击打状态的地鼠,在较短时间后消失。
让地鼠消失十分简单,我们只需要用Destory函数将当前的地鼠摧毁即可。
至于状态的转变,我们需要一个鼠标点击函数来获取点击状态,在当前地鼠的坐标上生成一个显示被击打的地鼠即可。生成方法与上述地鼠生成的方法几乎一致。
1 using UnityEngine; 2 /// <summary> 3 /// 地鼠类 4 /// </summary> 5 public class Gophers : MonoBehaviour { 6 //定义一个新的游戏对象beaten 7 public GameObject beaten; 8 9 /// <summary> 10 /// 地鼠出现后,如果未被点击,将在三秒后自动销毁 11 /// </summary> 12 void Update () { 13 Destroy(this.gameObject,3.0f); 14 } 15 16 /// <summary> 17 /// 鼠标点击函数 18 /// </summary> 19 void OnMouseDown() { 20 //在相同的位置生成一个被击打图像的地鼠 21 GameObject g; 22 g = Instantiate(beaten, gameObject.transform.position, Quaternion.identity); 23 //在0.1s后摧毁当前生成的地鼠 24 Destroy(this.gameObject, 0.1f); 25 } 26 } Gophers脚本
[6] 预制体拖入。返回Unity界面,将【Prefabs】文件夹下的【Mole_beaten】预制体拖入【Mole】的【Gophers】脚本中,如图2-14所示。
图2-14 预制体拖入
[7] 击打地鼠脚本。完成以上几个步骤以后,运行程序我们会发现beaten状态下的地鼠会一直存在。此时我们需要给预制体【Mole_beaten】也添加一个脚本【Beaten】,如图2-15所示,在其生成一段时间后自动销毁。
图2-15 脚本放置
1 using UnityEngine;
2 /// <summary>
3 /// 地鼠被击打以后调用该函数
4 /// </summary>
5 public class Beaten : MonoBehaviour {
6 /// <summary>
7 /// 在点击后3.5s销毁该地鼠
8 /// </summary>
9 void Update () {
10 Destroy(gameObject, 0.35f);
11 }
12 }
Beaten脚本
完成上述步骤后,我们就简单地完成了一个地鼠从出现到消失的整个过程。
2.4.4 地鼠的随机出现和出现频率
打地鼠游戏中,只在固定位置出现一次地鼠很显然是很单调的,我们接下来讲解如何在地图内随机生成地鼠。
[1] 设定洞口类。在GameControl脚本中,设定一个洞口Hole类,记录9个洞口的坐标以及地鼠出现的状态。
1 public class GameControl : MonoBehaviour { 2 public GameObject Gophers; 3 //用于记录地鼠的X,Y 坐标 4 public intPosX, PosY; 5 /// <summary> 6 /// 设定一个地洞类,存储地洞的坐标以及是否出现的布尔值 7 /// </summary> 8 public class Hole { 9 public boolisAppear; 10 public intHoleX; 11 public intHoleY; 12 } 13 14 public Hole[] holes; 15 16 /// <summary> 17 /// Awake函数实际上比Start函数调用的更早 18 /// 在场景初始化的时候,将每个洞口的坐标值存入一维数组中,并将每一个洞口的isAppear值设定为false 19 /// (-2,0)(0,0)(2,0) 20 /// (-2,-1)(0,-1)(2,-1) 21 /// (-2,-2)(0,-2)(2,-2) 22 /// </summary> 23 void Awake() { 24 PosX = -2; 25 PosY = -2; 26 holes = new Hole[9]; 27 for (int i = 0; i < 3; i++) 28 { 29 for (int j = 0; j < 3; j++) 30 { 31 holes[i * 3 + j] = new Hole(); 32 holes[i * 3 + j].HoleX = PosX; 33 holes[i * 3 + j].HoleY = PosY; 34 holes[i * 3 + j].isAppear = false; 35 PosY++; 36 } 37 PosY = -2; 38 PosX = PosX + 2; 39 } 40 } 41 ...... 42 } GameControl脚本
[2] 地鼠出现函数。定义一个生成地鼠的函数“Appear()”,将原来Start函数中的生成语句删除,演示延时重复调用Appear函数。
1 public class GameControl : MonoBehaviour { 2 …… 3 void Start () { 4 Instantiate(Gophers, new Vector3(0, 0+ 0.4F, -0.1F), Quaternion.identity); 5 //从第0秒开始调用,每秒掉用一次 6 InvokeRepeating("Appear", 0,1); 7 } 8 …… 9 /// <summary> 10 /// 地鼠生成函数 11 /// </summary> 12 public void Appear() 13 { 14 //随机生成i值选择洞口 15 int i = Random.Range(0, 9); 16 //debug只是用来打印当前的坐标,便于观察,并不会影响游戏运行(可略过) 17 Debug.Log(holes[i].HoleX + "," + holes[i].HoleY); 18 //选定洞口以后,在洞口的坐标上生成地鼠 19 Instantiate(Gophers, new Vector3(holes[i].HoleX, holes[i].HoleY + 0.4F, -0.1F), Quaternion.identity); 20 } 21 } GameControl脚本
完成上述操作以后,我们可以正常运行游戏,地鼠以每秒一个的速度匀速出现。如何让地鼠出现的频率逐步加快呢?其实十分简单,我们只需套用两个延时调用函数即可。
[3] 地鼠频率增加。回到GameControl脚本,定义一个CanAppear函数,在Start函数中调用。
1 public class GameControl : MonoBehaviour { 2 …… 3 void Start () { 4 InvokeRepeating("Appear", 0,1); 5 //在游戏场景开始后延时调用canAppear函数,从第0秒开始,每隔十秒调用一次 6 InvokeRepeating("CanAppear", 0, 10); 7 } 8 …… 9 /// <summary> 10 /// 从第0秒开始调用函数,每隔1秒调用一次 11 /// </summary> 12 public void CanAppear() { 13 InvokeRepeating("Appear", 0,1); 14 } 15 } GameControl脚本
简单回顾一下地鼠出现的频率:在游戏开始的第0秒,CanAppear函数被调用一次,Appear函数每秒被调用一次,地鼠每秒出现一个;在游戏开始的第10秒,CanAppear被调用两次,Appear函数每秒被调用两次,地鼠每秒出现两个……以此类推,地鼠出现的频率会随着时间的增加而上升。
[4] 禁止地鼠同位置生成。在地鼠同时出现的次数升高以后我们会发现一个问题:同批次的地鼠很有可能会生成在同一个地洞上,这个时候我们就要运用地洞类型里的isAppear布尔值了。
1 public class GameControl : MonoBehaviour { 2 …… 3 /// <summary> 4 /// 地鼠生成函数 5 /// </summary> 6 public void Appear() 7 { 8 //当前地洞可以生成地鼠的条件:isAppear = false 9 //随机生成i值选择洞口,直到符合条件的洞口被选中 10 int i = Random.Range(0, 9); 11 while (holes[i].isAppear == true) 12 { 13 i = Random.Range(0, 9); 14 } 15 //debug只是用来打印当前的坐标,便于观察,并不会影响游戏运行(可写可不写) 16 Debug.Log(holes[i].HoleX + "," + holes[i].HoleY); 17 18 //选定洞口以后,在洞口的坐标上生成地鼠,传递洞口id,将当前洞口的isAppear值改为true 19 Instantiate(Gophers, new Vector3(holes[i].HoleX, holes[i].HoleY + 0.4F, -0.1F), Quaternion.identity); 20 Gophers.GetComponent<Gophers>().id = i; 21 holes[i].isAppear = true; 22 } 23 } GameControl脚本
[5] 状态修改。如果仅仅是上述代码中的修改,我们很快就会发现游戏程序会进入一个死循环,因为9个洞口的isAppear值全部会变成true,所以我们需要一个状态修改,在地鼠消失以后,将对应洞口的isAppear值改回false。而这个状态修改应该在Gophers和Beaten脚本中填写。
1 public class Gophers : MonoBehaviour { 2 …… 3 public int id; 4 void Update () { 5 Destroy(this.gameObject,3.0f); 6 //将对应洞口的isAppear值设为false 7 FindObjectOfType<GameControl>().holes[id].isAppear = false; 8 } 9 void OnMouseDown() { 10 …… 11 //将当前洞口id传递给beaten 12 g.GetComponent<Beaten>().id = id; 13 } 14 } Gophers 脚本
13 public class Beaten : MonoBehaviour { 14 public int id; 15 16 void Update () { 17 Destroy(gameObject, 0.35f); 18 FindObjectOfType<GameControl>().holes[id].isAppear = false; 19 } 20 } Beaten脚本
2.4.5 时间、分数和其他
[1] 创建文本框。在Hierachy面板内创建两个3D Text,其中之一重命名为“Time”,使其成为MainCamera的子物体,另一个重命名为“Score”。我们将用他们来显示游戏时间,具体属性如下图2-16所示。
图2-16 位置信息设置面板截图
[2] 时间管理,游戏结束。在GameControl脚本中定义TimeLabel,共有变量时间和分数。在update函数里不断改变Text显示的时间值,当游戏时间小于0时,调用游戏结束函数。
24 public class GameControl : MonoBehaviour { 25 …… 26 public TextMeshtimeLabel; 27 public float time = 30.0f; 28 public int score = 0; 29 void Update () { 30 //时间以秒的速度减少,并在timeLabel里显示当前剩余时间(一位小数) 31 time -= Time.deltaTime; 32 timeLabel.text = "Time: " + time.ToString("F1"); 33 34 //当时间耗尽,调用GameOver函数 35 if (time < 0) 36 { 37 GameOver(); 38 } 39 } 40 …… 41 /// <summary> 42 /// 游戏结束函数 43 /// </summary> 44 void GameOver() { 45 time = 0; 46 timeLabel.text = "Time: 0"; 47 48 //将所有延时调用函数全部取消 49 CancelInvoke(); 50 } 51 } GameControl脚本
[3] 在Unity界面添加对象,如图2-17所示。
图2-17 预制体拖入
[4] 分数设置。我们刚才在GameControl脚本中添加一个公有变量score。下面就该思考,在什么地方填写分数的语句呢?答案是在地鼠被点击之后。所以我们应该将分数改变的语句添加到Gophers脚本中的OnMouseDown函数中。
1 public class Gophers : MonoBehaviour { 2 …… 3 void OnMouseDown() 4 { 5 …… 6 //增加分数 7 FindObjectOfType<GameControl>().score += 1; 8 int scores = FindObjectOfType<GameControl>().score; 9 GameObject.Find("Score").gameObject.GetComponent<TextMesh>().text= "Score: " + scores.ToString(); 10 } 11 } Gophers脚本
[5] 重新开始按钮。创建一个按钮,将按钮上的Text值改为Restart,其坐标和大小信息修改如图2-18所示。
图2-18 Restart按钮设置面板
1 public class Gophers : MonoBehaviour { 2 …… 3 public int id; 4 void Update () { 5 Destroy(this.gameObject,3.0f); 6 //将对应洞口的isAppear值设为false 7 FindObjectOfType<GameControl>().holes[id].isAppear = false; 8 } 9 void OnMouseDown() { 10 …… 11 //将当前洞口id传递给beaten 12 g.GetComponent<Beaten>().id = id; 13 } 14 } Gophers 脚本 1 public class Beaten : MonoBehaviour { 2 public int id; 3 4 void Update () { 5 Destroy(gameObject, 0.35f); 6 FindObjectOfType<GameControl>().holes[id].isAppear = false; 7 } 8 } Beaten脚本
[6] 按钮脚本。给Button绑定一个Restart脚本,如图2-19所示,当点击时,将重新加载scene【Game】。
图2-19 脚本放置
1 using UnityEngine; 2 using UnityEditor.SceneManagement; 3 4 public class Restart : MonoBehaviour { 5 /// <summary> 6 /// 按钮被点击以后,重新调用游戏场景 7 /// </summary> 8 public void OnMouseDown() 9 { 10 Debug.Log("restart"); 11 EditorSceneManager.LoadScene("Game"); 12 } 13 } Restart脚本
[7] 添加方法。脚本添加完成后,选择【Button】,在【Inspector】面板的【Button】下找到On Click()部分,点击“+”号,选择对象为“Button”,方法为【Restart】-【OnMouseDown】,如图2-20所示。
图2-20 添加方法
打地鼠游戏到这里就已经完成了,运行游戏后效果如图2-21所示。
图2-21 游戏效果图