Unity 2017经典游戏开发教程:算法分析与实现
上QQ阅读APP看书,第一时间看更新

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面板中将图片的xy的坐标值定为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】图片拖入场景中,将其xy坐标值设置为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 游戏效果图