简介:这是一份用C语言实现的经典飞机大战小游戏完整源码包,面向正在学习结构体、函数、指针及游戏循环的C语言初学者,也适合希望了解如何借助第三方图形库构建小型项目的开发者。压缩包共3个文件,包含源文件、编译生成的目标文件以及可直接运行的exe程序,整体仅7KB,体积小巧但代码逻辑完整。已有966人学习下载。通过源码可以研究游戏主循环、碰撞检测、飞机移动、子弹发射与敌人生成等核心模块,并借助可执行文件快速验证运行效果;配合描述中提到的SDL或Allegro等图形库思路,还能进一步理解C语言与外部库的配合方式。对于希望从零动手编写入门级游戏、巩固C语言工程实践的同学而言,这是一份轻量而实用的参考,既便于阅读分析,也适合作为课程设计或兴趣练手的起点。
1. 用 C 语言写飞机大战:先弄清这个项目练的是哪层东西
学完 C 语言的人,十个里有八个会动手写一个控制台版飞机大战。这个项目看起来只是把飞机、子弹、敌机用字符画出来,实际上它把结构体、指针、链表、非阻塞输入、碰撞判定全串在了一起,比背 100 段代码练习题好用得多。我这套 C 语言飞机大战源码,Windows 下用 Dev-C++ 或 VS 都能直接编译跑起来。它教的不只是怎么写游戏循环,而是让你理解一个小型游戏在没有图形引擎时,如何组织数据、控制节奏、处理边界。适合刚学完指针和结构体,想找练手项目的读者。
2. 游戏核心架构:链表管敌机、结构体管实体、循环管节奏
2.1 实体与坐标系:为什么用结构体而不是一堆全局变量
新手写游戏最容易出现的问题,就是给飞机、子弹、敌机分别定义一堆全局变量,比如 playerX、playerY、bulletX、bulletY、enemyX[10]、enemyY[10]。飞机只有一个还好说,子弹和敌机一多,数组下标一乱,代码就变成了黑匣子。
我的做法是先把每个游戏实体定义成结构体,并且统一坐标命名规则。控制台窗口的坐标系和数学坐标系不一样,原点在左上角,行号从上往下增加,列号从左往右增加。如果代码里用 x 和 y,你写渲染时很可能搞反,导致飞机明明按了左键却往上跑。我建议所有实体都直接用 r(行)和 c(列)命名,从源头杜绝混乱。
typedef struct { int r, c; // 玩家位置:r 是行,c 是列 int w, h; // 碰撞矩形的宽和高,后面检测碰撞要用 int hp; // 生命值 } Player; typedef struct { int r, c; // 子弹位置 int speed; // 每帧上移行数 int alive; // 是否存活 } Bullet;结构体语义清楚,传参也方便。比如检测玩家是否被击中,直接把 Player 和 Enemy 的结构体指针传进函数,函数里改 r、c、hp,外部就能拿到结果,不需要搞一堆全局变量到处飞。
对于坐标,还有一个容易忽略的点:控制台字符宽高比不是 1:1,一个字符的高度大约是宽度的两倍,所以想让敌机看起来移动速度正常,垂直方向每次移动 1 行,水平方向也是每次 1 列,视觉上会有点偏慢。实际调的时候,可以让敌机垂直速度跑 2 帧一次,或者干脆依赖后面要讲的 delay 时间调整,不用太纠结。
2.2 敌机用链表管理:插入、遍历、销毁的顺序不能乱
敌机是动态生成的,数量不固定,这时候用固定数组就很不舒服。你开一个Enemy enemies[50],敌人一多就不够用,开大了又浪费。而且数组删除一个元素要移动后面的所有元素,复杂度不高但代码很啰嗦。
正确做法是用单链表。每一架新敌机从屏幕顶部出现,头插法最省事,新节点直接挂在链表头部,遍历时从 head 开始往下走就行。
Enemy* spawnEnemy(int c) { Enemy *e = (Enemy*)malloc(sizeof(Enemy)); e->r = 0; // 从第 0 行进入 e->c = c; // 固定列 e->w = 3; // 敌机占 3 列 e->h = 2; // 占 2 行 e->hp = 1; e->next = head; // 头插法 head = e; return e; }我一般会搭配一个 removeEnemy 函数,它接收链表当前节点的前一个节点指针,把当前节点从链表中断开,然后 free 掉。很多人栽在这里,遍历链表时边遍历边 free,free 完了又取 cur->next,野指针直接崩溃。正确顺序是先保存 next,再处理节点,最后把 cur 指向保存好的 next。
void updateEnemies() { Enemy *prev = NULL; Enemy *cur = head; while (cur) { Enemy *next = cur->next; // 先把下一个节点存下来 cur->r += cur->speed; // 每帧下移 if (cur->r >= MAP_H) { // 飞出屏幕底部 if (prev) prev->next = next; else head = next; free(cur); // 这里才能安全释放 } else { prev = cur; } cur = next; // 继续处理下一个 } }这段代码里最容易忽略的是prev指针。很多人写链表遍历只留 cur,删除节点时发现自己找不到前一个节点,只能用头节点重新遍历。我建议从一开始就维护 prev,删除节点时 prev->next 直接指向 next,效率高而且不容易漏。
子弹如果数量大也可以改成链表,但我个人建议子弹用固定数组加 alive 标记就够了。子弹每秒生成数量有限,循环遍历整个数组跳过 alive 为 0 的项,代码比链表简单,性能也够。
2.3 游戏循环:输入、更新、渲染、延时的次序就是你的节奏
玩过游戏的人都知道,所有实时游戏都有一个主循环。飞机大战的主循环只有四件事:读取输入、更新逻辑、绘制画面、等待一段时间。顺序不能乱,否则会出现输入延迟或者渲染花屏。
while (running) { handleInput(); // 1. 读取键盘 if (state == PLAY) { updateBullets(); // 2. 子弹向上移动 updateEnemies(); // 3. 敌机向下移动 checkCollisions(); // 4. 碰撞检测 } render(); // 5. 把画面画到控制台 Sleep(50); // 6. 等 50ms,约 20 帧每秒 }Update 在 render 前面,这是为了确保绘制时拿到的是最新一帧的数据。如果你先画再更新,玩家按一次键,画面上要下一帧才生效,手感会很肉。
Sleep 放在循环末尾而不是开头。放开头会导致启动时多等一次,放末尾能让每一帧的间隔比较均匀。50ms 对应 20 FPS,这个帧率对字符界面的飞机大战足够流畅,也不会让 CPU 空转发热。如果机器性能好,你可以改成 40ms,也就是 25 FPS,但不要低于 25 帧,否者移动起来一顿一顿的。
这里补充一个选型理由:为什么不直接用循环忙等来计时?while (GetTickCount() - last < 50);这种空转会让 CPU 占用率飙到 100%,笔记本风扇直接起飞。Sleep 会让出 CPU,虽然时间精度不算高,但对飞机大战这种游戏足够了。
3. 渲染与控制输入:控制台动画的两个基础
3.1 光标定位:从 system("cls") 到 gotoxy
几乎每个新手写控制台游戏都从system("cls")开始。它的逻辑很简单:清屏,然后从头把整个画面重新打印一遍。缺点是两个字:闪屏。画面内容一多,人眼能明显看到闪烁,因为清屏之后到重新绘制之间有一小段时间屏幕是空的。
更好的做法是光标定位法。我只需要把光标移动到指定位置,然后打印该位置的字符,不碰其他区域。这样画面更新局部化,闪烁几乎消失。Windows 下用 SetConsoleCursorPosition 函数:
void gotoxy(int r, int c) { HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); COORD pos = { c, r }; // 注意:X 对应列,Y 对应行 SetConsoleCursorPosition(hOut, pos); }注意这段代码里的 COORD,它的第一个成员是 X 轴,对应控制台窗口的列;第二个成员是 Y 轴,对应行。很多人写的时候习惯性写成{ r, c },结果光标跑到了镜像位置,调试半天发现所有字符画在了对角线方向。我在项目里干脆把所有坐标变量名定为 r 和 c,就是不想让这类问题发生。
有了 gotoxy,再封装一个 drawChar 函数,渲染代码就干净了:
void drawChar(int r, int c, char ch) { gotoxy(r, c); putchar(ch); }每次渲染时,调用 drawChar 把玩家的飞机、每一颗子弹、每一架敌机画出来。这里要提醒一个细节:上一帧画过、这一帧已经消失的对象,必须用空格擦掉。否则子弹已经飞走了,屏幕上还留着一个点,看起来就像残影。我一般会在每次主循环开始时,先把上一帧记录的旧位置用空格擦除,然后再画新位置。或者直接每次渲染时把整个地图区域从头到尾刷一遍,这种方式简单但会闪。擦旧画新性能更好,代码也不复杂。
另外还要隐藏光标。光标默认在每个字符位置闪烁显示,游戏运行时光标跳来跳去非常难看。用 SetConsoleCursorInfo 把光标大小设为 1、可见性设为 FALSE 就行。
CONSOLE_CURSOR_INFO cci = { 1, 0 }; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cci);这个设置只需要在程序开始时做一次,后面整个游戏过程中光标都不会显示,不需要每帧调用。
3.2 非阻塞输入:kbhit 与 getch 怎么组合不会留下残键
飞机大战不能等玩家按回车才处理按键,它需要实时响应。标准库里的 scanf 和 getchar 都是阻塞式输入,必须按完回车才返回,根本没法用。Windows 下要用 conio.h 里的 _kbhit 和 _getch,这是 C 语言做控制台小游戏最常见的组合。
_kbhit 检查缓冲区里是否有按键等待,返回非 0 说明有输入;_getch 立即读取一个字符,不回显也不等回车。这样主循环每次都能快速检查一次键盘,有按键就处理,没有就继续更新画面。
if (_kbhit()) { char ch = _getch(); switch (ch) { case 'a': player.c--; break; case 'd': player.c++; break; case 'w': player.r--; break; case 's': player.r++; break; case ' ': fireBullet(); break; case 27: running = 0; break; // ESC 退出 } }这段代码的问题在于,如果你在游戏开始前玩家按了一堆方向键,这些按键全都被缓冲下来了。进入游戏后,主循环会一口气把这些积压的按键全部读出来,飞机瞬间冲到屏幕边缘。我一般在 _kbhit 为真时先清空缓冲区,或者只取最后一个有效按键。
char ch = 0; while (_kbhit()) { ch = _getch(); // 一直读到没按键为止 } if (ch == 'a') player.c--;这样几个连按的按键会被合并成最后一次操作,玩家从菜单进入游戏时不会出现飞机自己乱动的情况。
另外,方向键的处理比字母键麻烦。PC 键盘的方向键在控制台输入中返回两个字节,第一个是 224(或者 0),第二个才是具体的扫描码。如果直接 _getch 读一次,只能拿到 224,屏幕上什么都不会发生。所以遇到 224 或 0 时要再读一次,第二次的 ch 才是方向键扫描码。
int ch = _getch(); if (ch == 224 || ch == 0) { ch = _getch(); // 第二次读取方向键扫描码 if (ch == 72) player.r--; // 上 else if (ch == 80) player.r++; // 下 else if (ch == 75) player.c--; // 左 else if (ch == 77) player.c++; // 右 }Windows 上 Dev-C++ 的 conio.h 提供的是 kbhit 和 getch,Visual Studio 里则强制要求带下划线前缀的 _kbhit 和 _getch。为了两个环境都能编译,我在项目头文件里加了条件宏定义,后面避坑章节会详细说。
4. 碰撞检测与生成节奏:让玩家觉得“打得中、有挑战”
4.1 矩形相交判定:一像素穿模的根因
飞机大战的碰撞检测,最简单的实现是“坐标相等”。子弹移动到某个位置,如果该位置的坐标正好和敌机坐标相等,就判定命中。这个方案初看没问题,实际跑起来全是穿模。
原因很简单:子弹一帧移动 1 行,敌机也可能在移动,两者最小单位是 1,但它们的字符宽度不是 1。我的飞机用三行字符表示,宽度是 3 列;敌机通常占两行两列以上。如果只用子弹左上角那个点的坐标来判断,子弹可能已经从敌机右上角擦过去了,双方都没被判定命中,玩家就会觉得子弹穿模了。
正确做法是把每个对象当成一个矩形区域,检测两个矩形是否相交。矩形相交的判断条件,是两个矩形在横轴和纵轴上的投影都有重叠。
int collide(int ar, int ac, int aw, int ah, int br, int bc, int bw, int bh) { return ar < br + bh && ar + ah > br && ac < bc + bw && ac + aw > bc; }参数含义:ar、ac 是 A 对象左上角的行和列,aw、ah 是它的宽和高。返回值非 0 说明两个矩形有交集。这个公式不需要记,推导起来很简单:A 的顶部在 B 的底部之上,A 的底部在 B 的顶部之下,列方向同理。
在碰撞检测循环里,我一般这么用:
for (int i = 0; i < MAX_BULLETS; i++) { if (!bullets[i].alive) continue; Enemy *e = head; while (e) { if (collide(bullets[i].r, bullets[i].c, 1, 1, e->r, e->c, e->w, e->h)) { bullets[i].alive = 0; // 子弹消失 e->hp -= 1; // 敌机扣血 if (e->hp <= 0) { score += 10; e->hp = 0; // 后续统一回收 } break; } e = e->next; } }子弹是一个点,所以宽高传 1、1;敌机宽 3 高 2 这样传。碰撞时机很重要:先让子弹移动完,再让敌机移动完,最后统一检测,而不是移动一个检测一次。如果移动一格就检测,可能同一颗子弹在一帧内被判定命中两个敌机,逻辑会乱。
玩家和敌机的碰撞也走同一个函数。另外记得在碰撞计算后给玩家一个短暂无敌时间,否则敌机刚擦到玩家,玩家就直接死亡,游戏体验很差。无敌时间可以用计数器实现,setInvincible 后每帧减 1,减到 0 之前不参与碰撞。
4.2 生成间隔与难度曲线:用计数器而不是随机数控制敌机
敌机生成如果写成“每帧都有概率生成一架”,比如if (rand() % 10 == 0) spawnEnemy(...),会出现一个明显问题:同一帧连续冒出好几架敌机,后面几帧又一架不出,节奏完全随机,玩家没法规划走位。
我一般用计数器加阈值的方式。每帧累计生成间隔计数器,达到阈值就生成一架,然后重置计数器,并且重新随机一个下一轮间隔。
static int spawnTimer = 0; static int spawnInterval = 8; if (state == PLAY) { spawnTimer++; if (spawnTimer >= spawnInterval) { int col = rand() % (MAP_W - ENEMY_W); spawnEnemy(col); spawnTimer = 0; spawnInterval = rand() % 4 + 3; // 下一架在 3~6 帧后出现 } }随机数只影响下一轮的间隔,不影响当前帧是否生成。这样生成节奏既均匀又有变化。基础间隔取多少取决于你主循环的延迟,如果 Sleep(50)、每帧约 50ms,3~6 帧就是 150ms 到 300ms 出一架敌机,这个频率前期压力适中。
难度曲线不要只靠生成频率,还可以叠加敌机速度。我的做法是给敌机的 speed 字段也做成动态的:每局游戏经过一段时间或者消灭一定数量敌机后,敌机整体下移速度从 1 提升到 2;分数每增加 100,spawnInterval 的基础值减 1,最低不能低于 2。
int difficulty = score / 100; spawnInterval = 8 - difficulty; if (spawnInterval < 2) spawnInterval = 2;这里关键是做下限保护。如果不限制,分数高了之后 spawnInterval 会变成负数或者 0,每次判断 spawnTimer >= spawnInterval 都会成立,每帧生成好几架敌机,游戏直接无法进行。这类边界条件,写代码时就要想到。
另外一个细节:敌机生成的位置不要只取全地图随机列,否则会有很多敌机出现在屏幕最边缘,玩家根本够不到。我一般把敌机列限制在[2, MAP_W - ENEMY_W - 2],留出两边边距,看起来更自然。
5. 避坑:飞机大战在 Windows 控制台上的六个翻车点
5.1 中文字符乱码
现象:控制台里所有中文字符变成一堆乱码,或者显示为问号。
原因:源文件保存成 UTF-8,但 Windows 控制台默认代码页是 GBK,UTF-8 的中文字节被按 GBK 解码,自然乱码。这个问题在 Visual Studio 上尤其明显,VS 默认保存 UTF-8 带 BOM,而 Dev-C++ 默认 ANSI。
解决:有两个方案。如果游戏界面不需要中文字符,全部用 ASCII 字符画飞机和敌机,这是最省事的,根本不需要碰编码问题。如果需要中文,在 main 函数开头调用SetConsoleOutputCP(65001),告诉控制台用 UTF-8 输出,再把源文件保存为 UTF-8 编码。注意两者缺一不可,只改代码不保存文件格式依然乱码。
提示:这个设置只在当前进程内有效,程序退出后控制台会自动恢复默认代码页,不需要额外还原。
5.2 Visual Studio 编译报错:kbhit 未定义
现象:Dev-C++ 编译好好的代码,放到 VS 里报错“_kbhit is not defined”,getch 也一样。
原因:Dev-C++ 的 conio.h 兼容了不带下划线的旧函数名,VS 里的 C 运行时只提供带下划线前缀的标准版本。
解决:在项目里统一做个宏映射,我一般放在 common.h 里:
#ifdef _WIN32 #include <conio.h> #define kbhit _kbhit #define getch _getch #endif这样代码里都写 kbhit 和 getch,两个环境都能编译。如果你用的是 Linux,conio.h 不存在,需要用 termios 设置终端为非规范模式,那是另一套做法,这里不展开。这个资源包里的代码默认面向 Windows 控制台,我建议初学者先别碰跨平台,专心把业务逻辑写完。
5.3 光标定位失效,或程序运行中突然卡住不动
现象:使用 gotoxy 后字符没有出现在预期位置,或者按 Windows 控制台标题栏时程序整个冻结,点一下才恢复。
原因:第一个问题的根源是控制台窗口的缓冲区与可见窗口大小不一致。屏幕缓冲区可能比窗口大,SetConsoleCursorPosition 的坐标是相对于缓冲区的,而字符显示在可见区域,于是视觉上位置偏了。第二个问题更隐蔽:控制台默认开启了“快速编辑模式”,鼠标点击标题栏或者窗口内部,程序会暂停等待用户输入。
解决:初始化时固定窗口大小和缓冲区大小一致,我一般用system("mode con cols=40 lines=30")设置成 40 列 30 行,再调用 SetConsoleScreenBufferSize 同步缓冲区。对于快速编辑模式,用 GetConsoleMode 和 SetConsoleMode 清除 ENABLE_QUICK_EDIT_MODE 标志位,或者提示玩家不要点击窗口。前一种方式是根治。
HANDLE hIn = GetStdHandle(STD_INPUT_HANDLE); DWORD mode; GetConsoleMode(hIn, &mode); mode &= ~ENABLE_QUICK_EDIT_MODE; // 禁止快速编辑 SetConsoleMode(hIn, mode);这段代码放在游戏初始化时执行一次,之后鼠标点击不会冻结程序。注意 Windows 10 以上的新终端模拟器兼容性更好,但老 CMD 环境必须做这个设置。
5.4 Sleep 时间不准,游戏帧率忽快忽慢
现象:设了 Sleep(50),实际体感有时快有时慢,用 CPU 占用监控能看到跑不满。
原因:Windows 默认时钟精度大约 15ms 左右,Sleep(50) 实际可能在 30ms 到 65ms 之间抖动,帧率不稳定是常态。
解决:如果只是做飞机大战,这个抖动可以接受,不用管。但如果追求平滑滚动,可以用 timeBeginPeriod 把系统计时精度临时提到 1ms,使用完再改回来。timeBeginPeriod 需要链接 winmm.lib,并且会影响系统全局,进程结束前要调用 timeEndPeriod 还原。
#include <windows.h> #include <mmsystem.h> // 链接: #pragma comment(lib, "winmm.lib") timeBeginPeriod(1); // 主循环结束后调用 timeEndPeriod(1)还有一种做法是不要依赖 Sleep 定时,而是记录每帧开始时间,用 GetTickCount 计算已经过去的毫秒数,再决定这一帧补多少等待。这个方案代码量稍大,但对帧率稳定性提升非常明显。我自己写的版本会先测量一帧逻辑加渲染的真实耗时,然后 Sleep(max(0, frameTime - costTime)),保证总帧时间接近 50ms。
5.5 链表内存只增不减,内存占用越来越高
现象:游戏长期运行后内存占用持续上涨,敌机明明已经消失,内存却没有回降。
原因:更新敌机时只把存活标识置成了 0,没有调用 free 释放节点。整个链表越挂越长,遍历也越来越慢,最终游戏变得卡顿。
解决:敌机离开屏幕或者被击杀时,要真正从链表断开并 free。对照 2.2 里的 updateEnemies 写法,用 prev 指针断开连接,然后 free(cur)。不在一段循环里直接 free,而是先保存 next 再操作。
另一个容易漏的是玩家发射的子弹。子弹数组如果包含大量已经死亡但没清除的弹体,遍历成本也不小。我一般会定期压缩数组,或者用环形缓冲区反复覆盖旧子弹,保证数组里始终有可用空位。判断内存是否泄漏,最笨也最有效的方法是任务管理器里盯进程内存,运行两分钟如果稳定不涨就基本没问题。
提示:Visual Studio 调试模式下可以用
_CrtDumpMemoryLeaks()输出内存泄露报告,这是排查链表问题的好工具。
5.6 按住方向键飞机疯狂抖动,键盘响应像抽风
现象:按住左键不放,飞机不是连续平滑移动,而是一顿一顿地跳,松开按键还偶尔多走一步。
原因:控制台输入对标准字符键有“打字机重复”机制,按住一个键会不断产生重复输入事件,速度取决于系统键盘设置,而不是游戏帧率,所以飞机的移动频率和主循环频率对不上,表现就是抖动和乱跳。
解决:如果用的还是 kbhit 循环读按键,处理时不要每个按键事件都当成一次独立移动。改为用 GetAsyncKeyState 直接查询键的物理状态,按住就是按下,松开就是松开,这是游戏开发更常用的方式:
if (GetAsyncKeyState('A') & 0x8000) player.c--; if (GetAsyncKeyState('D') & 0x8000) player.c++;这个函数是轮询式的,每次主循环查一次当前状态,不需要读输入缓冲区,打字机重复机制完全失效,键盘响应只取决于你的主循环频率。这也是我后来做控制台小游戏的首选方案,前提是只用 Windows 平台,因为 GetAsyncKeyState 是 Windows API。
使用这个方案时,射击按键也一起处理:空格键用GetAsyncKeyState(VK_SPACE) & 0x8000,发射逻辑就会变成按住连发。不需要连发就在里面加个射击间隔计数器。
6. 从能玩到好玩:状态机、音效与验证习惯
6.1 状态机是后续维护的地基
飞机大战这种规模的项目,不用状态机也能写出来,但加入菜单、暂停、结束画面之后,只用 if 嵌套就会变得不可维护。枚举状态,在每个状态里各自处理输入和更新,是最普通的做法,效果立竿见影。
typedef enum { MENU, PLAY, PAUSE, OVER } GameState; GameState state = MENU;主循环结构变成这样:
while (running) { handleInput(); switch (state) { case MENU: if (spacePressed) state = PLAY; drawMenu(); break; case PLAY: updateBullets(); updateEnemies(); checkCollisions(); drawGame(); break; case PAUSE: if (pPressed) state = PLAY; drawPauseTips(); break; case OVER: if (rPressed) restartGame(); drawGameOver(); break; } }这个结构里,每个状态只处理自己关心的按键,菜单不会执行敌人更新逻辑,暂停时不推进游戏。后续加设置界面、加关卡切换,只需要扩展枚举和对应分支,老逻辑不用动。
6.2 音效、得分与压力测试:收尾的验证手段
音效用Beep(frequency, duration)就能做。命中敌机时用较高频率短音效,玩家受伤时用低频长音效。注意 Beep 是阻塞的,会卡住主循环,所以音效频率和时长都要短,不能持续播放。真正流畅的做法是放在独立线程里调用,但飞机大战的体量不需要做这个优化,短音效就够了。
得分和最高分可以落盘写入文件,重新开始游戏时读取。用标准库的 fopen、fprintf、fscanf 就能实现,顺手把文件读写练了。程序退出时把最高分写回,下次启动读到后显示在启动画面。
验证这个项目是否真的做完了,我习惯用最笨的办法:开着任务管理器跑十分钟,把分数打高一点,看内存占用是否平稳;然后故意按住方向键撞敌机,看碰撞判定是否在边缘出现异常;最后从头开始新游戏,确认菜单、暂停、结束状态切换都没问题。这三项过了,飞机大战基本就立住了。
我现在不管写什么控制台小游戏,都会先列一个生命周期清单,把实体创建、更新、销毁的时间点写清楚,再写代码。链表节点该 free 的位置,碰撞判定发生在哪一帧,状态切换的入口在哪,全部先写出来,写代码时就少很多纠结。这算是我写这类项目吃过的亏总结下来的习惯。希望你拿到这份 C 语言飞机大战源码后,也能按这个流程走一遍,改出自己的玩法。希望帮到你。
本文还有配套的精品资源,点击获取