[案例研究]—superJumper 3.游戏中的物体与主游戏逻辑

来源:互联网 发布:白菜价是淘宝开的吗 编辑:程序博客网 时间:2024/06/05 18:53
注:请务必结合代码理解!

经过之前两节的分析,现在,游戏的资源加载和初始化已经完成,并且除了主游戏界面,其他界面也已经绘制完成,接下来就游戏的主界面的实现了。先讲一下作者是怎么定义游戏中的物体。

首先,这是一个2D的游戏,也就是说,所有的游戏物体都会在一个平面内绘制(或者说在屏幕大小内绘制),那么所有的物体都应该包含一个位置(position) 和一个作用范围(bounds),所以作者为所有的物体定义了一个名为GameObject的父类,代码如下 :

public class GameObject {
    public final Vector2 position;
    public final Rectangle bounds;
    
    public GameObject(float x, float y, float width, float height) {
        this.position = new Vector2(x,y);
        this.bounds = new Rectangle(x-width/2,y-height/2, width, height);
    }
}

其中, position 为 Vector2类型,包含了 x ,y 坐标,代表物体的位置。而 bounds为Rectangle 类型,包含了x,y,width,height信息,代表这个游戏物体在屏幕中的范围。
这里需要注意,postion在这里是指游戏物体的中心点,而bounds中的x ,y 坐标是指这个游戏原素在屏幕坐标系中的作用起点,也就是矩形的左下角顶点。两者是不一样的。


对于游戏中会移动的物体,除了position 和 bounds属性外,还需要添加移动的属性,所以作者又定义了一个继承自GameObject名为DynamicGameObject的类,代码如下:

public class DynamicGameObject extends GameObject {
    public final Vector2 velocity;
    public final Vector2 accel;
    
    public DynamicGameObject(float x, float y, float width, float height) {
        super(x, y, width, height);
        velocity = new Vector2();
        accel = new Vector2();
    }
}

其中,velocity 代表速度,而accel代表加速度,两者都是 Vector2类型,也就是包含了 x, y 轴方向相关的运动信息。
游戏中所有的物体都是继承自两个类,针对不同游戏物体特性再进行扩展。它们分别是:
                Bob ,主角
                Castle ,城堡
                Coin, 金币
                Platform , 平台
                Spring , 弹簧
                Squirrel ,空中飞行的松鼠


下面开始说说主游戏界面。当我们在菜单界面点击Play选项后,就正式开始游戏,而GameScreen类就是主游戏界面的实现类。在这个类中使用了状态机模式来实现游戏的逻辑,状态机模式就是将游戏分成具体的状态,不同的状态实现不同逻辑与画面绘制。简单的说就是用一些静态变量来定义游戏的状态,然后在绘制更新的方法里,通过判断当前的状态来执行不同的方法。例如,在GameScreen类中就定义了如下的状态:

static final int GAME_READY = 0;
static final int GAME_RUNNING = 1;
static final int GAME_PAUSED = 2;
static final int GAME_LEVEL_END = 3;
static final int GAME_OVER = 4;
 
        int state;

可以看出这些状态其实就是一些int 类型的静态成员变量,而它们的名字才真正定义了不同状态。同时在这里还提供了int 类型 的state变量,这个代指当前的状态。

如同其他的Screen一样,GameScreen也是首先执行present()方法进行游戏界面的绘制,然后在update()方法中进行触屏事件的监听。不同是,在这个两个方法里需要针对不同状态进行不同处理。先看一下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) {
caseGAME_READY:
presentReady();
break;
caseGAME_RUNNING:
presentRunning();
break;
caseGAME_PAUSED:
presentPaused();
break;
caseGAME_LEVEL_END:
presentLevelEnd();
break;
caseGAME_OVER:
presentGameOver();
break;
}
batcher.end();
}
这里就可以看出状态机模式是怎么使用的,非常的简单,通过swich 与 case 来判断当前的是什么状态,然后执行不同绘制方法。同样的对于update()方法也是类似的,代码如下 :

