概述
之前完成了主循环、帧率控制以及资源管理模块,现在我们需要完成渲染工作,将游戏对象绘制到屏幕上。
这章需要理解
- 精灵(Sprite)-描述“画什么”,使用哪张纹理、纹理的哪一部分
- 相机(Camera)-玩家视窗,决定显示,负责坐标和视图转换
- 渲染器(Renderer)-封装底层绘制API,接收精灵与相机信息,执行真正的绘制精灵

1.逻辑分辨率:像素游戏的关键
像素风格的游戏,希望在不同物理分辨率(1080p/2k/4k)下,画面像素块保持同样观感,避免被拉伸变形。SDL3提供了逻辑分辨率方法
实现--->在GameApp::initSDL()中添加
SDL_SetRenderLogicalPresentation(sdl_renderer_,640,360,SDL_LOGICAL_PRESENTATION_LETTERBOX); // 设置渲染器逻辑分辨率
设置含义:
- 逻辑画布固定 - 为
640×360 - 自动缩放 - SDL会将逻辑画布等比例缩放以适配实际窗口大小
- 黑边填充 - 当窗口宽高比不为
16:9时,LEFTRBOX模式自动填充黑边,避免画面拉伸
所有游戏逻辑与坐标计算均在640×360的逻辑分辨率下进行,大幅简化开发与适配难度。
Q:什么是逻辑画布?
A:就用640×360(16:9)举例子,我们在这个虚拟尺寸上做游戏,比如我在(100,50)这个坐标放了一个大小(40,40)的方块也就是说在这张640×360画纸上,画在了第100列、50行的位置。但实际上我们的窗口是1280×720(16:9),窗口比例和逻辑画布是一样的,那么就会显示缩放倍数为2的样子,没有黑边,真实像素位置:(100*2,50*2)=(200,100),真实像素大小:(40*2,40*2)=(80,80),代码里写的是(100,50,40,40)但玩家用户见到的显示确是(200,100,80,80)
但是如果不是16:9的窗口那怎么办?比如800×600(4:3),我们用了LETTERBOX,所以必须保持画纸的16:9,放不下的地方就加黑边了。
先算缩放倍数:
- 800/640 = 1.25
- 600/360 = 1.666…
为了“完整塞进窗口”,得取更小的那个 → scale = 1.25
缩放后画面尺寸:
- 宽:640*1.25 = 800
- 高:360*1.25 = 450
窗口是 800×600,但内容只有 800×450,所以还剩下:
- 垂直方向多出来 600-450 = 150
上下各一半 → 上黑边 75 px,下黑边 75 px
这时方块在真实窗口里的位置变成:
- 真实 x = 100*1.25 = 125
- 真实 y = 50*1.25 + 75 = 62.5 + 75 = 137.5(大概 138)
- 真实 w = 40*1.25 = 50
- 真实 h = 40*1.25 = 50
重点:你还是画在逻辑坐标 (100,50),SDL 帮你加了“缩放 + 黑边偏移”。
2.新项目结构
我们将渲染相关类放入render/目录,将通用工具放入utils/目录,需要在render目录下创建renderer.h/cpp、camera.h/cpp、sprite.h,utils下创建math.h
3.Sprite类
Sprite是一个轻量级数据类,用来描述一个“待绘制”的视觉元素,不包含复杂逻辑
// sprite.h
#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_; // 纹理资源标识符std::optional<SDL_FRect> source_rect_; // 要绘制的纹理部分bool is_flipped_ = false; // 是否水平翻转
public:/*** @brief 构造一个精灵* @param texture_id 纹理资源标识符* @param source_rect 要绘制的纹理部分 这是一个可选的参数,定义要使用的纹理部分,若为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_; }const std::optional<SDL_FRect>& getSourceRect() const { return source_rect_; }bool isFlipped() const { return is_flipped_; }void setTextureId(const std::string& texture_id) { texture_id_ = texture_id; }void setSourceRect(const std::optional<SDL_FRect>& source_rect) { source_rect_ = source_rect; }void setFlipped(bool is_flipped) { is_flipped_ = is_flipped; }};
}
核心属性
- texture_id_ : 要使用的纹理标识
- source_rect_ : 可选的
SDL_FRect,用于从原图中截取子区域,比如一张图上有多个游戏对象,就需要截取子区域来获取,如果是nullopt就使用整张纹理 - is_flipped_ : 是否水平翻转
4.Camera类
Camera模拟摄像机,定义在广阔世界中可见的区域,核心职责是坐标变换和视图控制
// camera.h
#pragma once
#include "../utils/math.h"
#include <optional>namespace engine::render {class Camera final {
private:glm::vec2 viewport_size_; // 视口大小(屏幕大小)glm::vec2 position_; // 相机左上角的世界坐标std::optional<engine::utils::Rect> limit_bounds_; // 相机限制范围,为空则无限制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); // 更新相机位置void move(const glm::vec2& offset); // 移动相机glm::vec2 worldToScreen(const glm::vec2& world_pos) const; // 将世界坐标转换为屏幕坐标glm::vec2 screenToWorld(const glm::vec2& screen_pos) const; // 将屏幕坐标转换为世界坐标// 将世界坐标转换为屏幕坐标,考虑视差效果glm::vec2 worldToScreenWithParallax(const glm::vec2& world_pos, const glm::vec2& scroll_factor) const; void setPosition(const glm::vec2& position); // 设置相机位置void setLimitBounds(const engine::utils::Rect& bounds); // 设置相机限制范围const glm::vec2& getPosition() const; // 获取相机位置std::optional<engine::utils::Rect> getLimitBounds() const; // 获取相机限制范围const glm::vec2& getViewportSize() const; // 获取视口大小// 禁用拷贝和移动语义Camera(const Camera&) = delete;Camera& operator=(const Camera&) = delete;Camera(Camera&&) = delete;Camera& operator=(Camera&&) = delete;private:void clampPosition(); // 限制相机位置
};}// camera.cpp
#include "camera.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::update(float ){}void Camera::move(const glm::vec2 &offset){position_ += offset;clampPosition();}glm::vec2 Camera::worldToScreen(const glm::vec2 &world_pos) const{// 屏幕坐标 = 世界坐标 - 相机位置 * 1return world_pos - position_;}glm::vec2 Camera::screenToWorld(const glm::vec2 &screen_pos) const{// 世界坐标 = 屏幕坐标 + 相机位置return screen_pos + position_;}glm::vec2 Camera::worldToScreenWithParallax(const glm::vec2 &world_pos, const glm::vec2 &scroll_factor) const{// 屏幕坐标 = 世界坐标 - 相机位置 * 滚动因子return world_pos - position_ * scroll_factor;}void Camera::setPosition(const glm::vec2 &position){position_ = position;clampPosition();}void Camera::setLimitBounds(const engine::utils::Rect &bounds){limit_bounds_ = bounds;clampPosition(); // 设置限制边界后,立刻应用限制}const glm::vec2 &Camera::getPosition() const{return position_;}std::optional<engine::utils::Rect> Camera::getLimitBounds() const{return limit_bounds_;}const glm::vec2 &Camera::getViewportSize() const{return viewport_size_;}void Camera::clampPosition(){if(limit_bounds_.has_value() && limit_bounds_->size.x > 0 && limit_bounds_->size.y > 0){// 计算允许的相机位置范围glm::vec2 min_pos = limit_bounds_->position;glm::vec2 max_pos = limit_bounds_->position + limit_bounds_->size - viewport_size_;// 如果世界大小小于视口大小呢? 这样min_pos 就比 max_pos 大了,max_pos < (0,0)// 我要保证max_pos 一定是大的max_pos.x = std::max(min_pos.x, max_pos.x);max_pos.y = std::max(min_pos.y, max_pos.y);position_ = glm::clamp(position_, min_pos, max_pos);}}
}
坐标系统
- 世界坐标&屏幕坐标 - 在"世界坐标"(关卡中的绝对位置)与"屏幕坐标"(
640×360画布上的绘制位置)之间转换
核心方法
- worldToScreen() - 将世界坐标转换为屏幕坐标,核心为
screen_pos = world_pos - camera_pos
视差滚动(Parallax)
worldToScreenWithParallax(scroll_factor)按滚动因子进行相对运动:
- scroll_factor = 1 -> 与相机同步移动
- scroll_factor = 0 -> 禁止(经典UI)
- 0 < scroll_factor < 1 -> 产生景深效果的视差滚动
用眼睛来作为相机,当我们往左边看去,近处的物体是向右边移动的,我相机往左移动5个单位,可以近似看作近处物体往右边移动5个单位,但是远处的景观不同,可能只移动3个单位,更远处的可能只有1个单位。我们可以利用这个视差滚动来实现游戏对象的显示更贴近实际
移动边界
setLimitBounds()限制相机移动范围,防止越界关卡。很简单,我们希望Camera位置在限定的位置移动。

比如我们希望Camera的pos只在世界坐标的范围中,那么我的camera_pos = world_pos - camera_viewport_size,camera_pos(camera左上角)只允许在蓝色框中。
自定义Rect
相机中的limitBounds会用到engine::utils::Rect定义在src/engine/utils/math.h中:
// math.h
#pragma once
#include<glm/glm.hpp>namespace engine::utils {
struct Rect{glm::vec2 position;glm::vec2 size;
};}
5.Renderer类
Renderer是渲染中枢,封装SDL底层绘制API,并提供更高层、易用的绘制接口:
// renderer.h
#pragma once
#include "sprite.h"
#include <glm/glm.hpp>struct SDL_Renderer;namespace engine::resource {class ResourceManager;
}namespace engine::render {
class Camera;/*** @brief 封装SDL3的渲染操作* 包装 SDL_Renderer 并提供清除屏幕、绘制精灵和呈现最终图像的方法* 构造时初始化。依赖于一个有效的 SDL_Renderer 和 ResourceManager* 构造失败抛出异常*/
class Renderer final {
private:SDL_Renderer *sdl_renderer_ = nullptr; // SDL3的渲染器engine::resource::ResourceManager *resource_manager_ = nullptr; // 资源管理器public:/*** @brief 构造函数* @param sdl_renderer SDL3的渲染器* @param resource_manager 资源管理器* @throws std::runtime_error 如果 sdl_renderer 或 resource_manager 为空 -> 抛出异常*/Renderer(SDL_Renderer *sdl_renderer, engine::resource::ResourceManager *resource_manager);/*** @brief 绘制一个精灵* * @param camera 摄像机* @param sprite 精灵 包含 纹理ID、源矩形、是否翻转* @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 camera 摄像机* @param sprite 精灵 包含 纹理ID、源矩形、是否翻转* @param position 精灵位置* @param scroll_factor 滚动因子* @param repeat 是否重复* @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精灵对象* * @param sprite 精灵 包含 纹理ID、源矩形、是否翻转* @param position 精灵位置* @param size (std::optional)目标矩形大小。如果为nullopt,则使用精灵的默认大小* */void drawUISprite(const Sprite& sprite, const glm::vec2& position,const std::optional<glm::vec2> size = std::nullopt);void present(); // 更新屏幕 --- SDL_RenderPresent 函数void clearScreen(); // 清屏 --- SDL_RenderClear 函数void setDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a = 255); // 设置绘制颜色void setDrawColorFloat(float r, float g, float b, float a = 1.0f); // 设置绘制颜色SDL_Renderer* getSDLRenderer() const {return sdl_renderer_;}; // 获取SDL3的渲染器// 禁用拷贝和移动语义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) const; // 获取精灵的源矩形bool isRectInViewport(const Camera& camera, const SDL_FRect& rect) const; // 判断矩形是否在视口中};}
// renderer.cpp
#include "renderer.h"
#include "../resource/resource_manager.h"
#include "camera.h"
#include <SDL3/SDL.h>
#include <stdexcept>
#include <spdlog/spdlog.h>namespace engine::render {Renderer::Renderer(SDL_Renderer *sdl_renderer, engine::resource::ResourceManager *resource_manager): sdl_renderer_(sdl_renderer), resource_manager_(resource_manager) {if(!sdl_renderer_ || !resource_manager_){throw std::runtime_error("sdl_renderer or resource_manager is null");}setDrawColor(0,0,0,255);spdlog::trace("渲染器创建成功!");}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){spdlog::error("无法为ID {} 获得源矩形。", sprite.getTextureId());return;}// 相机变换glm::vec2 position_screen = camera.worldToScreen(position);// 计算目标矩形, pos是左上角坐标SDL_FRect dst_rect = {position_screen.x,position_screen.y,src_rect.value().w * scale.x,src_rect.value().h * scale.y};if(!isRectInViewport(camera, dst_rect)){// 超出视口就不进行绘制return;}// 绘制 (默认旋转中心为精灵的中心点)if(!SDL_RenderTextureRotated(sdl_renderer_, texture, &src_rect.value(), &dst_rect, angle,nullptr, sprite.isFlipped() ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE)){spdlog::error("绘制纹理失败了,info:{}", 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){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;// 绘制视差背景,根据repeat参数决定是否重复绘制glm::vec2 start, stop;glm::vec2 viewport_size = camera.getViewportSize();if(repeat.x){// 水平重复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 x = start.x; x < stop.x; x += scaled_tex_w){for(float y = start.y; y < stop.y; y += scaled_tex_h){SDL_FRect dst_rect = {x,y,src_rect.value().w * scale.x,src_rect.value().h * scale.y};if(!SDL_RenderTexture(sdl_renderer_, texture, &src_rect.value(), &dst_rect)){spdlog::error("绘制纹理失败了,info:{}", 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){spdlog::error("无法为ID {} 获得源矩形。", sprite.getTextureId());return;}// 计算目标矩形, pos是左上角坐标SDL_FRect dst_rect = {position.x,position.y,size ? size.value().x : src_rect.value().w,size ? size.value().y : src_rect.value().h};if(!SDL_RenderTextureRotated(sdl_renderer_, texture, &src_rect.value(), &dst_rect,0.0,nullptr,sprite.isFlipped() ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE)){spdlog::error("绘制纹理失败了,info:{}", SDL_GetError());return;}}void Renderer::present(){SDL_RenderPresent(sdl_renderer_);}void Renderer::clearScreen(){if(!SDL_RenderClear(sdl_renderer_)){spdlog::error("清除渲染器失败,info:{}", SDL_GetError());};}void Renderer::setDrawColor(Uint8 r, Uint8 g, Uint8 b, Uint8 a){if(!SDL_SetRenderDrawColor(sdl_renderer_, r, g, b, a)){spdlog::error("设置绘制颜色失败了,info:{}", SDL_GetError());}}void Renderer::setDrawColorFloat(float r, float g, float b, float a){if(!SDL_SetRenderDrawColorFloat(sdl_renderer_, r, g, b, a)){spdlog::error("设置绘制颜色失败了,info:{}", SDL_GetError());}}std::optional<SDL_FRect> Renderer::getSpriteSrcRect(const Sprite &sprite) const{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){ // 如果源矩形存在 Sprite中存在指定rect, 则判断尺寸是否有效if(src_rect.value().w <= 0 || src_rect.value().h <= 0){spdlog::error("源矩形无效,宽度和高度必须大于0.");return std::nullopt;}return src_rect;} else { // 若为std::nullopt,则使用整个纹理SDL_FRect result = {0,0,0,0};if(!SDL_GetTextureSize(texture, &result.w, &result.h)){spdlog::error("源矩形rect获取失败");return std::nullopt;}return result;}}bool Renderer::isRectInViewport(const Camera &camera, const SDL_FRect &rect) const{glm::vec2 viewport_size = camera.getViewportSize(); // 获取视口尺寸// 相当于AABB碰撞检测if(rect.x >= viewport_size.x || rect.y >= viewport_size.y ||rect.x + rect.w <= 0 || rect.y + rect.h <= 0){return false;}return true;}
}
依赖关系
构造时接收SDL_Renderer*与ResourceManager*,以便直接调用SDL并向资源管理器请求纹理。
主要方法
- drawSprite() - 绘制受相机影响的世界物体;先做坐标转换,再进行视口裁剪,屏幕外则跳过节省资源
- drawParallax() - 用于可重复的视差背景;根据相机位置与滚动因子计算需要绘制的背景块,实现无限滚动
- drawUISpite() - 用于绘制固定在屏幕坐标系的UI(血条,菜单),不受相机影响。
6.集成&测试
在GameApp中以unique_ptr持有Renderer与Camera,在init()阶段初始化(与Time、ResourceManager模式一致)。
// game_app.h 新增
// ...
namespace engine::render{
class Camera;
class Renderer;
}class GameApp final {
private:SDL_Window* window_ = nullptr;SDL_Renderer* sdl_renderer_ = nullptr; bool is_running_ = false;// 引擎组件std::unique_ptr<engine::core::Time> time_;std::unique_ptr<engine::resource::ResourceManager> resource_manager_;std::unique_ptr<engine::render::Camera> camera_;std::unique_ptr<engine::render::Renderer> renderer_;[[nodiscard]] bool initRenderer();[[nodiscard]] bool initCamera();void testCamera();void testRenderer();}
// game_app.cpp
//...
bool GameApp::init(){//...if(!initRenderer()) return false;if(!initCamera()) return false;//...
}
void GameApp::update(float)
{// 游戏逻辑更新testCamera();
}void GameApp::render(){renderer_->clearScreen();testRenderer();renderer_->present();
}
bool GameApp::initSDL(){//...SDL_SetRenderLogicalPresentation(sdl_renderer_, 640,360, SDL_LOGICAL_PRESENTATION_LETTERBOX); // 设置渲染器逻辑分辨率
}bool GameApp::initRenderer()
{try {renderer_ = std::make_unique<engine::render::Renderer>(sdl_renderer_, resource_manager_.get());} catch (const std::exception& e){spdlog::error("初始化Renderer失败: {}", e.what());return false;}return true;
}bool GameApp::initCamera()
{try {camera_ = std::make_unique<engine::render::Camera>(glm::vec2(640,360));} catch (const std::exception& e){spdlog::error("初始化Camera失败: {}", e.what());return false;}return true;
}void GameApp::testCamera()
{auto key_state = SDL_GetKeyboardState(nullptr);if(key_state[SDL_SCANCODE_W]){camera_->move(glm::vec2(0, -1));}if(key_state[SDL_SCANCODE_S]){camera_->move(glm::vec2(0, 1));}if(key_state[SDL_SCANCODE_A]){camera_->move(glm::vec2(-1, 0));}if(key_state[SDL_SCANCODE_D]){camera_->move(glm::vec2(1, 0));}
}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");// 渲染顺序注意renderer_->drawParallax(*camera_, sprite_parallax, glm::vec2(0, 0), glm::vec2(0.3f,0.5f),{true,false});renderer_->drawSprite(*camera_,sprite_world, glm::vec2(250, 250));renderer_->drawUISprite(sprite_ui, glm::vec2(100, 100));
}
渲染流程
重构为三步:
- renderer_->clearScreen() - 用背景色清空画布
- testRenderer() - 发出所有具体绘制指令
- renderer_->present() - 将画布内容呈现到屏幕
测试函数
testCamera() - 用方向键移动相机,验证视图移动与边界限制
testRenderer() - 同时绘制背景(视差)、青蛙(世界)与按钮(UI),并让青蛙旋转,演示绘制方法
7.编译与运行
确保资源在正确的路径下
效果

