深入解析canvas画布游戏机制

canvas使用了一个特别特殊的模式,上屏的元素,立刻被像素化。也就是说,上屏幕的元素,你将得不到这个“对象”的引用。比如,一个圆形画到了ctx上面,此时就是一堆像素点,不是一个整体的对象了,你没有任何变量能够得到这个圆形,改变这个圆形的x、y。也就是说,这种“改变”的思路在canvas中是行不通的。

所以canvas的画图的原理就是:

清屏 → 重绘 → 清屏 → 重绘  → 清屏 → 重绘 → 清屏 → 重绘  →清屏 → 重绘 → 清屏 → 重绘  →清屏 → 重绘 → 清屏 → 重绘  →清屏 → 重绘 → 清屏 → 重绘  →清屏 → 重绘 → 清屏 → 重绘  →清屏 → 重绘 → 清屏 → 重绘  →……

下面是我写的flappBird,采用了中介者模式参考了白鹭引擎中的场景管理

首先一个游戏类负责资源的加载与渲染,以及定时器的添加

// 中介者类
class Game {
    // 构造函数
    constructor(dataJson) {
        // 获取画布
        this.canvas = document.getElementById(dataJson.canvasId);
        this.ctx = this.canvas.getContext("2d");
        this.jsonUrl = dataJson.jsonUrl;
        // 数据对象
        this.R = {};
        // 定时器
        this.timer = null;
        // 帧数
        this.fno = 0;
        // 管子数组
        this.pipeArr = [];
        this.init();
        // 游戏logo坐标
        this.logoY = -48;
        // 开始按钮坐标
        this.buttonY = this.canvas.height;
        // 提示板透明度
        this.tutorialOpacity = 1;
        // 提示板闪烁间隔
        this.tutorialNum = 0.1;
        // 作者介绍从上而下
        this.autorY = -330;
        // 坠地动画序列
        this.boomNum = 1;
        // 碰撞闪屏
        this.screenFlash = 1;
        // 闪屏是否开启
        this.flashLook = true;
        // 分数
        this.score = 0;
        // 资源加载完毕
        this.allResourceLoadEnd(() => {
            // 开始按钮
            this.button_play = this.R['button_play'];
            // 游戏logo
            this.logo = this.R['logo'];
            // 游戏提示板
            this.tutorial = this.R['tutorial'];
            // 作者介绍
            this.autor = this.R['autor'];
            // 坠地动画资源
            this.boomImg;
            // 结束标志
            this.text_game_over = this.R["text_game_over"];
            // 绑定场景管理器
            this.sceneManager = new SceneManager();
            // 开始游戏
            this.start();
        });
    };
    // 初始化画布
    init() {
        let windowX = document.documentElement.clientWidth;
        let windowY = document.documentElement.clientHeight;
        // 设置画布最小宽度
        windowX = windowX < 320 ? 320 : windowX;
        windowX = windowX > 414 ? 414 : windowX;
        windowY = windowY < 500 ? 500 : windowY;
        windowY = windowY > 812 ? 812 : windowY;
        this.canvas.width = windowX;
        this.canvas.height = windowY;

    };
    // 资源管理器函数
    allResourceLoadEnd(callback) {
        let that = this;
        // 图片加载计数
        let imageLoadNum = 0;
        // 异步加载资源
        let xhr = new XMLHttpRequest();
        xhr.onreadystatechange = function() {
            if (xhr.readyState == 4) {
                if (xhr.status >= 200 && xhr.status < 300 || xhr.status == 304) {
                    // 获取数据数组
                    let imageArr = JSON.parse(xhr.responseText).images;
                    // 遍历对象
                    imageArr.forEach(value => {
                        that.R[value.name] = new Image();
                        that.R[value.name].src = value.url;
                        // 所有资源加载完毕开始游戏
                        that.R[value.name].onload = function() {
                            imageLoadNum++
                            // 渲染进度
                            that.ctx.clearRect(0, 0, that.canvas.width, that.canvas.height);
                            that.ctx.font = "20px Microsoft YaHei"
                            that.ctx.textAlign = "center"
                                // 资源加载提醒
                            that.ctx.fillText("正在加载资源" + imageLoadNum + "/" + imageArr.length + "请稍后...", that.canvas.width / 2, that.canvas.height * (1 - 0.618))
                            if (imageLoadNum == imageArr.length) {
                                callback && callback()
                            }
                        }

                    });

                }
            }

        }

        xhr.open("get", this.jsonUrl);
        xhr.send(null);

    };
    // 分数渲染
    scoreRender() {
        // 渲染分数
        let scoreStr = this.score.toString();
        for (let i = 0; i < scoreStr.length; i++) {
            this.ctx.drawImage(this.R['shuzi' + scoreStr[i]], this.canvas.width / 2 - scoreStr.length / 2 * 28 + i * 28, this.canvas.height * 0.2);
        }
    };
    // 游戏开始函数
    start() {
        this.timer = setInterval(() => {
            // 清屏
            this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            // 渲染场景
            this.sceneManager.update();
            this.sceneManager.render();
            // 帧数自增
            this.fno++;
            // 打印帧数
            this.ctx.font = "14px Arial";
            this.ctx.fillStyle = "#333"
            this.ctx.textAlign = "left";
            this.ctx.fillText("FNO:" + this.fno, 10, 20);
            // 打印场景编号
            this.ctx.font = "550 14px Microsoft YaHei";
            this.ctx.fontWeight = "700";
            this.ctx.fillText("场景号:" + this.sceneManager.sceneNumber, 10, 40)

        }, 10)
    }
}