@Override public void update (float deltaTime) {
if (deltaTime > 0.1f) deltaTime = 0.1f;

switch(state) {
caseGAME_READY:
updateReady();
break;
caseGAME_RUNNING:
updateRunning(deltaTime);
break;
caseGAME_PAUSED:
updatePaused();
break;
caseGAME_LEVEL_END:
updateLevelEnd();
break;
caseGAME_OVER:
updateGameOver();
break;
}
}

在update()方法中,在不同的状态下需要监听不同位置的触屏事件,然后通过改变state的值,来实现不同游戏状态的切换。比如,在 GAME_READY 状态下(也就是游戏一开始的初始状态),就会执行updateReady()方法,该方法代码如下 :

private void updateReady () {
if (Gdx.input.justTouched()) {
state = GAME_RUNNING;
}
}
该方法表示:只要有触屏事件发生,就把state 切换为 GAME_RUNNING 状态,这样就正式开始了游戏。那么同样的在present()方法里面就开始执行GAME_RUNNING 状态所对应的presentRunning()方法。这就是状态机模式的应用,后面会发现这种模式被大量运用。


下面看一下GameScreen其他的成员变量和构造方法,代码如下:
 OrthographicCamera guiCam;
Vector3 touchPoint;
SpriteBatch batcher;
World world;
WorldListener worldListener;
WorldRenderer renderer;
Rectangle pauseBounds;
Rectangle resumeBounds;
Rectangle quitBounds;
int lastScore;
String scoreString;

public GameScreen (Game game) {
super(game);
state = GAME_READY;
guiCam = new OrthographicCamera(320, 480);
guiCam.position.set(320 / 2, 480 / 2, 0);
touchPoint = new Vector3();
batcher = new SpriteBatch();
worldListener= new WorldListener() {
@Override public void jump () {
Assets.playSound(Assets.jumpSound);
}

@Override public void highJump () {
Assets.playSound(Assets.highJumpSound);
}

@Override public void hit () {
Assets.playSound(Assets.hitSound);
}

@Override public void coin () {
Assets.playSound(Assets.coinSound);
}
};
world= new World(worldListener);
renderer= new WorldRenderer(batcher, world);
pauseBounds = new Rectangle(320 - 64, 480 - 64, 64, 64);
resumeBounds = new Rectangle(160 - 96, 240, 192, 36);
quitBounds = new Rectangle(160 - 96, 240 - 36, 192, 36);
lastScore = 0;
scoreString = "SCORE: 0";
}

        除了 常规的guiCam ,touchPoint, batcher 以及一些用于检测触屏区域的Bounds外,还多了三个其他Screen没有的类,分别是:World,WorldRenderer,以及WorldListener。

这里思路是这样的:由于GameScreen中,涉及的物体比较多,并且操作也都比较复杂,所以作者把GameScreen当成了GUI,只负责游戏状态的监听和处理触屏事件。而真正游戏中的物体都被放置在World类中,并且游戏中物体的绘制则由WorldRender类来处理,另外WorldListener则作为一个播放音效的接口。也就是说,把GameScreen当成一个逻辑框架,让内置的World和WorldRednder来处理游戏的实现。

再进一步分析World和WorldRednder类之前,需要说明一下WorldRednder类 与 GameScreen类采用OrthographicCamera有什么不同。
对于GameScreen,定义OrthographicCamera代码如下:

   guiCam = new OrthographicCamera(320, 480);
guiCam.position.set(320 / 2, 480 / 2, 0); 

显然它指定了和屏幕一样大小的 OrthographicCamera ,并把相机对准屏幕的中心。
但是对于WorldRender类中,定义OrthographicCamera代码如下:

                 static final float FRUSTUM_WIDTH = 10;
         static final float FRUSTUM_HEIGHT = 15;
                   
                 OrthographicCamera cam;

                public WorldRenderer (SpriteBatch batch, World world) {
this.world = world;
this.cam = new OrthographicCamera(FRUSTUM_WIDTH, FRUSTUM_HEIGHT);
this.cam.position.set(FRUSTUM_WIDTH / 2, FRUSTUM_HEIGHT / 2, 0);
this.batch = batch;
}
其中OrthographicCamera 被定义成 宽度为10,高度为15,同样的也把相机对准中心点。

