[Unity] 允许棋子点击移动和长按拖动,在移动周期中触发自定义事件的逻辑思路

673 阅读12分钟

逻辑和表现分离

丑陋的例子 1:拖动逻辑和 UI 表现逻辑放在一起

一开始我的逻辑是很丑陋的

一个被拖动的棋子,它的逻辑和表现被我混合在了一起

例如:

using UnityEngine;

/// <summary>
/// 棋盘上的棋子的基类
/// 基类主要处理 DetailsUI 拖动放置相关的逻辑
/// </summary>
public class Chess : MonoBehaviour
{
    protected bool isDraging;
    
    [Header("Detail UI")]
    
    [SerializeField]
    private string chessName;

    public string ChessName
    {
        get => chessName;
        set
        {
            chessName = value;
        }
    }

    [SerializeField]
    private string details;

    public string Details
    {
        get => details;
        set
        {
            details = value;
        }
    }
    
    [SerializeField, ReadOnly]
    protected bool isShowingDetailsUI;

    [SerializeField, ReadOnly] 
    protected float showDetailsUITime;
    
    [SerializeField, ReadOnly]
    protected Vector3 lastToggleDetailsUIPos;

    [SerializeField]
    protected float showDetailsUIMinTime = 1f;

    [SerializeField]
    protected float showDetailsUIMaxDisOffset = 1f;

    #region Mouse Detection

    // 这是 Unity 在内部实现的用鼠标射线与 collider 2d 相交的检测

    private void OnMouseDown()
    {
        OnPressDownChess();
    }

    private void OnMouseUp()
    {
        OnPressUpChess();
    }
    
    protected void Update()
    {
        OnPressHoldChess();
    }
    
    #endregion

    #region Virtual Mouse Detection Behaviour
 
    // 这是提供给子类继承的函数,因为 Unity 自带的 OnMouseDown OnMouseUp 等函数不能继承

    protected virtual void OnPressDownChess()
    {
        showDetailsUITime = 0f;
        lastToggleDetailsUIPos = transform.position + Vector3.one * showDetailsUIMaxDisOffset;
    }

    protected virtual void OnPressUpChess()
    {
        HideDetailsUI();
    }
    
    protected virtual void OnPressHoldChess()
    {
        if (!isDraging)
            return;
        
        if(isShowingDetailsUI)
        {
            // 显示 UI 时如果又移动就收起来
            if ((transform.position - lastToggleDetailsUIPos).magnitude > showDetailsUIMaxDisOffset)
            {
                HideDetailsUI();
            }
        }
        else
        {
            showDetailsUITime += Time.deltaTime;
            // 时间久了,移动距离也长,不是误触
            if (showDetailsUITime > showDetailsUIMinTime && (transform.position - lastToggleDetailsUIPos).magnitude > showDetailsUIMaxDisOffset)
            {
                ShowDetailsUI();
            }
        }
    }
    
    #endregion

    #region Details UI

    private void ShowDetailsUI()
    {
        isShowingDetailsUI = true;
        lastToggleDetailsUIPos = transform.position;
        
        Vector3 screenPos = Camera.main.WorldToScreenPoint(transform.position + Vector3.right * GameSpriteController.Instance.GridSize);
        GameUI.Instance.ShowDetails(screenPos, ChessName, Details);
    }

    private void HideDetailsUI()
    {
        isShowingDetailsUI = false;
        lastToggleDetailsUIPos = transform.position;
        showDetailsUITime = 0f;
        
        GameUI.Instance.HideDetails();
    }

    #endregion

}

在这里,虽然明确地创建了虚函数,提供给子类

    private void OnMouseDown()
    {
        OnPressDownChess();
    }

    private void OnMouseUp()
    {
        OnPressUpChess();
    }
    
    protected void Update()
    {
        OnPressHoldChess();
    }

但是我却在父类中写了关于显示一个 UI 的表现逻辑

