在本章,你将学到怎么使用LibGD的粒子系统为Canyon Bunny添加特效,以及线性插值和其他几种提高游戏可视化外观的方法。你还将学会使用图形化编辑器设计一个dust(灰尘)自定义粒子效果。当玩家角色行走在rock对象之上时将显示该灰尘效果。我们还将介绍线性插值的概念,作为实践示例,我们为一个具有跟踪目标的相机实现平滑移动效果,并实现rock(平台)对象在水面上上下缓慢漂浮移动。
另外,我们还将为显示在背景上的山丘实现一个视差卷动效果。天空中漂浮的clouds(云朵)将以随机速度持续不断的从关卡的右侧向左侧移动。我们还将为玩家失去一条生命和游戏分数增加等事件实现一些微妙的效果。
综上所述,在本章你将学到:
- 使用LibGDX的粒子编辑器创建复杂的效果
- 为玩家角色对象添加灰尘粒子效果
- 使用linear interpolation(Lerp)为clouds(云朵)和rocks(平台)实现平滑移动效果
- 为当前分数和玩家生命图标添加一些动画以反应状态的变化
使用粒子系统创建复杂效果
粒子系统是非常适合模拟复杂效果的,如火焰、烟气、爆炸等等。从更本上说,一个粒子系统是由多张图片组成,这些图片可能使用正常模式(alpha 屏蔽)或混合模式渲染以获得有趣的结果。
观察下面截图,找出正常渲染模式和混合渲染模式的区别:
LibGDX使用ParticleEffect类为我们提供了一个精致的粒子系统。我们可以将该类理解为一个粒子效果的容器,他允许你在高层次上操纵最终的粒子效果,如设置显示位置、触发显示以及取消显示等等。
下面是对ParticleEffect类中几个重要方法的简要描述:
- start():启动粒子效果
- reset():重置并重启粒子效果
- update():为了让粒子效果的行为与时间一致,该方法必须被调用
- draw():在当前位置渲染粒子效果
- allowCompletion():即使粒子效果被设置为持续播放,该方法也能允许我们平滑的停止发射器运行
- setDuration():该方法设置粒子效果的持续时间
- setPosition():该方法设置粒子效果的显示位置
- setFlip():该方法设置水平和竖直模式
- save():将一个粒子效果和相应的全部配置保存到文件中
- load():从文件中载入一个粒子效果和全部配置
- dispose():该方法释放所有由粒子效果占用的资源
仅仅使用ParticleEffect实例还不能在屏幕上显示任何效果。这是因为粒子效果是由图片组成,而ParticleEffect类并不包含任何图片的引用。因此,LibGDX还提供了一个ParticleEmitter类,该类包含了一些其他属性,如图片引用。发射器用于管理使用同一张图片的粒子。同一个发射器发射的粒子共用一个具有限定范围的特殊行为。该限定范围定义了每个粒子随机数的生成范围,这样将使得最终效果看起来更自然一些。如果需要,我们可以结合多个发射器使用多图片和集合行为创建更为复杂的粒子效果。
下面代码展示了如何使用单个发射器创建粒子效果:
ParticleEffect effect = new ParticleEffect(); ParticleEmitter emitter = new ParticleEmitter(); effect.getEmitters().add(emitter); emitter.setAdditive(true); emitter.getDelay().setActive(true); emitter.getDelay().setLow(0.5f); // ... more code for emitter initialzation ...
可也确定的说上述代码没有任何错误,但是我们不推荐这样初始化粒子发射器。粗略的估算,粒子发射器大概包含20个用户可以设置的属性,如果按照上述方法进行初始化的话,那么每个发射器的初始化代码将迅速变为一个庞大的代码片段,很明显,这样的代码难以理解和维护。一个清晰有趣的解决方案就是使用LibGDX的图形化粒子编辑器创建或修改粒子效果。该编辑器的特点是包含一个预览界面,我们可以通过该界面实时观察当前设置下的效果,该特性大大的简化了设计阶段的工作。
你可以通过http://wiki.libgdx. googlecode.com/git/jws/particle-editor.jnlp链接下载并运行该编辑器。
如果对该编辑器想要了解更多内容,请访问这篇wiki文章:https://github.com/libgdx/libgdx/wiki/2D-Particle-Editor。
下面截图显示了Particle Editor启动后的窗口:
实时预览界面位于窗口的左上角,并且显示着当前的粒子效果和一些运行信息:
- FPS:当前帧率
- Count:当前的粒子数量
- Max:同时存在的最大粒子数
- Percentage:已经完成的时间百分比
在实时预览界面我们可以通过鼠标拖拽移动粒子特效的显示位置。这是一个非常有用的功能,但不仅仅是因为可以改变特效的显示位置,还有一个重要的原因,该功能可以让我们快速检查特效在非静态环境下的效果。
编辑器的左下角包含一个粒子发射器列表。我们可以为每个发射器分配一个任意的名称。发射器名称的右侧包含一个复选框,用于决定是否显示该发射器产生的粒子效果。然而,需要注意,这里只使用了一个编辑器就能完成由多个发射器组成的复杂效果。目前为止,效果文件中还不会保存特效是否显示的状态。发射器的顺序可以通过up和down按钮调整。列表中的发射器渲染顺序是从上到下的。这意味着,列表中的最后一个将显示在最上层。New按钮可以添加新发射器。同样,Delete按钮可以从列表中移除当前选中的发射器。
编辑器的右边被分割为两部分,分别包含一种属性类型。其中,上面部分被标记为Editor Properties,用于设置实时预览窗口的属性。Pixels per meter用于定义渲染视口的逻辑单位,表示每单位多少像素。Zoom level表示渲染视口的缩放大小。
编辑器最复杂和最重要的地方就是右下角标记为Emitter Properties部分。这里包含了发射器发射的粒子的所有行为和样式设置。
下面对所有属性作了简要的介绍:
- Image:代表粒子的图片文件。
- Count:分别表示同时存在的粒子的最大和最小值。需要注意的是,最大值直接影响预分配的内存大小。
- Delay:表示特效显示之前的延时时间,单位:毫秒。使用该属性之前,必须单击Active按钮激活该属性。
- Duration:表示发射器持续发射的总时间。
- Emission:每秒发射的粒子数。
- Life:每个粒子的生命周期(持续显示的时间)。
- Life Offset:表示粒子生命周期开始的延时时间。
- X Offset:水平方向位移,长度为逻辑单位,该属性也需要激活。
- Y Offset:竖直方向位移,长度为逻辑单位,该属性也需要激活。
- Spawn:定义孵化粒子的形状。可用的形状有点、线、矩形和椭圆,默认为点。
- Size:以逻辑单位表示粒子的大小。
- Velocity:粒子的移动速度,以逻辑单位/s表示,该属性也需要激活。
- Angle:表示粒子的发射角度,该属性也需要激活。
- Rotation:表示粒子的旋转角度,该属性也需要激活。
- Wind:表示水平方向施加的力,单位为逻辑单位/s,同样,该属性也需要激活。
- Gravity:表示竖直方向施加的力,单位为逻辑单位/s,同样,该属性也需要激活。
- Tint:表示粒子的渲染着色,使用单色调图片更容易获得较好的结果。
- Transparency:表示粒子的透明度,该属性允许粒子全透明和半透明。更进一步,该属性允许在粒子的生命周期内实现渐入渐出效果。
- Options:这部分包含了下面五种额外配置发射器行为的选项:
- Additive:如果选中,则表示激活添加剂模式。
- Attached:如果选中,表示要求存在的粒子跟随发射器移动。
- Continuous:如果选中,表示当发射器完成总的发射时间(Duration)后重新开始,如果没有选中,则该粒子效果只会显示一次。
- Aligned:如果选中,则为粒子的旋转角度加上发射角度,这样将导致粒子图片面向发射角度移动。
- Behind:如果选中该选项,则将发射器的behind属性设置为true,否则设置为false。在代码中我们可以通过isBehind()方法查询该属性值并决定粒子特效在游戏对象的前面还是后面渲染。
上述某些属性包含一个坐标图表。在图表中,x轴表示粒子的生命周期或特效持续时间,y轴表示属性真实值的百分比。单击图表任意位置添加一个新节点,再次双击节点即可删除他。点击并拖拽可以移动节点。
下面截图显示了部分发射器属性:
坐标图表可以通过右侧的+号放大。图表有一个Hight值和一个Low值,这两个值定 义了图表y坐标的最大值(100%)和最小值(0%)。Hight值右侧有一个>按钮。单击该按钮将显示另一个值域,该如果使用该值域则将一个单一的值更改为一个取值范围。取值范围具有最大值和最小值,运行时属性的具体值将从该取值范围内随机获取。
为玩家角色创建一个灰尘粒子特效
现在我们将创建一个灰尘粒子特效,只要玩家角色在rock平台上移动时就会显示该特效。如果玩家停止移动或者玩家角色不在rock平台上,则停止显示特效。
首先,我们需要设计一个看起来像灰尘的粒子特效。下面截图就是我们设计的目标:
打开粒子编辑器并使用默认的火焰粒子特效设置,然后按照下面步骤进行修改:
- Pixels per meter属性修改为200
- 在Size属性中,将High值设置为0.75,Low值设置为0,然后为图表添加(0,0),(67,28),(100,0)三个点
- 在Velocity属性中,将High设置为1到5,将Low设置为-1到-5,然后为图表添加(0,50)点
- 将Duration属性设置为100
- 在Emission属性中,将High值设置为200,将Low值设置为0,为图表添加(0,100)点
- 在life属性中,将High值设置为250到500,将Low设置为0,为图表添加(0,100)点
- 在Angle属性中,将High和Low都设置为0,为图表添加(0,100)点
- 在Gravity属性中,将High设置为5,将Low设置为-1,并为图表添加(0,0),(67,28),(100,0)三个点
- 在Tint属性中,设置颜色值为RGB(107,107,107)
- 在Transparency属性中,为图表添加(0,100)和(100,0)两个点
- 在Options选项中,选中Additive和Continuous两个选项
接下来,单击Save按钮将效果文件保存到CanyonBunny-android/assets/particles目录下。
【LibGDX官方并没有规定或限制粒子特效文件的扩展名。但是创建一个能表明用途的命名总是有好处的。众所周知,sfx是一个著名的声音效果简称,同样这样的形式也适用于粒子效果文件,因此本书将所有粒子效果文件的后缀名设置为.pfx。】
在Canyon Bunny游戏中实现粒子特效之前我们还需完成一项准备工作。现在你也知道,粒子特效是由图片组成,那么我们这里创建的特效也需要图片。你可能注意到,在粒子编辑器设计效果时我们跳过了选择图片文件那部分。这里我们没有从头开始,而是使用了编辑器默认的火焰效果的图片文件。该图片文件名是particle.png并且可以在GDX工具的assets文件夹中找到。你也可以通过下面地址下载:https://raw.githubusercontent.com/libgdx/libgdx/master/tests/gdx-tests-android/assets/data/particle.png。
particle.png图片基本就是一张只包含一个很小的从中心到周围渐变的白色圆圈图片。如下面截图所示:
将particle.png拷贝到CanyonBunny-android/assets/particles/文件夹下。
接下来,为BunnyHead类添加下面两行导入代码:
import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.g2d.ParticleEffect;
完成之后,根据下面代码修改BunnyHead类:
public class BunnyHead extends AbstractGameObject { public ParticleEffect dustParticles = new ParticleEffect(); public void init() { ... // 飞翔特效 hasFeatherPowerup = false; timeLeftFeatherPowerup = 0; // Particles dustParticles.load(Gdx.files.internal("particles/dust.pfx"), Gdx.files.internal("particles")); } @Override public void update(float deltaTime) { super.update(deltaTime); ... dustParticles.update(deltaTime); } @Override public void render(SpriteBatch batch) { TextureRegion reg = null; // Draw Particles dustParticles.draw(batch); // Apply Skin Color ... } }
其中dustParticles成员变量引用了我们即将载入并准备显示的粒子特效对象。可能你也发现了,我们仅仅添加了几行代码就在游戏中实现了粒子特效。在init()方法中我们首先为对象载入效果文件然后持续不断的在update()和render()方法更新并渲染。但是,调用draw()方法并不意味着粒子特效总是会被渲染。
在游戏首次启动时,粒子特效需要被激活。进一步,如果粒子特效属于持续性效果,则需要在不需要时明确停止显示它。
为了实现这一点,修改BunnyHead类的updateMotionY()方法:
@Override protected void updateMotionY(float deltaTime) { switch (jumpState) { case GROUNDED: jumpState = JUMP_STATE.FALLING; if(velocity.x != 0) { dustParticles.setPosition(position.x + dimension.x / 2, position.y); dustParticles.start(); } break; ... } if(jumpState != JUMP_STATE.GROUNDED) { dustParticles.allowCompletion(); super.updateMotionY(deltaTime); } }
下面截图就是我们完成之后的效果:
移动云朵
接下来我们要强化云朵的运动,我们只需要将云朵放在空中并向左移动。其实这里就是在游戏世界模拟风的过程,这里我们将使每朵云以稍微不同的速度向左移动,这样看起来更自然一些。除此之外,这里还需要一个条件让更多的云朵出现在关卡的最右侧。否则,当游戏运行到某点将超出云朵的存在范围。我们可以简化云朵运行路线,并在某点产生大量云朵,如1000朵,但是以渲染效率来讲,这并不是一个聪明的做法,而且并不能完全解决超出界限的问题。一个更好的解决方法是限制当前存在的云朵数量的范围。
根据下面代码修改Clouds类:
private Cloud spawnCloud() { Cloud cloud = new Cloud(); cloud.dimension.set(dimension); // select random cloud image cloud.setRegion(regClouds.random()); // position Vector2 pos = new Vector2(); pos.x = length + 10; // postition after end of level pos.y = 1.75f; // base postition pos.y += MathUtils.random(0.0f, 0.2f) * (MathUtils.randomBoolean() ? 1 : -1); cloud.position.set(pos); // speed Vector2 speed = new Vector2(); speed.x += 0.5f; // base speed // random additional speed speed.x += MathUtils.random(0.0f, 0.75f); cloud.terminalVelocity.set(speed); speed.x *= -1; // move left cloud.velocity.set(speed); return cloud; }
接下来,为Clouds添加下面代码:
@Override public void update(float deltaTime) { for (int i = clouds.size - 1; i >= 0; i--) { Cloud cloud = clouds.get(i); cloud.update(deltaTime); if(cloud.position.x < -10) { // cloud moved outside of world. // destroy and spawn new cloud at end of level. clouds.removeIndex(i); clouds.add(spawnCloud()); } } super.update(deltaTime); }
spawnCloud()方法现在将使用我们刚刚添加的代码创建一个新的Cloud对象。update()方法迭代所有存在的Cloud对象,分别为每个对象调用update()方法更新最新状态,接着,检测每个对象的最新位置,如果满足条件,则从列表中移除该对象,并为列表添加一个位于关卡最右侧的新对象。
【在update()方法中,我们使用反向迭代的方法是为了避免变异列表问题。正常情况下,在迭代过程中不能修改列表内容。然而,当反向迭代时,从列表中移除一个元素,该元素只会发生在已经处理过的元素,就像本例一样。】
使用线性插值(Lerp)平滑运动
Lerp是一个查找位于两点之间的某个未知值的方法。该未知值是通过Lerp计算出来的最接近两个已知点所连接的直线。
Lerp操作还可以被用于平滑运动。下面我们将使用本项目的一个实例介绍该方法,在项目中我们将实现相机与目标对象之间平滑跟踪过程,还有,为了模拟石块漂浮在水面的效果,我们将让rock对象上下轻轻的来回移动。首先,在CameraHelper中添加下面代码:
private final float FOLLOW_SPEED = 4.0f;
之后,修改下面方法:
public void update(float deltaTime) { if(!hasTarget()) return ; position.lerp(target.position, FOLLOW_SPEED * deltaTime); // 防止camera移动太远 position.y = Math.max(-1f, position.y); }
幸运的是,LibGDX的Vector2类已经提供了lerp()方法,这使得Lerp操作非常容易实现。上面代码中,为当前相机的位置向量调用lerp()方法具体发生的变化:一个2D坐标,并传入一个目标位置和一个alpha值。该alpha值用于描述当前位置和目标位置之间的比例。记住,Lerp就是假想将当前位置和目标位置连接成一条直线,然后通过alpha值确定最新坐标在该直线上的位置。如果alpha值等于0.5,则意味着新坐标位于当前位置和目标位置连线的中点处。
因为Lerp操作是在update()方法中进行的,所以我们每次的移动量应该稍微小一点。deltaTime通常大约等于0.016秒(16毫秒,如果从Lerp的角度考虑则等于1.6%)。我们使用deltaTime变量是为了让Lerp操作与时间相关。但是这里有个错误观点,如果我们只使用deltaTime变量作为alpha值,则意味着整个移动将发生在1秒之内。这样将导致开始时移动很快,之后当两点之间的距离越来越近时将变得非常缓慢。为了加速这一过程我们为deltaTime乘上一个FLLOW_SPEED常量。
模拟石块在水面的漂浮过程
下面我们将使用几乎相同的方法让石块上下平滑移动,这样可以给用户产生一种石块漂浮在水面上的幻觉。
为Rock类添加下面两行导入代码:
import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector2;
接下来为Rock添加下面几个成员变量:
private final float FLOAT_CYCLE_TIME = 2.0f; private final float FLOAT_AMPLITUDE = 0.25f; private float floatCycleTimeLeft; private boolean floatingDownwards; private Vector2 floatTargetPosition;
然后,修改init()方法:
private void init() { dimension.set(1, 1.5f); regEdge = Assets.instance.rock.edge; regMiddle = Assets.instance.rock.middle; // 设定rock初始长度 setLength(1); floatingDownwards = false; floatCycleTimeLeft = MathUtils.random(0, FLOAT_CYCLE_TIME / 2); floatTargetPosition = null; }
上述代码确保与浮动过程相关的变量都得了到正确的初始化。初始化浮动方向被设定为竖直向上;循环周期被设定为位于0到1秒之间的一个随机数。使用随机循环周期可以让最终效果看起来更加自然,这是因为每个石块的移动过程看起来都是互不相关的。floatTargetPositon变量用于存储下一个目标位置,如下代码所示:
ss @Override public void update(float deltaTime) { super.update(deltaTime); floatCycleTimeLeft -= deltaTime; if(floatTargetPosition == null) floatTargetPosition = new Vector2(position); if(floatCycleTimeLeft <= 0) { floatCycleTimeLeft = FLOAT_CYCLE_TIME; floatingDownwards = !floatingDownwards; floatTargetPosition.y += FLOAT_AMPLITUDE * (floatingDownwards ? -1 : 1); } position.lerp(floatTargetPosition, deltaTime); }
为山丘添加视差滚动效果
视差滚动是一个可以让人们在2D场景中产生“深度”幻觉的高级技术。其原理是,当相机移动时,背景中的对象要比前景中的对象移动的慢一些。
下面我们将为游戏界面的山丘背景实现一个视差滚动效果。
首先,为Mountains类添加下面导入代码:
import com.badlogic.gdx.math.Vector2;
接下来,为Mountains添加下面新方法:
public void updateScrollPosition(Vector2 camPosition) { position.set(camPosition.x, position.y); }
接下来修改drawMountain()和render()方法:
private void drawMountain (SpriteBatch batch, float offsetX, float offsetY, float tintColor, float parallaxSpeedX) { TextureRegion reg = null; batch.setColor(tintColor, tintColor, tintColor, 1); float xRel = dimension.x * offsetX; float yRel = dimension.y * offsetY; // mountains 跨越整个关卡 int mountainLength = 0; mountainLength += MathUtils.ceil( length / (2 * dimension.x) * (1 - parallaxSpeedX)); mountainLength += MathUtils.ceil(0.5f + offsetX); for(int i = 0; i < mountainLength; i++) { // mountain 左侧 reg = regMountainLeft; batch.draw(reg.getTexture(), origin.x + xRel + position.x * parallaxSpeedX, origin.y + yRel + position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); xRel += dimension.x; // mountain 右侧 reg = regMountainRigth; batch.draw(reg.getTexture(), origin.x + xRel + position.x * parallaxSpeedX, origin.y + yRel + position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x, scale.y, rotation, reg.getRegionX(), reg.getRegionY(), reg.getRegionWidth(), reg.getRegionHeight(), false, false); xRel += dimension.x; } // 重置颜色为白色 batch.setColor(1, 1, 1, 1); } @Override public void render(SpriteBatch batch) { // distant mountains(dark gray) drawMountain(batch, 0.5f, 0.5f, 0.5f, 0.8f); // distant mountains(gray) drawMountain(batch, 0.25f, 0.25f, 0.7f, 0.5f); // distant mountains(light gray) drawMountain(batch, 0.0f, 0.0f, 0.9f, 0.3f); }
我们为drawMountain()添加了第五个参数,该参数用于描述滚动距离或者称为滚动速度,取值范围限定在0.0到1.0之间。滚动的距离等于相机当前位置乘以距离因子。这就是我们为什么添加了一个新方法updateScrollPosition(),为了获得相机最新位置,我们需要在每个更新循环调用该方法。
修改WorldController的update()方法:
public void update(float deltaTime) { handleDebugInput(deltaTime); if(isGameOver()) { timeLeftGameOverDelay -= deltaTime; if(timeLeftGameOverDelay < 0) backToMenu(); } else { handleInputGame(deltaTime); } level.update(deltaTime); testCollisions(); cameraHelper.update(deltaTime); if(!isGameOver() && isPlayerInWater()) { lives--; if(isGameOver()) { timeLeftGameOverDelay = Constants.TIME_DELAY_GAME_OVER; } else { initLevel(); } } level.mountains.updateScrollPosition( cameraHelper.getPosition());
现在三个mountain层将分别以不同的速度滚动:30%,50%,80%。
改善游戏界面GUI
在最后一部分,我们将专注于提高游戏界面的两处GUI。首先,为了当玩家失去一条生命时,能给用户一个醒目的反馈,我们将为游戏添加一个短暂的动画。第二,为游戏的当前得分实现一个连续的动画。
玩家失去生命事件
我们希望当玩家刚刚失去一条生命时播放一个短暂的动画。玩家当前的额外生命在界面的右上角以兔子头图标显示着。当失去一条生命时,代表下一条生命的图标将变为灰色。
我们将要实现的动画目标就是在刚刚失去生命的那个图标之上显示一个临时的兔子头图标动画。动画的内容包含让临时图标放大、旋转并以淡红色渲染。
下面截图就是我们实现的目标:
;;;;;;;;;;;
首先为WorldController类添加下面成员变量:
public float livesVisual;
接下来按照下面代码修改WorldController类:
private void init() { Gdx.input.setInputProcessor(this); cameraHelper = new CameraHelper(); lives = Constants.LIVES_START; livesVisual = lives; timeLeftGameOverDelay = 0; initLevel(); } public void update(float deltaTime) { handleDebugInput(deltaTime); if(isGameOver()) { timeLeftGameOverDelay -= deltaTime; if(timeLeftGameOverDelay < 0) backToMenu(); } else { handleInputGame(deltaTime); } level.update(deltaTime); testCollisions(); cameraHelper.update(deltaTime); if(!isGameOver() && isPlayerInWater()) { lives--; if(isGameOver()) { timeLeftGameOverDelay = Constants.TIME_DELAY_GAME_OVER; } else { initLevel(); } } level.mountains.updateScrollPosition( cameraHelper.getPosition()); if(livesVisual > lives) { livesVisual = Math.max(lives, livesVisual - 1 * deltaTime); } }
我们为WorldController类引入了一个新的成员变量livesVisual,该变量包含许多有关生命的信息。但是,当任意时间失去生命时livesVisual只会随之时间慢慢减小。这就允许我们在livesVisual还没有达到当前生命的数量时播放相应的动画。
最后,修改WorldRenderer类的renderGuiExtraLive()方法:
private void renderGuiExtraLive(SpriteBatch batch) { float x = cameraGUI.viewportWidth - 50 - Constants.LIVES_START * 50; float y = -15; for(int i = 0; i < Constants.LIVES_START; i++) { if(i >= worldController.lives) batch.setColor(0.5f, 0.5f, 0.5f, 0.5f); batch.draw(Assets.instance.bunny.head, x + i * 50, y, 50, 50, 120, 100, 0.35f, -0.35f, 0); batch.setColor(1, 1, 1, 1); } if(worldController.lives >= 0 && worldController.livesVisual > worldController.lives) { int i = worldController.lives; float alphaColor = Math.max(0, worldController.livesVisual - worldController.lives - 0.5f); float alphaScale = 0.35f * (2 + worldController.lives - worldController.livesVisual) * 2; float alphaRotate = -45 * alphaColor; batch.setColor(1.0f, 0.7f, 0.7f, alphaColor); batch.draw(Assets.instance.bunny.head, x + i * 50, y, 50, 50, 120, 100, alphaScale, -alphaScale, alphaRotate); batch.setColor(1, 1, 1, 1); }
上面新添加的代码将绘制一个兔子头图标,并且将随着时间自动播放alpha颜色、缩放和旋转动画。该动画的过程(时间)是通过livesVisual的当前值控制的。
分数递增事件
每当玩家角色收集到一枚道具时,作为奖励,玩家获得一定分数,并加到总分上。在游戏界面的左上角显示着当前总分数和金币图标。我们将为游戏添加两处精妙的动画效果,并且当玩家收集到一枚道具时触发播放。首先,我们希望分数慢慢增长到新分数。第二,在分数增长期间,金币图标将持续震动。
下面截图通过五个步骤展示了玩家收集到一枚道具时播放的动画:
;
;
;
;
;
为WorldController类添加下面成员变量:
public float scoreVisual;
接下来根据下面代码修改WorldController类:
private void initLevel() { score = 0; scoreVisual = score; level = new Level(Constants.LEVEL_01); cameraHelper.setTarget(level.bunnyHead); } public void update(float deltaTime) { ... level.mountains.updateScrollPosition( cameraHelper.getPosition()); if(livesVisual > lives) { livesVisual = Math.max(lives, livesVisual - 1 * deltaTime); } if(scoreVisual < score) scoreVisual = Math.min(score, scoreVisual + 250 * deltaTime); }
这里引入了一个新变量scoreVisual,该变量和前面的livesVisual变量基本类似,只不过这里是为了控制分数动画。
另外,根据下面代码修改WorldRenderer类:
// 游戏分数渲染方法 private void renderGuiScore(SpriteBatch batch) { float x = -15; float y = -15; float offsetX = 50; float offsetY = 50; if(worldController.scoreVisual < worldController.score) { long shakeAlpha = System.currentTimeMillis() % 360; float shakeDist = 1.5f; offsetX += MathUtils.sinDeg(shakeAlpha * 2.2f) * shakeDist; offsetY += MathUtils.sinDeg(shakeAlpha * 2.9f) * shakeDist; } batch.draw(Assets.instance.goldCoin.goldCoin, x, y, offsetX, offsetY, 100, 100, 0.35f, -0.35f, 0); Assets.instance.fonts.defaultBig.draw(batch, "" + (int) worldController.scoreVisual, x + 75, y + 37); }
scoreVisual被强制转换为整型数值是为了省略小数。转换之后的中间数字就是我们准备实现的动画。为了让金币图标发生震动,我们使用了一个正弦函数和两个不同的角度因子作为正弦函数的输入值,最后计算出临时位移。
章末总结
在本章,我们使用了多种方法为游戏添加了几个特效,使得游戏变得更为生动。你学到了粒子系统和如果和在LibGDX中使用它。我们可以使用强大的粒子编辑器轻松的设计一个复杂的粒子特效,并且这也是我们推荐的创建粒子特效的方法,最好要避免使用代码直接创建粒子特效。你还学会了怎么在项目中添加一个已完成的粒子特效并控制它。你还学会了怎么使用简单的原理为本项目的云朵实现随风移动效果。我们还简单的介绍了一下线性插值原理,并介绍了怎么使用线性插值实现相机平滑移动和模拟石块漂浮在水面的效果。还有,为了进一步提高游戏GUI,我们为游戏背景中的山丘添加了一个视差滚动效果。最后,我们使用了一些精妙的动画效果为游戏界面提高了GUI。
在下一章,我们将继续学习在第七章介绍的多屏管理。但是,为了可以轻松的创建屏幕动画和升级屏幕转换效果,我们将实现一个灵活的系统。