前端实战小案例--canvas实战之FlappyBird小游戏

前端实战小案例--canvas实战之FlappyBird小游戏

想练习更多前端案例,请进个人主页,点击前端实战案例->传送门

觉得不错的记得点个赞?支持一下我0.0!谢谢了!

不积跬步无以至千里,不积小流无以成江海。

效果图如下:(由于图片有大小限制,所以录制的是10帧,有点卡顿的感觉,实际是很流畅的)

素材如下:

 

文件结构目录如下:

代码如下:

index.html

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no">
		<title>FlappyBird</title>
		<link rel="stylesheet" href="./css/index.css" />
	</head>
	<body>
		<canvas>
			<span>您的浏览器不支持画布元素,请使用谷歌浏览器</span>
		</canvas>
	</body>
	<script src="./js/Game.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 src="./js/SceneManager.js"></script>
	<script>
		var game = new Game();
	</script>
</html>

index.css

*{
	margin: 0;
	padding: 0;
}

canvas{
	display: block;
	margin: 0 auto;
}

resource.json

[
	{
		"imgs":[
			{"name":"Back1", "url":"./resource/images/Back1.png"},
			{"name":"Bird", "url":"./resource/images/Bird.png"},
			{"name":"Flash", "url":"./resource/images/Flash.png"},
			{"name":"Logo", "url":"./resource/images/Logo.png"},
			{"name":"Number", "url":"./resource/images/Number.png"},
			{"name":"Land2", "url":"./resource/images/Road2.png"},
            {"name":"Pipe1", "url":"./resource/images/Pipe1.png"},
            {"name":"Pipe2", "url":"./resource/images/Pipe2.png"},
			{"name":"Bomb", "url":"./resource/images/Bomb.png"}
		]
	}
]

Background.js

//背景类
(function () {
    var Background = window.Background = function () {
        this.image = game.imgArr["Back1"];
        this.speed = 0;
    };
    Background.prototype.render = function () {
        game.ctx.clearRect(0,0,game.canvas.width,game.canvas.height);
        game.ctx.drawImage(this.image,this.speed,game.canvas.height*0.618-this.image.height*0.618);
        game.ctx.drawImage(this.image,this.image.width+this.speed,game.canvas.height*0.618-this.image.height*0.618);
        game.ctx.drawImage(this.image,this.image.width*2+this.speed,game.canvas.height*0.618-this.image.height*0.618);
        game.ctx.save();
        game.ctx.fillStyle = "#4EC0CA";
        game.ctx.beginPath();
        game.ctx.fillRect(0,0,game.canvas.width,game.canvas.height*0.618-this.image.height*0.618+10);
        game.ctx.restore();
        game.ctx.save();
        game.ctx.fillStyle = "#DED895";
        game.ctx.beginPath();
        game.ctx.fillRect(0,game.canvas.height*0.618+this.image.height*(1-0.618)-10,game.canvas.width,game.canvas.height*(1-0.618)-this.image.height*(1-0.618)+10);
        game.ctx.restore();
    };
    Background.prototype.update = function () {
        this.speed--;
        if(-this.speed === this.image.width){
            this.speed = 0;
        }
    }
})();

Bird.js

