手把手教你用JS写一个贪食蛇小游戏

2018-05-22  本文已影响203人  HolidayPeng

本文阅读时间15分钟,要求你有一定的html5+css3+es6基础。打开编辑器跟着我一起把代码敲一遍,效果更佳。

废话不多说,先贴代码,里面有详细的注释,不想看的可以跳过看下面的讲解:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <style>
        .game-container {
            color: gray;
            border: 10px double gray;
            width: 40%;
              text-align: center
        }
        p:first-child {
            margin-right: 200px;
        }
        .snake-area {
            width: 315px;
            height: 525px;
            margin: auto;
            padding-bottom: 10px
        }
        .grid {
            border-right: 1px solid purple;
            border-bottom: 1px solid purple;
            width: 20px;
            height: 20px;
            border-radius: 10px;
            float: left;
        }
        .snake {
            background: pink;
        }
        .food {
            background: yellow;
        }
        p {
            display: inline-block;
        }
        .first-one {
            margin-right: 20px;
        }
        button {
            width: 100px;
            height: 21px;
            line-height: 18px;
            color: brown;
            font-weight: bold;
            font-size: 15px;
        }
        .statement-wrapper {
            background: orange;
            display: none;
            padding: 15px;
            opacity: 0.9;
            position: fixed;
            top: 40%;
            left: 16%;
            color: #fff;
        }
    </style>
</head>

