文章目录
- 简答题
- 编程实践——井字棋
- 思考题
简答题
解释游戏对象与资源的区别与联系
区别
游戏对象:是放置各种组件(包括常用的变换组件等)的一个容器,代表了各种人物、道具或者是场景,对象本身不能完成很多工作,但是里面的组件共同完成功能。
资源:是自行设计或者从商店下载的,可以附加在游戏对象上的一种素材,可以是对象外观、脚本、音频等,可以被多个不同的对象使用。相比于对象来讲,资源更加丰富,像是一种可扩展的模板包。
联系
资源作为对游戏对象的辅助,既可以被对象使用,增强其外观或功能。资源本身也能够实例化,采用预制的方式可以实例化一个对象。
下载几个游戏案例,分别总结资源、对象组织的结构
对象组织结构
以下是一个3d赛车游戏(游戏项目链接)的开始菜单面板的结构:
这个层次结构就跟我们的分类一样,跟编程语言中的类的结构也是一样。
主菜单由多个按钮聚合而成,而按钮又是由文字等对象构成的。其余结构也类似。
也可以参考老师课件的内容:
资源的结构
资源结构包括了素材、模型、预制、文字、脚本、场景等等。
编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件
编写以下代码,并将脚本组建添加到某个游戏对象中去。
点击运行,查看控制台输出:
可以参考官方文档的说法,MonoBehaviour 基本行为或事件触发的条件:
中文手册可参考:游戏蛮牛
Awake:当一个脚本实例被载入时Awake被调用。
Start:Start仅在Update函数第一次被调用前调用。
FixedUpdate:当MonoBehaviour启用时,其 FixedUpdate 在每一固定帧被调用。这个固定帧的时间可以在Edit->Project Setting->Time->Fixed Timestep来设置。通常用于物理属性的更新操作,使其变化更平滑。
Update:在每一帧被调用。相比于FixedUpdate,Update更新的时间不固定,收到被渲染的物体的影响,时快时慢。是最常用的一个函数。
LateUpdate:在所有Update函数调用后才被调用。可以用于相机追踪物体的实现。由于每个Update先后顺序是随机的,所以可能会出现相机先移动而造成景象抖动的情况。
OnEnable:当对象变为可用或激活状态时此函数被调用。
OnDisable:当对象被禁用或销毁的时候调用。(在这次测试中,停止运行时,也就是在控制台最后被调用)。
OnGUI:为了响应GUI事件,每帧会被调用多次(一般最低两次)。
查找脚本手册,了解 GameObject,Transform,Component 对象
翻译官方脚本手册描述
(以下描述结合了部分Manual手册描述)
- GameObject
所有在Unity场景中的实体的基类,是放置各种组件的一个容器,由各类组件完成该对象的功能。 - Transform
变换组件,每一个对象都拥有的组件。决定一个对象的位置、旋转角度、缩放比例。 - Component
一切附加到游戏物体的基类。游戏对象实现功能的主体。
描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table的部件(图不见了??)
- table实体属性
第一个选择框是决定是否激活对象,文本框对应的是对象实体的名字,Static选择框则是对象是否标记为静态对象(不太理解)。
还有Tag和Layer属性。 - Tranform属性
包括位置XYZ坐标、XYZ方向的旋转角度、沿XYZ轴的缩放比例。 - 部件
Transform,Mesh Filter,Box Collider,Mesh Renderer,还有自己添加的Script脚本。
用 UML 图描述三者的关系(GameObject, Transform,Component)
整理相关学习资料,编写简单代码验证以下技术的实现
查找对象
var obj = GameObject.Find("Cube");
if (obj != null) Debug.Log("Cube was Found!");
添加子对象
GameObject obj2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj2.name = "chair1";
obj2.transform.position = new Vector3((float)(1.5), (float)(-0.3), 0);
obj2.transform.localScale = new Vector3(1, (float)0.3, 1);
obj2.transform.parent = this.transform;
遍历对象树
GameObject[] objs = GameObject.FindObjectsOfType<GameObject>();
foreach (GameObject tmp in objs) {
Debug.Log(tmp.name);
}
清除所有子对象
int count = transform.childCount;
Debug.Log(count);
for (int i = 0; i < count; i ++) {
Debug.Log("Destroy " + transform.GetChild(i).gameObject.name);
Destroy(transform.GetChild(i).gameObject);
}
执行结果
可以看到分别执行了寻找对象(Cube was Found!),添加了一个chair1的子对象,遍历对象树所有对象(包括摄像机和光照),删除Cube的所有子对象。
完整代码
将以上代码整合并放置到Start中
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Object_Operation : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
var obj = GameObject.Find("Cube");
if (obj != null) Debug.Log("Cube was Found!");
GameObject obj2 = GameObject.CreatePrimitive(PrimitiveType.Cube);
obj2.name = "chair1";
obj2.transform.position = new Vector3((float)(1.5), (float)(-0.3), 0);
obj2.transform.localScale = new Vector3(1, (float)0.3, 1);
obj2.transform.parent = this.transform;
GameObject[] objs = GameObject.FindObjectsOfType<GameObject>();
foreach (GameObject tmp in objs) {
Debug.Log(tmp.name);
}
int count = transform.childCount;
Debug.Log(count);
for (int i = 0; i < count; i ++) {
Debug.Log("Destroy " + transform.GetChild(i).gameObject.name);
Destroy(transform.GetChild(i).gameObject);
}
}
// Update is called once per frame
void Update()
{
}
}
资源预设(Prefabs)与 对象克隆 (clone)
- 预设的好处
使得一组对象能够保存到资源栏,并且重新实例化,增加效率和提高利用率。
在编辑时,如果改变预制属性,所有放到场景中的由同一个预制创建的对象都会改变。 - 联系
克隆需要场景中有可克隆的对象,但是预制的话是处于资源栏的,不存在这个问题。 - 制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
首先编写以下脚本代码,声明一个t叫able的公有变量,然后在Start阶段实例化。using System.Collections; using System.Collections.Generic; using UnityEngine; public class Perfabs : MonoBehaviour { public Transform table; // Start is called before the first frame update void Start() { Instantiate(table, transform.position, transform.rotation); } // Update is called once per frame void Update() { } }
注意到我们创建的公有变量table此时是未赋值的,所以需要将其与我们已有的table预制关联起来,只需要将资源栏的table预制拖入脚本组件的对应变量区:
最后尝试运行,发现对象树中出现了table(clone)的对象,正是实例化出来的。结果如下:
编程实践——井字棋
完整项目地址:Github
思路
- 利用IMGUI中的button可以实现监听点击事件,每次点击的时候就用一个新的button(带有棋子标识的)覆盖原来的空的button,实现“下棋”的功能。
- 给棋盘每一格设置一个状态(标识不同玩家的棋子或空棋盘),并且通过状态来判断输赢。
- 设置重置功能,将棋盘状态初始化,重新游戏。
实现
初始化函数
void init() {
//初始化棋盘每一格状态
for (int i = 0; i < 3; i ++) {
for (int j = 0; j < 3; j ++) {
array[i, j] = 0;
}
}
finish = 0; //初始化游戏结束标签
turn = 1; //初始化轮次
count = 0; //初始化棋盘上棋子数量
}
判断游戏是否结束
暴力循环,判断每行每列或者对角线是否有三个一样状态的棋子。
但是可能出现三个格子都为空的状态,所以可以当出现以上情况时,选择直接返回三个一样状态格子的状态(0,1,2分别表示未分胜负、玩家1胜、玩家2胜)
int isFinished()
{
for (int i = 0; i < 3; i ++) {
if (array[i, 0] == array[i, 1] && array[i, 1] == array[i, 2]) {
return array[i, 0];
}
}
for (int i = 0; i < 3; i ++) {
if (array[0, i] == array[1, i] && array[1, i] == array[2, i]) {
return array[0, i];
}
}
if (array[1, 1] == array[0, 0] && array[1, 1] == array[2, 2] ||
array[1, 1] == array[0, 2] && array[1, 1] == array[2, 0]) {
return array[1, 1];
}
return 0;
}
构建棋盘
这里的思路跟一开始想的有点不一样,因为直接根据点击事件来新建button的话,在下一次OnGUI渲染的时候就会重新用空的button去覆盖,导致每次看到的都是空的格子。所以需要用一个状态来表示需要渲染成什么格式的棋子,这里就可以用之前构建的3x3状态数组来表示。
//当棋子数大于等于5才有可能分出胜负,此时再判断
if (count >= 5 && isFinished() != 0) {
finish = 1;
string str = (isFinished() == 1 ? "Player1 Win!" : "Player2 Win!");
//分出胜负后,显示Label空间,输出胜利信息。
GUI.Label(new Rect(300f, 50f, 200f, 60), str, style);
}
else if (count == 9) { //如果没有分出胜负时,输出平局信息
finish = 1;
GUI.Label(new Rect(350f, 50f, 200f, 60), "Draw", style);
}
for (int i = 0; i < 3; i ++) {
for (int j = 0; j < 3; j ++) {
//每次渲染都判断当前棋盘格子是什么状态,根据状态来创建button
if (array[i,j] == 1) {
GUI.backgroundColor = Color.white;
//使用Texture类型的circle变量来引入图片
GUI.Button(new Rect(i*60 + 300f, j *60 + 100f, 60, 60), circle);
}
else if (array[i,j] == 2) {
GUI.backgroundColor = Color.white;
GUI.Button(new Rect(i*60 + 300f, j *60 + 100f, 60, 60), cross);
}
else if (GUI.Button(new Rect(i*60 + 300f, j *60 + 100f, 60, 60), white)) {
//如果棋盘状态为0,则有空的button处理点击事件
if (finish == 0) { //游戏未结束,则点击一次调换轮次,并且将空格子状态改变
array[i, j]= turn;
turn = (turn == 1 ? 2 : 1);
count ++;
}
}
}
}
一些变量声明和GUI控件添加
变量
//用于引入图片
public Texture cross;
public Texture circle;
public Texture2D white;
//用于表示状态
private int[,] array = new int [3, 3];
private int finish = 0, turn = 1, count = 0;
//定义格式
GUIStyle style = new GUIStyle();
GUIStyle tStyle = new GUIStyle();
其他控件
GUI.Label(new Rect(290, 10, 100, 100), "Tic Tac Toe", tStyle); //添加标题
//添加重置按钮
if (GUI.Button (new Rect (150, 220, 100, 50), "reset")) {
init();
}
结果演示:
思考题
- 微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。为什么是“模板方法”模式而不是“策略模式”呢?
因为策略模式是将一组算法中的每一个算法封装到具有共同接口的独立的类中,而这些算法本身可以有较大的差异,只需要保证有共同的接口,这样就可以保证在不同情况下,用户能够使用适应不同条件的算法。
而模板方法则有所不同,它定义了一个算法的基本框架和步骤,使得用户可以不改变算法结构的基础上,重新定义算法某些步骤的特殊实现。
在XNA引擎中的游戏循环基本流程是一致的,所以没有必要让用户重新实现整个算法,只需要在现有的框架上,添加各个流程的实现。防止代码重用造成冗余。
参考博客:https://blog.csdn.net/shensky711/article/details/53418034