在 前一节 我们实现了游戏的核心逻辑,比如鸟的飞行效果,管道和地面的滚动,碰撞检测等等。本节会给各种场景和状态的转换添加过渡效果,让整个游戏进程更加自然。本节你将会学会:
- 如何添加场景转换
- 如何使用补间动画
注:如果你没有完成上一节的教程,也可以直接从 这里 下载到上节结束时的代码,以便开始本节的内容。
添加死亡菜单
上节结束的时候,我们游戏已经可以正常游戏了,但是死亡的时候仅仅是暂停了动效。此时需要弹出一个“死亡”菜单以更好的提示用户。我们依然使用 GUI 系统来实现这个菜单,有了之前的基础,实现这个菜单应该不是太困难的事情,按部就班:
// 添加新的菜单参数
type GameScene struct {
...
gameover struct{
gfx.Tex2D
gui.Rect
}
score struct{
gfx.Tex2D
gui.Rect
}
restart struct{
gfx.Tex2D
gui.Rect
}
...
}
// 初始化菜单参数
sn.gameover.Tex2D, _ = at.GetByName("gameover.png")
sn.gameover.Rect = gui.Rect{
X: (320-233)/2,
Y: 70,
W: 233,
H: 70,
}
sn.score.Tex2D, _ = at.GetByName("result_board.png")
sn.score.Rect = gui.Rect{
X: (320 - 240)/2,
Y: 200,
W: 240,
H: 120,
}
sn.restart.Tex2D, _ = at.GetByName("start.png")
sn.restart.Rect = gui.Rect{
X: (320 - 120)/2,
Y: 360,
W: 120,
H: 60,
}
// 实现 showOver 方法,绘制死亡菜单
func (sn *GameScene) showOver(dt float32) {
// show game over
gui.Image(1, sn.gameover.Rect, sn.gameover.Tex2D, nil)
// show score
gui.Image(2, sn.score.Rect, sn.score.Tex2D, nil)
// show restart button
e := gui.ImageButton(3, sn.restart.Rect, sn.restart.Tex2D, sn.restart.Tex2D, nil)
if e.JustPressed() {
// do something...
}
}
这部分代码我们就不做过多的解释了,它和实现 Ready
状态的菜单,并没有任何区别。此时点击 "重新开始" 按钮,并没有任何反应,这是因为我们还没有处理点击事件, 运行这部分代码大概会得到这样的“死亡”菜单:
重置游戏状态非常简单,在
GameScene
中添加新的方法 reStart ()
,在这里重置了鸟的当前状态,管道管理系统的状态:
func (sn *GameScene) reStart() {
sn.state = Ready
// bird
sn.bird.state = Flying
sn.bird.Vec2 = f32.Vec2{80, 240}
sn.bird.vy = 0
sn.bird.rotate = 0
korok.Transform.Comp(sn.bird.Entity).SetRotation(0)
korok.Flipbook.Comp(sn.bird.Entity).Play("flying")
// pipes
sn.PipeSystem.Reset()
sn.PipeSystem.StartScroll()
}
然后在之前的事情处理方法中调用之,现在运行:
reset game state
游戏状态已经正确的重置了。但是两种状态之间的转换略显僵硬。界面直接切换没有任何的过渡效果,如果能让场景渐变过去那就再好不过了。
场景转化动画
在我们当前的实现中,有好几个地方的场景过渡都是有问题的,最早的是开始游戏的场景过渡,其次是重新开始的场景过渡。实现场景过渡有很多种办法,此处的算法是:在场景上蒙上一层白色的遮罩,遮罩逐渐从透明过渡到白色,然后再从白色过渡到透明,在过渡的中间时刻切换场景。
从新回到 StartScene
这个场景,这次我们将会在它上面添加一层遮罩。这层遮罩是用 GUI 系统添加的一个白色图片,然后用补间动画系统来对遮罩的颜色做插值动画。在 StartScene
中添加一个属性:
type StartScene struct {
...
mask gfx.Color
}
它的默认值为透明色,这样默认情况下它是不会遮住场景的,然后在 Update
方法中添加方法绘制遮罩:
// fade color
if sn.mask.A > 0 {
gui.ColorRect(gui.Rect{W:320,H:480}, sn.mask,0)
}
这段代码绘制了一个白色的矩形框,它的大小完全遮住整个屏幕。接下来是最重要的逻辑了 —— 启动动画:
func (sn *StartScene) fadeOut() {
anim.OfColor(&sn.mask, gfx.Transparent, gfx.White).SetFunction(ease.InOutSine).SetDuration(1).OnComplete(func(reverse bool) {
sn.LoadGame()
}).Forward()
}
if e.JustPressed() {
sn.fadeOut()
}
在之前的代码中,点击按钮之后就直接开始 LoadGame
了。现在点击之后,先执行一段动画,动画把遮罩颜色从透明色渐变到白色,在动画结束之后再加载游戏。anim.OfColor
的方法签名如下:
func OfColor(target *gfx.Color, from, to gfx.Color) *proxyAnimator
它的可以把一个 gfx.Color 在指定的时间内从初始值过渡到目标值。在 Korok 的当前实现中还绘制纯色的图形还有些缺陷,需要设置字体系统之后才可以绘制图形,所以还需要在设置一下字体:
func (sn *StartScene) Load() {
asset.Texture.LoadAtlas("images/bird.png", "images/bird.json")
asset.Font.LoadTrueType("font1", "fonts/Marker Felt.ttf")
}
func (sn *StartScene) OnEnter(g *game.Game) {
font, _ := asset.Font.Get("font1")
gui.SetFont(font)
...
}
运行这段代码,下面就是渐变效果:
fade out
现在当前场景已经可以渐出了,但是结束的时候好像闪烁了一下。这是因为我么只给当前场景加入了渐出的动画,却没有给下一个场景添加渐入的动画。接下来会 GameScene
加入渐入的动画,但是在实现上会略有不同。在 StartScene
中,我们使用 anim.OfColor
来执行动画,这个方法适合快速实现的场景但是效率不高,在 GameScene
中,我们尝试一种更快的动画实现方式——Tween系统。
Tween 系统
这是 Korok 的底层动画系统实现,核心是一个归一化的 Animator 动画驱动器。此处会使用 ColorTween
来执行颜色的补间动画,首先在 GameScene
中添加属性:
type GameScene struct {
...
alphaTween ween.ColorTween
}
在 OnEnter
方法中,给它设置一个动画驱动器 Animator:
sn.alphaTween.Range(gfx.White, gfx.Transparent).Animate(g.TweenEngine.NewAnimator())
sn.alphaTween.Animator().SetFunction(ease.InOutSine).SetDuration(.5).Forward()
这段代码给 ColorTween 设置了起始颜色 [White, Transparent],并配置了 Animator
,然后配置 Animator
的 ease
方程为 InOutSine
,设置动画时长为 0.5秒,然后直接启动动画(因为我们希望场景加载之后立刻执行一个渐入的动画)。同时,和前一个场景类似,在 Update
方法中,需要绘制一个遮罩:
if sn.alphaTween.Value().A > 0 {
z := gui.SetZOrder(gui.DefaultZOrder+1)
gui.ColorRect(gui.Rect{0,0, 320, 480}, sn.alphaTween.Value(), 0)
gui.SetZOrder(z)
}
此处绘制遮罩的时候,调整了遮罩的z-order,这样可以使它绘制在其它GUI元素的上层,之所以要改变 z-order 是因为GUI的绘制使用的是画家算法,先绘制的UI元素实际会显示在下层,而我们在 Update
方法的入口绘制的遮罩,如果不改变z-order他会显示在其它UI的下方。现在运行一下:
现在我们已经实现了一次完美的过渡动画!!
接下来还有一个地方,在点击重新开始的时候也需要过渡动画。前面已经写了很多代码了,实现这里的动画,只要调用 ColorTween
直接执行新的即可:
if e.JustPressed() {
sn.alphaTween.Range(gfx.Transparent, gfx.White)
sn.alphaTween.Animator().SetFunction(ease.InOutSine).SetDuration(.5).OnComplete(func (r bool) {
sn.reStart()
}).Forward()
}
func (sn *GameScene) reStart() {
....
// reverse
sn.alphaTween.Animator().OnComplete(nil).Reverse()
}
检测到点击事件后,不再直接调用 reStart()
方法,而是先执行 ColorTween
启动动画,在动画结束后在调用 reStart()
方法,同时在 reStart()
结束后执行 ColorTween
的 Reverse
方法,这是一个便捷的方法可以把刚刚的动画过程反向执行。这样我们就实现了一个: 透明 -> 白色 (restart) -> 透明 的过程。
总结
以上我们实现了场景转化动画,学会使用了补间动画系统。"死亡" 菜单在原游戏中是有一个跳出的动画的,此处我们并没有实现,这部分交给你来实现了。下一节,将会给游戏添加音效,没有音效怎么好意思叫视频游戏呢!
代码我已经传到 GitHub - ntop001/flappybird,请关注 ch4 分支。
网友评论