您好,登錄后才能下訂單哦!
這篇文章主要介紹了Canvas怎么實現二娃翠花回家之路小游戲的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Canvas怎么實現二娃翠花回家之路小游戲文章都會有所收獲,下面我們一起來看看吧。
Canvas是HTML5中的一個非常有用的技術,它可以用于實現各種圖形化效果。本文將介紹使用Canvas實現的小游戲——“二娃翠花回家之路”。這個小游戲非常有趣,玩家需要通過繪制角色的行走路線來控制他們的行動,并避免他們相撞。
玩家需要通過繪制二娃和翠花的行走路線,來控制他們的行動。
二娃和翠花需要分別回到各自的房子才能過關。
玩家需要避免二娃和翠花的行走的路上發生碰撞,否則游戲失敗。
玩家可以使用“開始”按鈕開始游戲,使用“重置”按鈕重新開始游戲。
在實現“二娃翠花回家之路”小游戲的過程中,我遇到了如下幾個技術難點:如何繪制路徑不被頁面刷新影響、如何計算兩條路線交叉點最相近坐標距離、如何判斷碰撞、如何計算人物的移動速度和步長。
繪制路線:在Canvas中,通過監聽鼠標事件獲取鼠標的坐標,并根據鼠標的移動軌跡來繪制路徑。在繪制路線時需要保存路徑的坐標,以便于后續的操作。需要考慮多個角色之間的交互,頁面刷新函數調用時機影響著路徑的繪制。
計算距離:需要計算二娃和翠花之間的距離,以及二娃和翠花與路線之間的距離。距離的計算需要使用勾股定理:
需要注意單位的轉換和精度的控制。
判斷碰撞:需要在角色行走時,判斷角色與另一個角色的距離是否小于一定的閾值。如果小于閾值,則需要根據一定的幾率避免碰撞,或者直接暫停游戲并提示失敗。需要考慮多個角色之間的相互作用。
人物移動速度和步長計算:需要計算人物與目標點之間的距離,以及人物的速度,計算出人物的移動步長。需要注意人物在路徑交叉點位置碰撞的問題。這里我的解決方案是:計算兩條路徑的最近的兩個坐標(因為交叉點不一定存在坐標,這是canvas繪制線條存在的可能性),然后拿到這兩個坐標,找出原路徑起點到該坐標的路徑保存起來,再計算路徑長度,得到長度用公式v=s/t的到速度v。
???? 劃重點: 針對人物移動速度和步長計算。我的實現方案雖然可以得到兩個人物的各自獨立的移動速度,但是仍然無法保證人物在路徑交叉點位置碰撞,這里我暫時沒有好的解決方案,希望各位掘友讀者們,能把代碼fork過去,幫忙解決這個問題,后在評論區,附上你的解決方案。
使用HTML和JavaScript來創建了一個畫布和兩個按鍵元素。首先創建了一個HTML文件,然后在其中添加了一個畫布和兩個按鍵。然后使用JavaScript來獲取畫布和按鍵元素,并設置了它們的屬性和事件監聽器。最后為畫布創建了一個繪圖環境,并在畫布上繪制了兩個人物和他們的家。代碼實現在本文第四部分。
創建一個人物類character
,并在其中實現繪制角色draw()
、繪制家drawHouse()
、計算路徑總長度calculatePathLength(def_path)
這里def_path
是為了后面找到兩條路線交叉點最相近坐標到各自路線起點的路線備用的。計算兩個坐標的距離distance(x1,y1,x2,y2)
、人物移動move()
等方法。通過這些方法,我們可以實現人物的移動和路徑的劃分。在實現路徑劃分時,我們可以通過計算路徑總長度,將路徑劃分成若干個點,使角色在這些點之間移動。代碼實現在本文第四部分。
創建兩個人物實例,并設置它們的屬性和方法。我們將為每個人物實例設置起點、終點和當前位置,姓名和顏色屬性。在移動時判斷碰撞。
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');
在 Canvas 元素上監聽鼠標事件,并根據鼠標的移動軌跡來繪制路徑。我們將使用鼠標事件監聽器來獲取鼠標的坐標,并使用 Canvas API 來繪制路徑。在繪制路線時需要保存路徑的坐標,以便于后續的操作。
//在鼠標事件中會調用該方法繪制路徑 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() } //事件監聽 canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); resetBtn.addEventListener('click', resetGame); startBtn.addEventListener('click', startGame);
根據繪制好的路徑,將路徑按照一定的長度劃分成若干個點。這些點可以作為人物移動的目標位置。然后,我們可以在每個點上計算出人物應該移動的目標位置,從而實現人物的移動。
在 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(); //...... //...... }
在角色行走時,判斷角色與另一個角色的距離是否小于一定的閾值。如果小于閾值,則需要根據一定的幾率避免碰撞,或者直接暫停游戲并提示失敗。需要考慮多個角色之間的相互作用。
//核心部分 //...... 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} 碰到了對方,游戲失敗`); } }
實現開始和重置游戲的功能,包括重置路徑、重置人物位置等。當游戲開始時,需要計算兩個人物最短的路徑,并將其保存到對應的路徑數組中。當游戲結束時,將人物位置重置,并清空路徑數組和繪制的路徑。
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; }
動畫效果的實現主要是通過 requestAnimationFrame 方法來實現的。requestAnimationFrame 是一個用來優化動畫效果的方法,可以讓動畫更流暢自然。具體地,requestAnimationFrame 方法會在下一幀動畫之前調用一個回調函數,以便于更新動畫效果。在這個回調函數中,可以實現人物的移動、路徑的繪制等等,從而達到動畫效果。
動畫效果的實現主要在人物類(Character)中。每個人物都有自己的動畫狀態和動畫參數,包括位置、速度、目標位置等等。在每一幀的動畫中,都會根據當前位置和目標位置之間的距離來計算移動的距離和速度,并且不斷更新人物的位置和狀態。這個過程中,使用了一些基本的數學計算,比如計算兩點之間的距離、計算兩點之間的角度等等。
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); }
下面是實現該游戲的完整代碼,這里著重強調幾個關鍵函數,在代碼中找到對應的**注釋**
,這跟我在上面文章第二部分劃重點的提問有關哦!。
主要的涉及的邏輯是,每個人物類有一個屬性total_distance
用于存儲距離,當用戶點擊開始回家按鈕后,在startGame
方法里會計算兩個人物回家路徑最近的兩個坐標點,并獲得這兩個坐標所在路徑到所在路徑起點的路徑坐標數組,然后計算該新路徑的長度保存到total_distance
中,在人物移動方法move()
中會計算人物移動速度和人物移動步長。
頁面的布局相對簡單,基本就是一個畫布+按鈕。當然想要游戲體驗更好,可以更改頁面布局,增加更多互動元素。
<!DOCTYPE html> <html> <head> <title>二娃、翠花的回家之路</title> </head> <body> <h2>二娃和翠花的回家之路</h2> <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>
樣式的話,相信不用講太多,大家一看就知道了。
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; }
這部分代碼著實有點長,需要點耐心來閱讀,利用代碼中的注釋或者變量和方法名稱來輔助理解。本來不想貼完整代碼的,但是,為了保證大家能夠更好的理解這個游戲,還是貼上來吧,相信可以第一時間閱讀完整代碼的感覺還是挺好的????。
// 游戲狀態 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,//動畫控制句柄 //畫布、按元素節點 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; } //獲取坐標點到路徑起點的路徑數組 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); } //事件監聽 canvas.addEventListener('mousedown', handleMouseDown); canvas.addEventListener('mousemove', handleMouseMove); canvas.addEventListener('mouseup', handleMouseUp); resetBtn.addEventListener('click', resetGame); startBtn.addEventListener('click', startGame); init();
關于“Canvas怎么實現二娃翠花回家之路小游戲”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Canvas怎么實現二娃翠花回家之路小游戲”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。