Xungerrrr's Blog

Unity 3D - 牧师与魔鬼 2.0

3D 游戏编程与设计 第五周作业

Word count: 1.9kReading time: 8 min
2018/04/10 Share

操作与总结

参考 Fantasy Skybox FREE 构建自己的游戏场景

利用 Skybox 和 Terrain 构建游戏场景。Unity 的 Terrain 有十分方便的地形设计工具,利用它可以轻松绘制自己的地形及素材。地形工具和场景效果如下:

总结游戏对象的使用

常用游戏对象

  • 空对象 (Empty):不显示的游戏对象,一般用于挂载管理器等脚本。
  • 摄像机 (Camera):游戏显示画面的来源。创建多个摄像机可以展现多个游戏视角。
  • 3D 物体 (3D Object):游戏中的三维物体,由三角面拼接而成。
  • 2D 物体 (2D Object):游戏中的二维物体。
  • 光线 (Light):游戏画面表现的灵魂所在,用于营造游戏场景的光线效果。
  • 声音 (Audio):游戏中的声音素材,例如背景音乐。
  • 视频 (Video):游戏中的视频素材。
  • UI:游戏中用户的交互接口,如按钮。

游戏对象的使用方法

游戏对象有不同的创建方法,可以在游戏运行前创建,可以在游戏运行过程中动态创建,还可以使用预设创建对象。创建位置变化不大、数量较少的对象可以采用前两种方法,而创建大量重复的、类似的对象则采用预设方法较好。

游戏对象有各种各样的组件 (Component),组件决定了游戏对象在游戏中的表现形式,包括外观和动作。使用组件时,应该仔细查阅文档,了解组件各个属性的功能和用法,再根据游戏的设计,实现游戏对象的各个需求。

编程实践:牧师与魔鬼 动作分离版

基础版本:牧师与魔鬼基础 MVC 版

在上一个版本中,我实现了该游戏的基本 MVC 架构,但是,场记和控制器负责的功能太多,导致代码耦合性太强。这一版本将游戏对象的基本动作抽离出来,并引入动作管理器来管理动作,更好地实现了模块间的松耦合,提高了代码的复用性。

实现思路

实现动作基类

在动作基类里面,要声明游戏对象的动作的基本属性和方法。在这个游戏中,游戏对象只有直线运动,因此动作基类只需包含一个用于指定对象的 GameObject 属性和一个用于指定运动路径的 Transform 属性。虚函数 Start 和 Update 是实现动作的基本方法,需要由子类重载。动作基类的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class SSAction : ScriptableObject {
public bool enable = true;
public bool destroy = false;

public GameObject GameObject { get; set; }
public Transform Transform { get; set; }
public ISSActionCallback Callback { get; set; }

protected SSAction() { }

// Use this for initialization
public virtual void Start() {
throw new System.NotImplementedException();
}

// Update is called once per frame
public virtual void Update() {
throw new System.NotImplementedException();
}
}

实现直线运动

定义 SSMoveToAction 类,并继承动作基类,来实现对象的直线运动。具体实现如下:

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
public class SSMoveToAction : SSAction {
public Vector3 target; // 移动目标
public float speed; // 移动速度

// 创建并返回动作的实例
public static SSMoveToAction GetSSMoveToAction(Vector3 target, float speed) {
SSMoveToAction action = ScriptableObject.CreateInstance<SSMoveToAction>();
action.target = target;
action.speed = speed;
return action;
}

// Use this for initialization
public override void Start() {}

// 在 Update 函数中用 Vector3.MoveTowards 实现直线运动
public override void Update() {
this.Transform.position = Vector3.MoveTowards(this.Transform.position, target, speed * Time.deltaTime);
if (this.Transform.position == target) {
this.destroy = true;
// 完成动作后进行动作回掉
this.Callback.SSActionEvent(this);
}
}
}

实现动作序列

在游戏中,船的动作是简单的直线运动,而游戏角色的运动是由两段直线运动组成的折线运动。通过定义 SSSequenceAction 类,将基本动作(直线运动)组合在一起,可以实现折现运动。在这个类中,用 List 列表记录待执行的动作序列,并依次执行,执行完毕后清空列表,从而完成动作的组合(其中可以自定义循环的次数)。具体实现如下:

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
46
47
48
49
50
51
52
53
54
public class SSSequenceAction : SSAction, ISSActionCallback {
public List<SSAction> sequence; // 动作队列
public int repeat = -1; // 循环次数,-1表示无限循环
public int start = 0; // 当前执行的动作

// 创建并返回动作序列的实例
public static SSSequenceAction GetSSSequenceAction(int repeat, int start, List<SSAction> sequence) {
SSSequenceAction action = ScriptableObject.CreateInstance<SSSequenceAction>();
action.repeat = repeat;
action.sequence = sequence;
action.start = start;
return action;
}

// Use this for initialization
public override void Start() {
foreach (SSAction action in sequence) {
action.GameObject = this.GameObject;
action.Transform = this.Transform;
action.Callback = this;
action.Start();
}
}

// 在 Update 中执行当前动作
public override void Update() {
if (sequence.Count == 0) return;
if (start < sequence.Count) {
sequence[start].Update();
}
}

// 执行完毕后销毁动作
void OnDestroy() {
foreach (SSAction action in sequence) {
DestroyObject(action);
}
}

// 更新当前执行的动作
public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed,
int intParam = 0, string strParam = null, object objectParam = null) {
source.destroy = false;
this.start++;
if (this.start >= sequence.Count) {
this.start = 0;
if (repeat > 0) repeat--;
if (repeat == 0) {
this.destroy = true;
this.Callback.SSActionEvent(this);
}
}
}
}

