1. 简答题
-
解释 游戏对象(GameObjects) 和 资源(Assets)的区别与联系。
- 游戏对象:游戏对象是 Unity 场景(Scenes)中所有实体的基类,并充当功能组件的容器,这些功能组件确定游戏对象的外观及其功能。游戏中的每一个实体都是一个游戏对象。
- 资源:资源是任何可以在游戏中使用的各类物品,既可以是 Unity 外部创建的各类文件,例如模型、动画、音频、图片等等;也可以是通过 Unity 创建的其他类型,例如动画控制器,音频混合器等。
- 两者的区别与联系:游戏对象是实体化的,例如玩家、敌人、环境等,是资源整合的具体表现;而资源是抽象的,并不在场景中出现,可以同时被多个游戏对象使用。
-
下载几个游戏案例,分别总结资源、对象组织的结构(指资源的目录组织结构与游戏对象树的层次结构)。
-
资源目录结构:资源一般将动画、音频、预制、脚本等分门别类放置在不同的文件夹中,便于游戏制作过程中的整理和利用。例子如下:
Asserts | - Animations | - MainMenu | - UI | - Audio | - Music | - SFX | - Data | - Agents | - Alignments | - Levels | - Materials | - Levels | - Particles | - Player | - Projectiles | - UI | - Units | - Models | - PostProcessing | - Prefabs | - Scenes | - Scripts | - Sprites | - Textures | - UI
-
游戏对象层次结构:根据不同的类型划分不同的游戏对象,例如玩家、敌人、环境、音乐等,低层次的游戏对象属于同一大类的不同子类。游戏对象的层次结构与资源的目录结构类似,两者都是从较高的层级不断细化,符合自上而下的组织方式。
GameObject | - Player | - Enemy | - Surroundings | - Bulidings | - Trees ... | - Camera | - Music | - Audio | - Environment
-
-
编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件。
-
基本行为包括:Awake()、Start()、Update()、FixedUpdate()、LateUpdate();
-
常用事件包括:OnGUI()、OnDisable()、OnEnable()
using System.Collections; using System.Collections.Generic; using UnityEngine; public class NewBehaviourScript : MonoBehaviour { void Awake() { Debug.Log ("Awake"); } void Start () { Debug.Log ("Start"); } void Update () { Debug.Log ("Update"); } void FixedUpdate() { Debug.Log ("FixedUpdate"); } void LateUpdate() { Debug.Log ("LateUpdate"); } void OnGUI() { Debug.Log ("GUI"); } void OnDisable() { Debug.Log ("Disable"); } void OnEnable() { Debug.Log ("OnEnable"); } }
-
触发条件分析:
根据上图,我们可以看到执行顺序为:Awake() – OnEnable() – Start() – FixedUpdate() – Update() – LateUpdate() – OnGUI() – OnDisable(),每个生命周期函数的定义如下:
- Awake:这个函数总是在任何启动函数之前被调用,并且在实例化预制件之后。(如果游戏对象在启动过程中处于非活动状态,则在唤醒之前不会调用它。)
- OnEnable:(只有在对象处于活动状态时才会调用)在启用对象后立即调用此函数。当创建一个 MonoBehaviour 实例时会发生这种情况,比如当一个级别被加载或者一个带有脚本组件的GameObject被实例化时。
- Start:只有在启用(Enable)脚本实例时,才会在第一次帧更新(Update)之前调用启动。
- FixedUpdate:该函数通常比 update 函数更频繁地被调用。如果帧速率较低,则可以每帧调用多次,如果帧速率较高,则可能不会在帧之间调用。所有物理计算和更新都会在 FixedUpdate 后立即发生。
- Update:每帧调用一次。它是帧更新的主要功能。
- LateUpdate:更新(Update)完成后,LateUpdate 每帧调用一次。 LateUpdate 开始时,update 中执行的任何计算都将完成。
- OnGUI:响应 GUI 事件,每帧调用多次。 Layout 和 Repaint 事件首先被处理,然后是每个输入事件的布局和键盘/鼠标事件。
- OnDisable:当行为被禁用或不活动时,将调用此函数。
PS:官方文档关于脚本生命周期的说明:https://docs.unity3d.com/Manual/ExecutionOrder.html
-
-
查找脚本手册,了解 GameObject,Transform,Component 对象
-
分别翻译官方对三个对象的描述(Description):
-
GameObject:场景中所有实体的基类。
Base class for all entities in Unity Scenes.
-
Transform:场景中的每一个对象都有一个转换,用于存储和操纵对象的位置、旋转和比例。
Every object in a Scene has a Transform. It’s used to store and manipulate the position, rotation and scale of the object.
-
Component:所有附加到游戏对象上的事物的基类。
Base class for everything attached to GameObjects.
-
-
描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件:
- table 对象的属性:
- ActiveSelf:定义对象的名称,动静态,所处图层,对象标签等属性;
- Transform:定义对象的位置、旋转角度、大小;
- Mesh Filter:定义对象的形状;
- Mesh Renderer:在 Transform 所定义的位置渲染该对象;
- Sphere Collider:可以调整坐标系的位置、大小;
- table 的 transform 的属性:
- Position:定义对象的位置;
- Rotation:定义对象旋转的角度;
- Scale:定义对象的大小;
- table 的部件:Mesh Filter、Mesh Renderer、Sphere Collider;
- table 对象的属性:
-
用 UML 图描述 三者的关系(请使用 UMLet 14.1.1 stand-alone版本出图):
-
-
资源预设(Prefabs)与 对象克隆 (clone):
-
预设(Prefabs)有什么好处?
- 预设类似于一个模板,通过预设可以创建相同属性的对象,这些对象和预设关联。在设计的过程中,随时可以直接从资源当中加载,成为一个游戏对象。预设的存在,应用了面向对象的思想,一旦预设发生改变,所有通过预设实例化的对象都会产生相应的变化(适合批量处理)。
-
预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
- 对象克隆的实例之间不会相互影响,即克隆对象A不会因克隆对象B的改变而改变。而对预设进行修改会作用到该预设所有的实例上。
-
制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象。
public class InitBeh : MonoBehaviour { public GameObject table; // Use this for initialization void Start () { //将table预设实例化为游戏对象 GameObject newTable = (GameObject)Instantiate(table.gameObject); newTable.transform.position = Vector3.zero; newTable.transform.parent = this.transform; } }
-
2. 编程实战,井字棋
将文件Game.cs直接添加为摄像头的行为文件即可,点击开始即可进行井字棋游戏。
// Game.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Game : MonoBehaviour
{
private int empty = 9;
private int turn = 1;
private int[,] chess = new int[3, 3];
// Use this for initialization
void Start()
{
reset();
}
// Update is called once per frame
void reset()
{
empty = 9;
turn = 1;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
chess[i, j] = 0;
}
}
}
private void OnGUI()
{
GUI.skin.button.fontSize = 20;
GUI.skin.label.fontSize = 30;
if (GUI.Button(new Rect(450, 400, 200, 80), "Reset"))
{
reset();
}
int result = is_win();
if (result == 1)
{
GUI.Label(new Rect(500, 20, 200, 50), "WINNER O");
}
else if (result == 2)
{
GUI.Label(new Rect(500, 20, 200, 50), "WINNER X");
}
else if (result == 3)
{
GUI.Label(new Rect(470, 20, 200, 50), "NO WINNER");
}
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");
if (GUI.Button(new Rect(i * 100 + 400, j * 100 + 80, 100, 100), ""))
{
if (result == 0)
{
if (turn == 1) chess[i, j] = 1;
if (turn == 2) chess[i, j] = 2;
empty--;
if (empty % 2 == 1)
{
turn = 1;
}
else
{
turn = 2;
}
}
}
}
}
}
int is_win()
{
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
}
if (chess[0, i] == chess[1, i] && chess[0, i] == chess[2, i] && chess[0, i] != 0)
{
return chess[0, i]; //2
}
}
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 (empty == 0)
{
return 3;//没有空的地方,因此双方平局
}
else
{
return 0;//还未分出胜负
}
}
}
3. 思考题
-
微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。
- 为什么是“模板方法”模式而不是“策略模式”呢?
答:
- 模板方法定义了一个过程或算法中的核心步骤,而对于子步骤或是子方法则是使用继承或接口在子类中实现,从而使得可以通过不改变整体而对一些子步骤进行重构;
- 策略模式则是指对象具备某个行为,但是在不同的场景中,该行为有不同的实现算法。
- 屏蔽游戏循环的细节,并使用一组虚方法由继承者完成,目的就是使得可以在不改变代码基本结构的情况下将一些具体模块交由继承者具体实现,这明显更符合“模板方法”,而非“策略模式”。
-
将游戏对象组成树型结构,每个节点都是游戏对象(或数)。
- 尝试解释组合模式(Composite Pattern / 一种设计模式)。
答:将对象组合成树形结构以表示“部分整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。
- 使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?
答:
-
为子对象添加方法:
void message(string someMessage){ Debug.Log(someMessage); }
-
为父对象添加方法:
void Start(){ this.BroadcastMessage("message", someMessage); }
-
BroadcastMessage() 方法伪代码:
void BoradcastMessage(string message){ foreach (child of this){ if (child.hasFunction(message)) child.stringToFunction(message); } }
-
一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)。
- 这是什么设计模式?
答:这是装饰器模式(Decorator)。
- 为什么不用继承设计特殊的游戏对象?
答:
- 用继承机制去描述所有对象是几乎不可能的。现实世界的需求和构成过于复杂导致不能够仅仅使用继承机制来实现。
- 在用继承机制时,多重继承产生的类过于冗杂和难以使用;
- 继承中的多态特性使得游戏对象的行为变得难以预测和维护;
- 继承是静态的,在运行前就已经决定了继承的所有性质;而游戏往往需要实时的、多种的组合方式来实现;
- 降低耦合度,方便对 坦克 游戏对象进行拓展。
。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)。*
- 这是什么设计模式?
答:这是装饰器模式(Decorator)。
- 为什么不用继承设计特殊的游戏对象?
答:
- 用继承机制去描述所有对象是几乎不可能的。现实世界的需求和构成过于复杂导致不能够仅仅使用继承机制来实现。
- 在用继承机制时,多重继承产生的类过于冗杂和难以使用;
- 继承中的多态特性使得游戏对象的行为变得难以预测和维护;
- 继承是静态的,在运行前就已经决定了继承的所有性质;而游戏往往需要实时的、多种的组合方式来实现;
- 降低耦合度,方便对 坦克 游戏对象进行拓展。