3D游戏(2)——离散仿真引擎基础


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)允许向一个现有的对象添加新的功能,同时又不改变其结构。” 动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。

猜你喜欢

转载自blog.csdn.net/qq_43278234/article/details/108681913
今日推荐