实现动作管理

在定义基本动作和动作序列之后,需要有一个基础动作管理器来管理要执行的动作,它继承自 MonoBehaviour,以便实现对游戏对象动作的控制。以下是 SSActionManager 类的定义:

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
public class SSActionManager: MonoBehaviour {
private Dictionary<int, SSAction> actions = new Dictionary<int, SSAction>();
private List<SSAction> waitingAdd = new List<SSAction>();
private List<int> waitingDelete = new List<int>();

protected void Update() {
foreach (SSAction action in waitingAdd) {
actions[action.GetInstanceID()] = action;
}
waitingAdd.Clear();

foreach (KeyValuePair<int, SSAction> KeyValue in actions) {
SSAction action = KeyValue.Value;
if (action.destroy) {
// release action
waitingDelete.Add(action.GetInstanceID());
}
else if (action.enable) {
// update action
action.Update();
}
}

foreach (int key in waitingDelete) {
SSAction action = actions[key];
actions.Remove(key);
DestroyObject(action);
}
waitingDelete.Clear();
}

// 执行动作
public void RunAction(GameObject gameObject, SSAction action, ISSActionCallback callback) {
action.GameObject = gameObject;
action.Transform = gameObject.transform;
action.Callback = callback;
waitingAdd.Add(action);
action.Start();
}
}

修改原有的游戏动作

分离好动作后,要修改原来的动作执行方式。首先定义一个具体的动作管理者,继承自上述的基础动作管理者,来管理第一个游戏场景的动作(本游戏仅有一个场景)。

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
public class FirstActionManager : SSActionManager, ISSActionCallback {
// 移动船
public void MoveBoat(BoatController boatController) {
SSMoveToAction action = SSMoveToAction.GetSSMoveToAction(boatController.getDestination(), 20);
RunAction(boatController.boat, action, this);
}

// 移动角色
public void MoveCharacter(MyCharacterController myCharacterController, Vector3 destination) {
Vector3 current = myCharacterController.character.transform.position;
Vector3 middle = destination; // 利用 middle 实现折线运动
if (destination.y < current.y) {
middle.y = current.y;
} else {
middle.x = current.x;
}
SSAction firstMove = SSMoveToAction.GetSSMoveToAction(middle, 20);
SSAction secondMove = SSMoveToAction.GetSSMoveToAction(destination, 20);
SSAction sequenceAction = SSSequenceAction.GetSSSequenceAction(1, 0, new List<SSAction> { firstMove, secondMove });
RunAction(myCharacterController.character, sequenceAction, this);
}

public void SSActionEvent(SSAction source, SSActionEventType events = SSActionEventType.Completed,
int intParam = 0, string strParam = null, object objectParam = null) {}
}

接着修改 FirstController 中的动作内容,利用 FirstActionManager 来调用动作的执行。

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
46
47
48
public class FirstController : MonoBehaviour, SceneController, IUserAction {
private FirstActionManager actionManager;
void Start() {
actionManager = GetComponent<FirstActionManager>();
}
//...
public void moveBoat() {
if (boat.isEmpty ())
return;
actionManager.MoveBoat(boat); // 移动船
boat.ChangePosition(); // 修改船的位置属性
userGUI.status = check_game_over ();
}

public void characterIsClicked(MyCharacterController characterCtrl) {
if (characterCtrl.isOnBoat ()) {
ShoreController whichShore;
if (boat.get_to_or_from () == -1) { // to->-1; from->1
whichShore = toShore;
} else {
whichShore = fromShore;
}

boat.GetOffBoat (characterCtrl.getName());
// 移动角色
actionManager.MoveCharacter(characterCtrl, whichShore.getEmptyPosition());
characterCtrl.getOnShore (whichShore);
whichShore.getOnShore (characterCtrl);
} else { // character on shore
ShoreController whichShore = characterCtrl.getShoreController ();

if (boat.getEmptyIndex () == -1) { // boat is full
return;
}

if (whichShore.get_to_or_from () != boat.get_to_or_from ()) // boat is not on the side of character
return;

whichShore.getOffShore(characterCtrl.getName());
// 移动角色
actionManager.MoveCharacter(characterCtrl, boat.getEmptyPosition());
characterCtrl.getOnBoat (boat);
boat.GetOnBoat (characterCtrl);
}
userGUI.status = check_game_over ();
}
//...
}

至此,动作分离全部完成。

第二版游戏美化

在游戏中,加入了天空盒和水环境,使得整体界面比上一版更加美观,立体感更强。

游戏预览视频及完整项目见 GitHub.

返回 Unity 3D Learning
CATALOG
  1. 1. 操作与总结
    1. 1.1. 参考 Fantasy Skybox FREE 构建自己的游戏场景
    2. 1.2. 总结游戏对象的使用
      1. 1.2.1. 常用游戏对象
      2. 1.2.2. 游戏对象的使用方法
  2. 2. 编程实践:牧师与魔鬼 动作分离版
    1. 2.1. 实现思路
      1. 2.1.1. 实现动作基类
      2. 2.1.2. 实现直线运动
      3. 2.1.3. 实现动作序列
      4. 2.1.4. 实现动作管理
      5. 2.1.5. 修改原有的游戏动作
    2. 2.2. 第二版游戏美化