简化版的Html5 开发Flappy Bird开发教程-Html5-优质IT资源分享社区

admin
管理员
管理员
  • UID1
  • 粉丝29
  • 关注4
  • 发帖数581
  • 社区居民
  • 忠实会员
  • 原创写手
阅读:203回复:0

  简化版的Html5 开发Flappy Bird开发教程

楼主#
更多 发布于:2016-06-03 23:11

.0 开端之前

之前曾经用html5/javascript/CSS完结过2048,用Cocos2d-html5/Chipmunk写过一个Dumb

Soccer的对战游戏,但没有运用过原生的Canvas写过任何东西,为了加深对Canvas的学习,就心血来潮花了快到一天的时刻运用原生Canvas完结了一个简化版的flappy

bird,下面就总结一下开发的进程。

在正式开前,对于没有运用本地效劳器的开发者来说,建议下载一个firefox来进行测验或运用IE

10,由于firefox和IE

10对本地文件的拜访约束较低,在本地无效劳器环境调试的时分,在firefox阅读器中不容易碰到由于跨域而无法获取文件的疑问。假如可以在本地建立一个HTTP效劳器那就十分好了,根本不会碰到相似的过错。

.1 构造国际

.1.1 html页面

首要需求在index.html中加载所需求的脚本,设置canva标签,详细代码如下:

运用将canvas嵌入到一个div标签中,运用div标签来操控canvas的方位,如今是将canvas居中。

需求留意的是,有必要要在canvas标签内部设置款式,不然Javascript中所制作的图画的份额很发作严峻失真(图画被拉伸,变形)。如今,暂时不思考自适应屏幕巨细的疑问,首要把游戏完结了。

别的,脚本的加载最终放在body结尾,防止脚本获取元素的时分html页面并未加载完结。本来,十分好的办法是运用window.onload设置页面加载完结后的动作,确保Javascript脚本不会在元素未加载完结的时分去读取元素。

.1.2 game.js脚本

为了防止污染全局变量,在game.js中界说一个World方针,World担任游戏中的一切元素操控(创立、毁掉、操控)、动画帧的循环、磕碰检查等作业,是悉数游戏运作的引擎。下面是World中的一切办法和特点:

var World = {

// 保留Canvas

theCanvas : null,

// 游戏是不是暂停

pause: false,

// 初始化并运转游戏

init : function(){},

// 重置游戏

reset: function(){},

// 动画循环

animationLoop: function(){},

// 制作布景

BGOffset: 0, // scroll offset

backgroundUpdate : function() {},

// 更新元素

elementsUpdate: function(){},

// 磕碰检查

collisionDectect: function(){},

hitBox: function ( source, target ) {},

pixelHitTest: function( source, target ) {},

// 鸿沟检查

boundDectect: function(){},

// 创立烟囱

pipesCreate: function(){},

// 铲除烟囱

pipesClear: function(){},

// 小鸟出界检查

isBirdOutOfBound: function(callback){},

};

经过这些办法,World就可以运转游戏,完结对游戏中“小鸟”和“烟囱”的操控。

.1.3 游戏的初始化:World.init

游戏经过World.init来初始化游戏并运转。运用Html5

canvas完结的游戏或动画的原理都是相同的,即以特定的时刻距离不断地更新canvas画布上的图画以完结动画。因而,游戏初始化时分有必要要做的即是下面几件作业:

获取DOM中的canvas元素;

经过canvas元素获取context;

进入动画循环(animationLoop);

在咱们的game.js写下这么的代码:

1 World.init = function(){

2 var theCanvas = this.theCanvas =

document.getElementById('game_box');

3 this.ctx = theCanvas.getContext('2d');

4 this.width = theCanvas.width;

5 this.height = theCanvas.height; 6 this.bird =

null;

7 this.items = [];

8 this.animationLoop();

9 },

除了保留canvas元素及context以外,还将canvas画布的长宽也保留下,并创立了bird方针和

items数组以保留游戏中的元素。然后就进入了animationLoop这个动画循环。

.1.4 动画循环

动画循环以特定的时刻距离运转,担任更新游戏中每个元素特点,并将一切元素在canvas中制作出来。通常游戏会以60fps或30fps的帧率运转,显然60fps的帧率需求更大的运算量,而画面也更为流通,如今就暂时运用60fps帧率,那么每帧图画的时刻距离为1000ms/60

=

16.7ms。别的,要留意的是元素的制作次序,一定要首要将布景制作出来,然后再制作别的游戏元素,不然这些元素就会被布景图所掩盖。由于帧距离比照短,因而animationLoop中所允许的函数应当尽也许快。下面看看代码:

1 animationLoop: function(){

2

3 // scroll the background

4 this.backgroundUpdate();

5

6 // detect elements which is out of boundary

7 this.boundDectect();

8

9 // detect the collision between bird and

pipes

10 this.collisionDectect();

11

12 // update the elements

13 this.elementsUpdate();

14

15 // next frame

16 if(!this.pause){

17 setTimeout(function(){

18 World.animationLoop();

19 }, 16.7)

20 }

21 }

animationLoop中的作业次序是:

制作布景;

鸿沟检查,对出界的元素进行处理;

磕碰检查,履行相应的处理;

制作游戏中的元素;

设置下一帧的守时;

