软件工程课程第二次个人作业:用AIGC辅助生成游戏
一、作业信息
| 项目 | 内容 |
|---|---|
| 这个作业属于哪个课程 | 2026-01 软件工程与软件工程实践 |
| 这个作业要求在哪里 | 第二次个人作业:利用 AIGC 完成"一箭又一箭"小游戏 |
| 这个作业的目标 | 使用 Python 和 AIGC 完成"一箭又一箭"小游戏 |
| 学号 | 102401208 |
| GitHub 仓库 | 请关注我吧(>ω<) |
二、项目展示
- 开始界面 / 选择关卡界面
开始界面我让gpt帮我生成了一个比较卡通的背景...结果做出来很像4399小游戏,真是时代的眼泪了,,()

ps我的gif好像压缩太过了有点太糊了,反耳呢又增加了一丝风味,总之意思到了就可以,想体验满血版的话可以前往github自己试一试❥(^_-)
- 游戏过程(含碰撞动画)
在主菜单点击开始游戏会自动进入第一关,第一关会弹出新手教程,在空白处点掉之后会回收到右上角的问号里

按照教程点击箭头可以通关

点击错误会变红碰撞并扣一滴生命值

- 通关结算(三星评价)
通关后显示获得星级、用时、点击次数和失误数

- 失败界面
游戏失败后给出三个选项:再玩一次、选择关卡和回到主菜单

选择关卡
5 个关卡依次解锁,未解锁的显示为灰色,底部是「无尽模式」入口。

设置界面
音量滑条、音效开关,都带悬停放大效果。

提示功能
点提示按钮后,会高亮一个当前可以飞出的箭头。