我在这里写,主要是因为我一开始以为,这个计时相关的逻辑,必须要获取一个开始一个结尾,那么在触发这个拖动事件的棋子里面写很方便

但是现在一看,确实……很累

丑陋的例子 2:每一个棋子独立处理自己的拖动逻辑

第二个丑陋的例子是,每一个可以移动的方块都可能独立计算自己的拖动逻辑

using System;
using System.Collections;
using DG.Tweening;
using UnityEngine;

public class DraggableChess : Chess
{
    [Header("Drag")]
    
    [ReadOnly]
    public bool canDrag;

    [ReadOnly]
    public Vector3 dragBeginPos;

    [Header("Drag Behaviour Setting")]

    [SerializeField]
    protected int reachableSteps = 1;

    public int ReachableSteps => reachableSteps;

    [Header("Click Behaviour Setting")] 
    
    [SerializeField]
    [Tooltip("点击的灵敏度,单位 s,表示在这个时间范围内完成按下松开则判断为点击")]
    protected float clickSensitivity = 0.2f;

    [SerializeField]
    [Tooltip("玩家本次按下松开之间的时间间隔")]
    protected float clickInterval;

    [SerializeField]
    [Tooltip("是否正在计时")]
    protected bool isCounting;
    
    [SerializeField]
    [Tooltip("是否处于被点击状态")]
    public bool isClicked;

    protected void Start()
    {
        dragBeginPos = transform.position;
    }

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

        if(!CanBeginDrag())
            return;

        isCounting = true;
        clickInterval = 0f;
    }

    protected override void OnPressUpChess()
    {
        base.OnPressUpChess();
        
        if (!canDrag)
            return;

        isCounting = false;

        // 判定为点击
        if (clickInterval < clickSensitivity)
        {
            if(!isClicked) DragChessGameCue.Instance.BeginClick(this);
            else DragChessGameCue.Instance.BreakClick(this);
        }
        
        // 如果正在拖动棋子,那么终止拖动
        if (isDraging) EndDrag();
    }

    protected void FixedUpdate()
    {
        if(isCounting)
        {
            clickInterval += Time.fixedDeltaTime;

            if (clickInterval > clickSensitivity)
            {
                isCounting = false;

                if (isClicked)
                {
                    isClicked = false;
                }
                
                BeginDrag();
            }
        }
    }

    #region Check if can begin darg

    protected virtual bool CanBeginDrag()
    {
        return canDrag;
    }

    #endregion

    #region Drag Behaviour

    private void BeginDrag()
    {
        isDraging = true;
        dragBeginPos = transform.position;
        
        // DragChessGameCue

        DragChessGameCue.Instance.BeginDrag(this);
    }

    private void EndDrag()
    {
        // drag setting
        
        isDraging = false;

        // DragChessGameCue

        DragChessGameCue.Instance.EndDrag();
    }

    #endregion

    // 提供给资源方块或者道具方块,拖动到某个位置时判断该怎么行动
    public virtual IEnumerator WaitForDragTo(Vector3 worldChessPos)
    {
        yield return null;
    }
    
    // 返回原位置
    // 不结束玩家可拖动回合
    public IEnumerator WaitForReturnPos()
    {
        // return last pos
            
        yield return StartCoroutine(ChessTweenHelper.WaitForMoveTo(transform, dragBeginPos));
            
        // continue to drag other chess
            
        ChessGameCue.Instance.SetAllResChessesCanDrag(true);
    }
}

这里我需要记录一个位置 dragBeginPos,来记录他是从哪里开始被拖动的

结果后面为了维护这个位置,就写成了屎山

比如这里我在子类中,比如资源棋子 ResChess,也要费尽维护

在初始化时,在移动结束时,在每一种移动结束时都是

图片.png

之后很多 bug 的出现都是因为这个位置维护的有问题

因此这样的问题一看就是,因为在各个流程中都要用到同一个变量,所以这个变量的维护就变得很重要,需要考虑很多情况,要让它在各个使用情况下都不犯错

