问题描述
我在 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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。