找回密码
 立即注册
查看: 716|回复: 0

[笔记] 1.7 从0开始学习Unity游戏开发--物体的组成

[复制链接]
发表于 2023-3-8 15:26 | 显示全部楼层 |阅读模式
前篇我们简单的入门了对于一个游戏场景(Scene)如何使用编辑器进行编辑,这是我们进行游戏内容创作的重要工作流程,但是目前为止,我们能操作的最小单位也只是物体,并没有深入到物体本身,本篇我们将借助Inspector面板和代码来讲解组成游戏世界的物体,到底是什么,以及我们如何进行操纵。
Inspector面板

上篇文章我们也简单提了这个面板,这里我们详细了解一下。
在选中任何编辑器上的物体时,不论是从Hierarchy选中还是直接从Scene窗口里面选中的,都会显示其内容,而物体内容显示的面板则是Inspector面板。


对于一个内建的3D立方体Cube,则是这样的,我点击了左侧的箭头,收起了每个部分的细节,这样能够有一个直观的感受。
从代码层面构建游戏物体

假设我们需要自己用代码来设计游戏物体这样一个泛用的概念,学过面向对象编程的应该都会第一时间想起那个经典的例子:
// 基类是Person,代表所有子类都是人类
class Person {};
// Student继承自Person,可以带有Person的能力,又能有自己的特殊功能
class Student : public Person {};
// Doctor也是类似
class Doctor : public Person {};
这样设计的好处就是复用了子类可以复用父类的能力,还能利用多态对相同接口做出不同的响应,从而实现一些统一的封装逻辑。
对于一些方向,确实很好用,例如UI框架就是非常典型的可以一层一层继承下去的场景。
但是对于游戏,我们可以这样设想一下:
// 基类是GameObject,代表所有物体都是GameObject
class GameObject{};
// 立方体Cube继承自GameObject
class Cube : public GameObject {};
// 球形Sphere继承自GameObject
class Sphere : public GameObject {};
这样设计看起来好像也没啥问题,但是如果有这样的情况呢:

  • 所有物体都需要支持物理碰撞,那是不是得中间加个PhysicalGameObject来作为抽象?
  • 新增一个物体玩家,到底应该继承自Cube还是Sphere还是单独再写一个继承自GameObject?如果是重新从GameObject继承,那Cube,Sphere和玩家都需要的渲染功能是不是只能放在GameObject里面?
等等。
这些问题其实就反应出了游戏世界其实是非常灵活的,一开始设计的基类,可能因为子类共同所需的功能越来越多而急剧膨胀,最后的结果就会出现一个非常大的GameObject类而导致无法维护或者效率低下。
所以在Unity和UE4中,对于游戏物体的功能组成,都是使用的组件形式的设计,类似如下:
// 仍然有一个基类来通用的代表所有游戏物体,但是不再被继承
class GameObject
{
public:
    const GameObjectComponent* GetComonent(size_t componentIndex) const
    {
        // 返回对应的组件指针
    }

    // 不要纠结这里返回下标会被后面的增删所影响,这里只是一个示例,实际实现肯定还是遍历查找
    size_t AddComponent(GameObjectComponent* component)
    {
        mComponents.push_back(component);
        return mComponents.size() - 1;
    }
private:
    // 有一个数组成员,保存这个物体有哪些功能组件
    std::vector<GameObjectComponent*> mComponents;
};而功能组件则可以被各种继承
// 游戏物体功能组件的基类
class GameObjectComponent
{
public:
    // 组件创建的时候需要将自己设置给GameObject,以便后续功能访问GameObject的资源
    GameObjectComponent(GameObject* owner)
    {
        owner->AddComponent(this);
        mGameObject = owner;
    }

protected:
    GameObject* mGameObject;
};

// 渲染相关的功能组件
class RenderComponent : public GameObjectComponent
{
public:
    virtual void Render();
}

// 移动相关的功能组件
class MoveComponent : public GameObjectComponent
{
public:
    virtual void Move();
}

// 玩家的移动功能组件可以在基础移动组件上进行扩展或者改写
class PlayerMoveComponent : public MoveComponent
{
public:
    virtual void Move() override;
}可以看到功能组件是通过加入到GameObject下的成员来实现给GameObject提供功能的。
这样做的好处有几点:

  • GameObject会保持在一个比较小的体积,这无论是对开发效率还是运行效率都会有帮助,特别是避免了巨大GameObject类带来后续难以维护的问题。
  • 每个组件可以不用卡的那么死,即使组件A和组件B的功能有所重叠都没有关系。
  • 组件也可以使用继承,而且因为功能划分出来后需要设计的功能范围小了很多,更可能一次性想清楚后面大概的开发走向,而不会导致基类膨胀。
不管怎么说,继承和组合这两种设计方法都不是银弹,所以没有谁一定比谁更好,只不过不同的项目根据自身情况选择了其中一种来实现功能而已,而Unity和UE4都使用了组合来实现游戏物体的功能也是有其本身的背景。
在编辑器上的体现



