谷歌小恐龙之对打版(一)—— 准备

改造离线小恐龙github源码地址

设计

制作一个仿照Google小恐龙的小游戏,对那种对战版的,同时为了增加一些趣味性,加了一点恶趣味的东西自己去发现吧

资源准备工作

准备一个canvas元素

<div id="game"><canvas id="canvas" width="800" height="250"></canvas>
</div>

再写一些简单的样式,居中、背景色啥的,不多解释,看起来好看就OK;

body {position: relative;
}
canvas {position: absolute;left: 50%; top: 50%;transform: translate(-50%, -0%);background-color: #eee;
}

先制作一些游戏所需要的素材资源,这里只用到了图片资源,一张Google小恐龙的原味精灵图,其他的都是艺术字制作网站做的png格式的图片,用来做游戏Logo、提示文字、按钮啥的
准备工作就这么多了,如果你对canvas不是很了解,那就往下看了解了解就ok了;

canvas基本知识

canvas的基础知识请看廖雪峰老师的这篇关于博客,这里就不重复了,毕竟是很简单的东西,但是还有一个非常重要的知识要在这里补充,大家在了解完canvas的基础后,都知道,canvas有一个坐标系统,利用坐标系统我们可以绘制一些形状,但是不仅仅可以绘制简单形状,还可以绘制图片,如下:

// 获取canvas对象
var canvas = document.getElementById('canvas');
// 获取2d的画笔
var ctx = canvas.getContext('2d');
// 绘制图形
ctx.drawImage(SourceImage, //第一个参数:图片对象sx, sy, //第2、3个参数:矩形区域的顶点坐标sw, sh, //第4、5个参数:矩形区域的宽高dx, dy, //第6、7个参数:矩形区域位于画布的顶点坐标dw, dh //第8、9个参数:矩形区域位于画布中的宽高
);

下图的参数和上面代码对应,最终我们就把SourceImage中的指定绿色区域按照我们设置的宽高绘制在Canvas上了,因此,我们可以将图片扩大或者缩放,但是这样做会使绘制出来的图像有一定程度的模糊;我们就利用这一点,将精灵图中的一个个元素绘制在画布上,加上一些动作,就变成了游戏;

画布小游戏基本知识

按照上面的代码,我们只是将禁止的图片绘制在画布上,如何将绘制的图形动起来了,ok,那就是定时器了,先定义一个定时器

var timer = setInterval(function () {// To do Something
}, 30);

这里做一个小球跟随鼠标滚动的动画,思路就是,在定时器没执行一次,我们就获取一次鼠标的位置,同时把小球的位置更新到鼠标的位置

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var ball = {x: 0, // x坐标y: 0, // y坐标r: 10, // 圆半径draw: function () {// 在更新小球的位置之前,将上一帧绘制的画布清除ctx.clearRect(0, 0, 500, 300);// 开始一个画笔ctx.beginPath();// 绘制一个圆ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);ctx.stroke();}
}
// 监听鼠标在canvas上的移动事件,并把鼠标的坐标赋给小球
canvas.onmousemove = function (e) {ball.x = e.offsetX;ball.y = e.offsetY;
}
// 在定时器中执行
var timer = setInterval(function () {ball.draw();
}, 30);

简单吧,这样,一个简单的带有交互的游戏就OK了,总结一下,先创建一个画布,然后将一个物体绘制在画布上,随着定时器的执行,我们就将上一次绘制的东西清除,然后重新绘制,这样一帧一帧的图像连起来就成了动画,然后加上用户交互,加上规则就成了小游戏,以后我们将开始逐渐的扩展我们上边所完成的这段简短的代码,使其更像一个小游戏;

谷歌小恐龙之对打版(二)—— 起步

上篇中,我用定时器来执行绘制任务,现在我们换一个性能更高的’定时器’——requestAnimationFrame,大多数电脑显示器的刷新频率是60Hz,大概相当于每秒钟重绘60次。

大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过那个频率用户体验也不会有提升。因此,最平滑动画的最佳循环间隔是1000ms/60,约等于16.6ms,而setTimeout和setInterval的问题是,它们都不精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。

如果队列前面已经加入了其他任务,那动画代码就要等前面的任务完成后再执行,,requestAnimationFrame采用系统时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果;

像setTimeout、setInterval一样,requestAnimationFrame是一个全局函数。调用requestAnimationFrame后,它会要求浏览器根据自己的频率进行一次重绘,它接收一个回调函数作为参数,在即将开始的浏览器重绘时,会调用这个函数,并会给这个函数传入调用回调函数时的时间作为参数。由于requestAnimationFrame的功效只是一次性的,所以若想达到动画效果,则必须连续不断的调用requestAnimationFrame,就像我们使用setTimeout来实现动画所做的那样。

来看看基本用法

// 这样就完成了和定时器一样的功能,实现了多次调用
function rander(){// To do Something// return stop the requestAnimationFramerequestAnimationFrame(rander);
}
requestAnimationFrame(function () {rander();
});

我们做一下简单的封装来解决一下浏览器的兼容性

window.requestAnimFrame = (function () {return window.requestAnimationFrame ||window.webkitRequestAnimationFrame ||window.mozRequestAnimationFrame ||window.oRequestAnimationFrame ||window.msRequestAnimationFrame ||/*** 降级处理,用setTimeout实现* 大约16ms 两帧间隔时间*/function (callback, element) {return window.setTimeout(callback, 1000 / 60);};
})();

ok,定时器的优化封装做好了,然后我们写一些常用的工具函数,这些方法也可以不急着定义,等到开发过程中根据需求来定义

// 指定范围的随机整数
function randomNumBoth(min, max) {var range = max - min;var rand = Math.random();var num = min + Math.round(rand * range); //四舍五入return num;
}// 取绝对值
function absolute(x) {return x >= 0 ? x : -x;
}// 获取两点之间的距离
function getS(pos1, pos2) {return Math.sqrt(Math.pow(pos1.x - pos2.x, 2) +  Math.pow(pos1.y - pos2.y, 2));
}// 判断是否为偶数
function isOdd(num) {if ((num % 2) == 1) {return false;}return true;
}

我们先缓存我们用到的素材资源

