Script
PalScriptPatcher
这是一个命令拦截器,作用是在游戏运行时将读取到的命令替换为自定义的命令。
SceCommandTypeResolver
这是一个虚拟机指令解码器,用于指令表映射,将指令码转换为具体的命令类。
1. 全局状态与缓存结构 (Global State & Cache Topology)
cs
private static bool _isInitialized;
private static readonly Dictionary<uint, Type> SceCommandTypeCache = new ();
-
静态缓存字典 (Dictionary<uint, Type>):系统在内存中维护了一张全局唯一的哈希表(Hash Map)。
-
Key (uint):一个 32-bit 的无符号整型,作为指令的唯一签名(Signature)。
-
Value (Type):CLR 的类型句柄(Type Handle),代表具体的指令逻辑类。它将在后续阶段配合 Activator 或反射进行对象的实例化。
-
2. 元数据扫描与映射构建 (Metadata Scanning & Mapping Construction)
cs
public void Init()
-
程序集反射扫描 (Assembly-Level Reflection Scanning) :
系统通过 Assembly.GetExecutingAssembly().GetTypes() 提取当前执行域中的所有元数据,并利用 LINQ (Where) 过滤出实现了 ICommand 接口的具体类(Concrete Classes)。
-
自定义特性提取 (Custom Attribute Extraction) :
遍历这些指令类,通过 GetCustomAttribute 探查是否存在 SceCommandAttribute。该特性相当于指令的元数据标签(Metadata Tag),内含了指令的核心 ID(CommandId)。
3. 动态位掩码生成 (Dynamic Bitmask Generation)
cs
private static ushort CalculateUserVariableMask(Type commandType)
这是该解析器中最底层的位运算(Bitwise Operations)模块,用于构建该指令的用户变量掩码(User Variable Mask)。
-
反射属性遍历:通过 GetProperties() 抓取该类的所有公共属性(PropertyInfo)。
-
强类型校验 (Strong Type Assertion):通过 propertyInfo.PropertyType != typeof(ushort) 实施契约断言,确保被 SceUserVariableAttribute 标记的属性必须精确对应底层的 16-bit 内存对齐(2-byte UInt16)。
-
按位或与左移 (Bitwise OR & Left Shift):
csuserVariableMask |= (ushort)(1 << i);利用循环变量 i 作为位偏移量(Bit Offset)。将常量 1 向左推移 i 个二进制位,然后与当前的掩码执行按位或(OR) 。
原理:这会将掩码的第 i 位置为 1。例如,如果第 0 和第 2 个属性带有标记,最终的二进制形态将是 0000 0000 0000 0101(即十进制的 5)。这是一种极其高效的标志位(Flags)聚合方式。
4. 32-bit 数据打包压缩 (32-bit Data Packing / Structuring)
cs
private static uint GetHashCode(ushort commandId, ushort userVariableMask)
{
return ((uint)commandId << 16) | (uint)userVariableMask;
}
这是一个标准的数据位移与拼接算法(Data Packing Algorithm)。
-
参数域:commandId(操作码)和 userVariableMask(变量掩码)各自占用 16-bit(ushort)。
-
左移合并:
-
将 commandId 强制转换为 32-bit 的 uint,然后左移 16 位 (<< 16)。这将其推到了高 16 位的内存区间。
-
将 userVariableMask 也转为 uint,它自然占据低 16 位。
-
通过按位或 (|),将两者完美拼接成一个单一的 32-bit 整数。
-
内存布局示例:[ 高 16位: CommandId ] [ 低 16位: VariableMask ]。这种设计消除了使用复合对象作为字典 Key 所带来的堆分配(Heap Allocation)开销。
-
UserVariableManager
基于事件驱动的状态机该类通过实现海量的 ICommandExecutor<T> 泛型接口,充当了**命令总线(Command Bus)**上的核心订阅者(Subscriber)
-
注册与生命周期 (Lifecycle & Registration):在构造函数中,通过调用 CommandExecutorRegistry<ICommand>.Instance.Register(this),它利用了运行时的类型信息(RTTI),将自身实现的十几个强类型 Execute 方法一次性挂载到全局事件总线中。
-
指令截获 (Instruction Interception):当剧本解析器(如你之前实现的 SceCommandTypeResolver)解码出特定的字节码并生成对应的 Command 实例后,引擎只需将该实例抛入总线,控制流(Control Flow)便会通过多态派发(Polymorphic Dispatch)精确路由到这里的某一个 Execute(T command) 函数栈中
PalScriptRunner
1. 核心管线:提取-解码-执行周期 (The Fetch-Decode-Execute Pipeline)
该类通过 Update(float deltaTime) 挂载到游戏引擎的主循环(Main Loop)中,驱动整个指令管线。
-
指令提取 (Instruction Fetching) :
_scriptDataReader.Position 充当了传统 CPU 中的程序计数器(PC, Program Counter)或指令指针(IP, Instruction Pointer)。它直接映射到 .sce 二进制内存块的物理偏移量。
-
指令解码 (Instruction Decoding) :
通过调用 _sceCommandParser.ParseNextCommand,将二进制字节流(Byte Stream)反序列化为运行时的托管对象(ICommand 实例)。
-
指令截获与热替换 (Hot-patching & Interception) :
解码后立即通过 _scriptPatcher 进行拦截。这相当于在微指令级(Microcode Level)注入了一个硬件钩子(Hardware Hook),允许在不修改原始二进制镜像的情况下实时替换脏指令。
2. 算术逻辑单元与状态寄存器 (ALU & State Registers)
为了处理剧本中的复杂条件分支(如:如果金钱大于100,并且主角在队伍中,则触发剧情),该 VM 内部构建了一个简化的算术逻辑单元(ALU)。
-
累加器 / 条件码寄存器 (Accumulator / Condition Code Register) :
bool _tempVariable 扮演了**零标志位(Zero Flag)**或累加器的角色。所有的 ScriptEvaluateVar... 评估指令(如大于、等于、范围检测)都会将底层查询的结果写入这个寄存器。
-
逻辑门状态 (Logical Gate State) :
_operatorType 记录了当前指令流的布尔代数(Boolean Algebra)规约模式。通过 EvaluateAndSetOperationResult,虚拟机支持了跨指令的逻辑与(AND)、逻辑或(OR)级联运算,实现了短路求值(Short-circuit Evaluation)的状态机累积。
3. 控制流与分支预测 (Control Flow & Branching)
该引擎通过直接修改指令指针实现了图灵完备(Turing-Complete)的流程控制:
-
无条件跳转 (Unconditional Jump) :
Execute(ScriptRunnerGotoCommand) 直接调用 _scriptDataReader.Seek,强制覆盖当前的 PC 指针,实现绝对内存地址的跳转。
-
条件跳转 (Conditional Branching) :
Execute(ScriptRunnerGotoIfNotCommand) 依赖于前置指令在 _tempVariable 中留下的状态字(Status Word)。如果条件不满足,则触发跳转。这是实现 if-else 或 while 循环等高级控制流的底层汇编基元。
4. 协作式协程与挂起机制 (Cooperative Coroutines & Yielding)
作为运行在游戏主线程上的虚拟机,它绝不能使用系统级的线程阻塞(Thread Blocking),否则会导致整个引擎帧率锁死。为此,它实现了一个基于堆栈的协程调度器(Stack-based Coroutine Scheduler)。
-
阻塞栈 (Waiters Stack) :
Stack<IScriptRunnerWaiter> _waiters 是该调度的核心数据结构。当解析到需要时间流逝(如等待动画播放完毕、等待玩家选择对话)的指令时,VM 会将一个 Waiter 压入栈中。
-
执行权让出 (Yielding Execution) :
在 Update 周期中,只要 _waiters 栈非空,VM 就会停止推进 PC 指针,转而更新栈顶的 Waiter(_waiters.Peek().ShouldWait(deltaTime))。这等同于协程的 yield return 语义,它将 CPU 执行权归还给引擎主循环(Game Loop),直到挂起条件解除(Waiter 被 Pop 出栈),VM 才会恢复"提取-解码"周期。
5. 自指总线订阅 (Self-Referential Bus Subscription)
在设计模式上,PalScriptRunner 展现出了一种独特的双态性(Dual-state) :
一方面,它是指令的生产者(Producer) ,通过 OnCommandExecutionRequested 事件将解码后的通用指令(如播放声音、生成特效)广播给全局环境。
另一方面,通过在构造函数中注册全局总线,它是自身 VM 指令的消费者(Consumer)。当遇到 VM 专属微指令(如更改执行模式、逻辑运算、跳转)时,控制流通过 ServiceLocator 或 Registry 被路由回实例自身的 Execute 多态栈中,实现了虚拟机运行状态的自我突变(State Mutation)。
GameSettings
GameTimeProvider
懒汉单例模式 (Lazy Singleton)
cs
public static GameTimeProvider Instance => _instance ??= new GameTimeProvider();
注意此实现非线程安全(没有 lock)。但在 Unity 这种主线程驱动的引擎中,这种写法因其高性能而常用于主线程专用的管理器。
startTimeUtc (时间锚点)
cs
private readonly DateTime _startTimeUtc = DateTime.UtcNow;
-
常量时间戳初始化。
-
物理内涵 :当 GameTimeProvider 实例被创建的一瞬间(即游戏启动或服务初始化时),系统捕获当前的 协调世界时 (UTC)。
-
关键字 readonly:
-
底层保证:该字段在内存中一旦写入,除了构造函数外任何地方都无法修改。
-
C++ 对照:类似于 const std::chrono::system_clock::time_point。
-
架构价值:确保了整个游戏运行期间,"零时刻"永远固定,不会因为代码 Bug 导致时间回溯。
-
-
为什么用 UtcNow 而不是 Now?
-
术语:时区无关性 (Timezone Agnostic)。
-
DateTime.Now 会受到电脑所在地时区和夏令时的影响,切换时区可能导致计算出负数。UtcNow 直接访问硬件时钟,具有绝对的单调性。
-
RealTimeSinceStartup (动态增量)
cs
public double RealTimeSinceStartup => DateTime.UtcNow.Subtract(_startTimeUtc).TotalSeconds;
-
逻辑流:
-
调用 DateTime.UtcNow 获取此刻的绝对时间。
-
调用 Subtract 方法,计算当前时间-开始时间_startTimeUtc。
-
返回一个 TimeSpan 结构体(代表两个时间点之间的间隔)。
-
访问 TotalSeconds 提取总秒数(包含毫秒部分)。
-
-
与逻辑时间 (TimeSinceStartup) 的本质区别:
-
逻辑时间:靠 Update 累加,受游戏帧率波动影响,可以被代码暂停。
-
真实时间 :靠操作系统时钟差值计算,永不停止。即便 Unity 引擎因为加载巨大的 .cpk 包而卡住 5 秒,这个值也会实诚地增加 5 秒。
-
timeSinceStartup
cs
public double TimeSinceStartup { get; private set; }
使用double
-
浮点数漂移 (Floating-point Drift):在 3D 引擎中,随着运行时间增加(比如玩家玩了 50 小时),float 的精度会显著下降。
-
后果:如果使用 float 累加时间,游戏后期的动画会出现微小的抖动(Jitter),或者计时器变得不准。double 能保证在极长运行时间内精度依然维持在微秒级。
cs
public void Tick(float deltaTime)
{
DeltaTime = deltaTime;
TimeSinceStartup += deltaTime;
}
在常规 Unity 开发中,开发者直接从 UnityEngine.Time.deltaTime 获取时间。但该项目选择通过 Tick 方法手动传入时间增量,具有以下深层用意:
逻辑与引擎的彻底解耦 (Decoupling)
-
物理内涵:该类不再被动依赖 Unity 的主循环(Main Loop)。
-
架构价值:开发者可以完全控制逻辑时间的流动。
-
暂停机制:通过停止调用 Tick,可以实现游戏逻辑的"硬暂停",而底层的渲染、UI 动画(如果基于 RealTimeSinceStartup)依然可以继续。
-
快进/慢动作:可以给 Tick 传入 Time.deltaTime * 2.0f 来实现全局倍速。
-
ModelRenderer
虽然 .mov 在现代常见为视频格式,但在《仙三》的资源体系中,它是一个纯粹的骨骼动画指令文件。
文件格式定义
1. .msh (Mesh - 网格模型文件)
-
物理本质 :存储的是角色的几何拓扑结构。
-
包含数据:顶点坐标(Positions)、法线(Normals)、骨骼权重(Bone Weights)以及骨骼索引(Bone Indices)。
-
角色:它是角色的**"肉体"**。它定义了角色长什么样,以及皮肤如何随骨骼拉伸。
-
C++ 类比 :等价于底层渲染中的 Vertex Buffer ,特别是支持 硬件蒙皮(Hardware Skinning) 的顶点声明。
2. .mtl (Material - 材质库文件)
-
物理本质 :一个描述表面属性的元数据文件。
-
包含数据:光照参数(环境、漫反射、高光)以及该角色引用了哪些 .tga 贴图。
-
角色:它是角色的**"皮肤/衣服"**。没有它,模型就是纯灰色的网格。
-
技术关联:.msh 文件中的不同子网格会对应 .mtl 中的不同材质槽位。
3. .mov (Movement - 骨骼动画数据)
-
物理本质:一个**关键帧序列(Keyframe Sequence)**数据库。
-
包含数据:每一根骨骼在不同时间点(Frame)的旋转(Quaternion)、平移(Translation)和缩放(Scale)。
-
角色:它是角色的**"灵魂/动作"**。它告诉骨骼在 1.0 秒时应该抬手,在 2.0 秒时应该迈步。
-
注意:它不包含任何像素或顶点,只包含变换矩阵的差值。
cs
public sealed class RenderMeshComponent
{
public Mesh Mesh { get; set; }
public StaticMeshRenderer MeshRenderer { get; set; }
public MeshDataBuffer MeshDataBuffer { get; set; }
}
由于Cvd是动态模型,储存了模型在不同时间点的顶点位置,所以需要一个组件来存储每一帧的变化。