牧师与恶魔
说明:此次项目对自己难度较大,主要参考了Unity3D学习笔记(4)—— 牧师和魔鬼游戏.进行实践。添加注释理解,并进行改进分析。完成项目
分析
游戏对象
游戏角色:3个牧师、3个魔鬼
游戏场景:2个河岸、1艘小船
游戏框架
- UserInterface 用户交互接口,处理GUI事件,使用 IUserActions 接口控制游戏。
- SceneController是单实例对象,用来处理对象间通信和实现 IUserActions 接口。
- BaseCode 用来保存游戏基本信息,它注册在 SceneController 中。
GenGameObject用来创建游戏对象和处理对象运动,它也注册在 SceneController 中。IUserActions 接口通过与 GenGameObject 互动完成游戏逻辑。
采用了MVC框架,使用单例对象可以实现对象之间的通信。无须再使用Find,SendMessage等消耗资源又耗时间的函数。
步骤
一、新建一个 3d 游戏项目,建立 BaseCode 脚本,并挂载到 主摄像机。
下面是目前该步骤的BaseCode代码,GameSceneController用来生成单实例。
1 | using UnityEngine; |
2 | using System.Collections; |
3 | |
4 | public class GameSceneController: System.Object { |
5 | |
6 | private static GameSceneController _instance; |
7 | private BaseCode _base_code; |
8 | |
9 | public static GameSceneController GetInstance() { |
10 | if (null == _instance) { |
11 | _instance = new GameSceneController(); |
12 | } |
13 | return _instance; |
14 | } |
15 | |
16 | public BaseCode getBaseCode() { |
17 | return _base_code; |
18 | } |
19 | |
20 | internal void setBaseCode(BaseCode bc) { |
21 | if (null == _base_code) { |
22 | _base_code = bc; |
23 | } |
24 | } |
25 | } |
26 | |
27 | public class BaseCode : MonoBehaviour { |
28 | |
29 | public string gameName; |
30 | |
31 | void Start () { |
32 | GameSceneController my = GameSceneController.GetInstance(); |
33 | my.setBaseCode(this); |
34 | } |
35 | } |
二、新建脚本 GenGameObjects ,也挂载到 主摄像机
三、在 GenGameObjects 中创建 长方形、正方形、球 及其色彩代表游戏中的对象。并把组件自己注入到 GameSceneController 单例模式对象
先在游戏面板创建游戏对象,在 Assets 文件夹下新建 Resources 文件夹,在 Resources 文件夹下新建 Prefabs 文件夹,然后将创建的对象拖入 Prefabs 中成为预设物体。
设置主摄像机的投影模式为正交投影,调整大小.
1 | public class GenGameObject : MonoBehaviour { |
2 | // 使用Stack来存储游戏对象,start代表开始岸,end代表目标岸 |
3 | // priests_start代表在开始岸上的牧师,devils_start代表在开始岸上的恶魔 |
4 | Stack<GameObject> priests_start = new Stack<GameObject>(); |
5 | Stack<GameObject> priests_end = new Stack<GameObject>(); |
6 | Stack<GameObject> devils_start = new Stack<GameObject>(); |
7 | Stack<GameObject> devils_end = new Stack<GameObject>(); |
8 | // 使用数组来存储在船上的游戏对象 |
9 | GameObject[] boat = new GameObject[2]; |
10 | // 船的实体 |
11 | GameObject boat_obj; |
12 | // 船的速度 |
13 | public float speed = 100f; |
14 | |
15 | GameSceneController my; |
16 | // 坐标 |
17 | Vector3 shoreStartPos = new Vector3(0, 0, -12); |
18 | Vector3 shoreEndPos = new Vector3(0, 0, 12); |
19 | Vector3 boatStartPos = new Vector3(0, 0, -4); |
20 | Vector3 boatEndPos = new Vector3(0, 0, 4); |
21 | |
22 | float gap = 1.5f; |
23 | Vector3 priestStartPos = new Vector3(0, 2.7f, -11f); |
24 | Vector3 priestEndPos = new Vector3(0, 2.7f, 8f); |
25 | Vector3 devilStartPos = new Vector3(0, 2.7f, -16f); |
26 | Vector3 devilEndPos = new Vector3(0, 2.7f, 13f); |
27 | void Start () { |
28 | // 将 GenGameObject 对象注入了 GameSceneController 单实例对象中 |
29 | my = GameSceneController.GetInstance(); |
30 | my.setGenGameObject(this); |
31 | loadSrc(); |
32 | } |
33 | // 用来实例化游戏对象 |
34 | void loadSrc() { |
35 | // shore |
36 | Instantiate(Resources.Load("Prefabs/Shore"), shoreStartPos, Quaternion.identity); |
37 | Instantiate(Resources.Load("Prefabs/Shore"), shoreEndPos, Quaternion.identity); |
38 | // boat |
39 | boat_obj = Instantiate(Resources.Load("Prefabs/Boat"), boatStartPos, Quaternion.identity) as GameObject; |
40 | // priests & devils |
41 | for (int i = 0; i < 3; ++i) { |
42 | priests_start.Push(Instantiate(Resources.Load("Prefabs/Priest")) as GameObject); |
43 | devils_start.Push(Instantiate(Resources.Load("Prefabs/Devil")) as GameObject); |
44 | } |
45 | } |
在GameSceneController加入,补全对应的函数
1 | public GenGameObject getGenGameObject() { |
2 | return _gen_game_obj; |
3 | } |
4 | |
5 | internal void setGenGameObject(GenGameObject ggo) { |
6 | if (null == _gen_game_obj) { |
7 | _gen_game_obj = ggo; |
8 | } |
9 | } |
四、用表格列出玩家动作表(规则表)
动作 | 条件 |
---|---|
开船 | 船在开始岸or船在结束岸 |
开始岸下船 | 船靠开始岸且船有人 |
目标岸下船 | 船靠目标岸且船有人 |
开始岸的牧师上船 | 船在开始岸,船有空位,开始岸有牧师 |
开始岸的魔鬼上船 | 船在开始岸,船有空位,开始岸有魔鬼 |
结束岸的牧师上船 | 船在结束岸,船有空位,结束岸有牧师 |
结束岸的魔鬼上船 | 船在结束岸,船有空位,结束岸有魔鬼 |
五、基于规则表完善 GenGameObject 类 |
考虑到牧师和魔鬼的位置时刻要根据堆栈的数据变化,因此先定义一个 setCharacterPositions 函数。该函数接受一个stack参数,和一个Vector3坐标。它的作用就是把stack里的object从Vector3坐标开始依次排开:
1 | void setCharacterPositions(Stack<GameObject> stack, Vector3 pos) { |
2 | GameObject[] array = stack.ToArray(); |
3 | for (int i = 0; i < stack.Count; ++i) { |
4 | array[i].transform.position = new Vector3(pos.x, pos.y, pos.z + gap*i); |
5 | } |
6 | } |
现在,我们来考虑规则抽象的行为,分为3种:上船、开船、下船。
1. 上船:把一个游戏对象设为船的子对象。
定义 getOnTheBoat 函数,接受一个游戏对象为参数,只要船上有空位,就把游戏对象设置为船的子对象,这样游戏对象便能跟着船移动:
1 | void getOnTheBoat(GameObject obj) { |
2 | if (boatCapacity() != 0) { |
3 | obj.transform.parent = boat_obj.transform; |
4 | if (boat[0] == null) { |
5 | boat[0] = obj; |
6 | obj.transform.localPosition = new Vector3(0, 1.2f, -0.3f); |
7 | } else { |
8 | boat[1] = obj; |
9 | obj.transform.localPosition = new Vector3(0, 1.2f, 0.3f); |
10 | } |
11 | } |
12 | } |
2. 开船:根据游戏“状态”,把船从一方移动到另一方。
这里,我们讨论到了游戏状态,我们需要游戏状态了解船当前的位置。游戏状态作为枚举类型声明在 BaseCode 脚本中:
1 | public enum State { BSTART, BSEMOVING, BESMOVING, BEND, WIN, LOSE }; |
2 | /* |
3 | * BSTART: boat stops on start shore |
4 | * BEND: boat stops on end shore |
5 | * BSEMOVING: boat is moving from start shore to end shore |
6 | * BESMOVING: boat is moving from end shore to start shore |
7 | * WIN: win |
8 | * LOSE: lose |
9 | */ |
有了游戏状态,只需要定义一个 moveBoat 函数,修改游戏状态为MOVING即可,剩下的动作均在Update函数中完成
1 | public void moveBoat() { |
2 | if (boatCapacity() != 2) { |
3 | if (my.state == State.BSTART) { |
4 | my.state = State.BSEMOVING; |
5 | } |
6 | else if (my.state == State.BEND) { |
7 | my.state = State.BESMOVING; |
8 | } |
9 | } |
10 | } |
3. 下船:取消船和游戏对象的父子关系,并且根据游戏“状态”将游戏对象压入stack。
定义 getOffTheBoat 函数,接受一个整型变量为参数,该变量可以为0或1:
1 | public void getOffTheBoat(int side) { |
2 | if (boat[side] != null) { |
3 | boat[side].transform.parent = null; |
4 | if (my.state == State.BEND) { |
5 | if (boat[side].tag == "Priest") { |
6 | priests_end.Push(boat[side]); |
7 | } |
8 | else if (boat[side].tag == "Devil") { |
9 | devils_end.Push(boat[side]); |
10 | } |
11 | } |
12 | else if (my.state == State.BSTART) { |
13 | if (boat[side].tag == "Priest") { |
14 | priests_start.Push(boat[side]); |
15 | } |
16 | else if (boat[side].tag == "Devil") { |
17 | devils_start.Push(boat[side]); |
18 | } |
19 | } |
20 | boat[side] = null; |
21 | } |
22 | } |
注意到,为了区分出牧师和魔鬼,我给牧师和魔鬼预设分别添加了Tag。Tag需要在控制面板添加。
除此以外,还需要判断游戏的输赢,定义一个 check 函数:
1 | void check() { |
2 | int pOnb = 0, dOnb = 0; |
3 | int priests_s = 0, devils_s = 0, priests_e = 0, devils_e = 0; |
4 | |
5 | if (priests_end.Count == 3 && devils_end.Count == 3) { |
6 | my.state = State.WIN; |
7 | return; |
8 | } |
9 | |
10 | for (int i = 0; i < 2; ++i) { |
11 | if (boat[i] != null && boat[i].tag == "Priest") pOnb++; |
12 | else if (boat[i] != null && boat[i].tag == "Devil") dOnb++; |
13 | } |
14 | if (my.state == State.BSTART) { |
15 | priests_s = priests_start.Count + pOnb; |
16 | devils_s = devils_start.Count + dOnb; |
17 | priests_e = priests_end.Count; |
18 | devils_e = devils_end.Count; |
19 | } |
20 | else if (my.state == State.BEND) { |
21 | priests_s = priests_start.Count; |
22 | devils_s = devils_start.Count; |
23 | priests_e = priests_end.Count + pOnb; |
24 | devils_e = devils_end.Count + dOnb; |
25 | } |
26 | if ((priests_s != 0 && priests_s < devils_s) || (priests_e != 0 && priests_e < devils_e)) { |
27 | my.state = State.LOSE; |
28 | } |
29 | } |
修改 Update 函数,加入船的移动,游戏结束条件的判断:
1 | void Update() { |
2 | setCharacterPositions(priests_start, priestStartPos); |
3 | setCharacterPositions(priests_end, priestEndPos); |
4 | setCharacterPositions(devils_start, devilStartPos); |
5 | setCharacterPositions(devils_end, devilEndPos); |
6 | |
7 | if (my.state == State.BSEMOVING) { |
8 | boat_obj.transform.position = Vector3.MoveTowards(boat_obj.transform.position, boatEndPos, speed*Time.deltaTime); |
9 | if (boat_obj.transform.position == boatEndPos) { |
10 | my.state = State.BEND; |
11 | } |
12 | } |
13 | else if (my.state == State.BESMOVING) { |
14 | boat_obj.transform.position = Vector3.MoveTowards(boat_obj.transform.position, boatStartPos, speed*Time.deltaTime); |
15 | if (boat_obj.transform.position == boatStartPos) { |
16 | my.state = State.BSTART; |
17 | } |
18 | } |
19 | else check(); |
20 | } |
不过,为了与玩家规则表对应,还需要定义4个函数:
priestStartOnBoat、priestEndOnBoat、devilStartOnBoat、devilEndOnBoat
它们的作用是调用相应的 getOnTheBoat 函数,把玩家指定的对象放到船上:
1 | public void priestStartOnBoat() { |
2 | if (priests_start.Count != 0 && boatCapacity() != 0 && my.state == State.BSTART) |
3 | getOnTheBoat(priests_start.Pop()); |
4 | } |
5 | |
6 | public void priestEndOnBoat() { |
7 | if (priests_end.Count != 0 && boatCapacity() != 0 && my.state == State.BEND) |
8 | getOnTheBoat(priests_end.Pop()); |
9 | } |
10 | |
11 | public void devilStartOnBoat() { |
12 | if (devils_start.Count != 0 && boatCapacity() != 0 && my.state == State.BSTART) |
13 | getOnTheBoat(devils_start.Pop()); |
14 | } |
15 | |
16 | public void devilEndOnBoat() { |
17 | if (devils_end.Count != 0 && boatCapacity() != 0 && my.state == State.BEND) |
18 | getOnTheBoat(devils_end.Pop()); |
19 | } |
六、修改 BaseCode 脚本,添加 IUserActions 接口,并在 GameSceneController 中实现。
回到 BaseCode 脚本
1 | public interface IUserActions { |
2 | void priestSOnB(); |
3 | void priestEOnB(); |
4 | void devilSOnB(); |
5 | void devilEOnB(); |
6 | void moveBoat(); |
7 | void offBoatL(); |
8 | void offBoatR(); |
9 | void restart(); |
10 | } |
在 GameSceneController 中添加接口的实现方法:
1 | public void priestSOnB() { |
2 | _gen_game_obj.priestStartOnBoat(); |
3 | } |
4 | |
5 | public void priestEOnB() { |
6 | _gen_game_obj.priestEndOnBoat(); |
7 | } |
8 | |
9 | public void devilSOnB() { |
10 | _gen_game_obj.devilStartOnBoat(); |
11 | } |
12 | |
13 | public void devilEOnB() { |
14 | _gen_game_obj.devilEndOnBoat(); |
15 | } |
16 | |
17 | public void moveBoat() { |
18 | _gen_game_obj.moveBoat(); |
19 | } |
20 | |
21 | public void offBoatL() { |
22 | _gen_game_obj.getOffTheBoat(0); |
23 | } |
24 | |
25 | public void offBoatR() { |
26 | _gen_game_obj.getOffTheBoat(1); |
27 | } |
28 | |
29 | public void restart() { |
30 | Application.LoadLevel(Application.loadedLevelName); |
31 | state = State.BSTART; |
七、修改 UserInterface 脚本,创建GUI对象,处理GUI事件。
1 | if (GUI.Button(new Rect(castw(2f), casth(6f), width, height), "Go")) { |
2 | action.moveBoat(); |
3 | } |
4 | if (GUI.Button(new Rect(castw(10.5f), casth(4f), width, height), "On")) { |
5 | action.devilSOnB(); |
6 | } |
7 | if (GUI.Button(new Rect(castw(4.3f), casth(4f), width, height), "On")) { |
8 | action.priestSOnB(); |
9 | } |
10 | if (GUI.Button(new Rect(castw(1.1f), casth(4f), width, height), "On")) { |
11 | action.devilEOnB(); |
12 | } |
13 | if (GUI.Button(new Rect(castw(1.3f), casth(4f), width, height), "On")) { |
14 | action.priestEOnB(); |
15 | } |
16 | if (GUI.Button(new Rect(castw(2.5f), casth(1.3f), width, height), "Off")) { |
17 | action.offBoatL(); |
18 | } |
19 | if (GUI.Button(new Rect(castw(1.7f), casth(1.3f), width, height), "Off")) { |
20 | action.offBoatR(); |
21 | } |
整个游戏代码中没有出现 Find、SendMessage 等破坏程序结构的通讯耦合语句,场景中除了主摄像机和一个 Empty 对象外,所有其他的游戏对象都由代码动态生成。
创建一个 Empty 对象或者使用Camera对象,用它挂载 三个脚本 代码。工作就完成了