美文网首页
手把手教你用JS写一个贪食蛇小游戏

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

作者: HolidayPeng | 来源:发表于2018-05-22 17:43 被阅读203次

    本文阅读时间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
    欢迎留言与我交流~

    相关文章

      网友评论

          本文标题:手把手教你用JS写一个贪食蛇小游戏

          本文链接:https://www.haomeiwen.com/subject/fzrkjftx.html