软件工程课程第二次个人作业:用AIGC辅助生成游戏

一、作业信息

项目 内容
这个作业属于哪个课程 2026-01 软件工程与软件工程实践
这个作业要求在哪里 第二次个人作业:利用 AIGC 完成"一箭又一箭"小游戏
这个作业的目标 使用 Python 和 AIGC 完成"一箭又一箭"小游戏
学号 102401208
GitHub 仓库 请关注我吧(>ω<)

二、项目展示

- 开始界面 / 选择关卡界面

开始界面我让gpt帮我生成了一个比较卡通的背景...结果做出来很像4399小游戏,真是时代的眼泪了,,()

9月22日

ps我的gif好像压缩太过了有点太糊了,反耳呢又增加了一丝风味,总之意思到了就可以,想体验满血版的话可以前往github自己试一试❥(^_-)

- 游戏过程(含碰撞动画)

在主菜单点击开始游戏会自动进入第一关,第一关会弹出新手教程,在空白处点掉之后会回收到右上角的问号里

第一关提示

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

游戏过程

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

失误

- 通关结算(三星评价)

通关后显示获得星级、用时、点击次数和失误数

通关

- 失败界面

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

屏幕截图 2026-09-22 204817

选择关卡

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

image

设置界面

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

设置界面

提示功能

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

9月22日(1)

其实还在开发一些可以换皮肤、添加音乐的功能和更多玩法,后面会添加到仓库中的,敬请期待

三、项目介绍

3.1 游戏规则

「一箭又一箭」是一款点击式箭头解谜小游戏,参考微信小游戏《一箭又一箭》的核心玩法。棋盘中有若干带方向的箭头(上、下、左、右),玩家需要观察箭头的方向与相互阻挡关系,按正确顺序点击箭头:

  • 点一个箭头,程序会看它前面有没有被别的箭头堵住;
  • 前面一路空到棋盘边缘,它就“嗖”地飞出去,从棋盘上消失;
  • 前面有别的箭头堵着,它飞不出去,只能先撞一下再弹回原位,顺便熄灭你一颗爱心;
  • 把本关箭头全清掉就过关;爱心用完,游戏就跟你say goodbye了。

3.2 界面设计

游戏包含以下界面:

  • 开始界面:标题、开始游戏、选择关卡、设置;
  • 选择关卡界面:5 个关卡(未解锁为灰色)+ 无尽模式入口;
  • 设置界面:音量调节滑条、音效开关;
  • 游戏界面:棋盘、爱心、剩余箭头、用时、菜单按钮、提示按钮;
  • 结果界面:通关 / 失败提示,可再玩一次、选择关卡、返回主菜单。

3.3 主要功能

  • 5 个 6×6 关卡,箭头数量递增(5 → 6 → 7 → 9 → 12),难度逐步提升;
  • 无尽模式:随机生成可通关关卡,清空后自动进入下一关;
  • 爱心生命系统:每关 3 颗爱心,失误一次熄灭一颗;
  • 提示功能:一键高亮当前可飞出的箭头;
  • 三星评价:过关时根据失误与点击次数结算星级;
  • 计时与点击次数统计;
  • 飞出 / 碰撞弹回动画、按钮悬停放大效果;
  • 图片化美术资源与棋盘装饰边框。

四、实现思路

4.1 数据表示

数据表示上我用了最轻的做法,只保留两个概念:箭头关卡

  • 箭头:用三元组 (row, col, direction) 表示,directionup / 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               # 一路到边界,无阻挡

拆开来看:

  1. r, c, d = arrow:先解包出当前箭头的行列和方向。
  2. dr, dc = DIRS[d]:把方向翻译成行列增量,比如 up 就是 (-1, 0)right 就是 (0, 1)
  3. occupied:用一个集合存下除自己之外所有箭头的格子坐标,这样判断某格是否被占就是 O(1)。虽然每次调用都会重建,但 6×6 的棋盘上这点开销可以忽略。
  4. while 循环:从箭头的下一格开始,沿方向一格一格地走。
  5. if (rr, cc) in occupied:一旦撞到被占据的格子,立刻返回这个坐标作为阻挡者。
  6. 循环自然结束,说明走到棋盘边界了,返回 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

拆开来看:

  1. remaining = list(self.arrows):先复制一份箭头列表,避免直接改动游戏里的状态。
  2. 外层 while:只要还有箭头,且上一轮确实移除过箭头,就继续扫。
  3. changed = False:每轮开始先重置“本轮有没有进展”的标记。
  4. 内层 for:遍历 remaining 的副本,挨个检查它当前有没有阻挡。
  5. if self._find_blocker_in(...) is None:如果没有阻挡,说明它此刻能飞出,就把它从 remaining 里删掉,并标记 changed = True
  6. 循环结束后,只要 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

这里的思路很直接:

  1. dt 是这段动画已经过去了多少秒;
  2. dist 就是时间乘以速度,FLY_SPEED_FACTOR 是每秒飞多少格;
  3. 把位移沿方向增量 dc / dr 加到原始坐标上,就得到当前应该画在哪;
  4. 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 能生成“看起来像关卡”的东西,但它不保证关卡有解
(真的过不去啊看到这个完全傻眼了)

屏幕截图 2026-09-22 013000

于是我写了一个 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 提示功能:做出来了,但太不明显

提示功能的需求很简单:点一下,高亮一个当前可以飞出的箭头。

第一版实现只画了个黄色边框,自己试的时候发现要盯着屏幕找半天,根本看不出来高亮在哪,虽然忘记截图了但真的很难找到,同时实现了射箭和猜猜我在哪这两个功能何尝不是一种成功呢()

后来做了三件事:

  1. 给目标格子加了黄色半透明底,还加了脉冲效果,让它一闪一闪的;
  2. 边框也加了脉冲,粗细随时间变化;
  3. 让被提示的箭头上下跳动。

三个效果叠加之后,提示就非常显眼了。

5.7 图片化:从代码绘图到素材

游戏逻辑稳定之后,我开始把界面从代码绘制换成图片素材。原因是纯色按钮和代码画的箭头,看起来太丑了...就算在早八课堂上看到这个界面也不会有人想玩吧!!!

换图片的过程比想象中麻烦,主要问题有两个:

一是图片比例对不上。 比如菜单按钮的图片是长方形,但我一开始按正方形 Rect 去缩放,结果图片被压扁了。后来改成按原图比例缩放、居中显示,才正常。

二是棋盘图片和代码算的格子对不齐。 棋盘图片里带有木框、叶子装饰,格子区域比整张图小,直接缩放就会错位。把边框去掉之后还是会错位like this

屏幕截图 2026-09-22 160251

后面把透明底图片的边框删掉就合适了()在这个基础上又改了箭头的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里传了一个手机也能玩的网页版,内容还在丰富中,欢迎试玩!!!

posted on 2026-09-22 21:21  Diox4n3  阅读(10)  评论(0)    收藏  举报