大概就是这样的效果,也就是说Hierarchy窗口里面每个节点,除去根节点,都是一个GameObject,GameObject自身带有一些属性:


而每个GameObject下可以带有一个或多个组件,首当其冲的是Transform组件,也就是表示物体的位置旋转和大小的信息,很显然这个非常重要,以至于Unity把这个作为默认组件,所有GameObject创建必带的组件,而且无法删除。
其他组件则可以进行删除,点击右边三个点就可以选择Remove Component来删除这个组件:


但是要注意的是有些时候组件和组件之间有依赖关系,不先删除依赖这个组件的组件,则无法删除被依赖的组件。当然这里针对Cube我们并没有遇到。
既然有删除,那么也有新增组件吧,很显然那个大大的Add Component按钮就是新增组件:


点击之后会显示一个菜单,然后根据功能分类你可以快速选到你想要的组件,而我们如果知道组件的名字,也可以直接在输入框里面输入名字进行查找,例如我们需要加个Box Collider组件,输入Box其实就可以搜到了:


Unity的实际代码

学会了编辑器上对一个GameObject进行增删组件,那么如果上面没有我们想要的功能,如何自己开发呢?
在Unity中,使用的是C#代码而非C++,这里不讨论哪种语言更好,这没啥意义。基于很多历史原因和综合考虑,Unity选择了门槛更低的C#来作为开发语言,那么我们首先肯定需要学会C#语法,这里肯定就不会讲了,语法随便网上搜就完事了。
那么我们如何在Unity中开始写代码呢?
跟新建一个场景资源文件一样,我们写的代码肯定要新建一个代码文件,那么操作是一模一样的,我们需要在Project窗口里面你喜欢的目录右键,Create->C# Script。
那么就会得到一个新的.cs后缀的文件,这个文件就是类似.cpp .c一样的,新建完成后,双击打开。
Unity默认配置的是Visual Studio,但是如果有喜好JetBrains系列IDE的,可以配置为Rider作为代码开发的IDE,配置入口是Unity窗口左上角的Edit->Preferences,打开如下窗口,可以切换IDE


我自己是使用Rider的,因为真的更丝滑,并且支持代码里面显示资源引用代码的情况(默认情况只显示代码引用代码的情况),这对游戏开发来说非常实用。
当然我们新手教程下,用啥IDE都一样,就算用记事本也无所谓,这里我用rider作为示例,因为开发过程中并不需要碰编译或者编译配置,所以只做代码文件编写的话,不同IDE之间没有显著的使用区别。
这里我新建了一个DemoTest.cs文件,新建完成后双击打开后可以看到这样的代码:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DemoTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
这是Unity根据你输入的文件名来预先创建的模板代码,而这个模板其实就是GameObject组件的模板,也就是说这里MonoBehaviour基类就是我们刚说的GameObjectComponent的子类。
而这里面自动给我们写了两个函数,Start和Update,其实就是Unity给组件设计的生命周期回调,也就是说到了对应的时机,Unity引擎就会利用C#的反射来魔法一样调用到你这里的代码,而无需实际写一个代码调用到你这个函数,至于反射的原理相关的东西我们后面专门再开一篇讲。
这里只需要知道Start和Update函数都跟自动生成的注释里面说的一样,游戏跑第一帧的之前,Start就会被调用,而每帧都会调用一次Update。
你可能会问了,这样写了就直接会被调用吗?跟GameObject好像没啥关系呢?
那就对了,因为目前我们还缺少一步,就是把我们的组件添加到GameObject上,还是上面说的方法,在GameObject的Inspector面板里面点击Add Component,然后输入DemoTest,你会发现你新建的这个类已经神奇般的能搜到了:


点击DemoTest,则这个组件就会被添加到这个GameObject上:


那么你会困惑GameObject又和组件代码的生命周期回调是个什么关系呢?其实就是GameObject才具有声明周期,而组件实际上是跟着GameObject一起调用声明周期回调。
现在我们给我们的组件加一点点小功能,在Start里面打印一句话:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class DemoTest : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        Debug.Log("Hello World!");
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}这里Debug类来自UnityEngine,一般IDE会自动加入using,跟Java很类似。Debug.Log就类似printf,std::cout,qDebug,Console.WriteLine , System.out.println,Unity则是Debug.Log,这个输出会输出到Console窗口:


ok,代码写好了,返回Unity后,Unity会自动检测到代码变更并编译,进度条跑完后,我们就可以点击播放按钮把游戏跑起来:


这个时候你就可以看到Console窗口里面已经输出了我们写的内容了:


下一章

本章花了不少篇幅来讲解Unity是如何设计GameObject,然后初步的使用了C#代码来创建了一个组件,了解了如何在Unity里面新增自己的代码,当然Unity里面写代码肯定远远不止给GameObjec添加新组件,但是这个却是非常重要的基础。
下一章我们会继续着手组件编写上,我们会进一步了解Inspector上每个组件可以修改的参数是怎么搞出来的,以及Unity是如何不通过写调用代码就能调用到我们新增的代码函数的。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-21 20:37 , Processed in 0.111167 second(s), 24 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表