其实还在开发一些可以换皮肤、添加音乐的功能和更多玩法,后面会添加到仓库中的,敬请期待
三、项目介绍
3.1 游戏规则
「一箭又一箭」是一款点击式箭头解谜小游戏,参考微信小游戏《一箭又一箭》的核心玩法。棋盘中有若干带方向的箭头(上、下、左、右),玩家需要观察箭头的方向与相互阻挡关系,按正确顺序点击箭头:
- 点一个箭头,程序会看它前面有没有被别的箭头堵住;
- 前面一路空到棋盘边缘,它就“嗖”地飞出去,从棋盘上消失;
- 前面有别的箭头堵着,它飞不出去,只能先撞一下再弹回原位,顺便熄灭你一颗爱心;
- 把本关箭头全清掉就过关;爱心用完,游戏就跟你say goodbye了。
3.2 界面设计
游戏包含以下界面:
- 开始界面:标题、开始游戏、选择关卡、设置;
- 选择关卡界面:5 个关卡(未解锁为灰色)+ 无尽模式入口;
- 设置界面:音量调节滑条、音效开关;
- 游戏界面:棋盘、爱心、剩余箭头、用时、菜单按钮、提示按钮;
- 结果界面:通关 / 失败提示,可再玩一次、选择关卡、返回主菜单。
3.3 主要功能
- 5 个 6×6 关卡,箭头数量递增(5 → 6 → 7 → 9 → 12),难度逐步提升;
- 无尽模式:随机生成可通关关卡,清空后自动进入下一关;
- 爱心生命系统:每关 3 颗爱心,失误一次熄灭一颗;
- 提示功能:一键高亮当前可飞出的箭头;
- 三星评价:过关时根据失误与点击次数结算星级;
- 计时与点击次数统计;
- 飞出 / 碰撞弹回动画、按钮悬停放大效果;
- 图片化美术资源与棋盘装饰边框。
四、实现思路
4.1 数据表示
数据表示上我用了最轻的做法,只保留两个概念:箭头和关卡。
- 箭头:用三元组
(row, col, direction)表示,direction为up / down / left / right; - 关卡:用字符网格定义,
↑ ↓ ← →表示箭头,·表示空格,肉眼即可校对,也便于修改和新增关卡。
LEVELS = [
{"name": "第 1 关 · 新手引导",
"grid": ["······", "·→·↑··", "··→···", "·↓·←··", "······", "······"],
"hearts": 3},
...
]
箭头存成三元组之后,路径检测和渲染都只依赖行列坐标,不需要再维护别的对象。字符网格的好处是把“关卡设计”和“游戏逻辑”分开了——改关卡时只动字符,不用碰代码。解析时用一个字典把字符映射成方向常量:
CHAR_TO_DIR = {"↑": UP, "↓": DOWN, "←": LEFT, "→": RIGHT}
4.2 路径检测核心算法
路径检测是整个游戏最关键的一步。我的做法是:从一个箭头出发,沿着它自己的方向一格一格往前走,走到棋盘边界为止;途中第一个被其它箭头占据的格子就是阻挡者,如果一路都没遇到,说明它可以飞出。
def _find_blocker_in(self, arrow, arrows):
r, c, d = arrow
dr, dc = DIRS[d] # 方向对应的行列增量
occupied = {(a[0], a[1]) for a in arrows if (a[0], a[1]) != (r, c)}
rr, cc = r + dr, c + dc
while 0 <= rr < self.rows and 0 <= cc < self.cols:
if (rr, cc) in occupied:
return (rr, cc) # 被挡住
rr += dr
cc += dc
return None # 一路到边界,无阻挡
拆开来看:
r, c, d = arrow:先解包出当前箭头的行列和方向。dr, dc = DIRS[d]:把方向翻译成行列增量,比如up就是(-1, 0),right就是(0, 1)。occupied:用一个集合存下除自己之外所有箭头的格子坐标,这样判断某格是否被占就是 O(1)。虽然每次调用都会重建,但 6×6 的棋盘上这点开销可以忽略。while循环:从箭头的下一格开始,沿方向一格一格地走。if (rr, cc) in occupied:一旦撞到被占据的格子,立刻返回这个坐标作为阻挡者。- 循环自然结束,说明走到棋盘边界了,返回
None,表示可以飞出。
这里之所以选择“逐格走”而不是“扫描整行整列”,主要是两个原因:一是逐格走天然只在一条直线上,不用额外判断方向;二是遇到第一个阻挡就返回,能提前终止。以后如果换成 8×8、10×10 的棋盘,这段逻辑完全不用改。
4.3 关卡可解性验证
为了确保每一关都能真的通关,我写了一个贪心验证函数 is_solvable():反复移除“当前没有阻挡”的箭头,如果最后能全部清空,就说明这一关有解。移除箭头只会让路径变空、不会新增阻挡,所以贪心一定是正确且完备的。无尽模式随机生成的关卡,也是靠这个函数保证可通关的。
def is_solvable(self):
remaining = list(self.arrows)
changed = True
while remaining and changed:
changed = False
for arrow in list(remaining):
if self._find_blocker_in(arrow, remaining) is None:
remaining.remove(arrow) # 这个箭头当前可以飞出
changed = True
return not remaining
拆开来看:
remaining = list(self.arrows):先复制一份箭头列表,避免直接改动游戏里的状态。- 外层
while:只要还有箭头,且上一轮确实移除过箭头,就继续扫。 changed = False:每轮开始先重置“本轮有没有进展”的标记。- 内层
for:遍历remaining的副本,挨个检查它当前有没有阻挡。 if self._find_blocker_in(...) is None:如果没有阻挡,说明它此刻能飞出,就把它从remaining里删掉,并标记changed = True。- 循环结束后,只要
remaining空了,就说明这一关存在合法通关顺序。
贪心在这里之所以成立,是因为“移除箭头”只会让别的箭头路径更空,绝不会让原本能飞的箭头变得不能飞。所以只要存在一个合法顺序,这个贪心一定能找到。generate_random_level 就是不断随机生成布局、用这个函数验证,直到找到可通关的为止。
4.4 状态管理
Game 里和游戏进行有关的字段大致是这些:
| 字段 | 含义 |
|---|---|
arrows |
当前存活的箭头列表 [(r, c, d), ...] |
hearts / max_hearts |
当前 / 最大生命值 |
status |
playing / level_clear / endless_clear / win / game_over |
clicks / mistakes / best_clicks |
用于三星评价的统计数据 |
elapsed |
本关用时(毫秒),由 tick(dt) 累计 |
endless / endless_num |
是否无尽模式、当前无尽关号 |
状态切换用 status 一个字段串起来:
playing ──清空全部箭头──→ level_clear(普通模式)
playing ──清空全部箭头──→ endless_clear(无尽模式)
playing ──爱心耗尽──────→ game_over
playing ──最后一关清空──→ win
main.py 只需要看 status 决定画哪个界面、响应哪些点击,逻辑集中在一处,调试起来也方便。
4.5 渲染与动画
渲染部分基本沿用了 Pygame 的经典做法:
- 箭头渲染:用
arrow_up/down/left/right.png四张图片,按方向选图、按格子大小缩放; - 飞出动画:点击可飞出的箭头后,把它加入
fly_anims,每帧用时间差算出当前偏移,飞出棋盘后自动从列表移除; - 碰撞弹回:被阻挡的箭头记录阻挡者坐标,用分段曲线让它先冲过去再弹回来;
- 提示高亮:点提示按钮后,用
sin脉冲让目标格子的黄色底和边框闪烁,同时箭头上下跳动。
# 飞出动画:根据已过去的时间计算位移
dt = (now - a["t0"]) / 1000.0
dist = dt * self.cell * FLY_SPEED_FACTOR
a["x"] = cx + dc * dist
a["y"] = cy + dr * dist
这里的思路很直接:
dt是这段动画已经过去了多少秒;dist就是时间乘以速度,FLY_SPEED_FACTOR是每秒飞多少格;- 把位移沿方向增量
dc / dr加到原始坐标上,就得到当前应该画在哪; update_anims里再检查它有没有飞出棋盘,飞出去的就从列表里移除。
4.6 计时与三星评价
- 计时:每帧调用
Game.tick(dt),只有status == "playing"时才把dt累加到elapsed; - 点击统计:每次有效点击(点到箭头)
clicks += 1,被阻挡时mistakes += 1; - 最佳点击:
best_clicks = len(arrows),也就是“每个箭头点一次”的理想次数; - 三星规则:
- ★:清空棋盘
- ★★:0 失误
- ★★★:0 失误且
clicks == best_clicks
4.7项目结构
yijian-yijian/
├── main.py
├── game.py
├── levels.py
├── constants.py
├── requirements.txt
├── README.md
├── DEVLOG.md
└── assets/
└── images/
DEVOG是从开始优化到现在的开发日志,更多详细的优化和更新在里面可以看得到
五、开发过程与思路
5.1 起点:先把骨架搭起来
最开始我没急着写界面,而是先把最核心的两个东西定下来:箭头怎么存和关卡怎么表示。
箭头用 (row, col, direction) 三元组,关卡用字符网格(↑↓←→ 表示箭头,· 表示空格)。这么定的原因很实际——字符网格改起来太方便了,想加一关就是复制一段字符串、挪几个箭头,改错了肉眼一眼就能看出来,不用去翻坐标。
定完数据结构,才让 AI 帮我把 Pygame 的主循环、字体加载、界面切换这些模板代码生成出来。这部分 AI 确实快,省了我不少时间,但真正麻烦的还在后头。
5.2 路径检测:AI 给的代码为什么跑不通
路径检测是整个游戏最关键的一步。我一开始跟 AI 提的需求很简单——“检查箭头前进方向上有没有别的箭头”。它很快给了我四个方向分别判断的代码,逻辑看着都对,一跑就报错。
问题出在向上检测的数组越界。AI 写的是从箭头位置往上、用 -1 一步步走,但没处理好边界条件,索引变成负数之后 Python 会从列表末尾取值,结果就是明明没挡住的箭头被判成了被挡住。
这个 bug 让我意识到一件事:AI 生成的代码看起来对,是因为它只处理了“正常情况”,边界全得自己补。我后来把它改成统一的 _find_blocker_in(arrow, arrows),用 while 0 <= rr < rows and 0 <= cc < cols 统一收口,四个方向共用一套逻辑,干净很多。
5.3 关卡设计:AI 给的布局不一定能通关
接下来是关卡。我让 AI 帮我生成 5 关的布局,它给出来的看起来挺合理——箭头分布均匀、方向也有变化。结果自己试玩第 3 关的时候,卡住了:不管怎么点,总有箭头飞不出去。
这就是那个经典问题:AI 能生成“看起来像关卡”的东西,但它不保证关卡有解。
(真的过不去啊看到这个完全傻眼了)