在这儿运用了setTimeout来设置下一帧的守时,而不是运用setInterval来完结一次性的守时操作,最主要的因素是为了坚持帧率的安稳。假如运用setInterval来守时,那么也许会呈现由于当时animationLoop处理时刻较长(超越16.7ms),致使下一帧处理

守时现已到来了而处于等候状况,等当时animationLoop处理完结后,当即履行下一帧的处理,这么使得帧距离被紧缩,呈现显着的帧率不稳的状况。假如运用setTimeout,即便当时处理时刻较长,帧处理完结到下一帧的距离也肯定是固定,而帧距离时刻会大于16.7ms。这么虽然帧率会下降,但可以下降跳帧这种帧率动摇较大的事件呈现。

.1.5 制作布景

在为游戏国际添加元素曾经,先为这个国际发明一个布景。为此,从网上下载了一个flappy元素包,里边有一张结合了一切图画元素的atlas图集,为了进步游戏的加载速度,决议运用这种图集,虽然显式图画的时分也许略微费事一点,游戏加载速度的进步作用很值得咱们这么做。

首要,是atlas.png图画的加载,通常有两种办法在html页面中运用标签进行加载,这么图画就会在DOM的构建进程中加载完结,此刻咱们设置该标签的长宽为0,令其不占有文本流的空间,一起也不会显现出来:

index.html:

1

game.js:

1 var image =

document.getElementById('atlas');

另一种是在Javascript脚本中动态加载,加载时刻更加灵敏:

1 var image = new image();

2 image.src = 'atlas.png';

3 imgge.onload = function(){

4 // wait for the loading

5 };

不管在哪种办法下都要等候图画加载完结才干运用图画,不然会犯错。动态加载会使得图画运转更加脚本运转流程变得更杂乱,由于在这个游戏中只需求加载一张图画,因而选用榜首种办法。

图画加载完结今后,只要在backgroundUpadte函数中将其制作出来,即可以顺畅完结布景的制作:

1 backgroundUpdate : function() {

2 var ctx = this.ctx;

3 ctx.drawImage(image, 0, 0, 288, 512, 0, 0, 288,

512);

4 },

drawImgae的第2个到第5个参数别离表明,方针图画在 图源中的x坐标 、 y坐标 以及

图源中图画的宽度 和 长度 ,最终四个参数是方针图画在 画布中的x坐标 、 y坐标 以及在 画布中的宽度 和 长度 。

可是,一个静态的布景图太简陋了,缺乏活力,所以咱们要令布景图卷动起来。完结原理十分简略,只需求将布景图制作的x轴偏移量跟着时刻改动而改动。但由于咱们所具有的布景图太窄了,需求将其会制作两次拼接出一张较宽的图画,完结的代码图下所示:

1 backgroundUpdate : function() {

2 var ctx = this.ctx;

3 this.BGOffset--;

4 if(this.BGOffset <= 0) {

5 this.BGOffset = 288;

6 }

7 ctx.drawImage(image, 0, 0, 288, 512,

this.BGOffset, 0, 288, 512);

8 ctx.drawImage(image, 0, 0, 288, 512,

this.BGOffset - 288, 0, 288, 512);

9 },

这时,图画还没有开端动,由于咱们的国际还没有初始化!!!为了让国际在页面加载完结后初始化,在index.html的body结尾中嵌入脚本。

1

  2 window.onload = function(){  3 console.log('start');

  4 World.init();  5 }  6" cdata_tag="script">

此刻,用阅读器翻开页面,就可以看到咱们所发明的新国际了。

.2 在国际中添加元素

国际发明完结今后,就可以往国际里边添加元素了。游戏国际里边每个元素都也许不相同,有不相同的巨细、形状、特点、图画,但这些元素也有一些共性,如它们都需求有记载巨细、方位、移动速度的特点,还需求有在元素中烘托该图画的办法。这儿有些特点和办法是特有的,如巨细特点,烘托办法,但一起这些元素也有共有的特点,如设置方位、速度的办法等。为此,咱们将创立一个名Item函数方针,运用这个函数方针的prototype来保留一些公有的办法和特点,再创立Bird类和Pipe类来创立构造“bird”和“pipe”方针。

.2.1 根本元素:Item类

国际中每个元素都需求有的根本特点是:巨细、方位、速度、重力(重力加速度),而这些特点每个详细的元素方针都也许不相同,因而它们不设置在prototype上,只在方针自身上创立。而prototype上有的是设置这些特点的办法,还有一个叫generateRenderMap的办法。这个办法是用来生成用于像素磕碰检查的数据的,暂时先不写。

1 /*

2 * Item Class

3 * Basic tiem class which is the basic elements

in the game world

4 [email protected] draw, the context draw

function

5 [email protected] ctx, context of the canvas

6 [email protected] x, posisiton x

7 [email protected] y, posisiton y

8 [email protected] w, width

9 [email protected] h, height

10 [email protected] g, gravity of this item

11 */

12 var Item = function(draw, ctx, x, y, w, h,

g){

13 this.ctx = ctx;

14 this.gravity = g || 0;

15 this.pos = { x: x || 0,

16 y: y || 0

17 };

18 this.speed = { x: 0, // moving speed of the

item

19 y: 0

20 }

21 this.width = w;

22 this.height = h;

23 this.draw = typeof draw == 'function' ? draw :

function(){};

24 return this;

25 };

