Version:

第4章 编写自己的组件

第4章 编写自己的组件

官方参考如何创建 O3DE 游戏项目: https://www.o3de.org/docs/user-guide/project-config/project-manage

游戏工程文件简介

我们在第 2 章 创建新项目中创建了一个新项目。在本章中,我们将查看涉及的文件,并向项目添加新组件。

注意:
在后面的章节中,一旦我们介绍了更多概念,例如 EBus、gem、预制件等,我们将重新审视 O3DE 中游戏项目的结构。现在,我将只介绍基础知识,让我们开始编写新组件。

本章包括以下主题:

  • 基本项目结构。
  • 添加新的 C++ 组件。
  • 在 Editor 中查找新的 C++ 组件

新项目的基本结构

注意:
此项目的源代码可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch04_creating_components

之前,我们创建了一个新项目并将其放置在 C:\git\book\MyProject 中。我们在 C:\git\book\build 中生成了包含 Visual Studio 解决方案MyProject.sln的 build 文件夹。

这里有很多项目和子文件夹,但我们将专注于基本内容。

  • ZERO_CHECK是一个特殊的 CMake 目标,它根据对 CMake 构建文件的任何更改重新构建 Visual Studio。当我们添加新组件时,我们需要构建此项目并使用更新重新加载解决方案。
  • Editor 是在我们的项目中运行 O3DE Editor 的目标。它实际上并不构建任何 Editor 代码。编辑器二进制文件由 C:\O3DE\21.11.2 中的 O3DE 安装提供。
  • MyProject.Static是用于添加新组件的静态库。

图 4.1.Visual Studio 中的 MyProject 解决方案

MyProject 是链接 MyProject.Static 并向 Editor 和游戏启动器提供组件信息的动态库。换句话说,MyProject.Static 定义组件,而 MyProject 为您的项目注册这些组件。

目前,我们在项目中拥有的唯一组件是默认的系统组件。

  • C:\git\book\MyProject\Code\Source\MyProjectSystemComponent.h
  • C:\git\book\MyProject\Code\Source\MyProjectSystemComponent.cpp

MyProject.Static 库的元素是什么?

  • Include/MyProject包含各种公共接口。在后面的章节中,当我们处理组件之间的通信时,我将介绍 O3DE 中可用的各种选项,从第 5 章 什么是 FindComponent 开始?
  • Source 包含组件的源文件和头文件,以及您可能编写的任何其他类和对象。
  • CMakeLists.txt 是定义 MyProject.Static 的生成脚本。
  • enabled_gems.cmake 处理 Gem 系统,我们将在第 12 章 什么是Gem?
  • myproject_files.cmake 定义要包含在生成和 Visual Studio 解决方案中的源文件和其他文件的列表

图 4.2.MyProject.Static 库

通常,您编写的任何新组件都将放在 MyProject\Code\Source 下,直接放在 Source 文件夹中或您选择的子文件夹下。

如果要在 C:\git\book\MyProject 下搜索对 MyProjectSystemComponent 的引用,则会在引用它的位置找到另外两个文件。

myproject_files.cmake

*_files.cmake 是 O3DE 项目文件,其中列出了要为项目包含和编译的文件。

例 4.1. MyProject\Code\myproject_files.cmake

set(FILES
    Include/MyProject/MyProjectBus.h
    Include/MyProject/MyProjectTypeIds.h
    Source/MyProjectSystemComponent.cpp
    Source/MyProjectSystemComponent.h
)
提示:
如果您希望在 Visual Studio 中更轻松地访问它们,您还可以包含源代码以外的文件,例如各种配置文件、着色器文件。构建系统会将它们从编译中排除。

你可以看到对 MyProjectSystemComponent 的源文件和头文件的引用。文件路径必须是项目 Code 文件夹的本地路径:C:\git\book\MyProject\Code。

重要:
为了将新文件添加到构建中,您必须在此文件中引用它们。
注意:
O3DE 使用 CMake 作为其构建系统。本章将仅介绍 CMake 的基本要素,以便添加新组件。接下来的每一章都将介绍足够的 CMake 知识来完成每项任务。随着您逐步阅读这些章节,您将获得所有必要的基础知识,以便对 O3DE 对 CMake 的使用感到满意。

