news 2026/9/28 22:30:58

C语言课程设计实战:控制台版球球大作战开发全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C语言课程设计实战:控制台版球球大作战开发全解析

C语言课程设计,最愁人的往往不是题目本身,而是题目无聊。我们班交上去的选题,十个里有八个是学生成绩管理系统、图书管理系统,剩下两个是计算器。我做的是“伪版球球大作战”——控制台里一张100×28的地图上,玩家用@表示,AI用o和O表示,食物用.表示。你控制小球去吃食物、吞比你小的球,同时躲开比你大的球,越吃越大,越大越强,直到把全场对手清光或者自己被吞。这个项目用到的全是C语言课内知识点:结构体、数组、随机数、字符界面输出,核心算法就是碰撞检测和简单的AI寻路。写这篇文章是给同样在为课程设计发愁的同学一个参考,代码在文末,可以直接复制去跑,也能当答辩演示用。

我自己的体验是:这类“游戏型课程设计”在答辩时非常占便宜,因为效果直观,老师一眼就能看出程序在干什么,不需要你从头解释“这是一个管理系统,能增删改查”。而且它展示的不只是语法,还有数据结构设计、算法思路和工程拆分能力,这些都是课程设计真正想考察的东西。

1. 项目概述与整体设计

1.1 为什么课程设计要选这个题目

选题目之前我先定了个标准:不能太简单,不能显得像抄作业,最好还能让答辩现场有点互动感。C语言课设的常见套路是各种管理系统,但这类项目有个问题——你很难证明代码是你自己想的,因为网上模板太多,老师都看腻了。反而是游戏类项目,尤其这种带实时交互的,代码风格、算法设计都带着明显的个人痕迹,老师一眼能看出你下了功夫。

“球球大作战”这个玩法本身就适合课程设计。核心规则一句话就能说明白:大球吃小球。但真做起来,涉及的东西一点都不少——地图管理、多对象模拟、移动逻辑、碰撞检测、AI决策、界面刷新,每一个模块都能单独拿出来讲。我把它定位成“伪版”,就是用自己的方式模仿核心玩法,而不是复刻商业游戏,这样既有辨识度,又不至于给自己挖一个填不完的坑。

答辩的时候我准备了一句话总结:这个项目用结构体数组管理三类游戏对象,用距离判定实现吞噬逻辑,用简单的威胁感知策略驱动AI运动,整个程序跑在一个固定帧率的实时循环里。这句话把项目骨架描述得清清楚楚,也是这篇文章要拆解的核心。

1.2 玩法规则与最终效果

最终做出来的游戏就是这个样子:一张100列×28行的字符地图,地图上分布着三种东西——食物、AI小球、玩家小球。食物用.表示,吃了一颗长一厘米(size加1),同时加1分;AI球分成两档,o是普通AI,O是已经长得比较大、很危险的AI;玩家用@表示。

核心规则是吞噬判定:两个球如果发生接触(距离小于半径之和),较大的球吞掉较小的球,并把对方一半的尺寸加到自己身上。比如玩家size是8,AI size是6,两者相遇,玩家变成11,AI消失,玩家加50分。反过来,如果玩家只有5,撞上一个size为8的AI,游戏直接结束。

这里有个关键设计:玩家移动步长固定为1个字符,AI也是一样,谁都不会“瞬移”,所以碰撞是有预判空间的,玩家可以通过走位躲大球、追小球。地图边界做了“穿越”处理,从左边出去从右边回来,从上面出去从下面回来,这是借鉴了球球大作战里的环形世界设计,其实实现起来就是几行取模运算,但带来的玩法变化很大——逃跑可以绕圈,追击可以包抄。

一局的体验大概是这样的:开局玩家size只有5,地图上一堆AI,最小的AI是3,最大的能到8,所以开局必须先找食物发育,避开大AI。中间阶段玩家size到10以上,就可以主动追击小AI了。后期目标就是把AI清光或者稳住局面刷分。

1.3 技术选型:控制台方案 vs 图形库

做这类游戏,第一反应往往是上EasyX图形库,画圆、贴图、监听鼠标,效果确实好。但我最后选了纯控制台方案,理由很实际:EasyX是Windows专属图形库,很多同学电脑上没装,或者装的时候各种报错,再加上EasyX项目在答辩时容易被追问“这里面的绘图函数是哪来的”,你得额外解释第三方库的原理。