// 游戏的画布
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');// 以下为加载游戏使用到的图片
var gameImage = new Image();
gameImage.src = './lib/game.png';var logo = new Image();
logo.src = './lib/logo.png';var over = new Image();
over.src = './lib/over.png';var kaishi = new Image();
kaishi.src = './lib/start.png';var win = new Image();
win.src = './lib/win.png';var suc = new Image();
suc.src = './lib/success.png';

我们还得配置一些全局的配置,比如游戏背景的宽度,高度之类的

var gameConfig = {bg: {WIDTH: 800,HEIGHT: 250}
}

用我们之前封装的requestAnimFrame方法来绘制我们的画布,这样我们就制作了一个游戏的核心控制器,每执行一次都更新一帧,我们之后的代码也只需要关心当前帧和下一帧就ok了

(function draw() {// 在下一帧绘制之前我们先清除上一帧的画布ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);// 跑一下,就可以看到大约每过60ms就会打印一个'下一帧'console.log('下一帧');window.requestAnimFrame(draw);
})();

到这里,游戏的起步工作就差不多了,下篇开始我们就要绘制一些对象在画布上了,好激动啊,终于要画东西了。

谷歌小恐龙之对打版(三)—— 地板、路障

绘制地板,在Google小恐龙的游戏中,小恐龙是前行的,用固定小恐龙来使地板后退的方式,使小恐龙在视觉上前进,但是我们这个场景是固定的,小恐龙的实际运动就是自己在动,所以地板就是一个固定的背景,所以也是整个代码中最简单的一块。

二话不说,先定义一个Floor类

/*** 地板* canvas 画布*/
function Floor(canvas) {this.canvas = canvas;this.ctx = canvas.getContext("2d");// 这个方法用来初始化一些参数,在其原型上定义,往下看就明白了this.init();
}

然后我们要绘制地板,需要一些地板的参数,比如他的长宽之类的

// 配置是我用PS一个像素一个像素量出来的
Floor.config = {WIDTH: 800,     // 宽600像素HEIGHT: 12,     // 高12像素YPOS: 227,      // 在Y轴的位置IMG: {y: 54},   // 地面在画布上的位置RANDOMPOS: {min: 2, max: 402} // 地板随机位置
}

我们现在定义Floor的方法,貌似没有啥方法,一个地板能有啥方法

Floor.prototype = {// 在canvas上绘制地板draw: function () {/*** 这里就是canvas的绘制图像的基本用法* 将精灵图中的地板取出来,然后画在画布上*/ this.ctx.drawImage(gameImage,Floor.config.IMG.x, Floor.config.IMG.y, Floor.config.WIDTH, Floor.config.HEIGHT,0, Floor.config.YPOS, Floor.config.WIDTH, Floor.config.HEIGHT)},// 初始化一些参数init: function () {/*** 因为每次加载都想要不同的地板* 所以,就把地板的位置随机一下,就从精灵图中截取出不同位置的地板* 给人的感觉就是不一样的地板了*/ Floor.config.IMG.x = randomNumBoth(Floor.config.RANDOMPOS.min, Floor.config.RANDOMPOS.max);this.draw();}
}

Floor我们就定义完了,然后要做的就是创建实例,调用其draw方法就可以把地板绘制在画布上了,因为我们每帧都会会清除整个画布,所以,我们必须每帧都绘制地板,否则当第二帧开始,地板就被清除了,所以这样

// 实例化一个floor对象,执行构造函数的时候就会调用floor.init方法,将地板绘制在画布上作为第一帧
var floor = new Floor(canvas);
(function draw() {// 在下一帧绘制之前我们先清除上一帧的画布ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);// 跑一下,就可以看到大约每过60ms就会打印一个'下一帧'// 这里调用draw方法,每次清除画布之后就会在画一个新的地板上去floor.draw();console.log('下一帧');window.requestAnimFrame(draw);
})();

当然,你也可以另外创建一个canvas,将一些不动的背景画在上面,这样性能会有所提升;
地板就画好了,还有一个就是固定不变的路障,这个路障虽然也是固定的,但是也和上边的地板一样,我们想随机的绘制不同的路障,使得我们的游戏更加丰富,上代码,解释都在代码里

