FireMonkey3D之中國象棋程序(一)界面設計


聲明:本程序設計參考象棋巫師源碼(開發工具dephi 11,建議用delphi 10.3以上版本)。

  本章目標:

  • 制作一個可操作的圖形界面

  第一步我們設計圖形界面,顯示初始化棋局。效果如下圖:

 我們先做個3D象棋子控件(請看我的博客關於FireMonkey3D的文章:萬能控件Mesh詳解),源碼如下:

unit ChessPiece;

interface

uses
  System.SysUtils,System.Types,System.UITypes,System.Classes, FMX.Types, FMX.Controls3D, FMX.Objects3D,FMX.Types3D,
  FMX.Materials,System.Math.Vectors,FMX.Graphics,System.Math,System.RTLConsts;
type
  TChessPiece = class(TControl3D)
  private
    FMat:TLightMaterial;
    FBitmap:TTextureBitmap;
	  FChessName:string;
    FSide,FID:Byte;//ID為棋子序號
    FColor:TAlphaColor;
    procedure SetChessName(const Value:string);
    procedure SetSide(const Value:Byte);
	  procedure SetID(const Value:Byte);
    procedure DrawPiece;
  protected
    procedure Render; override;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
  published
    property ChessName:string  read FChessName write SetChessName;
	  property Side:Byte  read FSide write SetSide default 0;
	  property id:Byte read FID write SetID;
    property Cursor default crDefault;
    property DragMode default TDragMode.dmManual;
    property Position;
    property Scale;
    property RotationAngle;
    property Locked default False;
    property Width;
    property Height;
    property Depth nodefault;
    property Opacity nodefault;
    property Projection;
    property HitTest default True;
    property VisibleContextMenu default True;
    property Visible default True;
    property ZWrite default True;
    property OnDragEnter;
    property OnDragLeave;
    property OnDragOver;
    property OnDragDrop;
    property OnDragEnd;
    property OnClick;
    property OnDblClick;
    property OnMouseDown;
    property OnMouseMove;
    property OnMouseUp;
    property OnMouseWheel;
    property OnMouseEnter;
    property OnMouseLeave;
    property OnKeyDown;
    property OnKeyUp;
    property OnRender;
  end;
procedure Register;

implementation
procedure TChessPiece.DrawPiece;
var
  Rect:TRectF;
begin
  with FBitmap do
  begin
	Canvas.BeginScene;
  Clear($FFFFFFFF);
  Rect:=TRectF.Create(2,2,98,98);
  Canvas.Stroke.Thickness:=2;
  Canvas.Stroke.Color:=FColor;
	Canvas.DrawEllipse(Rect,1);
  Canvas.Fill.Color:=FColor;
	Canvas.FillText(Rect,FChessName,false,1,[TFillTextFlag.RightToLeft],TTextAlign.Center,TTextAlign.Center);
	Canvas.EndScene;
  end;
  Repaint;
end;

constructor TChessPiece.Create(AOwner: TComponent);
begin
  inherited;
  FColor:=$FFFF0000;
  FChessName:='車';
  FMat:=TLightMaterial.Create;
  FMat.Emissive:=TAlphaColorRec.Burlywood;
  FBitmap:=TTextureBitmap.Create;
  with FBitmap do
  begin
    SetSize(100,200);
    Canvas.Font.Family:='方正隸書繁體';
	  Canvas.Font.Size:=85;
  end;
  DrawPiece;
end;

destructor TChessPiece.Destroy;
begin
  FMat.Free;
  FBitmap.Free;
  inherited;
  
end;
procedure TChessPiece.SetChessName(const Value:string);
begin
  if FChessName <> Value then
  begin
    FChessName := Value;
    DrawPiece;
  end;
end;
procedure TChessPiece.SetSide(const Value:Byte);
begin
  if FSide <> Value then
  begin
    FSide := Value;
    case FSide of
         0:  FColor:=$FFFF0000;
         1:  FColor:=$FF24747D;
    end;
    DrawPiece;
  end;
end;
procedure TChessPiece.SetID(const Value:Byte);
begin
  if FID<>value then
     FID:=Value;
