using DG.Tweening; using System.Collections.Generic; using TMPro; using UnityEngine; using UnityEngine.UI; /// /// 棋盘控件(挂在 ChineseChessMainPanel 下) /// /// 定位方案(配合自备棋盘底图): /// 1. 自备棋盘底图:作为本物体的子物体摆好(拖到 img_Board,可选) /// 2. 建两个空物体作定位点,精确摆到底图交叉点上: /// point_Start:楚河汉界【下方】的左上角交叉点(红方河沿最左侧,逻辑 (0,4)) /// point_End :楚河汉界【上方】的左下角交叉点(与起始点同列、隔河相邻,逻辑 (0,5)) /// 3. 两点距离 = 网格间距;代码从起始点出发生成全部 90 个定位点 /// 4. 黑方视角:逻辑坐标镜像映射(我方棋子永远在下方),底图不用转 /// /// 棋子: /// - prefab_Piece:只放 Image + Button 的预制体(不加任何脚本,脚本由代码动态添加) /// - sprite_RedPieces / sprite_BlackPieces:拖展示图,顺序:将/帅、士/仕、象/相、马、车、炮、兵/卒 /// - 都不配则代码生成默认棋子(米色圆 + 汉字) /// /// 调试:show_DebugPoints = true 生成 90 个半透明小点,用于校准定位点与底图对齐 /// public class UIChessBoard : MonoBehaviour { [Header("棋盘定位(必填)")] public RectTransform point_Start; // 起始定位点:楚河汉界下方左上角交叉点(红方河沿最左) public RectTransform point_End; // 间距定位点:楚河汉界上方左下角交叉点(与起始点同列隔河) public Image img_Board; // 棋盘底图(自摆拖入;不填且 draw_BoardLines 开启时代码画线) public bool draw_BoardLines = true; // 无底图时代码绘制棋盘线(有底图自动忽略) [Header("棋子/格子预制体(只放 Image + Button,脚本由代码动态添加)")] public GameObject prefab_Piece; public GameObject prefab_Cell; // 可选:格子预制体 [Header("棋子展示图(顺序:将/帅 士/仕 象/相 马 车 炮 兵/卒)")] public Sprite[] sprite_RedPieces; public Sprite[] sprite_BlackPieces; [Range(0.5f, 1.5f)] public float pieceScale = 0.92f; // 棋子直径 = 网格间距 × pieceScale [Header("上一步历史标记图(仅显示上一位玩家的起点/终点,不填用半透明方块)")] public Sprite sprite_LastFromMark; // 起点标记图 public Sprite sprite_LastToMark; // 终点标记图 [Range(0.3f, 1.5f)] public float lastMarkScale = 0.85f; // 标记大小 = 格距 × 此值 [Header("选中标记图(点击自己棋子时显示在棋子下面)")] public Sprite sprite_HintDot; // 点击选中的棋子下面显示的图(沿用原字段名,之前拖的图自动关联) [Range(0.3f, 1.5f)] public float selectMarkScale = 1.0f; // 标记大小 = 格距 × 此值 [Header("调试")] public bool show_DebugPoints = false; // 显示 90 个定位点小圆点(校准底图对齐用) [Header("无展示图时的默认样式")] public Sprite sprite_PieceBg; // 棋子底图(默认:代码生成的米色圆) public TMP_FontAsset font_Piece; // 棋子文字字体(默认:TMP 默认字体) public Color color_RedText = new Color32(0xC6, 0x28, 0x28, 0xFF); public Color color_BlackText = new Color32(0x21, 0x21, 0x21, 0xFF); [Header("运行时自动生成(不用拖)")] public RectTransform tr_Lines; // 棋盘线层(仅无底图时用) public RectTransform tr_Cells; // 格子层(90 个定位点) public RectTransform tr_UnderMarks; // 选中标记层(在棋子下面) public RectTransform tr_Pieces; // 棋子层 public RectTransform tr_Marks; // 提示层(上一步标记/提示文字) /// 解析后的字体(font_Piece 或 TMP 默认字体) public TMP_FontAsset ResolvedFont { get; private set; } // 格子缓存:按逻辑坐标存 [y,x] private UIChessCell[,] cells = new UIChessCell[ChessConst.BoardHeight, ChessConst.BoardWidth]; // 棋子缓存:pieceId → 控件 private readonly Dictionary pieceMap = new Dictionary(); private ChessGameManager gameManager; private ChessCamp myCamp; private bool flipped; // 我方为黑方时,逻辑坐标镜像映射(我方永远在下方) private UIChessPiece selectedPiece; private readonly List selectedMoves = new List(); private RectTransform lastFromMark; private RectTransform lastToMark; private Image img_SelectMark; // 选中标记(点击棋子时显示在棋子下面) private TextMeshProUGUI tmp_Toast; private Tween toastTween; // ---- 定位计算结果 ---- private bool gridValid; // 定位点是否有效 private Vector2 gridStart; // 起始点在格层本地坐标(逻辑 (0,4)) private Vector2 gridUp; // 逻辑 y+ 方向(指向黑方) private Vector2 gridRight; // 逻辑 x+ 方向(红方视角从左到右) private float cellStep; // 网格间距(两定位点距离) private float gridAngle; // 网格旋转角(底图正摆时为 0) private float pieceSize; // 棋子直径 private bool interactable = true; /// /// 初始化棋盘(每次开一局调用一次) /// public void Init(ChessGameManager manager, ChessCamp camp) { Clear(); gameManager = manager; myCamp = camp; flipped = camp == ChessCamp.Black; ResolvedFont = font_Piece != null ? font_Piece : TMP_Settings.defaultFontAsset; if (ResolvedFont == null) Debug.LogWarning("[UIChessBoard] 未找到可用 TMP 字体,棋子文字可能无法显示,请在 Inspector 给 font_Piece 拖入中文字体"); BuildContainers(); CalcGridFromPoints(); // ★ 两定位点 → 间距 + 方向 BuildCells(); // ★ 90 个棋盘定位点 if (img_Board == null && draw_BoardLines) BuildBoardLines(); // 无底图时才代码画线 BuildPieces(); // ★ 全部棋子(预制体优先,脚本动态添加) if (show_DebugPoints) BuildDebugPoints(); BuildToast(); // 订阅数据层事件 → 表现层 gameManager.OnMoveApplied += OnChessMoveApplied; gameManager.OnUndoApplied += OnChessUndoApplied; gameManager.OnGameOver += OnChessGameOver; gameManager.OnTurnChanged += OnTurnChanged; SetInteractable(true); if (gridValid) ShowToast(myCamp == ChessCamp.Red ? "红方先行" : "对方先行"); } /// 清空棋盘(对局结束/面板隐藏时调用,Init 内部也会先调一次保证可重入) public void Clear() { if (gameManager != null) { gameManager.OnMoveApplied -= OnChessMoveApplied; gameManager.OnUndoApplied -= OnChessUndoApplied; gameManager.OnGameOver -= OnChessGameOver; gameManager.OnTurnChanged -= OnTurnChanged; gameManager = null; } ClearSelection(); selectedPiece = null; selectedMoves.Clear(); HideLastMoveMark(); if (tr_Lines != null) { Destroy(tr_Lines.gameObject); tr_Lines = null; } if (tr_Cells != null) { Destroy(tr_Cells.gameObject); tr_Cells = null; } if (tr_UnderMarks != null) { Destroy(tr_UnderMarks.gameObject); tr_UnderMarks = null; } if (tr_Pieces != null) { Destroy(tr_Pieces.gameObject); tr_Pieces = null; } if (tr_Marks != null) { Destroy(tr_Marks.gameObject); tr_Marks = null; } lastFromMark = null; lastToMark = null; img_SelectMark = null; tmp_Toast = null; toastTween?.Kill(); toastTween = null; pieceMap.Clear(); cells = new UIChessCell[ChessConst.BoardHeight, ChessConst.BoardWidth]; gridValid = false; cellStep = 0f; interactable = true; } // ================= 资源取用(供棋子控件访问) ================= /// 取某方某类型棋子的展示图(未配置返回 null) public Sprite GetTypeSprite(ChessCamp camp, ChessPieceType type) { var arr = camp == ChessCamp.Red ? sprite_RedPieces : sprite_BlackPieces; int idx = (int)type - 1; // ChessPieceType:Jiang=1 Shi=2 Xiang=3 Ma=4 Ju=5 Pao=6 Bing=7 if (arr == null || idx < 0 || idx >= arr.Length) return null; return arr[idx]; } /// 取某方棋子文字颜色 public Color GetTextColor(ChessCamp camp) { return camp == ChessCamp.Red ? color_RedText : color_BlackText; } // ================= 定位计算(核心) ================= /// /// 由两个定位点计算网格:间距 = 两点距离,方向 = 起点→终点 为 y+(指向黑方) /// private void CalcGridFromPoints() { if (point_Start == null || point_End == null) { Debug.LogError("[UIChessBoard] 未配置定位点 point_Start / point_End!\n" + "摆法:楚河汉界下方左上角交叉点 = point_Start;上方左下角交叉点 = point_End(两点同列隔河)"); return; } Vector2 s = ToBoardLocal(point_Start); Vector2 e = ToBoardLocal(point_End); Vector2 delta = e - s; cellStep = delta.magnitude; if (cellStep < 1f) { Debug.LogError("[UIChessBoard] 两个定位点重合或距离过近(间距=" + cellStep + "),请检查摆放"); return; } gridStart = s; gridUp = delta.normalized; // 逻辑 y+ 方向(起点→终点,指向黑方) gridRight = new Vector2(gridUp.y, -gridUp.x); // 逻辑 x+ 方向(红方视角从左到右) gridAngle = Mathf.Atan2(gridUp.y, gridUp.x) * Mathf.Rad2Deg - 90f; pieceSize = cellStep * pieceScale; gridValid = true; Debug.Log($"[UIChessBoard] 定位成功:网格间距={cellStep:F2},起点=({s.x:F1}, {s.y:F1})," + $"将生成 90 个定位点 + 32 个棋子(棋子直径={pieceSize:F1})"); } /// 任意 Transform → 格层本地坐标(与格子/棋子的 anchoredPosition 同一坐标系) private Vector2 ToBoardLocal(Transform t) { return (Vector2)tr_Cells.InverseTransformPoint(t.position); } /// 物理网格坐标:从起始点(逻辑 0,4)出发按间距推算 private Vector2 PhysicalPos(int x, int y) { return gridStart + gridRight * (x * cellStep) + gridUp * ((y - 4) * cellStep); } /// 逻辑坐标 → 棋盘位置(黑方视角镜像:我方永远在下方) private Vector2 GetPos(int x, int y) { if (flipped) { x = ChessConst.BoardWidth - 1 - x; y = ChessConst.BoardHeight - 1 - y; } return PhysicalPos(x, y); } private Vector2 GetPos(Vector2Int p) { return GetPos(p.x, p.y); } // ================= 父子层级 ================= /// 生成容器层:线 → 格子 → 选中标记 → 棋子 → 提示 private void BuildContainers() { tr_Lines = CreateLayer("Lines"); tr_Cells = CreateLayer("Cells"); tr_UnderMarks = CreateLayer("UnderMarks"); tr_Pieces = CreateLayer("Pieces"); tr_Marks = CreateLayer("Marks"); } private RectTransform CreateLayer(string name) { var go = new GameObject(name, typeof(RectTransform)); var rt = go.GetComponent(); rt.SetParent(transform, false); rt.anchorMin = Vector2.zero; rt.anchorMax = Vector2.one; rt.offsetMin = Vector2.zero; rt.offsetMax = Vector2.zero; rt.pivot = new Vector2(0.5f, 0.5f); return rt; } // ================= 90 个棋盘定位点 ================= /// 按定位点生成全部格子(透明点击区 + 走子提示),支持拖格子预制体 private void BuildCells() { if (!gridValid) return; for (int y = 0; y < ChessConst.BoardHeight; y++) { for (int x = 0; x < ChessConst.BoardWidth; x++) { GameObject go; if (prefab_Cell != null) { go = Instantiate(prefab_Cell, tr_Cells); if (go.GetComponent() == null) { Debug.LogError("[UIChessBoard] 格子预制体根节点必须是 UI 物体(带 RectTransform)"); Destroy(go); continue; } } else { go = new GameObject("Cell", typeof(RectTransform), typeof(Image), typeof(Button)); go.GetComponent().color = new Color(1f, 1f, 1f, 0f); // 透明热区 } var crt = (RectTransform)go.transform; if (crt.parent != tr_Cells) crt.SetParent(tr_Cells, false); crt.anchoredPosition = GetPos(x, y); crt.sizeDelta = new Vector2(cellStep, cellStep); crt.localEulerAngles = new Vector3(0f, 0f, gridAngle); var cell = go.GetComponent(); if (cell == null) cell = go.AddComponent(); cell.Init(this, new Vector2Int(x, y), cellStep); cells[y, x] = cell; } } } /// 显示 90 个定位点小圆点(校准底图对齐用) private void BuildDebugPoints() { Sprite dot = ChessArtFactory.GetDotSprite(); for (int y = 0; y < ChessConst.BoardHeight; y++) { for (int x = 0; x < ChessConst.BoardWidth; x++) { var go = new GameObject("DebugPoint_" + x + "_" + y, typeof(RectTransform), typeof(Image)); var rt = (RectTransform)go.transform; rt.SetParent(tr_Marks, false); rt.anchoredPosition = PhysicalPos(x, y); rt.sizeDelta = new Vector2(cellStep * 0.16f, cellStep * 0.16f); var img = go.GetComponent(); img.sprite = dot; img.color = new Color(0f, 0.75f, 1f, 0.55f); img.raycastTarget = false; } } Debug.Log("[UIChessBoard] 已显示 90 个定位点(蓝色小点),确认与底图对齐后关闭 show_DebugPoints"); } // ================= 无底图时的代码绘线(可忽略) ================= /// 自备底图时不调用;无底图时代码绘制棋盘线(横线/竖线/河界断开/九宫/楚河汉界) private void BuildBoardLines() { if (!gridValid) return; Color lineColor = new Color32(0x5B, 0x3A, 0x1E, 0xFF); float thickness = Mathf.Max(2f, cellStep * 0.015f); // 棋盘底色 var brt = (RectTransform)transform; var bgGo = new GameObject("BoardBg", typeof(RectTransform), typeof(Image)); var bgRt = (RectTransform)bgGo.transform; bgRt.SetParent(tr_Lines, false); bgRt.anchorMin = bgRt.anchorMax = new Vector2(0.5f, 0.5f); bgRt.anchoredPosition = Vector2.zero; bgRt.sizeDelta = new Vector2(brt.rect.width, brt.rect.height); var bgImg = bgGo.GetComponent(); bgImg.color = new Color32(0xE9, 0xC5, 0x8C, 0xFF); bgImg.raycastTarget = false; // 10 条横线 for (int y = 0; y < ChessConst.BoardHeight; y++) DrawLine(PhysicalPos(0, y), PhysicalPos(8, y), thickness, lineColor); // 9 条竖线(中间 7 列在河界处断开) for (int x = 0; x < ChessConst.BoardWidth; x++) { if (x == 0 || x == 8) { DrawLine(PhysicalPos(x, 9), PhysicalPos(x, 0), thickness, lineColor); } else { DrawLine(PhysicalPos(x, 9), PhysicalPos(x, 5), thickness, lineColor); DrawLine(PhysicalPos(x, 4), PhysicalPos(x, 0), thickness, lineColor); } } // 九宫斜线 DrawLine(PhysicalPos(3, 0), PhysicalPos(5, 2), thickness, lineColor); DrawLine(PhysicalPos(5, 0), PhysicalPos(3, 2), thickness, lineColor); DrawLine(PhysicalPos(3, 7), PhysicalPos(5, 9), thickness, lineColor); DrawLine(PhysicalPos(5, 7), PhysicalPos(3, 9), thickness, lineColor); // 楚河 汉界 float riverY = (PhysicalPos(0, 4).y + PhysicalPos(0, 5).y) * 0.5f; CreateBoardText("楚河", new Vector2(PhysicalPos(2, 0).x, riverY), cellStep * 0.55f, lineColor); CreateBoardText("汉界", new Vector2(PhysicalPos(6, 0).x, riverY), cellStep * 0.55f, lineColor); } private void DrawLine(Vector2 a, Vector2 b, float thickness, Color color) { Vector2 dir = b - a; float len = dir.magnitude; if (len < 0.001f) return; float angle = Mathf.Atan2(dir.y, dir.x) * Mathf.Rad2Deg; var go = new GameObject("Line", typeof(RectTransform), typeof(Image)); var rt = (RectTransform)go.transform; rt.SetParent(tr_Lines, false); rt.localEulerAngles = new Vector3(0, 0, angle); rt.sizeDelta = new Vector2(len + thickness, thickness); rt.anchoredPosition = (a + b) * 0.5f; var img = go.GetComponent(); img.color = color; img.raycastTarget = false; } private void CreateBoardText(string text, Vector2 pos, float fontSize, Color color) { if (ResolvedFont == null) return; var go = new GameObject("Text_" + text, typeof(RectTransform)); var rt = (RectTransform)go.transform; rt.SetParent(tr_Lines, false); rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f); rt.anchoredPosition = pos; rt.sizeDelta = new Vector2(cellStep * 3f, fontSize * 1.8f); var t = go.AddComponent(); t.font = ResolvedFont; t.text = text; t.fontSize = fontSize; t.alignment = TextAlignmentOptions.Center; t.color = color; t.raycastTarget = false; } // ================= 棋子生成 ================= /// /// 按棋盘数据生成全部存活棋子: /// 有 prefab_Piece(只带 Image + Button)→ 实例化后动态加 UIChessPiece; /// 没有 → 代码生成默认棋子(米色圆 + 汉字) /// private void BuildPieces() { ClearPieces(); if (!gridValid) return; foreach (var pd in gameManager.board.allPieces) { if (!pd.isAlive) continue; GameObject go; if (prefab_Piece != null) { go = Instantiate(prefab_Piece, tr_Pieces); if (go.GetComponent() == null) { Debug.LogError("[UIChessBoard] 棋子预制体根节点必须是 UI 物体(带 RectTransform)"); Destroy(go); continue; } } else { go = new GameObject("Piece", typeof(RectTransform), typeof(Image), typeof(Button)); } var prt = (RectTransform)go.transform; if (prt.parent != tr_Pieces) prt.SetParent(tr_Pieces, false); prt.sizeDelta = new Vector2(pieceSize, pieceSize); prt.anchoredPosition = GetPos(pd.pos); prt.localEulerAngles = new Vector3(0f, 0f, gridAngle); // 预制体上没有任何脚本,这里动态添加 var piece = go.GetComponent(); if (piece == null) piece = go.AddComponent(); if (prefab_Piece == null) SetupDefaultPieceVisual(piece); // 代码生成时补默认子物体 piece.Init(this, pd); pieceMap[pd.id] = piece; } } /// 代码生成棋子时的默认子物体(选中圈 + 汉字) private void SetupDefaultPieceVisual(UIChessPiece piece) { var go = piece.gameObject; // 选中圈 var sgo = new GameObject("SelectRing", typeof(RectTransform), typeof(Image)); var srt = (RectTransform)sgo.transform; srt.SetParent(go.transform, false); srt.anchorMin = srt.anchorMax = new Vector2(0.5f, 0.5f); srt.anchoredPosition = Vector2.zero; srt.sizeDelta = new Vector2(pieceSize * 1.30f, pieceSize * 1.30f); var simg = sgo.GetComponent(); simg.sprite = ChessArtFactory.GetRingSprite(); simg.color = new Color(1f, 0.84f, 0.31f, 0.95f); simg.raycastTarget = false; sgo.SetActive(false); piece.img_Select = simg; // 汉字 if (ResolvedFont != null) { var tgo = new GameObject("Label", typeof(RectTransform)); var trt = (RectTransform)tgo.transform; trt.SetParent(go.transform, false); trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one; trt.offsetMin = Vector2.zero; trt.offsetMax = Vector2.zero; var t = tgo.AddComponent(); t.font = ResolvedFont; t.alignment = TextAlignmentOptions.Center; t.enableAutoSizing = true; t.fontSizeMin = 8f; t.fontSizeMax = Mathf.Max(10f, pieceSize * 0.55f); piece.tmp_Label = t; } } private void ClearPieces() { foreach (var kv in pieceMap) if (kv.Value != null) Destroy(kv.Value.gameObject); pieceMap.Clear(); } /// 悔棋后全量刷新棋子(按数据层状态重建,最可靠) public void RefreshAllPieces() { ClearSelection(); selectedPiece = null; BuildPieces(); HideLastMoveMark(); } private void BuildToast() { var go = new GameObject("Toast", typeof(RectTransform)); var rt = (RectTransform)go.transform; rt.SetParent(tr_Marks, false); rt.anchorMin = rt.anchorMax = new Vector2(0.5f, 0.5f); rt.anchoredPosition = new Vector2(0f, cellStep * 2.2f); rt.sizeDelta = new Vector2(cellStep * 9f, cellStep * 1.2f); tmp_Toast = go.AddComponent(); if (ResolvedFont != null) tmp_Toast.font = ResolvedFont; tmp_Toast.alignment = TextAlignmentOptions.Center; tmp_Toast.fontSize = cellStep * 0.50f; tmp_Toast.color = new Color(0.80f, 0.16f, 0.12f, 1f); tmp_Toast.raycastTarget = false; go.SetActive(false); } // ================= 点击交互 ================= public void OnPieceClicked(UIChessPiece piece) { if (!interactable || gameManager == null || gameManager.isGameOver) return; if (piece == null || piece.data == null) return; if (piece.data.camp == myCamp) { // 点自己的子:选中并显示走点 if (!gameManager.IsMyTurn) { ShowToast("还没轮到你走棋"); return; } if (selectedPiece == piece) { ClearSelection(); return; } SelectPiece(piece); } else { // 点对方的子:若它在当前选中子的合法落点里 → 吃子 if (selectedPiece != null && selectedMoves.Contains(piece.data.pos)) { gameManager.TryLocalMove(selectedPiece.data.pos, piece.data.pos); } ClearSelection(); } } public void OnCellClicked(UIChessCell cell) { if (!interactable || gameManager == null || gameManager.isGameOver) return; if (selectedPiece == null || cell == null) return; if (selectedMoves.Contains(cell.gridPos)) { gameManager.TryLocalMove(selectedPiece.data.pos, cell.gridPos); } ClearSelection(); } private void SelectPiece(UIChessPiece piece) { ClearSelection(); selectedPiece = piece; piece.SetSelected(true); ShowSelectMark(piece); // 点击选中的棋子下面显示标记图 selectedMoves.AddRange(gameManager.rules.GetLegalMoves(gameManager.board, piece.data)); for (int i = 0; i < selectedMoves.Count; i++) { var target = gameManager.board.GetPiece(selectedMoves[i]); cells[selectedMoves[i].y, selectedMoves[i].x].ShowHint(target != null); } } private void ClearSelection() { if (selectedPiece != null) selectedPiece.SetSelected(false); selectedPiece = null; selectedMoves.Clear(); HideSelectMark(); for (int y = 0; y < ChessConst.BoardHeight; y++) for (int x = 0; x < ChessConst.BoardWidth; x++) if (cells[y, x] != null) cells[y, x].HideHint(); } /// 选中标记:显示在点击的棋子下面(拖了 sprite_HintDot 才显示) private void ShowSelectMark(UIChessPiece piece) { if (sprite_HintDot == null || piece == null || piece.data == null || !gridValid) return; if (img_SelectMark == null) { var go = new GameObject("SelectMark", typeof(RectTransform), typeof(Image)); var rt = (RectTransform)go.transform; rt.SetParent(tr_UnderMarks, false); img_SelectMark = go.GetComponent(); img_SelectMark.raycastTarget = false; } img_SelectMark.sprite = sprite_HintDot; var mrt = (RectTransform)img_SelectMark.transform; mrt.anchoredPosition = GetPos(piece.data.pos); float s = cellStep * selectMarkScale; mrt.sizeDelta = new Vector2(s, s); mrt.localEulerAngles = new Vector3(0f, 0f, gridAngle); img_SelectMark.gameObject.SetActive(true); } private void HideSelectMark() { if (img_SelectMark != null) img_SelectMark.gameObject.SetActive(false); } // ================= 数据层事件 → 表现层 ================= /// 走子成功(本地或网络):动画 + 上一步标记 + 将军提示 private void OnChessMoveApplied(ChessMoveEventArgs e) { UIChessPiece pieceUI; if (pieceMap.TryGetValue(e.movePieceId, out pieceUI) && pieceUI != null) { var prt = (RectTransform)pieceUI.transform; prt.DOKill(); prt.DOAnchorPos(GetPos(e.move.toX, e.move.toY), 0.35f).SetEase(Ease.OutQuad); } UIChessPiece capUI; if (e.capturedPieceId >= 0 && pieceMap.TryGetValue(e.capturedPieceId, out capUI) && capUI != null) { var crt = (RectTransform)capUI.transform; crt.DOKill(); crt.DOScale(0f, 0.30f).SetDelay(0.18f).SetEase(Ease.InBack) .OnComplete(() => { if (capUI != null) capUI.gameObject.SetActive(false); }); } ShowLastMoveMark(e.move); if (e.isCheck) ShowToast("将军!"); } /// 悔棋完成:全量刷新 private void OnChessUndoApplied(int undoCount) { RefreshAllPieces(); } /// 对局结束:锁定交互 private void OnChessGameOver(ChessCamp winner) { SetInteractable(false); ShowToast(winner == myCamp ? "你赢了!" : "你输了"); } /// 回合切换 private void OnTurnChanged(ChessCamp camp) { if (camp == myCamp && gameManager != null && gameManager.moveCount > 0) ShowToast("轮到你走棋"); } // ================= 对外接口 ================= /// 显示居中提示文字(将军/轮到你/悔棋等) public void ShowToast(string msg) { if (tmp_Toast == null) return; tmp_Toast.text = msg; tmp_Toast.gameObject.SetActive(true); var rt = tmp_Toast.rectTransform; toastTween?.Kill(); rt.localScale = Vector3.one * 0.6f; toastTween = rt.DOScale(1f, 0.18f).SetEase(Ease.OutBack).OnComplete(() => { rt.DOScale(0.6f, 0.35f).SetDelay(1.0f).SetEase(Ease.InBack).OnComplete(() => { if (tmp_Toast != null) tmp_Toast.gameObject.SetActive(false); }); }); } /// 锁定/解锁棋盘交互(对局结束时锁定) public void SetInteractable(bool value) { interactable = value; foreach (var kv in pieceMap) if (kv.Value != null) kv.Value.SetInteractable(value); if (!value) ClearSelection(); } // ================= 内部工具 ================= /// 上一步标记(起点/终点,可用自己的图) private void ShowLastMoveMark(ChessMoveData move) { if (lastFromMark == null || lastToMark == null) { lastFromMark = CreateMark("LastFrom", sprite_LastFromMark, new Color(1f, 0.55f, 0f, 0.30f)); lastToMark = CreateMark("LastTo", sprite_LastToMark, new Color(1f, 0.55f, 0f, 0.45f)); } PlaceMark(lastFromMark, new Vector2Int(move.fromX, move.fromY)); PlaceMark(lastToMark, new Vector2Int(move.toX, move.toY)); } private void HideLastMoveMark() { if (lastFromMark != null) lastFromMark.gameObject.SetActive(false); if (lastToMark != null) lastToMark.gameObject.SetActive(false); } private RectTransform CreateMark(string name, Sprite sprite, Color fallbackColor) { var go = new GameObject(name, typeof(RectTransform), typeof(Image)); var rt = (RectTransform)go.transform; rt.SetParent(tr_Marks, false); rt.sizeDelta = new Vector2(cellStep * lastMarkScale, cellStep * lastMarkScale); var img = go.GetComponent(); img.sprite = sprite; // 拖了图用图,没拖保持代码生成的透明方块 img.color = sprite != null ? Color.white : fallbackColor; // 用图时不染色 img.raycastTarget = false; return rt; } private void PlaceMark(RectTransform mark, Vector2Int pos) { mark.anchoredPosition = GetPos(pos); mark.gameObject.SetActive(true); } }