简答题
##1. 解释游戏对象(GameObjects)和资源(Assets)的区别与联系。
游戏对象是出现在游戏场景中的所有物体,是资源整合的具体表现。游戏对象有专有属性,是能够容纳实现实际功能的组件。
资源是可以在游戏或项目中使用的任何项目的表示。资源可能来自Unity之外创建的文件,例如3D模型、音频文件、图像或Unity支持的任何其他类型的文件。还有一些资源类型可以在Unity中创建,比如动画控制器、音频混频器或渲染纹理。
二者的联系:游戏对象可以作为资源保存(从场景拖到资源栏)。资源可以作为组建或游戏对象创建游戏对象实例,资源还可以被多个游戏对象使用,成为游戏对象的属性。
##2. 下载几个游戏案例,分别总结资源、对象组织的结构(指资源的目录组织结构与游戏对象树的层次结构)
在github上下载了一个射击类小游戏,分析资源和游戏对象树的结构。
先看资源的组织结构,如图:
可以看出游戏资源的内容分为音频、脚本、材质等等,按照文件的类型的不同,添加到不同的文件夹当中来实现资源的目录组织结构,如脚本文件都放在Scripts中:
而游戏对象树类似于树,一个游戏对象往往是包括了多个子对象。子对象又包括了它的子对象,我下载的这个游戏虽然体现的不是很明显,但是也是能看出来的,游戏对象包括玩家,地图上的各个点以及四个方向的光照:
##3. 编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件
-
基本行为包括 Awake() Start() Update() FixedUpdate() LateUpdate()
-
常用事件包括 OnGUI() OnDisable() OnEnable()
代码如下,只是在事件中用log输出一段话:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour {
void Awake() {
Debug.Log ("This Awake!");
}
// Use this for initialization
void Start () {
Debug.Log ("This Awake!");
}
void Update () {
Debug.Log ("This Update!");
}
void FixedUpdate() {
Debug.Log ("This FixedUpdate!");
}
void LateUpdate() {
Debug.Log ("This LateUpdate!");
}
void OnGUI() {
Debug.Log ("This OnGUI!");
}
void OnDisable() {
Debug.Log ("This OnDisable!");
}
void OnEnable() {
Debug.Log ("This OnEnable!");
}
}
代码运行结果如下:
可以看出来
-
Awake在当前控制脚本实例被装载的时候调用。一般用于初始化整个实例使用。
-
Start在所有update函数之前被调用一次,表示开始进行游戏。
-
Update每帧都执行一次,对场景进行更新。
-
FixedUpdate每固定帧绘制时由物理引擎调用,FixedUpdate是渲染帧执行,如果渲染效率底下的时候FixedUpdate的调用次数就会下降。FixedUpdate比较适用于物理引擎的计算。
-
LateUpdate在每帧执行完毕调用,他在所有Update结束后才调用,比较适合于命令脚本的执行。一般用于摄像机的跟随。
-
OnEnable当对象变为可用或激活状态时此函数被调用,OnEnable不能用于协同程序。
-
OnDisable当对象变为不可用或非激活状态时此函数被调用。当物体销毁时它被调用,并且可用于任意清理代码。当脚本编译完成之后被重新加载时,OnDisable将被调用,OnEnable在脚本被载入后调用。(物体被禁用时调用)
-
OnGUI() : 绘制GUI时候触发。一般在这个函数里绘制GUI菜单。
##4. 查找脚本手册,了解 GameObject,Transform,Component对象
- 分别翻译官方对三个对象的描述(Description)
- 描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件
- 本题目要求是把可视化图形编程界面与 Unity API 对应起来,当你在 Inspector 面板上每一个内容,应该知道对应 API。
- 例如:table 的对象是 GameObject,第一个选择框是 activeSelf 属性。
- 用 UML 图描述 三者的关系(请使用 UMLet 14.1.1 stand-alone版本出图)
脚本手册上对三个对象的描述和翻译如下:
-
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.
翻译:组件是游戏中对象和行为的螺母和螺栓。它们是每个游戏对象的功能部件。
题中的“下图”没有给,估计是上面的table的图,图如下:
[外链图片转存失败(img-DoWmr4Jb-1568083373379)(https://pmlpml.github.io/unity3d-learning/images/ch02/ch02-prefabs.png)]
table的对象是GameObject,属性有layer(图层,默认值)、scene(场景)、tag(标签,untagged)、非静态的
table 的 Transform 的属性:位置在(0,0,0),旋转参数(0,0,0),缩放在初始值1
table 的部件:Tranform、Cube(Mesh Filter)、Box Collider、Mesh Renderer
GameObject、Transform、Component三者的关系如下:
5. 整理相关学习资料,编写简单代码验证以下技术的实现:
- 查找对象
- 添加子对象
- 遍历对象树
- 清除所有子对象
创建一个与上面类似的table的游戏对象,对象树和场景如图:
然后以这个场景为例进行操作。
- 查找对象:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour {
// Use this for initialization
private void Awake()
{
Debug.Log("This awake!");
}
void Start () {
Debug.Log("This start!");
var Found = GameObject.Find("chair1");
var notFound = GameObject.Find("chair5");
if(Found != null)
{
Debug.Log(Found);
}
else
{
Debug.Log("It is nothing!");
}
if(notFound != null)
{
Debug.Log(notFound);
}
else
{
Debug.Log("It is nothing!");
}
}
// Update is called once per frame
void Update () {
}
}
代码的运行结果如下:
可见chair1被找到并被输出,chair5找不到,输出"It is nothing!"
- 添加子对象:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour {
// Use this for initialization
private void Awake()
{
Debug.Log("This awake!");
}
void Start () {
Debug.Log("This start!");
GameObject new_chair = GameObject.CreatePrimitive(PrimitiveType.Cube);
new_chair.name = "chair5";
new_chair.transform.position = new Vector3(0, Random.Range(0, 10), 0);
new_chair.transform.parent = this.transform;
}
// Update is called once per frame
void Update () {
}
}
运行结果如下:
可以看出代码给table创建了子对象chair5,位置在table上方8个单位。
- 遍历对象树
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour {
// Use this for initialization
void Start () {
foreach(Transform child in transform)
{
Debug.Log(child.name);
}
}
// Update is called once per frame
void Update () {
}
}
代码运行结果:
可以看出该代码将table的子对象进行遍历并输出。
- 清除所有子对象:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour {
// Use this for initialization
void Start () {
foreach(Transform child in transform)
{
Destroy(child.gameObject);
}
}
// Update is called once per frame
void Update () {
}
}
运行结果:
可以看出来table的子对象都被删除了
6. 资源预设(Prefabs)与对象克隆 (clone)
- 预设(Prefabs)有什么好处?
预设可以把需要用到的对象做成一个模板,可以直接加载成为游戏对象。适合批量处理。 - 预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
克隆是将已经存在的游戏对象和预设进行复制。预设一般来说是克隆的一个基础步骤,在预设之后,就可以在资源当中为自己想要的对象进行克隆。克隆不受被克隆的对象影响,但是预设会。 - 制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
首先在资源界面创建一个预设My_Table,然后把场景中的Table拖到My_Table上,结果如下:
然后把这个预设进行实例化:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test : MonoBehaviour {
public GameObject My_Table;
// Use this for initialization
void Start () {
GameObject ins = (GameObject)Instantiate (My_Table, new Vector3(0,Random.Range(5,10),0), transform.rotation);
}
// Update is called once per frame
void Update () {
}
}
调用时先把脚本拖到table游戏对象上,再把预设My_table拖到脚本的My_table中,如图:
然后运行程序,得到结果如图:
编程实践,小游戏
- 游戏内容: 井字棋 或 贷款计算器 或 简单计算器 等等
- 技术限制: 仅允许使用 IMGUI 构建 UI
- 作业目的:
- 了解 OnGUI() 事件,提升 debug 能力
- 提升阅读 API 文档能力
代码如下,注释写的比较详细这里就不单独叙述了:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ChessBoard : MonoBehaviour {
private int[,] chessBoard = new int[3, 3];
//判断是O走或者是X走
int my_turn = 1;
//初始化函数
void Init()
{
my_turn = 1;
//把棋盘上的每个位置都置0
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
chessBoard [i,j] = 0;
}
}
}
//判断胜利条件
int is_win(){
//如果成行
for (int i = 0; i < 3; i++) {
if (chessBoard[i,0] == chessBoard[i,1] && chessBoard[i,0] == chessBoard[i,2] && chessBoard[i,0] != 0) {
return chessBoard[i,0];
}
}
//如果成列
for (int j = 0; j < 3; j++) {
if (chessBoard[0,j] == chessBoard[1,j] && chessBoard[0,j] == chessBoard[2,j] && chessBoard[0,j] != 0) {
return chessBoard[0,j];
}
}
//如果对角线
//返回的是胜利的数字
if(chessBoard[0,0] == chessBoard[1,1] && chessBoard[0,0] == chessBoard[2,2] && chessBoard[0,0] != 0)
return chessBoard[0,0];
if(chessBoard[0,2] == chessBoard[1,1] && chessBoard[0,2] == chessBoard[2,0] && chessBoard[0,2] != 0)
return chessBoar
//如果一共走了九步还没决出胜负
int total_step = 0;
for(int i = 0 ; i < 3 ; i++) {
for (int j = 0; j < 3; j++) {
if (chessBoard[i,j] != 0) {
total_step++;
}
}
}
//就返回平局
if (total_step == 9) {
return 3;
}
//如果还没决出胜负就返回-1
return -1;
}
// Use this for initialization
//开局先初始化
void Start () {
Init ();
}
// Update is called once per frame
void Update () {
}
//绘制GUI函数
void OnGUI(){
//字体颜色
GUI.contentColor = Color.yellow;
//如果按reset就重置棋盘
if(GUI.Button(new Rect(250,30,200,40), "Reset"))
{
Init ();
}
//判断是否决出胜负
int win = is_win ();
//如果1赢了(代表O)
if(win==1)
GUI.Label (new Rect (330, 75, 60, 50), "O Win!");
//如果2赢了(代表X)
else if(win==2)
GUI.Label (new Rect (330, 75, 60, 50), "X Win!");
//如果平局
else if(win==3)
GUI.Label (new Rect (330, 75, 60, 50), "Draw!");
//否则继续游戏
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
//如果某个按钮被按了
if (GUI.Button (new Rect (275 + 50 * i, 100 + 50 * j, 50, 50), "")) {
if(chessBoard[i,j]==0)
{
//没有决出胜负的时候才能改变按钮的值
if (win == -1) {
//如果是我的回合
if (my_turn == 1) {
chessBoard [i, j] = 1;
//现在是对方回合
my_turn = 0;
}//如果是对方回合
else if (my_turn == 0) {
chessBoard [i, j] = 2;
//现在是自己的回合
my_turn = 1;
}
}
}
}
}
}
//绘制按钮图案
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (chessBoard [i, j] == 1) {
GUI.Button (new Rect (275 + 50 * i, 100 + 50 * j, 50, 50), "O");
} else if (chessBoard [i, j] == 2) {
GUI.Button (new Rect (275 + 50 * i, 100 + 50 * j, 50, 50), "X");
}
}
}
}
}
另外我还给棋盘加了背景图,具体参照博客:博客连接,不过最后写的不够详细,在最后一步要在Image中添加GUITexture,然后把背景图片拖到Texture中,然后调整位置和缩放。最后得到的结果如下(我永远喜欢紫色美少女.jpg):
思考题
-
微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。
- 为什么是“模板方法”模式而不是“策略模式”呢?
模板方法的定义是定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
而策略模式的定义是:策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
而“Game对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们”说明Game是一个抽象类,声明了一些抽象方法让子类实现剩余的逻辑,此外,Game中可能还有某些子类共有的属性。而策略模式用于封装不同算法的是“接口”,这个“接口”类中往往不含属性。
- 为什么是“模板方法”模式而不是“策略模式”呢?
-
将游戏对象组成树型结构,每个节点都是游戏对象(或数)。
-
尝试解释组合模式(Composite Pattern / 一种设计模式)。
组合模式(Composite Pattern),是用于把一组相似的对象当作一个单一的对象。组合模式依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。这种模式创建了一个包含自己对象组的类。组合模式实现的最关键的地方是简单对象和复合对象必须实现相同的接口。 -
使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?
BroadcastMessage() { FOR child IN ObjectTree sendMessage(); }
-
-
一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是GameObject 添加一组行为部件(Component)。
- 这是什么设计模式?
装饰器模式 - 为什么不用继承设计特殊的游戏对象?
采用继承的话部件之间耦合度高,不够灵活,采用装饰器模式不仅可以动态地扩展一个对象的功能,还可以具体构件类和具体装饰类可以独立地变化。
- 这是什么设计模式?