<body>
    <div class="game-container">
        <h1>SUPER SNAKE</h1>
        <p class="first-one">
            <span>CURRENT SCORE:</span>
            <strong class="score"></strong>
        </p>
        <p>
            <span>HISTORY SCORE:</span>
            <strong class="history-score"></strong>
        </p>
        <div class="statement-wrapper"></div>
        <section class="snake-area"></section>
    </div>


    <script type="text/javascript">
        class Snake {
            constructor(height, width) {
                this.y_edge = height / 20; // 下边缘
                this.x_edge = width / 20; // 右边缘
                this.body = ['grid-1-1', 'grid-1-2', 'grid-1-3']; // 蛇身的初始位置
                this.moveTo = 39; // 默认向右移动,对应的键码为39
                this.interval = null; // 用来保存秒表
                this.shouldRemove = true; // 用来判断是否要去掉蛇尾的颜色(吃到食物时为假,此时身长增加1)
                this.stopGame = false; // 用来判断游戏是否还在进行中
                this.eatSelf = false; // 判断是否吃到自己
                this.food = new Food(); // 引入食物
                this.showScore = new Score(); // 引入比分
                this.addColor(); // 给蛇添加背景色
                this.move(); // 让蛇动起来
                this.controller(); // 绑定键盘事件
            }
            addColor() { // 循环蛇的身体,给每个格子添加class
                this.body.forEach(item => {
                    document.querySelector(`#${item}`).classList.add('snake');
                })
            };
            removeColor() { // 去掉蛇身体数组的第一个元素,去掉class
                const uncolored = this.body.shift()
                document.querySelector(`#${uncolored}`).classList.remove('snake');
            };
            move(to = 'right') {
                this.interval = setInterval(() => { // 每隔500ms执行一次:
                    const headId = this.body.slice(-1)[0], // 取出蛇头
                        headY = headId.split('-')[1], // 获取蛇头Y坐标
                        headX = headId.split('-')[2], // 获取蛇头X坐标
                        directions = {
                            right: [`${headY}`, +headX + 1, this.x_edge], // 蛇向右移动时,Y坐标保持不变,X坐标+1
                            left: [`${headY}`, +headX - 1, this.x_edge], // 蛇向左移动时,Y坐标保持不变,X坐标-1
                            down: [+headY + 1, `${headX}`, this.y_edge], // 蛇向下移动时,Y坐标+1,X坐标保持不变
                            up: [+headY - 1, `${headX}`, this.y_edge] // 蛇向上移动时,Y坐标-1,X坐标保持不变
                        };

                    this.eat(headX, headY); // 取到蛇头的坐标,若该坐标与食物坐标相等,执行吃的方法

                    for (const item of this.body.slice(0, this.body.length - 1)) { // 取出蛇身体除蛇头的部分
                        if (headId === item) this.eatSelf = true; // 循环判断每一部分的ID是否与蛇头ID相等,是则吃到了自己
                    }

                    this.body.push(`grid-${directions[to][0]}-${directions[to][1]}`); // 给蛇的数组添加新蛇头
                    this.addColor(); // 给蛇头染上颜色

                    if (this.shouldRemove) { // 如果这次移动没有迟到食物,去掉蛇尾颜色
                        this.removeColor(); 
                    } else { // 如果吃到了食物,重置shouldRemove为true(在eat方法里如果吃到了会将其设为false)
                        this.shouldRemove = true;
                    }
                    if (this.body.length + 1 === this.x_edge * this.y_edge) { // 当蛇身的长度与格子的数量相差1时,赢
                        clearInterval(this.interval); // 停止秒钟
                        this.stopGame = true; // 按键失效
                        this.showStatement('YOU WIN !');
                        this.showScore.updateHistory();
                    }
                    // 移动到边缘或吃到自己时,输
                    const last = typeof directions[to][1] === 'number' ? directions[to][1] : directions[to][0];
                    if (last === directions[to][2] || last === 1 || this.eatSelf) {
                        clearInterval(this.interval);
                        this.stopGame = true;
                        this.showStatement('GAME OVER !');
                        this.showScore.updateHistory();
                    };
                }, 500);
            };
            controller() {
                document.addEventListener('keyup', e => {
                    if (this.moveTo !== e.keyCode &&
                        this.moveTo !== e.keyCode + 2 &&
                        this.moveTo !== e.keyCode - 2 &&
                        !this.stopGame) {
                        this.moveTo = e.keyCode;
                        clearInterval(this.interval);
                        switch (e.keyCode) {
                            case 40:
                                this.move('down');
                                break;
                            case 38:
                                this.move('up');
                                break;
                            case 37:
                                this.move('left');
                                break;
                            case 39:
                                this.move('right');
                                break;
                            default:
                                break;
                        }
                    }
                }, false)
            };
            eat(x, y) {
                if (x == this.food.X && y == this.food.Y) { // 如果蛇头坐标与食物坐标相等
                    document.querySelector(`#grid-${y}-${x}`).classList.remove('food'); // 移除当前食物
                    this.shouldRemove = false; // 增加蛇长
                    this.showScore.addScore(); // 加分
                    this.food = new Food(); // 重新生成食物
                }
            };
            showStatement(content) {
                const statement = document.createElement('h4');
                statement.innerHTML = content;
                const restart = document.createElement('button');
                restart.innerHTML = 'RESTART';
                const statementWrapper = document.querySelector('.statement-wrapper');
                statementWrapper.style.display = 'block';
                statementWrapper.appendChild(statement);
                statementWrapper.appendChild(restart).addEventListener('click', () => location.reload(), false)
            };
        }
        class Food {
            constructor() {
                if (!document.querySelector('.food')) {
                    this.x = Math.floor(13 * Math.random() + 2);
                    this.y = Math.floor(23 * Math.random() + 2);
                    document.querySelector(`#grid-${this.y}-${this.x}`).classList.add('food');
                    return {
                        X: this.x,
                        Y: this.y
                    };
                }
            }
        };
        class Score {
            constructor() {
                // 用try/catch,以便在用户开启浏览器隐私模式或localStorage内存已满时使用
                try {
                    localStorage.getItem('historyScore') ? this.historyScore = localStorage.getItem('historyScore') :
                        this.historyScore = 0; // 取出历史得分,若没有则设为0,显示在页面
                    document.querySelector('.history-score').innerHTML = this.historyScore;
                } catch {
                    alert('Your localStorage is not functioning, please open this page in another browser!')
                }
                this.score = 0; // 本次游戏初始得分为0,显示在页面
                document.querySelector('.score').innerHTML = this.score;
            };
            addScore() {
                this.score++;
                document.querySelector('.score').innerHTML = this.score;
            };
            updateHistory() {
                if (this.score > this.historyScore) { // 若本次得分高于历史得分,存储新的历史得分
                    try {
                        localStorage.setItem('historyScore', this.score);
                        document.querySelector('.history-score').innerHTML = this.score;
                    } catch {
                        alert('Your localStorage is not functioning, please open this page in another browser!')
                    }
                }
            }
        }
        // 初始化页面,显示蛇和得分
        const initializeGrids = (height = 500, width = 300) => {
            for (let i = 1; i < height / 20 + 1; i++) {
                for (let j = 1; j < width / 20 + 1; j++) {
                    const grid = document.createElement('div');
                    grid.setAttribute('id', `grid-${i}-${j}`);
                    grid.classList.add('grid');
                    const con = document.querySelector('.snake-area').appendChild(grid);
                }
            }
            const showSnake = new Snake(height, width);
            const showScore = new Score();
        };
        initializeGrids();
    </script>
