GC DevKit 快速入門 -- 游戲概覽(三)
接上節 http://www.rzrgm.cn/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`是如何摞成一個鼴鼠的:

如果鼴鼠的身體下方伸出了鼴鼠洞那就完蛋了,我們必須讓人感覺鼴鼠的身體下部分在草地下面。這里可以使用剪貼蒙板來創建一個新的`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如何組織到一起。
浙公網安備 33010602011771號