java二柱子 · 程式 ·

前端仔教你一步步實現人人對戰五子棋小遊戲「canvas詳細版」

線上地址--gobang online pc上使用谷歌瀏覽器比較友好@~@

代碼倉庫-- gobang tutorial 歡迎對此倉庫進行擴展或star啦 @~@

前置知識點:阮生的es6教程和 MDN的canvas教程

以上, 兵馬未動,糧草先行 。看官可以先體驗下小遊戲並且粗略了解下相關的知識點後(熟悉者可跳過,歡迎留言改進哈),再往下讀。

哦對了,我這裡有一套Web前端從入門到精通的全套資料,價值4980元,現免費送給大家

轉發此文,關注並私信小編「00」即可馬上領取



前言

本來是沒打算在掘金上再寫關於canvas版本的五子棋小遊戲文章的,因為之前已經在掘金上發表過類似的文章-- 談談前端實現五子棋遊戲 。最近團隊輪到自己分享,然而,在短短的一個星期的時間內沒有想到比較實際可行的知識點或者項目拿來分享,畢竟工作日還得搬磚。於是乎,自己利用周末的時間將五子棋小遊戲重新梳理了一波,整理成一個教程,使它成為自己在一個小時的分享會上面分享的乾貨。

秉承著 會就分享,不會就折騰 的宗旨。竟然已經整理了一波的教程,那就放出來給大夥指點指點。下面進入正題:

五子棋規則

五子棋的規則有點點複雜,我這裡就簡化並改寫成下面這幾條:

  1. 對局雙方各執一色棋子。
  2. 空棋盤開局。
  3. 黑先、白後或者白先、黑後,交替下子,每次只能下一子。
  4. 橫線、豎線或者斜線上有連續五個同一色的棋子,則遊戲結束。

正式比賽的規則,看官可以到五子棋_百度百科這裡了解。本博文的案例是以上面列出來的四條規則為基礎,來實現五子棋小遊戲的。

項目骨架

為了方便管理、擴展功能和編寫代碼,我這裡使用了 es6的class語法 ,面向對象的思想來實現。首先,自己定義一個類 Gobang ,如下:

