作业二

本文深入解析游戏对象与资源的关系,通过Unity案例演示游戏对象的构建与组织,讲解资源预设与对象克隆的应用。并通过代码示例验证MonoBehaviour的行为与事件触发,最后实践井字棋小游戏,涵盖游戏开发的基础概念与Unity引擎的实用技巧。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

简答题

解释游戏对象和资源的区别与联系

  游戏对象:是指出现在游戏的场景中,一个有独立的属性并且能够容纳、实现实际功能的组件
  资源:是指可能用到的各种源文件,比如音频、贴图、模型、脚本等等
  这两者的区别可以从定义看出,游戏对象是一个独立、有功能的组件,而游戏资源只是一些碎片。这两者的联系是,我们需要利用这里"碎片"来进行游戏对象的构建,将资源实例化成为具体的一个游戏对象。

下载几个游戏案例,分别总结资源、对象组织的结构

  我们打开Unity的Windows->Asset Store,下载游戏案例Space Shooter,然后再进行导入。
  导入完成之后,我们可以看见游戏资源的结构如下:
在这里插入图片描述
  其中,包括一些脚本、场景、材质、模型、音频等等资源类型,都进行了分类处理。然后,对每一种资源类型其又进行了细分,比如预制建筑文件夹中,又有其专门的脚本类型等等。
  游戏对象的结构如下所示:
在这里插入图片描述
  我们可以看到游戏对象的结构也和游戏资源的结构类似,分为背景、玩家、边界、信息显示、光线等等类型,每种类型有细分了很多小类,比如信息显示分成了游戏结束的显示、游戏分数的显示等等。

编写一个代码,使用 debug 语句来验证 MonoBehaviour 基本行为或事件触发的条件

  我们建立一个新的C# 脚本,使用debug语句进行验证:
在这里插入图片描述  完整的代码如下:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class New : MonoBehaviour {

    private 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");
    }

    private void FixedUpdate()
    {
        Debug.Log("FixedUpdate");
    }

    private void LateUpdate()
    {
        Debug.Log("LateUpate");
    }

    private void OnGUI()
    {
        Debug.Log("OnGUI");
    }

    private void OnDisable()
    {
        Debug.Log("OnDisable");
    }

    private void OnEnable()
    {
        Debug.Log("OnEnable");
    }

}

  然后我们将这个脚本添加到玩家上,可以在控制台看见输出:
在这里插入图片描述
  进行分析我们可以得出:
  基本行为:
    Awake():当一个脚本实例被载入时Awake被调用。或者脚本构造时调用
    Start():第一次进入游戏循环时调用
    Update():所有 Start 调用完后,被游戏循环调用
    Fixedupdate():每个游戏循环,由物理引擎调用
    Lateupdate():所有 Update 调用完后,被游戏循环调用
  常用事件:
    OnGUI():游戏循环在渲染过程中,场景渲染之后调用
    OnEnable():当对象变为可用或者激活状态时被调用
    OnDisable():当对象变为不可用或者非激活状态时被调用

查找脚本手册,了解 GameObject,Transform,Component 对象

  1. 分别翻译官方手册对三个对象的描述:
    GameObject:Unity 场景中所有实体的基类
    Transform:对象的位置、旋转和规模
    Component:附加到游戏对象的部件的基类
  2. 属性描述:
    我们打开Unity,在右边的Inspector中显示如下:
    在这里插入图片描述
    首先是activeSelf,有对象的名称,标签和层次等属性
    其次是Transform,有对象的位置、旋转和规模
    然后是Table的部件,其包括Mesh Filter、Box Collider、Mesh Renderer等
  3. 画出UML图:
    在这里插入图片描述

整理相关学习资料,编写简单代码验证以下技术的实现

  我们首先建立一个基本的对象树,如下所示:
在这里插入图片描述

  1. 查找对象
    我们就对Cube1这个对象进行查找:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class FindObject : MonoBehaviour {

	// Use this for initialization
	void Start ()
    {
        Debug.Log("Start");
	}
	
	// Update is called once per frame
	void Update ()
    {
        Debug.Log("Update");
        var target = GameObject.Find("Cube1");
        if (target == null)
            Debug.Log("cannot find");
        else
            Debug.Log("can find");
    }
}

  运行结果:
在这里插入图片描述
2. 添加子对象
   我们给Cube1这个对象添加一个子对象,名称为SubObject

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AddSubObject : MonoBehaviour {


	// Use this for initialization
	void Start ()
    {
        Debug.Log("Start");
        GameObject SubObject = GameObject.CreatePrimitive(PrimitiveType.Cube);
        SubObject.name = "newSubObject";
        SubObject.transform.position = new Vector3(0, 0, 0);
        SubObject.transform.parent = this.transform;
    }
	
	// Update is called once per frame
	void Update ()
    {
        Debug.Log("Update");
    }
}

  运行结果:
在这里插入图片描述
3. 遍历对象树
  我们遍历Cube1这个对象的所有子对象:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class TraverseObjectTree : MonoBehaviour {

	// Use this for initialization
	void Start ()
    {
        Debug.Log("Start");
        foreach (Transform child in transform)
            Debug.Log(child.name);
    }
	
	// Update is called once per frame
	void Update ()
    {
        Debug.Log("Update");
    }
}

  运行结果:
在这里插入图片描述
4. 清除所有子对象
  我们清除Cube1这个对象的所有子对象:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ClearAllSubObject : MonoBehaviour {

	// Use this for initialization
	void Start ()
    {
        Debug.Log("Start");
        foreach (Transform child in transform)
            Destroy(child.gameObject);
    }
	
	// Update is called once per frame
	void Update ()
    {
        Debug.Log("Update");
    }
}

  运行结果:
在这里插入图片描述

资源预设与对象克隆

  1. 预设有什么好处?
      预设的目的是创建一类相同属性的对象,这些对象的属性和预设相关联,只要改变预设即可对这些所有的对象进行改变,方便操作,能够批量化的处理多对象。
  2. 预设与对象克隆关系?
      一旦改变预设,预设得到的所有对象都将作相对应的改变;而如果使用对象克隆,比方说我们利用物体一克隆得到了物体二,物体一作了改变不会影响物体二。
  3. 制作 table 预制,写一段代码将 table 预制资源实例化成游戏对象
      我们还是利用刚才的对象树,给对象Cube1预设一个子对象
      其代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Prefabs : MonoBehaviour {

    public GameObject temp;
	// Use this for initialization
	void Start ()
    {
        Debug.Log("Start");
        GameObject instance = (GameObject)Instantiate(temp, new Vector3(0, 0, 0), transform.rotation);
        instance.transform.parent = this.transform;
    }
	
	// Update is called once per frame
	void Update ()
    {
        Debug.Log("Update");
    }
}

  运行结果:
在这里插入图片描述

