3D游戏编程与设计作业02
简答题
- 解释 游戏对象(GameObjects) 和 资源(Assets)的区别与联系
- 区别:
游戏对象(GameObjects)
:相当于一个容器,可以容纳组件。它们本身不做任何事情,需要特殊属性(special properties)才能成为一个角色、一种环境或者一种特殊效果
资源(Assets)
:在项目中可以导入使用的文件,包括图像、视频、脚本文件、预制文件等 - 联系:
游戏对象可以通过资源保存起来
资源可以用来创建对象实例
一个资源可以创建多个对象
- 下载几个游戏案例,分别总结资源、对象组织的结构(指资源的目录组织结构与游戏对象树的层次结构)
以下下载了三个游戏案例:Fantasy Forest Environment Free Sample、race-trace-lake以及unity-chan!
总结——资源的目录组织结构:
-
Assets:主文件夹,包含所有工程需要用到的资源
-
Editor:所有在Editor和它的子文件夹的脚本,都不会作为运行期脚本被编译,而是作为动态添加Unity编译器功能的脚本来编译,在该文件夹和其子文件夹的脚本不能被添加到GameObject上
-
Materials:材料
-
Models:模型
-
Prefabs:预设
-
Scenes:场景
-
Textures:纹理
-
Scripts:C#脚本
游戏对象树的层次结构的组织则主要是一个继承或是组合/聚合的关系
- 编写一个代码,使用 debug 语句来验证
MonoBehaviour
基本行为或事件触发的条件
- 基本行为包括
Awake()
、Start()
、Update()
、FixedUpdate()
、LateUpdate()
- 常用事件包括
OnGUI()
、OnDisable()
、OnEnable()
代码如下所示:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
private void Awake()
{
Debug.Log("awake\n");
}
// Start is called before the first frame update
void Start()
{
Debug.Log("start\n");
}
// Update is called once per frame
void Update()
{
Debug.Log("update\n");
}
private void FixedUpdate()
{
Debug.Log("fixed-update\n");
}
private void OnEnable()
{
Debug.Log("on-enbale\n");
}
private void OnDisable()
{
Debug.Log("on-disable\n");
}
private void OnGUI()
{
Debug.Log("on-GUI\n");
}
}
运行结果如下:
事实上,查找脚本手册可知,MonoBehavior基本行为或事件触发的条件(按执行顺序):
-
MonoBehaviour.Awake
:唤醒,当一个脚本实例被载入时Awake被调用,用于在游戏开始之前初始化变量或游戏状态。在脚本整个生命周期内它仅被调用一次。Awake
不能用作协同程序 -
MonoBehaviour.Start
:开始,仅在Update
函数第一次被调用前调用,在behavior的生命周期里只被调用一次。它和Awake
不同的是:Start
指在脚本实例被启用时调用。在所有脚本实例中,Start
函数总是在Awake
函数之后调用 -
MonoBehaviour.Update
:更新,当MonoBehaviour启用时,其Update
在每一帧被调用。Update
是实现各种游戏行为最常用的函数 -
MonoBehaviour.FixedUpdate
:固定更新,当MonoBehaviour启用时,其FixedUpdate
在每一帧被调用。处理Rigidbody
时,需要用FixedUpdate
代替Update
-
MonoBehaviour.LateUpdate
:晚于更新,当Behaviour启用时,其LateUpdate
在每一帧被调用。LateUpdate
时在所有Update
函数调用后被调用。这可用于调整脚本执行顺序 -
MonoBehaviour.OnEnable
:当可用,当对象变为可用或激活状态时此函数被调用。OnEnable
不能用于协同程序 -
MonoBehaviour.OnDisable
:当不可用,当对象变为不可用或非激活状态时此函数被调用。当物体被销毁时它将被调用,并且可用于任意清理代码。OnDisable
不能用于协同程序 -
MonoBehaviour.OnGUI
:当界面,渲染和处理GUI事件时调用。如果MonoBehaviour的enabled
属性设为false
,OnGUI()
将不会被调用P.S.
协同程序:在主程序运行时同时开启另一段逻辑处理,来协同当前程序的执行(类似开启了一个线程,但两者不同)。在Unity3D中,使用MonoBehaviour.StartCoroutine
方法即可开启一个协同程序,也就是说该方法必须在MonoBehaviour或继承于MonoBehavior的类中调用。另外,可以使用StopCoroutine(string methodName)
来终止一个线程和StopAllCoroutines()
来终止所有可以终止的协同程序,不过这两个方法都只能终止该MonoBehaviour中的协同程序。更多详细内容可自行查询脚本手册。
- 查找脚本手册,了解
GameObject
,Transform
,Component
对象
-
分别翻译官方对三个对象的描述(Description)
GameObject
:Unity场景中所有实体的基类。注意:GameObject
类中的许多变量都被移除了。对于C#语言,要想访问这些变量,例如GameObject.renderer
,需要使用GetComponent<Renderer>()
Transform
:一个对象的位置、旋转角度和大小。一个场景中的每一个大小都由一个Transform
组件。它用来存储和控制物体的位置、旋转角度和大小。每一个Transform
组件都有一个父Transform
组件,这便允许我们分层地应用位置、旋转角度和大小。而这也正是层次结构面板中显示的层次结构。它们还支持枚举,例如:
```
using UnityEngine;public class Example : MonoBehaviour { // Moves all tranform children 10 units upwards void Start() { foreach (Transform child in transform) { child.positioin += Vector3.up * 10.0f; } } } ``` `Component`:附加到GameObjects上的所有内容的基类。需要注意的是,你的代码不会直接创建一个Component,你需要将写好的脚本代码附到一个GameObject上
-
描述下图中
table
对象(实体)的属性、table
的Transform
属性、table
的部件
-table
对象(实体)的属性:
-activeSelf = true
(已勾选)
-isStatic = false
(未勾选)
-layer = Default
-scene = hello_start
-tag = Untagged
-Transform
属性:Position = (0,0,0),Rotation = (0,0,0),Scale = (1,1,1)
-table
的部件有:Transform
、MeshFilter
、BoxCollider
、MeshRenderer
-
用UML图描述三者的关系
- 整理相关学习资料,编写简单代码验证以下技术的实现
- 查找对象:
- 通过对象名称:
public static GameObject Find(string name)
,通过名字寻找对象并返回它,只返回active GameObject,如果没有GameObject,则返回null。如果名称内包含“/”字符,会当做是hierarchy中的一个路径名 - 通过标签获取单个游戏对象:
public static GameObject FindWithTag(string tag))
,返回一个用tag做标识的活动的对象,如果没有找到则为null - 通过标签获取多个游戏对象:
public static GameObject[] FindGameObjectsWithTag(string tag)
,返回一个用对象标记的标签,如果没有找到对象则返回空数组 - 通过类型获取单个游戏对象,返回类型为type的活动的第一个游戏对象
- 通过类型获取多个游戏对象,返回类型为type的所有活动的游戏对象列表
- 通过对象名称:
- 添加子对象:
public static GameObject CreatePrimitive(PrimitiveType type)
,创建一个游戏对象与原始网格渲染器和适当的collider - 遍历对象树:
foreach (Transform child in transform) {
Debug.Log(child.gameObject.name);
}
- 清除所有子对象:
foreach (Transform child in transform) {
Destroy(child.gameObject);
}
- 资源预设(Prefabs)与 对象克隆 (clone)
- 预设(Prefabs)有什么好处?
预设是一个容易复用的类模板,可以迅速方便创建大量相同属性的对象、操作简单,代码量少,减少出错概率。修改的复杂度降低,一旦需要修改所有相同属性的对象,只需要修改预设即可,所有通过预设实例化的对象都会做出相应变化。
- 预设与对象克隆(clone or copy or Instantiate of Unity Object)关系?
两者都可用于批量产生对象,但是对象克隆不受克隆本体的影响,因此A对象克隆的对象B不会因为A的改变而相应改变。 - 制作table预制,写一段代码将table预制资源实例化成游戏对象
先按课堂上演示的方法制作一个table对象(其含有四个子对象:chair1、chair2、chair3、chair4),如下图所示:
将table拖入Assets面板中,将自动生成table的预制
将table游戏对象删除,接下来利用脚本实例化成游戏对象,方法是新建一个脚本例如命名为Instantiate
,在Instantiate
类中设置数据域public GameObject myObject
,在可视化窗口,将table
预制拖到该数据域上进行赋值,如图所示
编程实践:井字棋小游戏
技术限制:仅允许使用IMGUI
构建UI
游戏设计元素:
- 玩家:Player1(三笠 · 阿克曼)与Player2(艾伦 · 叶卡)
- 游戏目标:在棋盘上成功地将自己的头像图片连续按行或按列或按对角线放置三个,则胜出
- 规则:
- 每局Player1(三笠 · 阿克曼)先下(因为设计者比较喜欢三笠)
- 玩家轮流通过点击棋盘上未放置头像图片的空位放置自己的头像图片
- 每轮玩家只能点击一个空位,即只能放置一次头像图片
- 挑战预想:玩家选择级别(棋盘大小和胜出所需的连续头像图片个数)
关键算法
- 判断胜利:
int check()
函数
int check()
{
//一行连续三个为同一种棋子,则该种棋子胜利
for (int i = 0; i < dimension; i ++)
{
for (int j = 0; j <= dimension-3; j ++)
{
if (map[i, j] == map[i, j + 1] && map[i, j] == map[i, j + 2] && map[i,j] != 0)
{
return map[i, j];
}
}
}
//一列连续三个为同一种棋子,则该种棋子胜利
for (int i = 0; i < dimension; i ++)
{
for (int j = 0; j <= dimension-3; j ++)
{
if (map[j, i] == map[j+1, i] && map[j+2, i] == map[j, i] && map[j,i] != 0)
return map[j, i];
}
}
//对角线(left-to-right)连续三个为同一种棋子,则该种棋子胜利
for (int i = 0; i <= dimension-3; i ++)
{
for (int j = 0; j <= dimension-3; j ++)
{
if (map[i, j] == map[i + 1, j + 1] && map[i, j] == map[i + 2, j + 2] && map[i,j] != 0) return map[i, j];
}
}
//对角线(right-to-left)连续三个为同一种棋子,则该种棋子胜利
for (int i = 2; i < dimension; i ++)
{
for (int j = 0; j <= dimension-3; j ++)
{
if (map[i, j] == map[i - 1, j + 1] && map[i, j] == map[i - 2, j + 2] && map[i, j] != 0) return map[i, j];
}
}
return 0;
}
- 放置头像图片(棋子):在
void onGUI()
中实现
if (GUI.Button(new Rect((float)(Screen.width/2.0-restartWidth/2.0), (float)(originY+dimension*buttonHeight), restartWidth, restartHeight), "<color=#ADD8E6>Restart</color>"))
reset();
int result = check();
if (result == 1)
GUI.Label(new Rect((float)(Screen.width / 2.0 - winboxWidth / 2.0), (float)(originY -winboxHeight), winboxWidth, winboxHeight), "<color=#FFA500><size=30>Player 1 Wins!</size></color>");
else if (result == 2)
GUI.Label(new Rect((float)(Screen.width / 2.0 - winboxWidth / 2.0), (float)(originY -winboxHeight), winboxWidth, winboxHeight), "<color=#FFA500><size=30>Player 2 Wins!</size></color>");
for (int i = 0; i < dimension; i ++)
{
for (int j = 0; j < dimension; j ++)
{
float positionX = originX + buttonWidth * i;
float positionY = originY + buttonHeight * j;
if (map[i, j] == 1)
GUI.Button(new Rect(positionX, positionY, buttonWidth, buttonHeight), img1);
else if (map[i, j] == 2)
GUI.Button(new Rect(positionX, positionY, buttonWidth, buttonHeight), img2);
else
{
if (GUI.Button(new Rect(positionX, positionY, buttonWidth, buttonHeight), "")) {
if (result == 0)
{
if (player == 1)
{
map[i, j] = 1;
player = 2;
}
else
{
map[i, j] = 2;
player = 1;
}
}
}
}
}
}
思考题
- 微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节;;,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”,为什么是“模板方法”模式而不是“策略模式”呢?
要弄清这个问题,首先我们先来看看两者的介绍:
模板方法模式是类的行为模式。准备一个抽象类,将部分逻辑以具体方法以及具体构造函数的形式实现,然后声明一些抽象方法来迫使子类实现剩余的逻辑。不同的子类可以以不同的方式实现这些抽象方法,从而对剩余的逻辑有不同的实现。这就是模板方法模式的用意。
策略模式属于对象的行为模式。其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。策略模式使得算法可以在不影响到客户端的情况下发生变化。
然后,值得一提的是微软 XNA 引擎所称的Game“对象”实际上是“类”。正如题干所描述的“Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们”,Game是一个抽象类,它声明了一些抽象方法(虚方法)来迫使子类实现剩余的逻辑,此外,Game中可能还有某些子类共有的属性。而策略模式用于封装不同算法的是“接口”,这个“接口”类中往往不含属性。
- 将游戏对象组成树型结构,每个节点都是游戏对象(或树)
- 尝试解释组合模式(Composite Pattern / 一种设计模式)
组合模式允许你将对象组合成树形结构来表现”部分-整体“的层次结构,使得客户以一致的方式处理单个对象以及对象的组合。
组合模式实现的最关键的地方是——简单对象和复合对象必须实现相同的接口。这就是组合模式能够将组合对象和简单对象进行一致处理的原因。
组合部件(Component):它是一个抽象角色,为要组合的对象提供统一的接口。
叶子(Leaf):在组合中表示子节点对象,叶子节点不能有子节点。
合成部件(Composite):定义有枝节点的行为,用来存储部件,实现在Component接口中的有关操作,如增加(Add)和删除(Remove)。
- 使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?
foreach childObject
sendMessage();
- 一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)
- 这是什么设计模式?
策略(Strategy)模式 - 为什么不用继承设计特殊的游戏对象?
采用继承的方式进行设计,将使代码耦合性高而内聚性低。在游戏调试过程中,往往需要频繁变动组件(Component),如果使用继承设计,将使得调试变难(因为很可能会产生涟漪效应)。
参考博客
[1] https://www.cnblogs.com/java-my-life/archive/2012/05/14/2495235.html?tdsourcetag=s_pctim_aiomsg
[2] https://www.cnblogs.com/java-my-life/archive/2012/05/10/2491891.html?tdsourcetag=s_pctim_aiomsg
[3] https://www.cnblogs.com/snaildev/p/7647190.html