C++游戏开发之旅 7
问题背景:
上一节中,我们构建了关键的两个类GameObject 和 Component;为我们的引擎奠定了面向组件编程的基石。GameObject本身是没有什么东西的,里面存放了存储“积木”(组件)的容器,而Component基类也未实现任何的功能。所以,接下来我们将创建两个 最核心的组件,赋予游戏对象在世界中存在的基础属性:
| 组件 | 职责 |
|---|---|
| TransformComponent | 负责游戏对象的位置(position)、旋转(rotation)和缩放(scale)。几乎所有游戏对象都具备的组件 |
| SpriteComponent | 负责在游戏对象的位置上绘制一个精灵(sprite),让我们的对象变得可见 |
在开始前,我们可能会遇到一个问题,就是组件如何与引擎的其他部分进行通讯呢?
好比我们之前的Renderer类,我们需要使用到sdl_renderer*和ResourceManager*。
但是很多时候我们不知道需要用到什么,我们怎么办?向之前将参数传入吗?
1.依赖注入与 Context 类
问题分析:
SpriteComponent需要:
- 调用
Renderer来绘制自己 - 访问
ResourceManager来获取纹理信息
我们当然可以在创建SpriteComponent时把Renderer和ResourceManager的指针指向它,但会出现类似这样的情况:
// 构造函数变得很长
SpriteComponent(Renderer* r, ResourceManager* rm, Camera* cam,...);
随着组件和引擎模块越来越多:
- 构造函数会越来越长
- 依赖关系会变得错综复杂
- 修改和维护困难
解决方案 Context (上下文) 类

