山东大学软件工程2023级创新项目实训 | 四、StoryEcho项目——背包功能
时间: 2026年4月中旬 - 4月下旬
在项目框架搭建完成并成功验证大模型API接入后,我们FateWeaver团队进入了功能并行开发阶段。经过小组讨论,我们决定每人独立负责一个完整的功能模块(前后端+数据库),以验证团队的技术架构是否能够支撑真正的全栈开发。我被分配到的任务是实现游戏内的背包系统,包括物品的获取、使用、丢弃、与角色属性的联动,以及数据的持久化存储。
目录
一、分工背景与初始状态分析
在开始背包功能的开发之前,我先对项目当前状态进行了评估。初步框架虽然能够运行,但存在几个关键问题:
1. 后端数据存储问题
初始框架将游戏会话数据存储在Python内存字典中:
active_sessions = {} # 存在内存中
这意味着一旦后端重启,所有数据都会丢失。对于背包系统来说,物品的获取和使用必须持久化,否则每次刷新页面背包就空了。
2. 前端数据来源不统一
侧边栏显示的背包内容来自 gameState.inventory(一个字符串数组),而弹窗需要的是包含名称、图标、数量、效果等详细信息的结构化对象。两者没有对应关系。
3. 角色出身物品未同步
角色创建时选择了不同出身(如"流浪剑客"会获得精钢长剑、旅行者斗篷、干粮x5),但这些物品只存在于角色创建阶段,没有传递到背包系统中。
4. 没有独立的物品数据库
物品模板(如武器、药水、材料)硬编码在后端服务代码中,无法灵活扩展,也不便于后续其他模块(如商店系统、任务奖励)复用。
二、从零开始:技术选型与架构设计
面对以上问题,我决定重新设计背包系统的完整架构,确保它能够独立运行并与其他模块(用户系统、存档系统)对接。
2.1 数据库设计
我们团队确定使用SQLite作为开发阶段的数据库。对于背包系统,需要两张表:
item_templates 表:存储所有物品模板
| 字段 | 类型 | 说明 |
|---|---|---|
| item_id | TEXT | 物品唯一ID(主键) |
| name | TEXT | 物品名称 |
| type | TEXT | 类型(weapon/consumable/quest/material) |
| description | TEXT | 物品描述 |
| icon | TEXT | 图标emoji |
| usable | INTEGER | 是否可使用 |
| effects | TEXT | 使用效果的JSON数据 |
| story_id | TEXT | 所属故事ID |
inventory_items 表:存储每个会话的背包物品
| 字段 | 类型 | 说明 |
|---|---|---|
| id | INTEGER | 自增主键 |
| session_id | TEXT | 会话ID |
| item_id | TEXT | 物品ID |
| name | TEXT | 物品名称 |
| quantity | INTEGER | 数量 |
session_id作为外键关联到游戏会话,这意味着:
-
每个游戏会话有独立的背包
-
同一用户的不同存档互不影响
-
为多用户多存档场景做好了准备
2.2 后端分层架构
参考了实际生产项目的设计模式,我将后端代码分为三层:
api/v1/inventory.py # API路由层:处理HTTP请求 services/inventory_service.py # 业务逻辑层:物品使用、丢弃规则 models/database/inventory.py # 数据访问层:数据库CRUD操作
这种分层的优势在于:如果将来需要修改数据库(比如从SQLite换成MySQL),只需要修改数据访问层;如果要修改业务规则(比如增加物品合成功能),只需要修改服务层。各层职责清晰,便于维护。
2.3 物品模板的独立管理
我创建了 db/init_items.py 文件,将所有物品的数据集中管理。以迷雾森林故事为例,定义了武器、消耗品、任务物品、材料、特殊物品等五大类共计20+件物品:
items = [
("item_sword_001", "精钢长剑", "weapon", "一把锋利的精钢长剑...", "⚔️", 1, '{"attack":5}', "fantasy_001", ...),
("item_potion_001", "治疗药水", "consumable", "恢复30点生命值...", "🧪", 1, '{"hp":30}', "fantasy_001", ...),
("item_quest_003", "月光碎片", "quest", "月光凝聚而成的碎片...", "✨", 0, '{"luck":2}', ...),
# ...20+件物品
]
并且配置了不同出身对应的初始物品:
CHARACTER_STARTING_ITEMS = {
"bg_001": ["item_sword_001", "item_tool_002", "item_food_003"], # 流浪剑客
"bg_002": ["item_quest_002", "item_sword_002", "item_tool_002"], # 贵族后裔
"default": ["item_sword_001", "item_food_001", "item_tool_001"], # 默认
}
三、后端实现:从内存存储到数据库持久化
3.1 数据访问层的实现
在 models/database/inventory.py 中,我实现了 InventoryDB 类,封装了所有的数据库操作:
class InventoryDB:
@staticmethod
def get_items(session_id):
"""获取背包所有物品"""
conn = get_connection()
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM inventory_items WHERE session_id = ? AND quantity > 0',
(session_id,)
)
rows = cursor.fetchall()
# 转换为字典列表返回
...
@staticmethod
def add_item(session_id, item_data):
"""添加物品,支持同类堆叠"""
# 先检查是否已有同类可堆叠物品
cursor.execute(
'SELECT id, quantity FROM inventory_items WHERE session_id = ? AND item_id = ? AND type IN (?, ?)',
(session_id, item_id, "consumable", "material")
)
existing = cursor.fetchone()
if existing:
# 堆叠:增加数量
new_qty = existing["quantity"] + quantity
cursor.execute('UPDATE inventory_items SET quantity = ? WHERE id = ?', (new_qty, existing["id"]))
else:
# 新增:插入新记录
...
这里我学到一个重要的概念:物品堆叠。消耗品和材料类物品是可堆叠的——如果有两个同样的"面包",不是插入两条记录,而是将一条记录的 quantity 字段加1。这在数据库层面通过先查询再更新的方式实现。
3.2 业务逻辑层的实现
在 services/inventory_service.py 中,实现了核心的游戏规则:
-
使用物品:检查是否可用 → 应用效果 → 减少数量 → 记录系统消息
-
丢弃物品:检查是否可丢弃(任务物品不可丢弃)→ 确认 → 删除记录
-
任务物品保护:type为"quest"的物品不允许丢弃
def drop_item(self, session_id, item_id, quantity=1):
target_item = self._find_item(session_id, item_id)
if not target_item:
return {"success": False, "message": "背包中没有该物品"}
if target_item["type"] == "quest":
return {"success": False, "message": "任务物品不能丢弃"}
# 执行删除
...
3.3 API接口层
API接口保持与之前相同的格式(以便前端无需大改),但底层从内存字典切换到了数据库:
@router.get("/{session_id}")
async def get_inventory(session_id: str):
result = inventory_service.get_inventory(session_id) # 从数据库读取
return result
@router.post("/use")
async def use_item(request: UseItemRequest):
result = inventory_service.use_item(
session_id=request.session_id,
item_id=request.item_id,
quantity=request.quantity
)
if not result["success"]:
raise HTTPException(status_code=400, detail=result["message"])
return result
四、前端实现:侧边栏与弹窗的联动
4.1 数据结构统一
前端需要处理两套数据源:
-
gameState.inventory:游戏引擎返回的字符串数组(如["精钢长剑", "面包x3"]) -
inventoryItems:后端返回的结构化对象数组(包含item_id,name,icon,quantity等字段)
我实现了转换函数:
const syncMockInventory = () => {
const items = gameState.value.inventory || [];
inventoryItems.value = items.map((itemStr, idx) => {
const match = itemStr.match(/^(.+?)x(\d+)$/);
const name = match ? match[1].trim() : itemStr;
const qty = match ? parseInt(match[2]) : 1;
return {
item_id: 'mock_' + idx,
name: name,
quantity: qty,
icon: name.includes('剑') ? '⚔️' : '📦',
usable: name.includes('药水') || name.includes('面包'),
// ...
};
});
};
4.2 侧边栏改造
侧边栏从读取 gameState.inventory 改为读取 inventoryItems:
<div v-for="item in inventoryItems.slice(0, 6)" :key="item.item_id" class="item">
{{ item.icon || '📦' }} {{ item.name }}
<span v-if="item.quantity > 1">x{{ item.quantity }}</span>
</div>
这样侧边栏和弹窗读取同一数据源,保持数据一致性。
4.3 弹窗交互实现
弹窗中实现了四个核心交互:
-
选中物品:点击后高亮,显示物品详情
-
使用物品:调用后端API,消耗品使用后数量减少或消失
-
丢弃物品:调用后端API,任务物品不可丢弃
-
数量显示:右上角角标显示
x1、x3等
五、遇到的关键问题与解决方案
问题1:背包弹窗只显示图标,不显示数量
现象:弹窗中每个物品只显示图标和名称,右上角的数量角标始终不出现。
排查过程:我在浏览器开发者工具中打印 inventoryItems,发现数组中的每个物品对象都有 quantity 字段,值为1。但弹窗中 v-if="item.quantity > 1" 条件为假,所以角标不显示。
原因分析:初始物品都是从出身配置中来的,每个物品初始数量只有1。弹窗的条件 item.quantity > 1 导致数量为1时角标隐藏。
解决方案:去掉条件限制,让所有物品都显示数量:
<!-- 改前 -->
<div v-if="item.quantity > 1" style="...">x{{ item.quantity }}</div>
<!-- 改后 -->
<div style="...">x{{ item.quantity }}</div>
这样即使只有1个物品,用户也能清楚看到数量。
问题2:创建"流浪剑客"角色后,背包显示了重复物品
现象:选择"流浪剑客"出身(初始物品:精钢长剑、旅行者斗篷、干粮x5),背包里却出现了"长剑、面包x3、精钢长剑、旅行者斗篷、干粮x5",前面多了默认物品。
排查过程:我在 startMockGame 函数中找到问题。mockState.inventory 被设置为默认值 ['长剑', '面包x3'],然后 applyCharacterBonuses 函数又把出身物品 ['精钢长剑', '旅行者斗篷', '干粮x5'] 追加了进去。
原因分析:游戏初始化时,默认物品和出身物品没有互斥。原始代码是为"不使用出身系统"的场景设计的,加入出身系统后产生了冲突。
解决方案:修改逻辑,当角色选择了出身后,用出身物品替换默认物品,而不是追加:
if (characterData?.background?.starting_items) {
inventory = [...characterData.background.starting_items]; // 用出身物品替换
} else {
inventory = ['长剑', '面包x3']; // 无出身时用默认
}
问题3:后端模块导入路径反复报错
现象:运行 python run.py 后,不断出现 ModuleNotFoundError: No module named 'db'、No module named 'models' 等错误。
原因分析:我们的后端使用了包结构(backend/ 目录下有多个子目录),每个Python文件中的导入语句需要根据文件位置使用正确的相对路径。最初我在不同文件中混用了绝对导入和相对导入,导致模块找不到。
解决方案:统一使用相对导入,并总结了规律:
| 导入来源 | 写法 |
|---|---|
| 同目录 | from .session import get_connection |
| 上一级 | from ..models.database import InventoryDB |
| 上两级 | from ...db.session import get_connection |
同时确保每个子目录都有 __init__.py 文件(即使是空文件),这样Python才会将其识别为包。
问题4:加 type="module" 后页面无法运行
现象:为了让前端使用ES Module导入,我把 <script src="app.js"> 改为 <script type="module" src="app.js">,结果页面完全白屏,控制台报错。
原因分析:type="module" 要求服务器环境运行,不能直接双击HTML文件打开。而且模块导入中使用了Vue的CDN全局变量,在模块作用域中无法正确访问。
解决方案:放弃 type="module",将所有背包逻辑直接写在 app.js 的 setup() 函数中,通过 return 暴露给模板使用。这样虽然代码耦合度略高,但在当前CDN引入Vue的架构下是最稳定的方案。
六、当前成果与功能演示
经过多轮修改和调试,背包系统最终实现了以下功能:
后端功能
-
✅ 物品模板数据库(20+件迷雾森林专属物品)
-
✅ 背包数据库增删改查
-
✅ 消耗品使用(效果应用到角色属性)
-
✅ 任务物品丢弃保护
-
✅ 物品堆叠机制
-
✅ 背包容量上限检查
-
✅ 导出/导入接口(供存档系统使用)
前端功能
-
✅ 侧边栏实时显示背包物品
-
✅ 弹窗网格展示物品图标、名称、数量
-
✅ 选中物品高亮并显示详情
-
✅ 使用/丢弃按钮交互
-
✅ 使用物品后属性变化实时反映
-
✅ 不同出身对应不同初始物品




