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
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对象,用它挂载 三个脚本 代码。工作就完成了

成品图

参考

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

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