前端实战小案例--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;
}
}
}
})();