注:请务必结合代码理解!
这篇比较复杂,是整个游戏实现的核心所在,希望大家仔细阅读理解。
首先回到GameScreen中,在构造方法里,我们生成了World 和 WorldRender,接着就是调用present()方法渲染游戏画面和update方法监听触屏事件。那么显然,WorldRender应该放置在present方法中,代码如下:
@Override
public void present (float deltaTime) {
GLCommon gl = Gdx.gl;
gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
gl.glEnable(GL10.GL_TEXTURE_2D);
renderer.render();
guiCam.update();
batcher.setProjectionMatrix(guiCam.combined);
batcher.enableBlending();
batcher.begin();
switch (state) {
case GAME_READY:
presentReady();
break;
case GAME_RUNNING:
presentRunning();
break;
case GAME_PAUSED:
presentPaused();
break;
case GAME_LEVEL_END:
presentLevelEnd();
break;
case GAME_OVER:
presentGameOver();
break;
}
batcher.end();
}
这里就是调用WorldRender的render方法,不停的绘制游戏画面。
现在回想游戏的过程,我们点击Play项后,这时屏幕中间会绘制出Ready字符,并且游戏画面中所有的物体都是静止的。当我们点击屏幕之后,Ready字符被移除,主角Bob开始跳跃,Platform开始移动,金币开始旋转,就像是所有物体都想是活过来了。这是为什么呢?其实关键就在update()方法。
当我们第一进入游戏时,GameScreen的状态是GAME_READY,对应的在present()方法中就会调用presentReady()绘制Ready字符(之前已经分析),而在update()方法中则是调用updateReady()方法来监听触屏事件,只要一发生触屏事件,就把GameScreen的状态GAME_RUNNING。代码如下:
@Override
public void update (float deltaTime) {
if (deltaTime > 0.1f) deltaTime = 0.1f;
switch (state) {
case GAME_READY:
updateReady();
break;
case GAME_RUNNING:
updateRunning(deltaTime);
break;
case GAME_PAUSED:
updatePaused();
break;
case GAME_LEVEL_END:
updateLevelEnd();
break;
case GAME_OVER:
updateGameOver();
break;
}
}
private void updateReady () {
if (Gdx.input.justTouched()) {
state = GAME_RUNNING;
}
}
而让所有物体动起来的就是:当状态被更改为GAME_RUNNING时,update()方法会调用对应的updateRunning(deltaTime) 方法,这个方法除了监听是否点击了暂停按钮,以及左右方向键的点击事件外,它还负责刷新World中的所有物体,代码如下:
private void updateRunning (float deltaTime) {
//得到触碰点的位置
if (Gdx.input.justTouched()) {
guiCam.unproject(touchPoint.set(Gdx.input.getX(), Gdx.input.getY(), 0));
//是否点击了暂停键
if (OverlapTester.pointInRectangle(pauseBounds, touchPoint.x, touchPoint.y)) {
Assets.playSound(Assets.clickSound);
state = GAME_PAUSED;
return;
}
}
//监听按键
if(Gdx.app.getType() == Application.ApplicationType.Android) {
world.update(deltaTime, Gdx.input.getAccelerometerX());
}
else {
float accel = 0;
if(Gdx.input.isKeyPressed(Keys.DPAD_LEFT))
accel = 5f;
if(Gdx.input.isKeyPressed(Keys.DPAD_RIGHT))
accel = -5f;
world.update(deltaTime, accel);
}
//刷新分数
if (world.score != lastScore) {
lastScore = world.score;
scoreString = "SCORE: " + lastScore;
}
//是否通关
if (world.state == World.WORLD_STATE_NEXT_LEVEL) {
state = GAME_LEVEL_END;
}
//是否游戏结束
if (world.state == World.WORLD_STATE_GAME_OVER) {
state = GAME_OVER;
if (lastScore >= Settings.highscores[4])
scoreString = "NEW HIGHSCORE: " + lastScore;
else
scoreString = "SCORE: " + lastScore;
Settings.addScore(lastScore);
Settings.save();
}
}
这里关键就是调用Wrold.upate(deltaTime,accel) 来刷新World内的所有物体。其中 deltaTime 为刷新时间;而accel 则是控制Bob左右的移动,通过监听左右方向键获得,如果是在Android应用上,则是通过Gdx.input.getAccelerometerX() 获得accel,也就是是加速仪。
那么,显然,接下来,就是看看World.upate(deltaTime,accel)到底做了什么手脚让所有的物体都运动起来,代码如下:
public void update(float deltaTime, float accelX) {
updateBob(deltaTime, accelX);
updatePlatforms(deltaTime);
updateSquirrels(deltaTime);
updateCoins(deltaTime);
if (bob.state != Bob.BOB_STATE_HIT)
checkCollisions();
checkGameOver();
}
这里发现,原来upate()方法里不仅仅处理了物体的刷新,而且还包含了碰撞的检测:checkCollisions()。
为了能够把这里的逻辑讲清楚,我决定先针对Bob来进行分析。先看一下 updateBob(deltaTime, accelX)方法,代码如下:
private void updateBob(float deltaTime, float accelX) {
if (bob.state != Bob.BOB_STATE_HIT && bob.position.y <= 0.5f)
bob.hitPlatform();
if (bob.state != Bob.BOB_STATE_HIT)
bob.velocity.x = -accelX / 10 * Bob.BOB_MOVE_VELOCITY;
bob.update(deltaTime);
heightSoFar = Math.max(bob.position.y, heightSoFar);
}
首先第一个if 只有在游戏第一此启动的时候起作用,它表示只要Bob触碰到屏幕下方,就让它不停的跳跃,也就是hitPlatform()。运行游戏,让Bob不要触碰到任何Platform,就可以看到效果。
而第二个if 表示,只要Bob还没有死,也就是没有Hit到那些飞行的松鼠或者摔死,就赋予它在x方向的移动速度。其中accelX就是在GameScreen所监听到的按键时间传递过来的值,而Bob.BOB_MOVE_VELOCITY 变量就定义了Bob的移动能力。
接着就是调用Bob.upate(deltaTime)进行刷新,同样的传递进去的为刷新时间。
而最后的heightSoFar ,这个值会在 checkGameOver() 用于检测 Bob是否在跳跃的过程中,跌落至屏幕的底线,也就是是否摔死了。
看完了updateBob(float deltaTime, float accelX))方法,可以肯定Bob类一定也定义一些状态,并且Bob.upata(deltaTime)方法就是刷新Bob的关键所在。
那么接下来,就看看Bob类的实现,代码如下:
public class Bob extends DynamicGameObject{
public static final int BOB_STATE_JUMP = 0;
public static final int BOB_STATE_FALL = 1;
public static final int BOB_STATE_HIT = 2;
public static final float BOB_JUMP_VELOCITY = 11;
public static final float BOB_MOVE_VELOCITY = 20;
public static final float BOB_WIDTH = 0.8f;
public static final float BOB_HEIGHT = 0.8f;
int state;
float stateTime;
public Bob(float x, float y) {
super(x, y, BOB_WIDTH, BOB_HEIGHT);
state = BOB_STATE_FALL;
stateTime = 0;
}
public void update(float deltaTime) {
velocity.add(World.gravity.x * deltaTime, World.gravity.y * deltaTime);
position.add(velocity.x * deltaTime, velocity.y * deltaTime);
bounds.x = position.x - bounds.width / 2;
bounds.y = position.y - bounds.height / 2;
if(velocity.y > 0 && state != BOB_STATE_HIT) {
if(state != BOB_STATE_JUMP) {
state = BOB_STATE_JUMP;
stateTime = 0;
}
}
if(velocity.y < 0 && state != BOB_STATE_HIT) {
if(state != BOB_STATE_FALL) {
state = BOB_STATE_FALL;
stateTime = 0;
}
}
if(position.x < 0)
position.x = World.WORLD_WIDTH;
if(position.x > World.WORLD_WIDTH)
position.x = 0;
stateTime += deltaTime;
}
public void hitSquirrel() {
velocity.set(0,0);
state = BOB_STATE_HIT;
stateTime = 0;
}
public void hitPlatform() {
velocity.y = BOB_JUMP_VELOCITY;
state = BOB_STATE_JUMP;
stateTime = 0;
}
public void hitSpring() {
velocity.y = BOB_JUMP_VELOCITY * 1.5f;
state = BOB_STATE_JUMP;
stateTime = 0;
}
}
说明:
1.Bob有三个状态,分别是 :
public static final int BOB_STATE_JUMP = 0; //跳跃
public static final int BOB_STATE_FALL = 1; //下降
public static final int BOB_STATE_HIT = 2; //击中
同时,还规定了Bob的宽和高为0.8f,跳跃能力为11,移动能力为20。
2.在update()方法中,模拟了跳跃过程,并且刷新了Bob的位置使用了以下的4行代码:
velocity.add(World.gravity.x * deltaTime, World.gravity.y * deltaTime);
position.add(velocity.x * deltaTime, velocity.y * deltaTime);
bounds.x = position.x - bounds.width / 2;
bounds.y = position.y - bounds.height / 2;
首先,Bob继承了DynamicGameObject类,如同前面所说它包含了 position(位置) ,bounds(作用范围),velocity(速度)以及
accel(加速度)。
那么,要Y轴方向上模拟跳跃,在X轴方向上进行移动,要改变的就是Bob的position。如何达到模拟跳跃的过程呢?
这里用到了Vector2.add(x,y)方法,该方法会将原来
的x,y值加上传入的值
首先velocity它是一个
Vector2类型的变量,在这里它代表了Bob在x和y轴的移动速度。调用 velocity.add(World.gravity.x * deltaTime, World.gravity.y * deltaTime),表示 由于受到 gravity(重力为-12),velocityY轴方向的速度将不断的减小,X轴则不受影响。
调用 position.add(velocity.x * deltaTime, velocity.y * deltaTime), 表示根据当前Bob的velocity刷新它的位置。而velocity.x的值,则是在World.upate方法中被赋予。
那么接下来的:
bounds.x = position.x - bounds.width / 2;
bounds.y = position.y - bounds.height / 2;
就是根据新的position刷新bounds,而这个bounds如同其他游戏物体一样会被用于碰撞的检测(后面会说到)。
想象一下,开始时Bob的velocity.y为正值(11),Bob开始往上移动,在移动过程中由于受到gravity影响,velocity.y不断的减小,当减小为0后,velocity.y开始变为赋值,则Bob就会开始下落。那么对应的position就会不断的改变。
而如果在下降的过程中,碰到Platform ,也就是触发了hitPlatform() 函数,那么Bob的velocity.y又被赋值为11,继续往上跳。就是这样不断的改变velocity的值,来控制Bob的移动和跳跃,并且在update方法中更新Bob的位置。
从这里又可以得出一个结论,如果velocity.y 为正值,那么表示Bob正在往上跳;如果velocity.y为负值,Bob就是正在下跌。所以可以通过这个来更改Bob的状态,在Updata()方法中,实现的代码如下:
if(
velocity.y > 0 && state != BOB_STATE_HIT) {
if(state != BOB_STATE_JUMP) {
state = BOB_STATE_JUMP;
stateTime = 0;
}
}
if(
velocity.y < 0 && state != BOB_STATE_HIT) {
if(state != BOB_STATE_FALL) {
state = BOB_STATE_FALL;
stateTime = 0;
}
}
而接下来的:
if(position.x < 0)
position.x = World.WORLD_WIDTH;
if(position.x > World.WORLD_WIDTH)
position.x = 0;
则实现了让Bob穿越屏幕左右边框的能力。
最后的:
stateTime += deltaTime;
则是把为stateTime变量加上刷新的时间,也就是说每次刷新Bob都会添加stateTime的值。
同时还发现,只要是改变状态后,都会把stateTime的值设置为0。那么这个stateTime的作用是什么呢?这就要涉及到libgdx动画的绘制。
之前说过,在libgdx使用的动画类是
Animation。构造它的方法就是
设定每一帧动画持续的时间,然后将动画的具体帧(TextureRegion)传递给构造函数既可,例如在Assets类中构造金币的旋转动画,如下:
public static Animation coinAnim;
coinAnim = new Animation(0.2f, //每帧持续0.2秒
new TextureRegion(items, 128, 32, 32, 32), //第一帧
new TextureRegion(items, 160, 32, 32, 32), //第二帧
new TextureRegion(items, 192, 32, 32, 32), //```
new TextureRegion(items, 160, 32, 32, 32));
而播放动画就是调用
Animation.getKeyFrame(float stateTime, int mode)方法得到动画帧(
TextureRegion
)。原理就是,传入一个时间点(
stateTime),然后判断这个时间点对应的动画帧是哪一个帧,通过不停增加stateTime,就可以达到播放动画的效果,实际上就是不停的切换
TextureRegion。而mode则参数表示播放动画的模式,只有循环和非循环两种。至于具体的使怎么实现,其实看看Animation类就能理解了。
好了,大概分析了播放动画的原理,下面就来看看主角Bob的动画是怎么结合stateTime绘制出来的。该方法就是WorldRender中的renderBob(),代码如下:
private void renderBob () {
TextureRegion keyFrame;
switch
(world.bob.state) {
case Bob.BOB_STATE_FALL:
keyFrame = Assets.bobFall.getKeyFrame(world.bob.stateTime, Animation.ANIMATION_LOOPING);
break;
case Bob.BOB_STATE_JUMP:
keyFrame = Assets.bobJump.getKeyFrame(world.bob.stateTime, Animation.ANIMATION_LOOPING);
break;
case Bob.BOB_STATE_HIT:
default:
keyFrame = Assets.bobHit;
}
float side = world.bob.velocity.x < 0 ? -1 : 1;
if(side < 0)
batch.draw(keyFrame, world.bob.position.x + 0.5f, world.bob.position.y - 0.5f, side * 1, 1);
else
batch.draw(keyFrame, world.bob.position.x - 0.5f, world.bob.position.y - 0.5f, side * 1, 1);
}
首先定义一个
TextureRegion 类型的
keyFrame,作为当前的动画帧。接下来,(
显然,又是状态机模式)先判断当前World中Bob的状态是什么(记住,游戏中的物体都是存储在World中):如果是下降状态,则在
Assets.bobFall中取当前帧;如果是跳跃状态,则在
Assets.bobJump中取当前帧。两者都是
getKeyFrame(
world.bob.stateTime, Animation.ANIMATION_LOOPING
)获取当前帧。
参数一正是Bob中的stateTime变量,而改变这个变量只有两个途径:要么改变Bob的状态则stateTime会被设置为0,对应过来就是动画重新开始;要么是在Bob.update(deltaTime
)中,每次为
stateTime叠加deltaTime的值,对应过来就是正常播放动画。
也就是说,对Bob而言,是通过结合state 和 stateTime,来控制播放哪一个动画,以及实现该动画的连续播放。
例如:一开始,主角Bob是上升的,那么显然状态就是
Bob.BOB_STATE_JUMP,假设它没有触碰到任何物体,那么在Bob.update()中 ,stateTime的值会不断增加,对应过来,在
WorldRender.renderBob()中,播放的就是跳跃的动画。直到,Bob到达最高点后,这个时候
在Bob.update()中
Bob的状态会被设成
Bob.BOB_STATE_FALL,而stateTime被重置为0,同时
stateTime的值再被不断增加
,那么对应过来,在
WorldRender.renderBob(),播放的就是下跌时候的动画,直到触碰到其他物体改变状态为止。
而所谓的播放动画,其实就是调用SpriteBatch.draw()来绘制一个个的
TextureRegion 。随着
stateTime增加,那么获得的
keyFrame也就不同,通过切换
keyFrame,来达到动画播放的效果,代码如上面的两个draw()方法。
下面就让我们回到一开始的源头,也就是World.update()方法,再看一遍代码,如下:
public void update(float deltaTime, float accelX) {
updateBob(deltaTime, accelX);
updatePlatforms(deltaTime);
updateSquirrels(deltaTime);
updateCoins(deltaTime);
if (bob.state != Bob.BOB_STATE_HIT)
checkCollisions();
checkGameOver();
}
说完了最复杂的Bob,接着就是Platform的更新,先看一下updatePlatforms(deltaTime)方法代码如下:
private void updatePlatforms(float deltaTime) {
int len = platforms.size();
for (int i = 0; i < len; i++) {
Platform platform = platforms.get(i);
platform.update(deltaTime);
if (platform.state == Platform.PLATFORM_STATE_PULVERIZING
&& platform.stateTime > Platform.PLATFORM_PULVERIZE_TIME) {
platforms.remove(platform);
len = platforms.size();
}
}
}
非常简单,就是遍历所有的platform,分别调用它们的update方法。如果platform状态为Platform.PLATFORM_STATE_PULVERIZING(也就是分解状态),并且动画已经播放完了(platform.stateTime > Platform.PLATFORM_PULVERIZE_TIME),则把这个platform移除,也就是不再被绘制。
再来看一下,Platform类的实现,代码如下:
public class Platform extends DynamicGameObject {
public static final float PLATFORM_WIDTH = 2;
public static final float PLATFORM_HEIGHT = 0.5f;
public static final int PLATFORM_TYPE_STATIC = 0;
public static final int PLATFORM_TYPE_MOVING = 1;
public static final int PLATFORM_STATE_NORMAL = 0;
public static final int PLATFORM_STATE_PULVERIZING = 1;
public static final float PLATFORM_PULVERIZE_TIME = 0.2f * 4;
public static final float PLATFORM_VELOCITY = 2;
int type;
int state;
float stateTime;
public Platform(int type, float x, float y) {
super(x, y, PLATFORM_WIDTH, PLATFORM_HEIGHT);
this.type = type;
this.state = PLATFORM_STATE_NORMAL;
this.stateTime = 0;
if(type == PLATFORM_TYPE_MOVING) {
velocity.x = PLATFORM_VELOCITY;
}
}
public void update(float deltaTime) {
if(
type == PLATFORM_TYPE_MOVING) {
position.add(velocity.x * deltaTime, 0);
bounds.x = position.x - PLATFORM_WIDTH / 2;
bounds.y = position.y - PLATFORM_HEIGHT / 2;
if(position.x < PLATFORM_WIDTH / 2) {
velocity.x = -velocity.x;
position.x = PLATFORM_WIDTH / 2;
}
if(position.x > World.WORLD_WIDTH - PLATFORM_WIDTH / 2) {
velocity.x = -velocity.x;
position.x = World.WORLD_WIDTH - PLATFORM_WIDTH / 2;
}
}
stateTime += deltaTime;
}
public void pulverize() {
state = PLATFORM_STATE_PULVERIZING;
stateTime = 0;
velocity.x = 0;
}
}
如同Bob,同样定义了不同的状态,宽度和高度,以及移动速度的信息,不同的是对于Platform,有静止的 和动态的两种,所有这里还定义了两个不同的类别。
相对于Bob,Platform的update()方法就简单多了,因为只有type类型为PLATFORM_TYPE_MOVING的,才需要跟新它位置,也就是让他动起来。
同样先判断类型,然后调用position.add(velocity.x * deltaTime, 0); 让它在x方向移动,y方向则不动。随后也是更新bounds的信息:
bounds.x = position.x - PLATFORM_WIDTH / 2;
bounds.y = position.y - PLATFORM_HEIGHT / 2;
而接下来的:
if(position.x < PLATFORM_WIDTH / 2) {
velocity.x = -velocity.x;
position.x = PLATFORM_WIDTH / 2;
}
if(position.x > World.WORLD_WIDTH - PLATFORM_WIDTH / 2) {
velocity.x = -velocity.x;
position.x = World.WORLD_WIDTH - PLATFORM_WIDTH / 2;
}
}
则实现让Platform 左右不停的移动。关键就是改变 velocity.x 的值。
接下来也是增加stateTime:
stateTime += deltaTime;
再来就是看看WorldRender中renderPlatforms()方法,代码如下:
private void renderPlatforms () {
int len = world.platforms.size();
for (int i = 0; i < len; i++) {
Platform platform = world.platforms.get(i);
TextureRegion keyFrame = Assets.platform;
if (platform.state == Platform.PLATFORM_STATE_PULVERIZING) {
keyFrame = Assets.brakingPlatform.getKeyFrame(platform.stateTime, Animation.ANIMATION_NONLOOPING);
}
batch.draw(keyFrame, platform.position.x - 1, platform.position.y - 0.25f, 2, 0.5f);
}
}
同样的,遍历拿出World.platfroms中所有的platfrom,如果platfrom的状态不是分解中,那么就绘制静态的 Assets.platform,也就一个TextureRegion;如果为分解状态,那么就绘制动画 Assets.brakingPlatform。而绘制的位置,并不是去platform.position的x,y坐标,应为position是指它的中心点,绘制的起始坐标应该作用区域(bounds)的左下顶点,所有要分别减去platform的宽和高。
分析完Bob与Platform后,现在可以总结一下了。
World.update()作为游戏的刷新入口,它传递deltaTime(刷新时间) 和 accelX。除了Spring和Castle(它们既不用移动,也不用播放动画),其他所有的游戏物体,都有相应的update方法。在World.update()方法中,就是调用它们的update方法,将deltaTime作为参数,实现移动以及动画的播放(改变stateTime的值)。而Bob则是两个都使用来达到目的。通过在不同游戏物体中的update方法,设置不同的逻辑,来实现不同的需求。而物体被真正的绘制出来(以及动画的播放),则是在WorldRender中相应的render方法,用到的就是游戏物体的postition信息,以及statTime的值。
欢迎转载!请注明原文链接,谢谢!
http://tonmly.blog.163.com/blog/static/1747128562011713436115/