26

27 Item.prototype = {

28 // set up the 'draw' function

29 setDraw : function(callback) {

30 this.draw = typeof draw == 'function' ? draw :

function(){};

31 },

32

33 // set up the position

34 setPos : function(x, y) {

35 // Handle: setPos({x: x, y: y});

36 if(typeof x == 'object') {

37 this.pos.x = typeof x.x == 'number' ? x.x :

this.pos.x;

38 this.pos.y = typeof x.y == 'number' ? x.y :

this.pos.y;

39 // Handle: setPos(x, y);

40 } else {

41 this.pos.x = typeof x == 'number' ? x :

this.pos.x;

42 this.pos.y = typeof y == 'number' ? y :

this.pos.y;

43 }

44 },

45

46 // set up the speed

47 setSpeed : function(x, y) {

48 this.speed.x = typeof x == 'number' ? x :

this.speed.x;

49 this.speed.y = typeof y == 'number' ? y :

this.speed.y;

50 },

51

52 // set the size

53 setSize : function(w, h) {

54 this.width = typeof width == 'number' ? width :

this.width;

55 this.height = typeof height == 'number' ?

height : this.height;

56 },

57

58 // update function which ran by the animation

loop

59 update : function() {

60 this.setSpeed(null, this.speed.y +

this.gravity);

61 this.setPos(this.pos.x + this.speed.x,

this.pos.y + this.speed.y);

62 this.draw(this.ctx);

63 },

64

65 // generate the pixel map for 'pixel collision

dectection'

66 generateRenderMap : function( image, resolution

) {}

67 }

内部特点的初始化有Item函数完结,里边有设置简略的默许初始化。更加完善的初始化办法是首要检查输入参数的类型,然后再进行初始化。

gravity影响的是垂直方向的速度,即speed.y。而speed在每一次元素更新(动画循环)的时分,影响pos特点,然后改动元素的方位。

update这个公有办法经过setSpeed、setPos改动元素的速度和方位,并调用draw办法来将元素制作在canvas画布上。

元素初始化的时分,有必要从World中取得draw办法,不然元素的图画是不会制作到canvas画布上的。而制作所需求的context也是从World中获取的,在初始化的时分获取,并保留到内部变量中。

添加Item方针

经过Item来创立一个方针,就可以向国际中添加一个元素,在World.init中添加代码:

1 World.init = function(){

2 ...

3

4 var item = new Item(function(ctx){

5 ctx.fillStyle = "#111111";

6 ctx.beginPath();

7 ctx.arc(this.pos.x, this.pos.y, this.width/2, 0,

Math.PI*2, true);

8 ctx.closePath()

9 ctx.fill()

10 }, this.ctx, 50, 50, 10, 10, 0.2);

11 this.items.push(item); // 将元素放入到办理列表中

12

13 ...

14 }

经过上述代码,就可以往World中添加一个圆点。但此刻国际中依然不会显现圆点,那是由于World.elementsUpdate还没有完结。该办法需求遍历国际中的一切元素,调用元素的update办法,经过元素的update办法调用draw办法然后完结元素在画布上的制作。

1 World.elementsUpdate = function(){

2 // update the pipes

3 var i;

4 for(i in this.items) {

5 this.items.update();

6 }

7 }

改写页面今后,就会看到一个小圆点在做自由落体运动。

.2.2承继:extend函数

在创立别的类型的元素前,先来看看要怎么完结类的承继。

为何要运用承继?

在游戏中的元素都存在共性,它们都有记载巨细、方位、速度的特点,也都需求有设置巨细、方位、速度的特点,还有必要要有一个供给给World.elementsUpdate办法调用的更新元素特点、在画布上制作元素图画的接口。经过类的承继,在创立不相同类型的时分就可以将来自早已界说好的基类——Item类——的特点或办法承继下来,简化了类的创立,一起也节省了实例占用的空间。

怎么完结类的承继?

要完结类的承继,最主要的是应用了constructor和prototype。在子类构造器函数中,经过调用Parent.constructor.call(this)就可运用基类构造器为子类构造内部

特点和办法 ;经过Child.prototype =

Parent.prototype就可以承继基类的prototype,这么子类的实例方针就可以直接调用基类prototype上的代码。JavaScript里完结类承继的办法十分多,不相同的办法可以发作不相同的作用,更多详细的阐明请翻阅有关的参阅书,如《JavaScript面向方针编程攻略》,《JavaScript规划形式》等。

extend函数

在这儿,咱们选用一个简略的extend函数来完结承继。

1 /*

2 * for deriving a new Class

3 * Child will copy the whole prototype the Parent

has

4 */

5 function extend(Child, Parent) {

6 var F = function(){};

7 F.prototype = Parent.prototype;

8 Child.prototype = new F();

9 Child.prototype.constructor = Child;

10 Child.uber = Parent.prototype;

11 }

这个函数干了下面一些作业:

创立一个空函数方针F作为中心变量;

中心变量获取Parent的prototype;

子类从中心变量中承继原型并更新原型中的构造器,此刻子类的原型和基类的原型虽然包括相同的特点和办法,可是现已两个独立的原型了,不会相互影响;