于是我写了一个 is_solvable() 函数,用贪心验证:反复移除“当前没有阻挡”的箭头,如果最后能全清掉,就说明这一关有解。因为移除箭头只会让路径变空、不会新增阻挡,所以贪心一定是正确且完备的。第 3 关重新设计之后,我又跑了一遍验证,确认真的能通关。
这个函数后来还派上了大用场——无尽模式的随机关卡就是靠它来保证可通关的:随机生成布局,验证,不通过就重来,直到找到一个能通关的为止。
5.4 无尽模式:清空之后直接换关,飞出动画被截断了
无尽模式的设计目标是“清完一关自动进下一关”。我一开始的实现很直接:清空全部箭头后,立刻调用 next_level() 生成新关。
结果自己玩的时候发现一个很别扭的细节:上一关最后几个箭头的飞出动画还没播完,新关卡就已经出现了,看起来像是游戏出了 bug。
我改成先让 status 变成 endless_clear,等飞出动画播完(约 0.5 秒)再换关。后来又给无尽模式加了结算界面,显示星星、用时、点击、失误,玩家点击屏幕或者等 2 秒自动进下一关,节奏就顺多了。
很多问题不是逻辑错,而是体验上的细节没考虑到。不自己玩几遍,根本发现不了。
5.5 动画:从“抖动”到“真的撞上去再弹回来”
碰撞反馈一开始我让 AI 写的是“箭头原地抖动 + 变红”。功能是对的,但感觉很假——箭头原地抽搐,不像撞到了东西。
我想要的其实是更物理一点的效果:箭头朝阻挡它的那个箭头冲过去,撞到边缘再弹回原位。
改的时候碰到一个问题:如果只是固定前进一段距离,遇到离得远的阻挡者就撞不到;遇到离得近的又会穿过去。后来我想了个办法——记录阻挡者的坐标,算出它离当前箭头有几格,用这个格数减去 0.5 作为最大位移,再用分段曲线(前 30% 冲过去,后 70% 弹回来)控制节奏。这样就真的像撞上去了。
还有个小坑:一次游戏运行中,如果 bounce_anims 里残留了旧格式的数据(没有 blocker 字段),就会报 KeyError。加了个兼容判断之后才彻底解决。
5.6 提示功能:做出来了,但太不明显
提示功能的需求很简单:点一下,高亮一个当前可以飞出的箭头。
第一版实现只画了个黄色边框,自己试的时候发现要盯着屏幕找半天,根本看不出来高亮在哪,虽然忘记截图了但真的很难找到,同时实现了射箭和猜猜我在哪这两个功能何尝不是一种成功呢()
后来做了三件事:
- 给目标格子加了黄色半透明底,还加了脉冲效果,让它一闪一闪的;
- 边框也加了脉冲,粗细随时间变化;
- 让被提示的箭头上下跳动。
三个效果叠加之后,提示就非常显眼了。
5.7 图片化:从代码绘图到素材
游戏逻辑稳定之后,我开始把界面从代码绘制换成图片素材。原因是纯色按钮和代码画的箭头,看起来太丑了...就算在早八课堂上看到这个界面也不会有人想玩吧!!!
换图片的过程比想象中麻烦,主要问题有两个:
一是图片比例对不上。 比如菜单按钮的图片是长方形,但我一开始按正方形 Rect 去缩放,结果图片被压扁了。后来改成按原图比例缩放、居中显示,才正常。
二是棋盘图片和代码算的格子对不齐。 棋盘图片里带有木框、叶子装饰,格子区域比整张图小,直接缩放就会错位。把边框去掉之后还是会错位like this