遇到的问题:
-
在
renderer.cpp中编写drawParallax时,在判断start和stop
// 绘制视差背景,根据repeat参数决定是否重复绘制glm::vec2 start, stop;glm::vec2 viewport_size = camera.getViewportSize();if(repeat.x){// 水平重复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 x = start.x; x < stop.x; x += scaled_tex_w){for(float y = start.y; y < stop.y; y += scaled_tex_h){SDL_FRect dst_rect = {x,y,src_rect.value().w * scale.x,src_rect.value().h * scale.y};if(!SDL_RenderTexture(sdl_renderer_, texture, &src_rect.value(), &dst_rect)){spdlog::error("绘制纹理失败了,info:{}", SDL_GetError());return;}}}
当时想的是比如要绘制一个对象在x方向重复,那么应该是贴合整个视口viewport的,比如一个需要绘制的对象是w = 200px,视口是800px,然后position_screen.x定在50px这个位置,那么这个该怎么绘制,或许应该从position_screen.x mod 200 = 50px这个位置开始绘制,但是这样左边就空了50px,所以要减去一个宽度texture_w,就从1-texture_w = -150px(start.x),这里开始绘制,然后一直加到(stop.x) 650px(最后一个650+200=850px填满viewport_size.x方向),当时错误的以为stop.x是-150 + 800 = 650px,这样会导致650~800px这段空了出来,没有铺满

