一、前言

笔者闲来无事,某天github闲逛,看到了游戏引擎的专题,引起了自己的兴趣,于是就自己捣腾了一下Cocos2dx-JS。
由于是学习,所谓纸上得来终觉浅,只是看文档看sample看demo,并不会让自己有多大的提升,于是一开始就计划写一个小游戏,以作为自己完成这个阶段学习的一个标志,也算是目标导向吧。

百牛信息技术bainiu.ltd整理发布于博客园
完整源码移步Github: https://github.com/RogerKang/JasonAmbition
Online Demo: http://www.rogerkang.site:3000 (由于需要WebGL的支持,请使用较新版本的Chrome或者Firefox打开)
本游戏的图片素材以及特效素材来自腾讯的《全民飞机大战》,仅供学习参考,请勿用于其他用途。
笔者水平有限,本身也不是专业游戏开发者,如有错漏,还请多多指教。

二、Cocos2dx-JS

Cocos2dx的介绍请移步官网: http://www.cocos2d-x.org/
目前Cocos2dx支持3种开发语言:C++、Lua、 JavaScript,其中C++和Lua是使用较为广泛开发语言,而JS的支持最早是作为独立的项目进行开发的,在3.7之后并入了Cocos2d-x的主repository,成为了官方支持的正式开发语言。
使用JS开发Cocos2dx游戏的优点:

  1. 跨平台,是唯一可以到达web端的分支,同时也支持编译成iOS和Android原生APP
  2. 开发效率高,开发难度相对较低

缺点:

  1. 运行效率相对较低
  2. JS-binding在功能和文档以及开发者的使用经验上仍然落后于C++和Lua

阅读本文之前,你最好具备以下知识基础:

  • 基本的JavaScript技能
  • Cocos2dx-JS的基本知识

三、游戏简介

游戏菜单

游戏过程

这个游戏是一个简单的飞行射击游戏,主要使用鼠标点击页面(移动设备就是手指点击屏幕)控制飞机的飞行,从而实现攻击敌人,回避攻击,获取增强效果物品等常见的行为。
具体细节的可以到Online Demo去看一看。

四、整体设计

1. Game Scene

游戏总共有3个scene;Menu、About、Game,可以理解为不同的功能放在不同的场合之中,其中最重要的是菜单(Menu)场景和游戏(Game)场景,又以游戏场景最为核心,当我们控制飞机进行游戏的时候,实际上就是在于游戏场景在进行交互,此时我们看到的飞机,敌人,子弹,特效,背景等等一切,都是游戏场景在承载。

2. Game Layer

Layer的层次比Scene低一级,通常是一个scene会包含一个或者多个layer(当然也可以一个都没有)。
通常不同的内容可以放在不同的layer中,比如游戏场景里面,就使用了一个独立的layer来显示飞机的血量和得分。
本游戏只有3个layer,GameLayer,GameInfo 和 GameOverLayer,顾名思义,一个作为游戏场景的承载,一个显示飞机的血量和得分,一个在飞机死亡的时候显示相关的信息。

3. Game Object

使用了面向对象的设计来组织游戏的各个对象:
Plane:玩家控制的飞机
Enemy:敌人的基类,提供了公共行为接口,包括回收与重用的接口
Bullet:子弹的基类,提供了公共行为接口,包括回收与重用的接口
./Enemies:扩展Enemy基类,不同的敌人有自己的行为
./Bullets:扩展Bullet基类,不同的子弹的效果自己定义
./Boss:扩展Enemy基类,实际上就是特殊的敌人作为Boss登场

4. Manager

设计了若干个manager来组织游戏的各个方面:
BulletManager:每帧更新子弹的位置,检测碰撞和伤害,回收超出屏幕的子弹
EnemyManager:控制敌人和Boss的产生,检测敌人与飞机的碰撞,控制敌人的攻击,回收敌人
ItemManager:控制游戏中随机增强item的产生,并应用其效果
MusicManager:音效的控制
ParticleManager:特效的控制,包括被攻击的特效,item的特效,Boss特殊攻击的特效
StageManger:控制游戏阶段,比如常规阶段,boss阶段等等

五、启动游戏

启动游戏,我们需要设置game stage的配置以及初始化各个manager,并且以Menu Scene作为第一个scene:

var defaultStage = [{name:"normal", duration: 20}, {name:"mega", duration: 20}, {name:"boss", duration:100000}]; //game stage配置

cc.game.onStart = function(){
cc.view.adjustViewPort(false);
cc.view.setDesignResolutionSize(512, 768, cc.ResolutionPolicy.SHOW_ALL); // 设置分辨率
cc.view.resizeWithBrowserSize(true);
//load resources
cc.LoaderScene.preload(g_resources, function () {
cc.game.musicManager = new MusicManager(); //初始化各个manager
cc.game.bulletManager = new BulletManager();
cc.game.enemyManager = new EnemyManager();
cc.game.itemManager = new ItemManager();
cc.game.particleManager = new ParticleManager();
cc.game.stageManager = new StageManager(defaultStage);
cc.director.runScene(new MenuScene()); //首先启动Menu Scene
cc.director.setDisplayStats(false);
}, this);
};
cc.game.run(); //运行游戏 var trace = function() {
//cc.log(Array.prototype.join.call(arguments, ", "));
};

六、Menu Scene

Menu场景只需要一个背景图片,一段BGM,以及一个按钮开始游戏即可

var MenuScene = cc.Scene.extend({
ctor:function(){
this._super();
var layer = new cc.Layer();
this.addChild(layer);
var winSize = cc.director.getWinSize();
cc.game.winSize = winSize;
var bgImage = new cc.Sprite("res/bg.png"); //背景图片
bgImage.x=winSize.width/2;
bgImage.y = winSize.height/2;
layer.addChild(bgImage);
var gameName = new cc.LabelTTF("Jason's Ambition", "Arial",58, cc.Size(500, 200), cc.TEXT_ALIGNMENT_CENTER, cc.VERTICAL_TEXT_ALIGNMENT_TOP); //标题文本
gameName.x = winSize.width/2;
gameName.y = winSize.height+100;
var gameNameAction = cc.moveTo(3, cc.p(winSize.width/2, winSize.height-200)); //让标题动起来
gameName.runAction(gameNameAction);
layer.addChild(gameName);
this.scheduleOnce(function(){
this.showMenu();
}.bind(this),3); //3秒之后显示按钮
cc.game.musicManager.playStarWar(); //播放BGM
},
showMenu:function(){
var startGameButton = new cc.MenuItemImage("res/startgame2.png", "res/startgame2.png", this.startGame, this); //创建按钮并绑定回调事件
var menu = new cc.Menu(startGameButton);
this.addChild(menu);
},
startGame: function(){
trace('Game start..');
cc.director.runScene(new AboutScene()); //点击按钮后跳到About Scene
}
});

在这个场景里面,我们加载了背景图片,播放了BGM,设置了会移动的标题,并在标题停止移动后显示开始按钮,在按钮上绑定回调事件,点击后跳转到About Scene。

七、About Scene

About Scene只是一段文本,3秒后跳转到游戏场景。

