100、C++项目实战:完整游戏项目(平台跳跃游戏)

终于到了第100章。说实话,走到这一步挺不容易的。前面99个实战项目,我们从基础语法、数据结构、网络编程、图形界面一路打怪升级,现在终于要做一个真正完整的游戏了。

平台跳跃游戏,听起来是不是有点小激动?我当年第一次用C++写完一个能跑能跳的小人时,那种成就感比拿到什么证书都强。今天我们就来拆解这个项目的核心架构,让你也能亲手做出来。

项目整体架构

一个完整的平台跳跃游戏,说白了就是三个核心模块:游戏循环物理引擎渲染系统。这三者缺一不可。

我个人习惯先把架构图画出来,再动手写代码。这样心里有谱,不会写着写着就迷路了。

平台跳跃游戏核心架构 游戏循环 (Game Loop) 输入处理 物理更新 渲染输出 重力计算 碰撞检测 跳跃逻辑 精灵绘制 背景滚动 游戏状态 & 实体数据

游戏循环:一切的核心

游戏循环就是整个游戏的「心脏」。它不停地做三件事:读输入 → 更新状态 → 渲染画面。每循环一次就是一帧。

我记得刚开始做游戏时,直接把循环写成了 while(true),结果CPU直接飙到100%。后来才明白,必须控制帧率。

class GameEngine {
public:
    void run() {
        const int TARGET_FPS = 60;
        const int FRAME_DELAY = 1000 / TARGET_FPS;
        
        Uint32 frameStart;
        int frameTime;
        
        while (isRunning) {
            frameStart = SDL_GetTicks();
            
            handleInput();    // 处理键盘/鼠标
            update();         // 更新物理和逻辑
            render();         // 绘制画面
            
            frameTime = SDL_GetTicks() - frameStart;
            if (frameTime < FRAME_DELAY) {
                SDL_Delay(FRAME_DELAY - frameTime);  // 控制帧率
            }
        }
    }
    
private:
    bool isRunning = true;
};
小技巧:SDL_Delay 控制帧率是最简单的方法。但如果你追求更平滑的动画,可以考虑「固定时间步长」方案,把物理更新和渲染分开。

物理引擎:让角色动起来

物理引擎是平台跳跃游戏的灵魂。说白了,就是让角色感觉「有重量」——跳起来会下落,碰到平台会停住。

核心就三个东西:重力速度碰撞

重力与跳跃

重力其实很简单:每帧给角色的垂直速度加一个固定值。跳跃就是瞬间给一个向上的初速度。

class Player {
public:
    float x, y;          // 位置
    float vx, vy;        // 速度
    bool isOnGround;     // 是否在地面
    
    void applyGravity(float gravity, float deltaTime) {
        if (!isOnGround) {
            vy += gravity * deltaTime;  // 重力加速度
        }
    }
    
    void jump(float jumpForce) {
        if (isOnGround) {
            vy = -jumpForce;  // 向上跳,所以是负值
            isOnGround = false;
        }
    }
    
    void update(float deltaTime) {
        x += vx * deltaTime;
        y += vy * deltaTime;
    }
};
注意: 我曾经犯过一个低级错误——把 deltaTime 忘了乘进去。结果游戏在60帧时跑得正常,换到144帧显示器上,角色直接飞出去了。帧率无关的物理更新,这是基本功。

碰撞检测

平台跳跃的碰撞检测,最常用的是 AABB(轴对齐包围盒)。说白了就是判断两个矩形有没有重叠。

struct AABB {
    float x, y;      // 左上角
    float w, h;      // 宽高
    
    bool intersects(const AABB& other) const {
        return x < other.x + other.w &&
               x + w > other.x &&
               y < other.y + other.h &&
               y + h > other.y;
    }
};

嗯,这里要注意:碰撞检测不能只做「是否重叠」的判断。你想想看,如果角色每帧移动5像素,而平台只有4像素厚,角色可能直接穿过去。这就是传说中的「隧道效应」。

我建议的做法是:分轴检测。先沿X轴移动并检测,再沿Y轴移动并检测。这样能有效避免穿透。

渲染系统:让画面动起来

渲染系统负责把游戏世界画到屏幕上。我用的是 SDL2,轻量又好用。

核心思路是:双缓冲 + 纹理缓存。双缓冲避免画面闪烁,纹理缓存减少重复加载。

class Renderer {
public:
    bool init(const char* title, int width, int height) {
        window = SDL_CreateWindow(title, 
            SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
            width, height, SDL_WINDOW_SHOWN);
        renderer = SDL_CreateRenderer(window, -1, 
            SDL_RENDERER_ACCELERATED);
        return window && renderer;
    }
    
    void clear() {
        SDL_SetRenderDrawColor(renderer, 135, 206, 235, 255); // 天蓝色背景
        SDL_RenderClear(renderer);
    }
    
    void drawTexture(SDL_Texture* tex, int x, int y, int w, int h) {
        SDL_Rect dst = {x, y, w, h};
        SDL_RenderCopy(renderer, tex, nullptr, &dst);
    }
    
    void present() {
        SDL_RenderPresent(renderer);  // 交换缓冲区
    }
    
private:
    SDL_Window* window = nullptr;
    SDL_Renderer* renderer = nullptr;
};
核心要点: 渲染时只绘制「视口内」的物体。别把整个地图都画上去,那会卡死。用摄像机坐标裁剪,只显示屏幕范围内的精灵。

游戏实体管理

