unity3d-牧师与恶魔

牧师与恶魔

说明:此次项目对自己难度较大,主要参考了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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using UnityEngine;
using System.Collections;
public class GameSceneController: System.Object {
private static GameSceneController _instance;
private BaseCode _base_code;
public static GameSceneController GetInstance() {
if (null == _instance) {
_instance = new GameSceneController();
}
return _instance;
}
public BaseCode getBaseCode() {
return _base_code;
}
internal void setBaseCode(BaseCode bc) {
if (null == _base_code) {
_base_code = bc;
}
}
}
public class BaseCode : MonoBehaviour {
public string gameName;
void Start () {
GameSceneController my = GameSceneController.GetInstance();
my.setBaseCode(this);
}
}

二、新建脚本 GenGameObjects ,也挂载到 主摄像机

三、在 GenGameObjects 中创建 长方形、正方形、球 及其色彩代表游戏中的对象。并把组件自己注入到 GameSceneController 单例模式对象

先在游戏面板创建游戏对象,在 Assets 文件夹下新建 Resources 文件夹,在 Resources 文件夹下新建 Prefabs 文件夹,然后将创建的对象拖入 Prefabs 中成为预设物体。

设置主摄像机的投影模式为正交投影,调整大小.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class GenGameObject : MonoBehaviour {
// 使用Stack来存储游戏对象,start代表开始岸,end代表目标岸
// priests_start代表在开始岸上的牧师,devils_start代表在开始岸上的恶魔
Stack<GameObject> priests_start = new Stack<GameObject>();
Stack<GameObject> priests_end = new Stack<GameObject>();
Stack<GameObject> devils_start = new Stack<GameObject>();
Stack<GameObject> devils_end = new Stack<GameObject>();
// 使用数组来存储在船上的游戏对象
GameObject[] boat = new GameObject[2];
// 船的实体
GameObject boat_obj;
// 船的速度
public float speed = 100f;
GameSceneController my;
// 坐标
Vector3 shoreStartPos = new Vector3(0, 0, -12);
Vector3 shoreEndPos = new Vector3(0, 0, 12);
Vector3 boatStartPos = new Vector3(0, 0, -4);
Vector3 boatEndPos = new Vector3(0, 0, 4);
float gap = 1.5f;
Vector3 priestStartPos = new Vector3(0, 2.7f, -11f);
Vector3 priestEndPos = new Vector3(0, 2.7f, 8f);
Vector3 devilStartPos = new Vector3(0, 2.7f, -16f);
Vector3 devilEndPos = new Vector3(0, 2.7f, 13f);
void Start () {
// 将 GenGameObject 对象注入了 GameSceneController 单实例对象中
my = GameSceneController.GetInstance();
my.setGenGameObject(this);
loadSrc();
}
// 用来实例化游戏对象
void loadSrc() {
// shore
Instantiate(Resources.Load("Prefabs/Shore"), shoreStartPos, Quaternion.identity);
Instantiate(Resources.Load("Prefabs/Shore"), shoreEndPos, Quaternion.identity);
// boat
boat_obj = Instantiate(Resources.Load("Prefabs/Boat"), boatStartPos, Quaternion.identity) as GameObject;
// priests & devils
for (int i = 0; i < 3; ++i) {
priests_start.Push(Instantiate(Resources.Load("Prefabs/Priest")) as GameObject);
devils_start.Push(Instantiate(Resources.Load("Prefabs/Devil")) as GameObject);
}
}

在GameSceneController加入,补全对应的函数

1
2
3
4
5
6
7
8
9
public GenGameObject getGenGameObject() {
return _gen_game_obj;
}
internal void setGenGameObject(GenGameObject ggo) {
if (null == _gen_game_obj) {
_gen_game_obj = ggo;
}
}

四、用表格列出玩家动作表(规则表)

动作 条件
开船 船在开始岸or船在结束岸
开始岸下船 船靠开始岸且船有人
目标岸下船 船靠目标岸且船有人
开始岸的牧师上船 船在开始岸,船有空位,开始岸有牧师
开始岸的魔鬼上船 船在开始岸,船有空位,开始岸有魔鬼
结束岸的牧师上船 船在结束岸,船有空位,结束岸有牧师
结束岸的魔鬼上船 船在结束岸,船有空位,结束岸有魔鬼

五、基于规则表完善 GenGameObject 类

考虑到牧师和魔鬼的位置时刻要根据堆栈的数据变化,因此先定义一个 setCharacterPositions 函数。该函数接受一个stack参数,和一个Vector3坐标。它的作用就是把stack里的object从Vector3坐标开始依次排开:

1
2
3
4
5
6
void setCharacterPositions(Stack<GameObject> stack, Vector3 pos) {
GameObject[] array = stack.ToArray();
for (int i = 0; i < stack.Count; ++i) {
array[i].transform.position = new Vector3(pos.x, pos.y, pos.z + gap*i);
}
}

现在,我们来考虑规则抽象的行为,分为3种:上船、开船、下船。

1. 上船:把一个游戏对象设为船的子对象。

​ 定义 getOnTheBoat 函数,接受一个游戏对象为参数,只要船上有空位,就把游戏对象设置为船的子对象,这样游戏对象便能跟着船移动:

1
2
3
4
5
6
7
8
9
10
11
12
void getOnTheBoat(GameObject obj) {
if (boatCapacity() != 0) {
obj.transform.parent = boat_obj.transform;
if (boat[0] == null) {
boat[0] = obj;
obj.transform.localPosition = new Vector3(0, 1.2f, -0.3f);
} else {
boat[1] = obj;
obj.transform.localPosition = new Vector3(0, 1.2f, 0.3f);
}
}
}

2. 开船:根据游戏“状态”,把船从一方移动到另一方。

​ 这里,我们讨论到了游戏状态,我们需要游戏状态了解船当前的位置。游戏状态作为枚举类型声明在 BaseCode 脚本中:

1
2
3
4
5
6
7
8
9
public enum State { BSTART, BSEMOVING, BESMOVING, BEND, WIN, LOSE };
/*
* BSTART: boat stops on start shore
* BEND: boat stops on end shore
* BSEMOVING: boat is moving from start shore to end shore
* BESMOVING: boat is moving from end shore to start shore
* WIN: win
* LOSE: lose
*/

有了游戏状态,只需要定义一个 moveBoat 函数,修改游戏状态为MOVING即可,剩下的动作均在Update函数中完成

1
2
3
4
5
6
7
8
9
10
public void moveBoat() {
if (boatCapacity() != 2) {
if (my.state == State.BSTART) {
my.state = State.BSEMOVING;
}
else if (my.state == State.BEND) {
my.state = State.BESMOVING;
}
}
}

3. 下船:取消船和游戏对象的父子关系,并且根据游戏“状态”将游戏对象压入stack。

​ 定义 getOffTheBoat 函数,接受一个整型变量为参数,该变量可以为0或1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public void getOffTheBoat(int side) {
if (boat[side] != null) {
boat[side].transform.parent = null;
if (my.state == State.BEND) {
if (boat[side].tag == "Priest") {
priests_end.Push(boat[side]);
}
else if (boat[side].tag == "Devil") {
devils_end.Push(boat[side]);
}
}
else if (my.state == State.BSTART) {
if (boat[side].tag == "Priest") {
priests_start.Push(boat[side]);
}
else if (boat[side].tag == "Devil") {
devils_start.Push(boat[side]);
}
}
boat[side] = null;
}
}

注意到,为了区分出牧师和魔鬼,我给牧师和魔鬼预设分别添加了Tag。Tag需要在控制面板添加。

除此以外,还需要判断游戏的输赢,定义一个 check 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void check() {
int pOnb = 0, dOnb = 0;
int priests_s = 0, devils_s = 0, priests_e = 0, devils_e = 0;
if (priests_end.Count == 3 && devils_end.Count == 3) {
my.state = State.WIN;
return;
}
for (int i = 0; i < 2; ++i) {
if (boat[i] != null && boat[i].tag == "Priest") pOnb++;
else if (boat[i] != null && boat[i].tag == "Devil") dOnb++;
}
if (my.state == State.BSTART) {
priests_s = priests_start.Count + pOnb;
devils_s = devils_start.Count + dOnb;
priests_e = priests_end.Count;
devils_e = devils_end.Count;
}
else if (my.state == State.BEND) {
priests_s = priests_start.Count;
devils_s = devils_start.Count;
priests_e = priests_end.Count + pOnb;
devils_e = devils_end.Count + dOnb;
}
if ((priests_s != 0 && priests_s < devils_s) || (priests_e != 0 && priests_e < devils_e)) {
my.state = State.LOSE;
}
}

修改 Update 函数,加入船的移动,游戏结束条件的判断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void Update() {
setCharacterPositions(priests_start, priestStartPos);
setCharacterPositions(priests_end, priestEndPos);
setCharacterPositions(devils_start, devilStartPos);
setCharacterPositions(devils_end, devilEndPos);
if (my.state == State.BSEMOVING) {
boat_obj.transform.position = Vector3.MoveTowards(boat_obj.transform.position, boatEndPos, speed*Time.deltaTime);
if (boat_obj.transform.position == boatEndPos) {
my.state = State.BEND;
}
}
else if (my.state == State.BESMOVING) {
boat_obj.transform.position = Vector3.MoveTowards(boat_obj.transform.position, boatStartPos, speed*Time.deltaTime);
if (boat_obj.transform.position == boatStartPos) {
my.state = State.BSTART;
}
}
else check();
}

不过,为了与玩家规则表对应,还需要定义4个函数:

​ priestStartOnBoat、priestEndOnBoat、devilStartOnBoat、devilEndOnBoat

​ 它们的作用是调用相应的 getOnTheBoat 函数,把玩家指定的对象放到船上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void priestStartOnBoat() {
if (priests_start.Count != 0 && boatCapacity() != 0 && my.state == State.BSTART)
getOnTheBoat(priests_start.Pop());
}
public void priestEndOnBoat() {
if (priests_end.Count != 0 && boatCapacity() != 0 && my.state == State.BEND)
getOnTheBoat(priests_end.Pop());
}
public void devilStartOnBoat() {
if (devils_start.Count != 0 && boatCapacity() != 0 && my.state == State.BSTART)
getOnTheBoat(devils_start.Pop());
}
public void devilEndOnBoat() {
if (devils_end.Count != 0 && boatCapacity() != 0 && my.state == State.BEND)
getOnTheBoat(devils_end.Pop());
}

六、修改 BaseCode 脚本,添加 IUserActions 接口,并在 GameSceneController 中实现。

回到 BaseCode 脚本

1
2
3
4
5
6
7
8
9
10
public interface IUserActions {
void priestSOnB();
void priestEOnB();
void devilSOnB();
void devilEOnB();
void moveBoat();
void offBoatL();
void offBoatR();
void restart();
}

在 GameSceneController 中添加接口的实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public void priestSOnB() {
_gen_game_obj.priestStartOnBoat();
}
public void priestEOnB() {
_gen_game_obj.priestEndOnBoat();
}
public void devilSOnB() {
_gen_game_obj.devilStartOnBoat();
}
public void devilEOnB() {
_gen_game_obj.devilEndOnBoat();
}
public void moveBoat() {
_gen_game_obj.moveBoat();
}
public void offBoatL() {
_gen_game_obj.getOffTheBoat(0);
}
public void offBoatR() {
_gen_game_obj.getOffTheBoat(1);
}
public void restart() {
Application.LoadLevel(Application.loadedLevelName);
state = State.BSTART;

七、修改 UserInterface 脚本,创建GUI对象,处理GUI事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
if (GUI.Button(new Rect(castw(2f), casth(6f), width, height), "Go")) {
action.moveBoat();
}
if (GUI.Button(new Rect(castw(10.5f), casth(4f), width, height), "On")) {
action.devilSOnB();
}
if (GUI.Button(new Rect(castw(4.3f), casth(4f), width, height), "On")) {
action.priestSOnB();
}
if (GUI.Button(new Rect(castw(1.1f), casth(4f), width, height), "On")) {
action.devilEOnB();
}
if (GUI.Button(new Rect(castw(1.3f), casth(4f), width, height), "On")) {
action.priestEOnB();
}
if (GUI.Button(new Rect(castw(2.5f), casth(1.3f), width, height), "Off")) {
action.offBoatL();
}
if (GUI.Button(new Rect(castw(1.7f), casth(1.3f), width, height), "Off")) {
action.offBoatR();
}

整个游戏代码中没有出现 Find、SendMessage 等破坏程序结构的通讯耦合语句,场景中除了主摄像机和一个 Empty 对象外,所有其他的游戏对象都由代码动态生成。

创建一个 Empty 对象或者使用Camera对象,用它挂载 三个脚本 代码。工作就完成了

成品图

参考

Unity3D学习笔记(4)—— 牧师和魔鬼游戏

您的支持将鼓励我继续创作!