JSON文件

{
    "images": [
        { "name": "bg_day", "url": "images/bg_day.png" },
        { "name": "bird0_0", "url": "images/bird0_0.png" },
        { "name": "bird0_1", "url": "images/bird0_1.png" },
        { "name": "bird0_2", "url": "images/bird0_2.png" },
        { "name": "bird1_0", "url": "images/bird1_0.png" },
        { "name": "bird1_1", "url": "images/bird1_1.png" },
        { "name": "bird1_2", "url": "images/bird1_2.png" },
        { "name": "bird2_0", "url": "images/bird2_0.png" },
        { "name": "bird2_1", "url": "images/bird2_1.png" },
        { "name": "bird2_2", "url": "images/bird2_2.png" },
        { "name": "land", "url": "images/land.png" },
        { "name": "pipe_down", "url": "images/pipe_down.png" },
        { "name": "pipe_up", "url": "images/pipe_up.png" },
        { "name": "shuzi0", "url": "images/font_048.png" },
        { "name": "shuzi1", "url": "images/font_049.png" },
        { "name": "shuzi2", "url": "images/font_050.png" },
        { "name": "shuzi3", "url": "images/font_051.png" },
        { "name": "shuzi4", "url": "images/font_052.png" },
        { "name": "shuzi5", "url": "images/font_053.png" },
        { "name": "shuzi6", "url": "images/font_054.png" },
        { "name": "shuzi7", "url": "images/font_055.png" },
        { "name": "shuzi8", "url": "images/font_056.png" },
        { "name": "shuzi9", "url": "images/font_057.png" },
        { "name": "logo", "url": "images/title.png" },
        { "name": "button_play", "url": "images/button_play.png" },
        { "name": "tutorial", "url": "images/tutorial.png" },
        { "name": "b1", "url": "images/b1.png" },
        { "name": "b2", "url": "images/b2.png" },
        { "name": "b3", "url": "images/b3.png" },
        { "name": "b4", "url": "images/b4.png" },
        { "name": "b5", "url": "images/b5.png" },
        { "name": "b6", "url": "images/b6.png" },
        { "name": "b7", "url": "images/b7.png" },
        { "name": "b8", "url": "images/b8.png" },
        { "name": "b9", "url": "images/b9.png" },
        { "name": "b10", "url": "images/b10.png" },
        { "name": "b11", "url": "images/b11.png" },
        { "name": "b12", "url": "images/b12.png" },
        { "name": "b13", "url": "images/b13.png" },
        { "name": "b14", "url": "images/b14.png" },
        { "name": "b15", "url": "images/b15.png" },
        { "name": "b16", "url": "images/b16.png" },
        { "name": "text_game_over", "url": "images/text_game_over.png" },
        { "name": "autor", "url": "images/autor.png" }
    ]

}

场景管理类负责各个场景下的渲染以及更新指向

