Files
5.24BuYu/Assets/MyGame/aa/UI/Game/ChineseChess/ChessControl/UIChessBoard.cs
T
2026-09-13 01:40:32 +08:00

753 lines
30 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using DG.Tweening;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 棋盘控件(挂在 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 个半透明小点,用于校准定位点与底图对齐
/// </summary>
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; // 提示层(上一步标记/提示文字)
/// <summary>解析后的字体(font_Piece 或 TMP 默认字体)</summary>
public TMP_FontAsset ResolvedFont { get; private set; }
// 格子缓存:按逻辑坐标存 [y,x]
private UIChessCell[,] cells = new UIChessCell[ChessConst.BoardHeight, ChessConst.BoardWidth];
// 棋子缓存:pieceId → 控件
private readonly Dictionary<int, UIChessPiece> pieceMap = new Dictionary<int, UIChessPiece>();
private ChessGameManager gameManager;
private ChessCamp myCamp;
private bool flipped; // 我方为黑方时,逻辑坐标镜像映射(我方永远在下方)
private UIChessPiece selectedPiece;
private readonly List<Vector2Int> selectedMoves = new List<Vector2Int>();
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;
/// <summary>
/// 初始化棋盘(每次开一局调用一次)
/// </summary>
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 ? "红方先行" : "对方先行");
}
/// <summary>清空棋盘(对局结束/面板隐藏时调用,Init 内部也会先调一次保证可重入)</summary>
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;
}
// ================= 资源取用(供棋子控件访问) =================
/// <summary>取某方某类型棋子的展示图(未配置返回 null)</summary>
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];
}
/// <summary>取某方棋子文字颜色</summary>
public Color GetTextColor(ChessCamp camp)
{
return camp == ChessCamp.Red ? color_RedText : color_BlackText;
}
// ================= 定位计算(核心) =================
/// <summary>
/// 由两个定位点计算网格:间距 = 两点距离,方向 = 起点→终点 为 y+(指向黑方)
/// </summary>
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})");
}
/// <summary>任意 Transform → 格层本地坐标(与格子/棋子的 anchoredPosition 同一坐标系)</summary>
private Vector2 ToBoardLocal(Transform t)
{
return (Vector2)tr_Cells.InverseTransformPoint(t.position);
}
/// <summary>物理网格坐标:从起始点(逻辑 0,4)出发按间距推算</summary>
private Vector2 PhysicalPos(int x, int y)
{
return gridStart + gridRight * (x * cellStep) + gridUp * ((y - 4) * cellStep);
}
/// <summary>逻辑坐标 → 棋盘位置(黑方视角镜像:我方永远在下方)</summary>
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);
}
// ================= 父子层级 =================
/// <summary>生成容器层:线 → 格子 → 选中标记 → 棋子 → 提示</summary>
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<RectTransform>();
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 个棋盘定位点 =================
/// <summary>按定位点生成全部格子(透明点击区 + 走子提示),支持拖格子预制体</summary>
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<RectTransform>() == null)
{
Debug.LogError("[UIChessBoard] 格子预制体根节点必须是 UI 物体(带 RectTransform)");
Destroy(go);
continue;
}
}
else
{
go = new GameObject("Cell", typeof(RectTransform), typeof(Image), typeof(Button));
go.GetComponent<Image>().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<UIChessCell>();
if (cell == null) cell = go.AddComponent<UIChessCell>();
cell.Init(this, new Vector2Int(x, y), cellStep);
cells[y, x] = cell;
}
}
}
/// <summary>显示 90 个定位点小圆点(校准底图对齐用)</summary>
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<Image>();
img.sprite = dot;
img.color = new Color(0f, 0.75f, 1f, 0.55f);
img.raycastTarget = false;
}
}
Debug.Log("[UIChessBoard] 已显示 90 个定位点(蓝色小点),确认与底图对齐后关闭 show_DebugPoints");
}
// ================= 无底图时的代码绘线(可忽略) =================
/// <summary>自备底图时不调用;无底图时代码绘制棋盘线(横线/竖线/河界断开/九宫/楚河汉界)</summary>
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<Image>();
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<Image>();
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<TextMeshProUGUI>();
t.font = ResolvedFont;
t.text = text;
t.fontSize = fontSize;
t.alignment = TextAlignmentOptions.Center;
t.color = color;
t.raycastTarget = false;
}
// ================= 棋子生成 =================
/// <summary>
/// 按棋盘数据生成全部存活棋子:
/// 有 prefab_Piece(只带 Image + Button)→ 实例化后动态加 UIChessPiece;
/// 没有 → 代码生成默认棋子(米色圆 + 汉字)
/// </summary>
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<RectTransform>() == 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<UIChessPiece>();
if (piece == null) piece = go.AddComponent<UIChessPiece>();
if (prefab_Piece == null) SetupDefaultPieceVisual(piece); // 代码生成时补默认子物体
piece.Init(this, pd);
pieceMap[pd.id] = piece;
}
}
/// <summary>代码生成棋子时的默认子物体(选中圈 + 汉字)</summary>
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<Image>();
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<TextMeshProUGUI>();
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();
}
/// <summary>悔棋后全量刷新棋子(按数据层状态重建,最可靠)</summary>
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<TextMeshProUGUI>();
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();
}
/// <summary>选中标记:显示在点击的棋子下面(拖了 sprite_HintDot 才显示)</summary>
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<Image>();
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);
}
// ================= 数据层事件 → 表现层 =================
/// <summary>走子成功(本地或网络):动画 + 上一步标记 + 将军提示</summary>
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("将军!");
}
/// <summary>悔棋完成:全量刷新</summary>
private void OnChessUndoApplied(int undoCount)
{
RefreshAllPieces();
}
/// <summary>对局结束:锁定交互</summary>
private void OnChessGameOver(ChessCamp winner)
{
SetInteractable(false);
ShowToast(winner == myCamp ? "你赢了!" : "你输了");
}
/// <summary>回合切换</summary>
private void OnTurnChanged(ChessCamp camp)
{
if (camp == myCamp && gameManager != null && gameManager.moveCount > 0)
ShowToast("轮到你走棋");
}
// ================= 对外接口 =================
/// <summary>显示居中提示文字(将军/轮到你/悔棋等)</summary>
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);
});
});
}
/// <summary>锁定/解锁棋盘交互(对局结束时锁定)</summary>
public void SetInteractable(bool value)
{
interactable = value;
foreach (var kv in pieceMap)
if (kv.Value != null) kv.Value.SetInteractable(value);
if (!value) ClearSelection();
}
// ================= 内部工具 =================
/// <summary>上一步标记(起点/终点,可用自己的图)</summary>
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<Image>();
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);
}
}