CUFMH2Blog

3D Game 2 - 离散仿真引擎基础

Word count: 2.9k Reading time: 12 min
2019-09-10 calculating Share

You can’t connect the dots looking forward; you can only connect them looking backwards
— Steve Jobs, Stanford Report, June 14, 2005

游戏引擎

游戏引擎是一组游戏运行部件以及软件工具的集合。随着技术进步,多数现代游戏引擎都包含以下部件,游戏引擎架构如图所示:

一般,游戏引擎分为两个层次:

  • 游戏内容层:一组工具管理游戏需要的数据
  • 游戏引擎层:一组游戏运行部件,支撑游戏的运行与人机交互

而对于 Unity 3D 离散引擎,其核心内容为,游戏循环遍历所有游戏对象的所有部件,驱动游戏运行。

作业与练习

1 简答题

Question 1.1

解释 游戏对象(GameObjects) 和 资源(Assets)的区别与联系。

游戏对象(GameObjects):是对象的子类,是有些设计中绝大部分实例的基类,也可以说游戏对象是一种容器。它们是空盒,可以容纳各类组件 Component,游戏中的每个对象都是一个游戏对象。

资源(Assets):在游戏设计中可以被游戏对象所使用的游戏素材,资源也可以被实例化为游戏对象。

综上,游戏对象可以使用资源,资源也可以被预设实例化为游戏对象。

Question 1.2

下载几个游戏案例,分别总结资源、对象组织的结构(指资源的目录组织结构与游戏对象树的层次结构)

下载了几个游戏案例,下图由ChinesePoker举例说明,其资源(Assets)组织结构如下:

其中,Images 是图像文件,Prefabs 是预储存文件,Resours 是动态加载的资源文件,Scences 是场景文件,Scripts 是脚本代码文件。可知,资源的组织结构一般是根据文件种类、以文件夹的形式来进行构建的。更一般地,资源种类可以大致分为:模型、动画、材质、着色器、纹理、音频、脚本等类型。

对于游戏对象(GameObjects),其组织结构由具体游戏设计而定,一般有游戏玩家、环境、其他玩家(如AI)、摄像机等虚拟父类。

Question 1.3

编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件

  • 基本行为包括 Awake() Start() Update() FixedUpdate() LateUpdate()
  • 常用事件包括 OnGUI() OnDisable() OnEnable()

编写的代码内容如下:

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
// Awake() Start() Update() FixedUpdate() LateUpdate()
void Awake()
{
Debug.Log("awake");
}

void Start()
{
Debug.Log("start");
}

void FixedUpdate()
{
Debug.Log("fixedUpdate");
}

void Update()
{
Debug.Log("update");
}

void LateUpdate()
{
Debug.Log("lateUpdate");
}

// OnGUI() OnDisable() OnEnable()
void OnGUI()
{
Debug.Log("onGUI");
}

void OnEnable()
{
Debug.Log("onEnable");
}

void OnDisable()
{
Debug.Log("onDisable");
}
}

验证结果如下:

Question 1.4

查找脚本手册,了解 GameObject,Transform,Component 对象

  • 分别翻译官方对三个对象的描述(Description)
  • 描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件
    • 本题目要求是把可视化图形编程界面与 Unity API 对应起来,当你在 Inspector 面板上每一个内容,应该知道对应 API。
    • 例如:table 的对象是 GameObject,第一个选择框是 activeSelf 属性。
  • 用 UML 图描述 三者的关系(请使用 UMLet 14.1.1 stand-alone版本出图)

Question 1.4.1

根据脚本手册,有如下翻译:

GameObject: Base class for all entities in Unity Scenes.

游戏对象是 Unity 场景中所使用的所有实体的基类。

Transform: Position, rotation and scale of an object. Every object in a Scene has a Transform. It’s used to store and manipulate the position, rotation and scale of the object.

变换决定了场景中每个对象的位置、旋转和比例,每个 Object 都有一个变换。

Component: Base class for everything attached to GameObjects.

组件是每个游戏对象的功能部件,是附加于游戏对象上的一种基类。

Question 1.4.2

