ES6之像素鸟

130 阅读5分钟

ES6之像素鸟

设计这个小游戏时我们使用了类的思想。类似这样的界面:

截屏2025-06-12 14.24.13.png 我们先写好所有的 css 样式。这里就不写了。然后我们想这个游戏中天空,大地,管道,小鸟都在动,只是动的方式不一样而已。所以我们可以创建一个关于矩形移动的类。

/**
 * 创建一个移动的类,有宽度,高度,横坐标,纵坐标,横向速度,纵向速度,dom元素
 */
class Rectangle { 
    constructor(width, height, left, top, speedX, speedY, dom) {
        this.width = width;
        this.height = height;
        this.left = left;
        this.top = top;
        this.speedX = speedX;
        this.speedY = speedY;
        this.dom = dom;
        this.render();
    }
    //渲染
    render() {
        this.dom.style.width = this.width + "px";
        this.dom.style.height = this.height + "px";
        this.dom.style.left = this.left + "px";
        this.dom.style.top = this.top + "px";
    }
    //移动速度 单位 像素/秒 时间 秒
    move(duration) { 
        const xDis = this.speedX * duration;
        const yDis = this.speedY * duration;
        this.left = this.left + xDis;
        this.top = this.top + yDis;
        //如果子类上有onMove方法,则调用
        if (this.onMove) {
            this.onMove();
        }
        this.render();
    }
}

构造器把所有要用的属性添加了进去,同时也把渲染的属性添加进去。方便开始时加载界面元素。
然后就是移动效果,移动需要提供移动的时间,可以得出移动的横向和纵向距离。
注意:如果我们想要在父类中设置一些子类的情况,可以加入一些方法的判断。如果子类中有这些方法,就会先去调用子类的方法。
这是天空的效果:

const skyDom = document.querySelector(".sky");
const skyStyles = getComputedStyle(skyDom);
const skyWidth = parseFloat(skyStyles.width);
const skyHeight = parseFloat(skyStyles.height);
class Sky extends Rectangle {
    //继承属性
    constructor() {
        super(skyWidth,skyHeight,0, 0, -50,0, skyDom)
    }
    onMove() {
        if (this.left <= -skyWidth / 2) {
            this.left = 0;
        }
    }
}
const sky = new Sky();
setInterval(() => {
    //移动时间
    sky.move(10/1000);
}, 10);

大地的代码和天空类似,只是速度要比天空更快。最后的效果如下。 下面是小鸟的代码:

const birdDom = document.querySelector(".bird");
const birdStyles = getComputedStyle(birdDom);
const birdWidth = parseFloat(birdStyles.width);
const birdHeight = parseFloat(birdStyles.height);
const birdTop = parseFloat(birdStyles.top);
const birdLeft = parseFloat(birdStyles.left);
const gameHeight = parseFloat(getComputedStyle(document.querySelector(".game")).height);
class Bird extends Rectangle {
    constructor() {
        super(birdWidth, birdHeight, birdLeft, birdTop, 0, 0, birdDom)
        this.g = 1500;//重力加速度 单位:像素/平方秒
        this.maxY = gameHeight - landHeight - birdHeight;
        this.swingCount = 1;//初始化为1
        this.timer = null;//定时器
        this.render()
    }
    startSwing() {
        if (this.timer) {
            return;
        }
        this.timer = setInterval(() => {
            this.swingCount++;
            if (this.swingCount === 4) {
                this.swingCount = 1;
            }
        }, 200)
    }
    render() {
        super.render();
        this.dom.className = `bird swing${this.swingCount}`
    }
    stopSwing() {
        clearInterval(this.timer);
        this.timer = null;
    }
    move(duration) {
        super.move(duration); //调用父类方法
        //根据加速度改变速度
        this.speedY += this.g * duration;
    }
    onMove() {
        if (this.top < 0) {
            this.top = 0;
        } else if (this.top > this.maxY) {
            this.top = this.maxY;
        }
    }
    jump() {
        this.speedY = -450;
    }
}
var bird = new Bird();
setInterval(() => {
    bird.move(10/1000)
    bird.startSwing()
}, 10)

首先小鸟在落地的时候需要一个重力加速度,所以增加了一个属性,还需要在扇动翅膀时启用计时器,也要设置一个属性。同时,需要在小鸟落地时不能离开屏幕,有一个最大高度的限制。
之后,在 move 方法里进行覆盖,不断更改原来的纵向速度。 onmove 方法里也要重新覆盖。
注意:在扇动翅膀时,父类里没有相关代码,需要重新写一个 render 方法。增加一个类样式。

