WillKen's Blog.

3D游戏-离散仿真引擎基础

Word count: 3.3kReading time: 14 min
2019/09/11 Share

3D游戏-离散仿真引擎基础

简答题

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

游戏对象(GameObject):

​ 游戏对象是一个能够容纳实现实际功能的组件。游戏对象本身不做任何事情。它们需要特殊属性 (special properties) 才能成为一个角色、一种环境或者一种特殊效果。每个对象要做很多不同的事情,有专有属性。

资源(Assets):

​ 资源是可以在游戏或项目中使用的任何项的表示。资源可以来自unity外部创建的文件,例如3d模型、音频文件、图像或unity支持的任何其他类型的文件。也可以在Unity中创建一些资源类型,例如动画控制器、音频混合器或渲染纹理。

两者之间的联系:

​ 资源可以作为模板,实例化成具体的游戏对象,也可以作为游戏对象中的某种属性,同时也可以被多个游戏对象同时使用。

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

    在GitHub上下载了一个简单的游戏-飞机大战

    游戏资源结构:

    ​ 游戏资源的内容分为音频、脚本、图像等等,按照文件的类型的不同,这些资源被添加到不同的文件夹当中形成树状结构。如下图:

    游戏对象结构:

    ​ 该游戏将游戏对象按照其角色分类,按照其功能的不同,添加到不同的文件夹当中形成树状结构。如下图:

  • 编写一个代码,使用 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
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class NewBehaviourScript : MonoBehaviour {

    void Awake()
    {
    Debug.Log("This is Awake!");
    }

    void Start()
    {
    Debug.Log("This is Start!");
    }

    void Update()
    {
    Debug.Log("This is Update!");
    }

    void FixedUpdate()
    {
    Debug.Log("This is FixedUpdate!");
    }

    void LateUpdate()
    {
    Debug.Log("This is LateUpdate!");
    }

    void OnGUI()
    {
    Debug.Log("This is OnGUI!");
    }

    void OnDisable()
    {
    Debug.Log("This is OnDisable!");
    }

    void OnEnable()
    {
    Debug.Log("This is OnEnable!");
    }

    }

    运行结果:

事件名称 执行条件或时机
Awake 当一个脚本实例被载入时Awake被调用。或者脚本构造时调用
Start 第一次进入游戏循环时调用
FixUpdate 每个游戏循环,由物理引擎调用
Update 所有 Start 调用完后,被游戏循环调用
LastUpdate 所有 Update 调用完后,被游戏循环调用
OnGUI 游戏循环在渲染过程中,场景渲染之后调用
OnEnable 当对象变为可用或激活状态时被调用
OnDisable 当对象变为不可用或非激活状态时被调用
  • 查找脚本手册,了解GameObject,Transform,Component 对象

    • 分别翻译官方对三个对象的描述(Description)

      GameObject:GameObjects are the fundamental objects in Unity that represent characters, props and scenery. They do not accomplish much in themselves but they act as containers for Components, which implement the real functionality.

      GameObject是统一体中代表游戏角色、道具和场景的基本对象。它们本身并没有完成多少工作,但是它们充当组件的容器,组件实现真正的功能。

      Transform:The Transform is used to store a GameObject’s position, rotation, scale and parenting state and is thus very important. A GameObject will always have a Transform component attached - it is not possible to remove a Transform or to create a GameObject without one.

      Transform用于存储GameObject的位置、旋转、缩放和父元素的状态,因此非常重要。每个GameObject都有一个Transform组件而且不可删除,而且不能创建一个没有Transform的GameObject。

      Component:Components are the nuts & bolts of objects and behaviors in a game. They are the functional pieces of every GameObject.

      组件(Component)是游戏中对象和行为的螺母和螺栓。它们是每个游戏对象的功能部件。

    • 描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件

      • 本题目要求是把可视化图形编程界面与 Unity API 对应起来,当你在 Inspector 面板上每一个内容,应该知道对应 API。
      • 例如:table 的对象是 GameObject,第一个选择框是 activeSelf 属性。

      table的对象是GameObject,它的属性有layer(图层,Default)、scene(场景)、tag(标签,untagged)等。
      table 的 Transform 属性:位置在(0,0,0),旋转参数(0,0,0),缩放在初始值(1,1,1)。
      table 的部件有:Tranform、Cube(Mesh Filter)、Box Collider、Mesh Renderer。

    • 用 UML 图描述三者的关系(请使用 UMLet 14.1.1 stand-alone版本出图)

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

    当前项目的目录结构为:

    • 查找对象

      查找名为zwk1的GameObject 代码:

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

      public class NewBehaviourScript : MonoBehaviour
      {

      // Use this for initialization
      void Start()
      {

      var cube = GameObject.Find("zwk1");

      if (cube != null)
      {
      Debug.Log("Find zwk1!");
      }
      else
      {
      Debug.Log("There is no zwk1!");
      }

      }

      // Update is called once per frame
      void Update()
      {

      }
      }

      运行结果:

    • 添加子对象

      添加名为zwk3的cube 代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;

      public class NewBehaviourScript : MonoBehaviour
      {

      // Use this for initialization
      void Start()
      {
      GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
      cube.name = "zwk3";
      cube.transform.position = new Vector3(0, Random.Range(0, 3), 0);
      cube.transform.parent = this.transform;
      }

      // Update is called once per frame
      void Update()
      {

      }
      }

      运行结果:

      (注:zwk3仅在运行时出现,停止运行后自动消失)

    • 遍历对象树

      代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;

      public class NewBehaviourScript : MonoBehaviour
      {

      // Use this for initialization
      void Start()
      {
      foreach (Transform child in transform)
      {
      Debug.Log(child.name);
      }
      }

      // Update is called once per frame
      void Update()
      {

      }

      运行结果:

    • 清除所有子对象

      代码:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;

      public class NewBehaviourScript : MonoBehaviour
      {

      // Use this for initialization
      void Start()
      {
      foreach (Transform child in transform)
      {
      Destroy(child.gameObject);
      }
      }

      // Update is called once per frame
      void Update()
      {

      }
      }

      运行结果:

      (注:zwk1和zwk2仅在运行时消失,停止运行后出现)

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

    • 预设(Prefabs)有什么好处?

      它把需要用到的对象做成一个模板,可以被置入多个场景中,也可以在一个场景中多次置入。
      只要Prefabs原型发生改变,所有的Prefabs实例都会产生变化,适合批量处理。

    • 预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
      克隆将已经存在的游戏对象和预设进行复制,它们之间独立,互不干扰,即克隆不受被克隆的对象影响。

      如果预设被修改了,所有通过预设实例化的游戏对象都会被改变。

    • 制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象

      首先,做出Table预制,将其放在Resources中。

      预制资源实例化代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      using System.Collections;
      using System.Collections.Generic;
      using UnityEngine;

      public class NewBehaviourScript : MonoBehaviour
      {
      public GameObject Table;
      // Use this for initialization
      void Start()
      {
      GameObject cube1 = Instantiate(Resources.Load("Table") as GameObject);
      }

      // Update is called once per frame
      void Update()
      {

      }
      }

      将C#script拖放到camera上,点击运行。

      运行结果:

      (运行前)

      (运行中)

编程实践,小游戏

  • 游戏内容: 井字棋 或 贷款计算器 或 简单计算器 等等

  • 技术限制: 仅允许使用 IMGUI构建 UI

  • 作业目的:

    • 了解 OnGUI() 事件,提升 debug 能力

    • 提升阅读 API 文档能力

      游戏设计思路:

      ​ 在Awake行为中进行背景图片的加载。

      ​ 在start行为中进行初始化(调用Reset()函数)。

      ​ 在OnGUI中,首先完成背景图片及相关字体按钮的外观设置。接着设置点击按钮的事件。点击【reset】触发调用Reset()函数;判断游戏结果;点击空白的按钮进行下棋,根据数组的1或2选择显示O或X;player1和player2交替下棋。

      ​ 在判断游戏结果的过程中调用GetResult()函数。该函数首先检查每一行,再检查每一列,然后检查对角线,最后,若棋盘已满(count==9)还没分出胜负则是平局。

      实现代码:

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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class game : MonoBehaviour
{
private int count = 0;
private int player = 1;
private int[,] chess = new int[3, 3];
//img ----bc
public Texture2D img;

void Awake()
{
img = (Texture2D)Resources.Load("timg");
}


// Use this for initialization
void Start()
{
Reset();
}

// Update is called once per frame
void Reset()
{
count = 0;
player = 1;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
chess[i, j] = 0;
}
}
}

