这篇文章主要展示下在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被按下了
}
}