</body>

</html>

首先我们要有一个蛇的类,它要移动自己的身体,吃到食物;移动的时候撞了墙,或者咬到了自己的身体,会死;吃到了食物,身长会增加。

所以这个蛇的类,要有一个Move的方法,Move里包含Eat,还有控制Move方向的Controller。

那么怎么让它Move呢?是通过不断往蛇前进的方向添加DOM,并在尾巴处删除DOM?还是通过不断地改变蛇头和蛇尾的背景色来实现移动的效果?我选择了后者。

把蛇的活动区域(300px * 500px)打上格子,每个格子是一个div(20px * 20px)。每个div的ID按照自己所在的位置编号:比如第一排的第一个,编号为”grid-1-1“。


snake area
for (let i = 1; i < height / 20 + 1; i++) {
                for (let j = 1; j < width / 20 + 1; j++) {
                    const grid = document.createElement('div');
                    grid.setAttribute('id', `grid-${i}-${j}`);
                    grid.classList.add('grid');
                    const con = document.querySelector('.snake-area').appendChild(grid);
                }
            }

这样我们就能通过编号来记录并控制蛇运动的路线了。我们给蛇设置的初始身长为3个格子的长度(背景色为粉色);初始位置是第一排从左边起的前三个(把这三个位置保存在数组里:['grid-1-1', 'grid-1-2', 'grid-1-3']);初始的移动方向是从左到右。

为了实现从左到右的移动,每隔500ms,给蛇头右边的第一个div添加粉色背景,并去掉蛇尾的背景色。同时移除蛇身体数组的第一个元素,并添加新的蛇头位置。


snake
class Snake {
            constructor(height, width) {
                this.y_edge = height / 20;
                this.x_edge = width / 20;
                this.body = ['grid-1-1', 'grid-1-2', 'grid-1-3'];
                this.interval = null;
                this.wrapper = document.querySelector('.statement')
                this.addColor();
                this.move();
            }
            addColor() {
                this.body.forEach(item => {
                    document.querySelector(`#${item}`).classList.add('snake');
                })
            };
            removeColor() {
                const uncolored = this.body.shift()
                document.querySelector(`#${uncolored}`).classList.remove('snake');
            };
            move(to = 'right') {
                this.interval = setInterval(() => {
                    const headId = this.body.slice(-1)[0],
                        headY = headId.split('-')[1],
                        headX = headId.split('-')[2],
                        directions = {
                            right: [`${headY}`, +headX + 1, this.x_edge],
                            left: [`${headY}`, +headX - 1, this.x_edge],
                            down: [+headY + 1, `${headX}`, this.y_edge],
                            up: [+headY - 1, `${headX}`, this.y_edge]
                        };
                    this.body.push(`grid-${directions[to][0]}-${directions[to][1]}`);
                    this.removeColor();
                    this.addColor();
                    };
                }, 500);
            };
    };

取出蛇头的id,并用headY和headX分别保存蛇头在Y轴和X轴上的位置;y_edge和x_edge分别为蛇活动区域的下边界和有边界;directions主要保存蛇的位移和位置,为了方便后面使用,把y_edge和x_edge也放在了里面。

