1
|
<
li
><
a
href
=
"projects/flappybird/index.html"
>flappybird </
a
> <
span
> - Game</
span
></
li
>
|
首先加载资源和添加游戏背景:
1
2
3
4
5
|
this
.winSize = cc.Director.getInstance().getWinSize();
cc.SpriteFrameCache.getInstance().addSpriteFrames(res.flappy_packer);
this
.bgSprite = cc.Sprite.create(res.bg);
this
.bgSprite.setPosition(
this
.winSize.width / 2,
this
.winSize.height / 2);
this
.addChild(
this
.bgSprite, 0);
|
游戏资源使用TexturePacker打包在flappy_packer.plist文件中,函数名跟cocos2d-x c++版本是一样的。 初始化地面:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
Helloworld.prototype.initGround =
function
() {
//cc.log("initGround");
this
.groundSprite = cc.Sprite.create(res.ground);
var
halfGroundW =
this
.groundSprite.getContentSize().width;
var
halfGroundH =
this
.groundSprite.getContentSize().height;
this
.groundSprite.setAnchorPoint(0.5, 0.5);
this
.groundSprite.setPosition(halfGroundW / 2, halfGroundH / 2);
this
.addChild(
this
.groundSprite, GROUND_Z);
var
action1 = cc.MoveTo.create(0.5, cc.p(halfGroundW / 2 - 120,
this
.groundSprite.getPositionY()));
var
action2 = cc.MoveTo.create(0, cc.p(halfGroundW / 2,
this
.groundSprite.getPositionY()));
var
action = cc.Sequence.create(action1, action2);
this
.groundSprite.runAction(cc.RepeatForever.create(action));
};
|
js可以使用proptotype来为类型添加行为,不理解的可以google一下。当然也可以跟init函数一样写在里面,像这样:
1
2
3
4
5
6
7
|
var
Helloworld = cc.Layer.extend({
init:
function
() {
},
initGround::
function
() {
}
);
|
这里为地面定义两个动作,因为地面图片宽度是840px,而游戏屏幕分辨率指定是720×1280,所以先让地面向左移动120px,再迅速回到原位置。
初始化小鸟动画:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
Helloworld.prototype.initBird =
function
() {
//cc.log("initBird");
var
animation = cc.AnimationCache.getInstance().getAnimation(
"FlyBirdAnimation"
)
if
(!animation) {
var
animFrames = [];
var
str =
""
;
var
birdFrameCount = 4;
for
(
var
i = 1; i < birdFrameCount; ++ i) {
str =
"bird"
+ i +
".png"
;
var
frame = cc.SpriteFrameCache.getInstance().getSpriteFrame(str);
animFrames.push(frame);
}
var
animation = cc.Animation.create(animFrames, 0.05);
cc.AnimationCache.getInstance().addAnimation(animation,
"FlyBirdAnimation"
);
}
this
.flyBird = cc.Sprite.createWithSpriteFrameName(res.fly_bird);
this
.flyBird.setAnchorPoint(cc.p(0.5, 0.5));
this
.flyBird.setPosition(
this
.winSize.width / 2,
this
.winSize.height / 2);
this
.addChild(
this
.flyBird, BIRD_Z);
var
actionFrame = cc.Animate.create(animation);
var
flyAction = cc.RepeatForever.create(actionFrame);
this
.flyBird.runAction(cc.RepeatForever.create(flyAction));
};
|
小鸟自身动画是一个帧动画,创建成功后添加到缓存中。
初始化ready界面,就是游戏开始的时候提示用户点击的画面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Helloworld.prototype.initReady =
function
() {
this
.readyLayer = cc.Layer.create();
var
logo = cc.Sprite.createWithSpriteFrameName(res.logo);
logo.setAnchorPoint(cc.p(0.5, 0.5));
logo.setPosition(
this
.winSize.width / 2,
this
.winSize.height - logo.getContentSize().height - 50);
this
.readyLayer.addChild(logo);
var
getReady = cc.Sprite.createWithSpriteFrameName(res.getReady);
getReady.setAnchorPoint(cc.p(0.5, 0.5));
getReady.setPosition(
this
.winSize.width / 2,
this
.winSize.height / 2 + getReady.getContentSize().height);
this
.readyLayer.addChild(getReady);
var
click = cc.Sprite.createWithSpriteFrameName(res.click);
click.setAnchorPoint(cc.p(0.5, 0.5));
click.setPosition(
this
.winSize.width / 2, getReady.getPositionY() - getReady.getContentSize().height / 2 - click.getContentSize().height / 2);
this
.readyLayer.addChild(click);
this
.addChild(
this
.readyLayer);
};
|
效果如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
Helloworld.prototype.runBirdAction =
function
() {
var
riseHeight = 50;
var
birdX =
this
.flyBird.getPositionX();
var
birdY =
this
.flyBird.getPositionY();
var
bottomY =
this
.groundSprite.getContentSize().height -
this
.flyBird.getContentSize().height / 2;
var
actionFrame = cc.Animate.create(cc.AnimationCache.getInstance().getAnimation(
"FlyBirdAnimation"
));
var
flyAction = cc.RepeatForever.create(actionFrame);
//上升动画
var
riseMoveAction = cc.MoveTo.create(0.2, cc.p(birdX, birdY + riseHeight));
var
riseRotateAction = cc.RotateTo.create(0, -30);
var
riseAction = cc.Spawn.create(riseMoveAction, riseRotateAction);
//下落动画
//模拟自由落体运动
var
fallMoveAction = FreeFall.create(birdY - bottomY);
var
fallRotateAction =cc.RotateTo.create(0, 30);
var
fallAction = cc.Spawn.create(fallMoveAction, fallRotateAction);
this
.flyBird.stopAllActions();
this
.flyBird.runAction(flyAction);
this
.flyBird.runAction(cc.Spawn.create(
cc.Sequence.create(riseAction, fallAction) )
);
};
|
这里自定义了一个自由落体的Action:FreeFall:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
var
FreeFall = cc.ActionInterval.extend( {
timeElasped:0,
m_positionDeltaY:
null
,
m_startPosition:
null
,
m_targetPosition:
null
,
ctor:
function
() {
cc.ActionInterval.prototype.ctor.call(
this
);
this
.yOffsetElasped = 0;
this
.timeElasped = 0;
this
.m_positionDeltaY = 0;
this
.m_startPosition = cc.p(0, 0);
this
.m_targetPosition = cc.p(0, 0);
},
initWithDuration:
function
(duration) {
if
(cc.ActionInterval.prototype.initWithDuration.call(
this
, duration)) {
return
true
;
}
return
false
;
},
initWithOffset:
function
(deltaPosition) {
var
dropTime = Math.sqrt(2.0*Math.abs(deltaPosition)/k_Acceleration) * 0.1;
//cc.log("dropTime=" + dropTime);
if
(
this
.initWithDuration(dropTime))
{
this
.m_positionDeltaY = deltaPosition;
return
true
;
}
//cc.log("dropTime =" + dropTime + "; deltaPosition=" + deltaPosition);
return
false
;
},
isDone:
function
() {
if
(
this
.m_targetPosition.y >=
this
._target.getPositionY()) {
return
true
;
}
return
false
;
},
//Node的runAction函数会调用ActionManager的addAction函数,在ActionManager的addAction函数中会调用Action的startWithTarget,然后在Action类的startWithTarget函数中设置_target的值。
startWithTarget:
function
(target) {
//cc.log("startWithTarget target=" + target);
cc.ActionInterval.prototype.startWithTarget.call(
this
, target);
this
.m_startPosition = target.getPosition();
this
.m_targetPosition = cc.p(
this
.m_startPosition.x,
this
.m_startPosition.y -
this
.m_positionDeltaY);
},
update:
function
(dt) {
this
.timeElasped += dt;
//cc.log("isdone=" + this.timeElasped);
if
(
this
._target && !(
this
.m_targetPosition.y >=
this
._target.getPositionY())) {
var
yMoveOffset = 0.5 * k_Acceleration *
this
.timeElasped *
this
.timeElasped * 0.3;
if
(cc.ENABLE_STACKABLE_ACTIONS) {
var
newPos = cc.p(
this
.m_startPosition.x,
this
.m_startPosition.y - yMoveOffset);
if
(
this
.m_targetPosition.y > newPos.y) {
newPos.y =
this
.m_targetPosition.y;
this
._target.stopAction(
this
);
}
this
._target.setPosition(newPos);
}
else
{
this
._target.setPosition(cc.p(
this
.m_startPosition.x,
this
.m_startPosition.y +
this
.m_positionDeltaY * dt));
}
}
}
});
FreeFall.create =
function
(deltaPosition) {
var
ff =
new
FreeFall();
ff.initWithOffset(deltaPosition);
return
ff;
};
|
模仿了CCActionInterval.js中的其他内置的Action,如MoveBy,主要重写了initWithDuration,startWithTarget,update,isDone函数。 initWithDuration是设置该action运行的时间,时间的长短决定下降的速度。 startWithTarget函数由ActionManager调用,设置_target的值。 update函数在ActionInterval的step函数中会调用,在这个函数中不断更新精灵的坐标,使用了自由落体计算位移的公式。 isDone函数设置了动作是否运行结束。 重力加速度和action运行的时间需要不断调试。
添加水管:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
function
getRandom(maxSize) {
return
Math.floor(Math.random() * maxSize) % maxSize;
}
Helloworld.prototype.addPipe =
function
() {
cc.log(
"addPipe"
);
var
ccSpriteDown = cc.Sprite.createWithSpriteFrameName(res.holdback1);
var
pipeHeight = ccSpriteDown.getContentSize().height;
var
pipeWidth = ccSpriteDown.getContentSize().width;
var
groundHeight =
this
.groundSprite.getContentSize().height;
//小鸟飞行区间高度
var
acrossHeight = 300;
var
downPipeHeight = 100 + getRandom(400);
// cc.log("downPipeHeight=" + downPipeHeight);
var
upPipeHeight =
this
.winSize.height - downPipeHeight - acrossHeight - groundHeight;
var
PipeX =
this
.winSize.width + pipeWidth / 2;
ccSpriteDown.setZOrder(1);
ccSpriteDown.setAnchorPoint(cc.p(0.5, 0.5));
ccSpriteDown.setPosition(cc.p(PipeX + pipeWidth / 2, groundHeight + pipeHeight / 2 - (pipeHeight - downPipeHeight)));
var
ccSpriteUp = cc.Sprite.createWithSpriteFrameName(res.holdback2);
ccSpriteUp.setZOrder(1);
ccSpriteUp.setAnchorPoint(cc.p(0.5, 0.5));
ccSpriteUp.setPosition(cc.p(PipeX + pipeWidth / 2,
this
.winSize.height + (pipeHeight- upPipeHeight) - pipeHeight / 2));
this
.addChild(ccSpriteDown, PIPE_Z);
this
.addChild(ccSpriteUp, PIPE_Z);
this
.PipeSpriteList.push(ccSpriteDown);
this
.PipeSpriteList.push(ccSpriteUp);
this
.score += 1;
};
|
分为上下两根水管,随机设置上下水管的高度,固定小鸟飞行区间的高度为300。然后把创建的水管放到数组中,同时每添加一排水管就增加一分。
添加碰撞检测函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
Helloworld.prototype.getRect =
function
(a) {
var
pos = a.getPosition();
var
content = a.getContentSize();
return
cc.rect(pos.x - content.width / 2, pos.y - content.height / 2, content.width, content.height);
};
Helloworld.prototype.collide =
function
(a, b) {
var
aRect =
this
.getRect(a);
var
bRect =
this
.getRect(b);
return
cc.rectIntersectsRect(aRect, bRect);
};
Helloworld.prototype.checkCollision =
function
() {
if
(
this
.collide(
this
.flyBird,
this
.groundSprite)) {
//cc.log("hit floor");
this
.birdFallAction();
return
;
}
for
(
var
i = 0; i <
this
.PipeSpriteList.length; i++) {
var
pipe =
this
.PipeSpriteList[i];
if
(
this
.collide(
this
.flyBird, pipe)) {
cc.log(
"hit pipe i="
+ i);
this
.birdFallAction();
break
;
}
}
}
|
采用最简单的方式:判断矩形是否相交。把小鸟分别跟地面和数组中的水管进行检测,如果发生碰撞,则执行小鸟死亡动画:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
Helloworld.prototype.birdFallAction =
function
() {
this
.gameMode = OVER;
this
.flyBird.stopAllActions();
this
.groundSprite.stopAllActions();
var
birdX =
this
.flyBird.getPositionX();
var
birdY =
this
.flyBird.getPositionY();
var
bottomY =
this
.groundSprite.getContentSize().height +
this
.flyBird.getContentSize().width / 2;
var
fallMoveAction = FreeFall.create(birdY - bottomY);
var
fallRotateAction =cc.RotateTo.create(0, 90);
var
fallAction = cc.Spawn.create(fallMoveAction, fallRotateAction);
this
.flyBird.runAction(cc.Sequence.create(cc.DelayTime.create(0.1),
fallAction)
);
this
.runAction(cc.Sequence.create(cc.DelayTime.create(1.0),
cc.CallFunc.create(
this
.showGameOver,
this
))
);
}
|
让小鸟旋转90度,然后垂直下落,然后显示game over画面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
|
Helloworld.prototype.showGameOver =
function
() {
var
userDefault = cc.UserDefault.getInstance();
var
oldScore = userDefault.getIntegerForKey(
"score"
);
var
maxScore = 0;
if
(
this
.score > oldScore) {
maxScore =
this
.score;
userDefault.setIntegerForKey(
"score"
, maxScore);
}
else
{
maxScore = oldScore;
}
var
gameOverLayer = cc.Layer.create();
cc.log(
"gameover="
+ res.gameover);
var
gameOver = cc.Sprite.createWithSpriteFrameName(res.gameover);
gameOver.setAnchorPoint(cc.p(0.5, 0.5));
gameOver.setPosition(
this
.winSize.width / 2,
this
.winSize.height - gameOver.getContentSize().height / 2 - 150);
gameOverLayer.addChild(gameOver);
var
scorePanel = cc.Sprite.createWithSpriteFrameName(res.scorePanel);
scorePanel.setAnchorPoint(cc.p(0.5, 0.5));
scorePanel.setPosition(gameOver.getPositionX(), gameOver.getPositionY() - gameOver.getContentSize().height / 2 - scorePanel.getContentSize().height / 2 - 60);
gameOverLayer.addChild(scorePanel);
if
(
this
.score > oldScore) {
var
gold = cc.Sprite.createWithSpriteFrameName(res.gold);
gold.setAnchorPoint(cc.p(0.5, 0.5));
gold.setPosition(68 + gold.getContentSize().width / 2, 72 + gold.getContentSize().height / 2);
scorePanel.addChild(gold);
}
else
{
var
gray = cc.Sprite.createWithSpriteFrameName(res.gray);
gray.setAnchorPoint(cc.p(0.5, 0.5));
gray.setPosition(68 + gray.getContentSize().width / 2, 72 + gray.getContentSize().height / 2);
scorePanel.addChild(gray);
}
var
newScoreLabel = cc.LabelAtlas.create(
this
.score, res.number, 22, 28,
'0'
);
newScoreLabel.setAnchorPoint(cc.p(0.5, 0.5));
newScoreLabel.setScale(1.2);
newScoreLabel.setPosition(scorePanel.getContentSize().width - newScoreLabel.getContentSize().width - 90, newScoreLabel.getContentSize().height / 2 + 180);
scorePanel.addChild(newScoreLabel);
var
maxScoreLabel = cc.LabelAtlas.create(maxScore, res.number, 22, 28,
'0'
);
maxScoreLabel.setAnchorPoint(cc.p(0.5, 0.5));
maxScoreLabel.setScale(1.2);
maxScoreLabel.setPosition(newScoreLabel.getPositionX(), maxScoreLabel.getContentSize().height / 2 + 75);
scorePanel.addChild(maxScoreLabel);
var
start = cc.Sprite.createWithSpriteFrameName(res.start);
var
startMenuItem = cc.MenuItemSprite.create(start,
null
,
null
,
this
.restartGame,
this
);
var
startMenu = cc.Menu.create(startMenuItem);
startMenu.setAnchorPoint(cc.p(0.5, 0.5));
startMenu.setPosition(
this
.winSize.width / 2 , scorePanel.getPositionY() - scorePanel.getContentSize().height / 2 - start.getContentSize().height / 2 - 60);
gameOverLayer.addChild(startMenu);
this
.addChild(gameOverLayer, GAMEOVER_Z);
};
|
显示game over时保存游戏数据,显示这局游戏的分数和历史最高分。
1
2
3
4
5
|
Helloworld.prototype.restartGame =
function
() {
var
scene = cc.Scene.create();
scene.addChild(Helloworld.create());
cc.Director.getInstance().replaceScene(cc.TransitionFade.create(1.2, scene));
};
|
记得在init函数中清空水管数组:
1
|
this
.PipeSpriteList = [];
|
下面是Helloworld类的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
var
Helloworld = cc.Layer.extend({
gameMode:
null
,
bgSprite:
null
,
groundSprite:
null
,
flyBird:
null
,
PipeSpriteList:[],
passTime: 0,
winSize: 0,
screenRect:
null
,
readyLayer:
null
,
score: 0,
scoreLabel:
null
,
init:
function
() {
cc.log(
"helloworld init"
);
this
._super();
this
.PipeSpriteList = [];
this
.winSize = cc.Director.getInstance().getWinSize();
cc.SpriteFrameCache.getInstance().addSpriteFrames(res.flappy_packer);
this
.bgSprite = cc.Sprite.create(res.bg);
this
.bgSprite.setPosition(
this
.winSize.width / 2,
this
.winSize.height / 2);
this
.addChild(
this
.bgSprite, 0);
this
.initGround();
this
.initReady();
this
.screenRect = cc.rect(0, 0,
this
.winSize.width,
this
.winSize.height);
this
.gameMode = READY;
this
.score = 0;
this
.scheduleUpdate();
this
.setTouchEnabled(
true
);
return
true
;
},
onTouchesBegan:
function
(touches, event) {
},
onTouchesMoved:
function
(touches, event) {
},
onTouchesEnded:
function
(touches, event) {
if
(
this
.gameMode == OVER) {
return
;
}
if
(
this
.gameMode == READY) {
this
.gameMode = START;
this
.readyLayer.setVisible(
false
);
this
.initBird();;
}
this
.runBirdAction();
},
onTouchesCancelled:
function
(touches, event) {
},
update:
function
(dt) {
if
(
this
.gameMode != START) {
return
;
}
for
(
var
i = 0; i <
this
.PipeSpriteList.length; ++ i) {
var
pipe =
this
.PipeSpriteList[i];
pipe.setPositionX(pipe.getPositionX() - 3);
if
(pipe.getPositionX() < -pipe.getContentSize().width / 2) {
this
.PipeSpriteList.splice(i, 1);
//cc.log("delete pipe i=" + i);
}
}
this
.passTime += 1;
if
(
this
.passTime >=
this
.winSize.width / 6) {
this
.addPipe();
this
.passTime = 0;
}
this
.checkCollision();
}
});
|
在update函数中更新水管的位置,如果水管出了左边的屏幕就从数组中移除,每经过一定的时间就添加一排水管。 现在看看index.html的内容,在浏览器中访问的就是它:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
<!DOCTYPE HTML>
<
html
>
<
head
>
<
meta
charset
=
"utf-8"
>
<
title
>Flappy Bird-codingnow.cn</
title
>
<
link
rel
=
"icon"
type
=
"image/png"
href
=
"http://codingnow.cn/favicon.ico"
>
<
meta
name
=
"viewport"
content
=
"user-scalable=no"
/>
<
meta
name
=
"screen-orientation"
content
=
"portrait"
/>
<
meta
name
=
"apple-mobile-web-app-capable"
content
=
"yes"
/>
<
meta
name
=
"full-screen"
content
=
"yes"
/>
<
meta
name
=
"x5-fullscreen"
content
=
"true"
/>
<
style
>
body, canvas, div {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
-khtml-user-select: none;
-webkit-tap-highlight-color: rgba(0, 0, 0, 0);
}
</
style
>
</
head
>
<
body
style
=
"padding:0; margin: 0;text-align: center;background: #f2f6f8;"
>
<
canvas
id
=
"gameCanvas"
width
=
"720"
height
=
"1280"
></
canvas
>
<
script
src
=
"cocos2d.js"
></
script
>
<
img
style
=
"position:absolute;left:-9999px"
src
=
"http://zhoujianghai.github.io/games/flappybird/res/icon_wechat.png"
onerror
=
"this.parentNode.removeChild(this)"
>
</
body
>
</
html
>
|
在这个文件中指定了canvas的尺寸。html的内容从cocos2d-html5自带的例子中copy过来的,这里在底部添加了一个img,这个图片是分享到微信朋友圈时显示在左边的图片。 当浏览器窗口大小改变时,为了能自动调整显示游戏完整画面,需要在main.js的applicationDidFinishLaunching函数中添加:
1
2
3
|
cc.EGLView.getInstance().adjustViewPort(
true
);
cc.EGLView.getInstance().setDesignResolutionSize(720, 1280, cc.RESOLUTION_POLICY.SHOW_ALL);
cc.EGLView.getInstance().resizeWithBrowserSize(
true
);
|
还记得那个build.xml文件么,可以使用ant打包工具,把src目录下的js代码跟引擎代码打包成一个myApp-HelloWorld.js文件,这样我们只需要把res资源、cocos2d.js、index.html、myApp-HelloWorld.js放到网站上就可以了,也就更安全了。切换到项目build.xml所在目录,在命令符窗口执行:ant。很快就会生成myApp-HelloWorld.js文件,然后还需要修改cocos2d.js的window.addEventListener函数,修改s.src的值为myApp-HelloWorld.js,cocos2d.js文件里有详细注释的。
ok,flappy bird游戏的主要代码就完成了,感觉cocos2d-html5还是非常强大的,开发效率很高。 现在还只能在本地运行游戏,为了能在外网访问,可以把项目传到github上,创建github pages,可以参考:http://pages.github.com/