← 返回项目← Back to projects

SDLSpaceShooter

用 C++17 + SDL2 从零手写的 2D 太空射击游戏A 2D space shooter hand-written from scratch in C++17 + SDL2

C++17 SDL2 游戏开发Game Dev CMake 跨平台Cross-platform
SDLSpaceShooter ship

背景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

我的角色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:

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

项目基于 ZiyuGameDev 的 SDL2 教程起步,在其基础上做了架构与玩法的二次开发。Built on top of ZiyuGameDev's SDL2 tutorial, with architecture and gameplay redeveloped further.