🔷 第十六章 坦克动荡游戏
📄 一句话概括

动手写代码之前,先动笔画图。 这一节做两件事: ① 把"游戏要有什么"拆成一张功能结构图(16.2.1); ② 把"玩家坐下来到关掉程序"这段时间,画成一条业务流程(16.2.2)。 这两张图定下来,后面六节就是"照着图填代码"。

📝 本节和前面几节的关系

16.1 回答了"做什么、用什么",本节回答"怎么做"的第一步 —— 设计。 本章的顺序是 先设计(16.2)→ 再动手(16.3 起),这是本项目要教的第一条工程习惯。 和 9.3「统一建模语言」的关系:那节讲的 UML 是"设计的通用语言", 本节画的功能结构图和业务流程图,就是 UML 里最常用的两张图(功能分解图 + 活动图); 只不过本章不要求你画得多标准,要求你"想清楚"。

一、为什么先设计,不先写代码

先写代码的后果,你一定体会过 —— 写到一半发现"坦克和子弹都得会移动",回头改一堆地方。

设计这一步,解决的就是"返工":

先写代码 先设计
发现问题的时间 写到一半 画图时
改的成本 改好几个类、好几处调用 改一笔
心里有没有底 走一步看一步 知道还差什么

一句话:画图不费时,返工才费时。

二、系统功能结构(16.2.1)

先问一句:"这个游戏要有什么功能?"

把答案一条条列出来,再归类、再分层,就得到功能结构:

                    坦克动荡游戏
                         │
       ┌─────────────────┼─────────────────┐
       │                 │                 │
   【菜单系统】      【游戏核心】      【数据与资源】
       │                 │                 │
   ├─ 开始游戏       ├─ 游戏循环        ├─ 地图数据
   ├─ 单人模式       ├─ 坦克(移动/转向)├─ 图片素材
   ├─ 双人对战       ├─ 子弹(发射/反弹)└─ 成绩记录
   └─ 退出游戏       ├─ 碰撞检测
                     ├─ 自动寻路
                     └─ 胜负判定

功能清单(按"谁负责"重新归一次)

功能 一句话说明 难点在哪
游戏菜单 鼠标点选项,换背景图 鼠标响应 + 状态切换
坦克移动转向 任意角度前进、拐弯 角度与坐标的换算(三角函数)
子弹发射 沿坦克朝向飞出 方向向量
子弹反弹 撞墙后按反射定律弹开 与墙的碰撞判定
自动寻路 电脑坦克自己找路 最短路径(BFS 一类算法)
碰撞检测 判断"谁撞上了谁" 圆与圆、圆与矩形
地图加载 从文件读地图格子 文件操作(15 章)
胜负判定 一方被击中即结束 状态管理

注意:这张表里,每一行都对应后面某一节的代码。 设计的意义就在于:你现在就知道"总共要做几件事、每件事难在哪"。

三、系统业务流程(16.2.2)

把"玩家的一生"画成一条线

    启动程序
       │
       ▼
   ┌─────────┐   点"退出"    ┌─────────┐
   │  主菜单  │──────────────▶│  退出   │
   └────┬────┘               └─────────┘
        │ 点"开始"
        ▼
   ┌─────────────┐
   │ 初始化游戏   │  加载地图、放好坦克、清空子弹
   └──────┬──────┘
          ▼
   ┌──────────────────────────────┐
   │      游戏主循环(每秒几十次)   │◀──┐
   │  ① 读输入(键盘/鼠标)         │   │
   │  ② 更新(坦克动、子弹飞、判碰撞)│   │
   │  ③ 绘制(把这一帧画到屏幕上)   │───┘
   └──────────────┬───────────────┘
                  │ 有一方被击中
                  ▼
            ┌───────────┐
            │  胜负结算  │  出示获胜方 → 回主菜单 / 退出
            └───────────┘

这条流程里最值得盯的一句:游戏主循环

普通程序:从头跑到尾,结束。 游戏程序:while (游戏没结束) { 输入 → 更新 → 绘制 } —— 每秒重复几十次,一直不结束。

这个循环就是"游戏"和"普通程序"的分水岭(16.4 会把它落到实处)。

三件事的分工,一句话记住:

阶段 干什么 记住它的一句话
输入 看键盘鼠标 "玩家想干什么"
更新 改游戏世界里的数据 "这个世界变成什么样了"
绘制 把数据画到屏幕上 "让玩家看见"

顺序不能乱:先更新、后绘制 —— 反了的话,你画的是"上一帧的世界"。

四、从功能到类:设计图的下一站

功能图是"要有什么",接下来要落到"用哪些类实现":

