〇、工程结构与整体框架 - BoomingTechDev/Piccolo GitHub Wiki

欢迎来到Piccolo社区游玩! Welcome, new Piccolo players!

0. Hello World from Piccolo

这是由中国游戏引擎社区Piccolo开源的一款mini游戏引擎。采用世界-关卡-游戏对象-组件的简洁架构,便于理解游戏引擎架构思想,帮助每一个热爱游戏引擎的探索者掌握游戏引擎的基本原理。同时,它也能帮助一线开发者实验引擎算法与第三方库、辅助个人项目快速启动。

Piccolo项目GitHub地址:https://github.com/BoomingTech/Piccolo

1. 初识Piccolo

a. Piccolo源码First Impact

使用GitHub Desktop克隆Piccolo仓库到本地。

首先可以一眼找到 engine 文件夹,其中包含Piccolo的全部源码。

engine 文件夹包括:

  • 3rdparty 第三方库
  • asset 资产
  • configs Piccolo启动配置
  • jolt-asset 物理调试器资产
  • shader shader代码
  • source Piccolo引擎主体代码
  • template 代码生成模板

Piccolo引擎主体代码包括:

  • editor 编辑器
  • meta_parser 代码生成工具
  • precompile 预编译配置
  • runtime 引擎运行时

引擎运行时的内容将在下面“玩转Piccolo”部分详细介绍。

b. Play Piccolo!

下面以Windows为例介绍Piccolo引擎的构建、编译过程。

我们推荐使用 Visual Studio 2019或者更新版本 作为Windows上的集成开发环境。

使用CMake生成工程文件

Piccolo仓库根目录运行以下命令:

$ cmake -S . -B build

使用 Visual Studio 打开生成的 Piccolo.sln

设置 PiccoloEditor 工程为启动项

编译工程

运行

  • 也可以使用快捷键 Ctrl + F5(不调试直接运行)或者 F5(调试启动)运行。
  • 也可以在根目录下找到 bin 目录中的 PiccoloEditor.exe 双击运行。
  • 如果你用Piccolo实现了什么有趣的东西,你可以把bin目录打包分享给你的朋友让TA玩玩看!

看看Piccolo都能做什么!

编辑器模式

  • 左侧是大纲面板,显示场景中所有物体,点击可以选中物体
  • 右侧详细信息面板显示当前选中物体的所有组件,展开可以查看以及修改该组件中的反射属性
  • A S W D 键控制相机移动,Q E 控制相机上下移动
  • 主窗口鼠标点击选择物体,可以平移、旋转、缩放选中的物体
  • T R C快速切换平移、旋转、缩放模式
  • 下方资产查看器显示资产目录中的所有文件,点击物体定义资产 .object.json 可以向场景中添加物体
  • Delete 键可以删除选中的物体
  • 菜单栏 Menu -> Save Current Level可以保存当前关卡到 bin 目录中的资产目录
  • 菜单栏 Menu -> Debug 下有若干debug draw功能
  • 菜单栏 Window 可以显示/隐藏编辑器的各个面板

游戏模式

  • A S W D 键控制机器人移动,SHIFT 键加速跑,空格键跳跃
  • 移动鼠标可以调整相机视角
  • 支持状态机的动画系统
  • 基于Jolt Physics物理引擎的物理系统
  • deferred shading渲染管线
  • 场景角落有GPU驱动的粒子特效

2. 玩转Piccolo

Piccolo引擎运行时架构采用GAMES104课程中的分层架构。

对应分为平台层 platform 、核心层 core 、资源层 resource 、功能层 function

a. 平台层 platform

提供操作系统/平台相关的底层功能。

目前包括:

  • 文件系统 file_service
  • 路径 path

b. 核心层 core

提供软件系统常用模块。

目前包括:

  • 基础库 base (宏、哈希)
  • 色彩 color
  • 数学库 math
  • 元数据系统 meta
    • 反射 reflection
    • 序列化/反序列化 serializer
  • 日志系统 log

c. 资源层 resource

提供资产加载、保存功能,资产的结构化数据定义和相关路径配置等。

目前包括:

  • 资产系统 asset_manager
  • 配置系统 config_manager
  • 结构化数据定义 res_type
    • 全局数据 global
      • 全局粒子设置 global_particle
      • 全局渲染配置 global_rendering
    • 通用数据 common
      • 世界 world
      • 关卡 level
      • 对象 object
    • 组件数据 components
      • 动画 animation
      • 相机 camera
      • 粒子发射器 emitter
      • 网格 mesh
      • 运动 motor
      • 刚体 rigid_body
    • 其他数据 data
      • 动画片段 animation_clip
      • 动画骨骼节点 animation_skeleton_node_map
      • 基本形状 basic_shape
      • 动画混合状态 blend_state
      • 相机配置 camera_config
      • 材质 material
      • 网格数据 mesh_data
      • 骨骼 skeleton_data
      • 骨骼掩膜 skeleton_mask