一轮解耦:将缩放动画,UI 显示等表现与判定鼠标点击和拖动的逻辑分离

首先,基类只是提供一个 collider 2d 的点击射线检测

Chess.cs

using UnityEngine;

/// <summary>
/// 棋盘上的棋子的基类
/// 基类主要处理 DetailsUI 拖动放置相关的逻辑
/// </summary>
public class Chess : MonoBehaviour
{
    protected bool isDraging;
    
    [Header("Detail UI")]
    
    [SerializeField]
    private string chessName;

    public string ChessName
    {
        get => chessName;
        set
        {
            chessName = value;
        }
    }

    [SerializeField]
    private string details;

    public string Details
    {
        get => details;
        set
        {
            details = value;
        }
    }

    #region Mouse Detection

    // 这是 Unity 在内部实现的用鼠标射线与 collider 2d 相交的检测

    private void OnMouseDown()
    {
        OnPressDownChess();
    }

    private void OnMouseUp()
    {
        OnPressUpChess();
    }
    
    protected void Update()
    {
        OnPressHoldChess();
    }
    
    #endregion

    #region Virtual Mouse Detection Behaviour
 
    // 这是提供给子类继承的函数,因为 Unity 自带的 OnMouseDown OnMouseUp 等函数不能继承

    protected virtual void OnPressDownChess()
    {
        
    }

    protected virtual void OnPressUpChess()
    {
        
    }
    
    protected virtual void OnPressHoldChess()
    {
  
    }
    
    #endregion

}

然后子类调用表现方面的逻辑

DraggableChess.cs

using System;
using System.Collections;
using DG.Tweening;
using UnityEngine;

public class DraggableChess : Chess
{
    [Header("Drag")]
    
    [ReadOnly]
    public bool canDrag;

    [ReadOnly]
    public Vector3 dragBeginPos;

    [Header("Drag Behaviour Setting")]

    [SerializeField]
    protected int reachableSteps = 1;

    public int ReachableSteps => reachableSteps;

    [Header("Click Behaviour Setting")] 
    
    [SerializeField]
    [Tooltip("点击的灵敏度,单位 s,表示在这个时间范围内完成按下松开则判断为点击")]
    protected float clickSensitivity = 0.2f;

    [SerializeField]
    [Tooltip("玩家本次按下松开之间的时间间隔")]
    protected float clickInterval;

    [SerializeField]
    [Tooltip("是否正在计时")]
    protected bool isCounting;
    
    [SerializeField]
    [Tooltip("是否处于被点击状态")]
    public bool isClicked;

    protected void Start()
    {
        dragBeginPos = transform.position;
    }

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

        // 这里使用虚函数是因为,是否能够被拖动,除了一个 canDrag 布尔值之外还可能有其他逻辑
        // 例如道具方块是否能移动,还取决于它的数量是否大于 0
        if(!CanBeginDrag())
            return;

        isCounting = true;
        clickInterval = 0f;
    }

    protected override void OnPressUpChess()
    {
        base.OnPressUpChess();
        
        if (!canDrag)
            return;

        isCounting = false;

        // 判定为点击
        if (clickInterval < clickSensitivity)
        {
            if(!isClicked) DragChessGameCue.Instance.BeginClick(this);
            else DragChessGameCue.Instance.BreakClick(this);
        }
        
        // 如果正在拖动棋子,那么终止拖动
        if (isDraging) EndDrag();
    }

    protected void FixedUpdate()
    {
        if(isCounting)
        {
            clickInterval += Time.fixedDeltaTime;

            if (clickInterval > clickSensitivity)
            {
                isCounting = false;

                if (isClicked)
                {
                    isClicked = false;
                }
                
                BeginDrag();
            }
        }
    }

    #region Check if can begin darg

    protected virtual bool CanBeginDrag()
    {
        return canDrag;
    }

    #endregion

    #region Drag Behaviour

    private void BeginDrag()
    {
        isDraging = true;
        dragBeginPos = transform.position;
        
        // DragChessGameCue

        DragChessGameCue.Instance.BeginDrag(this);
    }

    private void EndDrag()
    {
        // drag setting
        
        isDraging = false;

        // DragChessGameCue

        DragChessGameCue.Instance.EndDrag();
    }

    #endregion

    // 提供给资源方块或者道具方块,拖动到某个位置时判断该怎么行动
    public virtual IEnumerator WaitForDragTo(Vector3 worldChessPos)
    {
        yield return null;
    }
    
    // 返回原位置
    // 不结束玩家可拖动回合
    public IEnumerator WaitForReturnPos()
    {
        // return last pos
            
        yield return StartCoroutine(ChessTweenHelper.WaitForMoveTo(transform, dragBeginPos));
            
        // continue to drag other chess
            
        ChessGameCue.Instance.SetAllResChessesCanDrag(true);
    }
}

