C++游戏开发之旅 7

问题背景:

上一节中,我们构建了关键的两个类GameObjectComponent;为我们的引擎奠定了面向组件编程的基石。GameObject本身是没有什么东西的,里面存放了存储“积木”(组件)的容器,而Component基类也未实现任何的功能。所以,接下来我们将创建两个 最核心的组件,赋予游戏对象在世界中存在的基础属性:

组件 职责
TransformComponent 负责游戏对象的位置(position)、旋转(rotation)和缩放(scale)。几乎所有游戏对象都具备的组件
SpriteComponent 负责在游戏对象的位置上绘制一个精灵(sprite),让我们的对象变得可见

在开始前,我们可能会遇到一个问题,就是组件如何与引擎的其他部分进行通讯呢?

好比我们之前的Renderer类,我们需要使用到sdl_renderer*ResourceManager*
但是很多时候我们不知道需要用到什么,我们怎么办?向之前将参数传入吗?

1.依赖注入与 Context 类

问题分析

SpriteComponent需要:

  • 调用Renderer来绘制自己
  • 访问ResourceManager来获取纹理信息

我们当然可以在创建SpriteComponent时把RendererResourceManager的指针指向它,但会出现类似这样的情况:

// 构造函数变得很长
SpriteComponent(Renderer* r, ResourceManager* rm, Camera* cam,...);

随着组件和引擎模块越来越多:

  • 构造函数会越来越长
  • 依赖关系会变得错综复杂
  • 修改和维护困难

解决方案 Context (上下文) 类

image-20260125131754350

为了解决这个问题,引入一种优雅的设计模式:依赖注入容器,在这里就是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作用:

任何需要使用引擎功能的部分(比如组件)---> 不需要关心如何获取 RendererInputManager ---> 只需要获取一个 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方法。这是一个强制性的设计约束,确保了所有组件都有明确的更新逻辑

相应地,GameObjecthandleInput、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)

问题场景

TransformComponentposition通常代表一个逻辑点(比如角色的脚底中心)。我们在绘制图片时,需要的是图片左上角坐标

逻辑位置(Transform.position)并不是说和渲染位置(Sprite左上角)一样。

解决方法

Alignment定义了图片几何中心与TransformComponentposition点之间的关系;

我们在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    // 右下角对齐
};

}

可视化说明

image-20260125155157767


比如这个红点表示变换组件的位置(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中的RendererCamera,最后调用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);

}

结果

image-20260126153900863

当然你还可以对其进行修改,看看效果。

本章总结

  • 引入依赖注入:通过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>());而我传入的模板参数Tengine::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偏移的更新。

posted @ 2026-01-31 21:51  wenyiGamecpp  阅读(0)  评论(0)    收藏  举报