实现定时器并不复杂,就是写个类存放回调,再写个类来统一管理这些回调类。但是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里每帧自己计时来执行回调,所有人都得处理好这些细节,这样一点也不好用,所以自己实现一个统一管理的定时器会方便很多。