〇、工程结构与整体框架 - BoomingTechDev/Piccolo GitHub Wiki
欢迎来到Piccolo社区游玩! Welcome, new Piccolo players!
这是由中国游戏引擎社区Piccolo开源的一款mini游戏引擎。采用世界-关卡-游戏对象-组件的简洁架构,便于理解游戏引擎架构思想,帮助每一个热爱游戏引擎的探索者掌握游戏引擎的基本原理。同时,它也能帮助一线开发者实验引擎算法与第三方库、辅助个人项目快速启动。
Piccolo项目GitHub地址:https://github.com/BoomingTech/Piccolo
使用GitHub Desktop克隆Piccolo仓库到本地。
首先可以一眼找到 engine
文件夹,其中包含Piccolo的全部源码。
engine
文件夹包括:
-
3rdparty
第三方库 -
asset
资产 -
configs
Piccolo启动配置 -
jolt-asset
物理调试器资产 -
shader
shader代码 -
source
Piccolo引擎主体代码 -
template
代码生成模板
Piccolo引擎主体代码包括:
-
editor
编辑器 -
meta_parser
代码生成工具 -
precompile
预编译配置 -
runtime
引擎运行时
引擎运行时的内容将在下面“玩转Piccolo”部分详细介绍。
下面以Windows为例介绍Piccolo引擎的构建、编译过程。
我们推荐使用 Visual Studio 2019或者更新版本 作为Windows上的集成开发环境。
在Piccolo仓库根目录运行以下命令:
$ cmake -S . -B build
- 也可以使用快捷键
Ctrl + F5
(不调试直接运行)或者F5
(调试启动)运行。 - 也可以在根目录下找到
bin
目录中的PiccoloEditor.exe
双击运行。 - 如果你用Piccolo实现了什么有趣的东西,你可以把
bin
目录打包分享给你的朋友让TA玩玩看!
- 左侧是大纲面板,显示场景中所有物体,点击可以选中物体
- 右侧详细信息面板显示当前选中物体的所有组件,展开可以查看以及修改该组件中的反射属性
-
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驱动的粒子特效
Piccolo引擎运行时架构采用GAMES104课程中的分层架构。
对应分为平台层 platform
、核心层 core
、资源层 resource
、功能层 function
。
提供操作系统/平台相关的底层功能。
目前包括:
- 文件系统
file_service
- 路径
path
提供软件系统常用模块。
目前包括:
- 基础库
base
(宏、哈希) - 色彩
color
- 数学库
math
- 元数据系统
meta
- 反射
reflection
- 序列化/反序列化
serializer
- 反射
- 日志系统
log
提供资产加载、保存功能,资产的结构化数据定义和相关路径配置等。
目前包括:
- 资产系统
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
- 动画片段
- 全局数据
提供引擎功能模块。分为框架和子系统两部分。
运行时功能核心框架。核心框架采用世界 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
目前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;
}
我们进入 editor
的 run
函数:
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主函数 rendererTick
。
logicalTick
函数看起来比较简单,tick了 WorldManager
,WorldManager
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基类 Component
的 tick
函数实现各自的功能,如相机组件根据相机的模式执行对应的相机参数计算:
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引擎的运行框架已经有了大体的了解,欢迎各位探索者在代码的海洋中尽情遨游!