d. 功能层 function

提供引擎功能模块。分为框架和子系统两部分。

框架 framework

运行时功能核心框架。核心框架采用世界 world -关卡 level -GO object -组件 component 的层级架构。

世界管理器 world_manager 负责管理世界的加载、卸载、保存,和tick下属当前关卡。 关卡 level 负责加载、卸载、保存关卡。同时关卡也管理下属GO的tick、创建和删除。 游戏对象 object 负责加载、保存GO。同时GO也管理下属组件。

组件全都继承自 component.h 中的 Component 类,目前组件包括:

  • 动画 animation
  • 相机 camera
  • 网格 mesh
  • 运动 motor
  • 粒子 particle
  • 刚体 rigidbody
  • 变换 transform

子系统

function 文件夹中 framework 文件夹之外所有部分。在具体GO组件的功能之外,运行时功能层其他子系统。

目前包括:

  • 动画 animation
  • 角色 character
  • 控制器 controller
  • 全局上下文 global
  • 输入 input
  • 粒子 particle
  • 物理 physics
  • 渲染 render

3. Piccolo运行时

目前Piccolo引擎在编辑器中运行,在 PiccoloEditor 工程下的 main.cpp 中我们可以找到 main 函数:

...
int main(int argc, char** argv)
{
    std::filesystem::path executable_path(argv[0]);
    std::filesystem::path config_file_path = executable_path.parent_path() / "PiccoloEditor.ini";

    Piccolo::PiccoloEngine* engine = new Piccolo::PiccoloEngine();

    // 初始化引擎
    engine->startEngine(config_file_path.generic_string());
    engine->initialize();

    // 初始化编辑器
    Piccolo::PiccoloEditor* editor = new Piccolo::PiccoloEditor();
    editor->initialize(engine);

    // 运行编辑器
    editor->run();

    editor->clear();

    engine->clear();
    engine->shutdownEngine();

    return 0;
}

我们进入 editorrun 函数:

void PiccoloEditor::run()
{
    assert(m_engine_runtime);
    assert(m_editor_ui);
    float delta_time;
    while (true)
    {
        delta_time = m_engine_runtime->calculateDeltaTime();
        g_editor_global_context.m_scene_manager->tick(delta_time);
        g_editor_global_context.m_input_manager->tick(delta_time);
        if (!m_engine_runtime->tickOneFrame(delta_time))
            return;
    }
}

可以看到Editor的主循环,控制正在编辑物体的更新、处理editor的输入,以及最重要的整个引擎的 tickOneFrame tick一帧函数。 进入 tickOneFrame 函数:

bool PiccoloEngine::tickOneFrame(float delta_time)
{
    logicalTick(delta_time);
    calculateFPS(delta_time);

    // single thread
    // exchange data between logic and render contexts
    g_runtime_global_context.m_render_system->swapLogicRenderData();

    rendererTick(delta_time);

#ifdef ENABLE_PHYSICS_DEBUG_RENDERER
    g_runtime_global_context.m_physics_manager->renderPhysicsWorld(delta_time);
#endif

    g_runtime_global_context.m_window_system->pollEvents();


    g_runtime_global_context.m_window_system->setTitle(
        std::string("Piccolo - " + std::to_string(getFPS()) + " FPS").c_str());

    const bool should_window_close = g_runtime_global_context.m_window_system->shouldClose();
    return !should_window_close;
}

可以看到引擎的逻辑tick主函数 logicalTick 、逻辑向渲染context数据交换 swapLogicRenderData 以及渲染tick主函数 rendererTicklogicalTick 函数看起来比较简单,tick了 WorldManagerWorldManager tick 了当前活动关卡:

void PiccoloEngine::logicalTick(float delta_time)
{
    g_runtime_global_context.m_world_manager->tick(delta_time);
    g_runtime_global_context.m_input_system->tick();
}

void WorldManager::tick(float delta_time)
{
    if (!m_is_world_loaded)
    {
        loadWorld(m_current_world_url);
    }

    // tick the active level
    std::shared_ptr<Level> active_level = m_current_active_level.lock();
    if (active_level)
    {
        active_level->tick(delta_time);
        m_level_debugger->tick(active_level);
    }
}

Level 会依次 tick 每个GO、当前角色以及物理场景

void Level::tick(float delta_time)
{
    if (!m_is_loaded)
    {
        return;
    }

    for (const auto& id_object_pair : m_gobjects)
    {
        assert(id_object_pair.second);
        if (id_object_pair.second)
        {
            id_object_pair.second->tick(delta_time);
        }
    }
    if (m_current_active_character && g_is_editor_mode == false)
    {
        m_current_active_character->tick(delta_time);
    }

    std::shared_ptr<PhysicsScene> physics_scene = m_physics_scene.lock();
    if (physics_scene)
    {
        physics_scene->tick(delta_time);
    }
}

