GC DevKit 快速入门 游戏概览(三)
接上节 http://www.cnblogs.com/hangxin1940/archive/2013/04/11/3015553.html
## 启动流程
在构造函数`init`中,我们通过监听`'app:start'`事件来处理上层的通知,然后调用 `start_game_flow` 方法来播放动画序列,最后调用`play_game`来开始游戏。
function start_game_flow () {
var that = this;
animate(that._scoreboard).wait(1000)
.then(function () {
that._scoreboard.setText(text.READY);
}).wait(1500).then(function () {
that._scoreboard.setText(text.SET);
}).wait(1500).then(function () {
that._scoreboard.setText(text.GO);
//开始游戏 ...
game_on = true;
play_game.call(that);
});
}
`Ready, Set, Go!`这三个单词在切换时,有一个短暂的停顿,每次都会更新计分板的内容,并且加载下一步动画。在最后一步动画,会调用`play_game.call(that)`方法。`that`只是`this`的一个引用而已,只是方便在不同的上下文中进行正确的引用。
## 游戏开始
`play_game` 函数包含了几个定时器,`tick`函数用来随机的让鼹鼠从洞中冒出来,一个计时器有来每秒钟在一个`TextView`更新倒计时,并且在超过时间时结束游戏。
function play_game () {
var i = setInterval(bind(this, tick), mole_interval),
j = setInterval(bind(this, update_countdown), 1000);
//复位所有定时器、标记以及倒计时
setTimeout(bind(this, function () {
game_on = false;
clearInterval(i);
clearInterval(j);
setTimeout(bind(this, end_game_flow), mole_interval * 2);
this._countdown.setText(":00");
}), game_length);
//让倒计时可见,并且删除开始提示信息
setTimeout(bind(this, function () {
this._scoreboard.setText(score.toString());
this._countdown.style.visible = true;
}), game_length * 0.25);
//没有时间时,置红倒计时文字
setTimeout(bind(this, function () {
this._countdown.updateOpts({color: '#CC0066'});
}), game_length * 0.75);
}
function tick () {
//随机选择一个鼹鼠
var len = this._molehills.length,
molehill = this._molehills[Math.random() * len | 0];
//如果选择鼹鼠已经是激活的,那么选择另一个
while (molehill.activeMole) {
molehill = this._molehills[Math.random() * len | 0];
}
molehill.showMole();
}
function update_countdown () {
countdown_secs -= 1;
this._countdown.setText(":" + (("00" + countdown_secs).slice(-2)));
}
## 结束序列
在游戏倒计时结束后,游戏的结束动画就会开始,由`end_game_flow`函数播放,它会检查游戏得分,并且将它显示出来,而鼹鼠则持续不断的冒出就好像它在嘲笑你。
function end_game_flow () {
var isHighScore = (score > high_score),
end_msg = get_end_message(score, isHighScore);
this._countdown.setText(''); //清除倒计时信息
//重设记分牌
this._scoreboard.updateOpts({
text: '',
x: 10,
size: 17,
verticalAlign: 'top',
textAlign: 'left',
multiline: true
});
//检查是否是最高分并播放相应动画
if (isHighScore) {
high_score = score;
this._molehills.forEach(function (molehill) {
molehill.endAnimation();
});
} else {
var i = (this._molehills.length-1) / 2 | 0; //居中鼹鼠
this._molehills[i].endAnimation(true);
}
this._scoreboard.setText(end_msg);
//短时间的延迟再允许触摸复位
setTimeout(bind(this, emit_endgame_event), 2000);
}
一旦新的记分板被设置,并且鼹鼠动画正在播放,会在2秒的时间后来监听用户的触摸事件
function emit_endgame_event () {
this.once('InputSelect', function () {
this.emit('gamescreen:end');
reset_game.call(this);
});
}
用户点击后,一个`gamescreen:end`事件会被发送,上层程序所处理这个事件,之后视图堆栈会弹出并关闭这个游戏视图,显示出标题视图,以便用户重新开始游戏。
## 鼹鼠资源:`MoleHill.js`
`MoleHill`类是另外一个比较大的类,它存放于`./src/MoleHill.js`
## 对齐组件
一个鼹鼠洞是由3种图像堆叠而成的:底层的鼹鼠洞,鼹鼠,以及上层的鼹鼠洞。鼹鼠的动画是在Y轴进行上下活动,并且会给它一个矩形遮罩,用以盖住鼹鼠图片超出的部分。鼹鼠看起来就像跳出洞口等待敲它一样。
this.build = function () {
var hole_back = new ui.ImageView({
superview: this,
image: hole_back_img,
//...
});
this._inputview = new ui.View({
superview: this,
clip: true,
//...
});
this._moleview = new ui.ImageView({
superview: this._inputview,
image: mole_normal_img,
//...
});
var hole_front = new ui.ImageView({
superview: this,
canHandleEvents: false,
image: hole_front_img,
//...
});
//...
this._inputview.on('InputSelect', bind(this, function () {
if (this.activeInput) {
sound.play('whack');
this.emit('molehill:hit');
this.hitMole();
}
}));
};
这副图形象的说明了三个`ImageView`是如何摞成一个鼹鼠的:
![devkit](http://docs.gameclosure.com/guide/assets/game-walkthrough/molehill-layers.png "devkit")
如果鼹鼠的身体下方伸出了鼹鼠洞那就完蛋了,我们必须让人感觉鼹鼠的身体下部分在草地下面。这里可以使用剪贴蒙板来创建一个新的`View`,只需要将`clip`属性置为`true`,这样任何附加于这个视图的子视图将只显示它范围内的图像。在上面的图中,鼹鼠只显示它所附加的剪贴蒙板中自己的图像(黑色方框内)。
我们同样也将这个剪贴蒙板视作一个按钮区域,用来检测是玩家否敲到正确的鼹鼠身上,只要触摸落在矩形区域内,并且鼹鼠是被激活的,都视为一次成功的敲击。但是,还有一个问题,用户的输入被鼹鼠图像捕获,但是在它之前还有一个鼹鼠洞的图像,它遮住了下面的区域,这样使得事件不能被正常的处理。我们可以通过将上层图像的`canHandleEvents`属性设置为`false`,来让触摸事件达到预期效果。它可以让事件`穿过`这个视图,继而让下方的控件捕获。
此外,在`build`函数中,我们给`Animator`对象赋了一个函数引用,我们将在游戏过程中使用到它。
this._animator = animate(this._moleview);
我们可以在任意时间执行这个函数,用来播放鼹鼠动画,当然它不会视为有效的游戏动作。这样引用有个好处,每次想播放动画时是要调用它就行,不必每次都创建一个新的`Animator`对象。
## 鼹鼠动画
`MoleHill`类中定义了三个动画序列:鼹鼠跳出洞、鼹鼠回洞以及结束动画中鼹鼠慢慢的出来并"嘲笑"玩家。它们都使用`Animator`对象进行定义:
//鼹鼠出洞
this.showMole = function () {
if (this.activeMole === false) {
this.activeMole = true;
this.activeInput = true;
this._animator.now({y: mole_up}, 500, animate.EASE_IN)
.wait(1000).then(bind(this, function () {
this.activeInput = false;
})).then({y: mole_down}, 200, animate.EASE_OUT)
.then(bind(this, function () {
this.activeMole = false;
}));
}
};
//打鼹鼠
this.hitMole = function () {
if (this.activeMole && this.activeInput) {
this.activeInput = false;
this._animator.clear()
.now((function () {
this._moleview.setImage(mole_hit_img);
}).bind(this))
.then({y: mole_down}, 1500)
.then(bind(this, function () {
this._moleview.setImage(mole_normal_img);
this.activeMole = false;
this.activeInput = false;
}));
}
};
//结束动画
this.endAnimation = function () {
this.activeInput = false;
this._animator.then({y: mole_up}, 2000)
.then(bind(this, function () {
this._interval = setInterval(bind(this, function () {
if (this._moleview.getImage() === mole_normal_img) {
this._moleview.setImage(mole_hit_img);
} else {
this._moleview.setImage(mole_normal_img);
}
}), 100);
}));
};
`animate`函数插入了一个JavaScript对象属性,如果它传入了一个`View`对象,那么会为其插入这个对象的样式属性。这种方式十分的便利,因为很有可以我们会基于当前样式进行动画。
举个例子,如果一个鼹鼠在洞中,我们让它出洞,一下是`showMole`函数:
this._animator.now({y: mole_up}, 500, animate.EASE_IN)
.wait(1000).then(bind(this, function () {
this.activeInput = false;
})).then({y: mole_down}, 200, animate.EASE_OUT)
.then(bind(this, function () {
this.activeMole = false;
}));
首先,动画对象调用 `.now({y: mole_up}, 500, animate.EASE_IN)` 函数,它会立即操作动画对象的Y轴属性,并且把它定义为`this._moleview`对象。因为主要的动画是一个`View`类的实例,我们实际上实在操作他的 `style.y` 属性,或者说鼹鼠图像在屏幕上的垂直位置。`mole_up`变量在文件一开始被设置为5,它是想对于父视图`this._inputview`的偏移量。这个动画的第一步执行了半秒钟/500毫秒,缓慢的移动到最终位置,最终鼹鼠露出了头。
然后,第二部分的动画会执行`.wait(1000)`方法暂停1秒,然后继续下一个动画。这样看起来,鼹鼠会钻出洞后然后飞快的又进到洞里。如果这段时间点击鼹鼠,就会被记上分数。
最后 `.then( ... )` 被调用,它通过一个回调函数被立即执行,这个方法将鼹鼠洞的`activeInput`置为`false`,这样这个鼹鼠洞就不会接受输入事件。这个动作结束,动画会进入下一个动画序列。
我们现在期望鼹鼠能回到洞里,所以调用 `.then({y: mole_down}, 200, animate.EASE_OUT)` ,正如我们看到的那样,` .then`函数可以用多种方式来调用。这里,我们通过之前的` .now() `来调用,这把鼹鼠在Y轴的位置进行改变,使之缓慢的降低,进入洞中。
整个动画结束后,我们最后使用一次 `.then( ... )` 来让这个鼹鼠的属性`activeMole`变为`false`,整个动画流程就结束了。
## 声音
我们使用一个单独的控制器来加入声音,它位于`./src/soundcontroller.js`:
import AudioManager;
exports.sound = null;
exports.getSound = function () {
if (!exports.sound) {
exports.sound = new AudioManager({
path: 'resources/sounds',
files: {
levelmusic: {
path: 'music',
volume: 0.5,
background: true,
loop: true
},
whack: {
path: 'effect',
background: false
}
}
});
}
return exports.sound;
};
在这里,我们在程序启动时创建了一个`AudioManager`对象,并且在任何时候调用`getSound`函数都会返回这个对象。
我们回到 `./src/Application.js` 文件的 `initUI` 函数来看看它的使用细节。
this.initUI = function () {
//...
var sound = soundcontroller.getSound();
//...
titlescreen.on('titlescreen:start', function () {
sound.play('levelmusic');
GC.app.view.push(gamescreen);
GC.app.emit('app:start');
});
gamescreen.on('gamescreen:end', function () {
sound.stop('levelmusic');
GC.app.view.pop();
});
};
当用户按下开始按钮后,`titlescreen:start`事件会被触发,背景音乐就会被播放。如果`levelmusic`的 `loop` 属性为`true`,那么整个声音会被循环播放,直到游戏结束,这些都在 `./src/soundcontroller.js` 文件中的`new AudioManager(...)`中设置
## 总结
打鼹鼠游戏很简单,但它是一个可工作的完整的游戏,我们通过它学习了引擎的api如何组织到一起。