end;
procedure TChessPiece.Render;
var
  i,j,k,VH,VW,AA,BB,M:Integer;
  indice:array of  Integer;
  P,P1:TPoint3D;
  Ver:TVertexBuffer;
  Idx:TIndexBuffer;
  Pt:TPointF;
  Angle,H,D,R:Single;//H:前后圓的半徑Height/2,R:棋子周邊圓弧的半徑,D棋子的厚度Height/5
begin
  VH:=32;VW:=12;
  indice:=[0,1,3,0,3,2];
  H:=0.5*Height;
  D:=0.2*Height;
  R:=D/sin(DegToRad(48));
  FMat.Texture:=nil;
  FMat.Texture:=FBitmap.Texture;
  Ver:=TVertexBuffer.Create([TVertexFormat.Vertex,TVertexFormat.Normal,TVertexFormat.TexCoord0],VH*VW*4+VH*2);
  Idx:=TIndexBuffer.Create(VH*6*VW+VH*6-12,TIndexFormat.UInt32);
  AA:=0;BB:=0;
  //Around棋子周邊
  for I := 0 to VH-1 do
  for J := 0 to VW-1 do
  begin
    for k := 0 to 1 do
    begin
      Angle:=DegToRad((318-(j+k)*8));
      P:=Point3D(0,R*sin(Angle),R*Cos(Angle));
      P1:=P/R;
      P.Offset(0,-R*Sin(DegToRad(318))-H,0);
      Ver.Vertices[AA+k*2]:=P*TMatrix3D.CreateRotationZ(2*Pi/VH*i);
      Ver.Normals[AA+k*2]:=P1*TMatrix3D.CreateRotationZ(2*Pi/VH*i);
	    Ver.Vertices[AA+k*2+1]:=P*TMatrix3D.CreateRotationZ(2*Pi/VH*(i+1));
      Ver.Normals[AA+k*2+1]:=P1*TMatrix3D.CreateRotationZ(2*Pi/VH*(i+1));
	    //按橫向、縱向細分一個貼圖
      Ver.TexCoord0[AA+k*2]:=PointF(1/12*(J+k),I/128+0.5);
      Ver.TexCoord0[AA+k*2+1]:=PointF(1/12*(J+k),(I+1)/128+0.5);
    end;
    inc(AA,4);
    for k :=0 to 5 do
    begin
      Idx.Indices[BB]:=indice[k]+4*(BB div 6);
      inc(BB);
    end;
  end;
  //Front Back 前后圓
  M:=AA;
  for I := 0 to VH-1 do
  begin
    P:=Point3D(0,-H,-D);
    Ver.Vertices[AA]:=P*TMatrix3D.CreateRotationZ(2*Pi/VH*i);
    Ver.Normals[AA]:=Point3D(0,0,-1);
    Pt:=PointF(0,-0.5).Rotate(2*Pi/VH*i);
    Pt.Offset(0.5,0.5);
    Ver.TexCoord0[AA]:=PointF(Pt.x,Pt.y/2);;
    P:=Point3D(0,-H,D);
    Ver.Vertices[AA+1]:=P*TMatrix3D.CreateRotationZ(2*Pi/VH*i);
    Ver.Normals[AA+1]:=Point3D(0,0,1);
    Ver.TexCoord0[AA+1]:=PointF(Pt.x,Pt.y/2+0.5);
    Inc(AA,2);
  end;
  for I := 0 to VH-3 do
  begin
    Idx.Indices[BB]:=M+2+I*2;
    Idx.Indices[BB+1]:=M+4+I*2;
    Idx.Indices[BB+2]:=M;
    Idx.Indices[BB+3]:=M+5+I*2;
    Idx.Indices[BB+4]:=M+3+i*2;
    Idx.Indices[BB+5]:=M+1;
    Inc(BB,6);
  end;
  Context.DrawTriangles(ver,idx,FMat,Opacity);
  Ver.Free;
  Idx.Free;
end;
procedure Register;
begin
  RegisterComponents('3D Others', [TChessPiece]);
end;

end.

  1.1 棋盤表示

  中國象棋有10行9列,很自然地想到可以用10×9矩陣表示棋盤。界面左側棋盤為Image3D控件,加載一個做好的“棋盤.png”,設定其width、height分別為9、10,3D里的單位不是像素,根據估算,1個單位相當於50像素。同時放置一個TDummy控件,Name=PieceDy,用來放棋子。由於3D控件的特性,其MouseUp事件並不能確定位縱橫坐標值,所有我定義了csLy:array [0..9,0..8]  of TLayout3D控件,對應10×9矩陣,這樣通過點擊TLayout3D就可以知道所在格子的縱橫坐標。先把棋盤做好:

var
  I,J:Integer;
  csLy:array[0..9,0..8]of TLayOut3D;
begin
  ChessBg.Bitmap.LoadFromFile('棋盤.png');
  for i := 0 to 9 do
    for j := 0 to 8 do
    begin
      csLy[i,j]:=TLayout3D.Create(self);
      csLy[i,j].Parent:=PieceDy;
      csLy[i,j].Position.Point:=Point3D(j-4,i-4.5,0);//3D物體的原點在其中心,所以csLy要偏移
      csLy[i,j].SetSize(1,1,0);
      csLy[i,j].OnClick:=csBoard;//Click事件統一使用csBoard。
    end;
end;

1.2、棋子表示

為了與象棋巫師兼容,使用整數表示棋子: 

 //棋子編號
 const 
  csName: string = '車馬相仕帥仕相馬車炮炮兵兵兵兵兵車馬象士將士象馬車炮炮卒卒卒卒卒';
  PcCode: array[0..31] of Byte=    //棋子的編號
   (4,3,2,1,0,1,2,3,4,5,5,6,6,6,6,6,
    4,3,2,1,0,1,2,3,4,5,5,6,6,6,6,6);

  PIECE_KING    = 0;    //帥(將)
  PIECE_ADVISOR = 1;    //士(仕)
  PIECE_BISHOP  = 2;    //相(象)
  PIECE_KNIGHT  = 3;    //馬
  PIECE_ROOK    = 4;    //車
  PIECE_CANNON  = 5;    //炮
  PIECE_PAWN    = 6;    //兵(卒) 

3D程序設計時,32個棋子必須要有自己的id,否則不好調用,0-15為紅棋,16-31為黑棋,其順序按csName排列,現在把32個棋子建立起來: 

var chess:array[0..31] of TChessPiece;
for I :=0 to 31 do
  begin
    chess[i]:=TChessPiece.Create(Self);
    chess[i].Parent:=PieceDy;
    if i>15 then
       chess[i].side:=1;//side表示紅黑走棋方
    chess[i].ChessName:=csName[i+1];
    chess[i].ID:=i;
    chess[i].Height:=0.8;
    chess[i].OnClick:=csBoard;//同上
  end;

  1.3 字符串(或數組)表示局面

  根據UCCI標准,我們用一行字符串表示一個局面,這就是FEN文件格式串。中國象棋的初始局面可表示為:

rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 1

  紅色區域,表示棋盤布局,小寫表示黑方,大寫表示紅方。一個字母表示一個棋子,與上面定義的首字母對應,數字表示有n個空。本程序只解析紅色部分。“/”用來區分每一行,共10行。1c5c1解析起來就是:□炮□□□□□炮□,代碼:

procedure FromFen(FEN:string);
var
  i,j,k,id:Integer;
  Str,CODE:string;
  ss:TArray<string>;
begin
  CODE:='RNBAKABNRCCPPPPPrnbakabnrccppppp';
  ss:=FEN.Substring(0,FEN.IndexOf(' ')).Split(['/']);
  for I := 0 to 31 do
  begin
    chess[i].Visible:=False;
  end;
  for I := 0 to 9 do
  begin
    Str:=ss[i];
    k:=0;
    for j := 1 to Length(Str) do
    begin
      id:=CODE.IndexOf(Str[j]);
      if id>=0 then
      begin
        chess[id].Visible:=True;
        chess[id].Position.Point:=Point3D(k-4,i-4.5,-0.16);
        CODE[id+1]:=' ';
        Inc(k);
      end
      else
        Inc(k,ord(Str[j])-$30);
    end;
  end;
end;  

 其實用數組簡單得多:

var
  i:Integer;
  P:TPoint;
const
  startPos: array[0..31] of Byte =//棋子的初始位置
    ($09, $19, $29, $39, $49, $59, $69, $79, $89, $17, $77, $06, $26, $46, $66, $86,
     $00, $10, $20, $30, $40, $50, $60, $70, $80, $12, $72, $03, $23, $43, $63, $83);
  