(function () {
    var Bird = window.Bird = function () {
        this.image = game.imgArr["Bird"];
        this.x = game.canvas.width*(1-0.618);
        this.y = game.canvas.height*0.618*(1-0.618);
        //小鸟旋转的角度
        this.deg = 0;
        //小鸟是否在飞
        this.isFly = false;
        //帧数
        this.fno = 0;
        //鸟的颜色
        this.birdColor = 24*parseInt(Math.random()*3);
        //鸟扇动翅膀的图片的编号
        this.wingNo = 0;
        //下面的四个变量用于碰撞检测,代表鸟的上下左右的最小矩形框
        this.L = this.R = this.T = this.B = 0;
        //该变量用于保存鸟是否为死亡状态
        this.isDie = false;
    };
    Bird.prototype.render = function () {
        game.ctx.save();
        game.ctx.translate(this.x,this.y);
        game.ctx.rotate(this.deg);
        game.ctx.beginPath();
        game.ctx.drawImage(this.image,34*this.wingNo,this.birdColor,34,24,-17,-12,34,24);
        game.ctx.restore();
        // game.ctx.fillRect(this.L,this.T,34,24);

    };
    Bird.prototype.update = function () {
        this.L = this.x-17;
        this.R = this.x + 17;
        this.T = this.y-12;
        this.B = this.y + 12;

        //判断鸟是否飞出天空,真则不让飞出,
        if(this.T < 0){
            //这里不能使用this.T = 0;因为
            //game.ctx.drawImage(this.image,34*this.wingNo,this.birdColor,34,24,-17,-12,34,24);
            //game.ctx.translate(this.x,this.y);这里是根据x,y来绘制鸟的,固定T鸟还是飞出屏幕
            this.y = 12;
        }
        if(this.isFly){
            //这里为扇动翅膀动画
            if(this.fno%2 === 0){
                this.wingNo++;
            }
            if(this.wingNo > 2){
                this.wingNo = 0;
            }
            //当有点击事件时,鸟儿向上飞一段距离
            this.y -= 0.4 * (20-this.fno);
            if(this.fno === 20){
                this.fno = 0;
                this.isFly = false;
            }
            this.deg -= (Math.PI/180)*6;
            if(this.deg < -(Math.PI/180)*45){
                this.deg = -(Math.PI/180)*45;
            }
        }else{
            this.y += 0.4 * this.fno;
            this.deg += (Math.PI/180)*3;
            if(this.deg > (Math.PI/180)*90){
                this.deg = (Math.PI/180)*90;
            }
        }
        this.fno++;
    };
    Bird.prototype.fly = function () {
        this.fno = 0;
        this.isFly = true;
    }
})();

Game.js

(function(){
	var Game = window.Game = function(){
		this.canvas = document.querySelector("canvas");
		this.canvas.width = document.documentElement.clientWidth;
		this.canvas.height = document.documentElement.clientHeight;
        this.init();
        this.ctx = this.canvas.getContext('2d');
        //这是图片字典
		this.imgArr = {};
		//这是管子数组
        this.pipeArr = [];
        //游戏分数:即通过的管子数
        this.score = 0;

        //记录过多少时间new一个管子
        this.time_count = 0;

        //这里也需要使用箭头函数,否则this指向window而不是window.Game
		this.loadResource(()=>{
		    this.startGame();
        });
	};

	//初始化画布宽高,适配移动端不同机型
	Game.prototype.init =  function(){
		if(this.canvas.width < 320){
			this.canvas.width = 320;
		}else if(this.canvas.width > 414){
			this.canvas.width = 414;
		}
		if(this.canvas.height < 568){
			this.canvas.height = 568;
		}else if(this.canvas.height > 823){
			this.canvas.height = 823;
		}
	};

	//加载资源
	Game.prototype.loadResource = function(callback){
		//定义已加载的资源数
		var resource_count = 0;
		var xhr = new XMLHttpRequest();
		//这里需使用箭头函数,否则将出现this指向错误,
		xhr.onreadystatechange = ()=>{
		    //此时的this指向window.Game
			if(xhr.readyState === 4 && xhr.status === 200){
				var obj = JSON.parse(xhr.responseText);
				for(let i=0;i<obj[0]["imgs"].length;i++){
                    this.imgArr[obj[0]["imgs"][i].name] = new Image();
                    this.imgArr[obj[0]["imgs"][i].name].src = obj[0]["imgs"][i].url;
                    this.imgArr[obj[0]["imgs"][i].name].onload = ()=> {
                        resource_count++;
                        this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height);
                        this.ctx.save();
                        let text = "正在加载资源"+ resource_count +"/"+obj[0]["imgs"].length;
                        this.ctx.textAlign = "center";
                        this.ctx.font = "20px 微软雅黑";
                        this.ctx.beginPath();
                        this.ctx.fillText(text,this.canvas.width/2,this.canvas.height*(1-0.618));
                        this.ctx.restore();
                        //当加载图片的数量等于图片资源的数量时,说明图片资源加载完毕,执行回调函数
                        if(resource_count === obj[0]["imgs"].length){
                            callback();
                        }
                    }
				}
			}
		};
		xhr.open('GET',"./resource.json");
		xhr.send();
	};

    //主线程:游戏开始
    Game.prototype.startGame = function () {
        //实例化场景管理器
        this.sm = new SceneManager();

        this.timer = setInterval(()=>{
            this.time_count += 20;

            this.sm.render();
            this.sm.update();
        },20)
    };
})();