接下来我们来写controller方法,即通过控制键盘的上下左右键,来实现蛇前进方向的转变:

controller() {
                document.addEventListener('keyup', e => {
                    if (this.moveTo !== e.keyCode &&
                        this.moveTo !== e.keyCode + 2 &&
                        this.moveTo !== e.keyCode - 2 &&
                        !this.stopGame) {
                        this.moveTo = e.keyCode;
                        clearInterval(this.interval);
                        switch (e.keyCode) {
                            case 40:
                                this.move('down');
                                break;
                            case 38:
                                this.move('up');
                                break;
                            case 37:
                                this.move('left');
                                break;
                            case 39:
                                this.move('right');
                                break;
                            default:
                                break;
                        }
                    }
                }, false);

绑定keyup事件。this.moveTo用来保存当前按键的值,避免重复按键或按下方向相反的键。this.stopGame后面会讲到。

keyup事件触发之后,清除当前interval(停止蛇在当前方向上的运动),改变方向后(this.moveTo取得了新的值)继续移动。

蛇的移动到这里告一段落。下面讲吃食物且身体长度增加的部分。

食物的出现位置是随机的,且每次蛇吃完以后需要重新生成。于是我们写一个Food的类:

class Food {
            constructor() {
                if (!document.querySelector('.food')) {
                    this.x = Math.floor(13 * Math.random() + 2);
                    this.y = Math.floor(23 * Math.random() + 2);
                document.querySelector(`#grid-${this.y}-${this.x}`).classList.add('food');
                    return {
                        X: this.x,
                        Y: this.y
                    };
                }
            }
        };

对应的蛇类里的吃的方法:

eat(x, y) {
                if (x == this.food.X && y == this.food.Y) {
                    document.querySelector(`#grid-${y}-${x}`).classList.remove('food');
                    this.shouldRemove = false;
                    this.showScore.addScore();
                    this.food = new Food();
                }
            };

到这里为止核心的部分就完成了。最后我们再添加一个得分的类:

class Score {
            constructor() {
                // 用try/catch,以便在用户开启浏览器隐私模式或localStorage内存已满时使用
                try {
                    localStorage.getItem('historyScore') ? this.historyScore = localStorage.getItem('historyScore') :
                        this.historyScore = 0; // 取出历史得分,若没有则设为0,显示在页面
                    document.querySelector('.history-score').innerHTML = this.historyScore;
                } catch {
                    alert('Your localStorage is not functioning, please open this page in another browser!')
                }
                this.score = 0; // 本次游戏初始得分为0,显示在页面
                document.querySelector('.score').innerHTML = this.score;
            };
            addScore() {
                this.score++;
                document.querySelector('.score').innerHTML = this.score;
            };
            updateHistory() {
                if (this.score > this.historyScore) { // 若本次得分高于历史得分,存储新的历史得分
                    try {
                        localStorage.setItem('historyScore', this.score);
                        document.querySelector('.history-score').innerHTML = this.score;
                    } catch {
                        alert('Your localStorage is not functioning, please open this page in another browser!')
                    }
                }
            }
        }

所有这些都完成之后,我们需要有一个初始化的方法,建立蛇的活动区域,并显示蛇和得分:

const initializeGrids = (height = 500, width = 300) => {
            for (let i = 1; i < height / 20 + 1; i++) {
                for (let j = 1; j < width / 20 + 1; j++) {
                    const grid = document.createElement('div');
                    grid.setAttribute('id', `grid-${i}-${j}`);
                    grid.classList.add('grid');
                    const con = document.querySelector('.snake-area').appendChild(grid);
                }
            }
            const showSnake = new Snake(height, width);
            const showScore = new Score();
        };
        initializeGrids();

这样,一个完整的贪食蛇小游戏就完成了,复制最前面的代码,在浏览器打开就可以跑起来了。也可以到这里下载:https://gist.github.com/PengHoliday/65029f78f385b5884c1b9dfd2162c611
欢迎留言与我交流~

上一篇下一篇

猜你喜欢

热点阅读