[案例研究]—superJumper 5.游戏刷新与碰撞检测

注:请务必结合代码理解!

这篇比较复杂,是整个游戏实现的核心所在,希望大家仔细阅读理解。


经过上面两节的分析,我们已经知道GameScreen是怎么运作,World是怎么构键的,WorldRender是怎么绘制游戏画面的,以及OrthographicCamera在这个跳跃类游戏中的巧妙应用。接下来,就是把这些都结合起来,看看这个游戏到底是怎么运作的。

首先回到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/
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值