753 lines
30 KiB
C#
753 lines
30 KiB
C#
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);
|
||
}
|
||
}
|