表现方面的一部分逻辑如下

DragChessGameCue.cs

using System;
using System.Collections;
using DG.Tweening;
using UnityEngine;
using Universal;

public class DragChessGameCue : Singleton<DragChessGameCue>
{
    [SerializeField] 
    [Tooltip("被点选的/正在拖动着的棋子")] 
    private DraggableChess currChess;
    
    private void Update()
    {
        OnClicking();
        OnDragging();
        OnShowingDetailsUI();
    }
    
    #region Click Behaviour

    [SerializeField]
    [Tooltip("是否正在点选")]
    private bool isClicking;
    
    public event Action OnClickBegin;
    
    public event Action OnClickEnd;

    public void BeginClick(DraggableChess chess)
    {
        isClicking = true;

        chess.isClicked = true;
        
        if (currChess != chess) BreakClick(currChess);
        
        print("BeginClick");
        
        // 显示在上层
        chess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 1;
        
        ShowReachableTint(chess.transform.position, chess.ReachableSteps);
        
        currChess = chess;
    }

    public void BreakClick(DraggableChess chess)
    {
        if (!currChess) return;
        
        isClicking = false;
        
        chess.isClicked = false;
        
        print("BreakClick");
        
        // 恢复
        chess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 0;
        
        HideReachableTint();
        
        currChess = null;
    }

    private void EndClick(Vector3 worldChessPos)
    {
        print("EndClick");
        
        isClicking = false;
        currChess.isClicked = false;
        
        HideReachableTint();
        
        // 恢复
        currChess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 0;
        
        // 禁用玩家输入
        ChessGameCue.Instance.SetAllResChessesCanDrag(false);
        
        Chessboard.Instance.StartCoroutine(currChess.WaitForDragTo(worldChessPos));
        
        currChess = null;
    }

    private void OnClicking()
    {
        if (!isClicking) return;

        if (!currChess) return;
        
        // 如果点击到了其他位置,就
        if (Input.GetMouseButtonDown(0))
        {
            Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            Vector3 worldChessPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);

            if ((currChess.transform.position - worldChessPos).magnitude >= 1.0f)
            {
                EndClick(worldChessPos);
            }
        }
    }
    
    #endregion

在这里其实也是变屎了,因为在表现的脚本中也出现了判定点击相关的逻辑

这个主要是因为,一开始我是纯在棋子 mono 用用 collider 2d 的鼠标检测来管理拖动

但是现在关系到了鼠标点击之后,单个棋子就无法捕捉到棋子之外的点击

于是就变成了丑陋的样子

并且简单的写了一下还会有棋子自己的判定与 GameCue 的判定杂糅的情况

于是玩家输入就真的要独立出来了

但是这个时候还是需要一个事件来告知什么时候开始点击/触摸,什么时候按住鼠标/保持触摸,什么时候松开鼠标/结束触摸

如果直接用 if (Input.GetMouseButtonDown(0)) 的话,首先局限在 Windows 不说,那还要自己写开始保持结束的函数

