Unity DOTS纯ECS写一个类似UGUI的EventSystem(实现IPointerDownHandler/IPointerUpHandler/IBeginDragHandler等)

这篇文章主要展示下在Unity的ProjectTiny环境中用纯ECS代码实现UGUI里的EventSystem,包括常用的IPointerDownHandler、IPointerUpHandler、IBeginDragHandler、IDragHandler、IEndDragHandler、IPointerEnterHandler、IPointerExitHandler、IPointerClickHandler等功能。

虽然ProjectTiny里有自带的Button等UI,但是它的点击事件并不是通用的,无法随便给想要的2D entity加上点击事件。不过ProjectTiny提供了最基本的InputSystem,可以获得输入数据。根据这些数据,再处理下,就可以实现UGUI里的那几个Handler了,然后就可以让任意的2D entity(带RectTransform的entity)响应按下抬起点击滑动等事件,还有游戏中常用的鼠标滑动到物品icon上就弹出一个tip,滑出icon后tip消失(也就是IPointerEnter/IPointerExit的功能)。

需要注意一点,由于IComponentData中不能存放NativeList这种Container,所以用的是IBufferElementData,跟数组用法差不多,可以动态增长,但是增删比较麻烦。也可以直接在System里用一个NativeList来存放数据,但是这样不够ECS,而且数据不多,遍历几遍数组对性能影响不会太大。

看代码。代码只是展示一下实现方法,可以继续优化。

using Unity.Entities;
using Unity.Jobs;
using Unity.Mathematics;
using Unity.Transforms;
using Unity.Tiny.UI;
using Unity.Tiny;
using Unity.Tiny.Input;
using Unity.Tiny.Rendering;
using Unity.Collections;

namespace wangtal.EventSystem {
    public interface IPointerDownHandler {}

    public interface IPointerUpHandler {}

    public interface IPointerClickHandler {}

    public interface IPointerEnterHandler {}

    public interface IPointerExitHandler {}

    public interface IBeginDragHandler {}

    public interface IDragHandler {}

    public interface IEndDragHandler {}

    public interface ICancelHandler {}

    internal struct PointerEventData : IBufferElementData {
        internal enum TriggerState : byte {
            None = 0 << 0,
            PointerDown = 1 << 0,
            PointerUp = 1 << 1,
            PointerClick = 1 << 2,
            BeginDrag = 1 << 3,
            Drag = 1 << 4,
            EndDrag = 1 << 5,
            PointerEnter = 1 << 6,
            PointerExit = 1 << 7
        }

        public TriggerState State;

        public int PointerId;

        public Entity Target;

        // 当前位置
        public float2 Position;

        // 滑动间隔
        public float2 Delta;

        // 按下时的位置
        public float2 PressPosition;
    }

	public class InputSystemGroup : ComponentSystemGroup
    {
        const int MaxTriggerEvent = 10;

        protected override void OnCreate()
        {
            base.OnCreate();
            var entity = EntityManager.CreateEntity();
            var buffer = EntityManager.AddBuffer<PointerEventData>(entity);
            for (int i = 0; i < MaxTriggerEvent; i++) {
                PointerEventData data = new PointerEventData {
                    State = PointerEventData.TriggerState.None, PointerId = -1, Target = Entity.Null, Position = float2.zero, Delta = float2.zero, PressPosition = float2.zero
                };
                buffer.Add(data);
            }
        }
    }

	[UpdateInGroup(typeof(InputSystemGroup))]
    unsafe public class EventSystem : SystemBase
    {
        private ScreenToWorld screenToWorld;

        private InputSystem inputSystem;

        protected override void OnCreate()
        {
            base.OnCreate();

            screenToWorld = World.GetExistingSystem<ScreenToWorld>();
            inputSystem = World.GetExistingSystem<InputSystem>();
        }

