接上节 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如何组织到一起。