MyProjectModule.cpp

对 MyProjectSystemComponent 的另一个引用在模块文件中。模块源文件是项目的根文件,用于声明项目,对于本章来说,最重要的是,它注册了所有组件。

        MyProjectModule()
            : AZ::Module()
        {
            // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
            m_descriptors.insert(m_descriptors.end(), {
                MyProjectSystemComponent::CreateDescriptor(),
            });
        }

在上面的代码片段中添加到 m_descriptors 的任何组件都可以在您的项目中使用,如果配置正确,还可以在 Editor 中使用。这是供参考的整个模块文件。

例 4.2.新项目的默认模块文件 MyProjectModule.cpp

#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Module/Module.h>

#include "MyProjectSystemComponent.h"

#include <MyProject/MyProjectTypeIds.h>

namespace MyProject
{
    class MyProjectModule
        : public AZ::Module
    {
    public:
        AZ_RTTI(MyProjectModule, MyProjectModuleTypeId, AZ::Module);
        AZ_CLASS_ALLOCATOR(MyProjectModule, AZ::SystemAllocator);

        MyProjectModule()
            : AZ::Module()
        {
            // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
            m_descriptors.insert(m_descriptors.end(), {
                MyProjectSystemComponent::CreateDescriptor(),
            });
        }

        /**
         * Add required SystemComponents to the SystemEntity.
         */
        AZ::ComponentTypeList GetRequiredSystemComponents() const override
        {
            return AZ::ComponentTypeList{
                azrtti_typeid<MyProjectSystemComponent>(),
            };
        }
    };
}// namespace MyProject

#if defined(O3DE_GEM_NAME)
AZ_DECLARE_MODULE_CLASS(AZ_JOIN(Gem_, O3DE_GEM_NAME), MyProject::MyProjectModule)
#else
AZ_DECLARE_MODULE_CLASS(Gem_MyProject, MyProject::MyProjectModule)
#endif
注意:
请注意,GetRequiredSystemComponents() 是注册标记为系统组件的组件的特殊方法。系统组件放置在一个特殊的系统实体上,该实体在引擎启动后立即激活,并在游戏关卡之外持续存在,直到关闭。

创建您自己的组件

注意:
创建 C++ 组件的官方参考可以在以下位置找到: https://www.o3de.org/docs/user-guide/programming/components/create-component/

您无法在 O3DE 中创建自己的自定义 AZ::Entity,但可以创建从 AZ::Component 派生的自定义组件。本章将介绍如何创建显示在 Editor 中的简单组件。我们将创建一个名为 MyComponent 的虚拟组件。通常,每当向项目添加新组件时,都必须执行以下步骤:

  • 在 MyProject\Gem\Code\Source 中创建其头文件 MyComponent.h。
  • 在同一文件夹中创建其源文件MyComponent.cpp。
  • 使用对上述两个新文件的引用更新 myproject_files.cmake。
  • 在 MyProjectModule 的构造函数中MyProjectModule.cpp注册 MyComponent。
  • 构建项目。

现在,我将介绍每个步骤。

MyComponent.h

除了现有的组件之外,最基本和最简单的组件如下所示:

例 4.3.最简单的组件

#pragma once
#include <AzCore/Component/Component.h>

namespace MyProject
{
    // An example of the simplest O3DE component
    class MyComponent : public AZ::Component
    {
    public:
        AZ_COMPONENT(MyComponent,
            "{4b589f6b-79f3-47b6-b730-aad0871d5f8f}");

        // AZ::Component overrides
        void Activate() override {}
        void Deactivate() override {}

        // Provide runtime reflection, if any
        static void Reflect(AZ::ReflectContext* reflection);
    };
}

MyComponent 必须派生自 AZ::Component。所有 O3DE 游戏组件都必须直接或通过从它派生的另一个类从 AZ::Component 派生。

宏AZ_COMPONENT。此宏使用唯一的 guid 字符串标记组件类型。第一个参数是类名。第二个参数是 guid 格式的此类的唯一标识符。您负责创建一个带有大括号的唯一 GUID。