后面把透明底图片的边框删掉就合适了()在这个基础上又改了箭头的ui,在确保能运行之后把原本的边框加上了,就变成了大家现在看到的样子~o( =∩ω∩= )m
5.8 HUD:从纯文字到木质底板
HUD 也迭代了好几版。
最早是纯文字,生命、剩余箭头、用时直接画在左上角,和棋盘叠在一起,看起来很乱。
第二版把信息挪到左上角,加了半透明白底,但和右侧的问号按钮、菜单按钮经常打架。
第三版做了木质底板 + 图标 + 文字的布局——底板图片上自带三条分隔带,生命、剩余箭头、用时各占一行,每一行前面配一个小图标(金色生命图标、箭头图标、时钟图标)。因为底板图片本身就带有装饰,视觉上一下就统一了。
调整过程中,我反复微调了三行内容的坐标,让它们正好落进底板的三条分隔带里。对于强迫症来说差几个像素其实真的很明显。!
5.9 按钮悬停:让界面“活”起来
所有按钮都加上了鼠标悬停放大的效果——悬停时图片等比放大 8%,或者文字按钮放大 10×8 像素、颜色变亮。
加上之后整个界面的手感明显不一样了。以前点按钮像在戳一块不会动的板子,现在有反馈了会好很多哦。
六、AIGC 使用过程
本次开发借助AI辅助,以下记录几次有代表性的协作过程:
| 子任务 | 借助何种 AIGC 技术 | AI 实现或提供了什么 | 效果如何 | 人工修改 |
|---|---|---|---|---|
| 核心路径检测 | Claude Code | 生成四个方向的路径检测代码 find_blocker |
逻辑正确 | 核对边界条件,补充可解性验证函数 |
| 关卡生成与可解性 | Claude Code | 用"逆向构造"生成 8×8 关卡,并提供贪心可解性验证 | 生成的关卡均可通关 | 手工试玩确认难度合理,最终改为 6×6 |
| 修复字体崩溃 | Claude Code | 定位到 pygame SysFont 在 Windows 扫描注册表时崩溃,改用字体文件路径加载 |
修复后中文正常显示 | 确认字体文件存在并验证渲染 |
| 修复失败界面闪退 | Claude Code | 定位失败画面引用了已删除常量导致崩溃 | 修复后失败界面正常显示 | 无 |
| 棋盘边框对齐 | Claude Code | 扫描边框图透明内框坐标,计算缩放使其贴合棋盘外框 | 内框精确贴合 | 调整内缩量参数 FRAME_INSET |
| 生成背景及UI | Chat GPT | 生成动画风图片 | 符合效果 | 调整尺寸 |
| 代码细节微调 | Deepseek | 了解用户需求,对代码细节做调整 | 符合预期 | 提出要求 |
这些记录均来自真实开发过程。AI 在代码编写、Bug 定位、关卡生成、素材对齐等方面提供了很大帮助,但最终的运行验证、试玩和细节调整仍需要人工判断与修改。
七、测试结果
对程序进行了测试,结果如下:
| 编号 | 测试内容 | 预期结果 | 实际结果 | 是否通过 |
|---|---|---|---|---|
| T01 | 点击前方无阻挡的箭头 | 箭头飞出棋盘并消失 | 箭头飞出并消失 | ✅ |
| T02 | 点击前方有阻挡的箭头 | 箭头不消失,爱心 -1 | 爱心 3→2,播放碰撞动画 | ✅ |
| T03 | 点击边缘且朝向棋盘外的箭头 | 正常消失,不发生越界 | 正常消失,无越界 | ✅ |
| T04 | 消除本关全部箭头 | 显示通关并进入下一关 | 显示过关,点击进入下一关 | ✅ |
| T05 | 爱心耗尽 | 显示失败并允许重新开始 | 显示失败界面,可再玩一次 | ✅ |
| T06 | 游戏进行中重新开始 | 箭头布局与爱心恢复初始 | 布局与爱心恢复 | ✅ |
八、PSP 表格
| 任务 | 预估耗时(小时) | 实际耗时(小时) | 差异(小时) |
|---|---|---|---|
| 需求分析与游戏设计 | 0.5 | 0.5 | 0 |
| Python 与图形库学习 | 0.5 | 1 | +0.5 |
| 游戏界面实现 | 2 | 3.5 | +1.5 |
| 路径与碰撞逻辑实现 | 1.5 | 2 | +0.5 |
| 关卡设计 | 1 | 3 | +2 |
| AIGC 辅助开发 | 1 | 6 | +5 |
| 测试与修改 | 1.5 | 9 | +7.5 |
| README 与博客撰写 | 3 | 1 | -2 |
| 合计 | 11 | 26 | +15 |
九、心得体会
这次作业做下来,最大的感受不是“AI 能写多少代码”,而是“我能判断多少代码”。
一开始我以为用AIGC帮忙会很快,实际做下来发现,AI确实能帮我省掉大量重复劳动,比如把Pygame的初始化、字体加载、按钮绘制这些模板代码直接生成出来,但真正花时间的,是我要搞清楚它给的东西哪里不对、为什么不对。像路径检测那段,AI写出来结构是对的,但向上检测时越界了;碰撞弹回,AI一开始给我的是原地抖动,我想要的是真的撞到阻挡者再弹回来。这些都不是AI“写错了”那么简单,而是它对“我要的效果”理解得不够具体,需要我自己把需求拆清楚、把边界想明白,它才能给出能用的东西。
另一个很深的体会是,游戏做出来和游戏好玩,中间隔了很远。基础版本能通关之后,我加了计时、三星评价、提示、无尽模式、结算界面。每加一样,都要重新想玩家看到的是什么、点击之后发生什么、异常情况怎么办。比如无尽模式清空后如果立刻换关,上一关的飞出动画就会被截断,看起来像是游戏出了bug——这种细节,不自己玩几遍根本不会发现,AI也不可能替我想。反倒是玩过之后,才知道哪些地方该加延迟、哪些地方该给反馈。
还有一点是关于想清楚再动手。像6×6棋盘、图片素材对齐、HUD布局这些,前期如果数据结构没定好,后期每改一次就是连锁反应。我中途好几次推倒重来,本质上都是因为一开始没把箭头怎么存、关卡怎么表示、界面状态怎么切换这三件事想透。等把它们理顺之后,后面加功能就顺很多,也不容易被AI带偏。
最后想说,AI在这次开发里更像一个执行力很强但需要被反复校准的助手。它写代码很快,但它不会替我做判断:判断一个关卡是不是真的能通关、判断一个动画是不是太突兀、判断一个按钮点了有没有反馈。这些判断,只能自己一点点试出来。
写在结尾
啊其实调教ai的过程真的很恼怒....很多时候只要不把需求说到最清楚ai就听不明白。但是做游戏真的很好玩hhhh,从一个丑不拉几的框架(如下)到一个复古的小游戏的过程还是蛮有趣的!一点点增加功能的时候也很有成就感呢(▽)总之还在github里传了一个手机也能玩的网页版,内容还在丰富中,欢迎试玩!!!
浙公网安备 33010602011771号