简练的ecs实现

主要是代替Unity的EC来使用

  1. 稀疏集实现,增删组件会很快,遍历稍慢
  2. 轻量的实体,8字节结构体
  3. struct和class都可作为组件,并支持池化
  4. 支持单例组件
  5. System有FixedUpdate、Update
  6. 简洁的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. 状态内存、网络包的序列化
  1. 使用静态接口方法,减少虚函数调用
  2. ref struct实现,限制在栈上分配
  3. 使用ref托管指针直接操作内存
  4. 使用Span安全、高效的操作内存
  5. 针对array、list连续内存的容器,采取批量操作
  6. 类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)
    {
        ...
    }
}

高效计时器

  1. 采用目标时间,而非剩余时间,避免每帧变化
  2. 基于优先队列实现,轮询消耗大幅降低
  3. 池化的结构,并有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

  1. 支持多态序列化(json)
  2. 支持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 可视化信息。
  • 绘制碰撞体形状与物理调试信息。
  • 提供实体面板,便于查看与调试运行时数据。

单元测试覆盖

  • 为基础模块编写单元测试,保障核心逻辑的迭代稳定性和可靠性。