class Pipe extends Rectangle {
    //需要的参数
    constructor(height, top, speed, dom) {
        super(52, height, gameWidth, top, speed, 0, dom);
    }
    onMove() {
        if (this.left <= -this.width) {
            this.dom.remove();
        }
    }
}
function getRandom(min, max) {
    return Math.floor(Math.random() * (max - min) + min);
}
class PipePair {
    constructor(speed) {
        this.spaceHeight = 150;
        this.minHeight = 80;
        this.maxHeight = landTop - this.spaceHeight - this.minHeight;
        const upHeight = getRandom(this.minHeight, this.maxHeight);
        //创建updom
        const upDom = document.createElement('div');
        upDom.className = 'pipe up';
        //创建uppipe
        this.upPipe = new Pipe(upHeight, 0, speed, upDom)

        const downHeight = landTop - upHeight - this.spaceHeight;
        const downTop = landTop - downHeight;
        //创建downDom
        const downDom = document.createElement('div');
        downDom.className = 'pipe down';
        this.downPipe = new Pipe(downHeight, downTop, speed, downDom)

        gameDom.appendChild(upDom);
        gameDom.appendChild(downDom);
    }
    get useLess() {
        return this.upPipe.left < -this.upPipe.width;
    }
    move(duration) {
        this.upPipe.move(duration);
        this.downPipe.move(duration);
    }
}
//不断创建柱子对
class PipePairProducer {
    constructor(speed) {
        this.speed = speed;
        this.pairs = [];//存放柱子对
        this.timer = null;//计时器
        this.tick = 1500;//间隔时间
    }
    startProduce() {
        if (this.timer) {
            return;
        }
        this.timer = setInterval(() => {
            this.pairs.push(new PipePair(this.speed))
            for (let i = 0; i < this.pairs.length; i++) {
                var pair = this.pairs[i];
                if (pair.useLess) {
                    //pair里面的useLess属性可以判断是否超出视野
                    this.pairs.splice(i, 1);
                    i--;
                }
            }
        }, this.tick);
    }
    stopProduce() {
        clearInterval(this.timer);
        this.timer = null;
    }
}
var producer = new PipePairProducer(-100);
producer.startProduce();
setInterval(() => {
    producer.pairs.forEach(pair => {
        pair.move(16/1000);
    })
}, 16);

我们先创建一个 pipe 类。继承父类的属性,其中 height, top, speed, dom 是需要额外添加的属性。其中,当元素块移动到左边的边缘移出视野之后,移除这个元素块。
然后创建一个 pipePair 类。设置一些需要添加的参数,建立上管道和下管道两个实例。添加到 game 中。 useLess 用来避免生成过多的管道对,如果超出视野范围,进行移除。
还需要创建一个不断生成管道对的类。需要设置一个计时器去控制生成的速度。当结束生成时,需要关闭计时器。

class Game {
    constructor() {
        //天空和柱子对调整成相同的速度
        this.sky = new Sky();
        this.land = new Land(-100);
        this.bird = new Bird();
        this.pipeProducer = new PipePairProducer(-100);
        this.timer = null;//计时器
        this.tick = 16;//时间间隔16毫秒
        this.gameOver = false;//是否游戏结束
    }
    // 游戏开始
    start() {
        if (this.timer) {
            return;
        }
        if (this.gameOver) {
            //如果游戏结束,则重新开始
            window.location.reload();
        }
        this.bird.startSwing();
        this.pipeProducer.startProduce();
        this.timer = setInterval(() => {
            const duration = this.tick / 1000;
            this.sky.move(duration);
            this.land.move(duration);
            this.bird.move(duration);
            this.pipeProducer.pairs.forEach(pair => {
                pair.move(duration);
            });
            if (this.isGameOver()) {
                this.stop();
                this.gameOver = true;
            }
        }, this.tick);
    }
    isGameOver() {
        //小鸟掉地上了
        if (this.bird.top === this.bird.maxY) {
            this.gameOver = true;
            console.log('小鸟掉地上了')
            return true;
        }
        for (let i = 0; i < this.pipeProducer.pairs.length; i++) {
            const pair = this.pipeProducer.pairs[i];
            //还有一种情况是小鸟撞到柱子上了
            if (this.isHit(this.bird, pair.upPipe) || this.isHit(this.bird, pair.downPipe)) {
                this.gameOver = true;
                return true;
            }
        }
        return false;
    }
    //发生碰撞函数
    isHit(rec1, rec2) {
        //横向碰撞:两个矩形中心点的横向距离小于两个矩形宽度的一半
        //纵向碰撞:两个矩形中心点的纵向距离小于两个矩形高度的一半
        const centerX1 = rec1.left + rec1.width / 2;
        const centerX2 = rec2.left + rec2.width / 2;
        const centerY1 = rec1.top + rec1.height / 2;
        const centerY2 = rec2.top + rec2.height / 2;
        const disX = Math.abs(centerX1 - centerX2);
        const disY = Math.abs(centerY1 - centerY2);
        if (disX < (rec1.width + rec2.width) / 2 && disY < (rec1.height + rec2.height) / 2) {
            return true;
        }
        return false;
    }
    // 游戏结束
    stop() {
        clearInterval(this.timer);
        this.timer = null;
        this.bird.stopSwing();
        this.pipeProducer.stopProduce();
    }
    regEvent() {
        window.addEventListener('keydown', e => {
            if (e.key === 'Enter') {
                if (this.timer) {
                    this.stop();
                }
                else {
                    this.start();
                }
            } else if (e.key === ' ') {
                this.bird.jump();
            }
        });
    }
}

var game = new Game();
game.regEvent()

现在我们需要从游戏整体的角度来创建类。先把之前创建的各个模块加进去。游戏一定会包含开始和结束方法。
开始方法会调用计时器,保证各个模块都可以动起来。还要判断游戏是否结束。
结束方法会停掉计时器,同时阻止小鸟继续扇翅膀和管道对的生成。
中间的游戏结束方法会判断两种情况。一种是小鸟是否掉地上了,另外一种是小鸟是否和柱子发生碰撞。需要增加一个辅助函数来判断两个矩形的中心点距离和两个矩形的本身的距离。如果中心点距离小的话,则判定为碰撞。
最后,通过键盘的键位来控制游戏的暂停和继续。