// 场景管理器类game
class SceneManager {
    // 构造函数
    constructor() {
        // 绑定天空个背景
        game.backGround = new BackGround();
        // 绑定大地背景
        game.land = new Land();
        // 绑定小鸟
        game.bird = new Bird();
        // 场景标识符
        this.sceneNumber = 1;
        this.bindEvenet();
    };
    // 更新函数
    update() {
        switch (this.sceneNumber) {
            case 1:
                // 更新logo和开始按钮坐标
                game.logoY += 10;
                game.buttonY -= 10;
                // logo距离天空200
                if (game.logoY > 120) {
                    game.logoY = 120;
                }
                // 按钮距离大地140
                if (game.buttonY < game.land.y - 150) {
                    game.buttonY = game.land.y - 150
                }
                break;
            case 2:
                // 煽动翅膀
                game.bird.wingUpdate();
                // 背景板闪烁
                if (game.fno % 3 == 0) {
                    if (game.tutorialOpacity < 0.1 || game.tutorialOpacity > 0.9) {
                        game.tutorialNum *= -1;
                    }
                    game.tutorialOpacity += game.tutorialNum;
                }
                break;
            case 3:
                // 煽动翅膀
                game.bird.wingUpdate();
                // 作者介绍
                game.autorY += 10;
                if (game.autorY > 118) {
                    game.autorY = 118
                }
                break;
            case 4:
                // 更新小鸟
                game.bird.update();
                game.backGround.update();
                game.land.update();
                // 每150帧创建管子
                game.fno % 150 == 0 && new Pipe();
                // 遍历管子数组更新
                game.pipeArr.forEach(ele => {
                    ele.update();
                });
                break;
            case 5:
                if (game.bird.y > game.land.y) {
                    game.bird.isDropFloor = true;
                }
                // 碰撞后小鸟失去能量
                game.bird.hasEnergy = false;
                // 闪屏更新
                if (game.flashLook) {
                    game.screenFlash -= 0.1;
                }
                if (game.screenFlash < 0) {
                    game.flashLook = false;
                    game.screenFlash = 1;
                }
                // 没落地更新小鸟
                if (!game.bird.isDropFloor) {
                    // 更新小鸟
                    game.fno % 3 == 0 && (game.bird.birdFon++);
                    game.bird.y += game.bird.downSpeed * game.bird.birdFon;
                } else {
                    game.fno % 3 == 0 && (game.boomNum++);
                    // 坠地动画结束后进入场景6
                    if (game.boomNum >= 16) {
                        this.enter(6);
                    };
                    game.boomImg = game.R["b" + game.boomNum];
                }
                break;
            case 6:
        }

    }
    render() {
        switch (this.sceneNumber) {
            case 1:
                // 场景一静态天空大地
                game.backGround.render();
                game.land.render();
                // 渲染小鸟
                game.bird.render();
                // 渲染logo和button
                game.ctx.drawImage(game.logo, game.canvas.width / 2 - 89, game.logoY);
                game.ctx.drawImage(game.button_play, game.canvas.width / 2 - 58, game.buttonY);
                break

            case 2:
                // 场景二静态天空大地
                game.backGround.render();
                game.land.render();
                // 渲染小鸟
                game.bird.render();
                // 渲染提示板
                game.ctx.save();
                game.ctx.globalAlpha = game.tutorialOpacity
                game.ctx.drawImage(game.tutorial, game.canvas.width / 2 - 57, game.land.y - 180);
                game.ctx.restore();
                break;

            case 3:
                // 场景三静态天空大地
                game.backGround.render();
                game.land.render();
                // 渲染小鸟
                game.bird.render();
                game.ctx.drawImage(game.autor, game.canvas.width / 2 - 175, game.autorY);
                break;
            case 4:
                // 场景四静态天空大地
                game.backGround.render();
                game.land.render();
                // 遍历管子数组渲染
                game.pipeArr.forEach(ele => {
                    ele.render();
                });
                // 渲染小鸟
                game.bird.render();
                // 渲染分数
                game.scoreRender();
                break;
            case 5:
                game.ctx.globalAlpha = game.screenFlash;
                // 场景五静态天空大地
                game.backGround.render();
                game.land.render();
                // 遍历管子数组渲染
                game.pipeArr.forEach(ele => {
                    ele.render();
                });

                // 坠地动画
                if (!game.bird.isDropFloor) {
                    // 渲染小鸟
                    game.bird.y <= game.land.y && game.bird.render();
                } else {
                    game.boomImg && game.ctx.drawImage(game.boomImg, game.bird.x - 93, game.land.y - 350)
                }
                // 渲染分数
                game.scoreRender();
                break;
            case 6:
                // 场景五静态天空大地
                game.backGround.render();
                game.land.render();
                game.ctx.drawImage(game.text_game_over, game.canvas.width / 2 - 102, game.canvas.height * (1 - 0.618))
                    // 渲染分数
                game.scoreRender();
                break;


        }

    }
    enter(num) {
        this.sceneNumber = num;
        switch (this.sceneNumber) {
            case 1:
                // 每次进入场景一瞬间还原位置
                game.fno = 0;
                game.logoY = -48;
                game.buttonY = game.canvas.height;
                game.bird = new Bird();
                game.score = 0;
                break;
            case 2:
                // 场景二位置重设
                game.bird.y = 150;
                break;
            case 3:
                // 场景三位置重设
                game.autorY = -330;
                break;
            case 4:
                game.pipeArr = [];
                game.bird.birdFon = 0;
                break;
            case 5:
                game.boomNum = 0;
                game.screenFlash = 1;
                game.flashLook = true;
                game.bird.isDropFloor = false;
        }

    }
    bindEvenet() {
        // document.onclick = (event) => {
        //     clickHandler.call(this, event.clientX, event.clientY);
        // };
        document.addEventListener("touchstart", (event) => {
            clickHandler.call(this, event.touches[0].clientX, event.touches[0].clientY);
        })

        function clickHandler(mousex, mousey) {
            switch (this.sceneNumber) {
                case 1:
                    if (mousey > game.buttonY && mousey < game.buttonY + 70 && mousex > game.canvas.width / 2 - 58 && mousex < game.canvas.width / 2 + 58) {
                        this.enter(2)
                    }
                    break;
                case 2:
                    this.enter(3);
                    break;
                case 3:
                    this.enter(4)
                    break;
                case 4:
                    game.bird.fly();
                    break;
                case 6:
                    this.enter(1)
                    break;

            }
        }



    }
}