注意:

Visual Studio IDE 在菜单中有一个内置的 guid 生成器:Tools → Create GUID。您还可以使用 Visual Code 插件甚至 PowerShell 命令生成 guid:

PS C:\work\book\MyProject> (New-Guid).ToString("B")
{dcbbe9e2-5630-4e4b-ad7a-308c7abe5abf}

AZ::Component 接口实现。Activate() 和 Deactivate() 是 AZ::Component 中的纯虚拟方法。它们不必执行任何工作,但必须被覆盖。AZ::Component 在 C:\O3DE\21.11.2\Code\Framework\AzCore\AzCore\Component\Component.h 中定义。我们稍后将讨论这些方法,因为现在它们不需要在 Editor 中显示 MyComponent。

Reflect() 方法:将此组件的运行时序列化提供给 O3DE 引擎。在下一节中,我将展示将组件反映到 Editor 所涉及的最少工作。

MyComponent.cpp

这是组件的描述和行为的位置。但是,现在我们只提供一个简单的存根。

例 4.4.最简单的MyComponent.cpp

#include "MyComponent.h"
#include <AzCore/Serialization/EditContext.h>

using namespace MyProject;

void MyComponent::Reflect(AZ::ReflectContext* reflection)
{
    auto sc = azrtti_cast<AZ::SerializeContext*>(reflection);
    if (!sc) return;

    sc->Class<MyComponent, Component>()
        ->Version(1);

    AZ::EditContext* ec = sc->GetEditContext();
    if (!ec) return;

    using namespace AZ::Edit::Attributes;
    // reflection of this component for O3DE Editor
    ec->Class<MyComponent>("My Component", "[my description]")
      ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
        ->Attribute(AppearsInAddComponentMenu, AZ_CRC("Game"))
        ->Attribute(Category, "My Project");
}
提示:
AZ_UNUSED 是一个宏,它什么都不做,只是假装使用参数来使某些编译器警告静音。在现代 C++ 中,您还可以在参数声明中使用 [[maybe_unused]]。

这足以编译项目。一切就绪后,我们将返回到 Reflect 方法。尝试 Reflect 的不同实现并查看结果会很有用。因此,让我们完成所需的其余 C++ 代码更改。

myproject_files.cmake

该项目的文件列表位于 C:\git\book\MyProject\Code\myproject_files.cmake。由于我们只添加了一个组件,因此以下更改就足够了:

set(FILES
    Include/MyProject/MyProjectBus.h
    Include/MyProject/MyProjectTypeIds.h
    Source/MyProjectSystemComponent.cpp
    Source/MyProjectSystemComponent.h
    Source/MyComponent.cpp
    Source/MyComponent.h
)

MyProjectModule.cpp

最后,项目需要通过将新组件的描述符添加到项目模块来注册新组件。

...
#include "MyComponent.h"
...
        MyProjectModule()
            : AZ::Module()
        {
            // Push results of [MyComponent]::CreateDescriptor() into m_descriptors here.
            m_descriptors.insert(m_descriptors.end(), {
                MyProjectSystemComponent::CreateDescriptor(),
                MyComponent::CreateDescriptor(),
            });
        }

您可以在 Visual Studio 中的 MyProject build target 下找到此模块文件。

图 4.3.Visual Studio 中的 MyProjectModule.cpp

CreateDescriptor 是 AZ::Component 的一种方法。对于简单的任务,此时您无需担心它的作用。请注意,它为引擎提供了组件的描述。

重新编译项目

现在我们已经了解了新的空组件的基本 C++ 更改,让我们讨论更新项目所涉及的工作流程。

提示:

关闭 Editor 和 Asset Processor(如果它们正在运行)。否则,Asset Processor 或 Editor 可能会锁定您的某个项目二进制文件。在这种情况下,您将收到构建错误:

9>LINK : fatal error LNK1104: cannot open file
'C:\git\book\build\bin\profile\MyProject.dll'

我们有两种在 Windows 上编译项目的方法:Visual Studio 解决方案或通过 CMake 命令行界面。

从命令行构建