最终创立一个内部uber变量来引证Parent原型;

该办法参阅自《JavaScript面向方针编程攻略》。运用这个办法,会仿制基类原型链并承继之,并不会承继基类的内部特点和办法(this.xxxx)。这么做的因素是,虽然子类和基类也许会有一起的元素,可是初始化构造时要履行的参数不相同,有些元素也许具有更多内部特点,有些内部特点也许现已被一些子类元素扔掉了,但原型链上的公有办法则是子类想承继的。

运用内部uber特点引证基类原型链的因素在于,子类有也许需求重载原型链上的公有办法,这么就会把原有承继而来的办法掩盖掉,但有时又需求调用基类原有的办法,因而就运用内部特点uber保留对基类原型链的引证。

.2.3 主角:Bird类

这个国际的主角是Bird,虽然它是主角,但它也是这个国际的元素之一,与Item类相同具有记载巨细、方位、速度的内部特点,它将会承继来自Item类原型链上设置内部特点的办法,当然也有一个更主要的与众不相同的fly办法。

但首要,要获取atlas中小鸟的图源参数。为了方便起见,创立一个方针将其记载下来。

1 var atlas = {};

2 atlas.bird =[

3 { sx: 0, sy: 970, sw: 48, sh: 48 },

4 { sx: 56, sy: 970, sw: 48,sh: 48 },

5 { sx: 112, sy: 970, sw: 48, sh: 48 },

6 ]

atlas.bird中记载了atlas图左下角三只黄色小鸟的信息,别离是表明三种状况。如今暂时用atlas.bird[1]展现小鸟的滑翔状况。

1 /*

2 * Bird Class

3 *

4 * a sub-class of Item, which can generate a

'bird' in the world

5 [email protected] ctx, context of the canvas

6 [email protected] x, posisiton x

7 [email protected] y, posisiton y

8 [email protected] g, gravity of this item

9 */

10 var Bird = function(ctx, x, y, g) {

11 this.ctx = ctx;

12 this.gravity = g || 0;

13 this.pos = { x: x || 0,

14 y: y || 0

15 };

16 this.depos = { x: x || 0, // default position

for reset

17 y: y || 0

18 };

19 this.speed = { x: 0,

20 y: 0

21 }

22 this.width = atlas.bird[0].sw || 0;

23 this.height = atlas.bird[0].sh || 0;

24

25 this.pixelMap = null; // pixel map for 'pixel

collistion detection'

26 this.type = 1; // image type, 0: falling down,

1: sliding, 2: raising up

27 this.rdeg = 0; // rotate angle, changed along

with speed.y

28

29 this.draw = function drawPoint() {

30 var ctx = this.ctx;

31 ctx.drawImage(image, atlas.bird[this.type].sx,

atlas.bird[this.type].sy, this.width, this.height,

32 this.pos.x, this.pos.y, this.width,

this.height); // draw the image

33 };

34 return this;

35 }

36

37 // derive fromt the Item class

38 extend(Bird, Item);

39

40 // fly action

41 Bird.prototype.fly = function(){

42 this.setSpeed(0, -5);

43 };

44

45 // reset the position and speed

46 Bird.prototype.reset = function(){

47 this.setPos(this.depos);

48 this.setSpeed(0, 0);

49 };

50

51 // update the bird state and image

52 Bird.prototype.update = function() {

53 this.setSpeed(null, this.speed.y +

this.gravity);

54 this.setPos(this.pos.x + this.speed.x,

this.pos.y + this.speed.y); // update position

55 this.draw();

56 }

Bird的构造器根本上Item相同,格外的在于它的宽度和长度由图画的巨细决议,而它在内部定制了draw办法,用于将小鸟的图画制作到画布上。draw办法中调用了跟制作布景时相同的drawImage办法,只不过图源信息从atlas.bird中获取,暂时默许小鸟以滑翔状况显现。

Bird多了两个办法,别离是reset和fly。reset用于重置小鸟的方位和速度;而fly则是给小鸟设置一个向上的速度(speed.y),让其向上飞一下。

此外Bird还“重载”了update办法。如今看来,这个办法跟Item中的没有什么差异,但由于它是国际的主角,后来会为它添加更多的动画等,所以预先在这儿“重载”了。

要留意的是,extend函数需求在界说Bird的prototype办法之前,不然新界说的办法会被Item类的prototype掩盖掉。

在国际中添加小鸟

如今,就可以往国际里添加小鸟了,在World.init中添加如下代码:

1 World.init = function(){

2 ...

3 this.bird = new Bird(this.ctx, this.width/10,

this.height/2, 0.15);

4 ...

5 }

此刻,类封装的优点就显现出来了,由于Bird类现已将小鸟的构造进程封装好,创立小鸟实例的时分只需求传入context并设置方位及重力参数,创立进程变得极为简便。

除此以外,还需求在World.elementsUpdate中添加代码,让动画循环把小鸟图画制作在画布上:

1 World.elementsUpdate = function(){

2 // update the pipes

3 var i;

4 for(i in this.items) {

5 this.items.update();

6 }

7

8 // update the bird

9 this.bird.update();

10 },

改写页面,就可以在游戏国际中看到一只只会自由落体的小鸟了。

操控小鸟

