CD Code Digest Hub /GTA V

从真实游戏代码理解 SanScript

7 分钟阅读 7 个章节 1 张图

本篇按“要完成的游戏行为 → 脚本组织 → Native/运行时 → 设计启发”阅读五类实际代码。下面的节选和概括不构成已编译的独立示例;省略的初始化、共享定义与平台条件仍然重要。

1. 常驻主控制器:main.sc#

生命周期#

main.sc 先包含基础命令、全局数据与大量公共控制逻辑,声明各模块的实例状态结构。入口登记单机转多人等清理条件,启动 initial 脚本并等待它完成,再进入常驻循环。[S1]

加载/启动过程中的关键代码是:

text
REQUEST_SCRIPT(initialScriptName)
WHILE NOT (HAS_SCRIPT_LOADED(initialScriptName))
    REQUEST_SCRIPT(initialScriptName)
    WAIT(0)
ENDWHILE

START_NEW_SCRIPT(initialScriptName, FRIEND_STACK_SIZE)
SET_SCRIPT_AS_NO_LONGER_NEEDED(initialScriptName)

它同时体现三个概念:程序资源异步加载、虚拟线程启动、请求引用释放。最后一行不能理解为终止 initial。父脚本通过活动线程数量等接口继续等待子脚本阶段完成,而非普通函数返回。

更新频率#

主循环处理帮助文字、过场握手、脚本重启检查、区域逻辑、角色/载具提示、属性系统和电话等,再执行 WAIT(0)。其中有一秒和五秒计时门限,把低频维护从每帧工作中分离。[S1]

这种写法容易让作者理解时序,但常驻脚本会逐渐成为依赖中心。设计新语言时,可以提供显式 tick/periodic 任务与实例状态模块,保留顺序语义,同时让工具显示每个子更新的成本和调用效果。

2. 剧情任务:Franklin0.sc#

任务并非一个很长的阻塞过程#

任务入口设置 mission flag,登记 force cleanup,创建开发 Widget,然后进入 WHILE TRUE。初始化成功后,每帧推进下一阶段资源流式准备、处理失败条件和共同角色逻辑,再按 currentMissionStage 分派:GET_TO_BAD_GUY、VAN_CHASE、FOOT_CHASE、FIND_BAD_GUY、GET_TO_HOUSE。[S2]

真正游戏行为由任务阶段中的 Native 和共享 .sch 封装驱动;脚本负责把异步系统组织成可推进、可失败、可恢复的任务。

清理比内存释放更广#

MISSION_CLEANUP 恢复或释放音乐事件、文本、视觉效果、控制器灯效、音频场景、通缉参数、流式 volume、道路/区域限制、过场、商店可用性、人口预算等。[S3]

这份真实清理清单说明,脚本资源的核心不是几块内存。一次任务会改变共享世界配置,退出时需要撤销副作用。仅给新语言增加 GC 不能解决这些问题;更有价值的是可撤销状态覆盖、task scope 和带归属的资源租约。

也不能只对主循环证明“每帧一个 WAIT”:大型函数内部仍可能有等待路径。新的 effect 分析应把隐藏挂起点展示在调用图中。

3. 自动存档:autosave_controller.sc#

用枚举表达业务阶段#

脚本定义 INITIALISE、WAIT_FOR_REQUEST、PERFORM_AUTOSAVE、CLEANUP 四个状态,保存当前 eScriptStage。入口处理单机转多人清理,循环先 WAIT(0),再按状态调用对应过程。[S4]

请求来自共享 g_sAutosaveData。处理过程区分“暂时不安全,稍后重试”与“本次请求不允许,标记完成/取消”:例如玩家死亡可延迟,某些游戏模式/角色状态可能直接拒绝。若条件尚未满足,PROC 的 EXIT 只退出本次处理,让后续帧继续推进。

这是普通 ENUM + SWITCH + PROC 形成的状态机,不能据此说语言具有已验证的 statechart 语法。它展示了为什么游戏编排需要一种可观察的业务状态,而不只是调用栈。

与底层存档的分工#

controller 决定何时可以保存;REGISTER_*_TO_SAVE 决定保存哪些数据;SaveLoad 模块负责序列化和恢复。三者分层有利于复用,但现在依赖很多 BOOL 和全局字段维持一致性。

新语言可把请求结果表达为 Pending/Retry/Complete/Rejected 等明确状态,减少“多个布尔变量组合出非法状态”的机会,并提供状态变更记录。

4. 多人压力脚本:net_jacking_soaktest.sc#

该脚本处于调试条件分支,注册网络脚本、等待主流程、注册 host broadcast、等待首次广播,再初始化机器人和车辆,持续处理并按终止条件结束。[S5]

模型加载阶段:

text
REQUEST_MODEL(POLICE)
WHILE NOT HAS_MODEL_LOADED(POLICE)
    WAIT(0)
ENDWHILE

建车阶段先检查 NETWORK_IS_HOST_OF_THIS_SCRIPT,预留任务车辆容量并确认可以注册,取得玩家位置/方向后逐辆创建;把 VEH_TO_NET 转换得到的标识存入广播区。

用法要点:本地 VEHICLE_INDEX 与跨机器网络标识不同;模型加载成功与主机权限不同;广播登记完成与首次数据到达不同。这些条件全部完成后,玩法才具备运行前提。

示例无限等待模型的写法展示了实际代码习惯,但对新语言更适合设计超时、取消、失败结果和自动释放请求引用的结构化接口。这里是设计改进,不是宣称现有脚本必然卡住。

5. 开发场景:vehicle_test.sc#

该文件在 Final 条件下生成空 SCRIPT,在 Debug 下引入车辆、任务、模型、相机、Widget/XML 等接口。场景枚举与外部 XML 菜单对应,固定数组保存测试角色、车辆、路线和镜头,并暴露速度、动画、座位等调试参数。[S6]

这说明脚本语言也承担原生程序员的测试编排工具:不用为每个 AI/车辆问题改 C++ 主循环,就能配置可重现场景。它的生产力来自 API 覆盖、参数面板和可重复启动,而非语法复杂程度。

需要注意外部 XML 与枚举值的契约仍靠工程维护。新系统可以从同一 schema 生成 UI、枚举和校验,减少菜单选择指向错误测试的风险。

6. 如何继续阅读任意一个玩法脚本#

  1. 从 SCRIPT 入口找初始化、WAIT、终止和 force cleanup。
  2. 列出文件级状态和 GLOBALS,判断哪些每实例独立、哪些共享。
  3. 找阶段枚举、SWITCH、定时器和事件队列,画业务状态图。
  4. 选一项外部行为,追到 .sch 声明、Native ID、C++ 注册和实现。
  5. 核对资源请求、加载完成、句柄有效性、网络权限及释放路径。
  6. 检查每帧频率、循环上界、结构体复制和高频 Native。
  7. 记录 Debug/Final 条件与平台差异,最后再判断可迁移到新语言的抽象。

这一路线比从文件头逐行读到尾更容易先建立正确模型,尤其适合数万行脚本。

源码证据#

  • S1 — main.sc 初始化启动;入口;主循环。
  • S2 — Franklin0 任务入口;阶段分派。
  • S3 — Franklin0 清理。
  • S4 — autosave 状态;请求处理;主循环。
  • S5 — soaktest 初始化;入口。
  • S6 — vehicle_test。