学习记录

Quantum3 是用的 ECS(Entity-Component-System) 做的底层架构,所以先简单的说一下这个系统的原理。
以前用面向对象这种方式做游戏的时候,游戏中的数据和行为都是绑定在游戏物体上的,所以每次行为驱动数据的时候都需要从对象这个类里面去找,在离散的内存中就非常耗时。
ECS 的架构设计就是面向数据和面向行为,一个实体身上附带了许多数据(Component),如果我想要这个实体做出什么动作就可以用系统(System)找到特定的实体,然后让他们执行特定的行为。
然后 Quantum 的帧同步基本原理就是,通过 FP(Fixed Point)定点数,来确保程序即使在不同平台运行也能保证数据的一致性。
在游戏运行的过程中,通过只同步玩家的操作,来进行确定性模拟,降低带宽成本。

Qtn

既然是 ECS 架构,那么肯定不能有类了,所以 Quantum “贴心” 的为我们写了一套用来配合结构体使用的工具,也就是 Qtn。
Qtn 是 Quantum 中特有的一种结构,类似于结构体,Quantum 附带的这一套结构会在你编辑完 Qtn 文件后,自动生成配套的 C# 结构体代码。比如如下:

  • 你在名为 Buff.qtn 的文件下创建了一个这样的结构体。
    BuffObj
  • 对应的,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++ 中,联合是一种特殊的数据类型,允许你在同一块内存位置存储不同的数据类型。所有成员共享同一段内存起点,修改其中一个成员会覆盖其他成员的值。联合的大小由其最大的成员决定。

  • 比如我定义了一个如下这样的联合体。
    union

  • 那么他在 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 没有声明指针