問題描述
我在 canvas 元素中建立了一個經典的蛇遊戲。在做這件事時,我沒有考慮過最佳做法,我只想先完成。現在是改進編碼實踐的時候了。你可以透過提到不好的做法來幫助我,改程序式碼並提出其他建議。
<!DOCTYPE HTML>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>Feed the Snake v 1.1 beta</title>
<style>
body
{
background:#000;
color:#FFF;
}
canvas
{
background:#FFF;
}
#controls
{
position:absolute;
top:0;
right:0;
margin:10px;
}
</style>
<script type="text/javascript">
var snake = window.snake || {};
function launchFullscreen(element) {
if(element.requestFullscreen) {
element.requestFullscreen();
} else if(element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if(element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if(element.msRequestFullscreen) {
element.msRequestFullscreen();
}
}
window.onload = function(){
document.addEventListener("fullscreenchange", function(){snake.game.adjust();});
document.addEventListener("webkitfullscreenchange", function(){snake.game.adjust();});
document.addEventListener("mozfullscreenchange", function(){snake.game.adjust();});
document.addEventListener("MSFullscreenChange", function(){snake.game.adjust();});
snake.game = (function()
{
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var status=false;
var score = 0;
var old_direction = 'right';
var direction = 'right';
var block = 10;
var score = 0;
var refresh_rate = 250;
var pos = [[5,1],[4,1],[3,1],[2,1],[1,1]];
var scoreboard = document.getElementById('scoreboard');
var control = document.getElementById('controls');
var keys = {
37 : 'left',
38 : 'up',
39 : 'right',
40 : 'down'
};
function adjust()
{
if (document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement || document.msFullscreenElement )
{
canvas.width=window.innerWidth;
canvas.height=window.innerHeight;
control.style.display='none';
}
else
{
canvas.width=850;
canvas.height=600;
control.style.display='inline';
}
}
var food = [Math.round(Math.random(4)*(canvas.width - 10)), Math.round(Math.random(4)*(canvas.height - 10)),];
function todraw()
{
for(var i = 0; i < pos.length; i++)
{
draw(pos[i]);
}
}
function giveLife()
{
var nextPosition = pos[0].slice();
switch(old_direction)
{
case 'right':
nextPosition[0] += 1;
break;
case 'left':
nextPosition[0] -= 1;
break;
case 'up':
nextPosition[1] -= 1;
break;
case 'down':
nextPosition[1] += 1;
break;
}
pos.unshift(nextPosition);
pos.pop();
}
function grow()
{
var nextPosition = pos[0].slice();
switch(old_direction)
{
case 'right':
nextPosition[0] += 1;
break;
case 'left':
nextPosition[0] -= 1;
break;
case 'up':
nextPosition[1] -= 1;
break;
case 'down':
nextPosition[1] += 1;
break;
}
pos.unshift(nextPosition);
}
function loop()
{
ctx.clearRect(0,0,canvas.width,canvas.height);
todraw();
giveLife();
feed();
if(is_catched(pos[0][0]*block,pos[0][1]*block,block,block,food[0],food[1],10,10))
{
score += 10;
createfood();
scoreboard.innerHTML = score;
grow();
if(refresh_rate > 100)
{
refresh_rate -=5;
}
}
snake.game.status = setTimeout(function() { loop(); },refresh_rate);
}
window.onkeydown = function(event){
direction = keys[event.keyCode];
if(direction)
{
setWay(direction);
event.preventDefault();
}
};
function setWay(direction)
{
switch(direction)
{
case 'left':
if(old_direction!='right')
{
old_direction = direction;
}
break;
case 'right':
if(old_direction!='left')
{
old_direction = direction;
}
break;
case 'up':
if(old_direction!='down')
{
old_direction = direction;
}
break;
case 'down':
if(old_direction!='up')
{
old_direction = direction;
}
break;
}
}
function feed()
{
ctx.beginPath();
ctx.fillStyle = "#ff0000";
ctx.fillRect(food[0],food[1],10,10);
ctx.fill();
ctx.closePath();
}
function createfood()
{
food = [Math.round(Math.random(4)*850), Math.round(Math.random(4)*600)];
}
function is_catched(ax,ay,awidth,aheight,bx,by,bwidth,bheight) {
return !(
((ay + aheight) < (by)) ||
(ay > (by + bheight)) ||
((ax + awidth) < bx) ||
(ax > (bx + bwidth))
);
}
function draw(pos)
{
var x = pos[0] * block;
var y = pos[1] * block;
if(x >= canvas.width || x <= 0 || y >= canvas.height || y<= 0)
{
document.getElementById('pause').disabled='true';
snake.game.status=false;
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.font='40px san-serif';
ctx.fillText('Game Over',300,250);
ctx.font = '20px san-serif';
ctx.fillStyle='#000000';
ctx.fillText('To Play again Refresh the page or click the Restarts button',200,300);
throw ('Game Over');
}
else
{
ctx.beginPath();
ctx.fillStyle='#000000';
ctx.fillRect(x,y,block,block);
ctx.closePath();
}
}
function pause(elem)
{
if(snake.game.status)
{
clearTimeout(snake.game.status);
snake.game.status=false;
elem.value='Play'
}
else
{
loop();
elem.value='Pause';
}
}
function begin()
{
loop();
}
function restart()
{
location.reload();
}
function start()
{
ctx.fillStyle='#000000';
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.fillStyle='#ffffff';
ctx.font='40px helvatica';
ctx.fillText('Vignesh',370,140);
ctx.font='20px san-serif';
ctx.fillText('presents',395,190);
ctx.font='italic 60px san-serif';
ctx.fillText('Feed The Snake',240,280);
var img = new Image();
img.onload = function()
{
ctx.drawImage(img,300,300,200,200);
ctx.fillRect(410,330,10,10);
}
img.src ='snake.png';
}
function fullscreen()
{
launchFullscreen(canvas);
}
return {
pause: pause,
restart : restart,
start : start,
begin: begin,
fullscreen : fullscreen,
adjust : adjust,
};
})();
snake.game.start();
}
</script>
</head>
<body>
<canvas width="850" height="600" id="canvas" style="border:1px solid #333;" onclick="snake.game.begin();">
</canvas>
<div id="controls" style="float:right; text-align:center;">
<input type="button" id="pause" value="Play" onClick="snake.game.pause(this);" accesskey="p">
<input type="button" id="restart" value="Restart" onClick="snake.game.restart();">
<br/><br/>
<input type="button" id="fullscreen" value="Play Fullscreen" onClick="snake.game.fullscreen();">
<br/><br/>
<div style="font-size:24px;">
Score :
<span id="scoreboard">0</span>
</div>
</div>
</body>
</html>
您可以看到 here 遊戲的即時版本。
最佳解決思路
從一次:
好
-
我喜歡你如何使用 IIFE
-
我真的很喜歡你如何使用
direction = keys[event.keyCode];
不太好
-
你不是一直應用第二個好的技術,例如:
function setWay(direction)
{
switch(direction)
{
case 'left':
if(old_direction!='right')
{
old_direction = direction;
}
break;
case 'right':
if(old_direction!='left')
{
old_direction = direction;
}
break;
case 'up':
if(old_direction!='down')
{
old_direction = direction;
}
break;
case 'down':
if(old_direction!='up')
{
old_direction = direction;
}
break;
}}
可以簡單地是function setWay(direction)
{
var oppositeDirection = {
left : 'right',
right: 'left',
up: 'down',
down:'up'
}if( direction != oppositeDirection[old_direction] ){
old_direction = direction;
}
}
我會留下深刻的想法是否-
您要指定
'left'與'right'相反,因為您已經指定了'right'與'left'相反 -
是否要合併
oppositeDirection和keys
-
-
您複製了
giveLife和grow中的一些程式碼,也可以從上述方法中受益。我會寫這個:switch(old_direction)作為
{
case 'right':
nextPosition[0] += 1;
break;
case 'left':
nextPosition[0] -= 1;
break;
case 'up':
nextPosition[1] -= 1;
break;
case 'down':
nextPosition[1] += 1;
break;
}
//2 properly named array indexes for x and y
var X = 0;
var Y = 1;
//vectors for each direction
var vectors = {
right : { x : 1 , y : 0 },
left : { x : -1 , y : 0 },
up : { x : 0 , y : -1 },
down : { x : 0 , y : 1 }
}function updatePosition( direction ){
var vector = vectors( direction );
if( vector ){
nextPosition[X] += vector.x;
nextPosition[Y] += vector.y;
}
else{
throw "Invalid direction: " + direction
}
}
這裡的優點是:-
如果你想玩 8 個方向的蛇,你可以
-
如果傳遞無效的方向,則不會發生無聲的故障
-
-
以下程式碼給我的爬行:
function launchFullscreen(element) { if(element.requestFullscreen) { element.requestFullscreen(); } else if(element.mozRequestFullScreen) { element.mozRequestFullScreen(); } else if(element.webkitRequestFullscreen) { element.webkitRequestFullscreen(); } else if(element.msRequestFullscreen) { element.msRequestFullscreen(); } }你是否考慮過使用類似的東西
function launchFullscreen(e) { var request = e.requestFullscreen || e.mozRequestFullScreen || e.webkitRequestFullscreen || e.msRequestFullscreen; request(); } -
這也不是一個漂亮的景象:
document.addEventListener("fullscreenchange", function(){snake.game.adjust();}); document.addEventListener("webkitfullscreenchange", function(){snake.game.adjust();}); document.addEventListener("mozfullscreenchange", function(){snake.game.adjust();}); document.addEventListener("MSFullscreenChange", function(){snake.game.adjust();});至少應該是
document.addEventListener("fullscreenchange", snake.game.adjust ); document.addEventListener("webkitfullscreenchange", snake.game.adjust ); document.addEventListener("mozfullscreenchange", snake.game.adjust ); document.addEventListener("MSFullscreenChange", snake.game.adjust );真的有一個比訂閱每個瀏覽器事件更好的方法;) 我假設你不是簡單地提供
snake.game.adjust,因為它還沒有被初始化。我寧願解決這個問題,然後建立處理這個問題的功能。
次佳解決思路
有關您的一般程式碼風格的一些想法 (有些點可能取決於個人喜好):
-
我建議將 HTML /CSS /JS 分割成不同的檔案
-
你使用縮排和空格是不一致的
function launchFullscreen(element) { if(element.requestFullscreen) { element.requestFullscreen();有兩個空格的縮排
snake.game = (function() { var canvas = document.getElementById('canvas');有四個空格的縮排
if(x >= canvas.width || x <= 0 || y >= canvas.height || y<= 0) { document.getElementById('pause').disabled='true'; snake.game.status=false;有八個空格的縮排
var status=false; // no spaces before/after '=' var block = 10; // space before/after '=' -
你有兩次:
var score = 0; -
方法名稱不一致
is_catched,setWay,todraw。 -
考慮用大寫寫入常量來區分它們與您要修改的變數:
BLOCK而不是block,或者在這種情況下,像BLOCK_SIZE這樣更加適合 -
您首次在所有功能之間宣告並分配您的
food變數,儘管您有creatfood方法 -
有幾個魔術數字,你可以/應該變成變數
-
一些引數名稱相當隱蔽:
is_catched(ax,ay,awidth,aheight,bx,by,bwidth,bheight) -
您可以對某些變數使用物件而不是陣列。例如:
food是一個有 2 個元素的陣列 (大概是 x /y pos) 。您可以將其轉換為物件{ x: XXX, y: XXX }。這可能會提高某些地方的可讀性。 -
目前,您的更新邏輯似乎與您的繪圖邏輯混合。這可能更好 (更容易維護),如果你分開這些。此外,您檢查您的繪圖呼叫內的遊戲
第三種解決思路
我建議將蛇繪製為單個折線,而不是一堆塊,因此您只能呼叫 stroke 一次,而不是像塊具有蛇一樣呼叫 fillRect 多次。
一般來說,敲擊一個複雜的形狀而不是一堆簡單的形狀會更有效率。你可以看到在 this test 。
另外一個選擇,我會考慮的是 clearRect 只有最後一塊蛇,然後繪製 (fillRect) 才是新的,所以你不必重畫所有的場景,只有那些已經改變的部分。另一個 test here 。
你必須測試這兩個選項,看看哪個更適合你,但我會去第二個。
我也會考慮使用 requestAnimationFrame setTimeout 。從 MDN 檔案:
The
window.requestAnimationFrame()method tells the browser that you wish to perform an animation and requests that the browser call a specified function to update an animation before the next repaint. The method takes as an argument a callback to be invoked before the repaint.
還要檢查 this post 。它解釋了基本知識以及如何設定自定義幀速率,以便您仍然可以在程式碼中使用 refresh_rate 。
有兩個選擇:
將 requestAnimationFrame 包裝在 setTimeout 中:
function draw() {
setTimeout(function() {
requestAnimationFrame(draw);
// Drawing code goes here
}, customTime);
}
或使用三角洲:
var time;
function draw() {
requestAnimationFrame(draw);
var now = new Date().getTime(),
dt = now - (time || now);
time = now;
// Drawing code goes here... for example updating an 'x' position:
this.x += 10 * dt; // Increase 'x' by 10 units per millisecond
}
在你的情況下,我認為這將是更好的第一個選擇,因為蛇只能繪製在某些塊,所以在這裡使用三角洲沒有什麼意義。
第四種思路
對於 @Sykin’s answer,我會補充說:
Bug
如果您按 down,然後 left 比蛇快速地彈出”one step”,蛇會轉動並滑過自身。
縮排,程式碼樣式和程式碼一致性
我發現 JSLint 真的幫助我獲得一致的程式碼風格。
嚴格模式
我建議你在你的 JS 中使用 strict mode,從一行"use strict"; 開始
參考文獻
注:本文內容整合自 Google/Baidu/Bing 輔助翻譯的英文資料結果。如果您對結果不滿意,可以加入我們改善翻譯效果:薇曉朵技術論壇。