学习记录
Quantum3 是用的 ECS(Entity-Component-System) 做的底层架构,所以先简单的说一下这个系统的原理。
以前用面向对象这种方式做游戏的时候,游戏中的数据和行为都是绑定在游戏物体上的,所以每次行为驱动数据的时候都需要从对象这个类里面去找,在离散的内存中就非常耗时。
ECS 的架构设计就是面向数据和面向行为,一个实体身上附带了许多数据(Component),如果我想要这个实体做出什么动作就可以用系统(System)找到特定的实体,然后让他们执行特定的行为。
然后 Quantum 的帧同步基本原理就是,通过 FP(Fixed Point)定点数,来确保程序即使在不同平台运行也能保证数据的一致性。
在游戏运行的过程中,通过只同步玩家的操作,来进行确定性模拟,降低带宽成本。
Qtn
既然是 ECS 架构,那么肯定不能有类了,所以 Quantum “贴心” 的为我们写了一套用来配合结构体使用的工具,也就是 Qtn。
Qtn 是 Quantum 中特有的一种结构,类似于结构体,Quantum 附带的这一套结构会在你编辑完 Qtn 文件后,自动生成配套的 C# 结构体代码。比如如下:
- 你在名为 Buff.qtn 的文件下创建了一个这样的结构体。

- 对应的,Quantum 的结构就会在 Quantum.CodeGen.Core 中生成这样的结构体。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| [StructLayout(LayoutKind.Explicit)] public unsafe partial struct BuffObj { public const Int32 SIZE = 1128; public const Int32 ALIGNMENT = 8; [FieldOffset(0)] public Int32 buffId; [FieldOffset(32)] public EntityRef caster; [FieldOffset(40)] public EntityRef owner; [FieldOffset(28)] [AllocateOnComponentAdded()] [FreeOnComponentRemoved()] [HideInInspector()] public QDictionaryPtr<Int32, Int32> tags; [FieldOffset(1072)] public DataContainer data; public override readonly Int32 GetHashCode() { unchecked { var hash = 11197; hash = hash * 31 + buffId.GetHashCode(); hash = hash * 31 + caster.GetHashCode(); hash = hash * 31 + owner.GetHashCode(); hash = hash * 31 + data.GetHashCode(); return hash; } } public void ClearPointers(FrameBase f, EntityRef entity) { if (tags != default) f.FreeDictionary(ref tags); } public void AllocatePointers(FrameBase f, EntityRef entity) { f.TryAllocateDictionary(ref tags); } public static void Serialize(void* ptr, FrameSerializer serializer) { var p = (BuffObj*)ptr; serializer.Stream.Serialize(&p->buffId); EntityRef.Serialize(&p->caster, serializer); EntityRef.Serialize(&p->owner, serializer); Quantum.DataContainer.Serialize(&p->data, serializer); } }
|
在 Quantum 的文档中,这样的结构体会附带有 [FieldOffset(0)] 这样的属性,这样是为了内存堆砌。
而假设你使用的是 Union 这样的联合体,那么情况会有一些不一样。
事实上,在 C# 中并没有 Union 这样的定义,Union 是来自 C++ 中的定义。
在 C/C++ 中,联合是一种特殊的数据类型,允许你在同一块内存位置存储不同的数据类型。所有成员共享同一段内存起点,修改其中一个成员会覆盖其他成员的值。联合的大小由其最大的成员决定。
比如我定义了一个如下这样的联合体。

那么他在 Quantum C# 端生成的代买就是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88
| [StructLayout(LayoutKind.Explicit)] [Union()] public unsafe partial struct TLNode { public const Int32 SIZE = 104; public const Int32 ALIGNMENT = 8; [FieldOffset(0)] private Int32 _field_used_; [FieldOffset(8)] [FieldOverlap(8)] [FramePrinter.PrintIf("_field_used_", Quantum.TLNode.LOG)] private TLNode_Log _Log; [FieldOffset(8)] [FieldOverlap(8)] [FramePrinter.PrintIf("_field_used_", Quantum.TLNode.ADDBUFFTOCASTER)] private TLNode_AddBuffToCaster _AddBuffToCaster; [FieldOffset(8)] [FieldOverlap(8)] [FramePrinter.PrintIf("_field_used_", Quantum.TLNode.ADDBUFFTOTARGET)] private TLNode_AddBuffToTarget _AddBuffToTarget; public const Int32 LOG = 1; public const Int32 ADDBUFFTOCASTER = 2; public const Int32 ADDBUFFTOTARGET = 3; public readonly Int32 Field { get { return _field_used_; } } public TLNode_Log* Log { get { fixed (TLNode_Log* p = &_Log) { if (_field_used_ != LOG) { Native.Utils.Clear(p, 64); _field_used_ = LOG; } return p; } } } public TLNode_AddBuffToCaster* AddBuffToCaster { get { fixed (TLNode_AddBuffToCaster* p = &_AddBuffToCaster) { if (_field_used_ != ADDBUFFTOCASTER) { Native.Utils.Clear(p, 96); _field_used_ = ADDBUFFTOCASTER; } return p; } } } public TLNode_AddBuffToTarget* AddBuffToTarget { get { fixed (TLNode_AddBuffToTarget* p = &_AddBuffToTarget) { if (_field_used_ != ADDBUFFTOTARGET) { Native.Utils.Clear(p, 96); _field_used_ = ADDBUFFTOTARGET; } return p; } } } public override readonly Int32 GetHashCode() { unchecked { var hash = 11701; hash = hash * 31 + _field_used_.GetHashCode(); hash = hash * 31 + _Log.GetHashCode(); hash = hash * 31 + _AddBuffToCaster.GetHashCode(); hash = hash * 31 + _AddBuffToTarget.GetHashCode(); return hash; } } public static void Serialize(void* ptr, FrameSerializer serializer) { var p = (TLNode*)ptr; if (serializer.InputMode) { serializer.Stream.SerializeBuffer((byte*)p, Quantum.TLNode.SIZE); return; } serializer.Stream.Serialize(&p->_field_used_); if (p->_field_used_ == ADDBUFFTOCASTER) { Quantum.TLNode_AddBuffToCaster.Serialize(&p->_AddBuffToCaster, serializer); } if (p->_field_used_ == ADDBUFFTOTARGET) { Quantum.TLNode_AddBuffToTarget.Serialize(&p->_AddBuffToTarget, serializer); } if (p->_field_used_ == LOG) { Quantum.TLNode_Log.Serialize(&p->_Log, serializer); } } }
|
- 可以看到 Union 和 普通的 Struct 最大的区别就是在属性 [FieldOffset(8)] 上有区别
- 普通的 Struct 会将每个属性依次在内存中取地址然后实现,而 Union 实际上只会在实例化的时候选择一种类型实例化,类似于 Class 中的多态。
自定义加载器
Quantum 的配置都是默认用 Resources 加载的,我们一般用 Bundle 肯定是不合适的,所以写一个自定义的 Loader。
- 写一个 Loader Attribute 继承自 QuantumGlobalScriptableObjectSourceAttribute
- 然后
- 再然后在注册的时候该 Loader 读取方式,在 Quantum Unity Runtime.Assembly.Attributes 。
常见错误
系统中的 filter 没有声明指针