文章目录
1、简答题
解释 游戏对象(GameObjects) 和 资源(Assets)的区别与联系。
区别:游戏对象通常都是直接出现在场景中,一般有玩家、敌人、以及环境和音乐等,是具有一定属性与功能的事物的实体化,简单来说就是在游戏过程中承担一部分职能并携带一定属性的组件。而资源则是由我们预先准备好的素材,例如3D模型、音频文件,图像,脚本或Unity支持的任何其他类型的文件,在创作游戏的时候可以直接迁移、重复使用。
联系:游戏对象是资源整合的具体表现,资源可以被一个或多个游戏对象使用,有些资源作为模板,可实例化成游戏中具体的对象。而游戏对象同样也可以被保存为资源以重复使用。
下载几个游戏案例,分别总结资源、对象组织的结构(指资源的目录组织结构与游戏对象树的层次结构)
首先从爱给网下载游戏源码。
首先下载了一个天天酷跑,这游戏在当年也是火爆了好一段时间的了。
资源文件目录当中包括动画(Animations)、预制(Prefabs)、脚本(Scripts)。
同时游戏对象也就只有主摄像机(Main Camera)、玩家(Player)、背景(BG)、画布(Canvas)、事件系统(EventSystem)、游戏管理器(GameManager)。因为只是一个2D的游戏,因而大致地在设置好玩家动作的基础上,再对背景跟画布做做更改就差不多了。
编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件
基本行为包括 Awake() Start() Update() FixedUpdate() LateUpdate()
常用事件包括 OnGUI() OnDisable() OnEnable()
// Start is called before the first frame update
void Start()
{
Debug.Log("Start");
}
// Update is called once per frame
void Update()
{
Debug.Log("Update");
}
private void Awake()
{
Debug.Log("Awake");
}
private void FixedUpdate()
{
Debug.Log("FixedUpdate");
}
private void LateUpdate()
{
Debug.Log("LateUpdate");
}
private void OnGUI()
{
Debug.Log("OnGUI");
}
private void OnDisable()
{
Debug.Log("OnDisable");
}
private void OnEnable()
{
Debug.Log("OnEnable");
}
运行结果:
查找脚本手册,了解 GameObject,Transform,Component 对象
分别翻译官方对三个对象的描述(Description)
GameObject:Unity Scenes中所有实体的基类。
Transform:对象的位置,旋转和比例。场景中的每个对象都有一个变换。它用于存储和操纵对象的位置,旋转和比例。每个变换都可以有一个父级,它允许您分层应用位置,旋转和缩放。这是在“层次结构”窗格中看到的层次结构。同时它们还支持枚举器。
Component:所有附加到GameObject的部件的基类。
描述下图中 table 对象(实体)的属性、table 的 Transform 的属性、 table 的部件
首先,table对象的属性是GameObject。
第一个选择框是activeSelf属性,第二个是对象名称,第三个是static属性,下一行是标签Tag和Layer,再下一行是预设。再往后就是Transform,Mesh Filter,Box Collider,Mesh Renderer。
table对象的Transform里,Position是空间位置,Rotation是旋转角度,Scale是比例大小。
最后,table的部件有chair1,chair2,chair3,chair4。
用 UML 图描述 三者的关系
资源预设(Prefabs)与 对象克隆 (clone)
预设(Prefabs)有什么好处?
可以重复的创建具有相同结构的游戏对象。有利于资源重用,减少工作量,适合批量处理。
预设与对象克隆 (clone or copy or Instantiate of Unity Object) 关系?
预设当中产生的对象与预设的对象是紧密联系的,当预设被修改时,由此产生出来的对象也会被修改,因而适合批量处理。
而克隆中,母体和子体是相互独立的,对母体做修改,子体并不会发生改变,同理对子体做修改,母体也不会发生改变。
制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class instantiation: MonoBehaviour
{
void Start () {
GameObject table = Resources.Load("table") as GameObject;
Instantiate(table);
table.transform.position = new Vector3(0, 6, 0);
table.transform.parent = this.transform;
}
void Update () {
}
}
将脚本挂载到一个空对象上,运行即可。
2、 编程实践,小游戏
简易井字棋小游戏
对于ui界面,我并没有花太多的精力,完完全全就是使用IMGUI来构建,只需要写一个脚本挂载到一个空对象上即可。
脚本需要定义的变量如下,分别保存棋盘、当前轮次、模式(玩家/AI)、先手后手。同时定义一个init函数,用于初始化棋盘;check函数,用于判断当前棋盘的游戏状态;再则就是PVP跟PVA函数,分别对两种不同模式做处理,其核心思想就是读取玩家的操作信息,更新棋盘最终再把棋盘一格一格地渲染出来;PVP跟PVA的区别就是,PVP直接读取两个玩家的操作信息即可,而PVA在玩家2的轮次里,需要调用一个AI函数来选择下一步操作;在这里,我的AI算法用的是最low的方式,能直接取胜就直接取胜,若是对面能直接取胜那就把棋子堵了,其余情况就是按照优先选取中间格子、其次四个角落、再次选棱边格子的策略来放棋子。
上述的变量和函数的设计都只有一个目的,那就是为了更好地实现onGUI函数。从前面的实验可知,onGUI函数在每隔一段时间就会触发调用一次的,作用就是每隔一段时间就将UI渲染一遍。因而这个在函数里就是先把总体框架弄出来,最后根据两种不同模式来调用PVP或者PVA函数。
完整代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class NewBehaviourScript : MonoBehaviour
{
private int[,] board=new int[3,3];
private int turn=0;
private int mode=0;
private int initturn=0;
void init(){
turn =initturn;
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
board[i,j]=0;
}
}
}
int check(){
if(board[0,0]!=0&&board[0,0]==board[1,1]&&board[0,0]==board[2,2]) return board[0,0];
if(board[2,0]!=0&&board[2,0]==board[1,1]&&board[2,0]==board[0,2]) return board[2,0];
int cnt=0;
for(int i=0;i<3;i++){
if(board[i,0]!=0&&board[i,0]==board[i,1]&&board[i,0]==board[i,2]) return board[i,0];
if(board[0,i]!=0&&board[0,i]==board[1,i]&&board[0,i]==board[2,i]) return board[0,i];
for(int j=0;j<3;j++){
if(board[i,j]==0) cnt++;
}
}
return cnt==0? 3:0;
}
void PVP(){
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
switch(board[i,j]){
case 0:
if(GUI.Button(new Rect(Screen.width/2-120+i*100,Screen.height/2-140+j*100,100,100)," ")&&check()==0){
board[i,j]=turn+1;
turn=1-turn;
}
break;
case 1:
GUI.Button(new Rect(Screen.width/2-120+i*100,Screen.height/2-140+j*100,100,100),"O");
break;
case 2:
GUI.Button(new Rect(Screen.width/2-120+i*100,Screen.height/2-140+j*100,100,100),"X");
break;
}
}
}
}
void AI(){
if(check()!=0) return;
int cnt=0;
int[] chose=new int[9];
int[] prefer={0,(2<<2)+0,2,(2<<2)+2};
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
if(board[i,j]==0){
board[i,j]=2;
if(check()==2){
return;
}
board[i,j]=1;
if(check()==1){
board[i,j]=2;
return;
}
board[i,j]=0;
chose[cnt++]=(i<<2)+j;
}
}
}
if(board[1,1]==0){
board[1,1]=2;
return;
}
for(int i=0;i<10;i++){
int temp1=(int)Random.Range(0,4),temp2,temp;
while((temp2=(int)Random.Range(0,4))==temp1);
temp=prefer[temp1];
prefer[temp1]=prefer[temp2];
prefer[temp2]=temp;
}
for(int i=0;i<4;i++){
if(board[prefer[i]>>2,prefer[i]&3]==0){
board[prefer[i]>>2,prefer[i]&3]=2;
return;
}
}
int rd=(int)Random.Range(0,cnt);
board[chose[rd]>>2,chose[rd]&3]=2;
return;
}
void PVA(){
for(int i=0;i<3;i++){
for(int j=0;j<3;j++){
switch(board[i,j]){
case 0:
if(turn==0){
if(GUI.Button(new Rect(Screen.width/2-120+i*100,Screen.height/2-140+j*100,100,100)," ")&&check()==0){
board[i,j]=turn+1;
turn=1-turn;
}
}
else{
GUI.Button(new Rect(Screen.width/2-120+i*100,Screen.height/2-140+j*100,100,100)," ");
AI();
if(check()==0||check()==2) turn=1-turn;
}
break;
case 1:
GUI.Button(new Rect(Screen.width/2-120+i*100,Screen.height/2-140+j*100,100,100),"O");
break;
case 2:
GUI.Button(new Rect(Screen.width/2-120+i*100,Screen.height/2-140+j*100,100,100),"X");
break;
}
}
}
}
// Start is called before the first frame update
void Start()
{
init();
}
// Update is called once per frame
void Update()
{
}
void OnGUI (){
GUI.Box(new Rect(Screen.width/2-300,Screen.height/2-200,600,400), "TicTacToe");
int state=check();
switch(state){
case 0:
GUI.Box(new Rect(Screen.width/2-70,Screen.height/2-170,200,25),"进行中, 玩家"+(turn+1)+"执棋");
break;
case 1:
case 2:
GUI.Box(new Rect(Screen.width/2-70,Screen.height/2-170,200,25),"玩家"+(2-turn)+"获胜");
break;
case 3:
GUI.Box(new Rect(Screen.width/2-70,Screen.height/2-170,200,25),"平局");
break;
}
if(GUI.Button(new Rect(Screen.width/2-280,Screen.height/2,100,25),"重置")) init();
if(GUI.Button(new Rect(Screen.width/2-280,Screen.height/2-120,100,25),"玩家"+(initturn+1)+"先手")){
initturn=1-initturn;
init();
}
string temp;
if(mode==0){
temp="玩家";
}
else{
temp="AI";
}
if(GUI.Button(new Rect(Screen.width/2-280,Screen.height/2-90,100,25),"玩家2: "+temp)){
mode=1-mode;
init();
}
if(mode==0){
PVP();
}
else{
PVA();
}
}
}
3、思考题【选做】
微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。
为什么是“模板方法”模式而不是“策略模式”呢?
模板方法模式可维护性好,纵向扩展性好,只不过耦合性较高,子类无法影响父类公用模块代码;但策略模式虽横向扩展性好,灵活性高,但是客户端需要知道全部策略,若策略过多则会导致复杂度升高。
综合来说,对于微软XNA引擎的Game对象,还是可维护性好的模板方法更为实用,尚且可以降低复杂度,同时防止客户端知道全部策略。
将游戏对象组成树型结构,每个节点都是游戏对象(或数)。
尝试解释组合模式(Composite Pattern / 一种设计模式)。
组合模式(Composite Pattern),又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。其依据树形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,创建了一个包含自己对象组的类,同时该类提供了修改相同对象组的方式。
使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?
void BroadcastMessage(string fun){
foreach(child of this){
if(child.hasFunction(fun)){
child.stringToFunction(fun)();
}
}
}
一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)。
这是什么设计模式?
装饰模式
为什么不用继承设计特殊的游戏对象?
过多的继承容易导致类与类之间结构混乱。
“装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。” 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。