于是我想到,能不能用一个大的 Collider 2D 来记录

OnMouseDrag 测试

但是我有一个问题是,如果 Collider 2d 不动,鼠标一开始点在 Collider 2d 上面,然后鼠标拖动拖出这个 Collider 2d 的范围,那么 OnMouseDrag 是否还会生效

OnMouseDrag 测试

using UnityEngine;

public class TestMouseHold : MonoBehaviour
{
    private void OnMouseDrag()
    {
        Debug.Log("On Mouse Drag!");
    }
}

实际测试是会生效的

一大块的 Collider 2d 作为接受鼠标输入的区域不太可靠

但是实践中发现 Collider 2d 作为接受鼠标输入的区域是不太可靠的,有的时候,上一次触发了 OnMouseDown,下一次在相近的位置上就不能触发 OnMouseDown

独立的 PlayerInputController 尝试

这一版的 Ctr 感觉思路上大概是这么一个意思,但是总是会有微小的 BUG

public class PlayerInputController : Singleton<PlayerInputController>
{
    [Header("Status")]
    
    [Tooltip("是否接受玩家输入")]
    private bool canPlayerInput;

    public bool CanPlayerInput
    {
        set => canPlayerInput = value;
    }
    
    [SerializeField]
    [Tooltip("是否处于被点击状态")]
    public bool isClicking;
    
    [SerializeField]
    [Tooltip("是否正在拖动")]
    private bool isDragging;
    
    [SerializeField] 
    [Tooltip("被点选的/正在拖动着的棋子")] 
    private DraggableChess currChess;

    [Header("Click Behaviour Setting")] 
    
    [SerializeField]
    [Tooltip("点击的灵敏度,单位 s,表示在这个时间范围内完成按下松开则判断为点击")]
    private float clickSensitivity = 0.6f;

    [SerializeField]
    [Tooltip("玩家本次按下松开之间的时间间隔")]
    private float clickInterval;

    [SerializeField]
    [Tooltip("是否正在计时")]
    private bool isCounting;

    [Header("Drag Behaviour Setting")]

    [SerializeField]
    [Tooltip("拖拽开始的位置")]
    private Vector3 dragBeginPos;
    
    private void Update()
    {
        if (Input.GetMouseButtonDown(0)) OnMyMouseDown();
        else if(Input.GetMouseButtonUp(0)) OnMyMouseUp();
        else ClickOrDrag();
    }
    
    private void OnMyMouseDown()
    {
        isCounting = true;
        clickInterval = 0f;
        
        print("OnMouseDown");
    }

    private void OnMyMouseUp()
    {
        if (!canPlayerInput)
            return;

        isCounting = false;

        // 判定为点击
        if (clickInterval < clickSensitivity && !isDragging)
        {
            clickInterval = 0f;
            
            // 如果不是正在点选,那么点选一个棋子
            if(!isClicking)
            {
                BeginClick(RaycastMovableChess());
            }
            // 如果正在点选,那么判断点选的新棋子的情况
            else
            {
                BreakClick();
            }
        }
        
        // 如果正在拖动棋子,那么终止拖动
        if (isDragging)
        {
            clickInterval = 0f;
            
            EndDrag();
        }
    }

    private void ClickOrDrag()
    {
        if(isCounting)
        {
            clickInterval += Time.deltaTime;

            if (clickInterval > clickSensitivity)
            {
                isCounting = false;

                isClicking = false;
                
                BeginDrag(RaycastMovableChess());
            }
        }
    }

于是我把 Update 那里改了一下,主要是梳理了一下激活点击或者拖动的逻辑

现在基本上是能够正常运行点击和拖拽了

using System;
using UnityEngine;
using Universal;

public class PlayerInputController : Singleton<PlayerInputController>
{
    [Header("Status")]
    
    [SerializeField]
    [Tooltip("是否接受玩家输入")]
    private bool canPlayerInput;

