CD Code Digest Hub /GTA V

脚本运行时与 Native 桥接

8 分钟阅读 8 个章节 2 张图

本文以本地源码静态分析为依据;分析日期为 2026-10-30,未编译或运行工程。总体分层承接README。返回总览。

1. 设计定位:脚本负责组织,原生系统负责实现#

SanScript 的 .sc 是可启动的玩法程序,.sch 同时承载 Native 声明、数据结构、公共函数及大段实现。USING 不是可以直接等同于 C++ 动态模块的机制;一个很短的 .sc 可能通过头文件引入完整玩法框架。分析规模时不能只统计入口脚本,也不能把所有 .sch 都当接口。

脚本控制“何时申请资源、创建哪些实体、向谁下达行为、何时完成目标”。模型载入、角色寻路、动画混合、碰撞、网络复制仍由游戏层和 RAGE 执行。脚本线程也不是每个脚本对应一个操作系统线程:scrThread::Update、Run、Wait 构成可让出的执行上下文。[S1][S2]

这是职责图,不表示每个 Native 都经过所有方框。部分查询直接返回游戏层缓存,部分调用创建跨帧完成的原生任务。

2. 执行模型和帧预算#

GtaThread::Update 在调用 scrThread::Update(insnCount) 前处理强制清理,并围绕执行过程管理脚本 HUD 和若干状态校验。RAGE 更新 TimerA/TimerB,再运行脚本;EXECUTABLE_SCRIPTS 条件下还存在另一条执行路径,因此不应宣称所有配置都只能解释字节码。[S1][S2]

WAIT 写入等待时间、将线程设为 BLOCKED,并让出执行。WAIT(0) 的含义是把控制权交回调度系统,而不是等待 0 毫秒后在同一调用栈不停自旋。脚本中常见的 WHILE TRUE / WAIT(0) 因而是一种合作式帧更新方式。insnCount 参数也不能直接解释成已经验证的固定毫秒预算。

三种任务必须分开:

名称持有的状态负责什么
剧情/活动任务目标、任务阶段、结算、进度玩家在做什么
NPC CTask行为阶段、目标实体、移动/战斗子任务NPC 如何执行命令
引擎执行任务工作批次、依赖、完成信号CPU 工作如何调度

脚本层的分帧函数列表也不自动产生并行工作线程,详见启动与调度。

3. 纵向追踪:CREATE_VEHICLE#

这条链路是理解边界的最小完整案例:

  1. 脚本先 REQUEST_MODEL,跨帧等待 HAS_MODEL_LOADED,再调用 CREATE_VEHICLE。
  2. game/script_headers/commands_vehicle.sch 通过 NATIVE FUNC 声明接口。commands_vehicle.cpp 的注册项把 Native 标识绑定到 CommandCreateVehicle。[S3][S4]
  3. 原生实现检查任务车辆容量、坐标、模型是否合法、模型是否已加载、是否确为车辆。
  4. 在线时检查当前脚本能否创建网络实体;若要求创建 script-host object,还验证本机是该脚本的主机。
  5. 游戏系统创建实体,脚本得到句柄。实体的网络生命周期进一步交给网络对象和脚本 Handler 管理。

因此,“函数存在”不表示在任何时刻、任意脚本中调用都有效。资源准备、脚本身份、所有权和世界条件都是接口契约的一部分。源码中的断言属于开发诊断;不能把每条断言都等同于发行配置下的完整恢复策略。

4. 句柄、网络 ID 与存档数据#

脚本实体句柄通过 GUID 查询解析到原生对象。句柄有效性、实体是否死亡、是否仍可修改是不同问题。DOES_ENTITY_EXIST 通过后,战斗角色仍可能已经受伤,车辆仍可能不可驾驶。

在线的 NETWORK_INDEX 与本机实体句柄也不是同一标识域:广播网络 ID 后,接收端还需确认网络实体存在并进行 NET_TO_PED/VEH 一类转换。不能把一个客户端的本地句柄作为跨机器实体标识。

存档保存的是显式注册的脚本数据和游戏保存系统支持的状态,而不是任意 C++ 指针、完整脚本堆或所有实时对象。见持久化报告。

5. 强制清理是一项运行时机制#

脚本入口常写 IF HAS_FORCE_CLEANUP_OCCURRED(...),但它并非只在脚本首次进入时检查一次的普通条件。SetForceCleanupReturnPoint 保存 PC、FP、SP;DoForceCleanupCheck 在匹配条件触发后恢复执行位置,使脚本进入已登记的清理路径。[S1]

一个重要差异是:原生代码明确说明,网络游戏中的死亡/被捕不走单机的自动死亡清理规则;在线脚本需要相应的网络退出和重生逻辑。也不能认为 TERMINATE_THIS_THREAD 会自动撤销脚本写入的所有全局状态、道路开关和镜头覆盖。

6. 异步调用与事件回传#

资源加载、网络加入、云查询和很多 NPC 动作都跨越多个帧。脚本通常保存一个进度枚举,反复调用推进函数;完成后再进入下一阶段。调用成功、请求被接受和最终动作成功应分别判断。

commands_script.cpp 暴露 GET_EVENT_DATA 和脚本事件发送接口,把消息队列与玩法数据连接起来。[S5] 旧 TRIGGER_SCRIPT_EVENT 在在线路径带有弃用断言,当前脚本可见 SEND_TU_SCRIPT_EVENT。广播变量、实体复制和事件有不同的保持/恢复语义,详见多人同步。

7. 对扩展玩法的实际启示#

先写清楚资源和状态的所有者,再实现正常流程:谁启动脚本,谁维护任务标志,谁负责模型、实体、摄像机、音频场景以及网络广播数组的退出。对于“每帧有效”的 Native,应在活动期持续调用;对于一次性创建和下达任务,应由状态切换保护,避免每帧重复创建或重启行为。

接口扩展要同时核对声明、注册、实现和条件编译;仓库中 core/common/native 也保存 Native 头文件,不能假设所有副本始终一致。本文未比较全部 Native 副本,也未验证脚本资源与原生二进制的实际匹配版本。

源码证据与阅读位置#

编号位置与重点
[S1]gta_thread.cpp:L280 Update;L782 返回点;L800 强制清理
[S2]thread.cpp:L2342 Update;L2607 命令注册;L2675 Wait
[S3]commands_vehicle.sch:CREATE_VEHICLE Native 声明
[S4]commands_vehicle.cpp:L104 创建实现;L15346 注册
[S5]commands_script.cpp:L109 脚本请求;L478/493 事件读写

[S1]: <../../../GTAV Source/src/dev_ng/game/script/gta_thread.cpp> [S2]: <../../../GTAV Source/src/dev_ng/rage/script/src/script/thread.cpp> [S3]: <../../../GTAV Source/src/dev_ng/game/script_headers/commands_vehicle.sch> [S4]: <../../../GTAV Source/src/dev_ng/game/script/commands_vehicle.cpp> [S5]: <../../../GTAV Source/src/dev_ng/game/script/commands_script.cpp>