        protected override void OnUpdate()
        {
            var input = inputSystem;
            Entities.ForEach((Entity entity, ref DynamicBuffer<PointerEventData> eventDatas) => {
                PointerEventData* p = (PointerEventData*)eventDatas.GetUnsafePtr();
                for (int i = 0; i < eventDatas.Length; i++, p++) {
                    ref var eventData = ref UnsafeUtility.AsRef<PointerEventData>(p);//eventDatas[i];
                    if (eventData.State == PointerEventData.TriggerState.PointerDown ||
                        eventData.State == PointerEventData.TriggerState.PointerUp ||
                        eventData.State == PointerEventData.TriggerState.BeginDrag ||
                        eventData.State == PointerEventData.TriggerState.EndDrag
                    ) {
                        // 按下只有一帧的时间,所以下一帧要清除状态
                        SetNone(p);
                    }
                }

                if (input.IsTouchSupported() && input.TouchCount() > 0) { // Touch
                    for (var i = 0; i < input.TouchCount(); i++)
                    {
                        var touch = input.GetTouch(i);
                        var inputPos = new float2(touch.x, touch.y);

                        switch (touch.phase)
                        {
                            case TouchState.Began:
                                OnDown(i, inputPos, ref eventDatas);
                                break;

                            case TouchState.Moved:
                                var inputDelta = new float2(touch.deltaX, touch.deltaY);
                                OnMove(i, inputPos, inputDelta, ref eventDatas);
                                break;

                            case TouchState.Stationary:
                                break;

                            case TouchState.Ended:
                                OnUp(i, inputPos, ref eventDatas);
                                break;

                            case TouchState.Canceled:
                                // OnInputCanceled(i);
                                break;
                        }
                    }
                } else if (input.IsMousePresent()) { // Mouse
                    var inputPos = input.GetInputPosition();

                    if (input.GetMouseButtonDown(0))
                    {
                        OnDown(-1, inputPos, ref eventDatas);
                    }
                    else if (input.GetMouseButton(0))
                    {
                        OnMove(-1, inputPos, input.GetInputDelta(), ref eventDatas);
                    }
                    else if (input.GetMouseButtonUp(0))
                    {
                        OnUp(-1, inputPos, ref eventDatas);
                    }
                }
            }).WithoutBurst().Run();
        }

        private void OnDown(int touchId, float2 inputPos, ref DynamicBuffer<PointerEventData> eventDatas) {
            var cam = GetSingleton<RectCanvasScaleWithCamera>().Camera;
            var target = GetHitEntity(screenToWorld.InputPosToWorldSpacePos(inputPos, 1, cam));

            if (target == Entity.Null) {
                return;
            }

            PointerEventData* p = (PointerEventData*)eventDatas.GetUnsafePtr();
            var bits = PointerEventData.TriggerState.PointerDown | PointerEventData.TriggerState.Drag | PointerEventData.TriggerState.BeginDrag;
            for (int i = 0; i < eventDatas.Length; i++, p++) {
                ref var eventData = ref UnsafeUtility.AsRef<PointerEventData>(p);

                if (eventData.Target == target && eventData.State == PointerEventData.TriggerState.PointerDown) {
                    // 已经有点击记录了,不处理多次点击,只处理第一个的
                    ClearBit(ref bits, PointerEventData.TriggerState.PointerDown);
                }

                if (eventData.Target == target && eventData.State == PointerEventData.TriggerState.BeginDrag) {
                    ClearBit(ref bits, PointerEventData.TriggerState.BeginDrag);
                }

                if (eventData.Target == target && eventData.State == PointerEventData.TriggerState.Drag) {
                    // 不用对比touchId,这样的效果是,第一个手指拖动时,再来一个手指拖动会没反应
                    ClearBit(ref bits, PointerEventData.TriggerState.Drag);
                }
            }

            for (int i = 0; i < eventDatas.Length; i++) {
                if (bits == 0) {
                    break;
                }

                if (eventDatas[i].State == PointerEventData.TriggerState.None) {
                    if ((bits & PointerEventData.TriggerState.PointerDown) != 0) {
                        var data = eventDatas[i];
                        data.State = PointerEventData.TriggerState.PointerDown;
                        data.Target = target;
                        data.PointerId = touchId;
                        data.PressPosition = inputPos;
                        eventDatas[i] = data;
                        ClearBit(ref bits, PointerEventData.TriggerState.PointerDown);
                    }
                }
                if (eventDatas[i].State == PointerEventData.TriggerState.None) {
                    if ((bits & PointerEventData.TriggerState.BeginDrag) != 0) {
                        var data = eventDatas[i];
                        data.State = PointerEventData.TriggerState.BeginDrag;
                        data.Target = target;
                        data.PointerId = touchId;
                        data.PressPosition = inputPos;
                        eventDatas[i] = data;
                        ClearBit(ref bits, PointerEventData.TriggerState.BeginDrag);
                    }
                }
                if (eventDatas[i].State == PointerEventData.TriggerState.None) {
                    if ((bits & PointerEventData.TriggerState.Drag) != 0) {
                        var data = eventDatas[i];
                        data.State = PointerEventData.TriggerState.Drag;
                        data.Target = target;
                        data.PointerId = touchId;
                        data.Position = inputPos;
                        data.PressPosition = inputPos;
                        data.Delta = float2.zero;
                        eventDatas[i] = data;
                        ClearBit(ref bits, PointerEventData.TriggerState.Drag);
                    }
                }
            }
            Unity.Tiny.Assertions.Assert.IsTrue(bits == 0, "PointerEventData长度不够!请检查");
        }