一个平台跳跃游戏里,有玩家、敌人、平台、金币、陷阱……这些东西怎么管理?

我习惯用 组件模式。每个实体是一个容器,挂载不同的组件(位置组件、渲染组件、物理组件)。这样灵活,也好扩展。

class Entity {
public:
    int id;
    std::unordered_map<std::string, Component*> components;
    
    template<typename T>
    T* getComponent(const std::string& name) {
        auto it = components.find(name);
        if (it != components.end()) {
            return dynamic_cast<T*>(it->second);
        }
        return nullptr;
    }
    
    void addComponent(const std::string& name, Component* comp) {
        components[name] = comp;
    }
};

// 使用示例
Entity player;
player.addComponent("transform", new TransformComponent(100, 300));
player.addComponent("physics", new PhysicsComponent());
player.addComponent("sprite", new SpriteComponent("player.png"));

关卡设计:从文件加载

硬编码关卡?别闹了。我建议用 JSON 或 CSV 文件来定义关卡。这样改关卡不用重新编译代码。

文件格式 优点 缺点
CSV(二维数组) 简单直观,适合瓦片地图 表达能力有限
JSON 结构清晰,支持嵌套 需要解析库
自定义二进制 加载快,体积小 调试困难

我个人推荐先用 JSON,等游戏做大了再考虑二进制。用 nlohmann/json 这个库,几行代码就能解析。

// 关卡 JSON 示例
{
  "width": 20,
  "height": 15,
  "tileSize": 32,
  "playerStart": {"x": 1, "y": 10},
  "platforms": [
    {"x": 2, "y": 12, "w": 5, "h": 1},
    {"x": 8, "y": 10, "w": 3, "h": 1},
    {"x": 14, "y": 8, "w": 4, "h": 1}
  ],
  "enemies": [
    {"x": 6, "y": 11, "type": "slime"},
    {"x": 12, "y": 9, "type": "bat"}
  ],
  "coins": [
    {"x": 4, "y": 11},
    {"x": 10, "y": 9}
  ]
}

声音与音效

没有声音的游戏,就像没有调料的菜。SDL2 的音频库 SDL_mixer 可以轻松播放音效和背景音乐。

// 初始化音频
Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048);

// 加载音效
Mix_Chunk* jumpSound = Mix_LoadWAV("jump.wav");
Mix_Chunk* coinSound = Mix_LoadWAV("coin.wav");

// 播放音效
Mix_PlayChannel(-1, jumpSound, 0);  // 播放一次

// 加载背景音乐
Mix_Music* bgm = Mix_LoadMUS("background.ogg");
Mix_PlayMusic(bgm, -1);  // 循环播放
经验之谈: 音效文件用 WAV 格式,加载快。背景音乐用 OGG 或 MP3,体积小。别用 WAV 做背景音乐,一首歌几十兆,加载慢还占内存。

调试与性能优化

游戏开发中,调试是家常便饭。我常用的几个技巧:

  • 显示 FPS:在窗口标题或角落显示帧率,一眼就能看出性能问题。
  • 碰撞盒可视化:把 AABB 边框画出来,方便调试碰撞逻辑。
  • 日志系统:记录关键事件(玩家死亡、金币收集),方便排查 bug。
void Game::toggleDebug() {
    showDebug = !showDebug;
}

void Game::renderDebug() {
    if (!showDebug) return;
    
    // 显示 FPS
    std::string fpsText = "FPS: " + std::to_string(currentFPS);
    renderText(fpsText, 10, 10);
    
    // 绘制碰撞盒
    for (auto& entity : entities) {
        auto* phys = entity->getComponent<PhysicsComponent>("physics");
        if (phys) {
            SDL_SetRenderDrawColor(renderer, 255, 0, 0, 128);
            SDL_RenderDrawRect(renderer, &phys->getAABB());
        }
    }
}

打包与发布

游戏写完了,怎么给别人玩?你不能让人家先装个 Visual Studio 再编译吧。

我建议用 CMake + CPack 来打包。把所有依赖(SDL2、SDL_image、SDL_mixer 等)的 DLL 都放到同一个目录下,然后打包成 ZIP 或安装包。

# CMakeLists.txt 片段
install(TARGETS MyPlatformGame DESTINATION bin)
install(DIRECTORY assets/ DESTINATION assets)
install(FILES 
    ${SDL2_DLL} 
    ${SDL2_IMAGE_DLL} 
    ${SDL2_MIXER_DLL}
    DESTINATION bin
)

# 打包命令
# cpack -G ZIP   # 生成 ZIP 包
# cpack -G NSIS  # 生成 Windows 安装包
踩坑提醒: 我曾经打包时忘了把 SDL2.dll 放进去,结果发给朋友后,他双击游戏直接报错「找不到 DLL」。从那以后,我每次打包都会用 Dependency Walker 检查一遍依赖。

总结

平台跳跃游戏,说难不难,说简单也不简单。核心就是:游戏循环驱动、物理引擎模拟、渲染系统输出。把这三点吃透了,你就能做出一个能跑能跳、有碰撞有收集的完整游戏。

我做了这么多年 C++ 项目,最深的体会是:别怕动手。代码写出来,跑起来,哪怕有 bug,那也是进步。你想想看,第一个平台跳跃游戏《大金刚》才多少行代码?现在的你,完全有能力做出更好的。

好了,拿起键盘,开始写你的第一个游戏循环吧。


公众号:蓝海资料掘金营,微信deep3321