问题描述

我在 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'相反

    • 是否要合并 oppositeDirectionkeys

  • 您复制了 giveLifegrow 中的一些代码,也可以从上述方法中受益。我会写这个: 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,因为它还没有被初始化。我宁愿解决这个问题,然后创建处理这个问题的功能。

次佳解决思路

有关您的一般代码风格的一些想法 (有些点可能取决于个人喜好):

  1. 我建议将 HTML /CSS /JS 分割成不同的文件

  2. 你使用缩进和空格是不一致的

    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 '='
    
  3. 你有两次:

    var score = 0;
    
  4. 方法名称不一致 is_catchedsetWaytodraw

  5. 考虑用大写写入常量来区分它们与您要修改的变量:BLOCK 而不是 block,或者在这种情况下,像 BLOCK_SIZE 这样更加适合

  6. 您首次在所有功能之间声明并分配您的 food 变量,尽管您有 creatfood 方法

  7. 有几个魔术数字,你可以/应该变成变量

  8. 一些参数名称相当隐蔽:is_catched(ax,ay,awidth,aheight,bx,by,bwidth,bheight)

  9. 您可以对某些变量使用对象而不是数组。例如:food 是一个有 2 个元素的数组 (大概是 x /y pos) 。您可以将其转换为对象 { x: XXX, y: XXX }。这可能会提高某些地方的可读性。

  10. 目前,您的更新逻辑似乎与您的绘图逻辑混合。这可能更好 (更容易维护),如果你分开这些。此外,您检查您的绘图呼叫内的游戏

第三种解决思路

我建议将蛇绘制为单个折线,而不是一堆块,因此您只能调用 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 辅助翻译的英文资料结果。如果您对结果不满意,可以加入我们改善翻译效果:薇晓朵技术论坛。