接着就是各个演员类,例:小鸟,管子,天空,大地

// 天空背景类
class BackGround {
    constructor() {
        this.bg_day = game.R['bg_day'];
        this.x = 0;
        this.y = game.canvas.height * 0.73 - 412;
        this.h = 512;
        this.w = 288;
        this.speed = 1.8;


    };
    // 更新函数
    update() {
        this.x -= this.speed;
        if (this.x < -this.w) {
            this.x = 0;
        }
    };
    // 渲染函数
    render() {
        game.ctx.fillStyle = "#4ec0ca";
        game.ctx.fillRect(0, 0, game.canvas.width, this.y);
        game.ctx.fillStyle = "#5ee270";
        game.ctx.fillRect(0, this.y + this.h - 1, game.canvas.width, game.canvas.height - this.y - this.h);
        game.ctx.drawImage(this.bg_day, this.x, this.y);
        game.ctx.drawImage(this.bg_day, this.x + this.w, this.y);
        game.ctx.drawImage(this.bg_day, this.x + this.w * 2 - this.speed, this.y);
    }
}
class Bird {
    constructor() {
        // 鸟颜色随机
        this.color = parseInt(Math.random() * 3);
        // 小鸟翅膀状态
        this.birdWing = 0;
        // 小鸟资源
        this.birdArr = [
            game.R["bird" + this.color + "_0"],
            game.R["bird" + this.color + "_1"],
            game.R["bird" + this.color + "_2"],
        ];

        // 小鸟坐标
        this.x = game.canvas.width / 2;
        this.y = game.canvas.height * (1 - 0.618);
        // 小鸟的旋转角度
        this.d = 0;
        // 小鸟每帧旋转角度
        this.framD = 0.023;
        // 小鸟帧
        this.birdFon = 0;
        // 下落速度
        this.downSpeed = 0.53;
        // 上升速度
        this.upSpeed = 0.18;
        // 是否有能量
        this.hasEnergy = false;
        // 能量数量
        this.energyNum = 16;
        // 是否落地
        this.isDropFloor = false;
    };
    // 更新
    update() {
        // 小鸟翅膀切换
        this.wingUpdate();
        // 小鸟帧数自增
        game.fno % 4 == 0 && this.birdFon++;
        // 没有能量下落加速度
        if (!this.hasEnergy) {
            this.y += this.downSpeed * this.birdFon;
            // 旋转
            this.d += this.framD;
        } else {
            // 有能量上升加速度
            this.y += this.upSpeed * parseInt((this.birdFon - this.energyNum));
            // 上升时旋转放缓
            game.fno % 4 == 0 && (this.d += this.framD);
            // 当能量耗尽关掉能量开关
            if (this.birdFon > this.energyNum) {
                this.hasEnergy = false;
                this.birdFon = 0;
            }
        }

        // 触碰大地结束
        if (this.y > game.land.y) {
            this.isDropFloor = true;
            game.sceneManager.enter(5);
        }
        // 不能飞过天空
        if (this.y < 0) {
            this.y = 0;
        }
        // 计算小鸟上右下左位置
        this.T = this.y - 12;
        this.R = this.x + 17;
        this.B = this.y + 12;
        this.L = this.x - 17;

    };
    // 渲染
    render() {
        game.ctx.save();
        game.ctx.translate(this.x, this.y);
        game.ctx.rotate(this.d);
        game.ctx.drawImage(this.birdArr[this.birdWing], -24, -24)
        game.ctx.restore();
    };
    // 翅膀更新
    wingUpdate() {
        game.fno % 8 == 0 && this.birdWing++;
        if (this.birdWing > 2) {
            this.birdWing = 0;
        }
    };
    // 小鸟飞翔
    fly() {
        // 点击屏幕时赋予小鸟能量
        // 赋予能量
        this.hasEnergy = true;
        // 重新计算帧数
        this.birdFon = 0;
        this.d = -0.6
    };
}
class Land {
    constructor() {
        this.land = game.R['land'];
        this.x = 0;
        this.y = game.canvas.height * 0.73;
        this.w = 336;
        this.h = 112;
        this.speed = 3;
    };
    // 更新函数
    update() {
        this.x -= this.speed;
        if (this.x < -this.w) {
            this.x = 0;
        }

    };
    // 渲染函数
    render() {
        game.ctx.fillStyle = "#ded895";
        game.ctx.fillRect(0, this.y + this.h - 1, game.canvas.width, game.canvas.height - this.y - 112);
        game.ctx.drawImage(this.land, this.x, this.y);
        game.ctx.drawImage(this.land, this.x + this.w, this.y);
        game.ctx.drawImage(this.land, this.x + this.w * 2, this.y);
    }
}
class Pipe {
    constructor() {
        this.pipe_down = game.R["pipe_down"];
        this.pipe_up = game.R["pipe_up"];
        this.w = 52;
        this.allH = 320;
        this.x = game.canvas.width;
        this.speed = 3;
        // 管子间隔
        this.spacing = 160;
        // 上管子高度随机
        this.height1 = parseInt(Math.random() * 121) + 100;
        console.log(this.height1)
            // 下管子高度自适应
        this.height2 = game.land.y - this.height1 - this.spacing;
        game.pipeArr.push(this);
        // 小鸟是否越过当前管子
        this.leapOver = false;
    };
    // 更新
    update() {
        this.x -= this.speed;
        if (this.x <= -52) {
            this.removeSelf();
        }
        // 碰撞检测
        if (game.bird.R > this.x && game.bird.L < this.x + 52) {
            if (game.bird.T < this.height1 || game.bird.B > this.height1 + this.spacing) {
                // 死亡进入场景5
                game.sceneManager.enter(5)
            }
        }
        // 小鸟是否跃过当前管子

        if (!this.leapOver) {
            if (game.bird.L > this.x + 52) {
                game.score++;
                this.leapOver = true;

            }
        }
    };
    // 渲染
    render() {
        game.ctx.drawImage(this.pipe_down, 0, this.allH - this.height1, this.w, this.height1, this.x, 0, this.w, this.height1);
        game.ctx.drawImage(this.pipe_up, 0, 0, this.w, this.height2, this.x, this.height1 + this.spacing, this.w, this.height2)

    };
    // 删除自己
    removeSelf() {
        game.pipeArr.forEach((ele, i) => {
            if (ele === this) {
                game.pipeArr.splice(i, 1)
            }
        });
    }
}

最后调用中介者类

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0,user-scalable=no" id="viewport" />
    <meta autor="xiaBin">
    <title>xiaBin_flappBird</title>
    <style>
        * {
            padding: 0;
            margin: 0;
        }
        
        body {
            overflow: hidden;
        }
    </style>
</head>

<body>
    <canvas id="xBinCanvas">

    </canvas>
    <script src="js/game.js"></script>
    <script src="js/sceneManager.js"></script>
    <script src="js/backGround.js"></script>
    <script src=js/land.js></script>
    <script src=js/pipe.js></script>
    <script src=js/bird.js></script>
    <script>
        let game = new Game({
            "canvasId": "xBinCanvas",
            "jsonUrl": "R.json"
        })
    </script>
    <script>
    </script>
</body>

</html>

 

posted on 2020-03-16 09:40  素心~  阅读(712)  评论(0编辑  收藏  举报

导航