JavaScript for Kids 学习笔记15. 贪吃蛇

游戏规则:

1. 蛇在一个方框中移动,玩家可以用键盘的上、下、左、右控制它的移动方向。

2. 蛇不能撞到方框边缘,否则,Game Over。

3. 蛇头不能撞到蛇身,否则,Game Over。

4. 蛇头碰到苹果(绿色圆点),则蛇身增长一格。分数加一。

上图中一共有四个东西:灰色外框、分数、苹果、蛇。

前三个都很简单,麻烦的逻辑都在蛇身上。我们从简单的做起。

1. 创建一个html文件

!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Snake!</title>
    <script src="https://code.jquery.com/jquery-2.1.0.js"></script>
</head>
<body>
<canvas id="canvas" width="400" height="400"></canvas>
<script>
    // 代码写到这里
</script>
</body>
</html>

2. 画外框

var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var width = canvas.width;
var height = canvas.height;
var blockSize = 10;

var drawBorder = function () {
    ctx.fillStyle = "Gray";
    ctx.fillRect(0, 0, width, blockSize);
    ctx.fillRect(0, height - blockSize, width, blockSize);
    ctx.fillRect(0, 0, blockSize, height);
    ctx.fillRect(width - blockSize, 0, blockSize, height);
};

呃,看起来有点奇怪啊。画一个矩形,为什么要用四个fillRect?一个 strokeRect 不也能搞定吗?

的确如此,改成这样好像也没有问题:

var drawBorder1 = function () {
    ctx.strokeStyle = "Gray";
    ctx.lineWidth = blockSize;
    ctx.strokeRect(0, 0, width, height);
};

用这种方法,我遇到了一个小问题:线框的宽度变成了上一种方法的一半。不过,我的屏幕是retina的,不知道是不是这个原因,难道fillRect和stokeRect在这方面有区别?先不管他,以后有空再说吧。

还有一个奇怪的东西:blockSize 。 这是干啥用的呢?  线框宽度是10,那就直接写10呗,为什么要定义这么个变量?

看看下图:

对,蛇是有宽度的,苹果也是有宽度的,为了方便定位和碰撞检测,我们把舞台分成block,blockSize = 10 。

所以,上图中,蛇由37个block(小矩形)组成,苹果是一个block(小圆形)。 画蛇,就是画37个矩形。

为了整体的视觉效果,外边框也用这个blockSize做为它的宽度。

3. 显示分数

这要用到一个新的方法: 

ctx.fillText("Hello world!", 50, 50);

可以在Chrome中试一下:


好,看看我们的计分牌:

var score = 0;

var drawScore = function () {
    ctx.font = "20px Courier";
    ctx.fillStyle = "Black";
    ctx.textAlign = "left";
    ctx.textBaseline = "top";
    ctx.fillText("Score: " + score, blockSize, blockSize);
};

嗯,还是有点复杂的。不过很好理解,先给context设置了四个属性,和CSS中的定义没什么区别。fillText 中用到了blockSize,这个就有点儿意思了,后两个参数不是起始坐标吗? 对,因为边框也是blockSize,所以,起始坐标就应该是(0 + blockSize, 0 + blockSize)。

4. 定义block

上面简单说了一下block,其实,它是这个游戏算法的基础。需要仔细的理解。

var Block = function (col, row) {
    this.col = col;
    this.row = row;
};

Block.prototype.drawSquare = function (color) {
    var x = this.col * blockSize;
    var y = this.row * blockSize;
    ctx.fillStyle = color;
    ctx.fillRect(x, y, blockSize, blockSize);
};

Block.prototype.drawCircle = function (color) {
    var centerX = this.col * blockSize + blockSize / 2;
    var centerY = this.row * blockSize + blockSize / 2;
    ctx.fillStyle = color;
    circle(centerX, centerY, blockSize/2, true);  // 前一章,前两章,这个函数就已经定义好了,画蜜蜂、画小球等等。
};

Block.prototype.equal = function (otherBlock) {
    return this.col === otherBlock.col && this.row === otherBlock.row;
};

真的觉得很神奇,这个block,一下子就提高了编程的抽象层次。有了(col, row) ,我们就不必关系(x, y) 坐标了。 那个,equal方法,是碰撞检测的基础(吃苹果、撞墙、撞自己身体,都用这个方法来判断)。

苹果是一个block。

蛇是多个block,即,block的数组。

后面的代码,再也见不到 ctx.fillRect 这样的东西了,所有的操作都是针对block。

这里用到了上一章定义的circle函数,记得复制过来。

5. 苹果

var widthInBlocks = width / blockSize;
var heightInBlocks = height / blockSize;

var Apple = function () {
    this.position = new Block(10, 10);
};

Apple.prototype.draw = function () {
    this.position.drawCircle("LimeGreen");
};

