游戏关卡前置条件存储方案选型:MySQL最优方案咨询
针对你的需求——存储关卡的技能/前置关卡要求,并能根据玩家当前技能值和已通关关卡快速查询可参与关卡,我来拆解下两种MySQL方案的优劣,再聊聊其他可能的选项:
方案1:范式化多表结构
这是典型的第三范式设计,优势非常明显:
- 超强扩展性:以后要加新技能(比如新增Swim技能),只需要在
SKILLS表插一条记录,SKILL_PREREQUISITE表加对应关卡的要求即可,完全不用改表结构,对生产环境非常友好。 - 无冗余数据:某个关卡只要求2个技能,就只存2条记录,不像方案2那样要空着其他3个技能字段,存储效率高。
- 逻辑清晰易维护:技能和关卡的关联关系完全解耦,要修改某个关卡的技能要求,直接增删
SKILL_PREREQUISITE的记录就行;统计某个技能被多少关卡要求这类需求也能轻松实现。
唯一的小缺点是查询SQL相对复杂一点,但只要做好索引(比如给SKILL_PREREQUISITE加(LevelID, SkillID)联合索引,LEVEL_PREREQUISITE加(LevelID, prerequisiteLevelID)索引),性能完全没问题。
方案2:单表固定技能字段
这个方案的优点只能说是查询SQL初期写起来直观,直接对比玩家技能值和表中对应字段就行,但缺点远大于优点:
- 扩展性极差:如果以后要加新技能,必须修改
LEVEL_SKILL表的结构,新增字段——这在生产环境中是非常忌讳的操作,而且会导致大量空值(很多关卡根本不需要新技能)。 - 数据冗余严重:每个关卡记录都要存5个技能字段,哪怕某个关卡只要求1个技能,其他字段只能设为0或NULL,还得额外区分“无要求”和“要求≥0”的逻辑,容易出bug。
- 维护成本高:要是哪天要改技能名称,不仅要改配置,还得改表字段名,完全不符合数据库设计规范。
其他可选方案
MySQL JSON字段方案
在LEVELS表新增一个skill_requirements JSON字段,存类似{"Run":90, "Fly":10, "Sleep":25}的结构,同时LEVEL_PREREQUISITE表保留。这种方案灵活性强,不用额外建表,但缺点是MySQL对JSON的查询性能不如关联表,数据量大时很难做索引优化,而且统计类需求(比如统计哪些关卡要求Fly技能)会非常麻烦,适合关卡数量少、查询频率不高的小型游戏。
非MySQL方案(Redis)
用Redis的Hash结构存储每个关卡的前置条件,比如键为level:5,值为{"skills":{"Run":90,"Fly":10,"Sleep":25}, "prereq_levels":[1,2,3]}。查询时可以批量获取所有关卡的条件,然后在应用层判断玩家是否满足。这种方案的优势是查询速度极快,适合高并发场景,但数据持久化不如MySQL,复杂的条件判断要放到代码里实现,增加了应用层的工作量。
总结:最优选择
如果你的游戏技能数量可能变化(比如以后加新技能),或者关卡的技能要求组合非常灵活,那方案1绝对是最优解——范式化结构带来的扩展性、维护性是长期的核心收益,这点SQL复杂度的代价完全值得。
哪怕技能数量暂时固定为5个,方案1的性能也能通过索引优化追上方案2,而且避免了方案2的各种维护坑。JSON或Redis方案可以作为特定场景下的补充,但MySQL方案1是通用的最优选择。
举个方案1的查询SQL示例(假设玩家技能值:Run=95、Fly=15、Sleep=30、Jump=50、Shoot=20;已完成关卡1、2、3):
SELECT l.ID, l.`Level Name` FROM LEVELS l -- 筛选满足所有技能要求的关卡 WHERE NOT EXISTS ( SELECT 1 FROM SKILL_PREREQUISITE sp JOIN SKILLS s ON sp.SkillID = s.ID WHERE sp.LevelID = l.ID AND ( (s.`Skill Name` = 'Run' AND 95 < sp.requiredValue) OR (s.`Skill Name` = 'Fly' AND 15 < sp.requiredValue) OR (s.`Skill Name` = 'Sleep' AND 30 < sp.requiredValue) OR (s.`Skill Name` = 'Jump' AND 50 < sp.requiredValue) OR (s.`Skill Name` = 'Shoot' AND 20 < sp.requiredValue) ) ) -- 筛选满足前置关卡要求的关卡 AND NOT EXISTS ( SELECT 1 FROM LEVEL_PREREQUISITE lp WHERE lp.LevelID = l.ID AND lp.prerequisiteLevelID NOT IN (1,2,3) ) -- 排除已完成的关卡 AND l.ID NOT IN (1,2,3);
内容的提问来源于stack exchange,提问作者Priyath Gregory