纯控制台方案的优势是零依赖,只要编译器支持标准C和Windows API的conio.h、windows.h就能跑。而且它的渲染思路其实是很多2D游戏的原型——维护一个逻辑坐标下的“游戏世界”,再把世界状态绘制到屏幕上。这个思想放到任何游戏引擎里都成立,只是我们用的是字符而不是像素。

当然,控制台方案的代价也很明显:画面简陋,没有真正的圆形,只有字符;不能接受鼠标输入,只能用键盘控制;刷新如果写得不好会闪屏。这些问题我在第四节会详细讲怎么解决。总体来说,对课程设计这个场景,控制台方案是在“效果”和“可解释性”之间最平衡的选择。

2. 核心模块设计与实现思路

2.1 地图与实体管理

游戏世界是一个100×28的字符矩阵。为什么是100×28?因为Windows控制台默认窗口大概能显示80×25,但我在设计时希望地图比屏幕稍大一点,让玩家有“探索”的感觉。实际跑的时候,把控制台窗口拉大或者把字体调小,100列打印出来效果正好。28行也是同理,太矮了地图显得局促,太高了刷屏时上下滚动会加剧闪烁。

地图上的对象分三类:玩家、AI、食物。每类对象都用一个结构体管理,结构体只有四个字段:x坐标、y坐标、size尺寸、alive存活标记。curriculum design里最常用的一个知识点就在这里——用结构体数组把游戏对象聚集起来,然后通过循环统一处理。

typedef struct { int x, y; int size; int alive; } Ball;

玩家只有一个,所以定义成单独变量Ball player;。AI有8个,定义成数组Ball ai[MAX_AI];。食物有60个,定义成数组Ball food[MAX_FOOD];。这里为什么不直接用链表?两点考虑:一是数组在循环遍历时更直观,访问ai[i]直接索引,不需要指针跳转;二是课程设计阶段,数据量很小(总共不到70个对象),链表节省的那点内存完全没有意义,反而增加代码复杂度和出bug概率。

2.2 控制台渲染方案

控制台渲染的原理,说穿了就是两件事:把光标定位到指定位置,然后打印字符。Windows提供了SetConsoleCursorPosition这个API,我封装成一个gotoxy(x, y)函数,把光标移动到(x, y)位置。配合printf输出,就能在屏幕任意位置画东西。

这里要特别说一个新手容易踩的坑:每帧刷新时,绝对不要用system("cls")清屏。cls会清空整个屏幕,然后所有内容重新打印,肉眼看到的就是剧烈闪烁。正确做法是维护一个屏幕缓冲区——定义一个二维字符数组buffer[MAP_H][MAP_W + 1],先把这一帧的所有对象画到buffer里,然后用gotoxy(0, 0)把光标定位到左上角,一次性把buffer里的所有行打印出来。这样打印的是整帧静态画面,视觉上干净很多。

还有一个隐藏细节:光标默认是闪烁的方块,正好挡在玩家位置,看起来很难受。所以初始化时调用hideCursor(),把光标隐藏掉。这行代码看起来不起眼,但对体验的提升非常明显。

2.3 移动与碰撞检测

玩家的移动逻辑简单直接:检测键盘输入,按W上移、S下移、A左移、D右移,每次移动一格。地图边界做穿越处理,坐标越界后取模回到另一边。这里要注意的是,我每帧只读取一次按键,而不是让按键持续生效。如果按住W不放,因为键盘有自动重复,系统会自动连续触发,效果上很像“持续移动”,实际代码里只是每次循环检测一次。

碰撞检测是整个游戏的核心算法,也是答辩时最值得讲的点。两个球是否接触,最朴素的方法是计算欧几里得距离:

距离 = sqrt((x1-x2)^2 + (y1-y2)^2) 碰撞条件:距离 <= size1 + size2

但sqrt开方运算在每次碰撞检测里调用几十次,虽然现代CPU扛得住,可我们完全没必要用。我的做法是两边同时平方,变成:

int dist2(int x1, int y1, int x2, int y2) { int dx = x1 - x2; int dy = y1 - y2; return dx * dx + dy * dy; } // 碰撞判断 if (dist2(ax, ay, bx, by) <= (asize + bsize) * (asize + bsize)) { // 发生碰撞 }