-
对于
renderer类中std::optional<SDL_FRect> getSpriteSrcRect(const Sprite& sprite) const;// 获取精灵的源矩形的疑惑
原本我以为这个函数感觉多此一举?因为在Sprite.h中有const std::optional<SDL_FRect>& getSourceRect() const { return source_rect_; } 但是后来发现不对,因为多数Texture使用的就是整张图片,所以这个函数得到的是std::nullopt,但是在通过renderer_绘制的应该需要源矩形 src_FRect 类似于{0,0,w,h}的东西,对应{x,y,w,h},但是你如果用sprite.getSourceRect()得到的就不对了,你拿到的是一个std::nullopt,所以我们在renderer类中写一个这样的函数得到{0,0,w,h}这样的SDL_FRect,实现了解耦,而且这个功能确实应该是renderer该做的。
-
这里简单讲讲3中draw的区别和理解
首先,绘制需要的sdl_renderer和ResourceManager不必多说,drawSprite和drawParallax都需要一个camera,你知道的,因为这些都涉及到世界坐标和屏幕坐标的转换,所以需要使用到相机的位置,而drawUISprite只是绘制在屏幕上。
-
Camera类的clampPosition函数
void Camera::clampPosition(){if(limit_bounds_.has_value() && limit_bounds_->size.x > 0 && limit_bounds_->size.y > 0){// 计算允许的相机位置范围glm::vec2 min_pos = limit_bounds_->position;glm::vec2 max_pos = limit_bounds_->position + limit_bounds_->size - viewport_size_;// 如果世界大小小于视口大小呢? 这样min_pos 就比 max_pos 大了,max_pos < (0,0)// 我要保证max_pos 一定是大的max_pos.x = std::max(min_pos.x, max_pos.x);max_pos.y = std::max(min_pos.y, max_pos.y);position_ = glm::clamp(position_, min_pos, max_pos);}}
这里注意的就是限制相机的位置,如果世界比相机还小就会出现异常,比如世界是500×500px,相机是800×800,那么我进来时候比如相机position是在(0,0),显然,它包住了世界,但是一般可能会去计算得到一个(500- 800,500-800) = (-300,-300)的位置,这是max_pos,所以需要多判断一步,或者在初始化Camera使用的时候做好判断,如果相机视口比世界还大的话,就需要一些额外的设置了