Land.js

//大地类
(function () {
    var Land = window.Land = function () {
        this.image = game.imgArr["Land2"];
        this.speed = 0;
    };

    Land.prototype.render =  function () {
        game.ctx.drawImage(this.image,this.speed,game.canvas.height*0.618+99);
        game.ctx.drawImage(this.image,this.image.width+this.speed,game.canvas.height*0.618+99);
    };

    Land.prototype.update =  function () {
        //每次更新判断鸟是否着地,着地则结束游戏
        if(game.bird.B > game.canvas.height*0.618+80){
            game.bird.isDie = true;
        }
        //近处的物体速度快,故大地的速度比远处的树林和白云快
        this.speed-=2;

        if(-this.speed > 640){
            this.speed = 0;
        }
    }
})();

Pipe.js

(function () {
    var Pipe = window.Pipe = function () {
        this.Pipe1 = game.imgArr["Pipe1"];
        this.Pipe2 = game.imgArr["Pipe2"];
        this.height = game.canvas.height*0.618+99;
        //两个管子之间的空隙高度
        this.interspace = 150;
        //上面的管子随机高度
        this.randomHeight = 140 + parseInt(Math.random()*180);
        //下面的管子的高度等于this.height-this.randomHeight-this.interspace
        this.Pipe2_height = this.height-this.randomHeight-this.interspace;
        this.speed = 0;
        //该变量用于碰撞检测
        this.L = 0;
        //该变量用于判断鸟是否已经通过管子,防止重复加分
        this.alreadyPass = false;
        //将自己推入数组
        game.pipeArr.push(this);
    };

    Pipe.prototype.render = function () {
        this.L = game.canvas.width+10+this.speed;
        //当下面的管子高度小于40时,重新取值
        while (this.Pipe2_height < 40){
            this.randomHeight = 140 + parseInt(Math.random()*180);
            this.Pipe2_height = this.height-this.randomHeight-this.interspace;
        }
        game.ctx.drawImage(this.Pipe1, 0, this.Pipe1.height-this.randomHeight, 52, this.randomHeight, this.L, 0, 52, this.randomHeight);
        game.ctx.drawImage(this.Pipe2, 0, 0, 52, this.Pipe2_height, this.L,  this.randomHeight+this.interspace, 52, this.Pipe2_height);
    };

    Pipe.prototype.update = function () {

        //每次更新判断管子是否碰到鸟clearInterval(game.timer)
        if(this.L < game.bird.R && this.L+52 > game.bird.L){
            if(this.randomHeight > game.bird.T || this.randomHeight + this.interspace < game.bird.B){
                game.bird.isDie = true;
            }
        }
        this.speed -= 2;
        //判断鸟是否通过管子,真则加分
        if(game.bird.L > game.canvas.width+this.speed+52 && !this.alreadyPass){
            game.score++;
            this.alreadyPass = true;
        }

        //当管子移出画布时,将其从数组中移除
        if(-this.speed > game.canvas.width + 60){
            game.pipeArr.shift();
            this.speed = 0;
        }
    }
})();

SceneManager.js

