完成渲染器相机与精灵

This commit is contained in:
Ziyu
2025-06-16 14:44:47 +08:00
parent 8e62837bbb
commit ac3b6121b4
10 changed files with 557 additions and 6 deletions
+2
View File
@@ -37,6 +37,8 @@ add_executable(${TARGET}
src/engine/resource/texture_manager.cpp
src/engine/resource/audio_manager.cpp
src/engine/resource/font_manager.cpp
src/engine/render/renderer.cpp
src/engine/render/camera.cpp
)
# 链接库
+67 -2
View File
@@ -1,6 +1,8 @@
#include "game_app.h"
#include "time.h"
#include "../resource/resource_manager.h"
#include "../render/renderer.h"
#include "../render/camera.h"
#include <SDL3/SDL.h>
#include <spdlog/spdlog.h>
@@ -40,6 +42,8 @@ bool GameApp::init() {
if (!initSDL()) return false;
if (!initTime()) return false;
if (!initResourceManager()) return false;
if (!initRenderer()) return false;
if (!initCamera()) return false;
// 测试资源管理器
testResourceManager();
@@ -59,11 +63,19 @@ void GameApp::handleEvents() {
}
void GameApp::update(float /* delta_time */) {
// 游戏逻辑更新,暂时为空
// 游戏逻辑更新
testCamera();
}
void GameApp::render() {
// 渲染代码,暂时为空
// 1. 清除屏幕
renderer_->clearScreen();
// 2. 具体渲染代码
testRenderer();
// 3. 更新屏幕显示
renderer_->present();
}
void GameApp::close() {
@@ -101,6 +113,9 @@ bool GameApp::initSDL() {
spdlog::error("无法创建渲染器! SDL错误: {}", SDL_GetError());
return false;
}
// 设置逻辑分辨率
SDL_SetRenderLogicalPresentation(sdl_renderer_, 640, 360, SDL_LOGICAL_PRESENTATION_LETTERBOX);
spdlog::trace("SDL 初始化成功。");
return true;
}
@@ -127,6 +142,31 @@ bool GameApp::initResourceManager() {
return true;
}
bool GameApp::initRenderer() {
try {
renderer_ = std::make_unique<engine::render::Renderer>(sdl_renderer_, resource_manager_.get());
} catch (const std::exception& e) {
spdlog::error("初始化渲染器失败: {}", e.what());
return false;
}
spdlog::trace("渲染器初始化成功。");
return true;
}
bool GameApp::initCamera() {
try {
camera_ = std::make_unique<engine::render::Camera>(glm::vec2(640, 360));
} catch (const std::exception& e) {
spdlog::error("初始化相机失败: {}", e.what());
return false;
}
spdlog::trace("相机初始化成功。");
return true;
}
// --- 测试用函数 ---
void GameApp::testResourceManager()
{
resource_manager_->getTexture("assets/textures/Actors/eagle-attack.png");
@@ -138,4 +178,29 @@ void GameApp::testResourceManager()
resource_manager_->unloadSound("assets/audio/button_click.wav");
}
void GameApp::testRenderer()
{
engine::render::Sprite sprite_world("assets/textures/Actors/frog.png");
engine::render::Sprite sprite_ui("assets/textures/UI/buttons/Start1.png");
engine::render::Sprite sprite_parallax("assets/textures/Layers/back.png");
static float rotation = 0.0f;
rotation += 0.1f;
// 注意渲染顺序
renderer_->drawParallax(*camera_, sprite_parallax, glm::vec2(100, 100), glm::vec2(0.5f, 0.5f), glm::bvec2(true, false));
renderer_->drawSprite(*camera_, sprite_world, glm::vec2(200, 200), glm::vec2(1.0f, 1.0f), rotation);
renderer_->drawUISprite(sprite_ui, glm::vec2(100, 100));
}
void GameApp::testCamera()
{
auto key_state = SDL_GetKeyboardState(nullptr);
if (key_state[SDL_SCANCODE_UP]) camera_->move(glm::vec2(0, -1));
if (key_state[SDL_SCANCODE_DOWN]) camera_->move(glm::vec2(0, 1));
if (key_state[SDL_SCANCODE_LEFT]) camera_->move(glm::vec2(-1, 0));
if (key_state[SDL_SCANCODE_RIGHT]) camera_->move(glm::vec2(1, 0));
}
} // namespace engine::core
+14 -3
View File
@@ -9,6 +9,11 @@ namespace engine::resource {
class ResourceManager;
}
namespace engine::render {
class Renderer;
class Camera;
}
namespace engine::core { // 命名空间的最佳实践:与文件路径一致
class Time;
@@ -24,6 +29,8 @@ private:
// 引擎组件
std::unique_ptr<engine::core::Time> time_;
std::unique_ptr<engine::resource::ResourceManager> resource_manager_;
std::unique_ptr<engine::render::Renderer> renderer_;
std::unique_ptr<engine::render::Camera> camera_;
public:
GameApp();
@@ -48,12 +55,16 @@ private:
void close();
// 各模块的初始化/创建函数,在init()中调用
bool initSDL();
bool initTime();
bool initResourceManager();
[[nodiscard]] bool initSDL();
[[nodiscard]] bool initTime();
[[nodiscard]] bool initResourceManager();
[[nodiscard]] bool initRenderer();
[[nodiscard]] bool initCamera();
// 测试用函数
void testResourceManager();
void testRenderer();
void testCamera();
};
} // namespace engine::core
+80
View File
@@ -0,0 +1,80 @@
#include "camera.h"
#include "../utils/math.h"
#include <spdlog/spdlog.h>
namespace engine::render {
Camera::Camera(const glm::vec2& viewport_size, const glm::vec2& position, const std::optional<engine::utils::Rect> limit_bounds)
: viewport_size_(viewport_size), position_(position), limit_bounds_(limit_bounds) {
spdlog::trace("Camera 初始化成功,位置: {},{}", position_.x, position_.y);
}
void Camera::setPosition(const glm::vec2& position) {
position_ = position;
clampPosition();
}
void Camera::update(float /* delta_time */)
{
// TODO 自动跟随目标
}
void Camera::move(const glm::vec2 &offset)
{
position_ += offset;
clampPosition();
}
void Camera::setLimitBounds(const engine::utils::Rect& bounds)
{
limit_bounds_ = bounds;
clampPosition(); // 设置边界后,立即应用限制
}
const glm::vec2& Camera::getPosition() const {
return position_;
}
void Camera::clampPosition()
{
// 边界检查需要确保相机视图(position 到 position + viewport_size)在 limit_bounds 内
if (limit_bounds_.has_value() && limit_bounds_->size.x > 0 && limit_bounds_->size.y > 0) {
// 计算允许的相机位置范围
glm::vec2 min_cam_pos = limit_bounds_->position;
glm::vec2 max_cam_pos = limit_bounds_->position + limit_bounds_->size - viewport_size_;
// 确保 max_cam_pos 不小于 min_cam_pos (视口可能比世界还大)
max_cam_pos.x = std::max(min_cam_pos.x, max_cam_pos.x);
max_cam_pos.y = std::max(min_cam_pos.y, max_cam_pos.y);
position_ = glm::clamp(position_, min_cam_pos, max_cam_pos);
}
// 如果 limit_bounds 无效则不进行限制
}
glm::vec2 Camera::worldToScreen(const glm::vec2& world_pos) const {
// 将世界坐标减去相机左上角位置
return world_pos - position_;
}
glm::vec2 Camera::worldToScreenWithParallax(const glm::vec2 &world_pos, const glm::vec2 &scroll_factor) const
{
// 相机位置应用滚动因子
return world_pos - position_ * scroll_factor;
}
glm::vec2 Camera::screenToWorld(const glm::vec2 &screen_pos) const
{
// 将屏幕坐标加上相机左上角位置
return screen_pos + position_;
}
glm::vec2 Camera::getViewportSize() const {
return viewport_size_;
}
std::optional<engine::utils::Rect> Camera::getLimitBounds() const {
return limit_bounds_;
}
} // namespace engine::render
+45
View File
@@ -0,0 +1,45 @@
#pragma once
#include "../utils/math.h"
#include <optional>
namespace engine::render {
/**
* @brief 相机类负责管理相机位置和视口大小,并提供坐标转换功能。
* 它还包含限制相机移动范围的边界。
*/
class Camera final {
private:
glm::vec2 viewport_size_; ///< @brief 视口大小(屏幕大小)
glm::vec2 position_; ///< @brief 相机左上角的世界坐标
std::optional<engine::utils::Rect> limit_bounds_; ///< @brief 限制相机的移动范围,空值表示不限制
public:
Camera(const glm::vec2& viewport_size, const glm::vec2& position = glm::vec2(0.0f, 0.0f), const std::optional<engine::utils::Rect> limit_bounds = std::nullopt);
void update(float delta_time); ///< @brief 更新相机位置
void move(const glm::vec2& offset); ///< @brief 移动相机
glm::vec2 worldToScreen(const glm::vec2& world_pos) const; ///< @brief 世界坐标转屏幕坐标
glm::vec2 worldToScreenWithParallax(const glm::vec2& world_pos, const glm::vec2& scroll_factor) const; ///< @brief 世界坐标转屏幕坐标,考虑视差滚动
glm::vec2 screenToWorld(const glm::vec2& screen_pos) const; ///< @brief 屏幕坐标转世界坐标
void setPosition(const glm::vec2& position); ///< @brief 设置相机位置
void setLimitBounds(const engine::utils::Rect& bounds); ///< @brief 设置限制相机的移动范围
const glm::vec2& getPosition() const; ///< @brief 获取相机位置
std::optional<engine::utils::Rect> getLimitBounds() const; ///< @brief 获取限制相机的移动范围
glm::vec2 getViewportSize() const; ///< @brief 获取视口大小
// 禁用拷贝和移动语义
Camera(const Camera&) = delete;
Camera& operator=(const Camera&) = delete;
Camera(Camera&&) = delete;
Camera& operator=(Camera&&) = delete;
private:
void clampPosition(); ///< @brief 限制相机位置在边界内
};
} // namespace engine::render
+199
View File
@@ -0,0 +1,199 @@
#include "renderer.h"
#include "../resource/resource_manager.h"
#include "camera.h"
#include "sprite.h"
#include <SDL3/SDL.h>
#include <stdexcept> // For std::runtime_error
#include <spdlog/spdlog.h>
namespace engine::render {
// 构造函数: 执行初始化,增加 ResourceManager
Renderer::Renderer(SDL_Renderer* sdl_renderer, engine::resource::ResourceManager* resource_manager)
: renderer_(sdl_renderer), resource_manager_(resource_manager)
{
spdlog::trace("构造 Renderer...");
if (!renderer_) {
throw std::runtime_error("Renderer 构造失败: 提供的 SDL_Renderer 指针为空。");
}
if (!resource_manager_) {
// ResourceManager 是 drawSprite 所必需的
throw std::runtime_error("Renderer 构造失败: 提供的 ResourceManager 指针为空。");
}
setDrawColor(0, 0, 0, 255);
spdlog::trace("Renderer 构造成功。");
}
void Renderer::drawSprite(const Camera& camera, const Sprite& sprite, const glm::vec2& position, const glm::vec2& scale, double angle) {
auto texture = resource_manager_->getTexture(sprite.getTextureId());
if (!texture) {
spdlog::error("无法为 ID {} 获取纹理。", sprite.getTextureId());
return;
}
auto src_rect = getSpriteSrcRect(sprite);
if (!src_rect.has_value()) {
spdlog::error("无法获取精灵的源矩形,ID: {}", sprite.getTextureId());
return;
}
// 应用相机变换
glm::vec2 position_screen = camera.worldToScreen(position);
// 计算目标矩形,注意 position 是精灵的左上角坐标
float scaled_w = src_rect.value().w * scale.x;
float scaled_h = src_rect.value().h * scale.y;
SDL_FRect dest_rect = {
position_screen.x,
position_screen.y,
scaled_w,
scaled_h
};
if (!isRectInViewport(camera, dest_rect)) { // 视口裁剪:如果精灵超出视口,则不绘制
// spdlog::info("精灵超出视口范围,ID: {}", sprite.getTextureId());
return;
}
// 执行绘制(默认旋转中心为精灵的中心点)
if (!SDL_RenderTextureRotated(renderer_, texture, &src_rect.value(), &dest_rect, angle, NULL, sprite.isFlipped() ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE)) {
spdlog::error("渲染旋转纹理失败(ID: {}):{}", sprite.getTextureId(), SDL_GetError());
}
}
void Renderer::drawParallax(const Camera &camera, const Sprite &sprite, const glm::vec2 &position, const glm::vec2 &scroll_factor, const glm::bvec2 &repeat, const glm::vec2 &scale)
{
auto texture = resource_manager_->getTexture(sprite.getTextureId());
if (!texture) {
spdlog::error("无法为 ID {} 获取纹理。", sprite.getTextureId());
return;
}
auto src_rect = getSpriteSrcRect(sprite);
if (!src_rect.has_value()) {
spdlog::error("无法获取精灵的源矩形,ID: {}", sprite.getTextureId());
return;
}
// 应用相机变换
glm::vec2 position_screen = camera.worldToScreenWithParallax(position, scroll_factor);
// 计算缩放后的纹理尺寸
float scaled_tex_w = src_rect.value().w * scale.x;
float scaled_tex_h = src_rect.value().h * scale.y;
glm::vec2 start, stop;
glm::vec2 viewport_size = camera.getViewportSize();
if (repeat.x) {
// 使用 glm::mod 进行浮点数取模
start.x = glm::mod(position_screen.x, scaled_tex_w) - scaled_tex_w;
stop.x = viewport_size.x;
} else {
start.x = position_screen.x;
stop.x = glm::min(position_screen.x + scaled_tex_w, viewport_size.x); // 结束点是一个纹理宽度之后,但不超过视口宽度
}
if (repeat.y) {
start.y = glm::mod(position_screen.y, scaled_tex_h) - scaled_tex_h;
stop.y = viewport_size.y;
} else {
start.y = position_screen.y;
stop.y = glm::min(position_screen.y + scaled_tex_h, viewport_size.y); // 结束点是一个纹理高度之后,但不超过视口高度
}
for (float y = start.y; y < stop.y; y += scaled_tex_h) {
for (float x = start.x; x < stop.x; x += scaled_tex_w) {
SDL_FRect dest_rect = {x, y, scaled_tex_w, scaled_tex_h};
if (!SDL_RenderTexture(renderer_, texture, nullptr, &dest_rect)) {
spdlog::error("渲染视差纹理失败(ID: {}):{}", sprite.getTextureId(), SDL_GetError());
return;
}
}
}
}
void Renderer::drawUISprite(const Sprite& sprite, const glm::vec2& position, const std::optional<glm::vec2>& size) {
auto texture = resource_manager_->getTexture(sprite.getTextureId());
if (!texture) {
spdlog::error("无法为 ID {} 获取纹理。", sprite.getTextureId());
return;
}
auto src_rect = getSpriteSrcRect(sprite);
if (!src_rect.has_value()) {
spdlog::error("无法获取精灵的源矩形,ID: {}", sprite.getTextureId());
return;
}
SDL_FRect dest_rect = {position.x, position.y, 0, 0}; // 首先确定目标矩形的左上角坐标
if (size.has_value()) { // 如果提供了尺寸,则使用提供的尺寸
dest_rect.w = size.value().x;
dest_rect.h = size.value().y;
} else { // 如果未提供尺寸,则使用纹理的原始尺寸
dest_rect.w = src_rect.value().w;
dest_rect.h = src_rect.value().h;
}
// 执行绘制(未考虑UI旋转)
if (!SDL_RenderTextureRotated(renderer_, texture, &src_rect.value(), &dest_rect, 0.0, nullptr, sprite.isFlipped() ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE)) {
spdlog::error("渲染 UI Sprite 失败 (ID: {}): {}", sprite.getTextureId(), SDL_GetError());
}
}
void Renderer::setDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a) {
if (!SDL_SetRenderDrawColor(renderer_, r, g, b, a)) {
spdlog::error("设置渲染绘制颜色失败:{}", SDL_GetError());
}
}
void Renderer::setDrawColorFloat(float r, float g, float b, float a)
{
if (!SDL_SetRenderDrawColorFloat(renderer_, r, g, b, a)) {
spdlog::error("设置渲染绘制颜色失败:{}", SDL_GetError());
}
}
void Renderer::clearScreen() {
if (!SDL_RenderClear(renderer_)) {
spdlog::error("清除渲染器失败:{}", SDL_GetError());
}
}
void Renderer::present()
{
SDL_RenderPresent(renderer_);
}
std::optional<SDL_FRect> Renderer::getSpriteSrcRect(const Sprite &sprite)
{
SDL_Texture* texture = resource_manager_->getTexture(sprite.getTextureId());
if (!texture) {
spdlog::error("无法为 ID {} 获取纹理。", sprite.getTextureId());
return std::nullopt;
}
auto src_rect = sprite.getSourceRect();
if (src_rect.has_value()) { // 如果Sprite中存在指定rect,则判断尺寸是否有效
if (src_rect.value().w <= 0 || src_rect.value().h <= 0) {
spdlog::error("源矩形尺寸无效,ID: {}", sprite.getTextureId());
return std::nullopt;
}
return src_rect;
} else { // 否则获取纹理尺寸并返回整个纹理大小
SDL_FRect result = {0, 0, 0, 0};
if (!SDL_GetTextureSize(texture, &result.w, &result.h)) {
spdlog::error("无法获取纹理尺寸,ID: {}", sprite.getTextureId());
return std::nullopt;
}
return result;
}
}
bool Renderer::isRectInViewport(const Camera& camera, const SDL_FRect &rect)
{
glm::vec2 viewport_size = camera.getViewportSize();
return rect.x + rect.w >= 0 && rect.x <= viewport_size.x && // 相当于 AABB碰撞检测
rect.y + rect.h >= 0 && rect.y <= viewport_size.y;
}
} // namespace engine::render
+91
View File
@@ -0,0 +1,91 @@
#pragma once
#include "sprite.h"
#include <string>
#include <optional> // For std::optional
#include <glm/glm.hpp>
struct SDL_Renderer;
struct SDL_FRect;
namespace engine::resource {
class ResourceManager;
}
namespace engine::render {
class Camera;
/**
* @brief 封装 SDL3 渲染操作
*
* 包装 SDL_Renderer 并提供清除屏幕、绘制精灵和呈现最终图像的方法。
* 在构造时初始化。依赖于一个有效的 SDL_Renderer 和 ResourceManager。
* 构造失败会抛出异常。
*/
class Renderer final{
private:
SDL_Renderer* renderer_ = nullptr; ///< @brief 指向 SDL_Renderer 的非拥有指针
engine::resource::ResourceManager* resource_manager_ = nullptr; ///< @brief 指向 ResourceManager 的非拥有指针
public:
/**
* @brief 构造函数
*
* @param sdl_renderer 指向有效的 SDL_Renderer 的指针。不能为空。
* @param resource_manager 指向有效的 ResourceManager 的指针。不能为空。
* @throws std::runtime_error 如果任一指针为 nullptr。
*/
Renderer(SDL_Renderer* sdl_renderer, engine::resource::ResourceManager* resource_manager);
/**
* @brief 绘制一个精灵
*
* @param sprite 包含纹理ID、源矩形和翻转状态的 Sprite 对象。
* @param position 世界坐标中的左上角位置。
* @param scale 缩放因子。
* @param angle 旋转角度(度)。
*/
void drawSprite(const Camera& camera, const Sprite& sprite, const glm::vec2& position,
const glm::vec2& scale = {1.0f, 1.0f}, double angle = 0.0f);
/**
* @brief 绘制视差滚动背景
*
* @param sprite 包含纹理ID、源矩形和翻转状态的 Sprite 对象。
* @param position 世界坐标。
* @param scroll_factor 滚动因子。
* @param scale 缩放因子。
*/
void drawParallax(const Camera& camera, const Sprite& sprite, const glm::vec2& position,
const glm::vec2& scroll_factor, const glm::bvec2& repeat = {true, true}, const glm::vec2& scale = {1.0f, 1.0f});
/**
* @brief 在屏幕坐标中直接渲染一个用于UI的Sprite对象。
*
* @param sprite 包含纹理ID、源矩形和翻转状态的Sprite对象。
* @param position 屏幕坐标中的左上角位置。
* @param size 可选:目标矩形的大小。如果为 std::nullopt,则使用Sprite的原始大小。
*/
void drawUISprite(const Sprite& sprite, const glm::vec2& position, const std::optional<glm::vec2>& size = std::nullopt);
void present(); ///< @brief 更新屏幕,包装 SDL_RenderPresent 函数
void clearScreen(); ///< @brief 清屏,包装 SDL_RenderClear 函数
void setDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255); ///< @brief 设置绘制颜色,包装 SDL_SetRenderDrawColor 函数,使用 Uint8 类型
void setDrawColorFloat(float r, float g, float b, float a = 1.0f); ///< @brief 设置绘制颜色,包装 SDL_SetRenderDrawColorFloat 函数,使用 float 类型
SDL_Renderer* getSDLRenderer() const { return renderer_; } ///< @brief 获取底层的 SDL_Renderer 指针
// 禁用拷贝和移动语义
Renderer(const Renderer&) = delete;
Renderer& operator=(const Renderer&) = delete;
Renderer(Renderer&&) = delete;
Renderer& operator=(Renderer&&) = delete;
private:
std::optional<SDL_FRect> getSpriteSrcRect(const Sprite& sprite); ///< @brief 获取精灵的源矩形,用于具体绘制。出现错误则返回std::nullopt并跳过绘制
bool isRectInViewport(const Camera& camera, const SDL_FRect& rect); ///< @brief 判断矩形是否在视口中,用于视口裁剪
};
} // namespace engine::render
+46
View File
@@ -0,0 +1,46 @@
#pragma once
#include <SDL3/SDL_rect.h> // 用于 SDL_FRect
#include <optional> // 用于 std::optional 表示可选的源矩形
#include <string>
namespace engine::render {
/**
* @brief 表示要绘制的视觉精灵的数据。
*
* 包含纹理标识符、要绘制的纹理部分(源矩形)以及翻转状态。
* 位置、缩放和旋转由外部(例如 SpriteComponent)标识。
* 渲染工作由 Renderer 类完成。(传入Sprite作为参数)
*/
class Sprite final{
private:
std::string texture_id_; ///< @brief 纹理资源的标识符
std::optional<SDL_FRect> source_rect_; ///< @brief 可选:要绘制的纹理部分
bool is_flipped_ = false; ///< @brief 是否水平翻转
public:
/**
* @brief 构造一个精灵
*
* @param texture_id 纹理资源的标识符。不应为空。
* @param source_rect 可选的源矩形(SDL_FRect),定义要使用的纹理部分。如果为 std::nullopt,则使用整个纹理。
* @param is_flipped 是否水平翻转
*/
Sprite(const std::string& texture_id, const std::optional<SDL_FRect>& source_rect = std::nullopt, bool is_flipped = false)
: texture_id_(texture_id),
source_rect_(source_rect),
is_flipped_(is_flipped)
{}
// --- getters and setters ---
const std::string& getTextureId() const { return texture_id_; } ///< @brief 获取纹理 ID
const std::optional<SDL_FRect>& getSourceRect() const { return source_rect_; } ///< @brief 获取源矩形 (如果使用整个纹理则为 std::nullopt)
bool isFlipped() const { return is_flipped_; } ///< @brief 获取是否水平翻转
void setTextureId(const std::string& texture_id) { texture_id_ = texture_id; } ///< @brief 设置纹理 ID
void setSourceRect(const std::optional<SDL_FRect>& source_rect) { source_rect_ = source_rect; } ///< @brief 设置源矩形 (如果使用整个纹理则为 std::nullopt)
void setFlipped(bool flipped) { is_flipped_ = flipped; } ///< @brief 设置是否水平翻转
};
} // namespace engine::render
+12
View File
@@ -0,0 +1,12 @@
#pragma once
#include <glm/glm.hpp>
namespace engine::utils {
struct Rect
{
glm::vec2 position;
glm::vec2 size;
};
} // namespace engine::utils
+1 -1
View File
@@ -2,7 +2,7 @@
#include <spdlog/spdlog.h>
int main(int /* argc */, char* /* argv */[]) {
spdlog::set_level(spdlog::level::debug);
// spdlog::set_level(spdlog::level::debug);
engine::core::GameApp app;
app.run();