避免开方,而且判断结果完全一样。这个细节很小,但答辩老师很吃这一套,因为这说明你真的理解计算开销,不是只会调sqrt。

碰撞后的大小判断就是比较size,大的吞小的。这里有个细节:如果两个球完全相等怎么办?我让它们擦肩而过,不触发吞噬。如果用>=判断,会出现两个同样大小的球同时被判定为“吃掉对方”,逻辑乱套。用严格大于等于就安全了。

2.4 AI怎么“有点傻又有点聪明”

AI设计是游戏是否好玩的关键。如果AI只是随机乱走,玩家会觉得索然无味;如果AI太聪明,玩家又会被虐到摔键盘。我的方案是让AI有两种状态:状态一,附近有威胁(比自己大的球),立刻朝反方向逃跑;状态二,没有威胁,就朝最近的食物走。

先看威胁检测:AI遍历所有其他AI和玩家,如果发现某个球的size比自己大,并且距离小于一个阈值(比如20格以内),就启动逃跑模式。逃跑方向很简单——当前AI的位置减去威胁的位置,得到的方向向量就是远离威胁的方向。比如AI在(50, 50),威胁在(40, 48),向量是(10, 2),AI就往右下方移动。

没有威胁时,AI执行觅食逻辑:遍历所有食物,找到离自己最近的那颗,然后让自身坐标向食物坐标逼近——如果食物在右边就向右移一格,在下方就向下移一格。这个逻辑模拟的是“贪心觅食”,每帧都重新规划目标,所以AI看起来是有目的地在探索地图。

这样设计的AI既有基本的生存本能,又不会过于聪明。开局阶段AI会四散找食物,一旦玩家长大靠近它们,它们会警觉地躲开;但如果玩家足够大,追击小人时会发现对方像“泥鳅”一样专门往反方向跑,这就有了游戏性。

3. 关键代码解析与实操演示

3.1 数据结构与全局变量

完整的代码我放在第六节。这里先说明几个核心定义。全局变量主要是四个:地图尺寸宏、玩家对象、AI数组、食物数组。地图尺寸用宏定义,因为后面多次用到,比如边界判断、循环遍历,如果以后想把地图调大调小,只需要改宏定义,全局自动生效。

#define MAP_W 100 #define MAP_H 28 #define MAX_FOOD 60 #define MAX_AI 8

这里有个经验:把游戏对象、地图尺寸这些东西全部定义在全局区,是课程设计阶段最省心的做法。虽然工程上不推荐全局变量,但你能在答辩时理直气壮地说“这是为了在小规模程序里简化函数之间的数据传递”,老师也不会怼你。等你做真正的项目,再考虑把全局变量收进结构体或者用参数传递。

3.2 初始化流程

游戏开始前的初始化分三块:玩家初始位置在屏幕正中,初始size为5;AI位置随机分布在整张地图,初始size在3到8之间随机,保证有大有小;食物位置随机,一颗接一颗铺满地图。

随机数种子必须加,而且要加在初始化函数里,用当前时间做种子:

srand((unsigned)time(NULL));

如果不加这行,每次运行游戏生成的随机布局都一样,一开局哪儿有食物、AI去哪吃,玩家背都背下来了,游戏就不存在随机性了。

食物生成我单独封装了一个函数:

void addFood(int index) { food[index].x = rand() % MAP_W; food[index].y = rand() % MAP_H; food[index].size = 1; food[index].alive = 1; }

这样在初始化时调用60次,在食物被吃掉后重新调用,让食物在随机位置刷新,代码结构很干净。

3.3 主循环的运行机制

主循环是整个游戏的心脏,结构就是经典的“输入-更新-渲染”三件套:

while (!gameOver) { handleInput(); // 处理玩家键盘输入 for (i = 0; i < MAX_AI; i++) { if (ai[i].alive) aiMove(&ai[i]); // 更新每个AI } checkCollision(); // 碰撞检测 + 吞噬处理 drawMap(); // 渲染当前画面 Sleep(50); // 控制帧率 }

Sleep(50)控制每帧间隔50毫秒,大约每秒20帧。对于字符界面游戏来说,这个帧率足够流畅。不写Sleep的话,循环会以CPU的最高速度狂转,AI移动快得看不清,而且CPU占用率直接飙到100%,所以我每次演示前都会特意提一句这个Sleep是控制节奏的关键。

循环顺序是有讲究的:先处理输入,再更新AI,再检查碰撞,最后渲染。如果顺序反了,比如先渲染后更新,玩家会看到自己按下按键之后要慢一帧才有反应,也就是俗称的“输入延迟”。虽然50毫秒的延迟感知不明显,但逻辑上应该保持“先逻辑后渲染”。

3.4 碰撞处理的顺序问题

碰撞检测不是一句“两两判断”就完了,顺序处理不好会出现很诡异的bug。我的代码里分了三步:

第一步,玩家和食物的碰撞。玩家碰到食物就吃掉,size加1,记1分,食物被标记为死亡并重生成。第二步,AI和食物的碰撞,逻辑一样。第三步,玩家和AI、AI和AI的碰撞。先检查玩家和每个AI,再检查AI之间的两两碰撞。

这里容易出问题的是AI互吃。假设有三个AI,A的大小是8,B是5,C是4。一帧之内A和B碰上了,A吞掉B,size变成10;紧接着A又和C碰上了,A又吞掉C,size变成12。这种情况在游戏逻辑上没问题,因为本来就该一帧只处理一次碰撞。但如果你在同一个循环里用了原地修改的数据结构,可能会出现A在同一帧被两个球“同时吞掉”的奇葩状态。我的方案是用alive标记配合严格的大小比较,每次吞噬后下一轮循环重新检查,保证吞并是链式但有序的。这个细节在答辩时讲出来,让人觉得你考虑得很全面。

4. 踩坑记录与避坑指南

4.1 屏幕闪烁问题

这个问题我调试了很久。最初版本我也是用system("cls")清屏,运行时整个屏幕疯狂闪动,看得眼睛疼。后来改成buffer整帧刷新,问题才解决。具体做法就是前面说的:不一行一行打印,而是先把所有内容整理进一个二维字符数组,然后从(0,0)起始一次性按行输出。还有个附加技巧:打印完地图后故意留两行空白,再在下面输出状态信息,这样状态信息不会和地图内容挤在一起。

4.2 数组越界导致“幽灵球”

第一次做碰撞检测的时候,我用了很多ai[i].alive判断,但早期版本里AI被吃掉之后只是标记alive=0,没有把坐标重置。结果渲染时虽然不显示它了,但碰撞检测循环还是会遍历它,导致“地图上明明没有这个球,玩家走过去却被弹开”的幽灵球现象。修复办法就是在所有对AI的遍历里加上alive判断,渲染跳过,碰撞也跳过,这是必然的。这个坑我建议在答辩时主动提出来,说明你知道循环遍历时要考虑对象存活状态,能加分。

4.3 边界穿越与卡位问题

实现边界穿越后,出现了一个新bug:AI在地图边缘逃跑时,坐标从0变成MAP_W-1,位置瞬间从左边跑到右边,但如果追踪它的玩家也跟着穿越边界,视觉上两个球像是在“瞬移”。调试半天发现这是逻辑正确的,只是显示效果有点跳。处理方法是在渲染层不做任何坐标修正,让玩家慢慢习惯“穿越后可能出现在对面”这个设定,毕竟球球大作战原版也是环形世界,玩家多玩几局就懂了。

4.4 编译环境与中文乱码

代码用到的_getch()和_kbhit()是Windows Visual Studio系列的头文件函数。如果你用Dev-C++或者老版本MinGW编译器,可能要改成getch()和kbhit(),不带下划线。另外中文提示信息在控制台上默认按GBK编码显示,如果源码文件是UTF-8编码,可能打印出乱码。我自己的处理是:代码里的中文注释不影响运行,但printf输出的中文如果乱码,要么把源码文件另存为GBK编码,要么用英文提示。答辩演示时建议提前在目标机器上完整跑一遍,别在现场调编码问题。

5. 答辩演示与加分扩展

5.1 向老师讲解的四个要点

答辩时间一般只有五到十分钟,不要意图把所有代码都讲一遍,讲清楚四个点就足够清晰了:

第一,数据结构。用结构体表示球,说明三个对象集合分别是结构体数组,alive字段用来标记存活状态。第二,碰撞检测。距离平方代替开方,严格大于避免同尺寸互吞,吞噬后size增量规则。第三,AI策略。威胁感知二态机,优先逃跑,其次觅食。第四,实时循环。输入→更新→碰撞→渲染的循环框架,Sleep控制帧率。

把四个点串成一句话:“这是一个由结构体数组组织对象、以距离碰撞为交互核心、让AI具备威胁感知能力、运行在标准游戏循环里的实时对战模拟程序。”这句话说完,老师基本知道你确实是自己写出来的。

5.2 想拿高分还能加什么

如果课程设计要求更高,或者你想冲优秀,至少有三个扩展方向:第一,添加障碍物。在地图里随机生成一些不可穿越的墙块,AI需要绕开,碰撞检测会复杂一些。第二,保存最高分。把历史最高分用文件读写存到本地txt文件,每次游戏结束更新,这正好用上文件操作知识点。第三,鼠标操作。如果敢引入EasyX,可以把整个游戏移植成图形版,画面效果翻好几倍,但工作量也会翻倍。

5.3 从课程设计到真实项目的差距

这个项目做完之后,我再回头看真实游戏开发的思路,发现其实很多原理是相通的。游戏引擎里的所谓的“物理系统”“AI行为树”“渲染管线”,本质上和我们这个控制台游戏里做的事是一样的——管理对象、判定碰撞、做决策、画画面。课程设计的限定环境逼着你用最笨的方案解决这些问题,反而把原理看得更透。等以后学Unity或者别的引擎,你会发现很多概念你已经通过C语言课设提前熟悉过了。

6. 完整代码可直接运行

下面是完整的C语言代码,保存为.c文件,在Windows环境下用Visual Studio或Dev-C++编译即可运行。编译前把控制台窗口拉大一点,字体调到合适大小,保证100列能完整显示。控制方式:WASD移动。

/* 伪版球球大作战 - C语言课程设计 * 运行环境:Windows + Visual Studio / Dev-C++ * 控制方式:WASD移动,大球可吞噬小球 */ #include <stdio.h> #include <stdlib.h> #include <time.h> #include <ctype.h> #include <conio.h> #include <windows.h> #define MAP_W 100 #define MAP_H 28 #define MAX_FOOD 60 #define MAX_AI 8 int score = 0; typedef struct { int x, y; int size; int alive; } Ball; Ball player; Ball ai[MAX_AI]; Ball food[MAX_FOOD]; void gotoxy(int x, int y); void hideCursor(); void initGame(); void drawMap(); void handleInput(); void aiMove(Ball* b); void checkCollision(); int dist2(int x1, int y1, int x2, int y2); void addFood(int index); int countAliveAI(); void gotoxy(int x, int y) { COORD c; c.X = x; c.Y = y; SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), c); } void hideCursor() { CONSOLE_CURSOR_INFO info; info.dwSize = 1; info.bVisible = 0; SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &info); } int dist2(int x1, int y1, int x2, int y2) { int dx = x1 - x2; int dy = y1 - y2; return dx * dx + dy * dy; } void addFood(int index) { food[index].x = rand() % MAP_W; food[index].y = rand() % MAP_H; food[index].size = 1; food[index].alive = 1; } void initGame() { int i; player.x = MAP_W / 2; player.y = MAP_H / 2; player.size = 5; player.alive = 1; score = 0; srand((unsigned)time(NULL)); for (i = 0; i < MAX_AI; i++) { ai[i].x = rand() % MAP_W; ai[i].y = rand() % MAP_H; ai[i].size = 3 + rand() % 6; ai[i].alive = 1; } for (i = 0; i < MAX_FOOD; i++) { addFood(i); } } int countAliveAI() { int i, cnt = 0; for (i = 0; i < MAX_AI; i++) { if (ai[i].alive) cnt++; } return cnt; } void drawMap() { int i, x, y; static char buffer[MAP_H][MAP_W + 1]; for (y = 0; y < MAP_H; y++) { for (x = 0; x < MAP_W; x++) { buffer[y][x] = ' '; } buffer[y][MAP_W] = '\0'; } for (i = 0; i < MAX_FOOD; i++) { if (food[i].alive) { buffer[food[i].y][food[i].x] = '.'; } } for (i = 0; i < MAX_AI; i++) { if (ai[i].alive) { buffer[ai[i].y][ai[i].x] = ai[i].size >= 8 ? 'O' : 'o'; } } if (player.alive) { buffer[player.y][player.x] = '@'; } gotoxy(0, 0); printf("Score: %d PlayerSize: %d AI Left: %d\n", score, player.size, countAliveAI()); for (y = 0; y < MAP_H; y++) { printf("%s\n", buffer[y]); } } void handleInput() { char key; if (_kbhit()) { key = _getch(); key = tolower(key); switch (key) { case 'w': player.y--; break; case 's': player.y++; break; case 'a': player.x--; break; case 'd': player.x++; break; } if (player.x < 0) player.x = MAP_W - 1; if (player.x >= MAP_W) player.x = 0; if (player.y < 0) player.y = MAP_H - 1; if (player.y >= MAP_H) player.y = 0; } } void aiMove(Ball* b) { int i; int nearestFood = -1; int minFoodDist = 999999; int threatFound = 0; int tx = 0, ty = 0; if (player.alive && player.size > b->size) { int d = dist2(b->x, b->y, player.x, player.y); if (d < 200) { threatFound = 1; tx = b->x - player.x; ty = b->y - player.y; } } if (!threatFound) { for (i = 0; i < MAX_AI; i++) { if (!ai[i].alive || &ai[i] == b) continue; if (ai[i].size <= b->size) continue; int d = dist2(b->x, b->y, ai[i].x, ai[i].y); if (d < 200) { threatFound = 1; tx = b->x - ai[i].x; ty = b->y - ai[i].y; break; } } } if (threatFound) { if (tx == 0) tx = (rand() % 2) ? 1 : -1; if (ty == 0) ty = (rand() % 2) ? 1 : -1; b->x += (tx > 0 ? 1 : -1); b->y += (ty > 0 ? 1 : -1); } else { for (i = 0; i < MAX_FOOD; i++) { if (!food[i].alive) continue; int d = dist2(b->x, b->y, food[i].x, food[i].y); if (d < minFoodDist) { minFoodDist = d; nearestFood = i; } } if (nearestFood >= 0) { if (food[nearestFood].x > b->x) b->x++; else if (food[nearestFood].x < b->x) b->x--; if (food[nearestFood].y > b->y) b->y++; else if (food[nearestFood].y < b->y) b->y--; } } if (b->x < 0) b->x = MAP_W - 1; if (b->x >= MAP_W) b->x = 0; if (b->y < 0) b->y = MAP_H - 1; if (b->y >= MAP_H) b->y = 0; } void checkCollision() { int i, j; for (i = 0; i < MAX_FOOD; i++) { if (!food[i].alive) continue; if (dist2(player.x, player.y, food[i].x, food[i].y) <= (player.size + food[i].size) * (player.size + food[i].size)) { food[i].alive = 0; addFood(i); player.size++; score += 1; } } for (i = 0; i < MAX_AI; i++) { if (!ai[i].alive) continue; for (j = 0; j < MAX_FOOD; j++) { if (!food[j].alive) continue; if (dist2(ai[i].x, ai[i].y, food[j].x, food[j].y) <= (ai[i].size + food[j].size) * (ai[i].size + food[j].size)) { food[j].alive = 0; addFood(j); ai[i].size++; } } } for (i = 0; i < MAX_AI; i++) { if (!ai[i].alive || !player.alive) continue; int d = dist2(player.x, player.y, ai[i].x, ai[i].y); int r = player.size + ai[i].size; if (d <= r * r) { if (player.size > ai[i].size) { player.size += ai[i].size / 2; score += 50; ai[i].alive = 0; } else if (player.size < ai[i].size) { ai[i].size += player.size / 2; player.alive = 0; } } } for (i = 0; i < MAX_AI; i++) { if (!ai[i].alive) continue; for (j = 0; j < MAX_AI; j++) { if (i == j || !ai[j].alive) continue; int d = dist2(ai[i].x, ai[i].y, ai[j].x, ai[j].y); int r = ai[i].size + ai[j].size; if (d <= r * r) { if (ai[i].size > ai[j].size) { ai[i].size += ai[j].size / 2; ai[j].alive = 0; } else if (ai[i].size < ai[j].size) { ai[j].size += ai[i].size / 2; ai[i].alive = 0; } } } } } int main() { int i; int gameOver = 0; hideCursor(); initGame(); while (!gameOver) { handleInput(); for (i = 0; i < MAX_AI; i++) { if (ai[i].alive) aiMove(&ai[i]); } checkCollision(); drawMap(); if (!player.alive) gameOver = 1; if (countAliveAI() == 0) gameOver = 1; Sleep(50); } gotoxy(0, MAP_H + 2); if (player.alive) { printf("=== You Win! All AI are swallowed! ===\n"); } else { printf("=== Game Over: You are swallowed! ===\n"); } printf("Final Score: %d\n", score); return 0; }

最后再说两个小经验。编译运行前,把控制台窗口最大化,再把字体改成点阵字体或者小号字体,100列才不会折行;如果你用的是老版本Dev-C++,_getch()若提示未声明,改成getch()就行。这个项目后续想扩展,建议先加一个“玩家吃球时显示增长动画”,哪怕只是闪烁一下字符,观感都会上一个台阶。我做完这个课设之后最大的体会是:别怕项目小而简单,把小而简单的项目做到逻辑闭环,把每一步原理吃透,比堆一个自己都讲不清楚的大项目有用得多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 22:30:45

Sealos:把200万运维成本省下来投入产品研发

如果你所在的技术团队有10个人&#xff0c;有没有认真算过&#xff1a;一年下来&#xff0c;花在“让系统不崩、环境不坏、部署不卡”上面的时间&#xff0c;占了多少比例&#xff1f;我见过不少从初创期走过来的团队&#xff0c;业务其实做得不错&#xff0c;但组里最忙的永远…

作者头像 李华
网站建设 2026/9/28 22:28:41

AI编程助手落地终端:Codex CLI与Claude CLI协同工作流实战

前阵子我把自己的终端工作流彻底重写了一遍&#xff0c;项目名叫 CLI-Anything。名字有点狂&#xff0c;但用下来是真顺手&#xff1a;Codex CLI 负责在仓库里跑代码任务、Claude CLI 负责多文件改造和代码解释&#xff0c;再配合 jq、rg 这些老伙计&#xff0c;我在终端里能完…

作者头像 李华
网站建设 2026/9/28 22:28:09

Unity AI Navigation新导航系统:动态烘焙与寻路实战解析

1. 从旧版导航到 AI Navigation&#xff1a;这次升级到底动了什么先说结论&#xff1a;如果你还在用 Unity 内置的 NavMesh&#xff08;Navigation 旧版组件&#xff09;&#xff0c;那你这几年做的地图寻路其实一直处于“能用&#xff0c;但不好扩展”的状态。Unity 从 2022 L…

作者头像 李华
网站建设 2026/9/28 22:27:45

RHEL 9启动过程全解析:从UEFI到systemd的排错指南

网上聊stm32的启动过程、聊项目管理里那个启动过程组&#xff0c;能搜出一大堆&#xff0c;但真正问 rhel9 的启动过程&#xff0c;很多运维兄弟能说出“GRUB 加载、内核起来、systemd 接管”三步&#xff0c;再往下就含糊了。我最早也是在服务器起不来的时候&#xff0c;才下决…

作者头像 李华
网站建设 2026/9/28 22:27:12

电动车电池不耐用?充电骑行存放保养全攻略,延长寿命关键在这

在车友群里看到有人吐槽&#xff0c;说自己的电动车电池骑了一年多就明显不行了&#xff0c;充满电跑不了多远&#xff0c;上坡稍微给点油就掉电。其实市面上绝大多数电池不是用坏的&#xff0c;而是被不正确的使用习惯折腾坏的。这篇文章就围绕电动车电池的使用和维护&#xf…

作者头像 李华
网站建设 2026/9/28 22:27:02

Flutter鸿蒙适配:Icon控件底层原理与交互动效实践

前阵子给一个鸿蒙平板的项目做 Flutter 跨平台适配&#xff0c;界面基本都调通了&#xff0c;结果启动页底部的几个图标全部显示成方框。当时第一反应是字体资源没打进去&#xff0c;但排查了一圈才发现问题出在图标控件对平台字体族的解析逻辑上。这件事让我重新审视了 Flutte…

作者头像 李华