Canvas實現(xiàn)二娃翠花回家之路小游戲demo解析
一、玩法介紹
Canvas是HTML5中的一個非常有用的技術,它可以用于實現(xiàn)各種圖形化效果。本文將介紹使用Canvas實現(xiàn)的小游戲——“二娃翠花回家之路”。這個小游戲非常有趣,玩家需要通過繪制角色的行走路線來控制他們的行動,并避免他們相撞。
- 玩家需要通過繪制二娃和翠花的行走路線,來控制他們的行動。
- 二娃和翠花需要分別回到各自的房子才能過關。
- 玩家需要避免二娃和翠花的行走的路上發(fā)生碰撞,否則游戲失敗。
- 玩家可以使用“開始”按鈕開始游戲,使用“重置”按鈕重新開始游戲。
二、預覽效果
三、開發(fā)難點
在實現(xiàn)“二娃翠花回家之路”小游戲的過程中,我遇到了如下幾個技術難點:如何繪制路徑不被頁面刷新影響、如何計算兩條路線交叉點最相近坐標距離、如何判斷碰撞、如何計算人物的移動速度和步長。
- 繪制路線:在Canvas中,通過監(jiān)聽鼠標事件獲取鼠標的坐標,并根據(jù)鼠標的移動軌跡來繪制路徑。在繪制路線時需要保存路徑的坐標,以便于后續(xù)的操作。需要考慮多個角色之間的交互,頁面刷新函數(shù)調用時機影響著路徑的繪制。
- 計算距離:需要計算二娃和翠花之間的距離,以及二娃和翠花與路線之間的距離。距離的計算需要使用勾股定理:
- 需要注意單位的轉換和精度的控制。
- 判斷碰撞:需要在角色行走時,判斷角色與另一個角色的距離是否小于一定的閾值。如果小于閾值,則需要根據(jù)一定的幾率避免碰撞,或者直接暫停游戲并提示失敗。需要考慮多個角色之間的相互作用。
- 人物移動速度和步長計算:需要計算人物與目標點之間的距離,以及人物的速度,計算出人物的移動步長。需要注意人物在路徑交叉點位置碰撞的問題。這里我的解決方案是:計算兩條路徑的最近的兩個坐標(因為交叉點不一定存在坐標,這是canvas繪制線條存在的可能性),然后拿到這兩個坐標,找出原路徑起點到該坐標的路徑保存起來,再計算路徑長度,得到長度用公式v=s/t的到速度v。
?? 劃重點: 針對人物移動速度和步長計算。我的實現(xiàn)方案雖然可以得到兩個人物的各自獨立的移動速度,但是仍然無法保證人物在路徑交叉點位置碰撞,這里我暫時沒有好的解決方案,希望各位掘友讀者們,能把代碼fork過去,幫忙解決這個問題,后在評論區(qū),附上你的解決方案。
四、核心實現(xiàn)步驟
1、創(chuàng)建畫布和按鍵元素
使用HTML和JavaScript來創(chuàng)建了一個畫布和兩個按鍵元素。首先創(chuàng)建了一個HTML文件,然后在其中添加了一個畫布和兩個按鍵。然后使用JavaScript來獲取畫布和按鍵元素,并設置了它們的屬性和事件監(jiān)聽器。最后為畫布創(chuàng)建了一個繪圖環(huán)境,并在畫布上繪制了兩個人物和他們的家。代碼實現(xiàn)在本文第四部分。
2、創(chuàng)建人物類
創(chuàng)建一個人物類character
,并在其中實現(xiàn)繪制角色draw()
、繪制家drawHouse()
、計算路徑總長度calculatePathLength(def_path)
這里def_path
是為了后面找到兩條路線交叉點最相近坐標到各自路線起點的路線備用的。計算兩個坐標的距離distance(x1,y1,x2,y2)
、人物移動move()
等方法。通過這些方法,我們可以實現(xiàn)人物的移動和路徑的劃分。在實現(xiàn)路徑劃分時,我們可以通過計算路徑總長度,將路徑劃分成若干個點,使角色在這些點之間移動。代碼實現(xiàn)在本文第四部分。
3、創(chuàng)建兩個人物實例
創(chuàng)建兩個人物實例,并設置它們的屬性和方法。我們將為每個人物實例設置起點、終點和當前位置,姓名和顏色屬性。在移動時判斷碰撞。
const A_Axis = [10, canvas.height - 20, canvas.width - 55, 10]; const B_Axis = [canvas.width - 10, canvas.height - 20, 5, 10]; const A = new Character('(紅)二娃', ...A_Axis, 'red'); const B = new Character('(藍)翠花', ...B_Axis, 'blue');
4、監(jiān)聽鼠標事件
在 Canvas 元素上監(jiān)聽鼠標事件,并根據(jù)鼠標的移動軌跡來繪制路徑。我們將使用鼠標事件監(jiān)聽器來獲取鼠標的坐標,并使用 Canvas API 來繪制路徑。在繪制路線時需要保存路徑的坐標,以便于后續(xù)的操作。
//在鼠標事件中會調用該方法繪制路徑 function drawPath(path, color, width) { ctx.beginPath(); ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) { ctx.lineTo(path[i].x, path[i].y); } ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineCap = 'round'; ctx.stroke(); ctx.closePath() } //事件監(jiān)聽 canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); resetBtn.addEventListener('click', resetGame); startBtn.addEventListener('click', startGame);
5、繪制路徑
根據(jù)繪制好的路徑,將路徑按照一定的長度劃分成若干個點。這些點可以作為人物移動的目標位置。然后,我們可以在每個點上計算出人物應該移動的目標位置,從而實現(xiàn)人物的移動。
在 Canvas 中,可以使用 moveTo
和 lineTo
方法來繪制路徑。使用 stroke
方法來繪制路徑。可以設定 lineWidth
和 strokeStyle
屬性來設置路徑的顏色和寬度。
算法部分,可以使用距離閾值來判斷兩個點之間的距離是否超過了閾值。如果超過了閾值,我們就將路徑劃分成兩部分,分別計算出每個部分的長度。然后,選擇較短的路徑作為人物移動的路徑。這樣可以避免人物走過太多的彎路,從而增加游戲的流暢度。
//核心部分 //...... //...... if (var_distance > DISTANCE_THRESHOLD) { this.x += dx / var_distance * speed; this.y += dy / var_distance * speed; } else { this.x = target.x; this.y = target.y; this.path.shift(); //...... //...... }
6、判斷碰撞
在角色行走時,判斷角色與另一個角色的距離是否小于一定的閾值。如果小于閾值,則需要根據(jù)一定的幾率避免碰撞,或者直接暫停游戲并提示失敗。需要考慮多個角色之間的相互作用。
//核心部分 //...... if (this.path.length > 0 && this.distance(target.x, target.y, this === A ? B.x : A.x, this === A ? B.y : A.y) < COLLISION_THRESHOLD) { const avoidCollision = Math.random() < COLLISION_AVOIDANCE_RATE; // 以一定的幾率避免碰撞 if (avoidCollision) { this.path.splice(0, 1); // 直接移動到下個點位 } else { gameStatus = GAME_PAUSE_STATUS; alert(`${this === A ? A.uname : B.uname} 碰到了對方,游戲失敗`); } }
7、開始和重置游戲
實現(xiàn)開始和重置游戲的功能,包括重置路徑、重置人物位置等。當游戲開始時,需要計算兩個人物最短的路徑,并將其保存到對應的路徑數(shù)組中。當游戲結束時,將人物位置重置,并清空路徑數(shù)組和繪制的路徑。
function resetGame() { A.x = A_Axis[0]; A.y = A_Axis[1]; A.path = []; A.moving = false; A.total_distance = 0; B.x = B_Axis[0]; B.y = B_Axis[1]; B.path = []; B.moving = false; B.total_distance = 0; drawing = false; path = []; gameStatus = GAME_LOOPING_STATUS; init(); } function startGame() { update(); if (!A.path.length || !B.path.length) { alert('請先繪制人物回家路線'); return; } let close_coord = getClosestCoords(A.path, B.path); console.log('得出的路徑:', ...close_coord) let A_def_path = getRoute(close_coord[0], A.path); let B_def_path = getRoute(close_coord[1], B.path); let A_def_path_len = A.calculatePathLength(A_def_path); let B_def_path_len = B.calculatePathLength(B_def_path); let A_B_def_path_max_len = Math.max(A_def_path_len, B_def_path_len); console.log('最長的是', A_def_path_len, B_def_path_len, '--->', A_B_def_path_max_len) A.total_distance = A_def_path_len; B.total_distance = B_def_path_len; drawing = false; path = []; A.moving = true; B.moving = true; }
8、實現(xiàn)動畫效果
動畫效果的實現(xiàn)主要是通過 requestAnimationFrame 方法來實現(xiàn)的。requestAnimationFrame 是一個用來優(yōu)化動畫效果的方法,可以讓動畫更流暢自然。具體地,requestAnimationFrame 方法會在下一幀動畫之前調用一個回調函數(shù),以便于更新動畫效果。在這個回調函數(shù)中,可以實現(xiàn)人物的移動、路徑的繪制等等,從而達到動畫效果。
動畫效果的實現(xiàn)主要在人物類(Character)中。每個人物都有自己的動畫狀態(tài)和動畫參數(shù),包括位置、速度、目標位置等等。在每一幀的動畫中,都會根據(jù)當前位置和目標位置之間的距離來計算移動的距離和速度,并且不斷更新人物的位置和狀態(tài)。這個過程中,使用了一些基本的數(shù)學計算,比如計算兩點之間的距離、計算兩點之間的角度等等。
function update() { if (!A.path.length && !B.path.length) { gameStatus = GAME_PAUSE_STATUS; ctx.clearRect(0, 0, canvas.width, canvas.height); A.drawHouse(); B.drawHouse(); A.draw(true); B.draw(true); } if (gameStatus === GAME_LOOPING_STATUS) { ctx.clearRect(0, 0, canvas.width, canvas.height); A.drawHouse(); B.drawHouse(); A.draw(); B.draw(); A.path.length && drawPath(A.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH); B.path.length && drawPath(B.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH); A.moving && A.move(); B.moving && B.move(); } requestId = requestAnimationFrame(update); }
五、完整的代碼實現(xiàn)
下面是實現(xiàn)該游戲的完整代碼,這里著重強調幾個關鍵函數(shù),在代碼中找到對應的**注釋**
,這跟我在上面文章第二部分劃重點的提問有關哦!。
主要的涉及的邏輯是,每個人物類有一個屬性total_distance
用于存儲距離,當用戶點擊開始回家按鈕后,在startGame
方法里會計算兩個人物回家路徑最近的兩個坐標點,并獲得這兩個坐標所在路徑到所在路徑起點的路徑坐標數(shù)組,然后計算該新路徑的長度保存到total_distance
中,在人物移動方法move()
中會計算人物移動速度和人物移動步長。
1. 頁面布局
頁面的布局相對簡單,基本就是一個畫布+按鈕。當然想要游戲體驗更好,可以更改頁面布局,增加更多互動元素。
<!DOCTYPE html> <html> <head> <title>二娃、翠花的回家之路</title> </head> <body> <h1>二娃和翠花的回家之路</h1> <div class="tool_bar"> <span>紅方:二娃</span> <span>藍方:翠花</span> <div> 攻略:鼠標繪制二娃、翠花的回家之路,不要讓他們相撞哦!<br />他們回家的速度看心情哦 </div> </div> <canvas id="canvas" width="600" height="500"></canvas> <div> <button id="startBtn">開始回家</button> <button id="resetBtn">重新開始</button> </div> <script src="game.js"></script> </body> </html>
2. 頁面css樣式
樣式的話,相信不用講太多,大家一看就知道了。
body { text-align: center; } canvas { border: 1px solid gray; border-radius: 5px; box-shadow: 0 0 20px 0px #ccc; margin: 10px 0 5px 0px; } #startBtn, #resetBtn { background-color: #4caf50; border: none; color: white; padding: 15px 32px; text-align: center; text-decoration: none; display: inline-block; font-size: 16px; margin: 4px 2px; cursor: pointer; } #resetBtn { background-color: red; } .tool_bar span { background-color: blue; color: white; padding: 2px 3px; margin: 0 5px; border-radius: 5px; } .tool_bar span:first-child { background-color: red; }
3. js代碼 (有點長,耐心點哈??)
這部分代碼著實有點長,需要點耐心來閱讀,利用代碼中的注釋或者變量和方法名稱來輔助理解。本來不想貼完整代碼的,但是,為了保證大家能夠更好的理解這個游戲,還是貼上來吧,相信可以第一時間閱讀完整代碼的感覺還是挺好的??。
// 游戲狀態(tài) const GAME_LOOPING_STATUS = 'looping'; const GAME_PAUSE_STATUS = 'pause'; // 繪制路線的顏色和寬度 const DRAW_LINE_COLOR = 'deepskyblue'; const DRAW_LINE_WIDTH = 8; // 碰撞的閾值 const COLLISION_THRESHOLD = 30; // 碰撞避免的概率 const COLLISION_AVOIDANCE_RATE = 0.001; // 路徑點之間的距離閾值 const DISTANCE_THRESHOLD = 1.5; let requestId = null,//動畫控制句柄 //畫布、按元素節(jié)點 const canvas = document.querySelector('#canvas'); const ctx = canvas.getContext('2d'); const startBtn = document.querySelector('#startBtn'); const resetBtn = document.querySelector('#resetBtn'); //人物類 class Character { path = []; oving = false; total_distance = 0; moveTime = 500; constructor(uname, x, y, houseX, houseY, color) { this.uname = uname; this.x = x; this.y = y; this.houseX = houseX; this.houseY = houseY; this.color = color; } //繪制角色 draw(isInit = false) { ctx.beginPath(); let h = isInit ? 20 : Math.random() * 30; let w = isInit ? 10 : Math.random() * 15; ctx.moveTo(this.x, this.y); ctx.lineTo(this.x - w, this.y + h); ctx.lineTo(this.x + w, this.y + h); ctx.closePath(); ctx.fillStyle = this.color; ctx.fill(); ctx.closePath() } //繪制角色的家 drawHouse() { ctx.beginPath(); ctx.arc(this.houseX + 25, this.houseY + 25, 25, 0, 2 * Math.PI); ctx.fillStyle = 'white'; ctx.fill(); ctx.strokeStyle = this.color; ctx.lineWidth = 5; ctx.stroke(); ctx.closePath() } // 計算路徑總長度 calculatePathLength(def_path) { let path = this.path if (Array.isArray(def_path) && def_path.length > 0) { path = def_path } let length = 0; for (let i = 1; i < path.length; i++) { length += this.distance(path[i].x, path[i].y, path[i - 1].x, path[i - 1].y); } return length; } //計算兩個坐標的距離 distance(x1, y1, x2, y2) { const dx = x1 - x2; const dy = y1 - y2; return Math.sqrt(dx * dx + dy * dy); } //人物移動 move() { if (this.path.length === 0) { this.moving = false; return; } const target = this.path[0]; const dx = target.x - this.x; const dy = target.y - this.y; const var_distance = this.distance(target.x, target.y, this.x, this.y); const speed = this.total_distance / this.moveTime; if (var_distance > DISTANCE_THRESHOLD) { this.x += dx / var_distance * speed; this.y += dy / var_distance * speed; } else { this.x = target.x; this.y = target.y; this.path.shift(); if (this.path.length > 0 && this.distance(target.x, target.y, this === A ? B.x : A.x, this === A ? B.y : A.y) < COLLISION_THRESHOLD) { const avoidCollision = Math.random() < COLLISION_AVOIDANCE_RATE; // 以一定的幾率避免碰撞 if (avoidCollision) { this.path.splice(0, 1); // 直接移動到下個點位 } else { gameStatus = GAME_PAUSE_STATUS; alert(`${this === A ? A.uname : B.uname} 碰到了對方,游戲失敗`); } } } } } const A_Axis = [10, canvas.height - 20, canvas.width - 55, 10]; const B_Axis = [canvas.width - 10, canvas.height - 20, 5, 10]; const A = new Character('(紅)二娃', ...A_Axis, 'red'); const B = new Character('(藍)翠花', ...B_Axis, 'blue'); let gameStatus = GAME_LOOPING_STATUS; let drawing = false; let path = []; //繪制路徑 function drawPath(path, color, width) { ctx.beginPath(); ctx.moveTo(path[0].x, path[0].y); for (let i = 1; i < path.length; i++) { ctx.lineTo(path[i].x, path[i].y); } ctx.strokeStyle = color; ctx.lineWidth = width; ctx.lineCap = 'round'; ctx.stroke(); ctx.closePath() } //計算兩條路徑最近的兩個坐標點 function getClosestCoords(arr1, arr2) { let minDistance = Number.MAX_VALUE; let closestCoords = []; for (let i = 0; i < arr1.length; i++) { for (let j = 0; j < arr2.length; j++) { const distance = Math.sqrt( Math.pow(arr1[i].x - arr2[j].x, 2) + Math.pow(arr1[i].y - arr2[j].y, 2) ); if (distance < minDistance) { minDistance = distance; closestCoords = [arr1[i], arr2[j]]; } } } return closestCoords; } //獲取坐標點到路徑起點的路徑數(shù)組 function getRoute(coord, targetRoute) { let res = []; for (let i = 0; i < targetRoute.length; i++) { res.push(targetRoute[i]) if (targetRoute[i].x === coord.x && targetRoute[i].y === coord.y) { return res; } } } //鼠標按下事件句柄 function handleMouseDown(event) { if (event.target.id === 'canvas') { if (A.distance(event.offsetX, event.offsetY, A.x, A.y) < COLLISION_THRESHOLD) { drawing = true; path.push({ x: A.x, y: A.y }); } else if (A.distance(event.offsetX, event.offsetY, B.x, B.y) < COLLISION_THRESHOLD) { drawing = true; path.push({ x: B.x, y: B.y }); } } } //鼠標移動事件句柄 function handleMouseMove(event) { if (drawing) { path.push({ x: event.offsetX, y: event.offsetY }); drawPath(path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH); } } //鼠標松開事件句柄 function handleMouseUp(event) { if (drawing) { if (A.distance(event.offsetX, event.offsetY, A.houseX + 25, A.houseY + 25) < 35) { path.push({ x: A.houseX + 25, y: A.houseY + 25 }); drawPath(path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH); A.path = path; } else if (A.distance(event.offsetX, event.offsetY, B.houseX + 25, B.houseY + 25) < 35) { path.push({ x: B.houseX + 25, y: B.houseY + 25 }); drawPath(path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH); B.path = path; } path = []; drawing = false; } } //重置游戲 function resetGame() { A.x = A_Axis[0]; A.y = A_Axis[1]; A.path = []; A.moving = false; A.total_distance = 0; B.x = B_Axis[0]; B.y = B_Axis[1]; B.path = []; B.moving = false; B.total_distance = 0; drawing = false; path = []; gameStatus = GAME_LOOPING_STATUS; init(); } //開始游戲 function startGame() { update(); if (!A.path.length || !B.path.length) { alert('請先繪制人物回家路線'); return; } let close_coord = getClosestCoords(A.path, B.path); console.log('得出的路徑:', ...close_coord) let A_def_path = getRoute(close_coord[0], A.path); let B_def_path = getRoute(close_coord[1], B.path); let A_def_path_len = A.calculatePathLength(A_def_path); let B_def_path_len = B.calculatePathLength(B_def_path); let A_B_def_path_max_len = Math.max(A_def_path_len, B_def_path_len); console.log('最長的是', A_def_path_len, B_def_path_len, '--->', A_B_def_path_max_len) A.total_distance = A_def_path_len; B.total_distance = B_def_path_len; drawing = false; path = []; A.moving = true; B.moving = true; } //刷新游戲界面 function update() { if (!A.path.length && !B.path.length) { gameStatus = GAME_PAUSE_STATUS; ctx.clearRect(0, 0, canvas.width, canvas.height); A.drawHouse(); B.drawHouse(); A.draw(true); B.draw(true); } if (gameStatus === GAME_LOOPING_STATUS) { ctx.clearRect(0, 0, canvas.width, canvas.height); A.drawHouse(); B.drawHouse(); A.draw(); B.draw(); A.path.length && drawPath(A.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH); B.path.length && drawPath(B.path, DRAW_LINE_COLOR, DRAW_LINE_WIDTH); A.moving && A.move(); B.moving && B.move(); } requestId = requestAnimationFrame(update); } //初始化 function init() { window.cancelAnimationFrame(requestId); ctx.clearRect(0, 0, canvas.width, canvas.height); A.drawHouse(); B.drawHouse(); A.draw(true); B.draw(true); } //事件監(jiān)聽 canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); resetBtn.addEventListener('click', resetGame); startBtn.addEventListener('click', startGame); init();
六、寫在最后
Canvas開發(fā)游戲是一項充滿趣味性和挑戰(zhàn)性的任務。使用Canvas可以實現(xiàn)各種各樣的游戲效果和交互方式,例如動畫、碰撞檢測、路徑計算等等。通過開發(fā)游戲,可以鍛煉我們自己的編程能力和解決問題的能力,提高自己的代碼質量和效率。
在開發(fā)過程中,常常需要考慮游戲的用戶體驗和界面設計。例如,我們可以為游戲添加音效和動畫效果,以增加游戲的趣味性和互動性。還可以為游戲添加計分板和排行榜,以便于玩家比較成績和分享游戲。
以上就是Canvas實現(xiàn)二娃翠花回家之路小游戲demo解析的詳細內容,更多關于Canvas回家之路小游戲的資料請關注腳本之家其它相關文章!
相關文章
使用原生js實現(xiàn)頁面蒙灰(mask)效果示例代碼
像js的框架Extjs的mask()和unmask()功能提供了蒙灰效果,當然jquery也提供了這種蒙灰方法,下面有個示例,大家可以參考下2014-06-06Javascript圖像處理—圖像形態(tài)學(膨脹與腐蝕)
上一篇文章,我們講解了圖像處理中的閾值函數(shù),這一篇文章我們來做膨脹和腐蝕函數(shù)2013-01-01javascript 讀取XML數(shù)據(jù),在頁面中展現(xiàn)、編輯、保存的實現(xiàn)
最近需要做這樣一個需求,數(shù)據(jù)保存在XML里,在頁面上通過表格顯示其內容,可以修改內容,再保存到XML。下面把做這個東西的過程記錄下來,做個筆記,也給需要的人一些幫助。2009-10-10關于javascript中限定時間內防止按鈕重復點擊的思路詳解
下面小編就為大家?guī)硪黄P于javascript中限定時間內防止按鈕重復點擊的思路詳解。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2016-08-08