        // 这方法里整个就是移除不必要的Event,和添加新Event
        private void OnUp(int touchId, float2 inputPos, ref DynamicBuffer<PointerEventData> eventDatas) {
            var cam = GetSingleton<RectCanvasScaleWithCamera>().Camera;
            var target = GetHitEntity(screenToWorld.InputPosToWorldSpacePos(inputPos, 1, cam));
            PointerEventData* p = (PointerEventData*)eventDatas.GetUnsafePtr();

            if (target == Entity.Null) {
                for (int i = 0; i < eventDatas.Length; i++, p++) {
                    ref var eventData = ref UnsafeUtility.AsRef<PointerEventData>(p);
                    // 指针抬起时,也要把drag数据清掉 - 不用清,直接转换为endDrag
                    if (touchId == eventData.PointerId && eventData.State == PointerEventData.TriggerState.Drag) {
                        // 抬起时,拖动状态转为结束拖动
                        eventData.State = PointerEventData.TriggerState.EndDrag;
                    }
                }
            } else {
                var bits = PointerEventData.TriggerState.PointerUp | PointerEventData.TriggerState.EndDrag;

                for (int i = 0; i < eventDatas.Length; i++, p++) {
                    ref var eventData = ref UnsafeUtility.AsRef<PointerEventData>(p);
                    if (eventDatas[i].Target == target && eventDatas[i].State == PointerEventData.TriggerState.PointerUp) {
                        // 已经有点击记录了,不处理多次点击,只处理第一个的
                        ClearBit(ref bits, PointerEventData.TriggerState.PointerUp);
                    }

                    if (eventData.Target == target && touchId == eventData.PointerId && eventData.State == PointerEventData.TriggerState.Drag) {
                        SetNone(p);
                    }

                    if (eventData.Target == target && eventData.PointerId == touchId && eventData.State == PointerEventData.TriggerState.EndDrag) {
                        // 清掉位标记,说明已存在,不用添加了
                        ClearBit(ref bits, PointerEventData.TriggerState.EndDrag);
                    }
                }

                for (int i = 0; i < eventDatas.Length; i++) {
                    if (bits == 0) {
                        break;
                    }

                    if (eventDatas[i].State == PointerEventData.TriggerState.None) {
                        if ((bits & PointerEventData.TriggerState.PointerUp) != 0) {
                            var data = eventDatas[i];
                            data.State = PointerEventData.TriggerState.PointerUp;
                            data.Target = target;
                            data.PointerId = touchId;
                            data.PressPosition = inputPos;
                            eventDatas[i] = data;
                            ClearBit(ref bits, PointerEventData.TriggerState.PointerUp);
                        }
                    }
                    if (eventDatas[i].State == PointerEventData.TriggerState.None) {
                        if ((bits & PointerEventData.TriggerState.EndDrag) != 0) {
                            var data = eventDatas[i];
                            data.State = PointerEventData.TriggerState.EndDrag;
                            data.Target = target;
                            data.PointerId = touchId;
                            data.PressPosition = inputPos;
                            eventDatas[i] = data;
                            ClearBit(ref bits, PointerEventData.TriggerState.EndDrag);
                        }
                    }
                }
                Unity.Tiny.Assertions.Assert.IsTrue(bits == 0, "PointerEventData长度不够!请检查");
            }
        }

