本文是3D游戏编程与设计第二次作业的博客,分为简答题部分与编程实践部分
目录
一、简答题
1.解释 游戏对象(GameObjects)和 资源(Assets)的区别与联系
游戏对象(GameObjects):游戏场景中的事物,可以创建为 Empty、3D物体、摄像机等不同种类,是游戏资源的容器,本身不具有功能,在被赋予资源后成为具有功能的对象
游戏资源(Assets):装饰游戏对象、配置游戏对象的素材与脚本,或是预先设置好的对象预制。
2.下载几个游戏案例,分别总结资源、对象组织的结构
资源按照类别进行组织,如游戏角色(Characters)、环境(Environment)、道具(Utility)等等,每个类别下有自己的材料(Materials)、脚本(Scripts)、动画(Animator)、预制(Prefabs)等等
对象以树进行组织,每个对象具有自己的子对象,如Car下有Wheelhub、light、body等,Wheelhub内又有rightWheel、leftWheel等等。
3.编写一个代码,使用 debug 语句来验证MonoBehaviour 基本行为或事件触发的条件
3.1.基本行为包括 Awake() Start() Update() FixedUpdate() LateUpdate()
3.2.常用事件包括 OnGUI() OnDisable() OnEnable()
Monobehaviour | 触发条件 |
---|---|
Awake() | 在加载脚本实例时调用 |
Start() | 在首次调用任何 Update 方法之前启用脚本时,在帧上调用 Start |
Update() | 如果启用了 MonoBehaviour,则每帧调用 Update |
FixedUpdate() | 用于物理计算且独立于帧率的 MonoBehaviour.FixedUpdate 消息 |
LateUpdate() | 如果启用了 Behaviour,则每帧调用 LateUpdate |
OnGUI() | 系统调用 OnGUI 来渲染和处理 GUI 事件 |
OnDisable() | 该函数在行为被禁用时调用 |
OnEnable() | 该函数在对象变为启用和激活状态时调用 |
用来验证的代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class test_Monobeh : 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 ("OnGUI");
}
void OnDisable() {
Debug.Log ("OnDisable");
}
void OnEnable() {
Debug.Log ("OnEnable");
}
}
验证结果
4.查找脚本手册,了解 GameObject,Transform,Component 对象
4.1.分别翻译官方对三个对象的描述(Description)
Description:
GameObject: Base class for all entities in Unity Scenes.
Transform: Position, rotation and scale of an object.
Component: Base class for everything attached to GameObjects.
翻译
GameObject:Unity场景中所有实体的基类
Transform:一个对象的位置,旋转和大小
Component:所有绑定到GameObjects的事物的基类
4.2.描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件
- 本题目要求是把可视化图形编程界面与 Unity API 对应起来,当你在 Inspector 面板上每一个内容,应该知道对应 API。
- 例如:table 的对象是 GameObject,第一个选择框是 activeSelf 属性。
对象的属性:依次为activeSelf、name、Static、Tag、Layer、Prefab
Transform的属性:依次为Position(X、Y、Z)、Rotation(X、Y、Z)、Scale(X、Y、Z)
部件:Transform、Cube、Box Collider等
4.3.用 UML 图描述 三者的关系(请使用 UMLet 14.1.1 stand-alone版本出图)
因为电脑java环境版本为12.0.2,与UMLet 14.1.1所需1.6.0不符,所以用了UMLet 14.3.0 stand-alone版本绘制UML图
5.资源预设(Prefabs)与 对象克隆 (clone)
5.1.预设(Prefabs)有什么好处?
当某些配置好的游戏对象和资源会被重复使用时,使用预设可以省去很多重复性的工作(如创建、修改等),提高效率。
5.2.预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
联系:都是创建新对象的方式
区别:修改预设会使其创建的对象一起改变,而修改本体不会使克隆体发生变化。
5.3.制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
首先创建五个(3D Object)Cube对象,使其中的四个成为另一个的子对象,然后调整Transform中的属性,得到对象如图
拖入Assets成为预制,命名为table
删除遗留在场景中的table对象,创建Empty对象命名为init,将脚本prefab_init添加到init上,然后将table预制拖入init属性中脚本prefab_init的Prefab中
prefab_init 代码如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class prefab_init : MonoBehaviour
{
public Transform prefab;
void Start()
{
Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity);
}
}
效果如下
二、编程实践
1.任务内容
- 游戏内容: 井字棋
- 技术限制: 仅允许使用 IMGUI 构建 UI
- 作业目的:
a)了解 OnGUI() 事件,提升 debug 能力
b)提升阅读 API 文档能力
2.任务分析
阅读IMGUI文档后,发现一大要点就是在GUI事件后执行OnGUI()函数中的代码,所以想到用一个二维数组记录棋盘的信息,依据二维数组中的信息更新棋盘的状态,然后在每次GUI事件后,判断当前棋局的状态以及更新二维数组以响应。因此游戏实现的重点就是OnGUI()函数以及判断棋局信息的函数check():
OnGUI()函数中,创建一个内容的主容器并加上游戏标题,然后添加具有重置游戏功能的Reset按钮,因为该功能不受当前棋局的干扰,然后进行棋局状态的判断与UI显示,之后便是关键性的棋盘创建,我选择利用按钮作为棋子落点,先将有棋子的按钮加入棋盘,然后检测无棋子按钮的状态判断下一落点,在落子后更新数组信息。
check()函数实现较为容易,因为我们将棋盘信息储存在数组中,所以只需要获取数组中的信息就可以,依次检测横线、竖线、斜线是否出现三连子判断是否有玩家胜利,有则返回胜利玩家的信息,无则通过棋子是否落慢棋盘检测棋局是否结束,返回状态信息。
3. 代码展示
Unity版本:2020.1.6f1c1
gitee传送门
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class tictactoe : MonoBehaviour {
private int turn = 1;//player1 is 1,player2 is 2
private int[,] chessBoard = new int[3,3];//0 for blank,1 for p1,2 for p2
private float box_len = 100;
private float scrmid = Screen.width / 2;
public GUIStyle pushed_box;
void Start() {
reset();
}
void OnGUI() {
GUI.Box(new Rect(scrmid - 160, 110, 320, 500), "Tic-Tac-Toe");// Main container
if (GUI.Button(new Rect(scrmid - 50, 520, 100, 40), new GUIContent("Reset", "Click to restart")))
reset();
int situation = check();
display(situation);
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (chessBoard[i,j] == 1) {
GUI.Button(new Rect(scrmid - 1.5f * box_len + i * box_len, 200 + j * box_len, box_len, box_len), "O", pushed_box);
}
else if (chessBoard[i,j] == 2) {
GUI.Button(new Rect(scrmid - 1.5f * box_len + i * box_len, 200 + j * box_len, box_len, box_len), "X", pushed_box);
}
if(GUI.Button(new Rect(scrmid - 1.5f * box_len + i * box_len, 200 + j * box_len, box_len, box_len), "")) {
if (situation == 0) {
if (turn == 1) {
chessBoard[i,j] = turn;
turn = 2;
}
else if(turn == 2){
chessBoard[i,j] = turn;
turn = 1;
}
}
}
}
}
}
/*check the situation*/
int check() {
//check x-axis
for (int i = 0; i < 3; i++) {
if (chessBoard[i,0] != 0 && chessBoard[i,0] == chessBoard[i,1] && chessBoard[i,1] == chessBoard[i,2]) {
return chessBoard[i,0];
}
}
//check y-axis
for (int i = 0; i < 3; i++) {
if (chessBoard[0,i] != 0 && chessBoard[0,i] == chessBoard[1,i] && chessBoard[1,i] == chessBoard[2,i]) {
return chessBoard[0,i];
}
}
//check slash
if (chessBoard[1,1] != 0 && chessBoard[0,0]== chessBoard[1,1] && chessBoard[2,2] == chessBoard[1,1] ||
chessBoard[0,2] == chessBoard[1,1] && chessBoard[2,0] == chessBoard[1,1]) {
return chessBoard[1,1];
}
//check end
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; ++j) {
if (chessBoard[i,j] == 0) return 0;
}
}
//end but no one win,return a tie
return 3;
}
/*Reset the game*/
void reset() {
turn = 1;//player1 first
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
chessBoard[i,j] = 0;//each board is blank
}
}
}
/*display the situation*/
void display(int situation) {
if (situation == 0) {
if (turn == 1)
GUI.Box(new Rect(scrmid - 50, 160, 100, 35), "Player1's turn");
else if(turn == 2)
GUI.Box(new Rect(scrmid - 50, 160, 100, 35), "Player2's turn");
}
else if (situation == 1) {
GUI.Box(new Rect(scrmid - 50, 160, 100, 35), "Player1 win!");
}
else if (situation == 2) {
GUI.Box(new Rect(scrmid - 50, 160, 100, 35), "Player2 win!");
}
else if (situation == 3) {
GUI.Box(new Rect(scrmid - 50, 160, 100, 35), "A tie!");
}
}
}
4. 成果展示
加载方法:与简答题5.3的方法相同,创建Empty后将脚本tictactoe挂载在上面,需要设置代码中出现的GUIStyle,我调整了字体大小(Font Size)和位置(Content Offset)
游戏中:
一方胜利:
三、思考题
- 微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。
- 为什么是“模板方法”模式而不是“策略模式”呢?
模板方法是定义一个算法骨架后,将一些步骤的实现延迟到子类中,使子类在不改变算法结构的情况下定义算法的某些步骤;策略模式是实现与封装多个算法,使它们在使用时可以相互替换,与这种设计的思想不一致。
- 将游戏对象组成树型结构,每个节点都是游戏对象(或数)。
- 尝试解释组合模式(Composite Pattern / 一种设计模式)。
用于将一组相似的对象当作一个单一的对象,以实现对一组对象的统一处理。将对象以"部分-整体"的层次结构组合成树形结构,在高层中提供可以统一修改组中对象的函数的接口。 - 使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?
void BoradcastMessage(string message){
foreach child in this{
child.getMessage(message);
}
}
- 一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)。
- 这是什么设计模式?
装饰器模式 - 为什么不用继承设计特殊的游戏对象?
- 难以用继承机制描述各类事物
- 继承、多态这种机制会导致对象行为变得复杂