动手写代码之前,先动笔画图。 这一节做两件事: ① 把"游戏要有什么"拆成一张功能结构图(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(); // 绘制一帧
};
看到什么算成功:
- 能一眼说出"哪个类管哪件事",不需要看实现;
- 能说出
CMovable为什么存在(坦克和子弹的共同部分); - 能说出
bullets为什么用vector而不是数组(数量会变)。
读结果:
virtual void move() = 0;是纯虚函数(11.5):父类只规定"必须有这个动作",不管怎么做 —— 这就是多态用在设计上的样子;virtual ~CMovable() {}是虚析构(11.4.2):通过父类指针删除子类对象时必须这样写,否则子类的析构不会被调用;std::vector<std::string> cells用字符串数组存地图:一行地图 = 一个字符串("##..##"),既直观又省事(15.3.3 读文本文件的思路)。
六、30 秒实验
- 拿纸笔,把"功能结构图"里的某个方块抹掉(比如"自动寻路")→ 后面的代码会少多少? 体会"设计图上少一笔,代码少一片"。
- 把游戏主循环里的"更新"和"绘制"调换顺序,在纸上想一遍 → 你会看到"画面比逻辑慢一帧"。这就是顺序不能乱的原因。
- 试着不用
CMovable父类,把CTank和CBullet各写一遍 → 位置、角度、alive全要写两份。体会继承省掉的是什么。
七、常见误区
- "设计就是浪费时间,直接写" —— 对这个规模的项目,设计省下的时间远超过画图的时间(第一节那张表)。
- "功能结构图要画得很正规" —— 不用。方块 + 连线,自己能看懂就行;目的是"想清楚",不是"交作业"。
- "主循环里先绘制再更新" —— 反了。必须先更新后绘制,否则画面永远慢一帧。
- "坦克和子弹各写各的就行" —— 会有大量重复。把共同部分抽成
CMovable父类(11.1 / 11.5),这是本章最值钱的一处设计。 - "
bullets用数组CBullet bullets[100]也行" —— 能用,但难维护:要自己数还剩几个、要手动挪位置。用vector(13.1)自动扩容、自动清理。 - "这个设计定了就不能改" —— 不是。设计是导航,不是铁轨 —— 写的过程中发现更合理的分法,改了就是(16.7 整合时还会微调)。
自测三问(点开对答案)
1. **为什么本章要"先设计、后编码"?** —— **因为设计图上改一笔,比代码里改一片便宜得多。** **设计解决的是"返工",画图不费时,返工才费时。** 2. **游戏主循环是哪三步?顺序能换吗?** —— **输入 → 更新 → 绘制**,**顺序不能换**(**先更新后绘制**,否则画面慢一帧)。 3. **`CMovable` 这个父类是怎么来的?为什么要有它?** —— **它是"坦克和子弹的共同部分"抽出来的**(都有位置、角度、生死、都要移动和绘制)。**有了它,`CTank` 和 `CBullet` 只写自己特有的部分** —— **这就是继承(11.1)和抽象类(11.5)在实战里的用法。**- ☐ 下一节(16.3 创建项目)
相关笔记
- 开发背景 —— 上一节:做什么、为什么、用什么
- 创建项目 —— 下一节:把工程建起来
- 统一建模语言 —— 设计图的"通用语言"
- 继承 ——
CMovable父类的依据 - 利用虚函数实现动态绑定 —— 虚函数与虚析构
- 抽象类 —— 纯虚函数
= 0 - 向量类模板 —— 用
vector装子弹 - 🔷 C++基础 · 索引 —— 全课路线图