对象(实体)属性:GameObject,Cube类,第一个框中为 activeSelf 属性,第二个框中为对象名称,第三个框中为 static 属性。

Transform属性:Position: (0, 0, 0)、Rotation: (0, 0, 0)、Scale: (1, 1, 1),分别表示对象的位置、旋转角度和长宽高。

部件:包括 Transform、Box Collider、Mesh Renderer、Default-Material 等。

Question 1.4.3

UML 图如下:

Question 1.5

整理相关学习资料,编写简单代码验证以下技术的实现:

  • 查找对象
  • 添加子对象
  • 遍历对象树
  • 清除所有子对象
  • 查找对象
1
2
3
4
5
6
7
8
9
10
// 通过名称查找
public static GameObject Find(string name);
// 通过标签查找单个对象
public static GameObject FindWithTag(string tag);
// 通过标签查找多个对象
public static GameObject[] FindGameObjectsWithTag(string tag);
// 通过类型查找单个对象
public static GameObject FindObjectOfType(Type type);
// 通过类型查找多个对象
public static GameObject[] FindObjectsOfType(Type type);
  • 添加子对象
1
2
// 添加子对象
public static GameObect CreatePrimitive(PrimitiveTypetype);
  • 遍历对象树
1
2
// 遍历对象树
foreach (Transform child in parent.transform);
  • 清除所有子对象
1
2
3
4
// 清除所有子对象
foreach (Transform child in parent.transform) {
Destroy(child.gameObject);
}

Question 1.6

资源预设(Prefabs)与 对象克隆 (clone)

  • 预设(Prefabs)有什么好处?
  • 预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
  • 制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象

预设的好处

  1. 使对象和资源能够重复利用,且相同的游戏对象可以用同一个预制来创建。
  2. 方便统一批量调整对象属性,对预设进行修改后,所有的游戏对象都会发生改变。

预设与对象克隆的关系

  1. 两者都能创建出相同的游戏对象。
  2. 预设创建的实体会因预设的变化而变化,而克隆不会。

table 预制的制作

一个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour {
void Start() {
GameObject table = Instantiate(table_prefab, new Vector3(0, 0, 0));
}
void Update() {
}
// ...
}

2 编程实践-小游戏

2.1 游戏内容

井字棋 Tic-Tac-Toe:一种在3*3格子上进行的连珠游戏,由分别代表O和X的两个游戏者轮流在格子里留下标记(一般来说先手者为X),任意三个标记形成一条直线,则为获胜。

2.2 技术限制

使用 IMGUI 构建 UI。

2.3 作业目的

  • 了解 OnGUI() 事件,提升 debug 能力
  • 提升阅读 API 文档能力

2.4 完成情况

核心代码如下:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class TicTacToe : MonoBehaviour
{
private bool turn;
private int winner, count;
private int[, ] state = new int[3, 3];
private int[, ] map = new int[3, 3];
private GUIStyle Style = new GUIStyle ();

void Start() {
turn = true;
count = 0;
// init
for (int i = 0; i < 3; i++)
for (int j = 0; j < 3; j++) {
state[i, j] = 0;
map[i, j] = 0;
}
// style init
Style.fontSize = 32;
Style.fontStyle = FontStyle.BoldAndItalic;
Style.normal.textColor = Color.blue;
Style.alignment = TextAnchor.MiddleCenter;
}

void Update() {

}

int Check() {
for (int i = 0; i < 3; i++) {
// check for 3 rows
if (state[i, 0] != 0 && state[i, 0] == state[i, 1] && state[i, 0] == state[i, 2])
return state[i, 0];
// check for 3 columns
if (state[0, i] != 0 && state[0, i] == state[1, i] && state[0, i] == state[2, i])
return state[0, i];
}
// check for 2 diagonals
if (state[1, 1] != 0 && state[1, 1] == state[0, 0] && state[1, 1] == state[2, 2])
return state[1, 1];
if (state[1, 1] != 0 && state[1, 1] == state[0, 2] && state[1, 1] == state[2, 0])
return state[1, 1];
// check for tie
if (count == 9)
return 3;
// playing
return -1;
}

string LabelMessage(int check_ans) {
// label message
if (check_ans == 1)
return "Player O is Winner!";
else if (check_ans == 2)
return "Player X is Winner!";
else if (check_ans == 3)
return "Tie!!!";
else {
if (turn)
return "Player O is playing";
else
return "Player X is playing";
}
}

string ButtonMessage(int state_ans) {
// 9 blocks-message
if (state_ans == 1)
return "O";
else if (state_ans == 2)
return "X";
else
return "";
}

private void OnGUI() {
// button for restart
if (GUI.Button(new Rect(200, 430, 100, 75), "Restart"))
Start();
// lable about playing message
GUI.Label(new Rect(100, 50, 300, 30), LabelMessage(Check()), Style);
// game loop
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
// Debug.Log(ButtonMessage(state[i, j]));
// Tic-Tac-Toe 9-blocks
if (GUI.Button(new Rect(i * 100 + 100, j * 100 + 100, 100, 100), ButtonMessage(state[i, j]))) {
if (state[i, j] == 0 && Check () == -1) {
state[i, j] = turn ? 1 : 2;
turn = !turn;
count++;
map[i, j] = count;
}
}
}
}
}
}