    public bool CanPlayerInput
    {
        set => canPlayerInput = value;
    }
    
    [SerializeField]
    [Tooltip("是否处于被点击状态")]
    public bool hasClickOnce;
    
    [SerializeField]
    [Tooltip("是否正在拖动")]
    private bool isDragging;
    
    [SerializeField] 
    [Tooltip("被点选的/正在拖动着的棋子")] 
    private DraggableChess currChess;

    [Header("Click Behaviour Setting")] 
    
    [SerializeField]
    [Tooltip("点击的灵敏度,单位 s,表示在这个时间范围内完成按下松开则判断为点击")]
    private float clickSensitivity = 0.2f;

    [SerializeField]
    [Tooltip("玩家本次按下松开之间的时间间隔")]
    private float clickInterval;

    [SerializeField]
    [Tooltip("是否正在计时")]
    private bool isCounting;

    [Header("Drag Behaviour Setting")]

    [SerializeField]
    [Tooltip("输入刚开始时鼠标点击的世界格子位置")]
    private Vector3 inputBeginPos;
    
    private void Update()
    {
        if (!canPlayerInput)
            return;

        if (Input.GetMouseButtonDown(0))
        {
            isCounting = true;
            clickInterval = 0f;
            
            Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            inputBeginPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);
        }
        else if (Input.GetMouseButtonUp(0))
        {
            // 如果点击和松开之间的时间小于限度,并且不是正在拖动棋子
            // 判定为点击
            if (clickInterval < clickSensitivity && !isDragging) TryBeginClick();
            // 如果不是判定为点击,那么就判定为取消拖拽
            else
            {
                isDragging = false;
                EndDrag();
            }
            
            isCounting = false;
            clickInterval = 0f;
        }

        if (isCounting)
        {
            clickInterval += Time.deltaTime;
        }
        
        if (clickInterval > clickSensitivity && !isDragging)
        {
            hasClickOnce = false;
            
            BeginDrag(RaycastMovableChess());
        }
    }

    private void TryBeginClick()
    {
        // 如果不是正在点选,那么点选一个棋子
        if(!hasClickOnce) BeginClick(RaycastMovableChess());
        // 如果正在点选,那么判断点选的新棋子的情况
        else BreakClick();
    }

    #region Click Behaviour

    public event Action<DraggableChess> OnClickBegin;

    public event Action<DraggableChess> OnClickBreak;

    private void BeginClick(DraggableChess chess)
    {
        if (!chess) return;
        
        OnClickBegin?.Invoke(chess);
        
        hasClickOnce = true;

        print("BeginClick");
        
        // 显示在上层
        chess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 1;

        currChess = chess;
    }
    
    private void BreakClick()
    {
        if (!currChess) return;
        
        OnClickBreak?.Invoke(currChess);

        hasClickOnce = false;
        
        print("BreakClick");
        
        // 恢复
        currChess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 0;

        // 不论被点击到的第二个棋子是否为空,都尝试移动到那里
        Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector3 worldChessPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);
            
        // 禁用玩家输入
        canPlayerInput = false;

        Chessboard.Instance.StartCoroutine(currChess.WaitForDragTo(worldChessPos, currChess.transform.position));
        
        currChess = null;
    }

    #endregion
    
    #region Drag Behaviour

    public event Action<DraggableChess> OnDragBegin;

    public event Action<DraggableChess> OnDragEnd;
    
    private DraggableChess RaycastMovableChess()
    {
        // 通过射线检测,获取鼠标开始点击时的位置下的棋子

        DraggableChess dragChess = ChessRaycastHelper.RaycastChess(inputBeginPos) as DraggableChess;

        if (!dragChess) return null;

        if (!dragChess.CanMove()) return null;

        return dragChess;
    }

    private void BeginDrag(DraggableChess chess)
    {
        if (!chess) return;
        
        OnDragBegin?.Invoke(chess);
        
        isDragging = true;

        // 有些棋子的位置可能不是世界格子坐标,例如道具棋子
        // 因此在这里再更新一下 inputBeginPos
        inputBeginPos = chess.transform.position;
        
        // 显示在上层
        chess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 1;

        currChess = chess;
    }
    
    private void EndDrag()
    {
        if (!currChess) return;
        
        print("EndDrag");
        
        OnDragEnd?.Invoke(currChess);
        
        isDragging = false;

        // 恢复显示在下层
        currChess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 0;
        
        // 禁用玩家输入
        canPlayerInput = false;

        // drag behaviour
        
        Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector3 worldChessPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);

        Debug.Log(worldChessPos);
        
        // 最主要是这个,开始判断被拖动到的位置是怎样的,该怎么行动
        // 不是棋子自己启动协程,因为棋子自己可能在完成拖放动作的时候会被送入对象池,enable = false
        // 这会导致棋子自己的协程停止
        Chessboard.Instance.StartCoroutine(currChess.WaitForDragTo(worldChessPos, inputBeginPos));
        
        currChess = null;
    }

    #endregion
}

但是这一版有一个问题是,在一轮移动操作结束之后,再次点击,有可能会出现,明明点击的是位置 B,但是却看上去是点到了位置 A 的情况

这里的问题应该是在判断点击到的棋子的位置时,使用的是上一次的 mouseButtonDownPos,但是上一次点击结束时,可能这一次点击直接进入了 else if (Input.GetMouseButtonUp(0)),因此就没有进入更新 mouseButtonDownPosif (Input.GetMouseButtonDown(0)) 块,那么这个时候 mouseButtonDownPos 保存的仍然是上一次的位置

于是现在改成,对于点击事件,比如要用当前鼠标的位置来打射线,就 ok 了

允许棋子点击移动和长按拖动的 PlayerInputController

总结的没有 BUG 的 Ctr

using System;
using UnityEngine;
using Universal;

public class PlayerInputController : Singleton<PlayerInputController>
{
    [Header("Status")]
    
    [SerializeField]
    [Tooltip("是否接受玩家输入")]
    private bool canPlayerInput;

    public bool CanPlayerInput
    {
        set => canPlayerInput = value;
    }
    
    [SerializeField]
    [Tooltip("是否处于被点击状态")]
    public bool hasClickOnce;
    
    [SerializeField]
    [Tooltip("是否正在拖动")]
    private bool isDragging;
    
    [SerializeField] 
    [Tooltip("被点选的/正在拖动着的棋子")] 
    private DraggableChess currChess;

    [Header("Click Behaviour Setting")] 
    
    [SerializeField]
    [Tooltip("点击的灵敏度,单位 s,表示在这个时间范围内完成按下松开则判断为点击")]
    private float clickSensitivity = 0.2f;

    [SerializeField]
    [Tooltip("玩家本次按下松开之间的时间间隔")]
    private float clickInterval;

    [SerializeField]
    [Tooltip("是否正在计时")]
    private bool isCounting;

    [Header("Drag Behaviour Setting")]

    [SerializeField]
    [Tooltip("鼠标按下的世界格子位置")]
    private Vector3 mouseButtonDownPos;
    
    [SerializeField]
    [Tooltip("拖拽开始时棋子的位置")]
    private Vector3 dragChessBeginPos;
    
    private void Update()
    {
        if (!canPlayerInput)
            return;

        if (Input.GetMouseButtonDown(0))
        {
            isCounting = true;
            clickInterval = 0f;
            
            Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            mouseButtonDownPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);
        }
        else if (Input.GetMouseButtonUp(0))
        {
            // 如果点击和松开之间的时间小于限度,并且不是正在拖动棋子
            // 判定为点击
            if (clickInterval < clickSensitivity && !isDragging) TryBeginClick();
            // 如果不是判定为点击,那么就判定为取消拖拽
            else
            {
                isDragging = false;
                EndDrag();
            }
            
            isCounting = false;
            clickInterval = 0f;
        }