begin
  for I := 0 to 31 do
  begin
    chess[i].Visible:=False;
    chess[i].ResetRotationAngle;
    P:=Point(startPos[i] shr 4,startPos[i] and $F);
    chess[i].Position.Point:=Point3D(P.X-4,P.Y-4.5,-0.16);
    chess[i].Visible:=true;
   end;
end;

  1.4 走動棋子

我們讓棋子可以通過點擊實現走棋。點擊時,要考慮源點和目標點,選中的是哪個棋子等細節。定義全局變量:selecti=32,因為棋子的id是從0-31,零已被占用,就用32表示未選中棋子。棋子從源點的位置走到目標點的位置,即實現走棋。

首先要定義兩個函數:

var  
  selecti:Byte=32;
function GetPos(Pt:TPosition3D):TPoint;
begin
  Result:=Point(Round(Pt.X+4),Round(Pt.Y+4.5));
end;
function GetChessPos(i:Byte):TPoint;
var
  Pt:TPosition3D;
begin
  Pt:=chess[i].Position;
  Result:=Point(Round(Pt.X+4),Round(Pt.Y+4.5));
end;
//GetPos根據3D控件的位置,獲取其坐標
//GetChessPos根據chess的id,獲取其坐標 

   然后,定義csBoard鼠標點擊事件,我們已經把chess和csLy的點擊事件關聯到了csBoard,這樣只需要寫一個函數即可實現所有控件的點擊事件。這里要實現紅黑方輪流走棋,就要定義Player,0表示紅棋,1表黑棋,完成走棋后必須切換Player。我們用動畫實現走棋,體現走棋的過程。代碼如下:

var
  player:Byte=0;//默認0為紅,黑為1;
  Animator:TAnimator;//動畫控件
procedure TChessForm.MoveAni(i:integer;dest:TPoint);
begin
  Animator.AnimateFloat(chess[i],'Position.X',csLy[dest.Y,dest.X].Position.X,0.1);
  Animator.AnimateFloat(chess[i],'Position.Y',csLy[dest.Y,dest.X].Position.Y,0.1);
  Animator.AnimateFloatWait(chess[i],'Position.Z',-0.16,0.1);
end;
procedure ChangeSide;
begin
  Player:=1-Player;//換邊
end;
procedure csBoard(Sender: TObject);
var
  id:Byte;
  src,dest:TPoint;
begin
  if selecti=32 then
  begin
    if Sender is TLayout3D then Exit; //未選棋且點擊空白處
    id:=TChessPiece(Sender).id;
    if Player<>chess[id].Side then Exit; //未輪到某方走棋
    chess[id].Position.Z:=-0.5;//選中的棋“抬”起來,類似天天象棋的效果
    selecti:=id;
    Exit;
  end;
  if Sender is TChessPiece then   //已選中棋子,且dest點也是棋子
  begin
    id:=TChessPiece(Sender).id;
    if id=selecti then Exit;      //與選中棋子相同
    if Player=chess[id].Side then //與選中棋子同屬一個陣營
    begin
      chess[id].Position.Z:=-0.5;
      chess[selecti].Position.Z:=-0.16;
      selecti:=id;
      Exit;
    end;
  end;
  src:=GetChessPos(selecti);
  dest:=GetPos(TControl3D(Sender).Position);
  MoveAni(selecti,dest);
   if  Sender is TChessPiece then
  begin
    chess[id].Visible:=False;
  end;
  selecti:=32;
  changeSide;
end;

  說明:為了便於程序后續設計,所有常量及全局變量放在csCommn單元內,實現走棋的函數全面放在csPieceMove單元。csPieceMove定義了一個record:

type TPieceMove=record
  Player:Integer;//輪到誰走,0=紅方,1=黑方
  procedure Startup;                   //初始化棋盤
  procedure FromFen(FEN:string);       //從棋譜開局初始化棋盤
  procedure ChangeSide;                //換邊
end;

  使用記錄將函數、變量進行封裝,調用起來就非常簡單,不需要聲明函數(為何不用calss,class需要Create和Free,哪有記錄使用方便)。

  下一章實現目標:

  • 實現中國象棋規則

  本程序關鍵部分已講解,整個源碼共享在百度網盤:

  鏈接:中國象棋程序設計(一)界面設計
  提取碼:1234

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM