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
是如何摞成一个鼹鼠的:
如果鼹鼠的身体下方伸出了鼹鼠洞那就完蛋了,我们必须让人感觉鼹鼠的身体下部分在草地下面。这里可以使用剪贴蒙板来创建一个新的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如何组织到一起。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述
2008-04-13 (c#)数据结构与算法分析 --树