[libgdx游戏开发教程]使用Libgdx进行游戏开发(8)-粒子系统
没有美工的程序员,能够依赖的还有粒子系统。
这一章我们将使用libGDX的粒子系统线性插值以及其他的方法来增加一些特效。
你也可以使用自己编辑的粒子效果,比如这个粒子文件dust:https://files.cnblogs.com/mignet/particles.zip
这个灰尘的特效用在兔子头在地面跑的时候,啪啪的一溜烟。
线性插值可以让我们的摄像机在移动的时候更平滑。
当然,之前提到的背景上的山要实现视差移动效果也要实现。
白云会用随机的速度从右向左飘。
GUI的部分也要增加些效果比如掉了命,得了分等。
粒子系统通常用来模拟复杂的特效:比如fire, smoke, explosions等等.
ParticleEffect简介:
• start(): This starts the animation of the particle effect
• reset(): This resets and restarts the animation of the particle effect
• update(): This must be called to let the particle effect act in accordance to time
• draw(): This renders the particle effect at its current position
• allowCompletion(): This allows emitters to stop smoothly even if particle effects are set to play continuously
• setDuration(): This sets the overall duration the particle effect will run
• setPosition(): This sets the position to where it will be drawn
• setFlip(): This sets horizontal and vertical flip modes
• save(): This saves a particle effect with all its settings to a file
• load(): This loads a particle effect with all its settings from a saved file
• dispose(): This frees all resources allocated by the particle effect
粒子效果通常需要一个粒子发射器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 initialization ...
当然,不建议在代码里初始化例子发射器。因为发射器有20多个属性,要是在代码里初始化会很杂乱并且不容易维护。我们使用Libgdx的编辑器来编辑想要的粒子。
https://github.com/libgdx/libgdx/wiki/Particle-editor
我们来调个灰尘特效吧:
保存文件到CanyonBunny-android/assets/particles/dust.pfx
虽然并没有规定粒子文件要用什么文件后缀,但是我们统一叫pfx。记得把图片https://github.com/libgdx/libgdx/blob/master/extensions/gdx-tools/assets/particle.png
也保存到相同的文件夹。
首先在兔子头BunnyHead里添加代码:
public ParticleEffect dustParticles = new ParticleEffect();
public void init () { ... // Power-ups 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
...
}
让灰尘跟着兔子:
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); } } }
ok,跑起..
接下来,让云飘起来:
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; // position after end of level pos.y += 1.75; // base position // random additional position 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; } @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()); } } }
线性插值,让摄像机平滑移动到跟随的目标(Libgdx已经实现了lerp)CameraHelper:
private final float FOLLOW_SPEED = 4.0f; public void update(float deltaTime) { if (!hasTarget()) return; position.lerp(target.position, FOLLOW_SPEED * deltaTime); // Prevent camera from moving down too far position.y = Math.max(-1f, position.y); }
让岩石浮在水面上Rocks:
private final float FLOAT_CYCLE_TIME = 2.0f; private final float FLOAT_AMPLITUDE = 0.25f; private float floatCycleTimeLeft; private boolean floatingDownwards; private Vector2 floatTargetPosition; private void init() { dimension.set(1, 1.5f); regEdge = Assets.instance.rock.edge; regMiddle = Assets.instance.rock.middle; // Start length of this rock setLength(1); floatingDownwards = false; floatCycleTimeLeft = MathUtils.random(0, FLOAT_CYCLE_TIME / 2); floatTargetPosition = null; } @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); }
让山随着兔子视差Mountains:
public void updateScrollPosition(Vector2 camPosition) { position.set(camPosition.x, position.y); } 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 span the whole level 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 left 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 right reg = regMountainRight; 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; } // reset color to white batch.setColor(1, 1, 1, 1); } @Override public void render(SpriteBatch batch) { // 80% distant mountains (dark gray) drawMountain(batch, 0.5f, 0.5f, 0.5f, 0.8f); // 50% distant mountains (gray) drawMountain(batch, 0.25f, 0.25f, 0.7f, 0.5f); // 30% distant mountains (light gray) drawMountain(batch, 0.0f, 0.0f, 0.9f, 0.3f); }
把这个加到worldcontroller里:
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()); }
现在,增加GUI的特效。
首先是掉了命:
在worldcontroller里加public float livesVisual;
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); }
同时,在WorldRenderer里相应的修改:
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 (worldController.lives <= i) 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); } }
数字增涨效果:
WorldController增加:public float scoreVisual;在initLevel中添加scoreVisual = score;
在update的最后增加
if (scoreVisual < score) scoreVisual = Math.min(score, scoreVisual+ 250 * deltaTime);
在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); }
在下一章,我们将使用转场动画来平滑的过渡场景