/*** 分割线的仙人掌 * canvas 画布*/
function Cacti(canvas) {this.canvas = canvas;this.ctx = canvas.getContext("2d");this.init();
}Cacti.config = {SPLIT_POS: gameConfig.bg.WIDTH,            // 路障的位置,总宽的一半TYPES_RATIO: 0.5                           // 这个是出现不同路障的比例
}
/**** 路障的配置,这里是个数组* 因为我们想随机从这两个路障中挑选一个出来* 作为游戏中间的路障*/
Cacti.types = [{WIDTH: 50,             // 宽12HEIGHT: 35,            // 高35像素XPOS: 400,             // 在x轴的位置YPOS: 205,             // 在Y轴的位置IMG: {x: 228, y: 3},   // 在图上的位置},{WIDTH: 50,             // 宽12HEIGHT: 50,            // 高35像素XPOS: 400,             // 在x轴的位置YPOS: 190,             // 在Y轴的位置IMG: {x: 430, y: 3},   // 地面在图上的位置}
]Cacti.prototype = {// 在canvas上绘制仙人掌draw: function() {this.ctx.drawImage(gameImage,Cacti.config.CACTI_TYPE.IMG.x, Cacti.config.CACTI_TYPE.IMG.y,Cacti.config.CACTI_TYPE.WIDTH, Cacti.config.CACTI_TYPE.HEIGHT,Cacti.config.CACTI_TYPE.XPOS - (Cacti.config.CACTI_TYPE.WIDTH / 2), Cacti.config.CACTI_TYPE.YPOS,Cacti.config.CACTI_TYPE.WIDTH, Cacti.config.CACTI_TYPE.HEIGHT)},// 获得随机类型的仙人掌init: function() {/*** 随机获取一个路障的类型,并作为路障的静态属性* 绘制的时候就从Cacti.config.CACTI_TYPE中获取参数*/ Cacti.config.CACTI_TYPE = Math.random() > Cacti.config.TYPES_RATIO ? Cacti.types[0] : Cacti.types[1];this.draw();}
}

最后实例化这个路障,在动画帧中绘制我们的路障,因为实例化的时候会调用init方法,同时获取一个路障的类型,所以在帧动画中不断的调用draw方法是不会随即改变路障类型的

var floor = new Floor(canvas);
// 实例化的时候已经确定的路障类型
var cacti = new Cacti(canvas);
(function draw() {// 在下一帧绘制之前我们先清除上一帧的画布ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);// 跑一下,就可以看到大约每过60ms就会打印一个'下一帧'// 这里调用draw方法,每次清除画布之后就会在画一个新的地板上去floor.draw();// 绘制路障, 这里重复绘制也不会改变路障的类型了cacti.draw();console.log('下一帧');window.requestAnimFrame(draw);
})();

ok,我们已经完成了两个静态的物体的绘制,下篇,我们画一个动态的东西,云朵,原理着静态的物体一样,只不过每次调用draw方法之前都会执行一些操作,使的云的位置不断的改变;

谷歌小恐龙之对打版(四)—— 云朵

为了丰富我们的游戏内容,我们要绘制一些背景使其不单调,上片我们画了一些地板和路障,这次,我们绘制一些在天空中飘动的云朵,云朵的数量和位置都在指定的范围内随机生成,来增加自然度,又特么随机

原理带大部分的代码逻辑和先前的一样,先创建云朵类并配置一些云朵的基本参数

/*** 云朵* canvas 画布*/
function Cloud(canvas) {this.canvas = canvas;this.ctx = canvas.getContext("2d");this.init();
}// 云朵的配置
Cloud.config = {WIDTH: 46,           // 云朵的宽度HEIGHT: 14,          // 云朵的高度IMG: {x: 88, y: 3},  // 云朵在图中的位置MAX_BASE_HEIGHT: 30, // 云朵离地面的最大高度MIN_BASE_HEIGHT: 71, // 云朵离地面的最小高度MAX_GAP: 300,        // 云朵之间的最大间隔MIN_GAP: 100,        // 云朵之间的最小间隔RANDOMSPEED: {min: 1, max: 2}, // 云朵飘动的随机速度范围
}

但是和之前不同的是,地面和路障都只有一个,我们的云朵却不止一朵,所以我们创建一个云朵类的静态属性来存储实例化好的云朵

Cloud.clouds = [];

ok,就这么简单,接下来就要定义方法了,但是和先前不一样的是,我希望当我实例化云朵的时候,得到的漫天的所有云朵实例的集合,而不是一朵,我并不想在动画帧的控制器中做太多业务,来使代码看起来很丑,所以我们要一个creat方法来创建单个云朵的实例,并存储在Cloud.clouds中,当画面更新调用Cloud实例的draw方法时,遍历这个由单个云朵组成的集合,然后再调用相应的方法来绘制云朵,上代码

Cloud.prototype = {// 在canvas上绘制云朵draw: function() {this.ctx.drawImage(gameImage,Cloud.config.IMG.x, Cloud.config.IMG.y,Cloud.config.WIDTH, Cloud.config.HEIGHT,this.xPos, this.yPos,Cloud.config.WIDTH, Cloud.config.HEIGHT)},// 实例化云朵时的初始参数init: function() {// 初始化的时候云朵的位置this.xPos = gameConfig.bg.WIDTH + Cloud.config.WIDTH;this.yPos = randomNumBoth(Cloud.config.MAX_BASE_HEIGHT, Cloud.config.MIN_BASE_HEIGHT);// 随机生成一个云朵和云朵之间的间隔宽度this.cloudGap = randomNumBoth(Cloud.config.MIN_GAP,Cloud.config.MAX_GAP)// 随机生成云朵的速度this.spead = randomNumBoth(Cloud.config.RANDOMSPEED.min, Cloud.config.RANDOMSPEED.max);},// 更新canvas上云朵的位置update: function() {// 当没有云朵的时候,绘制一个云朵if(Cloud.clouds.length == 0) {return this.creat();}// 当云朵移出屏幕外的时候,删掉这个云朵if(Cloud.clouds[0].xPos < -Cloud.config.WIDTH) {Cloud.clouds.shift();}var len = Cloud.clouds.length;/*** 当最后一个云朵大于其云朵间距的时候,生成一个云朵* 没有云朵在inti的时候都会初始化一个云朵间距* 当这个云朵距离右边界的位置的时候,就生成一个新的云朵* 这个间距是在一个范围内随机生成的,所以看起来就自然一些*/ if(gameConfig.bg.WIDTH - Cloud.clouds[len-1].xPos > Cloud.clouds[len-1].cloudGap) {this.creat();}// 循环这些云朵数组,绘制他们的自己for(var i = 0; i < len; i ++) {// 这里使每个云朵都在下一帧移动一定的距离Cloud.clouds[i].xPos = Cloud.clouds[i].xPos - Cloud.clouds[i].spead;// 调用云朵的绘制方法,画在画布上Cloud.clouds[i].draw();}},// 存储云朵的数组中添加一个云朵creat: function() {Cloud.clouds.push(new Cloud(this.canvas));}
}

这样我们就完成了云朵的绘制,老样子,在动画帧中绘制这些云朵,但是我们这次调用的是update方法,因为在update中我们调用了云朵集合中每个云朵的draw方法,本来命名是和先前一样的,后来为了大伙区分不同,就改叫update,之后静态的物体我们就用draw,动态的我们就用update

var floor = new Floor(canvas);
var cacti = new Cacti(canvas);// 创建漫天云朵
var cloud = new Cloud(canvas);
(function draw() {// 在下一帧绘制之前我们先清除上一帧的画布ctx.clearRect(0, 0, gameConfig.bg.WIDTH,  gameConfig.bg.HEIGHT);floor.draw();cacti.draw();// 调用云朵的update方法cloud.update();console.log('下一帧');window.requestAnimFrame(draw);
})();

ok,云朵的绘制就成功了,跑一下代码,就会发现还不错,总结一下,动态的物体和静态物体的区别就是,动态的物体在每帧动画执行的时候,根据一些规则,更新一下自己的位置,在下一帧的时候,位置就会发生变动;下篇,我们来做一些有意思的东西;

谷歌小恐龙之对打版(五)—— AABB盒模型

因为游戏实在太简单了,没有必要用到牛闪闪的物理引擎,只做一个简单的碰撞检测就OK了,算法不过关,可能会导致计算量比较大,但是我电脑配置高啊,以及后续的起跳和下落也一样,做一些简单的实现就ok了
先了解一下盒模型的概念,如下图所示,AABB盒的概念总结一下,就是用一个正方形来表示物体的边界,与其他两个不同的是AABB盒的正方形始终都是正的,这是这种模型的检测碰撞时计算量不是很大,但是范围不是很准确
当然有很多游戏都采用这种模型,我们小时候玩的红白机,大部分都采用这种碰撞检测,但是,他们会用很多AABB盒来提高边界的检测精度,先计算外圈红色盒的碰撞,当另一物体与其发生碰撞后再去检测内部绿色的碰撞,这样的方法减少计算量。
我这次只用了简单的两个碰撞盒来描述两个物体,看着下图思考一下,两个盒子如何碰撞

// X1 : AAX, X2 : AAX + AA.width, 以此类推
// 不过网上找的这个图和我们即将写的不太一样,坐标原点在左上角
AAX < BBX + BB.width && AAX + AA.width > BBX && AAY < BBY + BB.height && AA.height + AAY > BBY

大致了解之后就能开始写代码了

// AABB盒模型
function AABB(x, y, w, h, type) {// 盒的x坐标this.x = x;// 盒的y坐标this.y = y;// 盒的宽度this.width = w;// 盒的高度this.height = h;// 盒的类型,这里是为了不是让相同的盒做碰撞检测,如果类型相同,直接returnthis.type = type;
}
// 这个set也可以不要,直接通过.x、.y来修改
AABB.prototype = {setXY: function(x, y) {this.x = x;this.y = y;}
}

盒的大概就是这样了,但是还需要一个重要的方法来检测碰撞

// 我们把这个当作AABB的静态方法
AABB.boxCompare = function(AA, BB) {// AA、BB就是我们的AABB的实例// 这里检测AA和BB的类型,毕竟我们不希望相同类型的盒做碰撞检测if(AA.type == BB.type) {return false;}// 这里就是检测碰撞的关键returnAA.x < BB.x + BB.width &&AA.x + AA.width > BB.x &&AA.y < BB.y + BB.height &&AA.height + AA.y > BB.y;
}

当然这就简单的实现了碰撞检测,我们还可以封装一些其他的方法来完善一些代码,比如获取所有注册盒的类型之类的方法,来方便我们写代码,这一篇就到这里了,比较轻松有意思的一节。

谷歌小恐龙之对打版(六)—— Boss

这篇来完成我们的boss,因为boss没有用户操作,所以想对比较简单,就先写这个简单一点的。先来分析一下,boss需要发射子弹和向指定区域的随机位置移动两种功能,boss要在天上飞就要呼扇翅膀,呼扇翅膀就要进行图片的切换来做成,就是下面两幅图的切换没有一种呼扇翅膀的感觉,最后就是boss要能被子弹打中,因此,boss和子弹需要碰撞盒
上代码

/*** Boss* canvas 画布*/
// 还是一样的套路
function Boss(canvas) {this.canvas = canvas;this.ctx = canvas.getContext("2d");this.init();
}// 这里分别代表Boss翅膀向上和向下的两种状态
Boss.type = [{ WIDTH: 48,     // 宽600HEIGHT: 29,     // 高12像素POS: [410, 735, 10, 190],      // boss的活动范围IMG: {x: 180, y: 3},    // Boss在图上的位置SPEED: 1.5,MOUSEHEIGHT: {x: 0, y: 17},HP: 10, // boss的血量FLY_TIME: 30},{WIDTH: 48,     // 宽600HEIGHT: 39,     // 高12像素POS: [410, 735, 10, 190],      // boss的活动范围IMG: {x: 134, y: 3},    // Boss在图上的位置SPEED: 1.5,MOUSEHEIGHT: {x: 0, y: 17},HP: 10, // boss的血量FLY_TIME: 30}
]

接下来就是boss所具有的方法,首先就是通用的那些,init,draw,update,但是update有点不同的是,就是要接受一个时间的参数,为什么要这样做呢,因为我们要一个时间来制作boss呼扇翅膀的动画,我们就根据这个时间来绘制动画,比如每隔300ms就"扑棱"一哈,转换成代码就是这个时间是300ms奇数倍数就展示第一张图,偶数倍数就展示第二张图,往后的动画都采用这个方法来做;还有就是随机位置,思路就是在指定的区域内随机生成一个点,然后计算当前位置距离这个位置的距离,转化成x轴和y轴的分量,让每帧都移动一段距离就OK了,至于shoot方法,我们等子弹定义好之后在补充

Boss.prototype = {// 在canvas上绘制Bossdraw: function() {this.ctx.drawImage(gameImage,Boss.config.IMG.x, Boss.config.IMG.y,Boss.config.WIDTH, Boss.config.HEIGHT,this.xPos, this.yPos,Boss.config.WIDTH, Boss.config.HEIGHT);// 我们将boss的血量绘制在boss头顶上this.ctx.font = "13px Verdana";this.ctx.fillText('HP: ' + this.hp, this.xPos, this.yPos - 10);},// 初始化bossinit: function() {// boss的type,用作bossAABB盒的typethis.type = 0;// boss呼扇翅膀的类型Boss.config = Boss.type[0]// 这个记录的是时间,主要用来做呼扇翅膀的动画this.timer = 0;// boss的血量this.hp = Boss.config.HP;// 在y轴上的位置this.yPos = randomNumBoth(Boss.config.POS[2], Boss.config.POS[3]);// 在x轴上的位置this.xPos = randomNumBoth(Boss.config.POS[0], Boss.config.POS[1]);// 这个就是boss的盒this.box = new AABB(this.xPos, this.yPos, Boss.config.WIDTH, Boss.config.HEIGHT, this.type);// 生成下一个位置,在调用update方法时就像这边移动this.nextPos();// 调用draw方法,来吧boss绘制在canvas上this.draw();},update: function(time) {// 这里的time是在帧动画的控制器里传来的,为了就是记录这个时间来使boss呼扇翅膀this.timer = time;// 当这个不断增加的时间除以boss呼扇翅膀的时间间隔为基数就用翅膀向上的图,偶数就用翅膀向下的图if(isOdd(Math.floor(this.timer / Boss.config.FLY_TIME))) {Boss.config = Boss.type[0]}else {Boss.config = Boss.type[1]}// 每一帧boss的位置就增加一个对应的分量this.yPos += this.yIncrement;this.xPos += this.xIncrement;this.yPos = Math.ceil(this.yPos * 10) / 10;this.xPos = Math.ceil(this.xPos * 10) / 10;// 同时还得更新boss的盒this.box.setXY(this.xPos, this.yPos);// 当达到指定位置的时候,获取下个位置// 这里这么写是因为这些增量的分量是经过四舍五入的,即使不经过四舍五入,也是误差,毕竟// js的数字都是64位的,所以只要到达了这个误差范围内,我们就叫boss向下一个位置移动if(absolute(this.nextXPos - this.xPos) < 1 || absolute(this.nextYPos - this.yPos) < 1) {this.nextPos();}// 调用draw方法绘制this.draw();},// 这个是移动到下个位置的方法nextPos: function() {// 在boss的活动范围内随机生成下一个位置(x坐标和y坐标)this.nextXPos = randomNumBoth(Boss.config.POS[0], Boss.config.POS[1]);this.nextYPos = randomNumBoth(Boss.config.POS[2], Boss.config.POS[3]);// 计算移动到下个位置所用的时间var t = getS({x: this.nextXPos, y: this.nextYPos}, {x: this.xPos, y:this.yPos}) / Boss.config.SPEED;// 每 t 内 xPos 和 yPos 的增量// 这里计算的是每一帧boss要移动的距离在x轴和y轴的分量,取一下整数,方便计算this.xIncrement = Math.ceil((this.nextXPos - this.xPos) / t * 10) / 10;this.yIncrement = Math.ceil((this.nextYPos - this.yPos) / t * 10) / 10;this.shoot();},// 这个就是boss射击的方法,调用的子弹类的生成子弹的方法来生成子弹shoot: function() {// 子弹还没有定义,我们先注释掉// Bullet.creat(this.canvas, {x: Boss.config.MOUSEHEIGHT.x + this.xPos, y: Boss.config.MOUSEHEIGHT.y + this.yPos}, -1, 0);}
}

这一篇也没有什么难理解的地方,就是boss的随机移动,射击几个方法,稍微有一点复杂的地方就是boss的呼扇翅膀,下篇,开始写子弹。

谷歌小恐龙之对打版(七)—— 子弹

完成boss之后,boss需要发射子弹的功能,然后我们的小恐龙也需要发射子弹的功能,所以先把子弹撸完;子弹其实和云朵一个写法,只不过多了几个方法和碰撞盒,思路和云朵一样,将全屏幕的子弹都写到一个集合中,每次都去遍历集合中的子弹,子弹也有类型,来区分是boss的子弹还是我们小恐龙的子弹,其他的方法大家都快看吐了,init,draw,update

/*** 子弹的画布* canvas 画布* 子弹的构造函数和之前云朵啥的不一样,因为多了一些子弹的方向,子弹的* 类型和子弹的初始位置,因为子弹是跟着boss移动的*/
function Bullet(canvas, initPos, dir, type) {this.canvas = canvas;this.ctx = canvas.getContext("2d");this.b_xPos = initPos.x;this.b_yPos = initPos.y;// 子弹前进的方向,boss的子弹向左,小恐龙的子弹向右this.dir = dir;this.type = type;this.config = Bullet.types[this.type];this.box = new AABB(this.b_xPos, this.b_yPos, this.config.WIDTH, this.config.HEIGHT, this.type);
}// 这个就是子弹的类型了,我们想区分boss的子弹和小恐龙的子弹
// png图上并没有子弹这个东西,所以随便找了个黑色的像素将就了一下
// 如果觉得不够酷炫,可以去网上找一些冒蓝火的那种子弹画上来也是非常棒的
Bullet.types = [{WIDTH: 10,HEIGHT: 4,IMG: {x: 5, y: 5},SPEED: 5 },{WIDTH: 4,HEIGHT: 4,IMG: {x: 65, y: 7},SPEED: 5}
];

基本的一些参数配置完成后,就是子弹的方法了,还记得云朵吗,用一个方法生成云朵,存储在一个集合中,需要执行某些操作的时候,就遍历这个集合,那么子弹也一样,先创建一个集合来存这些子弹

Bullet.bullets = [];

然后就是子弹的方法,按照上面的思路来写

Bullet.prototype = {// 在canvas上绘制子弹draw: function() {this.ctx.drawImage(gameImage,this.config.IMG.x, this.config.IMG.y,this.config.WIDTH, this.config.HEIGHT,this.b_xPos, this.b_yPos, this.config.WIDTH, this.config.HEIGHT)},// 子弹需要移动,所以要给每个子弹加上move方法move: function() {this.b_xPos += this.config.SPEED * this.dir;}
}
// 这里就是创建子弹,然后存到集合中,并在子弹构造函数中生成AB盒
Bullet.creat = function(canvas, bPos, dir, type) {Bullet.bullets.push(new Bullet(canvas, bPos, dir, type));
}// 这里我写的很乱,按道理检测子弹是否打到boss或者小恐龙的方法不应该在这里
// 这个往后慢慢研究吧,我也第一次接触canvas的小游戏,还没有很好的思维方式
Bullet.update = function(trex, boss) {// 非空的判断var len = Bullet.bullets.length;if(len == 0) {return;}// 当子弹飞出屏幕外的时候,删除子弹,要不然的话通过时间的累积,子弹越来// 子弹越来越多,计算量就会越来越大if(Bullet.bullets[0].b_xPos > gameConfig.bg.WIDTH){Bullet.bullets.shift();}// 遍历这些集合for(var i = 0; i < Bullet.bullets.length; i ++) {// 移动Bullet.bullets[i].move();// 同时设置他的盒的位置Bullet.bullets[i].box.setXY(Bullet.bullets[i].b_xPos, Bullet.bullets[i].b_yPos);// 绘制Bullet.bullets[i].draw();// 检测子弹与boss和恐龙的碰撞,如果发生碰撞就把各自的血量减1,并删掉这个子弹if(Bullet.bullets[i] && AABB.boxCompare(trex.box, Bullet.bullets[i].box)) {trex.hp -= 1;Bullet.bullets.splice(i, 1);}if(Bullet.bullets[i] && AABB.boxCompare(boss.box, Bullet.bullets[i].box)) {boss.hp -= 1;Bullet.bullets.splice(i, 1);}}
}

子弹和云朵也没有什么差别,多了几个判断是否打到物体的方法,接下来就是小恐龙了,比较复杂一点的,写这个小恐龙花的时间最久。

谷歌小恐龙之对打版(八)—— 小恐龙

小恐龙放在最后,因为确实挺复杂的,花的时间也比较长,因为先前并没有接触过小游戏,事先设计的全是bug,通过不断的修修补补终于是完成了,虽然可以用,但是还是比较的乱,先把我的思路和代码亮出来,以后我会好好的设计一下,再者用一些打包工具,比如webpack之类的工具重新设计一份,学习打包工具的同时学习canvas,一举多得。

来整理一下思路,小恐龙应该具有哪些属性和方法,仔细想想,前进、后退、跳跃、射击这几个方法,然后是血量,最后就是跑动的动画和跳跃的动画,一个一个来分析。

前进后退是一组功能,这组功能中只能有一个在执行,或者都不执行,我们可以用一个数组来存放这些状态,每次都把用户的操作push进数组中,每次也都执行栈顶的操作,比如有一个数组

var state = [];
// 当用户按下前进的时候
state.push['GO'];
// 执行恐龙的前进方法
trex.GO();

大致的思路就是这样,当用户抬起按键时删除数组中对应的操作,这样满足用户按下一个案件的同时按下另一个按键的情况,同时满足用户按下很多按键之后按照不同顺序抬起按键的情况,因为我们只执行栈顶的操作,比如,当用户按下前进之后没有抬起按键,继续按下后退,这时我们就执行后退,然后松开后退,执行删除后退的操作,是的前进继续到达栈顶,然后执行前进的操作,松开前进按键,删除前进操作,小恐龙停止运动,流程图如下,灵魂画手,自行理解吧
当然不知这一个栈,我们要做很多栈来存放这些状态,比如存前进后退的,存跳跃和射击的,因为当前进或者后退的时候是可以跳跃或者射击的

接下来是一个起跳和下落的分析,看个图先,就是这个样子,当然没有改变canvas的坐标系的话,坐标原点在左上方,转化一下就OK了

/*** 恐龙* canvas 画布*/
function Trex(canvas) {this.canvas = canvas;this.ctx = canvas.getContext("2d");this.init();
}Trex.config = {JUMP_INITV: -18,     // 起跳初速度G: 1,                // 重力加速度BOTTOM_YPOS: 193,    // 恐龙与地面的接触位置SPEED: 4,             // 移动速度HP: 10,  // 恐龙的血量RUN_TIME: 4
}Trex.type = {STATICING: {NAME: 'STATICING', // 静止WIDTH: 44,        // 静止时的宽度HEIGHT: 47,      // 静止时的高度MOUSEHEIGHT: 40, // 嘴的高度 子弹发射口的高度IMG1: {x: 848, y: 3}, // 静止1状态在图中的位置IMG2: {x: 892, y: 3}, // 静止2状态在图中的位置IMG_TIMING: 3000, // 静止时 图片切换时间 用于眨眼BLINK_TIMING: 100, // 眨眼瞬间的时间MOUSEHEIGHT: {x: 20, y: 12},IMG: {x: 848, y: 3}},RUNNING: {NAME: 'RUNNING',   // 前进后退的图片WIDTH: 44,        // 前进时的宽度HEIGHT: 47,      // 前进时的高度MOUSEHEIGHT: 40, // 嘴的高度 子弹发射口的高度IMG1: {x: 980, y: 3},  // 前进状态在图中的位置IMG2: {x: 936, y: 3}, // 前进状态在图中的位置IMG_TIMING: 100,        // 奔跑时图片切换的时间MOUSEHEIGHT: {x: 20, y: 12},IMG: {x: 980, y: 3},}
}

老样子,构造函数和一些配置参数,上面配置中,把前进后退跑动过程中的图片切换配置在RUNNING状态中,分别为IMG1和IMG2,就像boss呼扇翅膀一样,小恐龙跑动的迈步子的动画就靠切换这两张图来执行了。
接下来是方法,老样子,把方法放在原型对象上,实在不知道单独拿出来该如何讲,所以把代码都贴上来,一点一点慢慢解释

Trex.prototype = {// 画笔工具绘制恐龙draw: function() {this.ctx.drawImage(gameImage,this.config.IMG.x, this.config.IMG.y, this.config.WIDTH, this.config.HEIGHT,this.xPos, this.yPos, this.config.WIDTH, this.config.HEIGHT)// 将hp绘制在小恐龙头部this.ctx.font = "13px Verdana";this.ctx.fillText('HP: ' + this.hp, this.xPos, this.yPos - 10);},/*** 初始化一些参数*    - 恐龙的状态,比如是站立还是跑动*    - 初始的位置,血量等等* 绑定AABB盒*/init: function() {// 初始化一些参数this.type = 1; // 盒模型的类型,用于检测不同类型之间的检测的this.timer = 0; // 记录各种动画切换的计时器this.hp = Trex.config.HP; // 恐龙的HPthis.xPos = 100; // 恐龙的初始位置this.yPos = Trex.config.BOTTOM_YPOS; // 恐龙的初始位置this.config = Trex.type.STATICING; // 恐龙初始的精灵图// 恐龙的AABB盒this.box = new AABB(this.xPos, this.yPos, this.config.WIDTH, this.config.HEIGHT, this.type);// 恐龙的起跳速度this.jumpV = Trex.config.JUMP_INITV;// 恐龙是否在起跳过程中this.isJumping = false;// 游戏进行的时间,用来制作恐龙跑动的动画this.timer = 0;// 恐龙的状态this.stateMap = {JUMPING: 'STATICING',WAITING: 'STATICING',GOING: 'STATICING',BACKING: 'STATICING'}// 控制前进后退的行为栈,WATING为默认行为,永远执行栈顶的行为;this.actionState = ['WATING'];// 跳跃的功能行为栈,this.funcState = [];this.draw();},/*** 根据用户的操作更新小恐龙的位置等状态 */update: function(time) {// 当血量为零的时候,游戏结束if(this.hp <= 0) {gameState = 'OVER';}// 这里是个小彩蛋,当小恐龙超过右边界的时候,游戏通关if(this.xPos >= gameConfig.bg.WIDTH) {success = 'SUCCESS';}this.timer = time;// 为actionState栈顶的行为// 获取前进还是后退状态var tmpActionState = this.actionState[this.actionState.length - 1];// 为funcState栈顶的行为// 获取是否为跳跃状态var tmpFuncState = this.funcState[this.funcState.length - 1];// 执行前进后退与跳跃的执行函数this.actionFsm(tmpActionState);this.funcFsm(tmpFuncState);// 当游戏进行时,根据恐龙的状态来绘制不同的动画帧,切换不同的小恐龙// 和boss一样,通过改变绘制的精灵图来达到动画的效果if(tmpFuncState != 'JUMPING' && (tmpActionState == 'GOING' || tmpActionState == 'BACKING')) {// 是奇数就迈左脚,偶数就迈右脚,跳跃状态或者等待状态就用两脚着地的图片if(isOdd(Math.floor(this.timer / Trex.config.RUN_TIME))) {Trex.type.RUNNING.IMG = Trex.type.RUNNING.IMG1;this.config =  Trex.type.RUNNING;}else {Trex.type.RUNNING.IMG = Trex.type.RUNNING.IMG2;this.config =  Trex.type.RUNNING;}}else {this.config =  Trex.type.STATICING;}// 更新AABB盒的状态this.box.setXY(this.xPos, this.yPos);// 绘制小恐龙this.draw();},// 将行为push到栈顶,并执行setState: function(stateType, state) {this.config = Trex.type[this.stateMap[state]];this[stateType].push(state);},// 当按键弹起时,将起对应的行为删掉delState: function(name, state) {// 同事删除其栈中的行为for(var i = 0, len = this[name].length; i < len; i ++) {if(state == this[name][i]) {this[name].splice(i, 1);}}// console.log(this.funcState);},// 根据不同的状态来执行不同的方法,这里是前进后退// 为什么要把跳跃和前进后退分开,因为前进的时候可以跳跃,但是前进的时候不能后退actionFsm: function(actionState) {switch(actionState) {case 'GOING':this.go();break;case 'BACKING':this.back();break;case 'WAITING':this.wait();break;}},// 根据不同的状态来执行不同的方法,这里是跳跃funcFsm: function(funcState) {switch(funcState) {case 'JUMPING':this.jump();break;}},// 没有任何操作的时侯执行的函数,但是没想好没有任何操作该执行怎样的操作wait: function() {// do wait},// 前进go: function() {// 当小恐龙走到中间的位置的时候停下了,被障碍物当着if(bossState == 'LIVE' && this.xPos >= gameConfig.bg.WIDTH / 2 - this.config.WIDTH) {return;}// 移动的速度是配置中的参数this.xPos += Trex.config.SPEED;},// 后退back: function() {// 当后退到边界的时候停下来if(this.xPos <= 0) {return;}this.xPos -= Trex.config.SPEED;},/*** 起跳* 起跳的时候要有状态标识是否在起跳中* 如果在起跳中则不做任何操作* 如果不在起跳状态中则执行起跳的操作* 起跳是怎么个逻辑呢,单独画个图讲一下 */jump: function() {this.isJumping = true;if(this.isJumping) {this.jumpV += Trex.config.G;this.yPos += this.jumpV;}if(this.yPos >= Trex.config.BOTTOM_YPOS) {this.yPos = Trex.config.BOTTOM_YPOS;this.jumpV = Trex.config.JUMP_INITV;this.isJumping = false;this[67] = true;this.delState('funcState', 'JUMPING');}},// 射击shoot: function() {// 这个简单,就调用方法创建子弹就OK了,子弹中包含有type信息会指定是谁的子弹Bullet.creat(this.canvas, {x: this.config.MOUSEHEIGHT.x + this.xPos, y: this.config.MOUSEHEIGHT.y + this.yPos}, 1, 1);}
}

boss到这里就完结了,确实有点乱,以后慢慢整理吧,接下来就是启动了和一些用户交互了

谷歌小恐龙之对打版(九)—— 交互和启动

到现在未知还差交互和启动了,先说交互,无非就是监听一些键盘事件,获取code码,根据不同的code码来执行不同的操作,因为我们先前定义的一些状态,当处于某个状态时会执行某个函数,所以我们只关心是哪种状态就OK了,而不需要直接调用函数

// 监听键盘事件
document.addEventListener('keydown', onKeyDown);
document.addEventListener('keyup', onKeyUp);// 根据不同的键盘code码来设置不同的状态
function onKeyDown(e) {switch(e.keyCode){case 37:trex.setState('actionState', 'BACKING');break;case 67:trex.setState('funcState', 'JUMPING');break;case 39:trex.setState('actionState', 'GOING');break;case 88:trex.shoot();break;}
}function onKeyUp(e) {switch(e.keyCode){case 37:trex.delState('actionState', 'BACKING');break;case 39:trex.delState('actionState', 'GOING');break;case 88:break;}
}

启动

// 监听Enter键,按下启动
document.addEventListener('keydown', GameStart);
// 当游戏的状态处在等待时启动游戏,并将游戏状态改为正在进行中
function GameStart(e) {if(gameState == 'WAITING' && e.keyCode == 13) {gameState = 'RUNNING';start();}
}

最后补充一下,前面的小恐龙和boss完成之后需要在动画帧的控制器中调用他们的update方法

var timer = 0;
(function draw(time) {timer ++;// time 大约16ms 两帧间隔时间ctx.clearRect(0,0,800,250);// 绘制完成后不会改变的图像 用draw方法floor.draw();cacti.draw();// 绘制完成后会改变的图像 用update方法cloud.update();trex.update(timer);boss.update(timer, trex);Bullet.update(trex, boss);// 游戏结束之后绘制gameover的图像if(gameState == 'OVER') {ctx.clearRect(0,0,800,250);return ctx.drawImage(over, 0, 0, 500, 98, 160, 70, 500, 98);}// 游戏成功之后绘制成功的图像if(gameState == 'SUCCESS') {ctx.clearRect(0,0,800,250);ctx.drawImage(suc, 0, 0, 387, 51, 200, 140, 387, 51);return ctx.drawImage(win, 0, 0, 653, 49, 120, 70, 653, 49);}window.requestAnimFrame(draw);
})();

到此为止,改造版的小恐龙就到此为止了。

chrome离线小恐龙改造版相关推荐

  1. chrome谷歌小恐龙作弊代码【无敌,快跑,高跳,一键满分】有需要的小伙伴快快看过来!

    谷歌小恐龙游戏是浏览器自带的,断网时候可以玩,当然联网状态也是可以玩的. 那么如何在联网的状态下进行游戏呢? 打开chrome, Ctrl + T 新建标签页,Alt + D或者Ctrl + L 定位 ...

  2. Chrome 的小恐龙游戏,被我破解了...

    一个阳光明媚的周末,透光的窗帘把我从睡梦中叫醒,大脑说今天是周六,可以慵懒个一上午,于是开心地打开我的 Mac 准备看两集Rick and Morty再起床洗漱. 我迫不及待打开了对应的网站,发现浏览 ...

  3. Chrome 的小恐龙游戏,被破解了...

    一个阳光明媚的周末,透光的窗帘把我从睡梦中叫醒,大脑说今天是周六,可以慵懒个一上午,于是开心地打开我的 Mac 准备看两集 Rick and Morty 再起床洗漱. 我迫不及待打开了对应的网站,发现 ...

  4. Chrome的小恐龙被我“开挂”了,看我如何用一行代码让它拥有不死之身

    个人网站:www.dzyong.top 微信公众号:关注<前端筱园>,不错过每一篇推送 作为一个开发人员,用到的最多的就是Chrome浏览器. 当没有网的时候打开浏览器就会看到下面这个界面 ...

  5. html5游戏刷分,google chrome浏览器离线小恐龙游戏刷分bug

    搜索热词 F12打开开发者工具->console->输入如下代码,分数要多少有多少 Runner.instance_.setSpeed(99999); 试试 瞬间 满分 window.te ...

  6. [c++简单小游戏]东搞西搞第二弹——谷歌chrome小恐龙升级版(啊哈)

    上效果图~~~ 灵感来源:谷歌chrome的小恐龙游戏(就是每次断网都会弹出来的那个) 那个#是墙... <是飞弹,移动速度为墙的两倍... 飞弹的走位很像小恐龙里的鸟,但它并不算一个墙...而 ...

  7. 谷歌Chrome小恐龙代码(自动跳,高跳,无敌,加速)

    目录 自动跳代码 无敌代码 高跳代码(可以改括号内参数) 疾跑代码(可以改括号内参数) 大多数浏览器都有自己的彩蛋,而今天我们分享的是谷歌Chrome 谷歌小恐龙游戏是一个浏览器自带的小游戏. 断网联 ...

  8. Chrome浏览器的彩蛋 - 小恐龙

    When Chrome的小恐龙 往往是在你断网时出来蹦跶的,这时候,我们一个空格键Space,就能触发彩蛋 - 超级玛(Kong)丽(Long). How

  9. chrome离线安装包

    Google Chrome 离线安装包 正式版: https://www.google.cn/chrome/?hl=zh-CN&standalone=1 测试版: https://www.go ...

  10. 一个低配版小恐龙游戏

    用过Chrome的同学应该玩耍过自带的那个小恐龙 无聊的时候刷上一下下倒也是一种消遣 (讲道理打到这个分我都快眼瞎了) 以下正经脸 所以呢我就用Construct 2做个真·低配版吧 既然是低配版那就 ...

最新文章

  1. [CTO札记]高效能辅导(Coach)转摘
  2. JDK 源码 Integer解读之一(toString)
  3. 以太坊私链搭建、truffle项目开发
  4. linux让数值依次递增的快捷键,如何将文件名批量修改成上一级文件夹的名字。如:A(文件夹名)-01这样依次递增?...
  5. 只用最适合的! 全面对比主流 .NET 报表控件:水晶报表、FastReport、ActiveReports 和 Stimulsoft...
  6. 波场php转,波场TRC20 Token PHP交互
  7. vue Iframe
  8. 如何将HBuilder中的项目Push至Gitee中!
  9. 如何写代码,才能越写越轻松?
  10. rest syntax(parameters)
  11. Ffmpeg视频压缩
  12. 转 C++异常机制的实现方式和开销分析 白杨 http://baiy.cn
  13. JAVA中apply方法的原理_关于学习java函数式接口Function中的apply方法的一些感悟
  14. UartAssist - 串口调试助手。
  15. MongoDB集群节点RECOVERING故障恢复
  16. python无法打开_终端里为什么无法运行python?
  17. Python练习:简单的登陆注册的信息管理;模块化;密码安全判断(没有用数据库和文件)
  18. 想要更高效地使用云计算,推荐学习云计算部署的五大策略
  19. 使用requests爬取实习僧网站数据
  20. idea导入项目却没有项目结构

热门文章

  1. PCB线宽与电阻的计算
  2. 小米路由器r2d_小米路由器二代R2D怎样设置无线中继模式
  3. oralce分析函数---group by || rollup || cude || grouping || grouping sets
  4. xs2鸿蒙系统,华为Mate XS2来了,搭载麒麟9000,依旧安卓10版本
  5. 微信小程序可视化开发工具之动态数据
  6. Android网络编程(一次网络请求)
  7. 视频文件加密的方法浅析
  8. 微信公众号网页链接失效解决方案
  9. CRT、CER、PEM、DER编码、JKS、KeyStore等格式证书说明及转换
  10. 用PS制作透明背景的电子签名