这里定义了两个不同OrthographicCamera 是因为:
首先不管是哪个OrthographicCamera,它的作用都是通过把映射矩阵绑定给SpritBatch,告诉SpritBatch怎么去绘制图形,如下代码:
guiCam.update();
        batcher.setProjectionMatrix(guiCam.combined);
        batcher.begin();
        batcher.draw ``````
        batcher.end();

那么在GameScreen,需要的其实只是一个屏幕大小的平面内做绘制的工作,在这个例子中也就是一个320*480的平面。例如当游戏开始时,会进入GAME_READY状态,这时候屏幕中间会绘制出ready的英文字母;而游戏进行时,在屏幕上方会绘制出分数。如同之前所分析的不同的状态会在屏幕中绘制不同的画面,截图如下:
[案例研究]—superJumper 3.游戏中的物体与主游戏逻辑 - tonmyWu - tonmyWu[案例研究]—superJumper 3.游戏中的物体与主游戏逻辑 - tonmyWu - tonmyWu
 而这些字符的绘制都是GameScreen中present()方法调用已经绑定了guiCam 映射矩阵的SpriteBatch所绘制出来的,关键代码如下:

@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();
}

        private void presentReady () {
batcher.draw(Assets.ready, 160 - 192 / 2, 240 - 32 / 2, 192, 32);   //在屏幕中间绘制Ready字符
}


而WorldRenderer 中 OrthographicCamera的定义就的得先说说游戏是怎么进行的。游戏中,我们的主角Bob会不断进行跳跃,但是他的最高点始终不会超过屏幕的中点,他停留在最高点的过程中会通过移动所有的物体来造成他看上去好像在往上跳,实际上他一直停留在屏幕中点的高度。同时,我们需要为每一关定义一个长度,也就是,需要‘跳’多高才能到达城堡,顺利通关。并且要准备好整一关的过程中,哪里应该出现什么物体,然后根据Bob到达的高度不停的切换这些物体。

想象一下有一段被垂直放置的胶卷,这就是我们的一个关卡,也就是一个World,它准备好了一个关卡的长度,并且设置好了所有的物体。而我们的Bob和WorldRender中的OrthographicCamera 一开始被放置在胶卷的底部,Bob开始不断的跳跃,当超过屏幕中点的高度时则OrthographicCamera 会被往上移动,并且所有进入OrthographicCamera 的物体都会被绘制。直到到达最高点,或Bob死亡。

所以 WorldRender中的OrthographicCamera 被设置成 10 *15 。
并且而在World类中,定义了两个变量:
    public static final float WORLD_WIDTH = 10;
    public static final float WORLD_HEIGHT = 15 * 20;
同样的关卡的宽带也被定义为10个单位,与WorldRender中的一致(因为我们不需要在X方向进行移动);而高度定义成15*20 这就是一关的长度,也就是Bob要'跳'的高度。

注:这里为什么要把 WorldRender中的OrthographicCamera  定义10 *15,实际上就是把屏幕320*480 映射成每个单位为32像素。这是因为游戏中的素材基本都是基于32像素为单位构建,同时屏幕的分辨率也可以被分解成以32像素为单位。


从上面的分析可以得出,分别设置两个不同 OrthographicCamera  ,就是因为不同场景的需求。并且 在WorldRender中的OrthographicCamera 其实也可以被设置成 320*480 只是为了简便,才把单位设为32像素变成 10*15。 所以不管是哪个OrthographicCamera它提供的只是一个映射信息,而这个映射的信息真正的使用者是SpriteBatch,它会根据映射信息的不同来决定究竟要把图片绘制在什么位置上,以及该不该绘制这些元素。

例如,如果在WorldRender中将图片绘制在(10,20),那么它不会被显示出来,因为单位为32像素,显然超出了屏幕的范围(除非OrthographicCamera 往上移动);但是如果是在GameScreen中这么做,那么它就会被精确的绘制在(10,20)处。Camera的使用就是这样的一个道理。

另外,值得一提的是,即使GameScreen和WorldRender使用的是同一个SpriteBatch,只要在恰当的时候绑定相应的投影矩阵,两者是互不影响的。

欢迎转载!请注明原文链接,谢谢! http://tonmly.blog.163.com/blog/static/174712856201163091130535/
0 0
原创粉丝点击