功能块 大约对应哪个类 来自前面的知识
坦克 CTank(坦克的属性:位置、角度、血量) 10 章 类与对象
子弹 CBullet(位置、方向、飞行) 10 章
地图 CMap(二维格子、墙在哪) 13.1 vector 装二维数据
共同的"可移动"部分 CMovable(位置 + 移动方向) 11 章 继承(坦克/子弹都继承它)
调度一切 CGame(持有上面所有对象) 10 章 + 13 章
菜单与绘制 主窗口 + GDI+ 16.4 / 16.5

看到 CMovable 那一行了吗? —— 把坦克和子弹的共同部分抽出来做成父类,就是 11.1 继承的用处。 设计阶段能想到这一层,后面代码就省一半。

五、动手 Demo:把设计"写"成类骨架

不写实现,只写"有哪几个类、每个类管什么" —— 这一步叫接口设计。

// ── 可移动对象:坦克和子弹的共同父类(11.1 继承)──────────
class CMovable {
public:
    double x, y;          // 位置
    double angle;         // 朝向(弧度或角度,本章统一用角度)
    bool   alive;         // 还活着吗

    CMovable() : x(0), y(0), angle(0), alive(true) {}
    virtual ~CMovable() {}                 // ★ 虚析构(11.4)
    virtual void move() = 0;               // ★ 纯虚函数(11.5):怎么动,子类自己定
    virtual void draw() = 0;               // 怎么画,子类自己定
};

// ── 坦克 ──────────────────────────────────────────────
class CTank : public CMovable {
public:
    bool  isComputer;     // true = 电脑控制
    void move() override; // 处理"前进 / 转向"
    void draw() override; // 画一台坦克
};

// ── 子弹 ──────────────────────────────────────────────
class CBullet : public CMovable {
public:
    int life;             // 还能存在多久
    void move() override; // 沿 angle 前进 + 撞墙反弹
    void draw() override; // 画一颗子弹
};

// ── 地图 ──────────────────────────────────────────────
class CMap {
public:
    std::vector<std::string> cells;        // 二维格子,用字符串数组存(13.1)
    bool isWall(int r, int c) const;       // 这一格是不是墙
    void load(const std::string &file);    // 从文件读地图(15 章)
};

// ── 总调度 ────────────────────────────────────────────
class CGame {
public:
    CMap    map;
    CTank   tanks[2];
    std::vector<CBullet> bullets;          // ★ 子弹会越来越多(13.1 vector)
    void init();                           // 初始化
    void update();                         // 更新一帧
    void render();                         // 绘制一帧
};

看到什么算成功:

读结果:

六、30 秒实验

  1. 拿纸笔,把"功能结构图"里的某个方块抹掉(比如"自动寻路")→ 后面的代码会少多少? 体会"设计图上少一笔,代码少一片"。
  2. 把游戏主循环里的"更新"和"绘制"调换顺序,在纸上想一遍 → 你会看到"画面比逻辑慢一帧"。这就是顺序不能乱的原因。
  3. 试着不用 CMovable 父类,把 CTank 和 CBullet 各写一遍 → 位置、角度、alive 全要写两份。体会继承省掉的是什么。

七、常见误区

  1. "设计就是浪费时间,直接写" —— 对这个规模的项目,设计省下的时间远超过画图的时间(第一节那张表)。
  2. "功能结构图要画得很正规" —— 不用。方块 + 连线,自己能看懂就行;目的是"想清楚",不是"交作业"。
  3. "主循环里先绘制再更新" —— 反了。必须先更新后绘制,否则画面永远慢一帧。
  4. "坦克和子弹各写各的就行" —— 会有大量重复。把共同部分抽成 CMovable 父类(11.1 / 11.5),这是本章最值钱的一处设计。
  5. "bullets 用数组 CBullet bullets[100] 也行" —— 能用,但难维护:要自己数还剩几个、要手动挪位置。用 vector(13.1)自动扩容、自动清理。
  6. "这个设计定了就不能改" —— 不是。设计是导航,不是铁轨 —— 写的过程中发现更合理的分法,改了就是(16.7 整合时还会微调)。
自测三问(点开对答案) 1. **为什么本章要"先设计、后编码"?** —— **因为设计图上改一笔,比代码里改一片便宜得多。** **设计解决的是"返工",画图不费时,返工才费时。** 2. **游戏主循环是哪三步?顺序能换吗?** —— **输入 → 更新 → 绘制**,**顺序不能换**(**先更新后绘制**,否则画面慢一帧)。 3. **`CMovable` 这个父类是怎么来的?为什么要有它?** —— **它是"坦克和子弹的共同部分"抽出来的**(都有位置、角度、生死、都要移动和绘制)。**有了它,`CTank` 和 `CBullet` 只写自己特有的部分** —— **这就是继承(11.1)和抽象类(11.5)在实战里的用法。**

相关笔记