var AboutScene = cc.Scene.extend({
ctor:function(){
this._super();
var winSize = cc.director.getWinSize();
var layer = new cc.Layer();
this.addChild(layer);
var aboutText = "Jason, a shameless man.\nHe want to kidnap all girls around the earth.\n";
aboutText += "Hero, you have to stop him!";
var aboutLabel = new cc.LabelTTF(aboutText, "Arial",24, cc.Size(500, 200), cc.TEXT_ALIGNMENT_CENTER, cc.VERTICAL_TEXT_ALIGNMENT_TOP);
aboutLabel.x = winSize.width/2;
aboutLabel.y = winSize.height-300;
layer.addChild(aboutLabel);
this.scheduleOnce(function(){
var gameScene = new GameScene();
cc.director.runScene(gameScene); //启动游戏场景,游戏开始
cc.game.gameScene = gameScene;
},3);
}
});

八、Game Scene

Game Scene的设计是最为复杂的,因为是游戏交互的场所,所以其之上需要处理的事情也最多。
我们需要一点一点说。
GameScene的执行流程:(简书不支持流程图,有兴趣的同学可以把下面的代码复制到支持flowchart流程图的Markdown编辑器里面查看,注意使用flow代码段包裹,例如https://maxiang.io)

st=>start: Start Game Scene
end=>end: End Game
addBackGround=>operation: Add scrolling background
addPlane=>operation: Add plane
listenClick=>operation: Listen to click operation
continueGame=>condition: Plane dead or Boss dead?
addEnemy=>operation: Add new enemies
enemyAttack=>operation: Enemy attack
bulletReuse=>operation: Reuse out bullets
bulletHit=>operation: Check collision between planes and bullets
bulletUpdate=>operation: Update bullets' action
enenmyCollide=>operation: Check the collision between enemies and plane
enemyOut=>operation: Reuse outing enemies
updateInfo=>operation: Update game information
genenrateItem=>operation: Generate items
itemCollide=>operation: Check the collision between items and plane
allyEffect=>operation: Apply items' effect to plane
planeBlood=>condition: Plane's dead?
gameOver=>operation: Game Over st->addBackGround->addPlane->listenClick->continueGame
continueGame(yes)->addEnemy->enemyAttack->bulletReuse->bulletHit->bulletUpdate->enenmyCollide->enemyOut
enemyOut->updateInfo->genenrateItem->itemCollide->allyEffect->planeBlood
planeBlood(yes)->gameOver
planeBlood(no)->continueGame
continueGame(no)->gameOver
gameOver->end

1. 帧

首先解释帧的概念。
对于一个游戏而言,连贯的画面其实是有快速切换的帧造成的,而每一帧之间会有细小的变化,连续播放帧就会形成动作。而帧之间的变化,就是我们的代码需要计算和处理的。
简而言之,如果你需要一个物体移动到某个地方,那你的代码就应该在每一帧里面对他的坐标进行一些改变,从而实现移动这个效果(当然,任何一个优秀的游戏引擎,这种基本的动画是不需要我们自己去做帧间的处理的,我们只需要调用一次,这个效果就会在它所进行的时间内在每帧进行更新)。

在Scene的ctor函数里面调用this.scheduleUpdate 方法,就会使用默认的update方法去进行帧间计算,我们在update方法里面需要做的事情:

  • 更新飞机的位置
  • 更新敌人的位置
  • 更新子弹的位置
  • 更新item的位置
  • 判断飞机与敌人、子弹、item的碰撞,并应用效果
  • 等等

其中,实际上我们只需要真的去在每一帧都处理的只有“更新子弹的位置” 以及 “判断飞机与敌人、子弹、item的碰撞,并应用效果”其余的都只需要在他们真的需要更新的时候声明一次action,之后的每帧,cocos都会帮我们去移动,而不需要我们再去计算改怎么移动。
如果说“判断飞机与敌人、子弹、item的碰撞,并应用效果”的处理是理所当然的,那么为什么“更新子弹的位置”不能交给cocos去处理?事实上大多数的子弹的位置也都是有cocos去应用action,我们只需要在子弹发生的时候告诉cocos接下来怎么去移动子弹就可以了,但是有例外的情况:追踪弹。它的移动和敌人相关,因此在敌人的位置、存活发生变化的时候可能需要改变它的移动,这个时候就需要我们在帧间进行处理:计算出新的移动方向,告诉cocos取消之前的action,应用新的action。

update函数的执行时间与游戏的帧率是息息相关的,因为游戏的每一帧都是根据这个函数的计算结果来产生的,如果它的执行时间太长,就会导致帧率下降。
举个例子,如果update的执行时间为0.1秒,那么这个游戏最高也只可能是10fps,因为在1秒内它最多执行10次,给出10次的结果,从而cocos也只能由此渲染10帧的画面。

总而言之,如果你想实现什么效果,那就在update function里面计算这个效果在每一帧里面的作用,然后让cocos去把帧渲染出来,联合起来就可以实现你要的效果。

2. 游戏背景

首先,作为一个飞行射击游戏,我们需要一个可以滚动的背景。自然,准备一张高度足够的图片,让它从上往下匀速下移是可以实现这个效果的,但是实际上应该不会有人会这样做。
在这里,实际上我们只需要一张图片,这张图片刚好可以覆盖整个游戏的可是窗口就可以了:

游戏背景图

如果你仔细观察,你会发现,这张图片的上面和下面是可以接合的,也就是说,如果你在这张图片的正上方再放一张同样的图片,你会发现这两张图片的景色、河流都是刚好接合起来,看不出是两种图片,而像是一张图片。
事实上,我们正式利用这样的图片来实现背景的无限滚动。
我们只需要在一开始就放一张图片铺满显示窗口,然后在窗口的正上方再放一张一样的图片,让它们同时匀速往下移动,等到第一张图片完全离开窗口,而第二种刚好铺满窗口的时候,在下一帧恢复它们的初始位置,不断重复这个过程,那么看起来背景就是在不断的移动。

 _scrollBackground:function(){

 this.bgImage1.y -= Math.ceil(this.winSize.height*0.001);    //下移两张图片的位置
this.bgImage2.y -= Math.ceil(this.winSize.height*0.001); if(this.bgImage1.y<-(this.winSize.height/2)){ //当第一张图片的y小于负的二分之一屏幕高度,就是它刚好离开视窗的时候,此时恢复两张图片的初始位置 this.bgImage1.y = this.winSize.height/2; //第一张置于中间
this.bgImage2.y = this.winSize.height/2+this.winSize.height; //第二张放着第一张的上面 } }

3. 添加/移动 飞机

飞机本身只是一个Sprite:

var Plane = cc.Sprite.extend({
image:null,
bullet:[],
status:null,
blood:5000,
layer:null,
gameScene:null,
shotInterval:200,
allowShot:true,
allowMissile:true,
allowUltra:true,
hitEffect:null,
addedEffects:[],
laserHit:true,
ctor:function(gameScene, layer){
this._super("res/main_plane.png"); //初始化飞机的图片
this.gameScene = gameScene;
this.layer = layer;
this.addedEffects = [];
this.allowShot = true;
this.allowMissile = true;
this.allowUltra = true;
this.laserHit = true;
}
}

在Game Scene中添加飞机:

 var plane = new Plane(this, layer);
plane.scale = 0.5;
this.targetX = plane.x = winSize.width/2;
this.targetY = plane.y = winSize.height/2;
layer.addChild(plane,7); //给game scene的layer添加飞机
this.plane = plane;

这样在game scene上就会出现飞机的图片,但是仅仅只是这样是不够的,我们需要这个飞机可以根据我们的点击进行移动,原理也很简单,就是获取点击的坐标,然后让飞机移动过去,这里我们需要监听两个事件:

  • Touch
  • MouseDown

在game scene里监听事件:

 if("touches" in cc.sys.capabilities){
cc.eventManager.addListener({
event: cc.EventListener.TOUCH_ONE_BY_ONE,
onTouchBegan: this._onTouchBegan.bind(this) //处理触摸的回调
}, this);
} else {
cc.eventManager.addListener({
event: cc.EventListener.MOUSE,
onMouseDown: this._onMouseDown.bind(this) //处理鼠标点击的回调
}, this);
}

在回调里使用点击的坐标让飞机移动:

 _onTouchBegan:function(touch, event){
var clickX = touch.getLocation().x;
var clickY = touch.getLocation().y;
this.targetX = clickX;
this.targetY = clickY;
var speedo = Math.sqrt(Math.pow(clickX-this.plane.x,2)+Math.pow(clickY-this.plane.y,2))/300;
var moveAction = cc.moveTo(speedo, cc.p(clickX, clickY));
if(this.action!=null)
this.plane.stopAllActions();
this.plane.runAction(moveAction);
this.action = moveAction;
},
_onMouseDown: function(event){
var clickX = event.getLocationX();
var clickY = event.getLocationY();
this.targetX = clickX;
this.targetY = clickY;
var speedo = Math.sqrt(Math.pow(clickX-this.plane.x,2)+Math.pow(clickY-this.plane.y,2))/300;
var moveAction = cc.moveTo(speedo, cc.p(clickX, clickY));
if(this.action!=null)
this.plane.stopAllActions();
this.plane.runAction(moveAction);
this.action = moveAction;
}

这样,我们点哪里,飞机就会移动到哪里。
但是此时的飞机只能够移动,并不能攻击。

4. 添加/管理敌人

考虑到敌人可能有多种多样,每种敌人会有自己的子弹、运动轨迹,所以我们先定义一个基类,里面定义了敌人的通用接口,具体的实现在每个敌人的之类里面完成。

var Enemy = cc.Sprite.extend({
bullets:null,
scene:null,
layer:null,
score:null,
blood:null,
explosionHarm:null,
//ctor:function(image){
//
// this._super(image);
//
//
//},
appear:function(){}, //敌人出场
attack:function(){}, //攻击
explode:function(){}, //爆炸
hurt:function(){}, //被攻击受到伤害
move:function(){}, //敌人移动
reuse:function(scene, layer){ //reuse和unuse方法用于敌人的缓存回收,这部分将独立说明
trace("reuse enemy:");
this.ctor(scene, layer);
},
unuse:function(){
this.layer.removeChild(this);
}
});

Alpha(一个Enemy的子类,是一个具体的敌人)的部分实现:

var Alpha = Enemy.extend({
image:"res/enemy/alpha.png",
allowShot:true,
blood:null,
score:100,
explosionHarm:100,
ctor:function(scene, layer){ //初始化Alpha,并调用appear方法,使得敌人在游戏场景中登场
this.allowShot = true;
this.scene = scene;
this.gameScene = this.scene;
this.layer = layer;
this._super(this.image);
this.blood = 50;
this.threshold = 50;
this.appear();
layer.removeChild(this);
if(scene.enemies.indexOf(this)!=-1)
scene.enemies.splice(scene.enemies.indexOf(this), 1);
layer.addChild(this, 2);
scene.enemies.push(this); //用一个数组存储所有的敌人,在之后的攻击判定、清理回收等会用到
},
appear:function(){ //登场,在可是窗口的上方的一个随机位置里面出现
this.scale = 0.5;
this.x = Math.round(Math.random()*cc.game.winSize.width);
this.y = cc.game.winSize.height+100;
this.move(); //登场后移动,移动到可视窗口下方的某个随机位置 },
move:function(){
var moveAction = cc.moveTo(12, cc.p(Math.random()*cc.game.winSize.width,-400));
this.runAction(moveAction); //移动敌人
}
}

这样,如果我们创建一个Alpha的实例,那么它就会出现在游戏场景,并且从游戏窗口的上方的某个位置匀速地向下移动。

EnemyManager

为了更好地管理敌人的生成和回收,我们创建了一个enemyManager来管理敌人的添加、移除回收、碰撞检测:

var EnemyManager = cc.Class.extend({
allowAddNewEnemy: true,
privateSprite:null,
enemyList:[
Alpha,
Beta,
Gamma
],
enemyFrequency:1,
ctor: function () {
this.privateSprite = new cc.Sprite();
this.allowAddNewEnemy = true;
},
addEnemy: function (scene, layer, frequency) { //负责添加新敌人
if (this.allowAddNewEnemy == false)
return;
//new Alpha(scene, layer);
//trace("New enemy");
this.allowAddNewEnemy = false; var enemyType = this.enemyList[new Date().getTime() % this.enemyList.length];
if(cc.pool.hasObject(enemyType)){
cc.pool.getFromPool(enemyType, scene, layer);
trace("reuse enemy");
}else{
new enemyType(scene, layer);
}
scene.scheduleOnce(function () {
trace("allow new enemy");
this.allowAddNewEnemy = true;
}
.bind(this),this.enemyFrequency);
},
addBoss : function(scene, layer){ //添加BOSS
new Jason(scene, layer);
},
removeEnemy:function(scene, layer, enemy, index){ //从场景里面删除一个敌人
scene.score+=enemy.score;
layer.removeChild(enemy);
scene.enemies.splice(index, 1);
cc.pool.putInPool(enemy);
},
enemyAttack:function(scene){ //命令所有敌人进行一次攻击
for(var i =0;i<scene.enemies.length;i++){
scene.enemies[i].attack();
}
},
_collide:function(x1, y1, x2, y2, threshold){ //碰撞检测
if((Math.pow((x1-x2),2)+Math.pow((y1-y2),2))>Math.pow(threshold, 2))
return false;
else
return true;
},
collisionCheck:function(scene, layer, plane){ //如果敌人受到攻击,且血量低于0,让敌人爆炸,同时回收敌人
var enemies = scene.enemies;
for(var i = 0;i<enemies.length;i++){
var checkEnemy = enemies[i];
if(this._collide(checkEnemy.x, checkEnemy.y, plane.x, plane.y, checkEnemy.threshold) == true){
plane.blood -= checkEnemy.explosionHarm;
scene.score += checkEnemy.score;
checkEnemy.explode();
this.removeEnemy(scene, layer, checkEnemy, i);
i--;
}
}
},
outEnemyCheck : function(scene, layer){ //如果敌人飞出了游戏场景,也回收敌人
var enemies = scene.enemies;
for(var i = 0;i<enemies.length;i++){
var checkEnemy = enemies[i];
if(checkEnemy.x>-200 && checkEnemy.x<cc.game.winSize.width+200 && checkEnemy.y>-200 && cc.game.winSize.height+200)
;
else {
trace("unuse out ennemy");
this.removeEnemy(scene, layer, checkEnemy, i);
i--;
}
}
}
});

5. 子弹系统

飞机与敌人都可以发射子弹,我们需要解决的问题是:

  • 子弹的生成、发射(位置,角度)、速度、伤害,动画
  • 子弹与飞机/敌人的碰撞检测
  • 子弹的回收

Bullet基类

与Enemy类似地,基类只定义了接口以及共同的方法:

var Bullet = cc.Class.extend({
BulletOwner:null,
type:null,
plane:null,
layer:null,
action:null,
bullets:null,
scene:null,
ctor:function(type, plane, layer, scene){ //根据给出的type初始化bullet
this.type = type;
this.plane = plane;
this.layer = layer;
this.scene = scene;
this.bullets = [];
this._shot(); //初始化完毕后马上发射子弹,_shot方法应该在子弹之类里面具体实现
},
_validate:function(){ //检测子弹是否离开了可视窗口
var valid = false;
for(var i =0;i<this.bullets.length;i++){
var childBullet = this.bullets[i];
if(childBullet.x>=-20 && childBullet.x<=cc.game.winSize.width+20 && childBullet.y>=0 && childBullet.y<=cc.game.winSize.height+20){
valid = true;
break;
}else{
valid = false;
//break;
}
}
return valid;
},
reuse:function(type, plane, layer, scene){ //reuse和unuse用于回收、重新利用子弹
this.ctor(type, plane, layer, scene);
},
unuse:function(){
for(var i =0;i<this.bullets.length;i++){
this.bullets[i].stopAllActions();
this.layer.removeChild(this.bullets[i]);
}
},
_shot:function(){}
});

BasicBullet

BasicBullet是一个具体实现的子弹之类,设置了子弹的图片、数量、发射位置,实现了_shot方法:

var BasicBullet = Bullet.extend({
BulletType:{
"basic" : "res/bullet/bullet_basic.png" //子弹的图片
},
BulletLocation : {
"basic" : [{x:0, y:0}] //数组的元素数量代表子弹的数量,x和y坐标代表某个子弹的发射位置
},
BulletOwner:"enemy",
harm : 5, //子弹的伤害
_shot:function(){ //_shot方法的作用:创建子弹Sprite,添加到scene中,调用runBulletCustomAction使得子弹开始运动
for(var i =0;i<this.BulletLocation[this.type].length;i++){
var bulletLocation = this.BulletLocation[this.type][i];
var newBullet = new cc.Sprite(this.BulletType[this.type]);
newBullet.scale = 0.15;
newBullet.x = this.plane.x+bulletLocation.x;
newBullet.y = this.plane.y+bulletLocation.y;
newBullet.harm = this.harm;
this.runBulletCustomAction(newBullet);
this.layer.addChild(newBullet, 1);
this.bullets.push(newBullet);
}
this.layer.bullets.push(this);
},
runBulletCustomAction:function(newBullet){ //实现了每种子弹自己的运动方式,不同的子弹的不同行为在这里定义
var action = cc.moveTo(4, cc.p(newBullet.x, -(cc.director.getWinSize().height+300)));
var rotation = cc.rotateBy(10,3600, 3600);
newBullet.runAction(cc.spawn(action,rotation));
}
});

实际的子弹种类会有多种,每个子弹的飞行轨迹也都各有不同。BasicBullet的飞行轨迹从初始化就已经决定了,以后不再改变,直到发生碰撞或者离开窗口被回收。
但是也可以实现更复杂的飞行轨迹,比如跟踪弹(MissileBullet)就会跟踪离它最近的敌人的位置,这个子弹的运动就需要每帧更新):

MissileBullet

var MissileBullet = Bullet.extend({
updatePerFrames:true, //是否需要每帧更新,对于MissileBullet来说是true
allowSwitchEnemy:false, //是否允许在原追踪敌人毁灭后更换瞄准目标
/* ... */
aimedEnemy:function(bulletX, bulletY, oldX, oldY, bullet){ //瞄准敌人,每帧都会计算当前离追踪弹最接近的敌人是哪一个,在allowSwitchEnemy为true时会在敌人摧毁后重新计算,否则将会沿着之前的轨迹方向飞行
var enemyX = 0;
var enemyY = 0;
var foundEnemy = false;
if(this.allowSwitchEnemy == true || bullet.lockEnemy == false ) {
if (this.scene.enemies != null && this.scene.enemies.length > 0) {
var enemyList = this.scene.enemies;
var leastDistance = 1000000000;
var leastX = bulletX;
var leastY = cc.game.winSize.height + 200;
;
for (var i = 0; i < enemyList.length; i++) {
var checkEnemy = enemyList[i];
if (checkEnemy.x > 0 && checkEnemy.x < cc.game.winSize.width && checkEnemy.y > 0 && checkEnemy.y < cc.game.winSize.height)
;
else
continue;
var distance = Math.pow((bulletX - checkEnemy.x), 2) + Math.pow((bulletY - checkEnemy.y), 2);
if (distance < leastDistance) {
foundEnemy = true;
bullet.lockEnemy = true;
bullet.enemy = checkEnemy;
leastDistance = distance;
leastX = checkEnemy.x;
leastY = checkEnemy.y;
}
}
enemyX = leastX;
enemyY = leastY;
}
}else{
if(bullet.enemy != null && this.scene.enemies.indexOf(bullet.enemy) > -1 ){
foundEnemy = true;
enemyX = bullet.enemy.x;
enemyY = bullet.enemy.y;
}
}
if(foundEnemy != true){
var deltaX = bulletX - oldX;
var deltaY = bulletY - oldY;
var deltaD = Math.sqrt(Math.pow(deltaX,2)+Math.pow(deltaY,2));
enemyX = bulletX + this.speed*deltaX/deltaD;
enemyY = bulletY + this.speed*deltaY/deltaD;
}
var rotationAngle = Math.atan((enemyY - bulletY)/(enemyX - bulletX))*360/(2*Math.PI);
if((enemyX - bulletX)>=0)
;
else
rotationAngle += 180;
if(rotationAngle<0)
rotationAngle +=360;
rotationAngle = 90 - rotationAngle;
//trace((enemyY - bulletY)+","+(enemyX - bulletX)+","+rotationAngle);
//trace(enemyY+","+(bulletY)+","+(oldY));
var duration = Math.sqrt(Math.pow(enemyX - bulletX, 2) + Math.pow(enemyY - bulletY, 2))/this.speed;
//trace("bullet speed:"+duration);
return {x:enemyX, y:enemyY, duration:duration, rotationAngle:rotationAngle};
},
perFramesUpdate:function(scene, layer, plane, bullet){ //每帧更新,每一帧都根据瞄准的敌人的位置调整子弹的运动轨迹
if(bullet.initial == true) {
bullet.scheduleOnce(function(){
this.initial = false;
}.bind(bullet), 0.2);
return ;
}
//if(Math.abs(bullet.x - bullet.oldX)>10 || Math.abs(bullet.y - bullet.oldY)>10)
//if(bullet.moveAction!=null)
// bullet.moveAction.speed(10000);
//
if(bullet.recentStop == null || bullet.recentStop == false) {
bullet.stopAllActions();
bullet.recentStop = true;
bullet.scheduleOnce(function(){
this.recentStop = false;
}.bind(bullet), 0.02);
}
else
return ;
//if(bullet.moveAction!=null)
// trace("speed:"+bullet.moveAction.getSpeed());
//
//bullet.stopAllActions();
var enemyLocation = this.aimedEnemy(bullet.x, bullet.y, bullet.oldX, bullet.oldY, bullet);
bullet.oldX = bullet.x;
bullet.oldY = bullet.y;
//trace("now:"+bullet.x+","+bullet.y);
//trace(enemyLocation.x+","+enemyLocation.y);
var action = cc.moveTo(enemyLocation.duration, cc.p(enemyLocation.x, enemyLocation.y));
//var action = cc.moveTo(1, cc.p(bullet.x+1000, bullet.y+1000));
//action.easing(cc.easeIn(20));
//bullet.setRotationSkewX(enemyLocation.rotationAngle);
var rotationAction = cc.rotateTo(0.01,enemyLocation.rotationAngle);
//trace("angel:"+enemyLocation.rotationAngle);
bullet.runAction(cc.spawn(action, rotationAction));
bullet.moveAction = action;
}
})

发射子弹

在飞机和敌人上发射子弹,是通过调用plane._shot()和enemy.attack()来进行的,他们的实现其实是类似的:
飞机的_shot方法里有:

 if (this.allowShot) {

 //cc.game.musicManager.playEffect("bullet");
if (cc.pool.hasObject(DefaultBullet)) { //从回收池里取出DefaultBullet实例 cc.pool.getFromPool(DefaultBullet, "normal", this, this.layer, this.gameScene);
//trace("reuse bullets"); } else { //没有就new一个DefaultBullet的实例 new DefaultBullet("normal", this, this.layer, this.gameScene);
} this.allowShot = false;
this.scheduleOnce(function () { //每0.2秒设计一次
this.allowShot = true; }
.bind(this), 0.2); }

碰撞检测

飞机发射的子弹和敌人之间,以及敌人的子弹与飞机之间,需要进行碰撞检测,如果发射了碰撞,就需要回收子弹,计算伤害(并且根据血量摧毁敌人/飞机,同时显示特效等)。
碰撞检测的方法有很多,这里我们只使用了最简单的方法:计算子弹和机体的距离,就是根据两个目标的x轴和y轴运用勾股定理计算距离,当距离小于一定的阈值(threshold)时判定为发生碰撞。
碰撞一共有3种:

  1. 子弹与机体
  2. 敌人与飞机
  3. 物品与飞机

这3种碰撞分别由3个manager来执行检查:BulletManager,EnemManager,ItemManager。
以BulletManager的子弹与机体的碰撞检测为例:

 collisionCheck:function(scene, layer){          //分别进行飞机与敌人的子弹,飞机的子弹与敌人的碰撞检测,计算伤害,在血量为0时调用机体的explode方法,回收敌人,同时进行子弹回收
var playerX= scene.plane.x;
var playerY= scene.plane.y; var bullets = layer.bullets; for(var i =0;i<bullets.length;i++) { var checkBullet = bullets[i];
if (checkBullet.BulletOwner == "enemy") { for(var k =0;k<checkBullet.bullets.length;k++){ var childBullet = checkBullet.bullets[k]; if (this._collide(playerX, playerY, childBullet.x, childBullet.y, 50) == true) { layer.removeChild(childBullet);
checkBullet.bullets.splice(k,1);
k--; //var blinkAction = cc.blink(1,3);
//scene.plane.runAction(blinkAction);
scene.plane.showHitEffect(childBullet.harm); if(checkBullet.bullets.length==0){
cc.pool.putInPool(checkBullet);
layer.bullets.splice(i, 1);
i--; } } } }
} for(var s = 0;s<scene.enemies.length;s++){ //trace("s:"+s);
var checkEnemy = scene.enemies[s];
var enemyX = checkEnemy.x;
var enemyY = checkEnemy.y; for(var i =0;i<bullets.length;i++) { var checkBullet = bullets[i];
if (checkBullet.BulletOwner == "player") { for(var k =0;k<checkBullet.bullets.length;k++){
var childBullet = checkBullet.bullets[k];
var bulletX = childBullet.x;
var bulletY = childBullet.y; if (this._collide(enemyX, enemyY, childBullet.x, childBullet.y, checkEnemy.threshold) == true) { layer.removeChild(childBullet);
checkBullet.bullets.splice(k,1);
k--;
//var blinkAction = cc.blink(1,3);
//scene.plane.runAction(blinkAction);
checkEnemy.showHitEffect(childBullet.harm, {x:bulletX, y:bulletY}); if(checkBullet.bullets.length==0){
cc.pool.putInPool(checkBullet);
layer.bullets.splice(i, 1);
i--;
}
} } }
}
// place out of inside loop, otherwise will case s++ run multiple times!
trace(checkEnemy.blood);
if(checkEnemy.blood<=0){
checkEnemy.explode();
cc.game.enemyManager.removeEnemy(scene, layer, checkEnemy, s);
s--;
}
//check ennemy done
}
},
_collide:function(x1, y1, x2, y2, threshold){ //用勾股定理计算举例,对比threshold判定碰撞 if((Math.pow((x1-x2),2)+Math.pow((y1-y2),2))>Math.pow(threshold, 2))
return false;
else
return true;
}

子弹回收

碰撞了的子弹就会被回收,还有一种情况需要回收:没发生碰撞但是飞出了可视窗口。
这种情况在BulletManager里面进行了每帧检测和处理:

 manageBullet:function(layer){
if(layer.bullets.length>0)
for(var i=0;i<layer.bullets.length;i++){
var bullet = layer.bullets[i];
if(bullet._validate()==true)
;
else{
cc.pool.putInPool(bullet);
layer.bullets.splice(i,1);
i--;
}
} }

6. 物品(Item)系统

物品系统是飞行射击游戏一个常见的系统,具体来说就是可以控制飞机去吃一些随机生成的物品,从而获得一些额外的增强效果。
笔者同样给游戏添加了这个系统。

ItemManager

var ItemManager = cc.Class.extend({
scene:null,
layer:null,
plane:null,
items:{
missile:"res/items/missile.png", //物品以及对于图片
blood:"res/items/blood.png",
shield:"res/items/shield.png",
bigBang:"res/items/bigBang.png",
shine:"res/items/shine.png",
ultra:"res/items/ultra.png"
},
itemName:["missile", "blood", "shield", "bigBang", "shine", "ultra"], //物品名
basicGap: 2,
randomGap: 3,
allowAddItem:true,
floatingItems:[], privateSprite:null,
durableEffectFunc:[],
allowDurableEffect:true,
ctor:function(){ //ItemManager初始化
this.privateSprite = new cc.Sprite();
this.durableEffectFunc = []; // remove durable effect when re-init game
this.floatingItems = [];
this.allowDurableEffect = true;
this.allowAddItem = true;
}
}

添加物品

添加物品其实和添加敌人的做法是类似的,就是在一个随机的位置上添加一个物品,并让它飞行。与敌人不同之处在于,物品只可能与飞机发生碰撞,既不会与敌人也不会与子弹发生碰撞。

 generateItem:function(scene, layer, plane){

 if(this.allowAddItem == false)
return ; if(this.allowAddItem == true) {
var item = this.itemName[new Date().getTime() % this.itemName.length]; //随机产生物品
//item = this.itemName[1];
var itemSprite = new cc.Sprite(this.items[item]);
itemSprite.itemName = item;
itemSprite.scale = 0.3;
itemSprite.x = Math.round(Math.random() * cc.game.winSize.width);
itemSprite.y = cc.game.winSize.height + 100;
var moveAction = cc.moveTo(6, cc.p(Math.random() * cc.game.winSize.width, -400));
itemSprite.runAction(moveAction); //移动物品 layer.addChild(itemSprite, 5);
scene.items.push(itemSprite);
this.allowAddItem = false; scene.scheduleOnce(function(){ //物品产生的间隔为基础间隔+随机时间
this.allowAddItem = true;
}.bind(this), Math.random()*this.randomGap+this.basicGap); }

检测物品碰撞

 _collide:function(x1, y1, x2, y2, threshold){

 if((Math.pow((x1-x2),2)+Math.pow((y1-y2),2))>Math.pow(threshold, 2))
return false;
else
return true; },
collisionCheck:function(scene, layer, plane){
var itemList = scene.items; for(var i=0;i<itemList.length;i++){ var checkItem = itemList[i]; if(this._collide(checkItem.x, checkItem.y, plane.x, plane.y, 50) == true){
this.takeEffect(scene, checkItem.itemName, plane); //如果物品与飞机发生了碰撞,那么就应用物品的效果 layer.removeChild(checkItem);
scene.items.splice(i, 1);
i--;
} }
}

物品效果

物品效果分两种:即时效果和延时效果,前者有加血、一次性范围攻击等,后者有护罩、增强型子弹等。

 takeEffect:function(scene, itemName, plane){

 switch(itemName){     //根据物品的名称选择要加的效果
case "blood": cc.game.itemManager.addPlaneEffect(scene, "blood", "res/particle/wsparticle_revival01.ccbi", plane, 2);break;
case "shield":cc.game.itemManager.addPlaneEffect(scene, "shield", "res/particle/wsparticle_buff01.ccbi", plane, 10);break;
case "missile":cc.game.itemManager.addPlaneEffect(scene, "missile","res/particle/wsparticle_tailinga.ccbi" , plane, 10);break;
case "bigBang":cc.game.itemManager.addPlaneEffect(scene, "bigBang","res/particle/wsparticle_item_boom_02.ccbi" , plane, 10);break;
case "shine":cc.game.itemManager.addPlaneEffect(scene, "shine","res/particle/wsparticle_universallylight2.ccbi" , plane, 10);break;
case "ultra":cc.game.itemManager.addPlaneEffect(scene, "ultra","res/particle/wsparticle_super02.ccbi" , plane, 10);break; } }, addPlaneEffect:function(scene, effectName, effectCCBI, plane, duration){ var timeStamp = new Date().getTime(); var locationFunc = null;
var followTarget = null; switch (effectName){ //根据效果的名称决定效果特效的起始位置 case "bigBang":locationFunc = function(x, y){ //bigBang的特效位置是固定的 return {
x:cc.game.winSize.width/2,
y:cc.game.winSize.height/2
}; };followTarget = false;break;
default : locationFunc = function(x, y){ //其余的与飞机当前位置相关 return {
x:x,
y:y
}; };followTarget = true;break; //followTarget 为true就以为则特效药跟随飞机
}
var ccbNode = cc.game.particleManager.showParticle(effectName, scene, plane, locationFunc,followTarget, duration); //使用粒子系统来展示物品的应用特效
if(followTarget == true){ //需要跟随的效果加入到飞机的addedEffects数组中
plane.addedEffects.push({name:effectName, effect:ccbNode, timeStamp:timeStamp});
scene.scheduleOnce(function(){
//this.removeChild(ccbNode);
this.plane.removeAddedEffect(timeStamp);
}.bind(scene), duration);
}
this.instantEffect(scene, effectName, plane); //即时效果和延时效果使用不同的处理
this.durableEffect(scene, effectName, plane);
}, instantEffect:function(scene, effectName,plane){ //即时效果
switch (effectName){
case "bigBang" : this.bigBang(scene, effectName,plane);break;
case "blood" : this.addBlood(scene, effectName,plane);break;
}
}, durableEffect:function(scene, effectName,plane){ //延时效果
switch (effectName){
case "shine" : this.shineField(scene, effectName,plane);break; //shine效果需要添加每帧处理方法
}
}, shineField:function(scene, effectName,plane){ //对于有些效果,除了需要添加到飞机的addedEffects上,还需要给出一个效果function来在每帧执行引用效果,例如shine特效,会计算飞机周围一定距离的敌人,并给予一定的伤害
this.durableEffectFunc.push({ effectName:"shine",
effectFunc:function (scene, layer, plane) {
var enemyList = scene.enemies; for (var i = 0; i < enemyList.length; i++) {
var checkEnemy = enemyList[i]; if (((Math.pow((plane.x-checkEnemy.x),2)+Math.pow((plane.y-checkEnemy.y),2))<Math.pow(300, 2)) == true) {
checkEnemy.showHitEffect(10);
//checkEnemy.blood -= 10;
//
//if (checkEnemy.blood < 0)
// checkEnemy.blood = 0;
}
}
}});
scene.scheduleOnce(function(){ for(var j =0 ;j<this.durableEffectFunc.length;j++){
if(this.durableEffectFunc[j].effectName == "shine"){ this.durableEffectFunc.splice(j, 1);
break;
}
} }.bind(this), 10);
}

7. 特效系统

特效系统负责产生并管理两种特效:Particle System和帧动画。
Particle System是设定一些规则,使用纹理图片随机产生一些粒子特效,这些效果是随机的,不可重复的;
帧动画是使用一些图片连续播放产生一些特效效果,这些特效每一次播放都是相同的。
本游戏中,飞机被击中时产生的火花效果是Particle System产生的,而物品效果,Boss的激光以及机体爆炸则是帧动画。
Particle Manager的作用:

  1. 在给定的位置上显示特效
  2. 回收结束的特效

Particle Manager中有3个show方法,分别处理3种特效:

  1. showParticle:物品特效
  2. showFire:机体被击中的特效
  3. showLaser:Boss的激光射击特效

在需要显示特效的时候,例如子弹碰撞检测判定为击中、敌人血量低于0要爆炸、Boss要发射激光攻击、飞机吃到物品等等,就会调用Particle Manager相应的方法来显示特效。

Particle System

产生Particle System特效的方法:读取plist文件

Particle Manager:
hitEffect = new cc.ParticleSystem("res/particle2/particle.plist");
hitEffect.duration = -1;
hitEffect.setAutoRemoveOnFinish(true);
hitEffect.x = plane.x;
hitEffect.y = plane.y;
plane.gameScene.addChild(hitEffect);
plane.hitEffect = hitEffect;
plane.scheduleOnce(function(){
plane.hitEffect = null;
//plane.gameScene.removeChild(hitEffect);
hitEffect.x = -1000;
hitEffect.y = -1000;
this.fireEffect.push(hitEffect);
}.bind(this), 1);

想要制作自定义的特效,可以去这个网站制作:
http://www.onebyonedesign.com/flash/particleeditor/

帧动画

对于本游戏的帧动画,有两个存储格式:plist,ccbi。
其中ccbi文件是cocos2dx所支持的特效文件,在C++和Lua使用广泛,但是JS中暂时找不到使用sample,在官方文档中也找不到相应的API,在stackoverflow中也没有相应的解答。
但是笔者在cocos2dx-js的最新版本的源码中找到了ccbi相关的模块,所以最终是通过阅读这个模块的源码来得知cocos2dx-js是如何读取ccbi文件的,希望可以给读者一些参考。

Particle Manager:
particleList:{

 hit:"/res/particle/wsparticle_hit_01.ccbi",
explode:"/res/particle/wsparticle_hit_02.ccbi",
missile:"res/particle/wsparticle_tailinga.ccbi",
blood:"res/particle/wsparticle_revival01.ccbi",
shield: "res/particle/wsparticle_buff01.ccbi",
bigBang: "res/particle/wsparticle_item_boom_02.ccbi",
shine: "res/particle/wsparticle_universallylight2.ccbi",
ultra:"res/particle/wsparticle_super02.ccbi",
bomb:"/res/particle/wsparticle_hit_03.ccbi",
warning:"/res/particle/wsparticle_warning.ccbi" }; var nodeLibrary = new cc.NodeLoaderLibrary();
nodeLibrary.registerDefaultCCNodeLoaders();
reader =new cc.BuilderReader(new cc.BuilderReader(nodeLibrary,null, null,null)); var ccbNode = reader.readNodeGraphFromFile(this.particleList[particleName], null, null, new cc.BuilderAnimationManager());
var location = locationFunc(target.x, target.y);
ccbNode.x = location.x;
ccbNode.y = location.y;
scene.addChild(ccbNode); reader.ccbNode = ccbNode; reader.ccbNode.scheduleOnce(function(){ this.readerCache[particleName].push(reader);
//reader.ccbNode.stopAllActions();
if(particleName == "missile") { // for missile particle re-show bug
reader.ccbNode.x = -400;
reader.ccbNode.y = -400;
}
else
scene.removeChild(reader.ccbNode); }.bind(this), endurance);

8. Stage系统

所谓stage系统,其实就是指游戏的阶段管理。
众所周知,一个游戏一般不会一上来就是打Boss,也不会一上来就是最高难度,会有一个阶段的推进。
所以笔者设计了Stage系统来控制游戏的阶段过渡。

Stage Manager

var StageManager = cc.Class.extend({

 stage:0,
stageList:null,
nextStage:true,
stageFunc:null,
preStageFunc:null, ctor:function(stageList){ //初始化,stageList包含游戏的阶段信息,stageManager将会使用这些信息来切换stage
this.stageList = stageList;
this.nextStage = true;
this.stage = -1;
this.stageFunc={};
this.preStageFunc = {};
this.stageFunc["normal"] = this.normalStage;
this.stageFunc["mega"] = this.megaStage;
this.stageFunc["boss"] = this.bossStage;
this.preStageFunc["boss"] = this.preBossStage; },
manageGameStage:function(scene, layer, plane){ //stage切换方法,每个stage都有自己的执行方法,主要是调整了一些运行配置,诸如敌人产生间隔等 if(this.nextStage == true){ //nextStage 为true时切换至下一个stage if(this.stage<this.stageList.length-1){ this.stage++; //console.log("Change stage to:"+this.stageList[this.stage].name); if(this.preStageFunc[this.stageList[this.stage].name] != null)
this.preStageFunc[this.stageList[this.stage].name](scene, layer, plane); this.stageFunc[this.stageList[this.stage].name](scene, layer, plane);
this.nextStage = false; scene.scheduleOnce(function(){ this.nextStage = true; }.bind(this), this.stageList[this.stage].duration);
}else{
this.endStage(scene);
}
}else{
this.stageFunc[this.stageList[this.stage].name](scene, layer, plane);
}
},
endStage:function(scene){ //默认的结束stage,游戏结束 //console.log("End stage.");
//cc.director.pause();
new GameOverLayer(scene); },
normalStage:function(scene, layer, plane){ //常规阶段 scene.plane._shot();
cc.game.enemyManager.addEnemy(scene, layer, 0.2);
cc.game.enemyManager.enemyAttack(scene); cc.game.bulletManager.manageBullet(layer);
cc.game.bulletManager.collisionCheck(scene, layer);
cc.game.bulletManager.updateBulletLocation(scene, layer, plane);
cc.game.enemyManager.collisionCheck(scene, layer, plane);
cc.game.enemyManager.outEnemyCheck(scene, layer); scene.plane.update();
scene.gameInfo.update(); if(scene.boss!=null)
scene.boss.update(); if(scene.laserCheck == true)
scene.boss.laserCheck(scene, layer, plane);
cc.game.itemManager.generateItem(scene, layer, plane);
cc.game.itemManager.collisionCheck(scene, layer, plane);
cc.game.itemManager.applyDurableEffect(scene, layer, plane);
if(plane.blood<=0) {
trace("dead");
//cc.director.pause();
cc.game.stageManager.nextStage = true;
}
},
megaStage:function(scene, layer, plane){ //mega阶段 cc.game.enemyManager.enemyFrequency = 1;
cc.game.stageManager.normalStage(scene, layer, plane); },
preBossStage:function(scene, layer, plane){ //preBoss阶段,Boss出来前显示一个警告
cc.game.enemyManager.addBoss(scene, layer); //添加一个Boss
cc.game.enemyManager.enemyFrequency = 3;
cc.game.particleManager.showParticle("warning", scene, plane, function(x, y){
return {
x:cc.game.winSize.width/2,
y:cc.game.winSize.height/2
};
},false, 4);
},
bossStage:function(scene, layer, plane){ //boss阶段
cc.game.stageManager.normalStage(scene, layer, plane);
},
});

Stage 配置

在main.js里面,启动游戏之前使用stageList配置来配置游戏的stage。

var defaultStage = [{name:"normal", duration: 20}, {name:"mega", duration: 20}, {name:"boss", duration:100000}];   //20秒normal stage,20秒mega stage,然后一直是Boss stage
cc.game.onStart = function(){
cc.view.adjustViewPort(false);
cc.view.setDesignResolutionSize(512, 768, cc.ResolutionPolicy.SHOW_ALL);
cc.view.resizeWithBrowserSize(true);
//load resources
cc.LoaderScene.preload(g_resources, function () {
cc.game.musicManager = new MusicManager();
cc.game.bulletManager = new BulletManager();
cc.game.enemyManager = new EnemyManager();
cc.game.itemManager = new ItemManager();
cc.game.particleManager = new ParticleManager();
cc.game.stageManager = new StageManager(defaultStage);
cc.director.runScene(new MenuScene());
cc.director.setDisplayStats(false);
}, this);
};
cc.game.run(); var trace = function() {
//cc.log(Array.prototype.join.call(arguments, ", "));
};

九、 Android/iOS 原生App

Cocos2dx-JS最大的特点就是跨平台,既可以以html的web形式发布,也可以编译成Android和iOS的原生App发行。但是实际上,要想在移动平台获得web的运行效果,其中有大量的调优和适配工作。有兴趣的同学可以自己研究一下。

十、总结

至此,笔者的小游戏就大致介绍完了,更加具体的细节,需要读者在学习了Cocos2dx-JS之后再参考源码。本文只是一个大致思路的说明。
笔者学习Cocos2dx-JS纯属偶然,更直接的原因其实是在亚马逊上看到了相关的教程,于是就买了,看了,然后就写了。这其中自然会遇到一些坑,但是在学习新知识的过程中,在不断思考不断改进中收获成长却也是学习的最大乐趣。
这篇简单的教程是自己的小小的总结,如果能帮到大家或者让大家看到后能产生一点点学习新知识的兴趣,也就足够了。

作者:RogerKang
链接:http://www.jianshu.com/p/61fbcf748ee3
來源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

使用Cocos2dx-JS开发一个飞行射击游戏的更多相关文章

  1. Cocos2d-x-Lua 开发一个简单的游戏(记数字步进白色块状)

    Cocos2d-x-Lua 开发一个简单的游戏(记数字步进白色块状) 本篇博客来给大家介绍怎样使用Lua这门语言来开发一个简单的小游戏-记数字踩白块. 游戏的流程是这种:在界面上生成5个数1~5字并显 ...

  2. Step by Step 使用HTML5开发一个星际大战游戏(1)

    本系列博文翻译自以下文章 http://blog.sklambert.com/html5-canvas-game-panning-a-background/ Languages: HTML5, Jav ...

  3. 如何用原生js开发一个Chrome扩展程序

    原文地址:How to Build a Simple Chrome Extension in Vanilla JavaScript 开发一个Chrome扩展程序非常简单,只需要使用原生的js就可以完成 ...

  4. 使用node.js开发一个生成逐帧动画小工具

    在实际工作中我们已经下下来不下于一万个npm包了,像我们熟悉的 vue-cli,react-native-cli 等,只需要输入简单的命令 vue init webpack project,即可快速帮 ...

  5. 前端每日实战:116# 视频演示如何用 CSS 和原生 JS 开发一个监控网络连接状态的页面

    效果预览 按下右侧的"点击预览"按钮可以在当前页面预览,点击链接可以全屏预览. https://codepen.io/comehope/pen/oPjWvw 可交互视频 此视频是可 ...

  6. Step by Step 使用HTML5开发一个星际大战游戏(2)

    HTML5 Canvas Game: 玩家飞船  本系列博文翻译自以下文章 http://blog.sklambert.com/html5-canvas-game-the-player-ship/ L ...

  7. 《Node入门》读书笔记——用Node.js开发一个小应用

    Android APP的开发告一段落,一个稳定的.实现了基本功能的APP已经交付用户使用了!我和老板交流了下,接下来准备转战Node.js了,而且一部分前端的功能也要做进去!哈哈哈~~~接下来要朝一个 ...

  8. 用Vue.js开发一个电影App的前端界面

    我们要构建一个什么样的App? 我们大多数人使用在线流媒体服务(如Netflix)观看我们最喜欢的电影或者节目.这篇文章将重点介绍如何通过使用vue.js 2 建立一个类似风格的电影流媒体WEB交互界 ...

  9. 从零开始手把手教你使用javascript+canvas开发一个塔防游戏01地图创建

    项目演示 项目演示地址: 体验一下 项目源码: 项目源码 代码结构 本节做完效果 游戏主页面 index.html <!DOCTYPE html PUBLIC "-//W3C//DTD ...

随机推荐

  1. Eclipse编译无响应

    Eclipse编译无响应 Eclipse进程无响应结束进程后,或电脑直接断电后,重新打开Eclipse偶尔重新编译会卡进度,下面有一种解决方案: 首先找到卡进度的工程的名字,关闭Eclipse,找到该 ...

  2. 3.JAVA语言基础部分—Class类与反射

    什么是Java反射机制? JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法:对于任意一个对象,都能够调用它的任意一个方法:这种动态获取的以及动态调用对象的方法的功能称为 ...

  3. iOS数据持久化存储

    本文中的代码托管在github上:https://github.com/WindyShade/DataSaveMethods 相对复杂的App仅靠内存的数据肯定无法满足,数据写磁盘作持久化存储是几乎每 ...

  4. leetcode最长递增子序列问题

    题目描写叙述: 给定一个数组,删除最少的元素,保证剩下的元素是递增有序的. 分析: 题目的意思是删除最少的元素.保证剩下的元素是递增有序的,事实上换一种方式想,就是寻找最长的递增有序序列.解法有非常多 ...

  5. 一种排序(nyoj8)(简单排序)

    一种排序 时间限制:3000 ms  |  内存限制:65535 KB 难度:3 描写叙述 如今有非常多长方形.每个长方形都有一个编号,这个编号能够反复.还知道这个长方形的宽和长,编号.长.宽都是整数 ...

  6. Android开发之入口Activity

    Android开发之入口Activity Adnroid App是怎样确定入口Activity的? 难道就由于class的类名叫MainActivity,布局文件叫activity_main.xml? ...

  7. MongoDB---出现no write has been done on this connection解决方式

    no write has been done on this connection 这个问题出现了好几天.日志里面一天出现几十次no write has been done on this conne ...

  8. 阿里云 ubuntu 14.04 模板上安装 docker

    ubuntu 14.04 的内核是 3.13 ,所以内核不用升级. 安装过程例如以下: # apt-get update # apt-get install apt-transport-https # ...

  9. C#连接池

    C#数据库连接池 MySql SqlServer 查阅了一天的资料来学习MySql数据库连接池,终于在一篇博文上找到了,自己也整理了一下,希望对大家有用处 1. 建立连接池 1 using MySql ...

  10. T-SQL查询进阶--变量

    概述 变量对于一种语言是必不可少的一部分,当然,对于T-SQL来讲也是一样.在简单查询中,往往很少用到变量,但无论对于复杂的查询或存储过程中,变量都是必不可少的一部分. 变量的种类 在T-SQL中,变 ...