为了解决这个问题,引入一种优雅的设计模式:依赖注入容器,在这里就是Context(上下文)类。
我们在core下创建context.h/context.cpp。
// context.h
#pragma once
namespace engine::input {
class InputManager;
}
namespace engine::render {
class Renderer;
class Camera;
}
namespace engine::resource {
class ResourceManager;
}
namespace engine::core {
/**
* @brief Context 类, 用于管理整个引擎的上下文
*
* 简化依赖注入,传递Context即可获取引擎的各个模块
*/
class Context final {
private:
engine::input::InputManager& inputManager_;
engine::render::Renderer& renderer_;
engine::render::Camera& camera_;
engine::resource::ResourceManager& resourceManager_;
public:
Context(
engine::input::InputManager& inputManager,
engine::render::Renderer& renderer,
engine::render::Camera& camera,
engine::resource::ResourceManager& resourceManager
);
// 禁用拷贝和移动语义
Context(const Context&) = delete;
Context& operator=(const Context&) = delete;
Context(Context&&) = delete;
Context& operator=(Context&&) = delete;
// getter
engine::input::InputManager& getInputManager() const { return inputManager_; }
engine::render::Renderer& getRenderer() const { return renderer_; }
engine::render::Camera& getCamera() const { return camera_; }
engine::resource::ResourceManager& getResourceManager() const { return resourceManager_; }
};
}
// context.cpp
#include "context.h"
#include <spdlog/spdlog.h>
namespace engine::core{
Context::Context(engine::input::InputManager &inputManager, engine::render::Renderer &renderer, engine::render::Camera &camera, engine::resource::ResourceManager &resourceManager)
: inputManager_(inputManager), renderer_(renderer), camera_(camera), resourceManager_(resourceManager)
{
spdlog::trace("Context 创建成功!包含:InputManager, Renderer, Camera, ResourceManager");
}
}
Context作用:
任何需要使用引擎功能的部分(比如组件)---> 不需要关心如何获取 Renderer 或 InputManager ---> 只需要获取一个 Context 对象 ---> 就可以通过它访问所有需要的工具
优势:
- 单一注入点:只需要传递一个
context对象 - 松耦合:组件不直接依赖具体的管理器
- 易于扩展:新增模块只需要修改
Context - 便于测试:对于模块
Context测试容易
2.升级组件系统
既然有了Context,那么我们的组件就需要升级。需要将几个关键的函数添加参数engine::core::Context&,(handleInput()、update()、renderer()等)
#pragma once
namespace engine::object {
class GameObject;
}
namespace engine::core {
class Context;
}
namespace engine::component {
/**
* @brief 组件基类
* 是所有组件的基类,定义了组件的基本接口(可能用到的基本的生命周期函数)
*/
class Component {
friend class engine::object::GameObject;
protected:
engine::object::GameObject* owner_ = nullptr; // 指向拥有该组件的游戏对象
public:
Component() = default;
virtual ~Component() = default; // 虚析构函数,确保派生类析构函数被调用
// 禁止拷贝和移动语义
Component(const Component&) = delete;
Component& operator=(const Component&) = delete;
Component(Component&&) = delete;
Component& operator=(Component&&) = delete;
void setOwner(engine::object::GameObject* owner) { owner_ = owner; } // 设置拥有该组件的游戏对象
engine::object::GameObject* getOwner() const { return owner_; } // 获取拥有该组件的游戏对象
protected:
virtual void init() {} // 保留两段初始化机制,GameObject添加组件时自动调用,不需要外部调用
virtual void handleInput(engine::core::Context& context) {} // 处理输入
/**
* @brief 更新组件 (纯虚函数,子类必须实现)
* @param deltaTime 上一帧到当前帧的时间间隔
* @param context 引擎上下文
*/
virtual void update(float deltaTime, engine::core::Context& context) = 0; // 更新
virtual void render(engine::core::Context& context) {} // 渲染
virtual void clean() {} // 清理
};
}
改动说明
| 修改 | 说明 |
|---|---|
| Context 参数 | 将 Context 对象作为参数添加到handleInput,update,render这些生命周期函数中 |
| 纯虚函数 | update函数声明为纯虚函数=0。这意味着Component类现在是一个抽象类,不能被直接实例化 |
| 强制实现 | 任何继承自Component的子类都必须实现自己的update方法。这是一个强制性的设计约束,确保了所有组件都有明确的更新逻辑 |
相应地,GameObject的handleInput、update、render方法也要接收Context参数,并将它 "透传"给器管理的所有组件。
// game_object.h
namespace engine::core{
class Context;
}
class GameObject final {
public:
// ...
void handleInput(engine::core::Context& context);
void update(float deltaTime, engine::core::Context& context);
void render(engine::core::Context& context);
// ...
}
// game_object.cpp
void GameObject::update(float delta_time, engine::core::Context& context)
{
// 遍历所有组件并调用它们的update方法
for(auto& pair : components_) {
pair.second->update(delta_time, context);
}
}
void GameObject::render(engine::core::Context& context)
{
// 遍历所有组件并调用它们的render方法
for(auto& pair : components_) {
pair.second->render(context);
}
}
void GameObject::handleInput(engine::core::Context& context)
{
for(auto& pair : components_) {
pair.second->handleInput(context);
}
}
3.TransformComponent:空间属性载体
这是第一个具体的组件,也是较为简单的一个,是一个纯粹的数据容器。
transform_component.h
#pragma once
#include "component.h"
#include <glm/vec2.hpp>
namespace engine::component
{
class TransformComponent final : public Component
{
friend class engine::object::GameObject; // 友元不可继承,需要手动为子类添加
private:
glm::vec2 position_ = {0.0f,0.0f}; // 位置
float rotation_ = 0.0f; // 旋转
glm::vec2 scale_ = {1.0f,1.0f}; // 缩放
public:
TransformComponent(glm::vec2 position = {0.0f,0.0f}, glm::vec2 scale = {1.0f,1.0f},float rotation = 0.0f)
: position_(position), rotation_(rotation), scale_(scale){}
// 禁用拷贝和移动语义
TransformComponent(const TransformComponent&) = delete;
TransformComponent& operator=(const TransformComponent&) = delete;
TransformComponent(TransformComponent&&) = delete;
TransformComponent& operator=(TransformComponent&&) = delete;
// getter and setter
glm::vec2 getPosition() const {return position_;}
float getRotation() const {return rotation_;}
glm::vec2 getScale() const {return scale_;}
void setPosition(const glm::vec2& position) {position_ = position;}
void setRotation(float rotation) {rotation_ = rotation;}
void setScale(const glm::vec2& scale);
void translate(const glm::vec2& offset) {position_ += offset;} // 平移
private:
void update(float, engine::core::Context&) override {}
};
} // namespace engine::component
// transform_component.cpp
#include "transform_component.h"
#include "../object/game_object.h"
#include "sprite_component.h"
namespace engine::component
{
void TransformComponent::setScale(const glm::vec2 &scale)
{
scale_ = scale;
if(owner_){
auto sprite_comp = owner_->getComponent<SpriteComponent>();
if(sprite_comp){
sprite_comp->updateOffset();
}
}
}
} // namespace engine::component
设计特点
| 特性 | 说明 |
|---|---|
| 数据容器 | 主要存储三个基本空间属性:位置、缩放、旋转角 |
| 空update | 它只有一个空的update方法(满足基类纯虚函数的要求) |
| 组件协作 | setScale函数展示了组件间协作的简单例子:当缩放改变时,它会通知SpriteComponent更新其偏移量 |
TransformComponent 是一个纯粹的数据组件,不包含复杂的逻辑,只负责存储和提供空间信息
4.SpriteComponent:赋予对象视觉形象
这是这章节的重点了,负责将一个Sprite数据对象在屏幕上绘制出来。
对齐(Alignment)概念
在进行编码前,我们需要理解一个概念:对齐(Alignment)与偏移(Offset)。
问题场景
TransformComponent的position通常代表一个逻辑点(比如角色的脚底中心)。我们在绘制图片时,需要的是图片左上角坐标。
逻辑位置(Transform.position)并不是说和渲染位置(Sprite左上角)一样。
解决方法
Alignment定义了图片几何中心与TransformComponent的position点之间的关系;
我们在utils文件夹中创建alignment.h :
#pragma once
namespace engine::utils {
/**
* @enum class Alignment
* @brief 定义对象或组件相对于其位置参考点的对齐方式
*/
enum class Alignment {
NONE, // 不指定对齐方式,偏移量通常为(0,0)或自己手动设置
TOP_LEFT, // 左上角对齐
TOP_CENTER, // 顶部居中对齐
TOP_RIGHT, // 右上角对齐
CENTER_LEFT, // 左侧居中对齐
CENTER, // 居中对齐
CENTER_RIGHT, // 右侧居中对齐
BOTTOM_LEFT, // 左下角对齐
BOTTOM_CENTER, // 底部居中对齐
BOTTOM_RIGHT // 右下角对齐
};
}
可视化说明

