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>*/Callback FunctionPointer;
        internal readonly IClockExecutor Executor;

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

        // 毫秒数
        internal double StartTime;

        // 毫秒
        internal readonly int Interval;

        public Clock(ClockHandle handle, /*FunctionPointer<Callback>*/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<Callback> GetBurstedFunction(Callback executor) {
        //     // return BurstCompiler.CompileFunctionPointer<Callback>(executor);
        //     return new FunctionPointer<Callback>(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里每帧自己计时来执行回调,所有人都得处理好这些细节,这样一点也不好用,所以自己实现一个统一管理的定时器会方便很多。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/38017.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

C#使用DataGridView模拟绘图

接到一个需求&#xff0c;绘制一个水管线的图片&#xff0c;这种管线可以有12种分段方法&#xff0c;最后将这12种分段方法合并后在一条水管线上展示&#xff0c;要求&#xff1a; ⒈支持分段的属性展示&#xff1b; ⒉要求每个分段都能清晰展示&#xff0c;分段数在0&#xff…

从CPU缓存结构到原子操作

文章目录 一、CPU缓存结构1.1 CPU的多级缓存1.2 Cache Line 二、写回策略三、缓存一致性问题及解决方案3.1 缓存一致性问题3.2 解决方案3.2.1 总线嗅探3.2.2 事务的串行化3.2.3 MESI 四、原子操作4.1 什么是原子操作4.2 c 标准库的原子类型4.2.1 atomic<T\>4.2.2 is_lock…

Python(四):Pycharm的安装配置

❤️ 专栏简介&#xff1a;本专栏记录了我个人从零开始学习Python编程的过程。在这个专栏中&#xff0c;我将分享我在学习Python的过程中的学习笔记、学习路线以及各个知识点。 ☀️ 专栏适用人群 &#xff1a;本专栏适用于希望学习Python编程的初学者和有一定编程基础的人。无…

【基于FPGA的芯片设计】32位RISC-V存储器

实验板卡&#xff1a;xc7a100tlc sg324-2L&#xff0c;共20个开关 实验要求

RabbitMQ常用工作模式+整合springboot

目录 1.MQ的相关概念 1.1 什么是MQ消息中间件 1.2 为什么使用MQ (1) 应用解耦 (2) 异步提速 (3)削峰填谷 1.3 使用MQ的劣势 1.4 常见的MQ组件​​​​​​​ 2. RabbitMQ的概述 2.1 RabbitMQ的概念 2.2 RabbitMQ的原理 2.3 安装RabbitMQ 3. RabbitMQ 的工作模式…

【NLP】Word2Vec原理和认识

一、介绍 Word2Vec是NLP领域的最新突破。Tomas Mikolov是捷克计算机科学家&#xff0c;目前是CIIRC&#xff08;捷克信息学&#xff0c;机器人和控制论研究所&#xff09;的研究员&#xff0c;是word2vec研究和实施的主要贡献者之一。词嵌入是解决NLP中许多问题不可或缺的一部分…

基于B/S架构SaaS服务的实验室信息系统(LIS)

实验室信息系统LIS源码 实验室信息系统&#xff08;Laboratory Information System&#xff09;&#xff0c;简称LIS&#xff0c;是一个全面基于网络化应用&#xff0c;能够帮助用户按照规范内容和规范流程进行多角色、多层次检验信息及资源管理的系统。通过条码管理系统从HIS…

云计算的学习(三)

三、云计算中的网络基础知识 1.虚拟化中网络的架构 1.1虚拟化中网络的架构 二层交换机作为接入交换机使用&#xff0c;三层交换机可以作为汇聚交换机或核心交换机&#xff0c;在抛开网络安全设备时&#xff0c;路由器直接连接在互联网上。 1.2广播和单播 物理服务器内部主要…

Iceberg从入门到精通系列之十七:Apache InLong往Iceberg同步数据

Iceberg从入门到精通系列之十七&#xff1a;Apache InLong往Iceberg同步数据 一、概览二、版本支持三、依赖项四、SQL API 用法五、多表写入六、动态表名映射七、动态建库、建表八、动态schema变更九、Iceberg Load 节点参数十、数据类型映射 一、概览 Apache Iceberg是一种用…

Flutter系列文章-Flutter环境搭建和Dart基础

Flutter是Google推出的一个开源的、高性能的移动应用开发框架&#xff0c;可以用一套代码库开发Android和iOS应用。Dart则是Flutter所使用的编程语言。让我们来看看如何搭建Flutter开发环境&#xff0c;并了解Dart语言的基础知识。 一、Flutter环境搭建 1. 安装Flutter SDK …

springboot+ElasticSearch+Logstash+Kibana实现日志采集ELK

ElasticSearchLogstashKibana日志管理 一、什么是ELK? ELK是Elasticsearch、Logstash、Kibana的简称&#xff0c;这三者是核心套件&#xff0c;但并非全部。一般情况下我们可以把日志保存在日志文件当中&#xff0c;也可以把日志存入数据库当中。但随着业务量的增加&#xf…

搭建 Java 部署环境,部署 Web 项目到 Linux

为了进行部署&#xff0c;把写好的 java web 程序放到 Linux 上&#xff0c;需要先把对应的依赖的软件 (环境) 搭建好&#xff0c;安装一些必要的软件程序 JDKTomcatMySqL jdk 直接使用包管理器进行安装(基于yum安装) 一、yum 1、认识 yum yum (Yellow dog Updater, Modified…

6. Java + Selenium 环境搭建

前提&#xff1a;Java 版本最低要求为 8&#xff1b;推荐使用 chrome 浏览器 chrome Java 1. 下载 chrome 浏览器&#xff08;推荐&#xff09; 2. 查看 chrome 浏览器版本 重点记住前两位即可。 3. 下载 chrome 浏览器驱动 下载链接&#xff1a; https://chromedriver.…

我爱学QT-仿写智能家居界面 上 中 下

学习链接&#xff1a; 仿写一个智能家居界面&#xff08;上&#xff09;_哔哩哔哩_bilibili 上 给QT工程添加资源文件 在这里 然后选这个&#xff0c;choose后会有起名&#xff0c;之一千万不能是中文&#xff0c;要不就等报错吧 然后把你要添加的图片托到文件夹下&#xf…

云计算基础教程(第2版)笔记——基础篇与技术篇介绍

文章目录 前言 第一篇 基础篇 一 绪论 1.1 云计算的概念以及特征 1.1.1云计算的基本概念 1.1.2云计算的基本特征 1.2 云计算发展简史 1.3 三种业务模式介绍 1. 基础设施即服务&#xff08;IaaS&#xff09; 2. 平台即服务&#xff08;PaaS&#xff09; 3. 软…

F#奇妙游(12):并行编程与π

核越多&#xff0c;越快乐 多核CPU对于计算机程序的开发带来了很大的挑战&#xff0c;但是也带来了很大的机遇。在多核CPU上&#xff0c;程序的性能可以通过并行化来提升&#xff0c;但是并行化的难度也随之提升。本文将介绍多核CPU的基本概念&#xff0c;以及如何在多核CPU上…

OpenCv图像基本变换

目录 一、图像翻转 二、图像旋转 三、仿射变换之平移 四、仿射变换之获取变换矩阵 五、仿射变换之透视变换 一、图像翻转 图像翻转不等同于旋转&#xff0c;类似于一些视频的拍摄&#xff0c;拍摄后实际是左右颠倒的&#xff0c;通过图像翻转可进行还原 案例代码如下: …

Android之Intent

意图介绍 一个意图(Intent)对象包含了目标组件、动作、数据、类别、附加数据、标志六个部分。 目标组件 目标组件可以帮助应用发送显式意图调用请求。在创建Intent时&#xff0c;可以通过setComponent方法来设置一个组件&#xff0c;如&#xff1a; //设置组件 intent.setC…

Linux - CentOS 二进制安装 MySQL 8.0.31(非常实用)

一、下载 mysql-8.0.31-linux-glibc2.12-x86_64.tar.xz 下载地址&#xff1a;MySQL :: Download MySQL Community Server (Archived Versions) 具体如下图所示&#xff1a; 二、将 mysql-8.0.31-linux-glibc2.12-x86_64.tar.xz 放入到服务器的 /usr/local &#xff08;路径可…

C++万字自学笔记

[TOC] 一、 C基础 C的IDE有CLion、Visual Studio、DEV C、eclipse等等&#xff0c;这里使用CLion进行学习。 0. C初识 0.1 第一个C程序 编写一个C程序总共分为4个步骤 创建项目创建文件编写代码运行程序 #include <iostream>int main() {using namespace std;cout…