C++游戏开发之旅 3
概述
之前完成了主循环、帧率控制以及资源管理模块,现在我们需要完成渲染工作,将游戏对象绘制到屏幕上。
这章需要理解
- 精灵(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
{
// 屏幕坐标 = 世界坐标 - 相机位置 * 1
return 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使用的时候做好判断,如果相机视口比世界还大的话,就需要一些额外的设置了

浙公网安备 33010602011771号