上午那篇把菜单和 HUD 统一成了「一套 Theme」。这一篇换个方向——玩家留存。很多独立游戏玩法不错,却缺少「再玩一局」的理由;Steam 的 成就(Achievements)与统计(Stats) 就是最便宜、最标准化的那个理由。今天用 Steamworks 官方文档 + GodotSteam 教程,把「通关一次就删」变成「总想再刷一把」。
今日速览
- 成就与统计绑定 Steam 账号、随账号漫游,换电脑也在;玩家个人资料页会自动展示——等于一个免费的炫耀位。
- 统计只有三种类型:INT(整数)/ FLOAT(浮点)/ AVGRATE(滑动平均);成就可以挂一个「进度统计」,社区里自动显示进度条。
- 关键 API 全在 ISteamUserStats:
GetStat/SetStat读写、StoreStats落库、SetAchievement/ClearAchievement解锁与清除。 - 必须先在后台上架(Publish)成就定义,否则 API 调用会报错——哪怕游戏还没正式发行,也可以先发布这些定义。
- GodotSteam 3.28 / 4.14 起,登录时自动同步本地用户数据,不再需要手动请求回调。
核心学习内容
1. 先想清楚「成就为什么存在」
成就不是彩蛋,是设计工具。Steamworks 文档列了它的四大用途:奖励玩家、鼓励合作与交互、给目标增加维度、奖励玩得久的玩家。翻译成人话,好成就要做到三件事:
- 教机制:
初次格挡成功、用环境杀死敌人,教会玩家系统还能这么用。 - 给方向:
不受伤通关第一章,让高手有挑战目标。 - 记里程碑:
首次通关、收集全部 10 张地图碎片,给正反馈。
最忌讳的是「击杀 1000 只怪」这种纯刷——它是留存杀手,不是留存钩子。
2. 统计的三种类型与关键属性
在 Steamworks 后台(App Admin)定义统计时,每个统计有这些要点:
- Type:
INT/FLOAT/AVGRATE。AVGRATE 是 Steam 帮你做滑动平均,适合「平均帧率」「平均通关时长」这类指标。 - Increment Only:勾上后数值只能增——
总击杀数、总游玩时长必须勾,防止读档/回滚把数据改小。 - Max Change:单次写入的最大变化量,防止一次异常调用把数据写飞。
- Min / Max Value:上下限。
- Aggregated:勾上后 Steam 会维护全局总计(Global Stats),可用来做「全服累计击杀 X 亿」这类社区目标。
成就侧最有用的是 Progress Stat:给它挂一个统计,社区里就自动显示进度条,而且统计到达解锁值时成就自动解锁——不用你手写判断。
3. 读写的正确时机(GodotSteam 实操)
调用顺序错了,数据就会丢。推荐的最小骨架:
# autoload: SteamStats.gd
var achievements: Dictionary = {"first_win": false, "no_hit_boss": false}
var statistics: Dictionary = {"total_kills": 0, "best_time": 0}
func _ready() -> void:
var r: Dictionary = Steam.steamInitEx()
if r.get("status", 1) > Steam.STEAM_API_INIT_RESULT_OK:
push_error("Steam 初始化失败: %s" % r)
return
load_from_steam() # 先拉云端
merge_local_save() # 再和本地存档合并
func load_from_steam() -> void:
for key in statistics.keys():
statistics[key] = Steam.getStatInt(key)
func set_stat(key: String, value: int) -> void:
if value <= statistics.get(key, 0): # Increment Only 语义:只增不减
return
statistics[key] = value
Steam.setStatInt(key, value)
func unlock(api_name: String) -> void:
if achievements.get(api_name, false):
return
achievements[api_name] = true
Steam.setAchievement(api_name)
Steam.storeStats() # 解锁后立刻落库
func save() -> void:
Steam.storeStats() # 不要每帧调用,退出/关键节点调用即可
三个容易踩的坑:
storeStats()要节流:它是一次网络/磁盘写入,别放在_process里每帧调用,放在存档点、关卡结束、退出前。- 离线要兜底:
steamInitEx失败时(无 Steam 客户端、断网)应降级为纯本地存档,而不是崩溃。 - 本地与云端「取大值」:高分类统计冲突时保留较大值,避免玩家换机后进度倒退;但像「当前生命」这种非单调统计不能盲目取大,需要按语义分别处理。
4. 上架前必须做的一步
在 Steamworks 后台把统计与成就定义并发布(Publish)。GodotSteam 文档特别强调:游戏不需要通过审核、也不需要已发行,但这些定义必须处于已发布状态,否则 API 读写会直接报错。默认上限是 100 个成就,达到 Profile Features 门槛后可以申请更多。
🎯 DeepSeek 提示词
提示词 1 · 设计一套成就不坐班清单
你是一名资深游戏系统设计师。我要为一款【2D 横版动作冒险游戏】设计 Steam 成就系统。
游戏核心循环:探索地图 → 战斗 → 收集碎片 → 挑战 BOSS。
请输出 12 个成就,要求覆盖四类:教学(教机制)、挑战(高手)、里程碑、隐藏(剧透保护)。
用 Markdown 表格输出,列:API Name(小写下划线) | 显示名 | 描述(≤80字) | 是否隐藏 | 触发条件 | 建议挂的进度统计(没有则填 -)。
约束:最多 2 个纯累积型成就;每个成就必须一句话说清"为什么它能提升留存或教学效果"。
提示词 2 · 生成 GodotSteam 成就管理 autoload
你是 Godot 4 + GodotSteam 专家。请写一个 autoload 单例 SteamAchievements.gd,要求:
1. 用字典管理统计与成就,键为 Steam API Name;
2. 初始化时从 Steam 读取,并与本地 JSON 存档合并(分类统计取较大值,非单调统计按"以云端为准");
3. 提供 set_stat_int / set_stat_float / unlock / check_all 四个方法;
4. storeStats() 只在存档点、关卡结束、退出前调用,不得出现在 _process;
5. steamInitEx 失败时自动降级为纯本地模式,并在控制台给出明确警告。
只返回完整 .gd 代码 + 关键行中文注释,不要多解释。
提示词 3 · 审查「漏触发 / 重复触发 / 丢存档」
你是代码审查员。下面是我的成就触发点列表和成就定义(粘贴进来)。
请逐条检查并输出表格:成就 API Name | 是否存在触发点 | 重复触发风险 | 存档/离线丢失风险 | 修复建议。
重点排查:① 定义了但从未触发的"僵尸成就";② 在 _process 里重复解锁;③ storeStats 未节流;
④ 断网或 Steam 未启动时的崩溃路径。按严重程度排序,只列问题,不要客套。
学习心得
今天最大的收获是:成就系统本质上是「设计目标的可视化」,而不是技术炫技。它逼我提前想清楚「这款游戏希望玩家追求什么」。对「用 DeepSeek 做一款 Steam 游戏」这个目标来说,这一环尤其划算——成就定义是纯数据,正好让 DeepSeek 生成、我来审核;GodotSteam 的接入代码也高度模板化,DeepSeek 一次就能写出能跑的骨架。先把成就清单定下来,再写代码,比反过来高效得多。
明日关注
- 在 Steamworks 后台实际定义 3 个统计 + 3 个成就并 Publish,跑通 GodotSteam 读写闭环。
- 把成就解锁接进已有的战斗/收集系统,验证「进度统计自动解锁」是否生效。
- 研究 Steam 云存档(Steam Cloud)与统计的联动,避免「云端有成就、本地没存档」的割裂感。
本文由自动化学习流水线每日生成,坚持即学即记。
评论