Skip to content
html
<!DOCTYPE html>
<html lang="cmn-Hans">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        * {
            margin: 0;
            padding: 0;
        }

        .gameBox {
            width: 800px;
            height: 500px;
            border: 1px solid black;
            margin: 50px auto 10px;
            position: relative;
            overflow: hidden;
        }

        .info {
            text-align: center;
        }

        .background {
            width: 200%;
            height: 100%;
            background: url("imgs/sky.png");
            position: absolute;
            top: 0;
            left: 0;
        }

        .bird {
            height: 25px;
            position: absolute;
            background-image: url("imgs/bird.png");
            width: 35px;
        }

        .bird.swing1 {
            background-position: -8px -10px;
        }

        .bird.swing2 {
            background-position: -60px -10px;
        }

        .bird.swing3 {
            background-position: -113px -10px;
        }

        .pipeUp,
        .pipeDown {
            width: 52px;
            height: 100px;
            position: absolute;
        }

        .pipeUp {
            background-image: url("imgs/pipeUp.png");
            bottom: 110px;

        }

        .pipeDown {
            background-image: url("imgs/pipeDown.png");
            background-position: bottom;
        }

        .ground {
            width: 200%;
            height: 110px;
            top: 390px;
            position: absolute;
            background-image: url("imgs/land.png");
        }
    </style>
</head>

<body>
    <div class="gameBox">
        <div class="background"></div>
        <div class="bird swing1"></div>
        <div class="ground"></div>
    </div>
    <p class="info">按Enter开始或暂停游戏,按Space跳跃</p>
</body>

</html>
<script src="./js/Common.js"></script>
<script src="./js/Sky.js"></script>
<script src="./js/Ground.js"></script>
<script src="./js/Bird.js"></script>
<script src="./js/Pipe.js"></script>
<script src="./js/Game.js"></script>

common. js ​

js
class Common {
    //公共父类,速度单位:像素每秒
    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.timer = null;
    }

    move(duration) {
        //按照speed的速度,移动duration的时间,duration为秒
        const disX = this.speedX * duration;
        const disY = this.speedY * duration;
        this.left += disX;
        this.top += disY;
    }
    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";

    }
}

sky. js ​

js
const skyDom = document.querySelector(".background");
class Sky extends Common {
    //天空类
    constructor(speedX, skyDom) {
        super(1600, 500, 0, 0, speedX, 0, skyDom);
    }
    move(duration) {
        //按照speed的速度,移动duration的时间,duration为秒
        super.move(duration);
        if (this.left <= -800) {
            this.left = 0;
        }
        this.render();
    }
    startMove() {
        if (this.timer) return;
        this.timer = setInterval(() => {
            this.move(16 / 1000);
        }, 16);
    }
    stopMove() {
        clearInterval(this.timer);
        this.timer = null;
    }
}

ground. js ​

js
const groundDom = document.querySelector(".ground");
class Ground extends Common {
    constructor(speedX, dom) {
        super(1600, 500, 0, 390, speedX, 0, dom);
    }
    move(duration) {
        //按照speed的速度,移动duration的时间,duration为秒
        super.move(duration);
        if (this.left <= -800) {
            this.left = 0;
        }
        this.render();
    }
    startMove() {
        if (this.timer) return;
        this.timer = setInterval(() => {
            this.move(16 / 1000);
        }, 16);
    }
    stopMove() {
        clearInterval(this.timer);
        this.timer = null;
    }
}

bird. js ​

js
const birdDom = document.querySelector(".bird");
const birdStyle = window.getComputedStyle(birdDom);
const birdWidth = parseFloat(birdStyle.width);
const birdHeight = parseFloat(birdStyle.height);
class Bird extends Common {
    constructor(speedY, birdDom) {
        super(birdWidth, birdHeight, 100, 100, 0, speedY, birdDom);
        this.g = 500;
        this.swingTimer = null;
        this.swingFlag = 1;
    }
    move(duration) {
        //按照speed的速度,移动duration的时间,duration为秒
        super.move(duration);
        this.speedY += this.g * duration;
        if (this.top < 0) {
            this.top = 0;
        }
        if (this.top > 500 - 110 - birdHeight) {
            this.top = 500 - 110 - birdHeight;
        }
        this.render();
    }
    startSwing() {
        if (this.swingTimer) {
            return;
        }
        this.swingTimer = setInterval(() => {
            this.swingFlag++;
            if (this.swingFlag == 4) this.swingFlag = 1;
            this.render();
        }, 300);
    }
    stopSwing() {
        clearInterval(this.swingTimer);
        this.swingTimer = null;
    }
    render() {
        super.render();
        this.dom.className = "bird swing" + this.swingFlag;
    }
    startMove() {
        if (this.timer) return;
        this.timer = setInterval(() => {
            this.move(16 / 1000);
        }, 16);
    }
    stopMove() {
        clearInterval(this.timer);
        this.timer = null;
    }
    jump() {
        this.speedY = -200;
    }
}
// var bird = new Bird(200, birdDom);
// bird.startMove();
// bird.startSwing();

pipe. js ​

js
const gameBox = document.querySelector(".gameBox");
class Pipe extends Common {
    constructor(height, isUp, speedX) {
        super(52, height, 800, 0, speedX, 0, null);
        if (isUp) this.top = 500 - 110 - height;
        const newPipe = document.createElement("div");
        if (isUp) newPipe.classList.add("pipeUp");
        else newPipe.classList.add("pipeDown");
        this.dom = newPipe;
        this.dom.style.left = this.left + "px";
        gameBox.append(this.dom);
    }
    move(duration) {
        super.move(duration);
        if (this.left <= -52) {
            this.dom.remove();
        }
        this.render();
    }
    startMove() {
        if (this.timer) return;
        this.timer = setInterval(() => {
            this.move(16 / 1000);
        }, 16);
    }
    stopMove() {
        clearInterval(this.timer);
        this.timer = null;
    }
}
// const pipe = new Pipe(200, false, -50);

