3. 棋盘与棋子
2026/4/13大约 14 分钟
3. 棋盘与棋子:打造 3D 立体棋局
3.1 棋盘 3D 模型
棋盘是整个游戏的"地板"。我们要用 Godot 的内置几何体创建一个看起来像木质棋盘的 3D 模型。
为什么不导入外部模型?
你当然可以从 Blender 导入精美的棋盘模型,但为了教学目的,我们用代码生成。这样你能理解 3D 模型的本质——它就是一堆顶点、面和材质的组合。
两种方式的对比
| 方式 | 优点 | 缺点 |
|---|---|---|
| 代码生成 | 不依赖外部工具,学习价值高 | 美观度有限 |
| 外部导入 | 可以非常精美 | 需要学习 Blender 等 3D 建模软件 |
创建棋盘主体
棋盘主体就是一个扁扁的长方体,上面贴着木纹纹理。
C#
using Godot;
public partial class Board : Node3D
{
private const float CellSize = 1.0f;
private const int Cols = 9;
private const int Rows = 10;
// 棋盘尺寸
private const float BoardWidth = 8.0f; // 8列间距
private const float BoardHeight = 9.0f; // 9行间距
private const float BoardThickness = 0.3f; // 棋盘厚度
public override void _Ready()
{
CreateBoardBody();
CreateBoardSurface();
CreateBoardLines();
CreateRiverText();
}
/// <summary>
/// 创建棋盘主体(扁长方体)
/// </summary>
private void CreateBoardBody()
{
var mesh = new MeshInstance3D();
mesh.Name = "BoardBody";
// 创建一个扁扁的长方体
var boxMesh = new BoxMesh();
boxMesh.Size = new Vector3(
BoardWidth + 1.0f, // 左右留0.5的边距
BoardThickness,
BoardHeight + 1.0f // 上下留0.5的边距
);
mesh.Mesh = boxMesh;
// 木纹材质
var material = new StandardMaterial3D();
material.AlbedoColor = new Color(0.76f, 0.60f, 0.42f); // 木头颜色
material.Roughness = 0.8f; // 粗糙度(木头不反光)
mesh.MaterialOverride = material;
// 放在棋盘中心,稍微下沉(让表面在 y=0)
mesh.Position = new Vector3(0, -BoardThickness / 2, 0);
AddChild(mesh);
}
/// <summary>
/// 创建棋盘碰撞体(用于鼠标点击检测)
/// </summary>
private void CreateBoardSurface()
{
var body = new StaticBody3D();
body.Name = "BoardSurface";
var shape = new CollisionShape3D();
var boxShape = new BoxShape3D();
boxShape.Size = new Vector3(BoardWidth + 1.0f, 0.1f, BoardHeight + 1.0f);
shape.Shape = boxShape;
shape.Position = new Vector3(0, 0.05f, 0);
body.AddChild(shape);
AddChild(body);
}
/// <summary>
/// 创建棋盘线条
/// </summary>
private void CreateBoardLines()
{
var lineNode = new Node3D();
lineNode.Name = "BoardLines";
AddChild(lineNode);
float offsetX = BoardWidth / 2.0f;
float offsetZ = BoardHeight / 2.0f;
// 画横线(10条)
for (int row = 0; row < Rows; row++)
{
float z = row * CellSize - offsetZ;
CreateLine(
lineNode,
new Vector3(-offsetX, 0.01f, z),
new Vector3(offsetX, 0.01f, z),
new Color(0.2f, 0.15f, 0.1f)
);
}
// 画竖线(上半部分和下半部分各9条,中间河界断开)
for (int col = 0; col < Cols; col++)
{
float x = col * CellSize - offsetX;
// 上半部分(黑方)
CreateLine(
lineNode,
new Vector3(x, 0.01f, -offsetZ),
new Vector3(x, 0.01f, -0.5f * CellSize),
new Color(0.2f, 0.15f, 0.1f)
);
// 下半部分(红方)
CreateLine(
lineNode,
new Vector3(x, 0.01f, 0.5f * CellSize),
new Vector3(x, 0.01f, offsetZ),
new Color(0.2f, 0.15f, 0.1f)
);
}
// 最左边和最右边的竖线贯穿(不过河也连着)
CreateLine(lineNode,
new Vector3(-offsetX, 0.01f, -0.5f * CellSize),
new Vector3(-offsetX, 0.01f, 0.5f * CellSize),
new Color(0.2f, 0.15f, 0.1f));
CreateLine(lineNode,
new Vector3(offsetX, 0.01f, -0.5f * CellSize),
new Vector3(offsetX, 0.01f, 0.5f * CellSize),
new Color(0.2f, 0.15f, 0.1f));
}
/// <summary>
/// 在两点之间画一条线
/// </summary>
private void CreateLine(Node3D parent, Vector3 from, Vector3 to, Color color)
{
var meshInstance = new MeshInstance3D();
var lineMesh = new ArrayMesh();
var arrays = new Godot.Collections.Array();
arrays.Resize((int)Mesh.ArrayType.Max);
var vertices = new Vector3[] { from, to };
arrays[(int)Mesh.ArrayType.Vertex] = vertices;
lineMesh.AddSurfaceFromArrays(Mesh.PrimitiveType.Lines, arrays);
meshInstance.Mesh = lineMesh;
var mat = new StandardMaterial3D();
mat.AlbedoColor = color;
mat.ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded;
meshInstance.MaterialOverride = mat;
parent.AddChild(meshInstance);
}
/// <summary>
/// 创建"楚河汉界"文字
/// </summary>
private void CreateRiverText()
{
// 使用 Label3D 在棋盘河界位置显示文字
var leftLabel = new Label3D();
leftLabel.Text = "楚 河";
leftLabel.Position = new Vector3(-2.0f, 0.02f, 0);
leftLabel.RotationDegrees = new Vector3(-90, 0, 0);
leftLabel.FontSize = 32;
leftLabel.Modulate = new Color(0.2f, 0.15f, 0.1f);
AddChild(leftLabel);
var rightLabel = new Label3D();
rightLabel.Text = "漢 界";
rightLabel.Position = new Vector3(2.0f, 0.02f, 0);
rightLabel.RotationDegrees = new Vector3(-90, 0, 0);
rightLabel.FontSize = 32;
rightLabel.Modulate = new Color(0.2f, 0.15f, 0.1f);
AddChild(rightLabel);
}
}GDScript
extends Node3D
const CELL_SIZE: float = 1.0
const COLS: int = 9
const ROWS: int = 10
# 棋盘尺寸
const BOARD_WIDTH: float = 8.0 # 8列间距
const BOARD_HEIGHT: float = 9.0 # 9行间距
const BOARD_THICKNESS: float = 0.3 # 棋盘厚度
func _ready():
create_board_body()
create_board_surface()
create_board_lines()
create_river_text()
## 创建棋盘主体(扁长方体)
func create_board_body():
var mesh := MeshInstance3D.new()
mesh.name = "BoardBody"
# 创建一个扁扁的长方体
var box_mesh := BoxMesh.new()
box_mesh.size = Vector3(
BOARD_WIDTH + 1.0, # 左右留0.5的边距
BOARD_THICKNESS,
BOARD_HEIGHT + 1.0 # 上下留0.5的边距
)
mesh.mesh = box_mesh
# 木纹材质
var material := StandardMaterial3D.new()
material.albedo_color = Color(0.76, 0.60, 0.42) # 木头颜色
material.roughness = 0.8 # 粗糙度(木头不反光)
mesh.material_override = material
# 放在棋盘中心,稍微下沉(让表面在 y=0)
mesh.position = Vector3(0, -BOARD_THICKNESS / 2.0, 0)
add_child(mesh)
## 创建棋盘碰撞体(用于鼠标点击检测)
func create_board_surface():
var body := StaticBody3D.new()
body.name = "BoardSurface"
var shape := CollisionShape3D.new()
var box_shape := BoxShape3D.new()
box_shape.size = Vector3(BOARD_WIDTH + 1.0, 0.1, BOARD_HEIGHT + 1.0)
shape.shape = box_shape
shape.position = Vector3(0, 0.05, 0)
body.add_child(shape)
add_child(body)
## 创建棋盘线条
func create_board_lines():
var line_node := Node3D.new()
line_node.name = "BoardLines"
add_child(line_node)
var offset_x: float = BOARD_WIDTH / 2.0
var offset_z: float = BOARD_HEIGHT / 2.0
# 画横线(10条)
for row in range(ROWS):
var z: float = row * CELL_SIZE - offset_z
create_line(line_node,
Vector3(-offset_x, 0.01, z),
Vector3(offset_x, 0.01, z),
Color(0.2, 0.15, 0.1))
# 画竖线(上半部分和下半部分各9条,中间河界断开)
for col in range(COLS):
var x: float = col * CELL_SIZE - offset_x
# 上半部分(黑方)
create_line(line_node,
Vector3(x, 0.01, -offset_z),
Vector3(x, 0.01, -0.5 * CELL_SIZE),
Color(0.2, 0.15, 0.1))
# 下半部分(红方)
create_line(line_node,
Vector3(x, 0.01, 0.5 * CELL_SIZE),
Vector3(x, 0.01, offset_z),
Color(0.2, 0.15, 0.1))
# 最左边和最右边的竖线贯穿
create_line(line_node,
Vector3(-offset_x, 0.01, -0.5 * CELL_SIZE),
Vector3(-offset_x, 0.01, 0.5 * CELL_SIZE),
Color(0.2, 0.15, 0.1))
create_line(line_node,
Vector3(offset_x, 0.01, -0.5 * CELL_SIZE),
Vector3(offset_x, 0.01, 0.5 * CELL_SIZE),
Color(0.2, 0.15, 0.1))
## 在两点之间画一条线
func create_line(parent: Node3D, from: Vector3, to: Vector3, color: Color):
var mesh_instance := MeshInstance3D.new()
var line_mesh := ArrayMesh.new()
var arrays := []
arrays.resize(Mesh.ARRAY_MAX)
var vertices := [from, to]
arrays[Mesh.ARRAY_VERTEX] = vertices
line_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_LINES, arrays)
mesh_instance.mesh = line_mesh
var mat := StandardMaterial3D.new()
mat.albedo_color = color
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
mesh_instance.material_override = mat
parent.add_child(mesh_instance)
## 创建"楚河汉界"文字
func create_river_text():
var left_label := Label3D.new()
left_label.text = "楚 河"
left_label.position = Vector3(-2.0, 0.02, 0)
left_label.rotation_degrees = Vector3(-90, 0, 0)
left_label.font_size = 32
left_label.modulate = Color(0.2, 0.15, 0.1)
add_child(left_label)
var right_label := Label3D.new()
right_label.text = "漢 界"
right_label.position = Vector3(2.0, 0.02, 0)
right_label.rotation_degrees = Vector3(-90, 0, 0)
right_label.font_size = 32
right_label.modulate = Color(0.2, 0.15, 0.1)
add_child(right_label)3.2 棋子 3D 模型
棋子用一个圆柱体来表示,上面贴着汉字纹理。就像真实的象棋棋子——圆圆的、扁扁的,正面刻着字。
棋子的结构
棋子 3D 模型结构:
___________
/ \ ← 顶面(贴着汉字纹理)
| | ← 侧面(纯色)
| "車" | ← 顶面显示棋子名称
| |
\___________/ ← 底面C#
using Godot;
public partial class Piece3D : Node3D
{
// 棋子类型枚举
public enum PieceType
{
King, // 将/帅
Advisor, // 士/仕
Elephant, // 象/相
Horse, // 马
Chariot, // 车
Cannon, // 炮
Pawn // 兵/卒
}
// 棋子颜色
public enum PieceColor
{
Red,
Black
}
// 棋子属性
[Export] public PieceType Type { get; set; }
[Export] public PieceColor Color { get; set; }
public int Col { get; set; } // 当前列位置
public int Row { get; set; } // 当前行位置
// 棋子尺寸
private const float PieceRadius = 0.4f;
private const float PieceHeight = 0.15f;
private MeshInstance3D _meshInstance;
private StaticBody3D _body;
public override void _Ready()
{
CreatePieceModel();
SetupInputDetection();
}
/// <summary>
/// 创建棋子3D模型
/// </summary>
private void CreatePieceModel()
{
_meshInstance = new MeshInstance3D();
_meshInstance.Name = "PieceMesh";
// 创建圆柱体
var cylinder = new CylinderMesh();
cylinder.TopRadius = PieceRadius;
cylinder.BottomRadius = PieceRadius;
cylinder.Height = PieceHeight;
_meshInstance.Mesh = cylinder;
// 设置材质(根据颜色不同)
var material = new StandardMaterial3D();
if (Color == PieceColor.Red)
{
material.AlbedoColor = new Color(0.85f, 0.2f, 0.15f); // 红色
}
else
{
material.AlbedoColor = new Color(0.15f, 0.15f, 0.15f); // 黑色
}
material.Roughness = 0.6f;
_meshInstance.MaterialOverride = material;
AddChild(_meshInstance);
// 创建顶面文字(使用 Label3D)
var label = new Label3D();
label.Name = "PieceLabel";
label.Text = GetPieceCharacter();
label.Position = new Vector3(0, PieceHeight / 2 + 0.01f, 0);
label.RotationDegrees = new Vector3(-90, 0, 0);
label.FontSize = 24;
label.OutlineSize = 5;
if (Color == PieceColor.Red)
{
label.Modulate = new Color(1, 1, 1); // 白字
label.OutlineModulate = new Color(0.6f, 0.1f, 0.1f);
}
else
{
label.Modulate = new Color(1, 1, 1); // 白字
label.OutlineModulate = new Color(0.1f, 0.1f, 0.1f);
}
AddChild(label);
}
/// <summary>
/// 设置鼠标点击检测
/// </summary>
private void SetupInputDetection()
{
_body = new StaticBody3D();
_body.Name = "ClickArea";
var shape = new CollisionShape3D();
var cylinderShape = new CylinderShape3D();
cylinderShape.Radius = PieceRadius;
cylinderShape.Height = PieceHeight;
shape.Shape = cylinderShape;
_body.AddChild(shape);
AddChild(_body);
// 连接输入信号
_body.InputEvent += OnInputEvent;
}
private void OnInputEvent(Node camera, InputEvent @event,
Vector3 position, Vector3 normal, long shapeIdx)
{
if (@event is InputEventMouseButton mouseEvent
&& mouseEvent.ButtonIndex == MouseButton.Left
&& mouseEvent.Pressed)
{
GD.Print($"点击了棋子:{GetPieceCharacter()} ({Col}, {Row})");
// 后续会发送信号给游戏管理器
}
}
/// <summary>
/// 获取棋子显示的汉字
/// </summary>
public string GetPieceCharacter()
{
return (Color, Type) switch
{
(PieceColor.Red, PieceType.King) => "帥",
(PieceColor.Red, PieceType.Advisor) => "仕",
(PieceColor.Red, PieceType.Elephant) => "相",
(PieceColor.Red, PieceType.Horse) => "馬",
(PieceColor.Red, PieceType.Chariot) => "車",
(PieceColor.Red, PieceType.Cannon) => "炮",
(PieceColor.Red, PieceType.Pawn) => "兵",
(PieceColor.Black, PieceType.King) => "將",
(PieceColor.Black, PieceType.Advisor) => "士",
(PieceColor.Black, PieceType.Elephant) => "象",
(PieceColor.Black, PieceType.Horse) => "馬",
(PieceColor.Black, PieceType.Chariot) => "車",
(PieceColor.Black, PieceType.Cannon) => "砲",
(PieceColor.Black, PieceType.Pawn) => "卒",
_ => "?"
};
}
/// <summary>
/// 移动棋子到指定棋盘坐标
/// </summary>
public void MoveTo(int col, int row)
{
Col = col;
Row = row;
Position = BoardCoords.ToWorldPosition(col, row);
}
}GDScript
extends Node3D
## 棋子类型枚举
enum PieceType {
KING, # 将/帅
ADVISOR, # 士/仕
ELEPHANT, # 象/相
HORSE, # 马
CHARIOT, # 车
CANNON, # 炮
PAWN # 兵/卒
}
## 棋子颜色枚举
enum PieceColor {
RED,
BLACK
}
## 棋子属性
@export var type: PieceType
@export var piece_color: PieceColor
var col: int # 当前列位置
var row: int # 当前行位置
# 棋子尺寸
const PIECE_RADIUS: float = 0.4
const PIECE_HEIGHT: float = 0.15
var _mesh_instance: MeshInstance3D
var _body: StaticBody3D
func _ready():
create_piece_model()
setup_input_detection()
## 创建棋子3D模型
func create_piece_model():
_mesh_instance = MeshInstance3D.new()
_mesh_instance.name = "PieceMesh"
# 创建圆柱体
var cylinder := CylinderMesh.new()
cylinder.top_radius = PIECE_RADIUS
cylinder.bottom_radius = PIECE_RADIUS
cylinder.height = PIECE_HEIGHT
_mesh_instance.mesh = cylinder
# 设置材质(根据颜色不同)
var material := StandardMaterial3D.new()
if piece_color == PieceColor.RED:
material.albedo_color = Color(0.85, 0.2, 0.15) # 红色
else:
material.albedo_color = Color(0.15, 0.15, 0.15) # 黑色
material.roughness = 0.6
_mesh_instance.material_override = material
add_child(_mesh_instance)
# 创建顶面文字(使用 Label3D)
var label := Label3D.new()
label.name = "PieceLabel"
label.text = get_piece_character()
label.position = Vector3(0, PIECE_HEIGHT / 2.0 + 0.01, 0)
label.rotation_degrees = Vector3(-90, 0, 0)
label.font_size = 24
label.outline_size = 5
if piece_color == PieceColor.RED:
label.modulate = Color(1, 1, 1) # 白字
label.outline_modulate = Color(0.6, 0.1, 0.1)
else:
label.modulate = Color(1, 1, 1) # 白字
label.outline_modulate = Color(0.1, 0.1, 0.1)
add_child(label)
## 设置鼠标点击检测
func setup_input_detection():
_body = StaticBody3D.new()
_body.name = "ClickArea"
var shape := CollisionShape3D.new()
var cylinder_shape := CylinderShape3D.new()
cylinder_shape.radius = PIECE_RADIUS
cylinder_shape.height = PIECE_HEIGHT
shape.shape = cylinder_shape
_body.add_child(shape)
add_child(_body)
# 连接输入信号
_body.input_event.connect(_on_input_event)
func _on_input_event(camera: Node, event: InputEvent,
position: Vector3, normal: Vector3, shape_idx: int):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
print("点击了棋子:%s (%d, %d)" % [get_piece_character(), col, row])
## 获取棋子显示的汉字
func get_piece_character() -> String:
var chars := {
[PieceColor.RED, PieceType.KING]: "帥",
[PieceColor.RED, PieceType.ADVISOR]: "仕",
[PieceColor.RED, PieceType.ELEPHANT]: "相",
[PieceColor.RED, PieceType.HORSE]: "馬",
[PieceColor.RED, PieceType.CHARIOT]: "車",
[PieceColor.RED, PieceType.CANNON]: "炮",
[PieceColor.RED, PieceType.PAWN]: "兵",
[PieceColor.BLACK, PieceType.KING]: "將",
[PieceColor.BLACK, PieceType.ADVISOR]: "士",
[PieceColor.BLACK, PieceType.ELEPHANT]: "象",
[PieceColor.BLACK, PieceType.HORSE]: "馬",
[PieceColor.BLACK, PieceType.CHARIOT]: "車",
[PieceColor.BLACK, PieceType.CANNON]: "砲",
[PieceColor.BLACK, PieceType.PAWN]: "卒",
}
var key := [piece_color, type]
return chars.get(key, "?")
## 移动棋子到指定棋盘坐标
func move_to(new_col: int, new_row: int):
col = new_col
row = new_row
position = BoardCoords.to_world_position(col, row)3.3 32 枚棋子数据结构
象棋共有 32 枚棋子。我们需要一个数据结构来记录每枚棋子的初始位置和类型。
初始位置表
| 行/列 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|---|
| 0 (黑底线) | 車 | 馬 | 象 | 士 | 將 | 士 | 象 | 馬 | 車 |
| 1 | |||||||||
| 2 | 砲 | 砲 | |||||||
| 3 | 卒 | 卒 | 卒 | 卒 | 卒 | ||||
| 4-5 | 楚河汉界 | ||||||||
| 6 | 兵 | 兵 | 兵 | 兵 | 兵 | ||||
| 7 | 炮 | 炮 | |||||||
| 8 | |||||||||
| 9 (红底线) | 車 | 馬 | 相 | 仕 | 帥 | 仕 | 相 | 馬 | 車 |
棋子初始化代码
C#
using Godot;
using System.Collections.Generic;
public partial class PieceManager : Node3D
{
private List<Piece3D> _allPieces = new();
private Piece3D[,] _board = new Piece3D[9, 10]; // [col, row]
public override void _Ready()
{
InitializePieces();
}
/// <summary>
/// 初始化所有 32 枚棋子
/// </summary>
private void InitializePieces()
{
// ========== 黑方棋子(上方,row 0-4)==========
// 黑方底线(row 0)
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Chariot, 0, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Horse, 1, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Elephant, 2, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Advisor, 3, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.King, 4, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Advisor, 5, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Elephant, 6, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Horse, 7, 0);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Chariot, 8, 0);
// 黑方炮(row 2)
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Cannon, 1, 2);
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Cannon, 7, 2);
// 黑方卒(row 3)
for (int col = 0; col <= 8; col += 2)
{
CreatePiece(Piece3D.PieceColor.Black, Piece3D.PieceType.Pawn, col, 3);
}
// ========== 红方棋子(下方,row 5-9)==========
// 红方兵(row 6)
for (int col = 0; col <= 8; col += 2)
{
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Pawn, col, 6);
}
// 红方炮(row 7)
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Cannon, 1, 7);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Cannon, 7, 7);
// 红方底线(row 9)
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Chariot, 0, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Horse, 1, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Elephant, 2, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Advisor, 3, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.King, 4, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Advisor, 5, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Elephant, 6, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Horse, 7, 9);
CreatePiece(Piece3D.PieceColor.Red, Piece3D.PieceType.Chariot, 8, 9);
}
/// <summary>
/// 创建一枚棋子并放到棋盘上
/// </summary>
private void CreatePiece(Piece3D.PieceColor color, Piece3D.PieceType type, int col, int row)
{
var piece = new Piece3D();
piece.Color = color;
piece.Type = type;
piece.Col = col;
piece.Row = row;
piece.Position = BoardCoords.ToWorldPosition(col, row);
piece.Name = $"{color}_{type}_{col}_{row}";
AddChild(piece);
_allPieces.Add(piece);
_board[col, row] = piece;
}
/// <summary>
/// 获取指定位置的棋子
/// </summary>
public Piece3D GetPieceAt(int col, int row)
{
if (col < 0 || col > 8 || row < 0 || row > 9)
return null;
return _board[col, row];
}
/// <summary>
/// 在棋盘上移动棋子
/// </summary>
public void MovePieceOnBoard(Piece3D piece, int toCol, int toRow)
{
// 清除旧位置
_board[piece.Col, piece.Row] = null;
// 如果目标位置有棋子,那就是吃子
var targetPiece = _board[toCol, toRow];
if (targetPiece != null)
{
CapturePiece(targetPiece);
}
// 更新位置
_board[toCol, toRow] = piece;
piece.MoveTo(toCol, toRow);
}
/// <summary>
/// 吃子(移除被吃的棋子)
/// </summary>
private void CapturePiece(Piece3D piece)
{
_allPieces.Remove(piece);
_board[piece.Col, piece.Row] = null;
// 播放吃子动画后再移除
piece.QueueFree();
GD.Print($"吃子:{piece.GetPieceCharacter()}");
}
}GDScript
extends Node3D
var _all_pieces: Array[Piece3D] = []
var _board: Array # [col][row] 二维数组,存 Piece3D 或 null
func _ready():
# 初始化棋盘数组
_board = []
for c in range(9):
_board.append([])
_board[c].resize(10)
_board[c].fill(null)
initialize_pieces()
## 初始化所有 32 枚棋子
func initialize_pieces():
# ========== 黑方棋子(上方,row 0-4)==========
# 黑方底线(row 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.CHARIOT, 0, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.HORSE, 1, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.ELEPHANT, 2, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.ADVISOR, 3, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.KING, 4, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.ADVISOR, 5, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.ELEPHANT, 6, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.HORSE, 7, 0)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.CHARIOT, 8, 0)
# 黑方炮(row 2)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.CANNON, 1, 2)
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.CANNON, 7, 2)
# 黑方卒(row 3)
for col in range(0, 9, 2):
create_piece(Piece3D.PieceColor.BLACK, Piece3D.PieceType.PAWN, col, 3)
# ========== 红方棋子(下方,row 5-9)==========
# 红方兵(row 6)
for col in range(0, 9, 2):
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.PAWN, col, 6)
# 红方炮(row 7)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.CANNON, 1, 7)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.CANNON, 7, 7)
# 红方底线(row 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.CHARIOT, 0, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.HORSE, 1, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.ELEPHANT, 2, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.ADVISOR, 3, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.KING, 4, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.ADVISOR, 5, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.ELEPHANT, 6, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.HORSE, 7, 9)
create_piece(Piece3D.PieceColor.RED, Piece3D.PieceType.CHARIOT, 8, 9)
## 创建一枚棋子并放到棋盘上
func create_piece(color: Piece3D.PieceColor, type: Piece3D.PieceType, col: int, row: int):
var piece := Piece3D.new()
piece.piece_color = color
piece.type = type
piece.col = col
piece.row = row
piece.position = BoardCoords.to_world_position(col, row)
piece.name = "%s_%s_%d_%d" % [Piece3D.PieceColor.keys()[color],
Piece3D.PieceType.keys()[type], col, row]
add_child(piece)
_all_pieces.append(piece)
_board[col][row] = piece
## 获取指定位置的棋子
func get_piece_at(col: int, row: int) -> Piece3D:
if col < 0 or col > 8 or row < 0 or row > 9:
return null
return _board[col][row]
## 在棋盘上移动棋子
func move_piece_on_board(piece: Piece3D, to_col: int, to_row: int):
# 清除旧位置
_board[piece.col][piece.row] = null
# 如果目标位置有棋子,那就是吃子
var target_piece: Piece3D = _board[to_col][to_row]
if target_piece != null:
capture_piece(target_piece)
# 更新位置
_board[to_col][to_row] = piece
piece.move_to(to_col, to_row)
## 吃子(移除被吃的棋子)
func capture_piece(piece: Piece3D):
_all_pieces.erase(piece)
_board[piece.col][piece.row] = null
piece.queue_free()
print("吃子:%s" % piece.get_piece_character())3.4 棋子数据汇总
32 枚棋子完整列表:
| 编号 | 颜色 | 类型 | 显示字 | 列 | 行 | 能力描述 |
|---|---|---|---|---|---|---|
| 1 | 黑 | Chariot | 車 | 0 | 0 | 横冲直撞,直线任意距离 |
| 2 | 黑 | Horse | 馬 | 1 | 0 | 走日字,有蹩腿 |
| 3 | 黑 | Elephant | 象 | 2 | 0 | 走田字,有塞眼,不过河 |
| 4 | 黑 | Advisor | 士 | 3 | 0 | 斜走一格,限九宫 |
| 5 | 黑 | King | 將 | 4 | 0 | 直走一格,限九宫 |
| 6 | 黑 | Advisor | 士 | 5 | 0 | 斜走一格,限九宫 |
| 7 | 黑 | Elephant | 象 | 6 | 0 | 走田字,有塞眼,不过河 |
| 8 | 黑 | Horse | 馬 | 7 | 0 | 走日字,有蹩腿 |
| 9 | 黑 | Chariot | 車 | 8 | 0 | 横冲直撞,直线任意距离 |
| 10 | 黑 | Cannon | 砲 | 1 | 2 | 隔子吃,移动如车 |
| 11 | 黑 | Cannon | 砲 | 7 | 2 | 隔子吃,移动如车 |
| 12-16 | 黑 | Pawn | 卒 | 0,2,4,6,8 | 3 | 过河前只能前进 |
| 17 | 红 | Chariot | 車 | 0 | 9 | 横冲直撞,直线任意距离 |
| 18 | 红 | Horse | 馬 | 1 | 9 | 走日字,有蹩腿 |
| 19 | 红 | Elephant | 相 | 2 | 9 | 走田字,有塞眼,不过河 |
| 20 | 红 | Advisor | 仕 | 3 | 9 | 斜走一格,限九宫 |
| 21 | 红 | King | 帥 | 4 | 9 | 直走一格,限九宫 |
| 22 | 红 | Advisor | 仕 | 5 | 9 | 斜走一格,限九宫 |
| 23 | 红 | Elephant | 相 | 6 | 9 | 走田字,有塞眼,不过河 |
| 24 | 红 | Horse | 馬 | 7 | 9 | 走日字,有蹩腿 |
| 25 | 红 | Chariot | 車 | 8 | 9 | 横冲直撞,直线任意距离 |
| 26 | 红 | Cannon | 炮 | 1 | 7 | 隔子吃,移动如车 |
| 27 | 红 | Cannon | 炮 | 7 | 7 | 隔子吃,移动如车 |
| 28-32 | 红 | Pawn | 兵 | 0,2,4,6,8 | 6 | 过河前只能前进 |
3.5 棋盘状态表示
为了方便后续的规则判断和 AI 计算,我们需要一个简洁的方式来表示整个棋盘状态。
C#
/// <summary>
/// 棋盘状态 —— 记录当前局面所有棋子的位置
/// 就像拍了一张"快照",记录某一时刻棋盘上所有棋子的状态
/// </summary>
public class BoardState
{
// 使用二维数组记录每个位置的棋子(null 表示空)
public Piece3D.PieceType?[,] Grid { get; } = new Piece3D.PieceType?[9, 10];
public Piece3D.PieceColor?[,] Colors { get; } = new Piece3D.PieceColor?[9, 10];
// 记录将/帅的位置(快速查找)
public Vector2I RedKingPos { get; set; }
public Vector2I BlackKingPos { get; set; }
// 当前轮到谁走
public Piece3D.PieceColor CurrentTurn { get; set; }
/// <summary>
/// 从棋盘数据更新状态快照
/// </summary>
public void UpdateFromBoard(Piece3D[,] board)
{
for (int col = 0; col < 9; col++)
{
for (int row = 0; row < 10; row++)
{
var piece = board[col, row];
if (piece != null)
{
Grid[col, row] = piece.Type;
Colors[col, row] = piece.Color;
// 记录将/帅位置
if (piece.Type == Piece3D.PieceType.King)
{
if (piece.Color == Piece3D.PieceColor.Red)
RedKingPos = new Vector2I(col, row);
else
BlackKingPos = new Vector2I(col, row);
}
}
else
{
Grid[col, row] = null;
Colors[col, row] = null;
}
}
}
}
}GDScript
class_name BoardState
extends RefCounted
## 棋盘状态 —— 记录当前局面所有棋子的位置
## 就像拍了一张"快照",记录某一时刻棋盘上所有棋子的状态
# 使用二维数组记录每个位置的棋子类型(null 表示空)
var grid: Array # [col][row] -> PieceType 或 null
var colors: Array # [col][row] -> PieceColor 或 null
# 记录将/帅的位置(快速查找)
var red_king_pos: Vector2i
var black_king_pos: Vector2i
# 当前轮到谁走
var current_turn: Piece3D.PieceColor
func _init():
grid = []
colors = []
for c in range(9):
grid.append([])
grid[c].resize(10)
grid[c].fill(null)
colors.append([])
colors[c].resize(10)
colors[c].fill(null)
## 从棋盘数据更新状态快照
func update_from_board(board: Array):
for col in range(9):
for row in range(10):
var piece: Piece3D = board[col][row]
if piece != null:
grid[col][row] = piece.type
colors[col][row] = piece.piece_color
# 记录将/帅位置
if piece.type == Piece3D.PieceType.KING:
if piece.piece_color == Piece3D.PieceColor.RED:
red_king_pos = Vector2i(col, row)
else:
black_king_pos = Vector2i(col, row)
else:
grid[col][row] = null
colors[col][row] = null本章小结
| 要点 | 说明 |
|---|---|
| 棋盘模型 | BoxMesh 扁长方体 + 木纹材质 + 线条绘制 + 楚河汉界文字 |
| 棋子模型 | CylinderMesh 圆柱体 + 颜色材质 + Label3D 汉字 |
| 棋子数据 | 32 枚棋子的类型、颜色、初始位置 |
| 棋盘状态 | 二维数组存储棋盘快照,用于规则判断和 AI 计算 |
