简练的ecs实现
主要是代替Unity的EC来使用
- 稀疏集实现,增删组件会很快,遍历稍慢
- 轻量的实体,8字节结构体
- struct和class都可作为组件,并支持池化
- 支持单例组件
- System有FixedUpdate、Update
- 简洁的api
网络实体复制
1. 实体状态内存布局
- 固定的实体网络内存大小(容器、字符串也是固定最大容量),但支持动态增删网络组件
- 每种类型的实体放在一个内存块里,且支持动态扩容
- 组件字段最小按4字节对齐,且都为非引用类型
快照内存布局
internal sealed class Snapshot
{
public int HeaderSize { get; private set; }
public int BodySize { get; private set; }
public int BufferSize => BodySize + HeaderSize;
private byte[] _buffer;
public Snapshot(int headerSize, int snapshotSize)
{
...
}
public NetSnapshotWriter GetHeaderWriter() => new(_buffer.AsSpan(0, HeaderSize));
public NetSnapshotWriter GetBodyWriter() => new(_buffer.AsSpan(HeaderSize, BodySize));
public NetSnapshotReader GetHeaderReader() => new(_buffer.AsSpan(0, HeaderSize));
public NetSnapshotReader GetBodyReader() => new(_buffer.AsSpan(HeaderSize, BodySize));
public void CopyTo(Snapshot to)
{
...
}
public void EnsureCapacity(int headerSize, int bodySize)
{
// memory layout
// | HeaderSize | BodySize |
...
}
public Span<byte> AsSpan()
{
return _buffer.AsSpan(0, BufferSize); // incase 返回实际有效长度
}
public Span<T> AsSpan<T>() where T : unmanaged
{
return MemoryMarshal.Cast<byte, T>(AsSpan());
}
}
2. 增量压缩
因为每个实体的状态内存大小是固定的,所以对内存逐int进行增量打包,配合zigzag、varint等压缩,可以实现实体数据无变化就状态包发送,连续变化时数据量也通常较小
内存增量压缩
public static void DeltaPack(ReadOnlySpan<int> lst, ReadOnlySpan<int> cur, ref NetBitWriter writer)
{
XLog.Assert(cur.Length == lst.Length);
var count = 0u;
var headerWriter = writer;
writer.Write<uint>(count);
#if ENABLE_SIMD_DIFF && NET7_0_OR_GREATER
if (Vector128.IsHardwareAccelerated) // 128 最快
count = DeltaPack_Vector128(lst, cur, ref writer);
else
#endif
count = DeltaPack_Fallback(lst, cur, ref writer);
headerWriter.Write(count);
}
internal static uint DeltaPack_Fallback(ReadOnlySpan<int> lst, ReadOnlySpan<int> cur, ref NetBitWriter writer)
{
var count = 0u;
var words = cur.Length;
var index = 0;
for (int i = 0; i < words; i++)
{
var deltaValue = cur[i] - lst[i];
if (deltaValue != 0)
{
var offset = i - index;
writer.WritePackedUInt((uint)offset);
writer.WritePackedInt(deltaValue);
index = i;
count++;
}
}
return count;
}
public static void DeltaUnpack(Span<int> target, ref NetBitReader reader)
{
var count = reader.Read<uint>();
uint index = 0;
while (count-- > 0)
{
index += reader.ReadPackedUInt();
XLog.Assert(index < target.Length/*, $"DeltaUnpack out of range {index} >= {target.Length} {count}"*/); // 热路径,先屏蔽,因为unity不支持内插字符串处理器,总是会生成这个字符串
target[(int)index] += reader.ReadPackedInt();
}
}
3. 状态内存、网络包的序列化
- 使用静态接口方法,减少虚函数调用
- ref struct实现,限制在栈上分配
- 使用ref托管指针直接操作内存
- 使用Span安全、高效的操作内存
- 针对array、list连续内存的容器,采取批量操作
- 类IBufferWriter设计,内部不再有任何分配
高效的状态序列化器
/// <summary>
/// 网络快照写入器
/// <code>为了性能,release不做任何检查,buffer是外部传入的,外部有责任保证大小合理</code>
/// </summary>
[DebuggerDisplay("Capacity={_length} Position={_position}")]
public ref partial struct NetSnapshotWriter
{
ref byte _buffer;
int _length;
int _position;
public readonly int Position => _position;
public NetSnapshotWriter(Span<byte> buffer)
{
...
}
public void Write<T>(T value) where T : unmanaged
{
ref var dst = ref _buffer;
Unsafe.WriteUnaligned(ref dst, value);
Advance(Unsafe.SizeOf<T>());
}
public void WriteArray<T>(ref T[] value, int capacity) where T : unmanaged
{
var length = Math.Min(value?.Length ?? 0, capacity);
Write(length);
if (length > 0)
{
var size = Unsafe.SizeOf<T>() * length;
ref var src = ref Unsafe.As<T, byte>(ref FUnsafeUtility.GetArrayDataReference(value));
ref var dst = ref _buffer;
Unsafe.CopyBlockUnaligned(ref dst, ref src, (uint)size);
}
Advance(Unsafe.SizeOf<T>() * capacity);
}
public void WriteList<T>(ref List<T> value, int capacity) where T : unmanaged
{
...
}
public void Advance(int count)
{
...
}
public void Seek(int position)
{
...
}
}
/// <summary>
/// 网络快照读取器
/// </summary>
[DebuggerDisplay("Capacity={_length} Position={_position} WorldId={WorldId}")]
public ref partial struct NetSnapshotReader
{
ref byte _buffer;
int _length;
int _position;
public readonly int Position => _position;
public NetSnapshotReader(ReadOnlySpan<byte> buffer)
{
...
}
public T Read<T>() where T : unmanaged
{
ref var src = ref _buffer;
var v = Unsafe.ReadUnaligned<T>(ref src);
Advance(Unsafe.SizeOf<T>());
return v;
}
public void ReadArray<T>(ref T[] value, int capacity) where T : unmanaged
{
var length = Read<int>();
if (value == null || length > value.Length)
value = new T[length];
if (length > 0)
{
var size = Unsafe.SizeOf<T>() * length;
ref var src = ref _buffer;
ref var dst = ref Unsafe.As<T, byte>(ref FUnsafeUtility.GetArrayDataReference(value));
Unsafe.CopyBlockUnaligned(ref dst, ref src, (uint)size);
// make safe
var init = (uint)Math.Max(value.Length * Unsafe.SizeOf<T>() - size, 0);
if (init > 0)
{
Unsafe.InitBlockUnaligned(ref Unsafe.Add(ref dst, size), 0, init);
}
}
Advance(Unsafe.SizeOf<T>() * capacity);
}
public void ReadList<T>(ref List<T> value, int capacity) where T : unmanaged
{
...
}
public void Advance(int count)
{
...
}
public void Seek(int position)
{
...
}
}
高效计时器
- 采用目标时间,而非剩余时间,避免每帧变化
- 基于优先队列实现,轮询消耗大幅降低
- 池化的结构,并有Version确保外部引用合法
API
public TimerHandle SetTimer(float interval, TimerCallback callback, object context = null, EcsEntity entity = default, bool loop = false, bool firstDelay = true)
{
XLog.Assert(callback != null);
int index;
int version;
if (_nfreelist == 0)
{
if (_ntimers == _timers.Length)
{
var newcapacity = Math.Max(1, _ntimers * 2);
Array.Resize(ref _timers, newcapacity);
Array.Resize(ref _versions, newcapacity);
}
index = _ntimers++;
version = _versions[index] = 1;
}
else
{
index = _freelist[--_nfreelist];
ref var v = ref _versions[index];
version = v = -v;
}
interval = GetInterval(interval);
ref var t = ref _timers[index];
t.Handle = new(index, version);
t.Loop = loop;
t.Interval = interval;
t.Callback = callback;
t.Context = context;
t.Entity = entity;
t.Target = CreateTickTimer(interval);
_activetimers.Enqueue(index, t.Target);
if (!firstDelay)
{
var args = new TimerArgs(_world, context, entity, t.Handle);
callback.Invoke(args);
}
return t.Handle;
}
public void UpdateTimer(TimerHandle handle, float interval)
{
ref var t = ref GetTimer(handle);
// 需要更新优先级
var removed = _activetimers.Remove(handle.Index, out _, out _);
XLog.Assert(removed);
interval = GetInterval(interval);
t.Interval = interval;
t.Target = CreateTickTimer(t.Interval);
_activetimers.Enqueue(handle.Index, t.Target);
}
public bool HasTimer(TimerHandle handle)
{
var index = handle.Index;
if (index > 0 && index < _ntimers)
{
return _versions[index] == handle.Version;
}
return false;
}
public ref Timer GetTimer(TimerHandle handle)
{
var index = handle.Index;
XLog.Assert(index > 0 && index < _ntimers);
XLog.Assert(_versions[index] == handle.Version);
return ref _timers[index];
}
配置资源
自定义的数据配置文件AssetObject,用于描述复杂逻辑配置,主要给服务器使用,类似Unity的ScriptableObject
- 支持多态序列化(json)
- 支持AssetObject间的引用
配置样例
{
"$id": "1",
"$type": "Moon.Gameplay.AbilitySystem.TriggerMap, Moon.AbilitySystem",
"Triggers": [
1049013026812243513,
3804198895876677490,
1436854693576817908,
3905961207146176956
],
"Guid": 2939160978917940160,
"name": "Assets/Raw/UAsset/Trigger/level_1.uasset",
"hideFlags": 0
}
开发效率和运行效率
通过c#的SourceGenerator生成模板代码,既能提高开发效率,又能保证运行效率
使用范例
[Networkable]
partial struct TestComp : IEcsComponent
{
[Networked]
public EcsEntity Entity;
[Networked]
public byte Byte;
}
// <auto-generated/>
partial struct TestComp : INetworkable<TestComp>
{
public unsafe static int StateSize { get; } = 0
+ /*Entity*/ Moon.Entities.EcsEntity.StateSize
+ /*Byte*/ Replicator.Size<byte>();
#if DEBUG
static string DebugStateSize = $"[StateSize] \nEntity:{Moon.Entities.EcsEntity.StateSize}\nByte:{Replicator.Size<byte>()}";
#endif
public static void OnWrite(ref NetSnapshotWriter writer, ref TestComp value)
{
#if false
var exist = value != null;
writer.WriteValue(ref exist);
if (!exist)
{
writer.Advance(TestComp.StateSize - 4);
goto LABEL;
}
#endif
writer.WriteValue(ref value.@Entity);
writer.WriteValue(ref value.@Byte);
LABEL:
OnPostWrite(ref value, writer.World);
}
static partial void OnPostWrite(ref TestComp value, EcsWorld world);
public static void OnRead(ref NetSnapshotReader reader, ref TestComp value)
{
#if false
Unsafe.SkipInit(out bool exist);
reader.ReadValue(ref exist);
if (!exist)
{
reader.Advance(TestComp.StateSize - 4);
return;
}
value ??= new();
#endif
var __old_Entity__ = value.@Entity;
var __old_Byte__ = value.@Byte;
reader.ReadValue(ref value.@Entity);
reader.ReadValue(ref value.@Byte);
OnRep_Entity(ref __old_Entity__, ref value);
OnRep_Byte(ref __old_Byte__, ref value);
}
static partial void OnRep_Entity(ref Moon.Entities.EcsEntity @old, ref TestComp @value);
static partial void OnRep_Byte(ref byte @old, ref TestComp @value);
static TestComp()
{
#if !NET7_0_OR_GREATER || DEBUG
Networkable<TestComp, TestComp>.StateSize = TestComp.StateSize;
Networkable<TestComp, TestComp>.OnRead = TestComp.OnRead;
Networkable<TestComp, TestComp>.OnWrite = TestComp.OnWrite;
#endif
}
}}
标签一览
using System;
namespace Moon.Entities
{
/// <summary>
/// 需要网络序列化的对象,打上此标签
/// <code>
/// eg.
///
/// [Networkable]
/// public partial struct Component
/// {
/// [Networked, Accuracy(100)]
/// public Vector3 Value;
/// }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct)]
public sealed class NetworkableAttribute : Attribute
{ }
/// <summary>
/// 需要网络复制的字段,打上此标签
/// <code>
/// eg.
///
/// [Networkable]
/// public partial struct Component
/// {
/// [Networked]
/// public int Value;
/// }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class NetworkedAttribute : Attribute
{ }
/// <summary>
/// 浮点数压缩,只对 float Vector2/3/4 Quaternion 生效
/// <code>需配合<see cref="NetworkedAttribute"/>使用</code>
/// <code>
/// eg.
///
/// [Networkable]
/// public partial struct Component
/// {
/// [Networked, Accuracy(100)]
/// public Vector3 Value;
/// }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class AccuracyAttribute : Attribute
{
public const int DefaultAccuracy = 100;
public int Accuracy { get; private set; }
public AccuracyAttribute(int accuracy = DefaultAccuracy)
{
Accuracy = accuracy;
}
}
/// <summary>
/// 指定复制容器的大小,支持 <see cref="System.Collections.Generic.List{T}"/>、<see cref="System.Array"/>
/// <code>需配合<see cref="NetworkedAttribute"/>使用</code>
/// <code>
/// eg.
///
/// [Networkable]
/// public partial struct Component
/// {
/// [Networked, Capacity(16), Accuracy(100)]
/// public Vector3[] Value;
/// }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public sealed class CapacityAttribute : Attribute
{
public int Capacity { get; private set; }
public CapacityAttribute(int capacity)
{
Capacity = capacity;
}
}
/// <summary>
/// 决定网络实体上的哪些组件需要复制
/// <code>
/// eg.
///
/// [Replicator(
/// typeof(Position),
/// typeof(Rotation),
/// typeof(Trigger)
/// )]
/// [ReplicatorOverride(
/// typeof(Position_Player),
/// typeof(Rotation_Player)
/// )]
/// sealed partial class SampleReplicator
/// {
/// // ...
/// }
/// </code>
///
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class ReplicatorAttribute : Attribute
{
public int Capacity { get; private set; }
public Type[] CompTypes { get; private set; }
public ReplicatorAttribute(int capacity, params Type[] compTypes)
{
Capacity = capacity;
CompTypes = compTypes;
}
}
/// <summary>
/// 重写网络组件的复制逻辑
/// <code>在 <see cref="NetCode.Replicator"/> 上使用, <see cref="FormatterTypes"/> 需要继承 <see cref="NetCode.INetworkable{T}"/></code>
/// <code>
/// eg.
///
/// [Replicator(
/// typeof(Position),
/// typeof(Rotation),
/// typeof(Trigger)
/// )]
/// [ReplicatorOverride(
/// typeof(Position_Player),
/// typeof(Rotation_Player)
/// )]
/// sealed partial class SampleReplicator
/// {
/// // ...
/// }
/// </code>
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class ReplicatorOverrideAttribute : Attribute
{
public Type[] FormatterTypes { get; private set; }
public ReplicatorOverrideAttribute(params Type[] formatterTypes)
{
FormatterTypes = formatterTypes;
}
}
/// <summary>
/// rpc方法,打上此标签
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class RpcAttribute : Attribute
{ }
/// <summary>
/// AssetObject,打上此标签,即可自动实现<see cref="NetCode.INetworkable{T}"/>接口
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class NetAssetObjectAttribute : Attribute
{ }
}
技能系统
- AttributeSet:角色属性管理模块,统一维护数值体系。
- Effect:与 Gameplay 解耦,提供事件机制与属性修改能力,支持灵活的逻辑扩展。
- Motion:类时间线(Timeline)机制,负责组织各类游戏逻辑的执行流程。
- Projectile:子弹系统的抽象层,统一处理弹道与命中逻辑。
帧同步版本实现(旧) 技能系统实现00 概况
游戏AI
数据驱动多态,多个实体共享一份配置文件,运行时数据保存在实体组件上
- UtilityAI,基于效用理论
- BehaviourTree,事件驱动模型,减少tick次数,提高响应速度
Recast 寻路
- 基于 DotRecast 实现,针对内存占用进行了专项优化。
1000个Agent
Bepu2 物理引擎
- 基于 ECS 架构封装常用物理接口,便于上层业务调用。
简单调试窗口
- 绘制 NavMesh 可视化信息。
- 绘制碰撞体形状与物理调试信息。
- 提供实体面板,便于查看与调试运行时数据。
单元测试覆盖
- 为基础模块编写单元测试,保障核心逻辑的迭代稳定性和可靠性。