独立游戏开发:背包系统实现与AI编程工具实战心得

独立游戏开发:背包系统实现与AI编程工具实战心得 一个人做一款游戏的系列记录这是第六篇。项目是一款2D像素风的生存探索游戏从立项到现在差不多两个月了。前几篇分别写了立项、场景搭建、角色移动、碰撞检测和资源管理今天这篇不打算按时间线把每天的琐事全列出来就集中聊两个核心话题这周做背包系统的完整经过以及我在实际开发中对AI编程工具的使用心得。顺便说一句这周我真正体会到了什么叫“提示词写得好下班下得早”当然也踩了几个AI给我挖的坑后面会一个个说。背包系统这个东西听起来很普通好像随便写写就能用。但真正上手之后你会发现它背后牵扯的是数据结构的稳定性、UI交互的响应速度、物品叠加的边界条件还有和存档系统的对接。一个人做游戏最怕的不是功能多而是“功能能用但改不动”。这周我为了不让背包系统变成那种“能用但改不动”的屎山代码花了不少时间在模块拆分上AI在里面帮了大忙但也制造了几个让我调了一整晚的bug。1. 先说这周的进度为什么卡在背包系统1.1 上周计划的完成情况上周我的计划是做地形生成和矿石分布因为游戏的地图如果要支持不同的矿脉、植物、生态区域这部分就必须提前规划好。结果真正动手之后发现光有地形和矿石没有背包系统整个采集资源的循环根本走不通。你可以想象一下玩家在地图上挖了三块铁矿然后呢没地方放也没有任何UI提示这个动作就没有正反馈。游戏设计里有个很基础的原则叫“行动-反馈闭环”你做了任何操作系统都要有清晰的反应。采完矿之后最自然的反馈就是“背包里多了一块铁矿石”这时候玩家才知道自己干了什么、接下来能干什么。所以我临时调整了优先级把背包系统提到地形生成之前来做。一个人开发的好处就在这里没有团队协作的行政成本没有上游依赖方的排期约束想调整就调整。坏处也有就是一旦判断失误浪费的所有时间都得自己扛。1.2 背包系统看起来简单做起来坑不少背包系统在很多玩家眼里就是一个网格界面有格子、有物品图标、有数字点击物品能使用拖拽能换位置。但这个“简单”背后至少拆成三块数据层物品怎么定义、怎么存储、怎么叠加、怎么拆分、怎么使用。逻辑层背包容量判断、物品添加移除、交换合并、排序刷新。表现层网格绘制、图标渲染、选中高亮、左键右键交互、拖拽动画、Tooltip提示。我一开始给AI下的需求是“帮我写一个背包系统”它给我整了个所有东西都堆在同一个类里的版本。数据结构、UI渲染、鼠标事件处理全混在一起大概不到五百行代码。说实话跑起来是能跑但我想加一个“按住Shift拆分一组物品”的小功能时发现无从下手因为UI层直接操作了内部数据。这就引出这周最核心的教训用AI编程不能让它自由发挥你得在提示词里把“怎么组织代码”这件事明确说清楚。AI不是不行而是它默认按最省事的路线走如果你不给边界它就怎么方便怎么来。2. 工具选型这几周我到底在用哪些AI编程工具2.1 我的主力工具Cursor这阵子一直有人问我“编程AI哪个好用”我的答案比较直白目前我给个人项目的推荐是Cursor。不是说别的工具不行而是在“项目级上下文理解”这件事上Cursor目前做得最深。什么叫项目级上下文理解就是AI不只看你当前打开的这一个文件而是能理解整个项目的结构。比如我在写背包系统的数据层AI会知道之前我在player.py里定义过角色属性在world.py里定义了地图块对象它生成的代码会尽量和这些已有模块保持风格一致。GitHub Copilot我也在用但它的定位更像是一个“超强补全器”。当你已经知道代码大概要怎么写只是想省去打字时间的时候Copilot的Tab补全非常舒服。但如果你脑子里只有一个模糊的需求比如说“我想要一个可以拖拽交换物品位置的网格UI”Copilot能给到的帮助就弱很多因为它更擅长补全代码片段而不是和你进行多轮设计讨论。还有一个之前提过的国内工具通义灵码。它对中文语义的理解确实不错免费版的功能也够用一个中小型项目。我身边有些朋友用它来写数据分析脚本和自动化工具反馈还行。不过跑到游戏这个场景它的项目级理解能力相比Cursor还是有一些差距主要体现在跨文件引用时准确性不够高。2.2 辅助组合Copilot 自建提示词片段说下我现在的实际用法日常主力环境是Cursor项目里同时开着Copilot专门处理那些“纯体力活”。具体怎么分工举个例子。游戏里有很多类似的物品定义比如矿石、木材、食物、工具它们的底层数据结构几乎一样只是参数不同。这种重复度极高的代码用Copilot写起来飞快我只需要起一个头定义好第一个类后面它就能照着生成。另外我给自己建了一个“提示词片段库”把一些高频使用的需求模板保存下来。比如“新增一种物品类型”这个场景我的标准提示词长这样在现有物品系统中新增一个物品类型。 需要修改的文件items.py添加ItemData、assets/icons/放置图标。 物品需求名称、描述、最大堆叠数、是否可用于合成、右键使用回调。 要求严格遵循现有ItemData的字段命名风格不改动其他模块。以后遇到类似任务直接复制这段再填充具体物品的参数就行。这个小习惯帮我省了很多时间也让我每次和AI协作时给出的上下文足够充分减少它瞎猜的概率。2.3 怎么判断一个AI编程工具适不适合你工具好不好用不是看宣传语而是看三个指标评估维度CursorGitHub Copilot通义灵码项目级上下文理解强弱中多轮对话设计能力强一般中代码补全跟手程度中强中中文交互体验中一般强价格敏感度高中低如果你和我一样是个人开发需求是“从零开始构建一个项目”我会首选Cursor。如果你在公司团队里已经有完善的代码规范和脚手架主要想提高编码速度Copilot是性价比不错的选择。如果预算有限但想体验AI辅助开发通义灵码的免费额度其实够用很久。3. 提示词工程一句话让AI写出能跑的代码3.1 差的提示词 vs 好的提示词很多人用完AI编程工具之后觉得“也就那样”大概率是提示词没给到位。我见过最典型的失败案例是下面这种帮我写个背包系统能装东西就行这种话给到任何一个AI它基本只能给你一个“看起来合理但完全没法用”的版本。为什么因为背包系统的约束条件太多了你要不要网格物品能不能叠加上限是多少有没有快捷键UI风格是什么数据存哪里这些信息全缺AI只能自己猜而它猜的方向往往不适合你的项目。换一种说法我在用Python和Pygame开发一个2D像素生存游戏。现在的项目结构是game.py主循环、player.py玩家对象、world.py地图资源。我需要实现一个背包系统Tab键开关背包网格布局10列4行物品支持叠加和右键使用鼠标可以拖拽交换位置。数据层和UI层分离不使用第三方UI库。请分三次输出先定义物品数据结构再写背包容器逻辑最后写网格UI和交互。这两种说法的区别不是字数而是信息密度。好的提示词至少包含四个要素项目背景告诉AI你用什么语言、什么框架、什么项目结构。需求清单用编号列出你要的功能。边界约束不能用什么库、必须怎么分层、代码风格要求。输出方式希望AI分几步输出每步给什么内容。3.2 三步结构法角色-背景-约束我习惯把提示词拆成三段来写。第一步给角色让它明确自己是“熟悉Python和Pygame的资深独立游戏开发者”。第二步给背景把所有和本次需求相关的已有代码结构、文件内容放进去。第三步给约束明确告诉AI“不要做什么”。举这周实际用过的一个例子。我需要让AI帮我写物品数据类的设计提示词是这样的你是一名熟悉Python和Pygame的独立游戏开发者帮我设计一个物品数据类。 项目背景 - 2D像素风生存游戏目前使用Pygame渲染 - 主循环文件为game.py已定义好统一的资源加载方式 - 物品需要支持两种类型可堆叠如矿石和不可堆叠如工具 - 后续要对接存档系统所以数据结构必须能序列化为JSON 约束条件 - 不要引入任何第三方依赖库 - 使用类型注解 - 代码添加中文注释 - 先输出设计方案再输出代码不要一次性把完整代码全贴出来这段提示词的关键在于“先输出设计方案再输出代码”。很多AI脚本有个毛病那就是一上来就把所有代码全吐出来一坨下来你看得头大代码质量也没法保证。先看设计思路如果不对就及时叫停能省掉很多返工时间。3.3 避免AI“自由发挥”的四个约束开关据我观察大部分人对AI编程失望问题出在AI“自由发挥”得太厉害。你让它写个背包它顺手给你把存档系统也写了还自作主张改了两个原有模块的接口。这种越权行为可以用四个约束开关来禁止文件范围开关明确说本次只创建哪个文件或者只修改哪个函数。依赖范围开关明确不允许引入新的第三方库。代码风格开关明确要求类型注解、中文注释、命名风格等。输出顺序开关要求先方案后代码分步输出而不是一次性倾泻。我用下来最有效的是“文件范围开关”。在提示词里加一句“不修改除inventory.py以外的任何文件”AI就会收敛很多不会到处乱动逻辑。4. 核心实现物品栏系统的关键代码解析4.1 数据结构设计物品不是你想的那么简单先说物品定义。一个物品至少要有编号、名称、图标、是否可堆叠、最大堆叠数、当前数量这些字段还要预留一个“使用回调”的口子。我这版用Python的dataclass定义简洁直观后期转JSON也方便# items.py from dataclasses import dataclass from typing import Callable, Optional import pygame dataclass class Item: item_id: str name: str icon_path: str stackable: bool True max_stack: int 99 count: int 1 use_callback: Optional[Callable] None def __post_init__(self): self.icon pygame.image.load(self.icon_path).convert_alpha() def to_dict(self) - dict: return { item_id: self.item_id, name: self.name, icon_path: self.icon_path, stackable: self.stackable, max_stack: self.max_stack, count: self.count, } classmethod def from_dict(cls, data: dict) - Item: return cls( item_iddata[item_id], namedata[name], icon_pathdata[icon_path], stackabledata.get(stackable, True), max_stackdata.get(max_stack, 99), countdata.get(count, 1), )这里有个小细节icon加载放在__post_init__里方便使用时直接拿到Surface对象。但要注意如果游戏后期物品数量多了这种方式会一次性加载大量图片内存压力会比较大。我目前的处理是偷懒先全部加载等性能问题了再改成懒加载。数据存储的核心是背包容器。我用了最简单的一维列表配合行数、列数来映射网格位置# inventory.py from typing import List, Optional import pygame from items import Item class Inventory: def __init__(self, rows: int 4, cols: int 10): self.rows rows self.cols cols self.slots: List[Optional[Item]] [None] * (rows * cols) def index(self, row: int, col: int) - int: return row * self.cols col def slot_at(self, row: int, col: int) - Optional[Item]: return self.slots[self.index(row, col)] def add_item(self, item: Item) - bool: # 先尝试叠加到相同物品上 if item.stackable: for i, slot in enumerate(self.slots): if (slot is not None and slot.item_id item.item_id and slot.count item.count slot.max_stack): slot.count item.count return True # 找不到能叠加的格子就放到空格 for i, slot in enumerate(self.slots): if slot is None: self.slots[i] item return True return False def remove_at(self, index: int, count: int 1) - Optional[Item]: slot self.slots[index] if slot is None: return None if slot.count count: self.slots[index] None return slot slot.count - count return Item( item_idslot.item_id, nameslot.name, icon_pathslot.icon_path, stackableslot.stackable, max_stackslot.max_stack, countcount, ) def swap_slots(self, a: int, b: int) - None: self.slots[a], self.slots[b] self.slots[b], self.slots[a]add_item的循环逻辑我加了“尽量叠加到已有同ID物品且不超过上限”的判断。这里如果没写count item.count slot.max_stack这个边界条件就会出现一种情况一个堆叠上限99的物品已经有98个再放10个进来直接被覆盖到99个剩下的莫名其妙消失了。这个bug第一次是AI写出来的我当时还没注意测试的时候发现物品凭空不见排查了一圈才找到原因。4.2 渲染与交互让物品栏“好用”的细节数据层搞定之后接着就是UI。网格绘制本身不难真正麻烦的是坐标转换鼠标位置要准确地映射到第几个格子拖拽的时候又要从哪个格子拿出来放到哪个格子。# inventory_ui.py import pygame from inventory import Inventory from items import Item class InventoryUI: CELL_SIZE 64 PADDING 8 def __init__(self, inventory: Inventory): self.inventory inventory self.visible False self.selected_slot -1 self.dragging_slot -1 def panel_rect(self, screen: pygame.Surface) - pygame.Rect: width self.inventory.cols * (self.CELL_SIZE self.PADDING) self.PADDING height self.inventory.rows * (self.CELL_SIZE self.PADDING) self.PADDING rect pygame.Rect(0, 0, width, height) rect.center (screen.get_width() // 2, screen.get_height() // 2) return rect def slot_at_pos(self, pos: tuple, panel: pygame.Rect) - int: x, y pos if not panel.collidepoint(x, y): return -1 col (x - panel.x - self.PADDING) // (self.CELL_SIZE self.PADDING) row (y - panel.y - self.PADDING) // (self.CELL_SIZE self.PADDING) if row 0 or row self.inventory.rows or col 0 or col self.inventory.cols: return -1 return self.inventory.index(row, col) def handle_event(self, event: pygame.event.Event, panel: pygame.Rect): if event.type pygame.MOUSEBUTTONDOWN and event.button 1: idx self.slot_at_pos(event.pos, panel) if idx ! -1: self.dragging_slot idx self.selected_slot idx elif event.type pygame.MOUSEBUTTONUP and event.button 1: if self.dragging_slot ! -1: target self.slot_at_pos(event.pos, panel) if target ! -1 and target ! self.dragging_slot: self.inventory.swap_slots(self.dragging_slot, target) self.dragging_slot -1 def draw(self, screen: pygame.Surface): if not self.visible: return panel self.panel_rect(screen) pygame.draw.rect(screen, (35, 35, 35), panel, border_radius6) for i, slot in enumerate(self.inventory.slots): row i // self.inventory.cols col i % self.inventory.cols cell pygame.Rect( panel.x self.PADDING col * (self.CELL_SIZE self.PADDING), panel.y self.PADDING row * (self.CELL_SIZE self.PADDING), self.CELL_SIZE, self.CELL_SIZE, ) if i self.selected_slot: pygame.draw.rect(screen, (220, 180, 60), cell, width2) else: pygame.draw.rect(screen, (60, 60, 60), cell, border_radius4) if slot is not None: screen.blit(slot.icon, cell.topleft) if slot.stackable and slot.count 1: font pygame.font.Font(None, 28) text font.render(str(slot.count), True, (255, 255, 255)) text_rect text.get_rect() text_rect.bottomright (cell.right - 4, cell.bottom - 2) screen.blit(text, text_rect)拖拽交换这里我现在是简化为“松手后直接交换”没有做跟随鼠标的半透明拖拽浮层。做拖拽浮层需要额外的临时Surface和draw调用属于锦上添花的体验优化。等基础对了我再回头补。4.3 数据与UI解耦的教训AI最初给我的版本里Inventory直接就带draw方法Item类里放了一堆Surface素材。我当时看着还挺顺眼但真到要加新功能的时候才发现问题。比如我想加“从背包拖出物品丢到世界地图上”逻辑上应该是先在Inventory数据层移除这个物品然后在地图对应位置生成一个可拾取实体。如果数据层和UI耦合在一起这个操作就得直接操作UI对象明显是不对劲的。所以我把类拆成了三层Item负责物品自身属性Inventory负责存储逻辑InventoryUI负责渲染和交互。它们的依赖关系是单向的UI依赖InventoryInventory依赖Item反过来一律不允许。这个约束我在给AI的提示词里明确写了之后它生成的新代码就规矩很多。数据与UI分离不是一个可选项而是项目后期扩展的硬前提。给AI提示词时直接写清“UI类不得直接访问物品内部数据必须通过Inventory的方法操作”比事后重构省太多力气。5. 这周踩过的坑AI代码调试实录5.1 问题一所有物品共享一个数量这是我本周遇到的第一个大坑。现象描述起来很简单背包里有三格木材每格显示的数量都是5但实际我只采集了两份木材。更诡异的是改动其中一格的数量另外两格也一起变了。定位到问题也很简单因为所有格子里的Item对象其实是同一个引用。我往背包里放物品时写了类似这样的代码item_a Item(item_idwood, name木材, icon_pathwood.png, count5) for i in range(3): inventory.slots[i] item_a三个格子都指向了同一个对象改其中一个当然全都变。这个bug其实是AI帮我写的测试代码里的问题但它的正主是Inventory.add_item在叠加逻辑里也有类似的引用陷阱。当我往已有物品上叠加数量时直接修改了原对象的count字段如果其他地方还持有这个Item的引用数据就会乱。解决办法有两个一是使用Python的copy模块创建独立实例二是在把Item塞进格子时强制new一个Item对象。我的选择是后者牺牲一点性能换来数据独立性。def _clone_item(self, item: Item) - Item: return Item( item_iditem.item_id, nameitem.name, icon_pathitem.icon_path, stackableitem.stackable, max_stackitem.max_stack, countitem.count, )这个教训让我意识到一件事AI写的代码能跑通不代表逻辑正确尤其是引用传递这种Python语言层面的隐性问题AI不容易主动规避你得自己在审查代码时盯住对象复用点。5.2 问题二AI自己发明了一个不存在的API这还是我这周第一次尝试让AI直接生成“存档读取”功能时遇到的。我让AI写一个读取存档并恢复背包状态的函数它给了一个对我来说完全陌生的调用方式data json.load(open(save.json, r, encodingutf-8)) # AI这里写了一个 pygame.image.load_from_bytes 的方法 icon pygame.image.load_from_bytes(data[icon_bytes])问题是Pygame原本根本不存在load_from_bytes这个方法。它不是需要import pygame.image之后才能用吗不对它有load从文件加载但并没有从bytes直接加载的API。我当时查了半天文档才确认是AI自己编造出来的API。这类问题在AI编程里非常常见。模型在训练数据里见过类似的函数名就自行组合了一个看着合理的名字。这种虚构API比报错还难排查因为程序在运行前不会报错等到真正执行到那一步才会直接崩溃而且报错信息还不一定指向原始问题。我的解决办法遇到AI给出的陌生API先不急着问它“这个API存在吗”而是直接丢给它“请给出该API的官方文档链接和最简单的调用示例”。让它自己验证自己比你自己查文档效率高。# 在 Cursor 对话中追加 请确认 pygame.image.load_from_bytes 是否存在如果不存在 给出 pygame 中从内存加载图片的正确方式并附上官方文档说明。AI拿到这个问题后会重新搜索自己的知识库大概率能给出正确的答案。5.3 问题三上下文太多导致前后风格不一致Cursor的项目级上下文能力再强也不能无限承载上下文。到第5天的时候我在和AI的对话里已经聊过地形生成、矿石分布、背包系统、物品叠加、存档结构等一堆话题。这时候让它生成新的UI代码时我发现生成的代码命名风格突然变得不一样了之前都用inventory这个变量名它新代码里改成了bag之前中文注释习惯用“获取”它开始用“得到”。这种不一致很让人抓狂尤其在个人项目里代码风格就是你的“团队规范”。一开始我没有意识到这个问题直到横向对比同一批代码时才发现AI已经开始“精神分裂”。解决方案其实很成熟把项目级约定写进一个固定的说明文件里比如AGENTS.md内容包含项目结构、命名规范、代码风格偏好、常用的类和方法签名。每次新开会话时我会在提示词里附上一句“请先读取项目根目录下的AGENTS.md了解项目约定后再给出代码”。这样AI即使换了会话也能快速摸清你的偏好。5.4 常见问题速查表这周的调试经验整理成表格方便后面查阅现象可能原因排查方向多个格子物品数量同步变化多个格子引用同一个Item对象检查放入物品时是否用了深拷贝物品摆放后图标显示空白图片路径错误或图片未加载成功检查icon_path、确认图片存在拖拽交换后物品消失交换索引计算错误检查行列到一维索引的转换叠加后数量超出上限未判断max_stack边界检查add_item的叠加条件AI生成的代码突然风格大变上下文过长导致状态丢失新开会话并附上项目约定文件AI编造不存在的API模型训练数据组合出的幻觉让它自己提供文档和示例6. 我当前的体验和一些想法如果你问我AI编程到底让一个人做游戏的效率提升了多少我不会给你一个“翻倍”或者“五倍”这种模板答案。我更愿意说它把“从完全不会到快速写出能跑的版本”这个过程压缩了很多但并没有把“把系统设计好”这件事变容易。这周的背包系统如果是纯手写我可能要多花大半天去翻文档、试错、调整参数。AI把这部分时间压缩到了几十分钟。但与此同时我在检查它生成的代码、修复引用传递导致的数据Bug上也投入了超出我预期的时间。这不是简单的“AI行不行”的问题而是你作为项目负责人到底有没有能力去识别它生成的东西哪里不对。一个人做游戏最大的挑战从来不是代码而是“知道自己下一步该做什么”。AI编程帮我减少了很多“怎么做”的时间但没有帮我回答“做什么”。这个决定权永远在自己手里。下周我应该会开始做NPC对话系统顺便把地图上的矿石分布和采集掉落一起串成完整流程。如果你也在用AI辅助做个人项目欢迎留言交流一下你是怎么处理AI给出的代码审查问题的。至少现在我还没放弃游戏还会继续做下去。下周见。