聲明:本程序設計參考象棋巫師源碼(開發工具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