class Gobang { // 這裡設置一個五子棋的類,統一管理代碼 // Gobang這個類的構造函數,options是在實例化的時候要傳過來的值 constructor(options={}){ // 設置參數的默認值,es6之前不允許這樣設置 this.options = options; // 初始化 this.init(); } // 初始化 init() { const { options } = this;// 結構賦值 console.log(options); // 列印出傳入的實例的配置選項 }}// 實例化對象let gobangInstance1 = new Gobang(); // 沒有傳配置項的時候let gobangInstance2 = new Gobang({ canvas: 'chess'}); // 傳配置項的時候複製代碼

上面的 Gobang 類中,包含了一個 constructor 和 init 方法。其中 constructor 方法是類默認的方法,通過 new 命令生成對象實例時候,自動調用該方法。一個類必須有一個 constructor方法,如果沒有顯式定義,一個空的 constructor 方法會默認添加。然後就是 init 方法了,這裡我是整個類的初始化的入口方法。

項目骨架 代碼在倉庫中對應的位置是 skeleton 。

繪製棋盤

棋盤,我們可以分為兩種,一種是視覺上的棋盤,另外一個是邏輯上的棋盤,你是看不見的。如下截圖:

首先,我們實現 20*20 的物理上的棋盤,並且配上一些樣式。當然,為了高可配置,我們使用上面代碼骨架上的 options 進行傳值:

// 實例化對象let gobang = new Gobang({ canvas: 'chess', // html中設定的畫布的id gobangStyle: { // 五子棋的一些樣式 padding: 30, // 邊和邊之間的距離 count: 20, // 棋盤的邊數,整數 borderColor: '#bfbfbf', // 描邊的顏色 }});複製代碼

然後就進行物理棋盤的繪製了,這裡是使用 canvas 的相關知識點,控制 畫筆 更改著筆點並畫線條:

// 繪製出物理棋盤drawChessBoard() { const context = this.chessboard.getContext('2d');// 獲取繪製上下文 const {padding, count, borderColor} = this.options.gobangStyle; // 設置棋盤的寬高 this.chessboard.width = this.chessboard.height = padding * count; // 設置畫筆的顏色 context.strokeStyle = borderColor; let half_padding = padding/2;// 考慮繪製的棋子展示的位置,所以要預留一些邊距,可以審查元素看下 // 畫棋盤 for(var i = 0; i < count; i++){ context.moveTo(half_padding+i*padding, half_padding); context.lineTo(half_padding+i*padding, padding*count-half_padding); context.stroke(); // 這裡繪製出的是豎軸 context.moveTo(half_padding, half_padding+i*padding); context.lineTo(count*padding-half_padding, half_padding+i*padding); context.stroke(); // 這裡繪製出的是橫軸 }}複製代碼

接著就是邏輯的棋盤的記錄了。這裡我使用了二維數組去記錄棋盤點的位置,比如 (0,0) 點對應的數組下標是 [0][0] ;然後 (1,2) 點對應的下標是 [1][2] ...以此類推。這裡在記錄好點之後,也為他們進行賦值為0,表示此處沒有落子,如果有落子,記錄為1(黑子)或2(白子)。具體邏輯棋盤代碼如下:

// 繪製邏輯矩陣棋盤initChessboardMatrix(){ const {count} = this.options.gobangStyle; const checkerboard = []; // 存在(x,y)矩陣點 for(let x = 0; x < count; x++){ checkerboard[x] = []; for(let y = 0; y < count; y++){ checkerboard[x][y] = 0; // 全部賦值為0,表示此坐標是沒有棋子的 } }}複製代碼

繪製棋盤 代碼在倉庫中對應的位置是 chess_board 。

繪製棋子

繪製棋子這個簡單。在標題中表明了是使用canvas的相關知識點,棋子是使用canvas來繪製的。具體用的canvas的知識點有 arc和createRadialGradient 方法。前者是繪製一個圓,後者是為這個圓添加顏色漸變效果,使得棋子看起來更加有質感。當然,這裡需要繪製黑白兩種顏色的棋子,需要有個flag來進行標識是否是黑色/白色,代碼中有介紹。

drawChessman(x , y, isBlack){// 繪製的(x,y)坐標,isBlack判斷是黑棋子還是白色棋子 const context = this.chessboard.getContext('2d'); context.beginPath(); context.arc(x, y, 10, 0, 2 * Math.PI);// 畫圓,半徑這裡設定為10px context.closePath(); // 為棋子添加漸變顏色 let gradient = context.createRadialGradient(x, y, 10, x-5, y-5, 0);// createRadialGradient(x1,y1,r1,x2,y2,r2)創建放射狀/圓形漸變對象。 if(isBlack){ // 黑子 gradient.addColorStop(0,'#0a0a0a'); // 開始的顏色 gradient.addColorStop(1,'#636766'); // 結束的顏色 }else{ // 白子 gradient.addColorStop(0,'#d1d1d1'); gradient.addColorStop(1,'#f9f9f9'); } context.fillStyle = gradient; context.fill();}複製代碼

對應的效果圖如下:

繪製棋子 代碼在倉庫中對應的位置是 chessman 。

落子實現人人對戰

在上一節中,只是講解了怎麼去繪製棋子。接下來我們要將繪製好的棋子放到要下在棋盤的相關點擊位置,並且實現黑白兩棋的交替下棋,也就是實現人人對戰啦。

首先,我們在初始化入口那裡先初始化下棋子的角色(是黑棋還是白棋),獲取單元格的寬度。

init() { // 角色,1是黑色棋子,2是白色棋子 this.role = options.role || 1; // 單個格子的寬高 this.lattice = { width: options.gobangStyle.padding, height: options.gobangStyle.padding };}複製代碼

接下來就可以實行點擊棋盤位置的計算了,獲取相關的邏輯棋盤的坐標點,之後在這個坐標點進行棋子的繪製:

// 監聽落子listenDownChessman() { // 監聽點擊棋盤對象事件 this.chessboard.onclick = event => { let {padding} = this.options.gobangStyle; // 獲取棋子的位置(x,y)坐標,如(0,0),(0,2) let { offsetX: x, offsetY: y, } = event; // 解構賦值 // console.log(x,y); x = Math.abs(Math.round((x-padding/2)/this.lattice.width));// 防止邊界的為負數,故取絕對值 y = Math.abs(Math.round((y-padding/2)/this.lattice.height)); // console.log(x,y); // 點擊的是棋盤,並且是空位置才可以落子 if(this.checkerboard[x][y] !== undefined && Object.is(this.checkerboard[x][y],0)){ // 更新矩陣值 this.checkerboard[x][y] = this.role; // 刻畫棋子 this.drawChessman(x,y,Object.is(this.role , 1)); // 切換棋子的角色 this.role = Object.is(this.role , 1) ? 2 : 1; } }}// 刻畫棋子drawChessman(x,y,isBlack) { const context = this.chessboard.getContext('2d'); const {padding} = this.options.gobangStyle; let half_padding = padding/2; context.beginPath(); context.arc(half_padding+x*padding,half_padding+y*padding,half_padding-2,0,2*Math.PI); let gradient = context.createRadialGradient(half_padding+x*padding+2,half_padding+y*padding-2,half_padding-2,half_padding+x*padding+2,half_padding+y*padding-2,0); if(isBlack){ gradient.addColorStop(0,'#0a0a0a'); gradient.addColorStop(1,'#636766'); }else{ gradient.addColorStop(0,'#d1d1d1'); gradient.addColorStop(1,'#f9f9f9'); } context.fillStyle = gradient; context.fill();}複製代碼

落子實現人人對戰 代碼在倉庫中對應的位置是 listen_chessman 。

實現悔棋

在雙方下棋中,允許對方或者自己對已經下的棋子進行調整,也就是悔棋,恢復上一步的操作,然後再重新下棋。實現悔棋功能的時候,需要知道下棋的歷史記錄和當前的落子步數和角色。

對於歷史的記錄,這裡對每一步的落子都使用一個對象進行存儲,並放到一個 history 的數組裡面進行保存:

init() { // 走棋的歷史記錄 this.history = []; // 當前步 this.currentStep = 0;}listenDownChessman() { ... // 落子之後有可能悔棋之後落子,這種情況下應該重置歷史記錄 this.history.length = this.currentStep; this.history.push({// 保存坐標和角色快照 x, y, role: this.role }); this.currentStep++; // 當前步驟自加 ...}複製代碼

然後在執行悔棋的時候,將前一個記錄的棋子的在棋盤上對應的ui給抹除掉就行了,不能將 history 中對應的位置移除哦,因為是要用到撤銷悔棋的啊。銷毀完棋子後,要對物理棋盤上的ui進行修補,修補的情況一共有九種:

  • 左上角棋盤
  • 左邊緣棋盤
  • 左下角棋盤
  • 下邊緣棋盤
  • 右下角棋盤
  • 右邊緣棋盤
  • 右上角棋盤
  • 上邊緣棋盤
  • 中間(非邊界)棋盤
// 悔棋regretChess() { // 找到最後一次記錄,回滾到上一次的ui狀態 if(this.history.length){ const prev = this.history[this.currentStep - 1]; if(prev){ const { x, y, role } = prev; // 銷毀棋子 this.minusStep(x,y); this.checkerboard[prev.x][prev.y] = 0; // 置空操作 this.currentStep--; // 步數自減 // 角色發生改變,下一步的下棋是該撤銷棋子的角色 this.role = Object.is(role,1) ? 1 : 2; } }}// 銷毀棋子minusStep(x, y) { const context = this.chessboard.getContext('2d'); const {padding, count} = this.options.gobangStyle; context.clearRect(x*padding, y*padding, padding,padding); // 修補刪除的棋盤位置 // 重畫該圓周圍的格子,對邊角的格式進行特殊的處理 let half_padding = padding/2; // 棋盤單元格的一半 if(x<=0 && y <=0){ // 情況比較多,一共九種情況 this.fixchessboard(half_padding,half_padding,half_padding,padding,half_padding,half_padding,padding,half_padding); }else if(x>=count-1 && y<=0){ this.fixchessboard(count*padding-half_padding,half_padding,count*padding-padding,half_padding,count*padding-half_padding,half_padding,count*padding-half_padding,padding); }else if(y>=count-1 && x <=0){ this.fixchessboard(15,count*padding-half_padding,half_padding,count*padding-padding,half_padding,count*padding-half_padding,padding,count*padding-half_padding); }else if(x>=count-1 && y >= count-1){ this.fixchessboard(count*padding-half_padding,count*padding-half_padding,count*padding-padding,count*padding-half_padding,count*padding-half_padding,count*padding-half_padding,count*padding-half_padding,count*padding-padding); }else if(x <=0 && y >0 && y <count-1){ this.fixchessboard(half_padding,padding*y+half_padding,padding,padding*y+half_padding,half_padding,padding*y,half_padding,padding*y+padding); }else if(y <= 0 && x > 0 && x < count-1){ this.fixchessboard(x*padding+half_padding,half_padding,x*padding+half_padding,padding,x*padding,half_padding,x*padding+padding,half_padding); }else if(x>=count-1 && y >0 && y < count-1){ this.fixchessboard(count*padding-half_padding,y*padding+half_padding,count*padding-padding,y*padding+half_padding,count*padding-half_padding,y*padding,count*padding-half_padding,y*padding+padding); }else if(y>=count-1 && x > 0 && x < count-1){ this.fixchessboard(x*padding+half_padding,count*padding-half_padding,x*padding+half_padding,count*padding-padding,x*padding,count*padding-half_padding,x*padding+padding,count*padding-half_padding); }else{ this.fixchessboard(half_padding+x*padding,y*padding,half_padding+x*padding,y*padding + padding,x*padding,y*padding+half_padding,(x+1)*padding,y*padding+half_padding) }}// 修補刪除後的棋盤fixchessboard (a , b, c , d , e , f , g , h){ const context = this.chessboard.getContext('2d'); const {borderColor, lineWidth} = this.options.gobangStyle; context.strokeStyle = borderColor; context.lineWidth = lineWidth; context.beginPath(); context.moveTo(a , b); context.lineTo(c , d); context.moveTo(e, f); context.lineTo(g , h); context.stroke();}複製代碼

實現悔棋 代碼在倉庫中對應的位置是 regret_chess 。

實現撤銷悔棋

有允許悔棋,那麼就有允許撤銷悔棋這樣子才合理。同悔棋功能,撤銷悔棋是需要知道下棋的歷史記錄和當前的步驟和棋子角色的。如下:

// 撤銷悔棋revokedRegretChess(){ const next = this.history[this.currentStep]; // 撤銷的點的下一個 if(next) { this.drawChessman(next.x, next.y, next.role === 1); // 在上次撤銷的點上畫棋 this.checkerboard[next.x][next.y] = next.role; this.currentStep++; // 當前步驟自加 this.role = Object.is(this.role, 1) ? 2 : 1; // 角色的切換 }}複製代碼

實現撤銷悔棋 代碼在倉庫中對應的位置是 revoked_regret_chess 。

勝利提示/遊戲結束

五子棋的的結束也就是必須要決出勝利者,或者是棋盤沒有位置可以下棋了。這裡考慮決出勝利為遊戲結束的切入點,上面也說到了如何才算是一方獲勝-- 橫線、豎線或者斜線上有連續五個同一色的棋子 。那麼我們就對這四種情況進行處理了,我們在矩陣中記錄當前點擊的數組點中是否有連續的五個1(黑子)或者連續的五個2(白子)即可。如下截圖的x軸獲勝,注意gif圖右側列印出來的數組內容:

四種獲勝的情況和或者的提示相關的代碼如下:

// 裁判觀察棋子,判斷獲勝一方checkReferee(x , y , role) { if((x == undefined)||(y == undefined)||(role==undefined)) return; // 連殺的分數,五個同一色的棋子連成一條直線就是勝利 let countContinuous = 0; const XContinuous = this.checkerboard.map(x => x[y]); // x軸上連殺 const YContinuous = this.checkerboard[x]; // y軸上連殺 const S1Continuous = []; // 存儲左斜線連殺 const S2Continuous = []; // 存儲右斜線連殺 this.checkerboard.forEach((_y,i) => { // 左斜線 const S1Item = _y[y - (x - i)]; if(S1Item !== undefined){ S1Continuous.push(S1Item); } // 右斜線 const S2Item = _y[y + (x - i)]; if(S2Item !== undefined) { S2Continuous.push(S2Item); } }); // 當前落棋點所在的X軸/Y軸/交叉斜軸,只要有能連起來的5個子的角色即有勝者 [XContinuous, YContinuous, S1Continuous, S2Continuous].forEach(axis => { if(axis.some((x, i) => axis[i] !== 0 && axis[i - 2] === axis[i - 1] && axis[i - 1] === axis[i] && axis[i] === axis[i + 1] && axis[i + 1] === axis[i + 2])) { countContinuous++ } }); // 如果贏了就給出提示 if(countContinuous){ this.win = true; let msg = (role == 1 ? '黑' : '白') + '子勝利:v:'; // 提示信息 this.result.innerText = msg; // 不允許再操作 this.chessboard.onclick = null; }}複製代碼

勝利提示/遊戲結束 代碼在倉庫中對應的位置是 winner_hint 。

嗯~至此,已經一步步講解完如何開發一個能夠在pc上愉快玩耍的休閒小遊戲-五子棋了。當然,很多的參數我都是設置在 options 這裡,其實為了更好的用戶體驗,你可以將這些設置在ui層面供用戶自行調節的;再者你可以在項目基礎上實現其他功能,比如人機對戰等。如果有什麼想法的話,歡迎下方留言或者前往此代碼倉庫 gobang-tutorial 進行相關動能補充或者完善@~@

聲明:文章觀點僅代表作者本人,PTTZH僅提供信息發布平台存儲空間服務。
喔!快樂的時光竟然這麼快就過⋯