class PipeController {
    constructor(speedX) {
        this.pipes = [];
        this.timer = null;
        this.speedX = speedX;
    }
    producePipe() {
        if (this.timer) return;
        this.pipes.forEach(function (item) {
            item.startMove();
        });
        this.timer = setInterval(() => {
            this.pipes.forEach(function (item, i, arr) {
                if (item.left < -52) arr.splice(i, 1);
            });
            const gap = 100;
            const minHeight = 80;
            const maxHeight = 390 - gap - minHeight;
            const height =
                Math.floor(Math.random() * (maxHeight - minHeight)) + minHeight;
            const newPipeUp = new Pipe(height, true, this.speedX);
            const newPipeDown = new Pipe(
                390 - gap - height,
                false,
                this.speedX
            );
            this.pipes.push(newPipeDown, newPipeUp);
            newPipeUp.startMove();
            newPipeDown.startMove();
        }, 2500);
    }
    stopProducePipe() {
        clearInterval(this.timer);
        this.timer = null;
        this.pipes.forEach((item) => {
            item.stopMove();
        });
    }
}

// var test = new PipeController();
// test.producePipe();

game. js ​

js
/**
 * 游戏类,负责游戏的整体逻辑和管理。
 * @param {number} pipeSpeed 管道移动的速度。
 * @param {number} groundSpeed 地面移动的速度。
 * @param {number} birdSpeed 鸟飞行的速度。
 * @param {number} skySpeed 天空移动的速度。
 */
class Game {
    constructor(pipeSpeed, groundSpeed, birdSpeed, skySpeed) {
        // 初始化游戏速度和各个游戏元素
        this.pipeSpeed = pipeSpeed;
        this.groundSpeed = groundSpeed;
        this.birdSpeed = birdSpeed;
        this.skySpeed = skySpeed;
        this.bird = new Bird(this.birdSpeed, birdDom);
        this.sky = new Sky(this.skySpeed, skyDom);
        this.ground = new Ground(this.groundSpeed, groundDom);
        this.pipe = new PipeController(this.pipeSpeed);
        this.end = false;
        this.pause = false;
    }

    /**
     * 开始游戏移动,启动所有游戏元素的移动。
     */
    startMove() {
        this.pause = false;
        this.bird.startMove();
        this.sky.startMove();
        this.ground.startMove();
        this.pipe.producePipe();
        // 每16ms检查游戏是否结束
        setInterval(() => {
            if (this.isEnd()) {
                this.stopMove();
                this.end = true;
            }
        }, 16);
    }

    /**
     * 停止游戏移动,暂停所有游戏元素的移动。
     */
    stopMove() {
        this.pause = true;
        this.bird.stopMove();
        this.sky.stopMove();
        this.ground.stopMove();
        this.pipe.stopProducePipe();
    }

    /**
     * 检测两个DOM元素是否相撞。
     * @param {HTMLElement} dom1 第一个DOM元素。
     * @param {HTMLElement} dom2 第二个DOM元素。
     * @returns {boolean} 如果相撞返回true,否则返回false。
     */
    crash(dom1, dom2) {
        const dom1Style = window.getComputedStyle(dom1);
        const dom2Style = window.getComputedStyle(dom2);
        const dom1CenterX =
            parseFloat(dom1Style.width) / 2 + parseFloat(dom1Style.left);
        const dom1CenterY =
            parseFloat(dom1Style.height) / 2 + parseFloat(dom1Style.top);
        const dom2CenterX =
            parseFloat(dom2Style.width) / 2 + parseFloat(dom2Style.left);
        const dom2CenterY =
            parseFloat(dom2Style.height) / 2 + parseFloat(dom2Style.top);
        if (
            Math.abs(dom1CenterX - dom2CenterX) <
                Math.abs(
                    (parseFloat(dom1Style.width) +
                        parseFloat(dom2Style.width)) /
                        2
                ) &&
            Math.abs(dom1CenterY - dom2CenterY) <
                Math.abs(
                    (parseFloat(dom1Style.height) +
                        parseFloat(dom2Style.height)) /
                        2
                )
        ) {
            return true;
        } else {
            return false;
        }
    }

    /**
     * 检测游戏是否结束。
     * @returns {boolean} 如果游戏结束返回true,否则返回false。
     */
    isEnd() {
        // 检查鸟是否撞上地面或者顶部,或者撞上管道
        if (this.bird.top <= 0 || this.bird.top >= 390 - this.bird.height) {
            return true;
        }
        for (let i = 0; i < this.pipe.pipes.length; i++) {
            const item = this.pipe.pipes[i];
            if (this.crash(item.dom, this.bird.dom)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 监听键盘事件,控制游戏的开始、暂停和重载。
     */
    onEvent() {
        window.addEventListener("keydown", (e) => {
            // 空格键让鸟跳跃
            if (e.key === " ") this.bird.jump();
            // 回车键用于游戏重载或暂停/继续游戏
            if (e.key === "Enter") {
                if (this.end) {
                    window.location.reload();
                } else {
                    if (this.pause) {
                        this.startMove();
                    } else {
                        this.stopMove();
                    }
                }
            }
        });
    }
}
// 创建游戏实例并启动
var test = new Game(-80, -80, 200, -50);
test.onEvent();
test.startMove();

以笔记为镜,可以知得失