Unity DOTS如何优雅地实现ECS框架下的定时器Timer系统(无对象池,零GC)

实现定时器并不复杂,就是写个类存放回调,再写个类来统一管理这些回调类。但是ECS写代码的方式变了,所以还是有些区别的。

实现过程中需要注意的几点:
1、由于IComponentData不能存放managed类型的数据,所以无法用常规的ECS方式来存放定时器数据,只能用一个NativeHashMap作为SystemBase里的一个字段来存数据了。

2、常规来说,NativeList肯定比NativeHashMap的内存占用小的,但是用过后发现List要在遍历过程中删除元素实在是不好看,就只能用Map了。不过影响不大。

3、试过回调的保存用c#官方的delegate,但是打包WASM到浏览器上运行,报了个错“function signature mismatch”。没有深入处理,直接放弃了该用法。也试过用Unity.Burst自带的FunctionPointer函数指针,但是有个限制,函数不能作为类的实例方法,只能是static的静态方法,而static方法遍历IComponentData很不方便,也只能放弃。

4、目前的实现方法有个小小的瑕疵,但不影响功能。就是每个SystemBase里只能实现一个定时器回调,如果需要实现一个每2秒执行一次的回调,和一个每1秒执行一次的回调,就不行,只能分开在两个SystemBase里。由于SystemBase是class来的,太多的话,创建和销毁会涉及到GC,对性能有一点影响。

不过新版的Entities有了ISystem,是struct类型的,可以随便创建删除,且可以完全BurstCompile的。ProjectTiny无法更新到新版,只能先这样用着。用法跟MonoBehavior实现的差不多,可以添加任意间隔,重复执行的回调,也可以添加指定执行几次或间隔执行一次的回调。看代码:

using Unity.Burst;
using Unity.Collections;
using Unity.Entities;
using System.Runtime.CompilerServices;
using System;

namespace wangtal.ClockSystem {
    #pragma warning disable 660, 661
    [BurstCompile]
    public readonly struct ClockHandle : IEquatable<ClockHandle>{
        private static uint _uid = 1;
        internal static uint uid => _uid++;
        public static ClockHandle Null = new ClockHandle(0);

        private readonly uint Id;

        internal ClockHandle(uint id) {
            Id = id;
        }

        bool IEquatable<ClockHandle>.Equals(Tiny3D.ClockHandle other) {
            return this == other;
        }

        public static bool operator ==(ClockHandle left, ClockHandle right) {
            return left.Id == right.Id;
        }

        public static bool operator !=(ClockHandle left, ClockHandle right)
        {
            return left.Id != right.Id;
        }

        public override string ToString() {
            return $"id:{Id}";
        }

        public override int GetHashCode()
        {
            return Id.GetHashCode();
        }
    }
    #pragma warning restore 660, 661

    public interface IClockExecutor {
        void Execute();
    }

    // public delegate void Callback(/*EntityManager entityManager*/);

    [BurstCompile]
    internal struct Clock {
        internal readonly ClockHandle Handle;

        // function signature mismatch
        // internal readonly /*FunctionPointer*/Callback FunctionPointer;
        internal readonly IClockExecutor Executor;

        // 负数代表无限循环,0代表结束循环了,大于0代表重复执行次数
        internal int Repeat;

        // 毫秒数
        internal double StartTime;

        // 毫秒
        internal readonly int Interval;

        public Clock(ClockHandle handle, /*FunctionPointer*/IClockExecutor executor, int repeat, double startTime, int interval) {
            Handle = handle;
            Executor = executor;
            Repeat = repeat;
            StartTime = startTime;
            Interval = interval;
        }
    }

    public class ClockSystem : SystemBase
    {
        private NativeHashMap<ClockHandle, Clock> clocks;

        protected override void OnCreate()
        {
            base.OnCreate();
            clocks = new NativeHashMap<ClockHandle, Clock>(10, Allocator.Persistent);
        }

        protected override void OnUpdate()
        {
            if (clocks.Count() <= 0) {
                return;
            }

            var handles = clocks.GetKeyArray(Allocator.Temp);
            foreach (var handle in handles) {

                if (clocks.TryGetValue(handle, out var clock)) {
                    if (clock.Repeat == 0) {
                        clocks.Remove(clock.Handle);
                        // Unity.Tiny.Debug.Log("移除定时器:" + clock.Handle);
                        continue;
                    }

                    if ((Time.ElapsedTime * 1000) >= (clock.StartTime + clock.Interval)) {
                        clock.Executor.Execute();

                        clock.StartTime = Time.ElapsedTime * 1000;
                        if (clock.Repeat > 0) {
                            clock.Repeat--;
                        }
                        clocks[handle] = clock;
                    }
                }
            }
            handles.Dispose();
        }

        protected override void OnDestroy()
        {
            clocks.Dispose();
            base.OnDestroy();
        }

        public ClockHandle AddOnce(IClockExecutor executor, int interval) {
            return AddRepeat(executor, 1, interval);
        }

        public ClockHandle AddLoop(IClockExecutor executor, int interval) {
            return Add(executor, -1, interval);
        }

        public ClockHandle AddRepeat(IClockExecutor executor, int repeat, int interval) {
            return Add(executor, repeat, interval);
        }

        public bool Valid(in ClockHandle handle) {
            if (handle == ClockHandle.Null) {
                return false;
            }
            return clocks.ContainsKey(handle);
        }

        public void Remove(ref ClockHandle handle) {
            if (clocks.ContainsKey(handle)) {
                Unity.Tiny.Debug.Log("移除!!!" + handle);
                clocks.Remove(handle);
                handle = ClockHandle.Null; // 定时器被清掉了,要重置掉外部的handle
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private ClockHandle Add(IClockExecutor executor, int repeat, int interval) {
            Unity.Tiny.Assertions.Assert.IsTrue(repeat != 0);
            var handle = GetClockHandle();
            Unity.Tiny.Debug.Log("创建定时器:" + handle);
            // var fp = GetBurstedFunction(executor);
            var startTime = Time.ElapsedTime * 1000;
            var clock = new Clock(handle, /*fp*/executor, repeat, startTime, interval);
            clocks.Add(handle, clock);
            return handle;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        private ClockHandle GetClockHandle() {
            var uid = ClockHandle.uid;
            for (uint i = 1; i <= uid; i++) { // 遍历一遍已经无效的ID,复用它
                var handle = new ClockHandle(i);
                if (!clocks.ContainsKey(handle)) {
                    return handle;
                }
            }

            Unity.Tiny.Assertions.Assert.IsTrue(false, "不应该走到这里");
            return new ClockHandle(ClockHandle.uid);
        }

        // 用函数指针会有问题,且会限制回调函数只能为static的。直接用delegate也会有问题,报错:function signature mismatch
        // [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
        // private FunctionPointer GetBurstedFunction(Callback executor) {
        //     // return BurstCompiler.CompileFunctionPointer(executor);
        //     return new FunctionPointer(Marshal.GetFunctionPointerForDelegate(executor));
        // }
    }
}

用法的示例代码:

public class TestLoopClock : SystemBase, IClockExecutor
    {
        private ClockSystem clockSystem;

        private int cnt;

        private ClockHandle timer;

        protected override void OnCreate()
        {
            base.OnCreate();
            clockSystem = World.GetOrCreateSystem<ClockSystem>();
            timer = clockSystem.AddLoop(this, 1000); // 每隔1秒就执行1次,不停执行
            cnt = 0;
        }
        
        void IClockExecutor.Execute() {
        	// 达到某些条件后,在回调里把定时器移除掉
            //if (cnt > 10) {
                //clockSystem.Remove(ref timer);
                //return;
            //}

            Entities.ForEach((Entity e, ref Unity.Tiny.Text.TextRenderer tr, in Unity.Tiny.UI.UIName uiName) =>
            {
                if (uiName.Name == "txtResolution")
                {
                    Unity.Tiny.Text.TextLayout.SetEntityTextRendererString(EntityManager, e, $"loop {cnt++}");
                }
            }).WithStructuralChanges().WithoutBurst().Run();
        }

        protected override void OnUpdate()
        {
        }
    }

    public class TestOnceClock : SystemBase, IClockExecutor
    {
        private ClockSystem clockSystem;

        private ClockHandle timer;

        protected override void OnCreate()
        {
            base.OnCreate();
            clockSystem = World.GetOrCreateSystem<ClockSystem>();
            timer = clockSystem.AddOnce(this, 3000); // 延迟3秒执行一次就停止
        }

        void IClockExecutor.Execute() {
            Entities.ForEach((Entity e, ref Unity.Tiny.Text.TextRenderer tr, in Unity.Tiny.UI.UIName uiName) =>
            {
                if (uiName.Name == "txtOnce")
                {
                    Unity.Tiny.Text.TextLayout.SetEntityTextRendererString(EntityManager, e, "Once END!");
                }
            }).WithStructuralChanges().WithoutBurst().Run();
        }

        protected override void OnUpdate()
        {
        }
    }

    public class TestRepeatClock : SystemBase, IClockExecutor
    {
        private ClockSystem clockSystem;

        private ClockHandle timer;

        private int cnt;

        protected override void OnCreate()
        {
            base.OnCreate();
            clockSystem = World.GetOrCreateSystem<ClockSystem>();
            timer = clockSystem.AddRepeat(this, 5, 1000); // 间隔1秒,执行5次停止
            cnt = 0;
        }

        void IClockExecutor.Execute() {
            Entities.ForEach((Entity e, ref Unity.Tiny.Text.TextRenderer tr, in Unity.Tiny.UI.UIName uiName) =>
            {
                if (uiName.Name == "txtRepeat")
                {
                    Unity.Tiny.Text.TextLayout.SetEntityTextRendererString(EntityManager, e, $"Repeat {cnt++}");
                }
            }).WithStructuralChanges().WithoutBurst().Run();
        }

        protected override void OnUpdate()
        {
        }
    }

打包WASM到浏览器上的运行效果:

由于ECS框架下没有协程,也没有InvokeRepeat这样的方法,如果在OnUpdate里每帧自己计时来执行回调,所有人都得处理好这些细节,这样一点也不好用,所以自己实现一个统一管理的定时器会方便很多。

你可能感兴趣的:(unity,DOTS,ECS,ProjectTiny,c#)