Xungerrrr's Blog

Unity 3D - 牧师与魔鬼 2.0

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

Word count: 1.9kReading time: 8 min
2018/04/10 31 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. 操作与总结
  2. 2. 编程实践:牧师与魔鬼 动作分离版