private void OnGUI()
{
string aa = "";

//构造一个空的GUIStyle
GUIStyle bb = new GUIStyle();

//设置bb正常显示时是背景图片
bb.normal.background = img;
GUI.Label(new Rect(0, 0, 1370, 780), aa, bb);

//font size, text color, background color of button.
GUI.skin.button.fontSize = 20;
GUI.skin.label.fontSize = 30;
GUI.skin.label.normal.textColor = Color.black;
GUI.backgroundColor = Color.cyan;

GUI.Label(new Rect(200, 180, 300, 50), "Player1 -- O");
GUI.Label(new Rect(200, 230, 300, 50), "Player2 -- X");

//press [reset] button.
if (GUI.Button(new Rect(500, 400, 100, 50), "Reset"))
{
Reset();
}

//get results (if any) ---3 conditions.
int result = GetResult();
if (result == 1)
{
GUI.Label(new Rect(510, 20, 100, 50), "O wins");
}
else if (result == 2)
{
GUI.Label(new Rect(510, 20, 100, 50), "X wins");
}
else if (result == 3)
{
GUI.Label(new Rect(530, 20, 200, 50), "Tie!");
}

//chess 1---O 2---X
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
if (chess[i, j] == 1)
GUI.Button(new Rect(i * 100 + 400, j * 100 + 80, 100, 100), "O");
if (chess[i, j] == 2)
GUI.Button(new Rect(i * 100 + 400, j * 100 + 80, 100, 100), "X");
//play chess
if (GUI.Button(new Rect(i * 100 + 400, j * 100 + 80, 100, 100), "") && result == 0)
{

if (player == 1)
{
chess[i, j] = 1;
player = 2;
}
else if (player == 2)
{
chess[i, j] = 2;
player = 1;
}
count++;
}
}
}
}

int GetResult()
{
//Rows.
for (int i = 0; i < 3; i++)
{
if (chess[i, 0] == chess[i, 1] && chess[i, 0] == chess[i, 2] && chess[i, 0] != 0)
{
return chess[i, 0]; //1---O wins
}
}

//Columns.
for (int j = 0; j < 3; j++)
{
if (chess[0, j] == chess[1, j] && chess[0, j] == chess[2, j] && chess[0, j] != 0)
{
return chess[0, j]; //2---X wins
}
}

//Diagonals.
if (chess[0, 0] == chess[1, 1] && chess[0, 0] == chess[2, 2] && chess[0, 0] != 0) return chess[0, 0];
if (chess[0, 2] == chess[1, 1] && chess[0, 2] == chess[2, 0] && chess[0, 2] != 0) return chess[0, 2];

//if the game is Tied.
if (count == 9) return 3; //tie!
return 0;
}
}

​ 添加背景部分的代码参考了网上的众多博客,最终选择使用代码效果来实现。首先进行加载图片,将图片资源放入new Unity Project\Assets\Resources ,图片的加载在Awake中进行。

运行效果:

前往项目地址下载并运行代码。

思考题【选做】

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

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

      策略模式:其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。

      模板方法模式:定义一个操作中算法的骨架,而将一些步骤延迟到子类中,模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。

      说的通俗一点,模板方法模式就是为子类设计一个模板以便于子类复用里面的方法。

      “Game对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们”这表示Game是一个抽象类,声明了一些抽象方法使得子类实现剩余的逻辑。此外,Game中可能还有某些子类共有的属性。而策略模式用于封装不同算法的是“接口”,这个“接口”类中往往不含属性。因此,我们称这种设计为“模板方法模式”。

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

    • 尝试解释组合模式(Composite Pattern / 一种设计模式)。

      ​ 组合模式(CompositePattern)属于结构型模式,把一组相似的对象作为一个单一的对象。该模式创建了对象组的树形结构,用来表示部分以及整体层次。组合模式实现的关键是简单对象和复合对象必须实现相同的接口

    • 使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?

      1
      2
      3
      4
      5
      BroadcastMessage()
      {
      FOR childObject IN ObjectTree
      sendMessage();
      }
  • 一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)。

    • 这是什么设计模式?

      装饰模式(Decorator Pattern)

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

      我们要尽量使得自己的代码高内聚,低耦合。

      采用继承的话部件之间耦合度高,不够灵活;采用装饰模式可以动态地扩展一个对象的功能(对其进行装饰),Decorator可以提供比继承更多的灵活性。

CATALOG
  1. 1. 3D游戏-离散仿真引擎基础
    1. 1.1. 简答题
    2. 1.2. 编程实践,小游戏
    3. 1.3. 思考题【选做】