在某些方面,这是编译项目的最简单方法。从命令行构建的好处是,它允许您使用所需的任何编辑器。执行:

例 4.5.从命令行构建项目

cmake --build C:\git\book\build\ --config profile
注意:
“–build” 是一个开关,它告诉 CMake 我们项目的二进制文件夹在哪里。早期我们在 C:\git\book\build 中创建了它。–config 配置文件告诉 CMake 构建我们项目的配置文件风格。
提示:
如果需要调试组件,可以添加以下行,并且仍然使用配置文件二进制文件,而不是在调试版本中编译项目。
#pragma optimize( "", off )
/* unoptimized code section */
#pragma optimize( "", on )

有关更多详细信息,请参阅您的编译器文档,例如: https://docs.microsoft.com/en-us/cpp/preprocessor/optimize?view=msvc-170

从 Visual Studio 构建

一种更用户友好的方法是使用 Visual Studio。

我们的 Visual Studio 解决方案位于构建文件夹:C:\git\book\build\MyProject.sln。在本章中,我们修改了其中一个构建文件 myproject_files.cmake,在编译解决方案时,您将看到以下构建日志。

Build started...
1>------ Build started: Project: ZERO_CHECK
1>Checking Build System
1>CMake is re-running because ...
1> the file 'C:/git/book/MyProject/Code/myproject_files.cmake'
1> is newer than ...
提示:
CMake 根据文件修改时间检测更改。仅当项目中的某个构建文件发生更改时,ZERO_CHECK 才应运行。如果您看到它在每个构建上运行,请检查构建日志并查看 CMake 认为自上次构建以来哪个文件已更改。

生成完成后,Visual Studio 将询问您是否要从磁盘重新加载解决方案,然后选择 Reload All。

重新加载 VS 解决方案

下面是项目现在在 Visual Studio 中使用 MyComponent 文件的样子。

图 4.4.使用 MyComponent 的 Visual Studio 解决方案

小结

如果要重新启动 Editor,则不会在 Entity Inspector 的 Add Component 菜单中找到该组件。这是因为 MyComponent::Reflect() 是空的,因此它没有告诉编辑器 MyComponent。让我们改变一下,以结束本章。

注意:
将组件反映到 Editor 是一种设计选择。并非所有组件都需要在 Editor 中可见。我们将在后面的章节中看到双方的各种原因。

例 4.6.使用 Editor 反射MyComponent.cpp

#include "MyComponent.h"
#include <AzCore/Serialization/EditContext.h>

using namespace MyProject;

void MyComponent::Reflect(AZ::ReflectContext* reflection)
{
    auto sc = azrtti_cast<AZ::SerializeContext*>(reflection);
    if (!sc) return;

    sc->Class<MyComponent, Component>()
        ->Version(1);

    AZ::EditContext* ec = sc->GetEditContext();
    if (!ec) return;

    using namespace AZ::Edit::Attributes;
    // reflection of this component for O3DE Editor
    ec->Class<MyComponent>("My Component", "[my description]")
      ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
        ->Attribute(AppearsInAddComponentMenu, AZ_CRC("Game"))
        ->Attribute(Category, "My Project");
}

O3DE 中的反射分为几个部分。现在,只需知道上述代码描述了以下内容就足够了:

  • MyComponent 是一个没有属性的空组件,其版本是一 (1)。
    sc->Class<MyComponent, Component>()
        ->Version(1);
  • MyComponent 是一个可在游戏(和 Editor)中使用的组件,其名称为 “My Component” ,位于 “My Project” 类别下。
    ec->Class<MyComponent>("My Component", "[my description]")
      ->ClassElement(AZ::Edit::ClassElements::EditorData, "")
        ->Attribute(AppearsInAddComponentMenu, AZ_CRC("Game"))
        ->Attribute(Category, "My Project");

现在,Editor 中的 Add Component 菜单将列出此组件。

编辑器中的 MyComponent

你可以将其添加到任何实体中,例如添加到我们在第 3 章 实体和组件简介中构建的对象的根实体。

注意:
此项目的源代码可以在 GitHub 上找到: https://github.com/AMZN-Olex/O3DEBookCode2111/tree/ch04_creating_components