3D游戏编程与设计第二次作业(含C#源码)
一、简答题
1.解释 游戏对象(GameObjects)和 资源(Assets)的区别与联系
-
游戏对象(GameObjects)
Unity中最基础的部件,游戏中的角色、道具、景象等等都是游戏对象。但游戏对象本身没有任何作用,一个空的游戏对象的作用仅仅是充当容纳组件(component)的容器而已。只有当游戏对象被添加了有用的组件之后它才成为了一个游戏中的有实际意义的游戏对象。
-
资源(Assets)
是指你在游戏或游戏开发项目中所能用到的任何物品,比如音频、图像、3D模型等文件,或者prefab、component、gameobject等Unity中的物品,都可以称为资源。
2.下载几个游戏案例,分别总结资源、对象组织的结构(指资源的目录组织结构与游戏对象树的层次结构)
-
资源的目录组织结构
好的目录结构能帮助我们快速定位到想要的文件,更有利于项目开发。经过对几个游戏的Assets目录结构的观察,我总结了一个适合自己的文件存放方式:
- 3rd/ 文件夹下存放所有的第三方的插件或者类库
- Framework/ 主要存放独立的基础组件,这部分的代码和具体的业务需求无关,可以在多个不同的项目之间进行复用。当然,也可以打包成 unitypackage 方便使用
- resources/ 下面存放所有的资源文件,在 _resources/目录下面还会有更详细的分类
- Scenes/ 存放所有的场景文件,为了避免多人协作编辑同一个场景导致的冲突,所以最好将可以将页面按照需求独立成不同的场景,不同的人编辑不同的场景
- Scripts/ 存放的是所有的和项目需求相关的代码脚本
-
对象树的层次结构
游戏对象一般包括玩家,敌人,环境,摄像机等父类。父类可以包含有若干个子类,这样父类和子类一起组成了游戏中的一整个对象
3.编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件
-
基本行为包括 Awake() Start() Update() FixedUpdate() LateUpdate()
-
常用事件包括 OnGUI() OnDisable() OnEnable()
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
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");
}
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");
}
}
在Unity中运行后控制台信息如下:
4.查找脚本手册,了解 GameObject,Transform,Component 对象
-
分别翻译官方对三个对象的描述(Description)
“GameObject 游戏对象”是 Unity 中的基础对象,表示角色、道具和景物。它们本身并没有取得多大作为,但它们充当组件的容器,而组件可实现真正的功能。例如,通过将光源组件附加到游戏对象来创建光源对象。
“Transform 变换组件”确定每个对象在场景中的 Position、Rotation 和 Scale 属性的值。每个游戏对象都有一个变换组件。
“Component 组件”是游戏中对象和行为的细节,它是每个游戏对象的功能部分。
- 描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件
-
本题目要求是把可视化图形编程界面与 Unity API 对应起来,当你在 Inspector 面板上每一个内容,应该知道对应 API。
-
例如:table 的对象是 GameObject,第一个选择框是 activeSelf 属性。
table的属性:
第一行:左边勾选框为activeSelf,用于决定GameObject在场景中是否处于active(可见?)状态;中间文本框用于修改对象的名称;右边勾选框调用isStatic的API,用于决定游戏对象是否为静态
第二行:左边为tag,可对对象进行标记;右边为layer,可以选择对象的框架
第三行:prefab(预设)属性,可进行选择、恢复、应用等操作
Tansform的属性:
第一行:x,y,z用于表示物体在三维空间的位置
第二行:x,y,z用于表示物体在三个方向的旋转角度
第三行:x,y,z用于调整物体的大小(长宽高)
table的部件:
Transform, Mesh Filter, Box Collider等都属于table的部件
-
- 用 UML 图描述 三者的关系(请使用 UMLet 14.1.1 stand-alone版本出图)
5.资源预设(Prefabs)与 对象克隆 (clone)
-
预设(Prefabs)有什么好处?
当我们要创建多个相同特性的object时,可以直接从预制中获取,避免了手动创建时大量重复、冗余的工作。
-
预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
预制体实例化后产生的新的游戏对象依然保持着与预制体的关联, 也就是对预制体进行添加组件、修改属性等操作, 预制体实例化后的游戏对象都会发生相应的改变。
而克隆中,母体和子体是独立的,相互不干扰,修改子体对母体不会产生任何影响。
-
制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
如图,将左边列表处的table用鼠标拖拽至下方的Assets处即可自动生成一个table的prefab:
二、编程实践 小游戏
井字棋游戏
设计思路:
用一个3*3的二维数组 chessBoard[3] [3] 来代表逻辑棋盘,
chessBoard[i] [j] == 0表示在棋盘[i] [j]处没有棋子;
chessBoard[i] [j] == 1表示在棋盘[i] [j]处的棋子是鸣人的;
chessBoard[i] [j] == 2表示在棋盘[i] [j]处的棋子是佐助的。
在每次的OnGUI函数中:先判断棋盘状态,若已分出胜负或平局则输出结果,否则继续。然后遍历逻辑棋盘,根据chessBoard[i] [j]的值在真实棋盘的[i] [j]处生成对应的图形(实际图形为button)。当chessBoard[i] [j]为0时,生成一个添加了下棋行为的button,若为1或2生成一个添加了鸣人/佐助图片的button。
游戏截图:
代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Chess : MonoBehaviour
{
//初始化 3*3 棋盘的逻辑结构
//turn的值(1/2)用于表示轮到的玩家
//turn为1代表鸣人的回合,turn为2代表佐助的回合
//图片用于装饰棋盘
private int[,] chessBoard = new int[3, 3];
private int turn = 1;
public Texture2D Konoha, Naruto, Sasuke;
//最开始要先生成一盘新的棋局
void Start()
{
Restart();
}
//每一帧都会调用Update()
void Update()
{
}
//重置,将没有放置棋子的(i, j)棋格的逻辑变量,chessBoard[i, j]置为0
//在后面的规则中会看到:
//若Naruto在(i, j)棋格下了一枚棋则置为1,Sasuke下了棋则置为2
void Restart()
{
turn = 1;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
chessBoard[i, j] = 0;
}
}
}
//即时GUI函数
void OnGUI()
{
//创建包含游戏名称文本的button,但不需要给button添加行为,仅为了与棋盘风格统一而设置成button
GUI.Button(new Rect(Screen.width / 2 - 75, 10, 150, 65), "Tic-tac-toe");
//创建“Restart”键方便玩家随时重新开始游戏
if (GUI.Button(new Rect(200, 100, 80, 80), "Retart!"))
{
Restart();
}
//首先检查结果,State用于表示棋局的结果
//State为0、1、2分别表示: 平局、Naruto(鸣人)胜、Sasuke(佐助)胜
//还有一种情况,State为3时,表示棋局正在进行,还没成定局,因此不用打印出结果
int State = check();
if (State == 0)
{
GUI.Label(new Rect(Screen.width / 2 - 25, Screen.height - 70, 50, 50), "Equally");
}
else if (State == 1)
{
GUI.Label(new Rect(Screen.width / 2 - 25, Screen.height - 70, 50, 50), "Naruto Win!");
}
else if (State == 2)
{
GUI.Label(new Rect(Screen.width / 2 - 25, Screen.height - 70, 50, 50), "Sasuke Win!");
}
//遍历棋盘的逻辑结构(chessBoard数组)
//根据棋盘的逻辑结构(chessBoard数组) 生成、更新棋盘的显示界面(GUI)
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
//chessBoard[i, j] == 1代表鸣人,2代表佐助
//若检测到1或2,则在这个位置生成一个添加了图片的button,但不能添加行为,因为这个格子已经被下了
if (chessBoard[i, j] == 1)
{
GUI.Button(new Rect(Screen.width / 2 - 120 + 80 * i, Screen.height / 2 - 120 + 80 * j, 80, 80), Naruto);
}
else if (chessBoard[i, j] == 2)
{
GUI.Button(new Rect(Screen.width / 2 - 120 + 80 * i, Screen.height / 2 - 120 + 80 * j, 80, 80), Sasuke);
}
//若chessBoard[i, j] == 0,则在这个位置生成一个无图片但有行为的button
if (GUI.Button(new Rect(Screen.width / 2 - 120 +80 * i, Screen.height / 2 - 120 + 80 * j, 80, 80), ""))
{
//约束:仅当棋局还没有结束时才按下这个button才会有行为
if (State == 3)
{
//判定轮次,turn == 1表示现在是鸣人在下棋
//将chessBoard[i, j]置为1,1代表鸣人的棋子
//下完后轮次翻转,置turn为2,下一次便是佐助的轮次了
if (turn == 1)
{
chessBoard[i, j] = 1;
turn = 2;
}
//turn == 2表示现在是佐助在下棋
//将chessBoard[i, j]置为2,2代表佐助的棋子
//下完后轮次翻转,置turn为1,下一次便是鸣人的轮次了
else if (turn == 2)
{
chessBoard[i, j] = 2;
turn = 1;
}
}
}
}
}
}
//检查当前棋局以判定结果的函数
int check()
{
//检查每一行有没有连成三个的同色棋子
//ps:添加chessBoard[i, 0] != 0 的条件是为了判定这个格子不是空的,是有棋子在的
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 chessBoard[0, 2];
//如果前面已经决出胜方的话,函数就已经return一个结果然后退出了,
//因此函数进行到这步说明还没有决出胜方,
//此时函数返回有两种可能的结果:1、平局 2、棋局还没结束。
//为了判定是哪一种我们只需数出棋盘上的棋子数(count)即可:
//count等于9说明平局,返回0;
//count不等于9说明棋局未结束,返回3。
int count = 0;
for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 3; j++)
{
//不等于0说明这个格子上有棋子
if (chessBoard[i, j] != 0)
{
count++;
}
}
}
if (count == 9)
{
return 0;
}
return 3;
}
}