背景Background
想真正搞懂一个游戏是怎么"跑起来"的,光看引擎封装好的 API 是不够的——渲染循环、帧同步、对象管理这些底层的东西,往往被 Unity、Godot 这类引擎藏在了背后。To really understand how a game "runs", reading engine-wrapped APIs isn't enough — the render loop, frame sync, and object management are exactly what engines like Unity or Godot hide behind the scenes.
所以我选了一条更"裸"的路:用 C++17 + SDL2,不借助任何游戏引擎,从窗口创建、事件循环到渲染,一行行自己写一个完整的 2D 太空射击游戏。So I chose the more "bare" path: with C++17 + SDL2, without any game engine, I wrote a complete 2D space shooter line by line — from window creation and the event loop all the way to rendering.
项目的起点是 B 站 UP 主 ZiyuGameDev 的 SDL2 教程,但我没有停在"跟着敲完"这一步——在教程的骨架上,我重写了架构里的几处、并加了一整套自己的玩法和工程化改造。The project started from ZiyuGameDev's SDL2 tutorial on Bilibili, but I didn't stop at "typing along". On top of the tutorial's skeleton I rewrote several parts of the architecture and added a whole set of my own gameplay and engineering improvements.
技术栈Tech Stack
- 语言:C++17Language: C++17
- 图形 / 事件:SDL2Graphics / Events: SDL2
- 扩展库:SDL2_image(PNG 纹理)、SDL2_ttf(TrueType 字体渲染 UI 与排行榜)、SDL2_mixer(背景音乐 + 音效)Extensions: SDL2_image (PNG textures), SDL2_ttf (TrueType UI & leaderboard rendering), SDL2_mixer (BGM + SFX)
- 构建:CMake(≥ 3.20),跨 Windows(MinGW) / Linux / macOS 三平台Build: CMake (≥ 3.20), cross-platform on Windows (MinGW) / Linux / macOS
- 规模:
src/约 1400 行 C++,另含完整美术、音乐、音效资源Scale: ~1400 lines of C++ insrc/, plus full art, music, and sound assets
我的角色My Role
从零到打包发布全部我一个人:架构设计、玩法编码、参数调优、跨平台构建、以及最后 Windows 端的 v1.0 打包(解压即玩)。From zero to a shippable build, entirely on my own: architecture design, gameplay coding, parameter tuning, cross-platform builds, and finally the Windows v1.0 package (unzip & play).
代码上我把整个游戏组织成了两个核心抽象:I organized the whole game around two core abstractions:
1. 单例 Game + 场景状态机1. Singleton Game + scene state machine
Game 用静态局部变量做单例,持有窗口、渲染器、主循环、全局分数和排行榜。游戏的不同阶段拆成三个场景,都继承自 Scene 抽象基类:Game is a singleton via a static local variable, holding the window, renderer, main loop, global score, and leaderboard. The game's phases are split into three scenes, all inheriting from the abstract Scene base class:
SceneTitle:标题页SceneTitle: title screenSceneMain:核心玩法SceneMain: core gameplaySceneEnd:结算 + 输入名字 + 写排行榜SceneEnd: results + name input + leaderboard write
Game::changeScene() 负责切换当前场景,事件、更新、渲染三件事统一转发给 currentScene。这样每个场景只管自己的逻辑,主循环干干净净。Game::changeScene() switches the active scene, and input/update/render are uniformly forwarded to currentScene. Each scene only cares about its own logic, keeping the main loop clean.
2. 数据驱动的对象设计2. Data-driven object design
所有游戏对象(玩家、敌人、双方子弹、爆炸、掉落物、背景)都是 Object.h 里的纯数据 struct,字段就是位置、速度、血量、冷却这些。玩法逻辑集中在 SceneMain 里用 std::list 管理各类对象的生灭。数据和逻辑分离,加新对象或调数值都很轻。Every game object (player, enemies, both sides' bullets, explosions, drops, background) is a plain-data struct in Object.h — fields like position, velocity, HP, cooldown. Gameplay logic lives in SceneMain, managing each object list's lifecycle with std::list. Separating data from logic makes adding objects or tuning numbers lightweight.
难点与解决Challenges & Solutions
① 帧率无关的运动
如果直接用"每帧移动 N 像素",游戏在 144Hz 和 60Hz 的机器上速度就会不一样。解决办法是引入 deltaTime:所有位移都写成 speed * deltaTime * scale,让运动跟真实时间挂钩,而不是跟帧数挂钩。这样任何刷新率下手感一致。① Framerate-independent motion
Moving "N pixels per frame" makes the game run at different speeds on 144Hz vs 60Hz machines. The fix is deltaTime: every displacement becomes speed * deltaTime * scale, tying motion to real time instead of frame count — consistent feel on any refresh rate.
② 多分辨率自适应
不同显示器分辨率差异很大。我用一个统一的 scale 缩放因子:窗口按屏幕高度的 90% 生成、保持 3:4 比例,然后所有精灵尺寸、移动速度、UI 元素都乘以这个因子。一套代码在任何屏幕上比例都不崩。② Multi-resolution scaling
Displays vary a lot. I use a single scale factor: the window is generated at 90% of screen height with a fixed 3:4 ratio, then all sprite sizes, movement speeds, and UI elements multiply by that factor. One codebase, no broken proportions on any screen.
③ 存档到底该放哪
最初排行榜写在项目根目录,结果一打包发布、或者重新下载,存档就丢了。后来改用 SDL_GetPrefPath 写到系统的用户数据目录(Windows 下是 AppData/Roaming)。这才是正确做法——存档跟着用户走,不跟着程序目录走。排行榜本身用 std::multimap<int, string, greater<int>>,插入即自动降序,省掉手动排序。③ Where should saves go
The leaderboard was first written to the project root — then a packaged release or re-download lost it. I switched to SDL_GetPrefPath, writing to the system user-data directory (AppData/Roaming on Windows). That's the correct approach — saves follow the user, not the program directory. The leaderboard itself uses std::multimap<int, string, greater<int>>, auto-sorted descending on insert, no manual sort.
④ 让战斗"有感觉"
教程原版打起来偏平淡。我做了几处玩法改造:敌方子弹会计算指向玩家的方向做追踪;玩家改成三连发、形成横向弹幕;难度随时间递增(开局一分钟后刷怪率按 3 + (elapsed-60000)/5000 增长、封顶 8);再配上精灵表爆炸动画、掉落物撞墙反弹三次、两层星空视差滚动。这些叠起来,手感和节奏就明显不一样了。④ Making combat "feel" right
The tutorial's original felt flat. I added several gameplay tweaks: enemy bullets compute a direction toward the player to track them; the player fires three-shot bursts forming a horizontal bullet pattern; difficulty ramps over time (spawn rate grows by 3 + (elapsed-60000)/5000 after the first minute, capped at 8); plus sprite-sheet explosion animation, drops bouncing off walls three times, and a two-layer parallax starfield. Stacked together, the feel and pacing change noticeably.
成果与收获Outcome & Takeaways
- 一个从窗口到发布完整闭环的 2D 游戏,Windows 端解压即玩(
dist/SDLSpaceShooter_v1.0.zip)。A 2D game with a complete loop from window to release, unzip-and-play on Windows (dist/SDLSpaceShooter_v1.0.zip). - 真正理解了游戏循环的本质:输入 → 更新(含 deltaTime)→ 渲染,以及为什么帧率无关这么重要。A real grasp of the game loop's essence: input → update (with deltaTime) → render, and why framerate independence matters.
- 练到了几个能迁移到任何项目的工程能力:单例与状态机的取舍、数据/逻辑分离、跨平台 CMake 配置、以及"存档/配置该放系统哪个目录"这种容易踩的实际问题。Engineering skills that transfer to any project: singleton vs state-machine tradeoffs, data/logic separation, cross-platform CMake config, and the easy-to-trip practical question of "which system directory should saves/config live in".
- 最大的体会是:跟教程只是起点,真正学到东西是在你开始改它、加自己的想法之后。平衡性、追踪弹、难度曲线、自适应缩放这些,都是教程之外自己啃下来的。Biggest lesson: following a tutorial is only the start; you truly learn once you begin changing it and adding your own ideas. Balance, tracking bullets, difficulty curves, adaptive scaling — all chewed through beyond the tutorial.
项目基于 ZiyuGameDev 的 SDL2 教程起步,在其基础上做了架构与玩法的二次开发。Built on top of ZiyuGameDev's SDL2 tutorial, with architecture and gameplay redeveloped further.