一只只会自由落体的小鸟显然是不好玩的,为此要在国际中添加操控小鸟的办法。简略地,咱们让键盘按下任何键都会使小鸟往上飞,需求在World.init中添加代码:

1 World.init = function(){

2 ...

3 (function(that){

4 document.onkeydown = function(e) {

5 that.bird.fly();

6 };

7 })(this);

8 ...

9 }

经过document.onkeydown设置按键按下时的回调函数,进而调用bird.fly使其往上飞。在这儿运用了闭包来传递World的this方针,由于履行回调的时分上下文会改动,需求运用闭包来获取界说回调函数时的上下文中的方针。除此之外,假如需求指定某个按键来操控小鸟的动作,则可以经过回调函数的参数e来得到被按下按键方针的keycode。可是,不相同内核的阅读的keycode保留的方位不相同,如webkit中是e.event.keycode,而Netscape中则是e.keycode。要处理这个兼容性疑问,可以先运用navigator.appName来区分阅读器类型来选用不相同办法获取keycode,或许直接在e方针中查找keycode。

从头改写页面,按下键盘上恣意一个按键,就可以让小鸟往上飞了。

.2.4 反派:Pipe类

有了主角,就要有反派,国际才会充溢乐趣。而在这儿,咱们的反派即是那些长长短短的烟囱们。尝到Bird创立的甜头后,咱们用相同的办法来构造一个Pipe类。

首要仍是得有图源的参数,选用与Bird相似的办法来保留,在这儿只选用图会集绿色的两根烟囱。

1 atlas.pipes = [

2 { sx: 112, sy: 646, sw: 52, sh: 320 }, // face

down

3 { sx: 168, sy: 646, sw: 52, sh: 320 } // face

up

4 ]

Pipe类代码:

1 /*

2 * Pipe Class

3 *

4 * a sub-class of Item, which can generate a

'bird' in the world

5 [email protected] ctx, context of the canvas

6 [email protected] x, posisiton x

7 [email protected] y, posisiton y

8 [email protected] w, width

9 [email protected] h, height

10 [email protected] spx, moving speed from left

to right

11 [email protected] type, choose to face down(0)

or face up(1)

12 */

13 var Pipe = function(ctx, x, y, w, h, spx, type)