编程实践

  我们使用IMGUI做一个井字棋的小游戏:
  比较关键的步骤是创建按钮和判断获胜条件,游戏的布局上我们可以直接定义一个二维数组,即是chess[3,3],再利用循环和使用new Rect()函数来完成。
  由于是3*3的布局,所以在判断胜利条件上,我们只需看是否有三个相同的符号排成一列、一行或者一条对角线即可,并且使用emptyPlace这个变量表示剩余的空位,以此来决定整局游戏是否结束。如果已经没有空位但双方都没有获胜,则被视为平局。
  最后我们还对"X"和"O"的形状做了一些调整,使用Resource.load()函数调用了本地的资源,使得其更加美观。
  最终代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Game : MonoBehaviour
{

    private int emptyPlace = 9;
    private int turn = 1;
    private int[,] chess = new int[3, 3];
    private Texture CrossImg;
    private Texture CircleImg;


    void Awake()
    {
        CrossImg = Resources.Load("Cross") as Texture;
        CircleImg = Resources.Load("Circle") as Texture;
    }

    // Use this for initialization
    void Start()
    {
        emptyPlace = 9;
        turn = 1;
        for (int i = 0; i < 3; ++ i)
            for (int j = 0; j < 3; ++ j)
                chess[i, j] = 0;
    }

    // Update is called once per frame
    void Update()
    {

    }

    private void OnGUI()
    {
    	GUI.skin.button.fontSize = 20;
        GUI.skin.label.fontSize = 20;
        if (GUI.Button(new Rect(225, 230, 100, 50), "Reset"))
            Start();
        int res = Win();
        if (res == 1)
            GUI.Label(new Rect(225, 20, 100, 50), "X wins");
        else if (res == 2)
            GUI.Label(new Rect(225, 20, 100, 50), "O wins");
        else if (res == 3)
            GUI.Label(new Rect(250, 20, 100, 50), "Tie");
        for (int i = 0; i < 3; ++i)
        {
            for (int j = 0; j < 3; ++j)
            {
                if (chess[i, j] == 1)
                    GUI.Button(new Rect(i * 50 + 200, j * 50 + 60, 50, 50), CrossImg);
                if (chess[i, j] == 2)
                    GUI.Button(new Rect(i * 50 + 200, j * 50 + 60, 50, 50), CircleImg);
                if (GUI.Button(new Rect(i * 50 + 200, j * 50 + 60, 50, 50), ""))
                {
                    if (res == 0)
                    {
                        if (turn == 1)
                            chess[i, j] = 1;
                        if (turn == 2)
                            chess[i, j] = 2;
                        emptyPlace--;
                        if (emptyPlace % 2 == 1)
                            turn = 1;
                        else
                            turn = 2;
                    }
                }
            }
        }
    }

    int Win()
    {
        // 0 for not finished & 1 for "X" wins & 2 for "O" wins & 3 for a tie
        int center = chess[0, 0];
        if (center != 0)
        {
            if ((center == chess[0, 1] && center == chess[0, 2]) ||
                (center == chess[1, 0] && center == chess[2, 0]))
                return center;
        }
        center = chess[1, 1];
        if (center != 0)
        {
            if ((center == chess[0, 0] && center == chess[2, 2]) ||
                (center == chess[0, 1] && center == chess[2, 1]) ||
                (center == chess[1, 0] && center == chess[1, 2]) ||
                (center == chess[0, 2] && center == chess[2, 0]))
                return center;
        }
        center = chess[2, 2];
        if (center != 0)
        {
            if ((center == chess[2, 0] && center == chess[2, 1]) ||
                (center == chess[0, 2] && center == chess[1, 2]))
                return center;
        }
        if (emptyPlace == 0)
            return 3;
        else
            return 0;
    }
}

UI界面:
在这里插入图片描述

思考题

  1. 微软 XNA 引擎的 Game 对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们,我们称这种设计为“模板方法模式”。为什么是“模板方法”模式而不是“策略模式”呢?
      策略模式定义了算法族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户;而模板方法模式在一个方法中定义一个算法骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。根据题目中的意思,“Game对象屏蔽了游戏循环的细节,并使用一组虚方法让继承者完成它们”,则Game对象是一个抽象类,声明了一些抽象方法让子类实现其剩余的逻辑。在Game对象中可能还有某些子类共有的属性,所以这是模板方法模式,策略模式用于封装不同算法的是“接口”,这个“接口”类中往往不含属性。
  2. 将游戏对象组成树型结构,每个节点都是游戏对象(或数)
    • 尝试解释组合模式(Composite Pattern / 一种设计模式)
      又叫部分整体模式,是用于把一组相似的对象当作一个单一的对象。组合模式依据树 形结构来组合对象,用来表示部分以及整体层次。这种类型的设计模式属于结构型模式,它创建了对象组的树形结构。
    • 使用 BroadcastMessage() 方法,向子对象发送消息。你能写出 BroadcastMessage() 的伪代码吗?
BroadcastMessage()
{
	for each childObject
		sendMessage();
}
  1. 一个游戏对象用许多部件描述不同方面的特征。我们设计坦克(Tank)游戏对象不是继承于GameObject对象,而是 GameObject 添加一组行为部件(Component)
    • 这是什么设计模式?
        策略模式
    • 为什么不用继承设计特殊的游戏对象?
         因为在游戏调试过程中,我们往往会需要频繁变动游戏对象的组件,如果采用继承的方式进行设计,将使代码耦合性高而内聚性低,代码不灵活,会使得调试变难。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值