(function () {
    var sm = window.SceneManager = function () {
        //sm的场景编号
        this.sceneNo = 1;
        //实例化背景
        game.bg = new Background();
        //实例化大地
        game.land = new Land();
        //实例化鸟
        game.bird = new Bird();
        //Flash和Logo图片的Y值
        this.FlashY = 0;
        this.LogoY = game.canvas.height;
        this.bindEvent();
        //这两个参数用于控制爆炸效果
        this.i = 0;
        this.j = 0;
        //这个参数用于控制鸟死亡下落的重力
        this.g = 1;
    };
    sm.prototype.update = function () {
        switch (this.sceneNo) {
            case 1:
                this.FlashY += 4;
                this.LogoY -= 4;
                if(this.FlashY > game.canvas.height * (1-0.618)){
                    this.FlashY = game.canvas.height * (1-0.618);
                }
                if(this.LogoY < game.canvas.height * (1-0.618)+100){
                    this.LogoY = game.canvas.height * (1-0.618)+100
                }
                break;
            case 2:
                //判断鸟是否死亡,真则切换到场景3
                if(game.bird.isDie){
                    this.enter(3)
                }
                break;
            case 3:
                this.g ++;
                if(game.bird.y < game.canvas.height*0.618+99){
                    game.bird.y += 0.4 * this.g;
                }else{
                    this.enter(4);
                }
                break;
            case 4:
                if(game.time_count%100 === 0){
                    this.i++;
                    if(this.i > 3 && this.j === 0){
                        this.i = 0;
                        this.j = 1;
                    }else if(this.i > 3 && this.j === 1){
                        this.i = 3;
                        this.j = 1;
                        this.enter(5);
                    }
                }
                break;
            case 5:
                this.FlashY += 4;
                if(this.FlashY > game.canvas.height * (1-0.618)){
                    this.FlashY = game.canvas.height * (1-0.618);
                }
                break;
        }
    };
    sm.prototype.render = function () {
        //清屏
        game.ctx.clearRect(0,0,game.canvas.width,game.canvas.height);
        switch (this.sceneNo) {
            case 1:
                //场景1:游戏开始界面
                game.bg.render();
                game.land.render();
                game.bg.update();
                game.land.update();
                game.ctx.drawImage(game.imgArr["Flash"],0,0,204,69,game.canvas.width / 2 - 102,this.FlashY,204,69);
                game.ctx.drawImage(game.imgArr["Logo"],game.canvas.width / 2 - 160,this.LogoY);
                break;
            case 2:
                //场景2:游戏开始
                //每隔两秒new一个管子
                if(game.time_count > 2000){
                    game.time_count = 0;
                    new Pipe();
                }

                game.bg.render();
                game.land.render();
                game.bg.update();
                game.land.update();

                //遍历管子数组,更新并渲染
                for (let i=0;i<game.pipeArr.length;i++){
                    game.pipeArr[i].update();
                    game.pipeArr[i].render();
                }
                game.bird.update();
                game.bird.render();

                //绘制游戏分数
                game.ctx.save();
                game.ctx.font = "26px 微软雅黑";
                game.ctx.textBaseline = "top";
                game.ctx.beginPath();
                game.ctx.fillText("分数:",0,15);
                game.ctx.restore();
                var number = game.score.toString();
                for(let i=0; i<number.length; i++){
                    game.ctx.drawImage(game.imgArr["Number"],28*parseInt(number[i]),0,28,36,28*i+75,10,28,36);
                }
                break;
            case 3:
                //场景3:小鸟撞地死亡
                //每隔两秒new一个管子
                if(game.time_count === 2000){
                    game.time_count = 0;
                    new Pipe();
                }

                game.bg.render();
                game.land.render();

                //遍历管子数组,更新并渲染
                for (let i=0;i<game.pipeArr.length;i++){
                    game.pipeArr[i].render();
                }
                game.bird.render();

                //绘制游戏分数
                game.ctx.save();
                game.ctx.font = "26px 微软雅黑";
                game.ctx.textBaseline = "top";
                game.ctx.beginPath();
                game.ctx.fillText("分数:",0,15);
                game.ctx.restore();
                var number = game.score.toString();
                for(let i=0; i<number.length; i++){
                    game.ctx.drawImage(game.imgArr["Number"],28*parseInt(number[i]),0,28,36,28*i+75,10,28,36);
                }
                break;
            case 4:
                //游戏结束:鸟撞管子死亡动画
                //每隔两秒new一个管子
                if(game.time_count === 2000){
                    game.time_count = 0;
                    new Pipe();
                }

                game.bg.render();
                game.land.render();

                //遍历管子数组,更新并渲染
                for (let i=0;i<game.pipeArr.length;i++){
                    game.pipeArr[i].render();
                }

                //绘制游戏分数
                game.ctx.save();
                game.ctx.font = "26px 微软雅黑";
                game.ctx.textBaseline = "top";
                game.ctx.beginPath();
                game.ctx.fillText("分数:",0,15);
                game.ctx.restore();
                var number = game.score.toString();
                for(let i=0; i<number.length; i++){
                    game.ctx.drawImage(game.imgArr["Number"],28*parseInt(number[i]),0,28,36,28*i+75,10,28,36);
                }
                //当鸟的y值大于地面的高度,则产生爆炸效果
                if(game.bird.y > game.canvas.height*0.618+99){
                    game.ctx.drawImage(game.imgArr["Bomb"],128*this.i,128*this.j,128,128,game.bird.x-34,game.bird.y-34,34*2,34*2);
                }
                break;
            case 5:
                //游戏结束:GameOver
                if(game.time_count === 2000){
                    game.time_count = 0;
                    new Pipe();
                }

                game.bg.render();
                game.land.render();

                //遍历管子数组,更新并渲染
                for (let i=0;i<game.pipeArr.length;i++){
                    game.pipeArr[i].render();
                }

                //绘制游戏分数
                game.ctx.save();
                game.ctx.font = "26px 微软雅黑";
                game.ctx.textBaseline = "top";
                game.ctx.beginPath();
                game.ctx.fillText("分数:",0,15);
                game.ctx.restore();
                var number = game.score.toString();
                for(let i=0; i<number.length; i++){
                    game.ctx.drawImage(game.imgArr["Number"],28*parseInt(number[i]),0,28,36,28*i+75,10,28,36);
                }
                //GameOver图标往下移动到屏幕中间
                game.ctx.drawImage(game.imgArr["Flash"],204,0,204,69,game.canvas.width / 2 - 102,this.FlashY,204,69);

        }
    };
    //进入某个场景时需要做的操作
    sm.prototype.enter = function (sceneNo) {
        switch (sceneNo) {
            case 1:
                this.sceneNo = 1;
                this.FlashY = 0;
                this.LogoY = game.canvas.height;
                game.bird = new Bird();
                //实例化背景
                game.bg = new Background();
                //实例化大地
                game.land = new Land();
                game.pipeArr = [];
                game.time_count = 0;
                game.score = 0;
                //这两个参数用于控制爆炸效果
                this.i = 0;
                this.j = 0;
                //这个参数用于控制鸟死亡下落的重力
                this.g = 1;
                break;
            case 2:
                this.sceneNo = 2;
                //sm的场景编号
                break;
            case 3:
                this.sceneNo = 3;
                break;
            case 4:
                this.sceneNo = 4;
                break;
            case 5:
                this.sceneNo = 5;
                this.FlashY = 0;
                break;
        }
    };
    //
    sm.prototype.bindEvent = function () {
        //当点击canvas时,通过判断在哪个场景的哪个位置来做出相应的事件
        game.canvas.onclick =  () => {
            switch (this.sceneNo) {
                case 1:
                    game.bird.isDie = false;
                    game.bird.x = game.canvas.width*(1-0.618);
                    game.bird.y = game.canvas.height*0.618*(1-0.618);
                    this.enter(2);
                    break;
                case 2:
                    game.bird.fly();
                    break;
                case 3:
                    break;
                case 4:
                    break;
                case 5:
                    this.enter(1);
                    break;
            }
        }
    }
})();

 

  • 0
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值