        private void OnMove(int touchId, in float2 position, in float2 delta, ref DynamicBuffer<PointerEventData> eventDatas) {
            // 这里要检测Enter和Exit,还有不应该在down那里就添加Drag的?
            // target已经在down那里处理了,这里只需更新position和delta
            for (int i = 0; i < eventDatas.Length; i++) {
                var eventData = eventDatas[i];
                if (eventData.PointerId == touchId && eventData.State == PointerEventData.TriggerState.Drag) {
                    eventData.Delta = delta;
                    eventData.Position = position;
                    // Debug.Log("update delta " + delta.ToString());
                    eventDatas[i] = eventData;
                }
            }
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        void ClearBit(ref PointerEventData.TriggerState bits, in PointerEventData.TriggerState mask) {
            bits &= ~mask;
        }
        
        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        void SetNone(PointerEventData* src) {
            UnsafeUtility.MemCpy(src, PointerEventData.None, PointerEventData.Size);
        }

        Entity GetHitEntity(float3 inputPos) {
            Entity target = Entity.Null;
            int targetDepth = int.MaxValue;
            var RectTransformResultFromEntity = GetComponentDataFromEntity<RectTransformResult>(true);
            var RectParentFromEntity = GetComponentDataFromEntity<RectParent>(true);
            var LocalToWorldFromEntity = GetComponentDataFromEntity<LocalToWorld>(true);

            Entities.WithAll<RaycastTarget>().ForEach((Entity e, in RectTransformResult rtr, in RectParent rp) => {
                int depth = RectangleTransformSystem.GetFinalPosAndDepth(e,
                    rtr.LocalPosition,
                    RectTransformResultFromEntity,
                    RectParentFromEntity,
                    LocalToWorldFromEntity,
                    out float2 finalPosition, out float4x4 pl2w);

                float2 pos = finalPosition + rtr.PivotOffset;
                float4x4 localToWorld = math.mul(pl2w, float4x4.Translate(new float3(pos, 0)));
                float2 lowLeft = localToWorld[3].xy;
                float2 upperRight = localToWorld[3].xy + rtr.Size;

                if (inputPos.x >= lowLeft.x
                    && inputPos.x <= upperRight.x
                    && inputPos.y >= lowLeft.y
                    && inputPos.y <= upperRight.y)
                {
                    if (targetDepth > depth)
                    {
                        target = e;
                        targetDepth = depth;
                    }
                }
            }).Run();

            return target;
        }
    }

	public static class PointerExtentions
    {
        private static bool TriggerState(SystemBase system, Entity target, PointerEventData.TriggerState state) {
            var entity = system.GetSingletonEntity<PointerEventData>();
            var eventDatas = system.GetBuffer<PointerEventData>(entity);

            for (int i = 0; i < eventDatas.Length; i++) {
                var eventData = eventDatas[i];
                if (eventData.Target == target && eventData.State == state) {
                    return true;
                }
            }

            return false;
        }

        public static bool PointerDown<T>(this T system, Entity target) where T : SystemBase, IPointerDownHandler {
            return TriggerState(system, target, PointerEventData.TriggerState.PointerDown);
        }

        public static bool PointerUp<T>(this T system, Entity target) where T : SystemBase, IPointerUpHandler {
            return TriggerState(system, target, PointerEventData.TriggerState.PointerUp);
        }

        public static bool BeginDrag<T>(this T system, Entity target) where T : SystemBase, IBeginDragHandler {
            return TriggerState(system, target, PointerEventData.TriggerState.BeginDrag);
        }

        public static bool Drag<T>(this T system, Entity target, out DragEventData dragEventData) where T : SystemBase, IDragHandler {
            var entity = system.GetSingletonEntity</*InputInstance*/PointerEventData>();
            var eventDatas = system.GetBuffer<PointerEventData>(entity);

            for (int i = 0; i < eventDatas.Length; i++) {
                var eventData = eventDatas[i];
                if (eventData.Target == target && eventData.State == PointerEventData.TriggerState.Drag) {
                    dragEventData = new DragEventData(eventData.PressPosition, eventData.Delta, eventData.Position);
                    // Debug.Log("dragEventData " + eventData.Delta.ToString());
                    return true;
                }
            }

            dragEventData = default;

            return false;
        }

        public static bool EndDrag<T>(this T system, Entity target) where T : SystemBase, IEndDragHandler {
            return TriggerState(system, target, PointerEventData.TriggerState.EndDrag);
        }
    }
}

用法与MonoBehavior的有点区别,但是也很简单,看代码。

public abstract class TestPointerEventSystem : SystemBase, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerDownHandler
{
	protected override void OnUpdate()
	{
	Entity entity = yourTargetEntity; // 带有RectTransform的2D UI entity,3D entity是不支持的!
	if (this.BeginDrag(entity)) {
		// 实现开始拖动逻辑
	}
	if (this.Drag(entity, out DragEventData dragEventData)) {
		// Debug.Log("drag position:" + dragEventData.Position.ToString() + " pressPosition:" + dragEventData.PressPosition.ToString());
		// 2D entity被拖动了,实现你自己的拖动功能
	}
	if (this.EndDrag(entity)) {
		// 实现结束拖动逻辑
	}

	if (this.PointerDown(entity) {
		// UI被按下了
	}
}

你可能感兴趣的:(unity,ECS,DOTS,ProjectTiny,按钮,EventSystem)