Apple.prototype.move = function () {
    var randomCol = Math.floor(Math.random() * (widthInBlocks - 2)) + 1;
    var randomRow = Math.floor(Math.random() * (heightInBlocks - 2)) + 1;
    this.position = new Block(randomCol, randomRow);
};

有了上面的代码,我们可以画苹果了:

注意看console中的代码,apple draw了两次,舞台上就有了两个苹果大笑。没错,游戏运行时,会启动 setInterval 方法,每100ms清空舞台,根据苹果和蛇的位置重新绘制,所以,实际运行中不会同时出现两个苹果。

6. 蛇

var Snake = function () {
    this.segments = [
        new Block(7, 5),
        new Block(6, 5),
        new Block(5, 5)
    ];
    this.direction = "right";
    this.nextDirection = "right";
};

Snake.prototype.draw = function () {
    for (var i=0; i<this.segments.length; i++) {
        this.segments[i].drawSquare("Blue");
    }
};

有了这些代码,我们可以在游戏舞台上画出所有的东西了。


7. 让蛇动起来

还记得上一章的小球吗,用键盘可以控制它的方向,现在用蛇来代替那个小球。代码是一样的。

Snake.prototype.move = function () {
    var head = this.segments[0];
    var newHead;

    this.direction = this.nextDirection;

    if (this.direction === "right") {
        newHead = new Block(head.col + 1, head.row);
    }
    else if (this.direction === "down") {
        newHead = new Block(head.col, head.row + 1);
    }
    else if (this.direction === "left") {
        newHead = new Block(head.col - 1, head.row);
    }
    else if (this.direction === "up") {
        newHead = new Block(head.col, head.row - 1);
    }
        
    this.segments.unshift(newHead);  // 添加新的蛇头
    this.segments.pop();   // 删除蛇尾
};

Snake.prototype.setDirection = function (newDirection) {
    if (this.direction === "up" && newDirection === "down") {
        return;
    }
    else if (this.direction === "right" && newDirection === "left") {
        return;
    }
    else if (this.direction === "down" && newDirection === "up") {
        return;
    }
    else if (this.direction === "left" && newDirection === "right") {
        return;
    }

    this.nextDirection = newDirection;
};

var directions = {
    37: "left",
    38: "up",
    39: "right",
    40: "down"
};

$("body").keydown(function (event) {
    var newDirection = directions[event.keyCode];
    if (newDirection !== undefined) {
        snake.setDirection(newDirection);
    }
});

var snake = new Snake();
var apple = new Apple();

var intervalId = setInterval(function () {
    ctx.clearRect(0, 0, width, height);
    drawScore();
    snake.move();
    snake.draw();
    apple.draw();
    drawBorder();
}, 200);

好长的代码,不过,绝大部分上一章已经见过了,只有两处很小的差别。第一处:move,蛇的移动,这个很巧妙啊,根据移动方向,新加一个蛇头,把蛇尾删除。(加蛇头、删蛇尾、加蛇头、删蛇尾,如此往复,蛇就动起来了。) 第二处:setDirection,改变蛇的移动方向,多了四个条件判断,蛇毕竟是蛇,不像小球,它不能直接掉头。

运行一下,看看效果。

嗯,还不能吃苹果,还不能检测撞墙。

8. 吃苹果

把这两行:

this.segments.unshift(newHead);  // 添加新的蛇头
this.segments.pop();   // 删除蛇尾
替换成:
this.segments.unshift(newHead);

if (newHead.equal(apple.position)) {
    score++;
    apple.move();
}
else {
    this.segments.pop();
}

如果新的蛇头和苹果位置相同,就不用删除蛇尾了。 amazing! 

9. 碰撞检测

Snake.prototype.checkCollision = function (head) {
    var leftCollision = (head.col === 0);
    var topCollision = (head.row === 0);
    var rightCollision = (head.col === widthInBlocks - 1);
    var bottomCollision = (head.row === heightInBlocks - 1);

    var wallCollision = leftCollision || rightCollision || topCollision || bottomCollision;

    var selfCollision = false;
    for (var i=0; i<this.segments.length; i++) {
        if (head.equal(this.segments[i])) {
            selfCollision = true;
            break;
        }
    }

    return wallCollision || selfCollision;
};

这里的逻辑有点抽象。前五行判断是否撞墙了,后面的循环判读是否撞上自己的身体了。

10. Game Over

碰撞检测的目的是结束游戏,所以,先写个gameOver函数。

var gameOver = function () {
    clearInterval(intervalId);
    ctx.font = "60px Courier";
    ctx.fillStyle = "Black";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    ctx.fillText("Game Over", width/2, height/2);
};

很简单哈,停止定时任务,写上大大的文字 “Game Over” 。

11. 使用碰撞检测

在这一行代码:

this.segments.unshift(newHead);

上面,添加:

if (this.checkCollision(newHead)) {
    gameOver();
    return;
}

完工了。

完整的代码在:jsforkids.zip 中的 ch16_1.html。


  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值