上一期讲了关于游戏初始化的一些设置,然而我们还不能真正在屏幕上看到任何东西。这一期我将会实现一个弹跳的方块,同时这期内容也将真正涉及到游戏的具体制作过程。
三、添加精灵
万事具备,只欠东风。是时候添加我们的主人公小方块了。
Q.Sprite.extend 'Player',
init: (p) !->
@_super p,
x: 100, y: 400
w: 42, h: 42
vx: 0, vy: 0
ax: 0, ay: 0
gravity: 0.02
jumpable: false
start: false
@add '2d, tween'
@on 'hit', @, @detect
detect: (collision) !->
if collision.obj.isA 'Ground'
@p.vy = 0
@p.jumpable = true
else @die!
pass: !->
loadNextLevel!
die: !->
if !@p.hidden
@p.hidden = true
@p.vx = 0
@animate { angle: 0 }, 0.2, Q.Easing.Linear, callback: !-> loadNextLevel!
draw: (ctx) !->
ctx.fillStyle = 'white'
ctx.fillRect -@p.cx, -@p.cy, @p.w, @p.h
step: (dt) !->
@p.vx = 5.5
@p.x += @p.vx
@p.y += @p.vy
if Q.inputs['up'] and @p.jumpable is true
@p.jumpable = false
@p.vy = -9.2
@animate { angle: 180 + @p.angle }, 0.9
Q.audio.play 'jump.mp3'
if @p.y > groundY - @p.h/2 then @p.y = groundY - @p.h/2 # avoid falling bug
if @p.x > @p.stage.options.w + @p.w / 2 then @pass!
Q.Sprite.extend( name, options ) 利用Quintus的继承方法扩展一个Sprite类,第一个参数name是扩展类的名称,第二个参数是一个集合,里面可以重定义默认函数或者自定义函数。这里,init 方法、draw方法、step方法均是覆盖了原有默认方法。 init( p ) 方法初始化精灵的默认大小、位置、速度和加速度等性质,这些性质可以在实例化时被覆盖,另外init方法还可以给精灵添加一些Quintus组件,比如这里的"2d"、"tween",实现碰撞检测和渐进动画。 draw( ctx ) 接收一个回调的 canvas 对象, 其用法和canvas一样,这里只是画了一个白色方块。 step( dt ) 函数每一秒被调用60次, 可以用来实现方块的移动和按键判断。注意在step方法中代码不宜过于复杂耗时,不然游戏的效率会变得非常低下。
另外,我还给小方块添加了一些自定义的方法。detect( collision ) 函数每当精灵发生碰撞时被调用,碰撞检测由组件'"2d"实现,具体细节可以忽略。我希望小方块在发生碰撞后判断对方是否为地面,如果是则允许再次跳跃,否则执行死亡方法。die() 函数将玩家方块隐藏起来并在0.2秒后重新加载当前关卡。win() 函数调用了自定义方法loadNextLevel(),代码会在下章讲解,作用是加载关卡。
这里我们可以看到animate方法出现了两次,它属于Quintus的"tween"组件来执行渐进动画的,obj.animate( options, time, style, callback ) options参数对应于init方法,为动画结束时的精灵性质, time是动画的时间,默认为1秒,style决定渐变的方法,默认为线性渐变,callback是动画结束后的回调函数。后三个参数在使用时均可以省略。
四、添加场景
光有精灵是不行的,精灵的展现需要有场景做支撑。
我们需要考虑好游戏的场景布局,通常我喜欢把游戏分3层:背景层、逻辑层、UI层。每一层其实就是Quintus场景的一个Scene实例。以下是背景层和UI层的代码实现。
Q.scene 'background', (stage) !->
stage.insert new Q.Repeater { asset: 'background.png' }
Q.scene 'UI', (stage) !->
button = new Q.UI.Button { x: stage.options.w/8*7, y: 150, fill: 'white', label: 'Pause', font: '400 24px Pico', fontColor: 'rgb(167,34,243)' }
button.on 'click', !->
if Q.loop isnt null then Q.pauseGame! else Q.unpauseGame!
stage.insert button
Q.scene( name, function(stage){...} ) 这里name参数是创建场景的名字,function(stage){...} 接收一个stage参数,它是Quintus的舞台,场景便是通过Quintus舞台来展现的,可以把场景想象成舞台上的背景道具和演员,而这里的函数通过stage参数告诉舞台,场景需要什么。比如,背景场景background通过 stage.insert 函数将平铺图"background.png"添加到了舞台中,也即是,每当Quintus舞台展现background场景时,我们就会看到“background.png”图片平铺整个屏幕。“background.png”图片会从先前自定义的图片路径下读取。
同理,UI层主要实现了一个暂停按钮,通过 Q.UI.Button 实例化出button, 并对button重写了 "click" 点击事件的执行函数。这里,利用Quintus的Q.loop 可以判断游戏是否在运行,利用Q.pauseGame() 暂停游戏,利用Q.unpauseGame() 恢复游戏。值得注意的是,暂停游戏并不会关闭游戏音效,Quintus在这一点上并没有实现关于声音的暂停和恢复。
现在我们便可以将小方块加载到屏幕上来了。
Q.scene 'level', (stage) !->
stage.insert new Q.Ground { w: stage.options.w, x: stage.options.w/2 }
player = stage.insert new Q.Player { stage: stage }
Q.Player和Q.Ground是我们扩展的Sprite类,Q.Ground在前面忽略了,它利用draw方法画了一条屏幕宽度的白线,并垂直居中于屏幕。通过stage.insert将两者加载到逻辑层"level"中来,当舞台展现“level”时便可以看见小方块了。
现在终于可以看到loadReady函数了
loadReady = !->
$ '#loading' .hide!
Q.stageScene 'background', 0
Q.stageScene 'UI', 2
loadNextLevel!
没错,通过Q.stageScene展现场景,第二个参数是场景的渲染层次,越低越容易被遮蔽。在loadNextLevel函数中调用Q.stageScene("level",1)便能看见小方块了。
loadNextLevel = !->
Q.clearStage 1
if currentLevel! > maxLevel!
Q.clearStage 2
Q.stageScene 'win', 1
else
Q.stageScene 'level', 1