GObject GO 会 tick 它的每个需要 tick 的组件。

void GObject::tick(float delta_time)
{
    for (auto& component : m_components)
    {
        if (shouldComponentTick(component.getTypeName()))
        {
            component->tick(delta_time);
        }
    }
}

class GObject : public std::enable_shared_from_this<GObject>
{
    ...
    std::vector<Reflection::ReflectionPtr<Component>> m_components;
}

这里 m_components 是反射指针数组,反射指针会在后续的讲解中剖析相关细节。 在这里可以先理解为通过各个组件的基类指针 Component * 调用虚函数 tick,从而执行各个组件自己的 tick 功能。 组件基类 Component 的声明看起来有些奇怪,是因为组件要能作为数据加载反序列化需要元数据标记以及使用相关的生成代码。

REFLECTION_TYPE(Component)
CLASS(Component, WhiteListFields)
{
    REFLECTION_BODY(Component)
protected:
    std::weak_ptr<GObject> m_parent_object;
    bool                   m_is_dirty {false};
    bool                   m_is_scale_dirty {false};

public:
    Component() = default;
    virtual ~Component() {}

    // Instantiating the component after definition loaded
    virtual void postLoadResource(std::weak_ptr<GObject> parent_object) { m_parent_object = parent_object; }

    virtual void tick(float delta_time) {};

    bool isDirty() const { return m_is_dirty; }

    void setDirtyFlag(bool is_dirty) { m_is_dirty = is_dirty; }

    bool m_tick_in_editor_mode {false};
};

各个组件override基类 Componenttick 函数实现各自的功能,如相机组件根据相机的模式执行对应的相机参数计算:

void CameraComponent::tick(float delta_time)
{
    ...

    switch (m_camera_mode)
    {
        case CameraMode::first_person:
            tickFirstPersonCamera(delta_time);
            break;
        case CameraMode::third_person:
            tickThirdPersonCamera(delta_time);
            break;
        case CameraMode::free:
            tickFreeCamera(delta_time);
            break;
        default:
            break;
    }
}

我们回到主循环一层,logicalTick 之后执行逻辑到渲染的数据交换:

void RenderSystem::swapLogicRenderData() { m_swap_context.swapLogicRenderData(); }

void RenderSwapContext::swapLogicRenderData()
{
    if (isReadyToSwap())
    {
        swap();
    }
}

void RenderSwapContext::swap()
{
    resetLevelRsourceSwapData();
    resetGameObjectResourceSwapData();
    resetGameObjectToDelete();
    resetCameraSwapData();
    resetEmitterTickSwapData();
    resetEmitterTransformSwapData();
    resetPartilceBatchSwapData();
    std::swap(m_logic_swap_data_index, m_render_swap_data_index);
}

可以看到在清空上一帧渲染使用过的交换数据之后,互换了逻辑交换区与渲染交换区的index,实现了快速数据交换。 接下来我们看渲染tick主函数 rendererTick

bool PiccoloEngine::rendererTick(float delta_time)
{
    g_runtime_global_context.m_render_system->tick(delta_time);
    return true;
}

void RenderSystem::tick(float delta_time)
{
    // 处理来自逻辑的交换数据
    processSwapData();

    // 准备渲染的command context
    m_rhi->prepareContext();

    // 更新帧buffer
    m_render_resource->updatePerFrameBuffer(m_render_scene, m_render_camera);

    // 更新当前帧可见的物体
    m_render_scene->updateVisibleObjects(std::static_pointer_cast<RenderResource>(m_render_resource),
                                          m_render_camera);

    // 准备渲染管线各pass数据
    m_render_pipeline->preparePassData(m_render_resource);

    g_runtime_global_context.m_debugdraw_manager->tick(delta_time);

    // 渲染当前帧
    if (m_render_pipeline_type == RENDER_PIPELINE_TYPE::FORWARD_PIPELINE)
    {
        m_render_pipeline->forwardRender(m_rhi, m_render_resource);
    }
    else if (m_render_pipeline_type == RENDER_PIPELINE_TYPE::DEFERRED_PIPELINE)
    {
        m_render_pipeline->deferredRender(m_rhi, m_render_resource);
    }
    else
    {
        LOG_ERROR(__FUNCTION__, "unsupported render pipeline type");
    }
}

到此,我们对Piccolo引擎的运行框架已经有了大体的了解,欢迎各位探索者在代码的海洋中尽情遨游!

⚠️ **GitHub.com Fallback** ⚠️