游戏界面如下:

img

img

img

2.5 项目地址

代码地址:https://github.com/CUFMHeart/3D-Game-Programming-Design/tree/master/homework2

视频地址:https://www.bilibili.com/video/av67477674/

3 思考题

Question 3.1

微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。

  • 为什么是“模板方法”模式而不是“策略模式”呢?

首先,要明确“模板方法模式”和“策略模式”的含义。

模板方法模式:在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。

策略模式:一个类的行为或其算法可以在运行时更改。这种类型的设计模式属于行为型模式。

两者相比,模版方法扩展性好,对不变的代码进行封装,对可变的进行扩展;可维护性好,因为将公共代码进行了提取,使用的时候直接调用即可;但另一方面,因为每一个不同的实现都需要一个子类来实现,导致类的个数增加,它会使系统变得复杂。而实际上,由于微软 XNA 引擎的游戏循环骨架是已经确定的,所以使用模板方法模式的设计更为合理。

Question 3.2

将游戏对象组成树型结构,每个节点都是游戏对象(或数)。

  • 尝试解释组合模式(Composite Pattern / 一种设计模式)。
  • 使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?

组合模式

用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。

BroadcastMessage()

一个简单的例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour {
void Start () {
this.BroadcastMessage("Message");
}
void Message()
{
print("Message");
}
}

Question 3.3

一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)。

  • 这是什么设计模式?
  • 为什么不用继承设计特殊的游戏对象?

这是装饰器模式(Decorator Pattern)

继承的设计,往往为类引入静态特征,而随着扩展功能的增多,子类很容易脱离设计者预期的掌控,变得笨重、无法封装,因此一般不用继承设计特殊的游戏对象。装饰器模式可以将具体功能职责划分,可以更方便地进行类的功能的修改。

参考资料

[1] 离散仿真引擎基础_教学讲义

[2] Maunal

[3] 官方案例

[4] 一篇关于面对对象与设计模式的有趣文章*

CATALOG
  1. 1. 游戏引擎
  2. 2. 作业与练习
    1. 2.1. 1 简答题
      1. 2.1.1. Question 1.1
      2. 2.1.2. Question 1.2
      3. 2.1.3. Question 1.3
      4. 2.1.4. Question 1.4
      5. 2.1.5. Question 1.5
      6. 2.1.6. Question 1.6
    2. 2.2. 2 编程实践-小游戏
      1. 2.2.1. 2.1 游戏内容
      2. 2.2.2. 2.2 技术限制
      3. 2.2.3. 2.3 作业目的
      4. 2.2.4. 2.4 完成情况
      5. 2.2.5. 2.5 项目地址
    3. 2.3. 3 思考题
      1. 2.3.1. Question 3.1
      2. 2.3.2. Question 3.2
      3. 2.3.3. Question 3.3
  3. 3. 参考资料