比如这个红点表示变换组件的位置(transform_->getPosition()),如果精灵组件就从该点进行绘制,也就是SpriteTransform的左上角位置就是变换组件的位置,那么这种对齐方式就是TOP_LEFT,对应的偏移Offset=(0,0);一般来说,对齐方式大多为CENTER,那么其偏移Offset=(-sprite的宽度width / 2, -sprite的高度height / 2)。
我们希望SpriteComponent会根据对齐方式Alignment、精灵尺寸、缩放 自动计算出offset_,在渲染时将这个偏移量应用到transform_->getPosition()上,从而得到正确的绘制坐标。
sprite_component.h
#pragma once
#include "component.h"
#include "../utils/alignment.h"
#include "../render/sprite.h"
#include <optional>
#include <string>
#include <glm/vec2.hpp>
namespace engine::resource {
class ResourceManager;
}
namespace engine::component {
class TransformComponent;
class SpriteComponent final : public Component {
friend class engine::object::GameObject;
private:
engine::component::TransformComponent* transform_ = nullptr;
engine::resource::ResourceManager* resource_manager_ = nullptr;
engine::render::Sprite sprite_;
engine::utils::Alignment alignment_ = engine::utils::Alignment::NONE;
glm::vec2 sprite_size_ = glm::vec2(0.0f, 0.0f); // 更新对齐方式时需要
glm::vec2 offset_ = glm::vec2(0.0f, 0.0f);
bool is_hidden_ = false; // 是否可见(渲染)
public:
/**
* @brief 构造函数
* @param texture_id 纹理ID
* @param resource_manager 资源管理器
* @param alignment 对齐方式
* @param source_rect_opt 源矩形
* @param is_flippend 是否翻转
*/
SpriteComponent(
const std::string& texture_id,
engine::resource::ResourceManager& resource_manager,
engine::utils::Alignment alignment = engine::utils::Alignment::NONE,
std::optional<SDL_FRect> source_rect_opt = std::nullopt,
bool is_flipped = false
);
~SpriteComponent() override = default; // (可不显式写出) 保证它确实覆盖了基类的虚析构(如果基类析构不是 virtual,会直接编译报错)
// 禁用拷贝和移动语义
SpriteComponent(const SpriteComponent&) = delete;
SpriteComponent& operator=(const SpriteComponent&) = delete;
SpriteComponent(SpriteComponent&&) = delete;
SpriteComponent& operator=(SpriteComponent&&) = delete;
void updateOffset(); // 更新偏移量(根据Alignment和SpriteSize)
// getters
const engine::render::Sprite& getSprite() const {return sprite_;} // 获取精灵
const std::string& getTextureId() const {return sprite_.getTextureId();} // 获取纹理ID
bool isFlipped() const {return sprite_.isFlipped();} // 获取是否翻转
bool isHidden() const {return is_hidden_;} // 获取是否隐藏
const glm::vec2& getSpriteSize() const {return sprite_size_;} // 获取精灵大小
const glm::vec2& getOffset() const {return offset_;} // 获取偏移量
engine::utils::Alignment getAlignment() const {return alignment_;} // 获取对齐方式
// setters
// 设置精灵对象
void setSpriteById(const std::string& texture_id, const std::optional<SDL_FRect>& source_rect_opt = std::nullopt); // 设置精灵
void setFlipped(bool is_flipped) {sprite_.setFlipped(is_flipped);}
void setHidden(bool is_hidden) {is_hidden_ = is_hidden;}
void setAlignment(engine::utils::Alignment anchor);
void setSourceRect(const std::optional<SDL_FRect>& source_rect);
private:
void updateSpriteSize(); // 根据 sprite_ 的source_rect_ 更新 sprite_size_
// Component 虚函数覆盖
void init() override;
void update(float, engine::core::Context& context) override {}
void render(engine::core::Context& ) override;
};
}
sprite_component.cpp
#include "sprite_component.h"
#include "transform_component.h"
#include "../resource/resource_manager.h"
#include "../object/game_object.h"
#include "../core/context.h"
#include "../render/renderer.h"
#include "../render/camera.h"
#include "spdlog/spdlog.h"
namespace engine::component {
SpriteComponent::SpriteComponent(
const std::string &texture_id,
engine::resource::ResourceManager &resource_manager,
engine::utils::Alignment alignment,
std::optional<SDL_FRect> source_rect_opt,
bool is_flipped
)
: resource_manager_(&resource_manager),
sprite_(texture_id,source_rect_opt,is_flipped),
alignment_(alignment)
{
if(!resource_manager_){
spdlog::critical("创建SpriteComponent时,resource_manager_为空!此组件无效!");
}
// 在init中计算 offset_ 和 sprite_size_
spdlog::trace("创建SpriteComponent,texture_id:{}",texture_id);
}
void SpriteComponent::init()
{
if(!owner_){
spdlog::error("SpriteComponent::init()时,owner_为空,这说明这个组件未设置拥有者");
return;
}
transform_ = owner_->getComponent<TransformComponent>();
if(!transform_){
spdlog::warn("GameObject {} 需要一个TransformComponent才能正确显示SpriteComponent",
owner_->getName());
return;
}
// 计算sprite_size_、offset
updateSpriteSize();
updateOffset();
}
void SpriteComponent::setAlignment(engine::utils::Alignment anchor)
{
alignment_ = anchor;
updateOffset();
}
void SpriteComponent::setSourceRect(const std::optional<SDL_FRect> &source_rect)
{
sprite_.setSourceRect(source_rect);
// 注意更新顺序,先更新sprite_size_再更新offset_
updateSpriteSize();
updateOffset();
}
void SpriteComponent::setSpriteById(const std::string &texture_id, const std::optional<SDL_FRect> &source_rect_opt)
{
sprite_.setTextureId(texture_id);
sprite_.setSourceRect(source_rect_opt);
updateSpriteSize();
updateOffset();
}
void SpriteComponent::render(engine::core::Context& context)
{
if(is_hidden_ || !transform_ || !resource_manager_) return;
context.getRenderer().drawSprite(context.getCamera(),sprite_,
transform_->getPosition() + offset_, transform_->getScale(),transform_->getRotation());
}
// --- 更新偏移 offset_ 和 sprite_size_ 函数 ---
void SpriteComponent::updateOffset()
{
// 判断sprite_size_是否有效
if(sprite_size_.x <= 0 || sprite_size_.y <= 0){
spdlog::warn("SpriteComponent::updateOffset()时,sprite_size_无效,无法计算offset");
return;
}
// 从transform中获取scale,计算offset. // 这里的sprite_size_是原始的默认大小
const auto& scale = transform_->getScale();
switch (alignment_)
{
case engine::utils::Alignment::TOP_LEFT: offset_ = glm::vec2(0.0f,0.0f) * scale; break;
case engine::utils::Alignment::TOP_CENTER: offset_ = glm::vec2(-sprite_size_.x/2,0) * scale; break;
case engine::utils::Alignment::TOP_RIGHT: offset_ = glm::vec2(-sprite_size_.x,0) * scale; break;
case engine::utils::Alignment::CENTER_LEFT: offset_ = glm::vec2(0,-sprite_size_.y/2) * scale; break;
case engine::utils::Alignment::CENTER: offset_ = glm::vec2(-sprite_size_.x/2,-sprite_size_.y/2) * scale; break;
case engine::utils::Alignment::CENTER_RIGHT: offset_ = glm::vec2(-sprite_size_.x,-sprite_size_.y/2) * scale; break;
case engine::utils::Alignment::BOTTOM_LEFT: offset_ = glm::vec2(0,-sprite_size_.y) * scale; break;
case engine::utils::Alignment::BOTTOM_CENTER: offset_ = glm::vec2(-sprite_size_.x/2,-sprite_size_.y) * scale; break;
case engine::utils::Alignment::BOTTOM_RIGHT: offset_ = glm::vec2(-sprite_size_.x,-sprite_size_.y) * scale; break;
case engine::utils::Alignment::NONE: break;
default: break;
}
}
void SpriteComponent::updateSpriteSize()
{
if(!resource_manager_){
spdlog::error("resource_manager_为空,无法获取纹理大小");
return;
}
/* 如果sprite_有source_rect,说明其是多个图片放在一起的纹理,那么用source_rect的大小
否则就是一个完整的纹理. 因为source_rect是类似{x,y,w,h}的格式. */
if(sprite_.getSourceRect().has_value()){
const auto& source_rect = sprite_.getSourceRect().value();
sprite_size_ = {source_rect.w,source_rect.h};
} else {
sprite_size_ = resource_manager_->getTextureSize(sprite_.getTextureId());
}
}
}
核心方法说明
| 方法 | 职责 |
|---|---|
init() |
从owner_(所属的GameObject)获取TransformComponent的指针,并计算初始的精灵尺寸和偏移量。 |
render(Context&) |
核心职责。从transform_获取最新的位置、缩放和旋转,加上计算好的偏移量,然后通过context中的Renderer和Camera,最后调用renderer.drawSprite()完成绘制 |
updateOffset() |
根据定义的对齐方式,配合精灵尺寸以及缩放,计算正确的渲染偏移量 |
updateSpriteSize() |
从sprite_中获取源矩形source_rect,因为这是一个可选的std::optional<SDL_FRect>&,所以在获取的时候需要进行判断 |
5.集成与测试
最后,我们需要在GameApp中将所有新模块整合起来。
初始化 Context
// game_app.h 新增
class GameApp final {
private:
std::unique_ptr<engine::core::Context> context_;
[[nodiscard]] bool initContext();
// ...
};
// game_app.cpp 新增
bool GameApp::initContext()
{
try {
context_ = std::make_unique<engine::core::Context>(*input_manager_, *renderer_, *camera_, *resource_manager_);
} catch (const std::exception& e){
spdlog::error("初始化Context失败: {}", e.what());
return false;
}
return true;
}
bool GameApp::init()
{
// ...
if(!initConfig()) return false;
if(!initSDL()) return false;
if(!initTime()) return false;
if(!initResourceManager()) return false;
if(!initCamera()) return false;
if(!initRenderer()) return false;
if(!initInputManager()) return false;
if(!initContext()) return false; // 注意顺序
// ...
}
测试
接下来定义一个游戏对象gameobj,然后通过testGameObject()函数进行设置。
// game_app.cpp
// 直接创建一个GameObject
engine::object::GameObject gameobj("test1");
bool GameApp::init()
{
if(!initConfig()) return false;
if(!initSDL()) return false;
if(!initTime()) return false;
if(!initResourceManager()) return false;
if(!initCamera()) return false;
if(!initRenderer()) return false;
if(!initInputManager()) return false;
if(!initContext()) return false; // 注意顺序
testResource();
testGameObject();
spdlog::info("GameApp初始化成功");
is_running_ = true;
return true;
}
void GameApp::render()
{
renderer_->clearScreen();
testRenderer();
gameobj.render(*context_);
renderer_->present();
}
void GameApp::testGameObject()
{
// 添加组件进行测试
gameobj.addComponent<engine::component::TransformComponent>(glm::vec2(500.0f,100.0f));
gameobj.addComponent<engine::component::SpriteComponent>(
"assets/textures/Props/straw-house.png",
*resource_manager_,
engine::utils::Alignment::CENTER
);
// 修改TransformComponent大小
gameobj.getComponent<engine::component::TransformComponent>()->setScale(glm::vec2(0.5f,0.5f));
// 修改TransformComponent角度
gameobj.getComponent<engine::component::TransformComponent>()->setRotation(45.0f);
}
结果:

当然你还可以对其进行修改,看看效果。
本章总结
- 引入依赖注入:通过Context类解决了组件与引擎模块的通讯问题
- 实现具体组件:创建了TransformComponent和SpriteComponent两个核心组件
- 数据行为分离:将数据(TransformComponent)和行为(SpriteComponent)分离
- 组件协作:展示了组件间如何相互配合(Transform通知Sprite更新)
设计亮点
| 特性 | 实现方式 | 优势 |
|---|---|---|
| 依赖注入 | 用Context类统一管理引擎模块引用 | 松耦合、易测试、可拓展 |
| 对齐系统 | 在utils文件夹下创建Alignment枚举,计算偏移量 | 灵活的精灵图片定位 |
| 纯虚函数 | Component::update() 声明虚函数 | 强制所有派生类组件更新逻辑 |
| 组件缓存 | SpriteComponent缓存Transform指针 | 避免重复查找,提高性能 |
工作流程
GameObject ---
|———— TransformComponent(数据层)
|———— position:(500,100)
|———— 缩放:(0.5,0.5)
|———— 旋转:45°
|———— SpriteComponent(表现层)
|———— 存放 TransformComponent 缓存数据
|———— 计算偏移
|———— Context 获取 Renderer
|———— 调用 drawSprite() 渲染
遇到的问题
1.遇到了无法实例化抽象类的问题,起因是我之前的测试代码中写了:
void GameApp::testGameObject()
{
engine::object::GameObject game_object("test1");
game_object.addComponent<engine::component::Component>();
}
因为addComponent<T>()函数的核心逻辑是创建T类型的组件实例(比如std::make_unique<T>());而我传入的模板参数T是engine::component::Component—这个类有纯虚函数update,是抽象类;抽象类只能被继承,不能被实例化。
2.在使用addComponent<TransformComponent>时遇到了无法重载的问题。
我的TransformComponent类中的构造函数是这样的
TransformComponent(glm::vec2 position, glm::vec2 scale,float rotation)
: position_(position), scale_(scale), rotation_(rotation) {}
而我用是这样的
gameobj.addComponent<engine::component::TransformComponent>(glm::vec2(100.0f,100.0f));
我的addComponent是一个 可变参数模板函数(带Args&&... args),它的实现通过完美转发,将参数原封不动的传给构造函数
std::unique_ptr<T> new_comp = std::make_unique<T>(std::forward<Args>(args)...);
也就是我们()圆括号中的参数会被转发给构造函数,所以参数需要去匹配构造函数的参数。
3.对于SpriteComponent中updateSize()函数的疑惑
当时在看代码的时候,以为精灵组件中的sprite_的size是根据变换组件调整变化的。但实际上sprite_size是图片的原始像素大小,不然变化可太愚蠢了,因为在更新偏移函数也就是updateOffset()中,我们会使用到Transform组件中的scale来适应偏移,因为Transform组件中scale初始是{1.0,1.0},然后调整大小后,我们的显示肯定要变化,偏移offset自然就要修改。顺带再提一嘴,我们的精灵组件中有很多方法比如源矩形设置(setSourceRect())、sprite替换(setSpriteById),修改后都需要进行精灵的大小size和offset偏移的更新。

浙公网安备 33010602011771号