        if (isCounting)
        {
            clickInterval += Time.deltaTime;
        }
        
        if (clickInterval > clickSensitivity && !isDragging)
        {
            hasClickOnce = false;
            
            BeginDrag(RaycastMovableChess(mouseButtonDownPos));
        }
    }

    private void TryBeginClick()
    {
        // 如果不是正在点选,那么点选一个棋子
        if(!hasClickOnce) BeginClick(RaycastMovableChess(Vector3.negativeInfinity));
        // 如果正在点选,那么判断点选的新棋子的情况
        else BreakClick();
    }

    #region Click Behaviour

    public event Action<DraggableChess> OnClickBegin;

    public event Action<DraggableChess> OnClickBreak;

    private void BeginClick(DraggableChess chess)
    {
        if (!chess) return;
        
        OnClickBegin?.Invoke(chess);
        
        hasClickOnce = true;

        print("BeginClick");
        
        // 显示在上层
        chess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 1;

        currChess = chess;
    }
    
    private void BreakClick()
    {
        if (!currChess) return;
        
        OnClickBreak?.Invoke(currChess);

        hasClickOnce = false;
        
        print("BreakClick");
        
        // 恢复
        currChess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 0;

        // 不论被点击到的第二个棋子是否为空,都尝试移动到那里
        Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector3 worldChessPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);
            
        // 禁用玩家输入
        canPlayerInput = false;

        Chessboard.Instance.StartCoroutine(currChess.WaitForDragTo(worldChessPos, currChess.transform.position));
        
        currChess = null;
    }

    #endregion
    
    #region Drag Behaviour

    public event Action<DraggableChess> OnDragBegin;

    public event Action<DraggableChess> OnDragEnd;
    
    private DraggableChess RaycastMovableChess(Vector3 raycastPos)
    {
        // 约定 raycastPos 为 Vector3.negativeInfinity 时表示使用当前鼠标位置作为射线检测起点
        // 使用向量长度作为判断是否是 Vector3.negativeInfinity
        if (raycastPos.magnitude > 10000f) 
        {
            Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            raycastPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);
        }
        
        // 通过射线检测,获取鼠标开始点击时的位置下的棋子

        DraggableChess dragChess = ChessRaycastHelper.RaycastChess(raycastPos) as DraggableChess;

        if (!dragChess) return null;

        if (!dragChess.CanMove()) return null;

        return dragChess;
    }

    private void BeginDrag(DraggableChess chess)
    {
        if (!chess) return;
        
        OnDragBegin?.Invoke(chess);
        
        isDragging = true;

        // 有些棋子的位置可能不是世界格子坐标,例如道具棋子
        // 因此 mouseButtonDownPos 和 dragChessBeginPos 是不同的
        dragChessBeginPos = chess.transform.position;
        
        // 显示在上层
        chess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 1;

        currChess = chess;
    }
    
    private void EndDrag()
    {
        if (!currChess) return;
        
        print("EndDrag");
        
        OnDragEnd?.Invoke(currChess);
        
        isDragging = false;

        // 恢复显示在下层
        currChess.GetComponentInChildren<SpriteRenderer>().sortingOrder = 0;
        
        // 禁用玩家输入
        canPlayerInput = false;

        // drag behaviour
        
        Vector3 targetPos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        Vector3 worldChessPos = ChessPosConverter.WorldPosToWorldChessPos(targetPos);

        Debug.Log(worldChessPos);
        
        // 最主要是这个,开始判断被拖动到的位置是怎样的,该怎么行动
        // 不是棋子自己启动协程,因为棋子自己可能在完成拖放动作的时候会被送入对象池,enable = false
        // 这会导致棋子自己的协程停止
        Chessboard.Instance.StartCoroutine(currChess.WaitForDragTo(worldChessPos, dragChessBeginPos));
        
        currChess = null;
    }

    #endregion
}