七、技术理解与收获
通过这个背包系统的完整实现,我获得了以下几点深刻体会:
1. 数据库是基础
最初我想用内存字典快速实现,但很快就遇到了数据丢失、多会话隔离、物品结构化存储等问题。切换到SQLite后发现,很多业务逻辑(如物品堆叠、容量检查)用SQL语句比Python代码更简洁可靠。
2. 导入路径的错误是"必经之路"
之前写单文件脚本时从没遇到过导入问题。但在包结构的项目中,from .xxx 和 from xxx 的区别至关重要。每一次报错都是对Python模块系统理解的加深。
3. 前后端数据格式统一很重要
最初前端用字符串数组(["长剑", "面包x3"]),后端用结构化对象({item_id, name, quantity, icon}),导致两端数据不同步。统一为结构化对象后,代码可维护性大大提高。
4. 任务物品不能丢弃——这条规则的价值
看似简单的一条规则,实际上反映了游戏设计的核心:某些物品是推动剧情的关键,玩家不能随意丢弃。这让我开始思考后续更多的物品类型和交互规则。
5. 增量开发 + 及时测试
每次只修改一个功能点(比如先实现"使用物品",再实现"丢弃物品"),修改完立即用Swagger或浏览器测试。这样即使出错也能快速定位问题所在。
背包系统目前已经能够独立运行并与数据库交互。接下来需要等待其他成员完成用户系统、存档系统后,进行跨模块联调。同时,我还需要补充"物品给予NPC"等更复杂的交互功能,以满足任务书中的要求。
从最初的内存字典到数据库持久化,从假数据到真实物品,这个功能模块的实现让我完整经历了一次前后端全栈开发的过程,也为后续的协作开发积累了宝贵的经验。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)