{

14 this.ctx = ctx;

15 this.type = type || 0;

16 this.gravity = 0; // the pipe is not moving

down

17 this.width = w;

18 this.height = h;

19 this.pos = { x: x || 0,

20 y: y || 0

21 };

22 this.speed = { x: spx || 0,

23 y: 0

24 }

25

26 this.pixelMap = null; // pixel map for 'pixel

collistion detection'

27

28 this.draw = function drawPoint(ctx) {

29 var pipes = atlas.pipes;

30 if(this.type == 0) { // a pipe which faces

down, that means it should be on the top

31 ctx.drawImage(image, pipes[0].sx, pipes[0].sy +

pipes[0].sh - this.height, 52, this.height, this.pos.x, 0, 52, this.height);

32 } else { // a pipe which faces up, that means

it should be on the bottom

33 ctx.drawImage(image, pipes[1].sx, pipes[1].sy,

52, this.height, this.pos.x, this.pos.y, 52, this.height);

34 }

35

36 return this;

37 }

38

39 // derived from the Item class

40 extend(Pipe, Item);

Pipe类的界说相同不杂乱,由于Pipe的长度会随机改动,并且有面朝上和面朝下两种形状,因而构造器保留长宽参数并设置有类型参数。在这儿假定Pipe不能上下移动,因而speed.y设置为0,一起只能初始化Pipe在x轴上的移动速度。

Pipe的draw办法也运用与Bird相似的办法,差异在于要依据烟囱类型来挑选制作办法和参数。

在国际中随机地添加烟囱

为了给国际添加趣味性,需求随机地在国际中创立烟囱,为此在World.pipesCreate写下代码:

1 pipesCreate: function(){

2 var type = Math.floor(Math.random() * 3);

3 var that = this;

4 // type = 0;

5 switch(type) {

6

7 // one pipe on the top

8 case 0: {

9 var height = 125 + Math.floor(Math.random() *

100);

10 that.items.push( new Pipe(that.ctx, 300, 0, 52,

height, -1, 0)); // face down

11 break;

12 }

13 // one pipe on the bottom

14 case 1: {

15 var height = 125 + Math.floor(Math.random() *

100);

16 that.items.push(new Pipe(that.ctx, 300,

that.height - height, 30, height, -1, 1)); // face up

17 break;

18 }

19 // one on the top and one on the bottom

20 case 2: {

21 var height = 125 + Math.floor(Math.random() *

100);

22 that.items.push( new Pipe(that.ctx, 300,

that.height - height, 30, height, -1, 1) ); // face up

23 that.items.push( new Pipe(that.ctx, 300, 0, 30,

that.height - height - 100, -1, 0) ); // face down

24 break;

25 }

26 }

27 }

pipesCreate中运用Math.random随机设置烟囱类型(仅一只烟囱在上面;仅一只烟囱在下面;上下都有烟囱;),并随机设置烟囱的长度。要留意的是,Math.random只会发作0~1的随机小数,乘上所需随机规模的最大值后就可以获取这个规模中0~max的随机小数,假如要获取整数则需求运用Math.floor或Math.round来去掉小数有些。更多的运用窍门请参阅有关的JavaScript书本。

每创立一个Pipe实例,都需求将其存入World.items中,由World.elementsUpdate来对一切的烟囱进行一致更新和制作。

完结随机创立办法今后,在World.init中添加守时器来调用发明烟囱的办法:

1 World.init = function(){

2 (function(that){

3 setInterval(function(){

4 that.pipesCreate();

5 }, 2000)

6 })(this);

7 }

相同的,需求运用闭包了传递World自身,不然守时函数无法获取this.pipesCreate办法。由于在pipesCreate中创立的Pipe都设置的固定的初始方位,Pipe以固定的速度向左移动,因而Pipe实例之间的距离就经过守时器的时刻距离来操控。当然,时刻距离越短,烟囱间距离就越窄,那么游戏的难度就加大了。

处理出界的元素

如今World.pipesCreate会不断创立烟囱方针并保留到World.items中,即便出界了也没有做任何处理,那么不再呈现的烟囱方针会一直累积下来,一点点地消耗内存。因而,需求对出界的烟囱来进行处理。

而对于Bird实例而言,Bird掉落到国际下部时,假如没有任何操作,那么小鸟就会永远地掉落下去,很难再飞上来了。因而有必要对小鸟的出界进行检查和处理。

要记住,canvas画布中,原点在左上角,X轴方向从左向右,而Y轴方向从上向下。

检查及处理出界元素的代码:

// boundary dectect

World.boundDectect = function(){

// the bird is out of bounds

if(this.isBirdOutOfBound()){

this.bird.reset();

this.items = [];

} else {

this.pipesClear();

}

},

// pipe clearance

// clear the pipes which are out of bound

World.pipesClear = function(){

var it = this.items;

var i = it.length - 1;

for(; i >= 0; --i) {

if(it.pos.x + it.width < 0) {

it = it.splice(i, 1);

}

}

};

// bird dectection

World.isBirdOutOfBound = function(callback){

if(this.bird.pos.y - this.bird.height - 5 >

this.height) { // the bird reach the bottom of the world

return true;

}

return false;

};

当检查到烟囱的方位越过了画面的左界的时分,就将该烟囱实例铲除。这儿运用了Array.splice办法,要留意的是,移除Array的时分会改动Array的长度和被移除元素后边元素的方位,因而在这儿运用从后往前的遍历办法。

当小鸟方位超越画面下界时,运用World.items =

[]铲除一切烟囱,并重置小鸟的方位。

在改写一下页面,试着任由小鸟自由落体至画面底部,就会看到小鸟会被重置。

.3 磕碰检查

到如今为止,游戏的根本元素都现已添加结束了,但你会发现一个疑问:无敌的小鸟像超级英雄相同穿越一切烟囱,反派只是起到装修的作用。这是由于,咱们还没有添加磕碰检查功用。

.3.1 边框磕碰检查

虽然每个元素的形状都不一定是方方正正的,可是咱们在创立元素的时分都为这个元素设置了长度和宽度,运用这个躲藏的边框,就可以完结边框检查。检查办法十分简略,只要检查两个框是不是有重复有些即可,完结手法即是坚持两个框的鸿沟距离是不是相互交织。相似的算法在leetcode上面有算法题,都可以用来学习。

留意的是pos表明的是边框左上角的坐标。

1 World.hitBox = function ( source, target ) {

2 return !(

3 ( ( source.pos.y + source.height ) < (

target.pos.y ) ) ||

4 ( source.pos.y > ( target.pos.y +

target.height ) ) ||

5 ( ( source.pos.x + source.width ) <

target.pos.x ) ||

6 ( source.pos.x > ( target.pos.x +

target.width ) )

7 );

8 }

边框检查极端简略且迅速,可是其作用是,小鸟还没有碰到烟囱就就会判定为磕碰已发作。那是由于小鸟的图画不仅没有填满这个边框,还具有不规则的形状。因而边框检查只能用做初步的磕碰检查。

.3.2 像素磕碰检查

依据精密的检查办法是对两个元素的像素进行检查,区分是不是有堆叠的有些。

可是,像素磕碰检查需求遍历元素的像素,运算速度比照慢,假如元素较多,那么帧距离时刻内来不及完结检查使命。为了削减磕碰检查的耗时,可以先运用边框检查区分那些元素之间有也许发作磕碰,对也许发作磕碰的元素运用像素磕碰检查。

1 // dectect the collision

2 Wordl.collisionDectect = function(){ 3 for(var i

in this.items) {

4 var pipe = this.items;

5 if(this.hitBox(this.bird, pipe) &&

this.pixelHitTest(this.bird, pipe)) {

6 this.reset();

7 break;

8 }

9 }

10 };

游戏里,只需求检查小鸟和烟囱之间的磕碰,因而只需求拿小鸟和烟囱逐一做检查,先进行边框磕碰检查,然后进行像素磕碰检查,以进步运算功率。检查到磕碰今后,调用World.reset来重置游戏或进行别的操作。

1 World.reset = function(){

2 this.bird.reset();

3 this.items = [];

4 }

下面来看看像素磕碰检查。虽然削减了像素磕碰检查的调用次数,但每次像素磕碰检查的运算量依然十分大。将如两个图画各包括200个像素,那么逐一像素进行比照就需求40000次运算,显然功率低下。

细心想想,图画发作磕碰时只要边际发作磕碰就可以。那么只要记载图画的边际数据,然后检查两幅图画边际是不是重合区分磕碰,下降需求运算的像素点数量然后下降运算量。可是边际检查及边际重合的算法并不简略,傍边会呈现许多疑问。

在这儿,咱们计划将边框磕碰检查应用到像素磕碰检查傍边。首要,需求将原图画进行稀少编码,行将原图画的分辨率下降,这么就相当于将一个1pixel的像素点编程一个由更多像素点构成的方形小框。

然后,把这些小框的数据保留到每个元素的pixelMap中,这么一来,在进行磕碰检查的时分,就可以看元素图画看作是多个边框组合而成的图画,咱们要做的只需求检查构成两个元素的小框之间有没有发作磕碰。

像素检查算法的完结

World.pixelHitTest = function( source, target )

{

// Loop through all the pixels in the source

image

for( var s = 0; s <

source.pixelMap.data.length; s++ ) {

var sourcePixel = source.pixelMap.data[s];

// Add positioning offset

var sourceArea = {

pos : {

x: sourcePixel.x + source.pos.x,

y: sourcePixel.y + source.pos.y,

},

width: target.pixelMap.resolution,

height: target.pixelMap.resolution

};

// Loop through all the pixels in the target

image

for( var t = 0; t <

target.pixelMap.data.length; t++ ) {

var targetPixel = target.pixelMap.data[t];

// Add positioning offset

var targetArea = {

pos:{

x: targetPixel.x + target.pos.x,

y: targetPixel.y + target.pos.y,

},

width: target.pixelMap.resolution,

height: target.pixelMap.resolution

};

/* Use the earlier aforementioned hitbox function

*/

if( this.hitBox( sourceArea, targetArea ) ) {

return true;

}

}

}

},

resolution是指像素点扩大的份额,假如为4,则是将1 pixel 扩大为4X4 pixel

巨细的边框。该算法是从初始的pixelMap中读取每个小框,并构造一对Area方针(方形边框)传递给World.hitBox办法进行边框磕碰检查。

pixelMap的构造

而pixelMap 的构造则需求用到context.getImageData办法。

本地环境下,getImageData在IE

10或firefox阅读器下可以顺畅运转,假如是在Chrome下则会发作跨域疑问。除非运用HTTP效劳器来供给web效劳,不然需求更改chrome的启动参数--allow-file-access-from-files才可以运用getImageData来获取本地图画文件的数据。

getImageData是从canvas画布的指定方位获取指定巨细的图画数据,因而假如存在布景的话,布景的图画数据也会被截取。因而需求创立一个暂时的canvas

DOM方针,在上面制作方针图画,然后再从暂时画布上截取图画信息。

Bird类的pixelMap:(在Bird类的draw办法中添加代码)

1 var Bird = function(){

2 ...

3 this.draw = function(){

4 ...

5 // the access the image data using a temporaty

canvas

6 if(this.pixelMap == null) {

7 var tempCanvas =

document.createElement('canvas'); // create a temporary canvas

8 var tempContext =

tempCanvas.getContext('2d');

9 tempContext.drawImage(image,

atlas.bird[this.type].sx, atlas.bird[this.type].sy, this.width, this.height,

10 0, 0, this.width, this.height); // put the

image on the temporary canvas

11 var imgdata = tempContext.getImageData(0, 0,

this.width, this.height); // fetch the image from the temporary canvas

12 this.pixelMap = this.generateRenderMap(imgdata,

4); // using the resolution the reduce the calculation

13 }

14 ...

15 }

16 ...

17 }

Pipe类的pixelMap:(相似地在draw办法中添加代码)

1 var Pipe = function(){

2 ...

3 this.draw = function(){

4 ...

5 if(this.pixelMap == null) { // just create the

pixel map from a temporary canvas

6 var tempCanvas =

document.createElement('canvas');

7 var tempContext =

tempCanvas.getContext('2d');

8 if(this.type == 0) {

9 tempContext.drawImage(image, 112, 966 -

this.height, 52, this.height, 0, 0, 52, this.height);

10 } else { // face up

11 tempContext.drawImage(image, 168, 646, 52,

this.height, 0, 0, 52, this.height);

12 }

13 var imgdata = tempContext.getImageData(0, 0,

52, this.height);

14 this.pixelMap = this.generateRenderMap(imgdata,

4);

15 }

16 ...

17 }

18 ...

19 }

不管是Bird类仍是Pipe类,都运用从Item类中承继而来的generateRenderMap办法

// generate the pixel map for 'pixel collision

dectection'

[email protected] image, contains the image size

and data

[email protected] reolution, how many pixels to

skip to gernerate the 'pixelMap'

Item.generateRenderMap = function( image,

resolution ) {

var pixelMap = [];

// scan the image data

for( var y = 0; y < image.height;

y=y+resolution ) {

for( var x = 0; x < image.width; x=x+resolution

) {

// Fetch cluster of pixels at current position

// Check the alpha value is above zero on the

cluster

if( image.data[4 * (48 * y + x) + 3] != 0 ) {

pixelMap.push( { x:x, y:y } );

}

}

}

return {

data: pixelMap,

resolution: resolution

};

}

resolution决议小框的巨细,也决议了每行每列越过的像素点数量。当检查到一个像素点的alpha通道值不为0,就将其保留到pixelMap中即可。

此刻改写一下页面,你会发现小鸟再也不是无敌的了。

.4 添加动画作用

根本的Flappy

Bird根本完结了,可是小鸟只能以滑翔的姿势运动,没有有扇动羽翼的动作,显得没有生气。为此,咱们可以给Bird类添加动画作用,让小鸟向上飞的时分会扇动羽翼,一起头部朝上;向下掉落的时分则头部朝下,以爬升的姿势运动。

这时分,之前提到了“重载”Bird.update办法的含义就来了。

1 // update the bird state and image

2 Bird.prototype.update = function() {

3 this.setSpeed(null, this.speed.y +

this.gravity);

4

5 if(this.speed.y < -2) { // raising up

6 if(this.rdeg > -10) {

7 this.rdeg--; // bird's face pointing up

8 }

9 this.type = 2;

10 } else if(this.speed.y > 2) { // fall

down

11 if(this.rdeg < 10) {

12 this.rdeg++; // bird's face pointing down

13 }

14 this.type = 0;

15 } else {

16 this.type = 1;

17 }

18 this.setPos(this.pos.x + this.speed.x,

this.pos.y + this.speed.y); // update position

19 this.draw();

20 }

当小鸟速度speed.y小于-2(飞起来初速度是-4)时,就削减其旋转视点rdeg让其脸逐步朝上并更改图画显现状况为2(向下拍羽翼);

当小鸟速度speed.y大于2(下落时速度>0)时,就添加其旋转视点rdeg让其脸逐步朝下并更改图画显现状况为0(羽翼上拉,成爬升姿势);

速度在-2和2之间时,就维持滑翔状况。

旋转小鸟

添加更新小鸟特点的办法后,还需求更新Bird.draw,不然旋转的作用是不会显现出来的。

1 var Bird = function(){

2 ...

3 this.draw = function(){

4 ...

5 ctx.save(); // save the current ctx

6 ctx.translate(this.pos.x, this.pos.y); // move

the context origin

7 ctx.rotate(this.rdeg*Math.PI/180); // rotate the

image according to the rdeg

8 ctx.drawImage(image, atlas.bird[this.type].sx,

atlas.bird[this.type].sy, this.width, this.height,

9 0, 0, this.width, this.height); // draw the

image

10 ctx.restore(); // restore the ctx after

rotation

11 ...

12 };

13 ...

14 };

运用context.rotate旋转图画前,需求先保留本来的context状况,将画布的原点移动到当时图画的坐标,接着依据Bird.rdeg旋转图画,然后制作图画。运用drawImage制作图形时,需求将方针坐标改为(0,0),由于此刻画布原点坐标以及移动到了Bird.pos上的。制作完结后康复context的状况。这么,就可以完结小鸟的身体倾斜了。

改写一下页面,小鸟的动画特效就完结了。

至于磕碰特效之类的动画特效,就由我们自个自由发挥了,在这儿,只是将最简略的Flappy Bird

游戏功用完结。

.5 总结

游戏耗时一天完结,期间在磕碰检查有些花费了很多的时刻查阅材料和测验,找到适宜的办法后又在getImageData折腾了良久处理本地调试的跨域疑问和截取不含布景的图画数据的疑问。但总算是完结了这个简化版的游戏。

如今,简化版本没有任何菜单、按键、显现文本等,日后会思考持续把这有些功用完善。此外,有有些代码写的并不行精简,构造也不行明晰,编程技术有待锻炼。

跟之前运用Cocos2d-html的开发阅历比照,不运用任何结构的开发难度进步了不少,尤其是在动画循环、元素制作、磕碰检查这些有些花了不是功夫。不过,之前看过的JavaScript编程有关的书本帮了我不少忙。有关的读书笔记都存在自个的Evernote里边,哪天再找时机收拾收拾把它们发到博客上来。

总归,这次的开发阅历让我学到了不少常识,对canvas的了解也更深了,期望未来有一天可以开发一个自个的游戏,而不是去重复完结他人的游戏。

参阅

磕碰检查算法参阅:http://benjaminhorn.io/code/pixel-accurate-collision-detection-with-javascript-and-canvas/

优质IT资源分享社区为你提供此文。

本站有大量优质html5 css js等web前端开发教程视频,资料等资源,包含html5 css js 前端开发框架等基础教程,高级进阶教程等等,教程视频资源涵盖传智播客,极客学院,达内,北大青鸟,猎豹网校等等IT职业培训机构的培训教学视频,价值巨大。欢迎点击下方链接查看。

WEB前端开发教程视频

优质IT资源分享社区(www.itziyuan.top)
一个免费,自由,开放,共享,平等,互助的优质IT资源分享网站。
专注免费分享各大IT培训机构最新培训教学视频,为你的IT学习助力!

!!!回帖受限制请看点击这里!!!
!!!资源失效请在此版块发帖说明!!!

[PS:按 CTRL+D收藏本站网址~]

——“优质IT资源分享社区”管理员专用签名~

本版相似帖子

游客