作业二
简答题
解释游戏对象和资源的区别与联系
游戏对象:是指出现在游戏的场景中,一个有独立的属性并且能够容纳、实现实际功能的组件
资源:是指可能用到的各种源文件,比如音频、贴图、模型、脚本等等
这两者的区别可以从定义看出,游戏对象是一个独立、有功能的组件,而游戏资源只是一些碎片。这两者的联系是,我们需要利用这里"碎片"来进行游戏对象的构建,将资源实例化成为具体的一个游戏对象。
下载几个游戏案例,分别总结资源、对象组织的结构
我们打开Unity的Windows->Asset Store,下载游戏案例Space Shooter,然后再进行导入。
导入完成之后,我们可以看见游戏资源的结构如下:
其中,包括一些脚本、场景、材质、模型、音频等等资源类型,都进行了分类处理。然后,对每一种资源类型其又进行了细分,比如预制建筑文件夹中,又有其专门的脚本类型等等。
游戏对象的结构如下所示:
我们可以看到游戏对象的结构也和游戏资源的结构类似,分为背景、玩家、边界、信息显示、光线等等类型,每种类型有细分了很多小类,比如信息显示分成了游戏结束的显示、游戏分数的显示等等。
编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件
我们建立一个新的C# 脚本,使用debug语句进行验证:
完整的代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class New : MonoBehaviour {
private void Awake()
{
Debug.Log("Awake");
}
// Use this for initialization
void Start()
{
Debug.Log("Start");
}
// Update is called once per frame
void Update()
{
Debug.Log("Update");
}
private void FixedUpdate()
{
Debug.Log("FixedUpdate");
}
private void LateUpdate()
{
Debug.Log("LateUpate");
}
private void OnGUI()
{
Debug.Log("OnGUI");
}
private void OnDisable()
{
Debug.Log("OnDisable");
}
private void OnEnable()
{
Debug.Log("OnEnable");
}
}
然后我们将这个脚本添加到玩家上,可以在控制台看见输出:
进行分析我们可以得出:
基本行为:
Awake():当一个脚本实例被载入时Awake被调用。或者脚本构造时调用
Start():第一次进入游戏循环时调用
Update():所有 Start 调用完后,被游戏循环调用
Fixedupdate():每个游戏循环,由物理引擎调用
Lateupdate():所有 Update 调用完后,被游戏循环调用
常用事件:
OnGUI():游戏循环在渲染过程中,场景渲染之后调用
OnEnable():当对象变为可用或者激活状态时被调用
OnDisable():当对象变为不可用或者非激活状态时被调用
查找脚本手册,了解 GameObject,Transform,Component 对象
- 分别翻译官方手册对三个对象的描述:
GameObject:Unity 场景中所有实体的基类
Transform:对象的位置、旋转和规模
Component:附加到游戏对象的部件的基类 - 属性描述:
我们打开Unity,在右边的Inspector中显示如下:
首先是activeSelf,有对象的名称,标签和层次等属性
其次是Transform,有对象的位置、旋转和规模
然后是Table的部件,其包括Mesh Filter、Box Collider、Mesh Renderer等 - 画出UML图:
整理相关学习资料,编写简单代码验证以下技术的实现
我们首先建立一个基本的对象树,如下所示:
- 查找对象
我们就对Cube1
这个对象进行查找:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class FindObject : MonoBehaviour {
// Use this for initialization
void Start ()
{
Debug.Log("Start");
}
// Update is called once per frame
void Update ()
{
Debug.Log("Update");
var target = GameObject.Find("Cube1");
if (target == null)
Debug.Log("cannot find");
else
Debug.Log("can find");
}
}
运行结果:
2. 添加子对象
我们给Cube1
这个对象添加一个子对象,名称为SubObject
:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class AddSubObject : MonoBehaviour {
// Use this for initialization
void Start ()
{
Debug.Log("Start");
GameObject SubObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
SubObject.name = "newSubObject";
SubObject.transform.position = new Vector3(0, 0, 0);
SubObject.transform.parent = this.transform;
}
// Update is called once per frame
void Update ()
{
Debug.Log("Update");
}
}
运行结果:
3. 遍历对象树
我们遍历Cube1
这个对象的所有子对象:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TraverseObjectTree : MonoBehaviour {
// Use this for initialization
void Start ()
{
Debug.Log("Start");
foreach (Transform child in transform)
Debug.Log(child.name);
}
// Update is called once per frame
void Update ()
{
Debug.Log("Update");
}
}
运行结果:
4. 清除所有子对象
我们清除Cube1
这个对象的所有子对象:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class ClearAllSubObject : MonoBehaviour {
// Use this for initialization
void Start ()
{
Debug.Log("Start");
foreach (Transform child in transform)
Destroy(child.gameObject);
}
// Update is called once per frame
void Update ()
{
Debug.Log("Update");
}
}
运行结果:
资源预设与对象克隆
- 预设有什么好处?
预设的目的是创建一类相同属性的对象,这些对象的属性和预设相关联,只要改变预设即可对这些所有的对象进行改变,方便操作,能够批量化的处理多对象。 - 预设与对象克隆关系?
一旦改变预设,预设得到的所有对象都将作相对应的改变;而如果使用对象克隆,比方说我们利用物体一克隆得到了物体二,物体一作了改变不会影响物体二。 - 制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
我们还是利用刚才的对象树,给对象Cube1
预设一个子对象
其代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Prefabs : MonoBehaviour {
public GameObject temp;
// Use this for initialization
void Start ()
{
Debug.Log("Start");
GameObject instance = (GameObject)Instantiate(temp, new Vector3(0, 0, 0), transform.rotation);
instance.transform.parent = this.transform;
}
// Update is called once per frame
void Update ()
{
Debug.Log("Update");
}
}
运行结果:
编程实践
我们使用IMGUI做一个井字棋的小游戏:
比较关键的步骤是创建按钮和判断获胜条件,游戏的布局上我们可以直接定义一个二维数组,即是chess[3,3]
,再利用循环和使用new Rect()
函数来完成。
由于是3*3的布局,所以在判断胜利条件上,我们只需看是否有三个相同的符号排成一列、一行或者一条对角线即可,并且使用emptyPlace
这个变量表示剩余的空位,以此来决定整局游戏是否结束。如果已经没有空位但双方都没有获胜,则被视为平局。
最后我们还对"X"和"O"的形状做了一些调整,使用Resource.load()
函数调用了本地的资源,使得其更加美观。
最终代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Game : MonoBehaviour
{
private int emptyPlace = 9;
private int turn = 1;
private int[,] chess = new int[3, 3];
private Texture CrossImg;
private Texture CircleImg;
void Awake()
{
CrossImg = Resources.Load("Cross") as Texture;
CircleImg = Resources.Load("Circle") as Texture;
}
// Use this for initialization
void Start()
{
emptyPlace = 9;
turn = 1;
for (int i = 0; i < 3; ++ i)
for (int j = 0; j < 3; ++ j)
chess[i, j] = 0;
}
// Update is called once per frame
void Update()
{
}
private void OnGUI()
{
GUI.skin.button.fontSize = 20;
GUI.skin.label.fontSize = 20;
if (GUI.Button(new Rect(225, 230, 100, 50), "Reset"))
Start();
int res = Win();
if (res == 1)
GUI.Label(new Rect(225, 20, 100, 50), "X wins");
else if (res == 2)
GUI.Label(new Rect(225, 20, 100, 50), "O wins");
else if (res == 3)
GUI.Label(new Rect(250, 20, 100, 50), "Tie");
for (int i = 0; i < 3; ++i)
{
for (int j = 0; j < 3; ++j)
{
if (chess[i, j] == 1)
GUI.Button(new Rect(i * 50 + 200, j * 50 + 60, 50, 50), CrossImg);
if (chess[i, j] == 2)
GUI.Button(new Rect(i * 50 + 200, j * 50 + 60, 50, 50), CircleImg);
if (GUI.Button(new Rect(i * 50 + 200, j * 50 + 60, 50, 50), ""))
{
if (res == 0)
{
if (turn == 1)
chess[i, j] = 1;
if (turn == 2)
chess[i, j] = 2;
emptyPlace--;
if (emptyPlace % 2 == 1)
turn = 1;
else
turn = 2;
}
}
}
}
}
int Win()
{
// 0 for not finished & 1 for "X" wins & 2 for "O" wins & 3 for a tie
int center = chess[0, 0];
if (center != 0)
{
if ((center == chess[0, 1] && center == chess[0, 2]) ||
(center == chess[1, 0] && center == chess[2, 0]))
return center;
}
center = chess[1, 1];
if (center != 0)
{
if ((center == chess[0, 0] && center == chess[2, 2]) ||
(center == chess[0, 1] && center == chess[2, 1]) ||
(center == chess[1, 0] && center == chess[1, 2]) ||
(center == chess[0, 2] && center == chess[2, 0]))
return center;
}
center = chess[2, 2];
if (center != 0)
{
if ((center == chess[2, 0] && center == chess[2, 1]) ||
(center == chess[0, 2] && center == chess[1, 2]))
return center;
}
if (emptyPlace == 0)
return 3;
else
return 0;
}
}
UI界面:
思考题
- 微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。为什么是“模板方法”模式而不是“策略模式”呢?
策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户;而模板方法模式在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。根据题目中的意思,“Game对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们”,则Game对象是一个抽象类,声明了一些抽象方法让子类实现其剩余的逻辑。在Game对象中可能还有某些子类共有的属性,所以这是模板方法模式,策略模式用于封装不同算法的是“接口”,这个“接口”类中往往不含属性。 - 将游戏对象组成树型结构,每个节点都是游戏对象(或数)
- 尝试解释组合模式(Composite Pattern / 一种设计模式)
又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树 形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。 - 使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?
- 尝试解释组合模式(Composite Pattern / 一种设计模式)
BroadcastMessage()
{
for each childObject
sendMessage();
}
- 一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)
- 这是什么设计模式?
策略模式 - 为什么不用继承设计特殊的游戏对象?
因为在游戏调试过程中,我们往往会需要频繁变动游戏对象的组件,如果采用继承的方式进行设计,将使代码耦合性高而内聚性低,代码不灵活,会使得调试变难。
- 这是什么设计模式?