LibGDX重建Flappy Bird——(4) 创建游戏对象

原文链接:https://my.oschina.net/u/2432369/blog/610412   在本章,我们将为Flappy Bird项目创建一个真正的场景。该游戏场景由几个具有共同属性和功能的游戏对象组成。但是,这些对象被渲染的方式和行为却各有不同, 简单的 对象直接渲染其所分配的纹理,复杂的对象可能需要多个纹理组合渲染。 创建游戏对象   首先创建AbstractGameObject类,并添加下面代码:
package com.art.zok.flappybird.game.object;

import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;

public abstract class AbstractGameObject {
	
	public Vector2 position;
	public Vector2 dimension;
	public Vector2 origin;
	public Vector2 scale;
	public float rotation;
	public Body body;
	
	public AbstractGameObject() {
		position = new Vector2();
		dimension = new Vector2(1, 1);
		origin = new Vector2();
		scale = new Vector2(1, 1);
		rotation = 0;
	}
	
	public void update(float deltaTime) {
		if(body != null) {
			position.set(body.getPosition());
			rotation = body.getAngle() * MathUtils.radiansToDegrees;
		}
	}
	
	public abstract void render(SpriteBatch batch);
}
  该类存储了对象的位置、尺寸、原点、缩放因子和旋转角度。该类还包含两个方法,update()和render(),这两个方法将分别在控制器和渲染器中调用。因为我们创建的每个对象都需要参与碰撞检测,因此我们在该类中还包含一个BOX2D的Body类型成员变量,在update()中,如果body不等于null则说明我们需要使用BOX2D进行物理仿真,然后我们使用body对象的位置和旋转角度更新该对象的位置和旋转角度。对于渲染方法,我们会为每个对象提供一个特定的实现,因此我们将render()定义为abstract。   这里有个问题,因为BOX2D在LIBGDX中属于扩展内容,因此我们需要先添加相关的库文件,才能消除上述代码存在的错误。添加BOX2D扩展可分为以下几个步骤:
  1. 将gdx-box2d.jar拷贝到FlappyBird项目(libs文件中)。
  2. 将gdx-box2d-native.jar拷贝到FlappyBird-desktop项目。
  3. 将armeabi、armeabi-v7a和x86三个文件夹下的libgdx-box2d.so拷贝到FlappyBird-android项目的相应文件夹中。
  4. 将各项目新添加的库文件加入构建路径中。
创建Brid对象
package com.art.zok.flappybird.game.object;

import com.art.zok.flappybird.game.Assets;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.utils.Array;

public class Bird extends AbstractGameObject {

	protected enum WAVE_STATE {
		WAVE_FALLING, WAVE_RISE
	}
	
	private Array<AtlasRegion> birds;
	
	private float animDuration;
	private Animation birdAnimation;
	private TextureRegion currentFrame;

	private float max_wave_height;
	private float min_wave_height;
	
	private WAVE_STATE waveState = WAVE_STATE.WAVE_RISE;

	public Bird() {
                init((int) (Math.random() * 3));
	}

	// 初始化
	public void init(int selected) {
		if (selected == 1) {
			birds = Assets.instance.bird.bird0;
		} else if (selected == 2) {
			birds = Assets.instance.bird.bird1;
		} else {
			birds = Assets.instance.bird.bird2;
		}
		
		birdAnimation = new Animation(0.1f, birds);
		birdAnimation.setPlayMode(Animation.PlayMode.LOOP);
		
		dimension.set(3.72f, 2.64f);
		position.set(-dimension.x * 1.5f, dimension.y / 2);
		max_wave_height = position.y + 0.7f;
		min_wave_height = position.y - 0.7f;
	}

	@Override
	public void update(float deltaTime) {
		super.update(deltaTime);
		if (body == null) {
			if (waveState == WAVE_STATE.WAVE_FALLING)
				position.y -= 0.05f;
			else if (waveState == WAVE_STATE.WAVE_RISE) {
				position.y += 0.05f;
			}
			if (position.y < min_wave_height) {
				waveState = WAVE_STATE.WAVE_RISE;
			} else if (position.y > max_wave_height) {
				waveState = WAVE_STATE.WAVE_FALLING;
			}
		} 
	}
	
	@Override
	public void render(SpriteBatch batch) {
		animDuration += Gdx.graphics.getDeltaTime();
		currentFrame = birdAnimation.getKeyFrame(animDuration);
		batch.draw(currentFrame.getTexture(), position.x - dimension.x / 2, position.y - dimension.y / 2,
			dimension.x / 2, dimension.y / 2, dimension.x, dimension.y, scale.x, scale.y, rotation,
			currentFrame.getRegionX(), currentFrame.getRegionY(), currentFrame.getRegionWidth(),
			currentFrame.getRegionHeight(), false, false);
	}
}
  首先,毋庸置疑Bird对象是一个动画对象,创建动画我们需要使用Animation类。Animation的构造函数需要一组动画帧作为参数,在Bird的 构造函数中我们首先产生了一个范围在[0-3)之内的随机整数作为init()方法的参数。在init()方法中我们首先使用传递进来的参数从三个可选的资源中选择一个作为Bird对象的动画资源。然后我们创建了一个周期为0.1秒的birdAnimation动画对象,并将其设定为循环 模式 。接下来我们设置了对象的尺寸和位置,这些魔法数是经过比例测算而来的,没有什么借鉴的价值,你也可以设定为其他值。   在ren der方法中,我们首先根据动画经过的时间获得当前帧的纹理,然后使用SpriteBatch.draw方法渲染该帧纹理,注意我们为draw方法的x,y参数传递的是position.x - dimension.x / 2和position.y - dimension.y / 2,也就是说渲染的矩形区域始终以position为中心,这样做是有原因的,后面我们使用BOX2D模拟时获得的旋转角度都是以position为原点的,因此为了更方便我们使用position位置表示Bird对象的中心位置。   从官方Flappy Bird游戏我们可以知道,当没有开始游戏时,Bird对象具有一个上下波动的动画状态。所以我们这里会将每个对象分为未开使模拟(仿真)和已经开始模两个状态进行处理。我们为Bird对象添加了两个枚举常量WAVE_FALLING, WAVE_RISE就是为这一动画服务的。在init()方法中我们初始化了max_wave_height、min_wave_height两个值分别表示波动动画的最大高度和最小高度。在update()方法中,如果body等于null表示没有开始模拟,则执行波动动画,该波动效果是通过两个波动状态互相转换和y轴坐标位置的持续变化实现的。 测试Bird类   测试Bird类之前我们还必须做一些准备工作。首先,修改FlappyBirdMain.Create()方法: public void create() {         Assets.instance.init(new AssetManager());    // 设定日志记录级别        Gdx.app.setLogLevel(Application.LOG_DEBUG);        worldController = new WorldController();        worldRenderer = new WorldRenderer(worldController);        paused = false; }   初始化资源方法Assets.instance.init()必须在一开始就调用,因为下面创建的WorldController实例就需要使用资源内容。   修改相同类的resume()方法: @Override  public void resume() {       paused = false;    Assets.instance.init(new AssetManager()); }   在Android系统中,当应用进入后台时,应用所占有的内存资源很有可能会被系统回收,当重新返回前台时我们应该重新加载资源。   修改WorldController类:
package com.art.zok.flappybird.game;  

import com.art.zok.flappybird.game.object.Bird;
  
public class WorldController {  
    private static final String TAG =   
            WorldController.class.getName();  
    
    public Bird bird;
    
    public WorldController() { 
    	init();
    }  
      
    private void init() { 
    	initBird();
    }  
    
    private void initBird() {
    	if(bird == null) {
    		bird = new Bird();
    	} else {
    		bird.init((int) (Math.random() * 3));
    	}
    }
      
    public void update(float deltaTime) {
    	bird.update(deltaTime);
    }  
}
  我们为WorldController添加了一个bird成员变量和一个私有initBird()方法,我们在initBird()方法中为成员变量bird创建了一个Bird对象。因为Bird对象的构造方法和初始化方法分离所以我们可以通过这个小技巧重用Bird对象,而不需要每次重新开始时都创建新的Bird对象。   在update()方法中我们调用了bird.update()方法更新Bird对象。
  修改WorldRenderer类:
package com.art.zok.flappybird.game;

import com.art.zok.flappybird.util.Constants;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.utils.Disposable;
  
public class WorldRenderer implements Disposable {  
      
    private OrthographicCamera camera;  
    private SpriteBatch batch;  
    private WorldController worldController;  
      
    public WorldRenderer(WorldController worldController) { 
    	this.worldController = worldController;
    	init();
    }  
    private void init() {
    	batch = new SpriteBatch();
		camera = new OrthographicCamera(Constants.VIEWPORT_WIDTH, Constants.VIEWPORT_HEIGHT);
		camera.position.set(0, 0, 0);
		camera.update();
    }  
      
    public void render() { 
    	renderWorld(batch);
    }  
    
    private void renderWorld (SpriteBatch batch) { 
    	batch.setProjectionMatrix(camera.combined);
		batch.begin();
		worldController.bird.render(batch);
		batch.end();
    }
    
    public void resize(int width, int height) { 
    	// 更新camera视口尺寸
		camera.viewportWidth = Constants.VIEWPORT_HEIGHT * width / height; 
		camera.update();
    }  
      
    @Override  
    public void dispose() { 
    	batch.dispose();
    }    
}
  这次为WorldRenderer添加了不少代码,首先我们将构造函数的worldController实例对象的引用保存到成员变量中。然后我们在init()初始化方法中创建了SpriteBatch对象,和OrthographicCamera正交投影相机实例对象,该相机的视口尺寸我们设定为Constants.VIEWPORT_WIDTH和Constants.VIEWPORT_HEIGHT即就是50*50米。接着,将相机的初始位置设定为(0,0,0),也就是世界坐标的原点,最后更新相机的投影矩阵。   我们为Wo rldRenderer类添加了一个私有的renderWorld()方法,然后在render()方法 中调用,该方法被用于渲染游戏中的所有对象。首先我们为SpriteBatch对象关联投影矩阵,接下来调用该对象的begin()方法和end()方法,并在在这两个方法之间调用worldController.bird对像的render()方法渲染bird对象。
  还有,我们在resize()方法中添加了两行代码,首先计算camera.viewportWidth的最新值,然后再更新camera的投影矩阵。这里其实就是我们之前遗留下来的那个视口单位长度不统一的解决地方。首先想象一下,无论窗口尺寸怎么变,我们始终保持视口的高度为50米,当窗口尺寸发生改变时,我们通过竖直方向上每像素等于几米来计算视口的最新宽度即就是width*(Constants.VIEWPORT_HEIGHT/height)。
  最后我们在dispose方法中释放了SpriteBatch对象。   现在可以启动桌面应用测试代码是否正常工作了,下面是一张截图: LibGDX重建Flappy Bird——(4) 创建游戏对象
  因为不能传视频,所以只能看截图了,真实的窗口应该是小鸟可以挥动翅膀并上下波动。 创建Land对象   根据创建Bird时的分析,Land在没有开始模拟时与开始模拟的状态是一样的,都是水平向左以固定的速度移动。但是因为在开始游戏之后,我们需要将整个物理模拟(仿真)过程交给BOX2D处理,但是在没有开始之前我们需要手动处理,所以尽管Land在两种状态相同,但是还是需要分情况处理。首先Land只有一张纹理,其次我们让Land移动基础原理是,从某个固定x轴坐标处向右将Land纹理无缝衔接的绘制三次,每次绘制的宽度和当前屏幕的宽度相同;记录Land向左移动的距离,当移动距离大于等于一个屏幕宽度时,将Land的位置重置于初始x轴位置。
package com.art.zok.flappybird.game.object;

import com.art.zok.flappybird.game.Assets;
import com.art.zok.flappybird.util.Constants;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;

public class Land extends AbstractGameObject {
	private static final float LAND_VELOCITY = -15f; 	// 恒定向左以15m/s的速度移动
	public static final float LAND_HEIGHT = 10.3f;
	
	private AtlasRegion land;
	private float leftMoveDist;
	private float viewWidth;
	public Land() {
		land = Assets.instance.land.land;
		viewWidth = Constants.VIEWPORT_HEIGHT *
				Gdx.graphics.getWidth() / Gdx.graphics.getHeight();
		init();
	}
	
	public void init() {
		float startPosX = -viewWidth / 2;
		dimension.set(viewWidth, LAND_HEIGHT);
		position.set(startPosX, -Constants.VIEWPORT_HEIGHT / 2);
	}
	
	private void wrapLand() {
		if (leftMoveDist >= viewWidth) {
                       if (body == null) {
                            position.x += leftMoveDist;
                       }
                       leftMoveDist = 0;
                }
	}
	
	@Override
	public void update(float deltaTime) {
		wrapLand(); 					// 转换land
		super.update(deltaTime); 		// 更新应用body对象
		if (body == null) { 			// 如果没有body对象自动更新
			position.x += LAND_VELOCITY * deltaTime;
		}
                leftMoveDist += Math.abs(LAND_VELOCITY * deltaTime);
	}
	
	@Override
	public void render(SpriteBatch batch) {
		batch.draw(land.getTexture(), position.x, position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x,
				scale.y, rotation, land.getRegionX(), land.getRegionY(), land.getRegionWidth(), land.getRegionHeight(),
				false, false);
		batch.draw(land.getTexture(), position.x + viewWidth, position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x,
				scale.y, rotation, land.getRegionX(), land.getRegionY(), land.getRegionWidth(), land.getRegionHeight(),
				false, false);
		batch.draw(land.getTexture(), position.x + 2 * viewWidth, position.y, origin.x, origin.y, dimension.x, dimension.y, scale.x,
				scale.y, rotation, land.getRegionX(), land.getRegionY(), land.getRegionWidth(), land.getRegionHeight(),
				false, false);
	}

}
   首先在构造函数中,我们获取到Land的纹理资源并保存到成员变量land。接下来我们计算了一个viewWidth值,根据表达式我们可以看出该值表示当前视口的宽度。但是这里有一个很大的问题,首先Land对象我们只在一开始创建,所以viewWidth的值只能表示启动时视口的宽度,当运行中改变窗口的尺寸时viewWidth并不会得到更新。所以这里我们需要将桌面窗口尺寸设定为不可调整,还有一个让窗口不可调整的重要原因是,Flappy Bird的背景图片是满屏显示到窗口中的,如果窗口尺寸可以改变,则背景可能会被拉伸,因此为了避免失真,窗口尺寸不能随意更改。要将窗口设定为不可更改尺寸,只需要打开FlappyBird-desktop项目的启动类Main,为配置参数cfg添加一行cfg.resizable = false;即可。   初始化函数init()首先为变量startPosX赋予-viewWidth/2,该值既表示第一个land纹理绘制的x轴坐标也表示整个Land对象的x轴初始位置。接下来我们设置了Land对象的尺寸和初始位置。   wrapLand()方法中,我们首先测试Land对象向左移动的距离是否大于viewWidth,如果大于,此时我们应该将其x轴的位置向右移动leftMoveDist距离。   update()方法中我们首先调用wrapLand()方法测试Land位置并重置。然后调用父类update()方法,然而父类的update()方法只有当开始模拟时才有用,所以这里不用考虑,但不能忘记调用。接下来我们测试body是否等于null,如果等于null则手动将x轴向左以LAND_VELOCITY=-15m/s的速度向左移动相应的距离。最后我们更新Land对象向左移动的距离。上面我们要认真考虑一个问题,为什么wrapLand()要在update()的第一行调用呢?试想如果我们的Land向左移动的速度足够大,每一帧都移动相当的距离,那么就产生一个足够引起重视的问题,如果在最后一行调用wrapLand(), 当update()在更新本帧后Land的位置已经超出移动的距离,则我们将其移动到初始化位置,但是此时屏幕显示的还是上一帧Land没有到达指定位置的场景,所以为了避免这种延迟,我们首先调用wrapLand()方法。   最后在render()方法中我们从position位置开始水平衔接连续绘制三个land纹理。 测试Land对象   到现在为止,Land对象已经完成了整个骨架内容,可以通过测试观察他的没有开始模拟的状态。   为WorldController添加代码:
package com.art.zok.flappybird.game;  

import com.art.zok.flappybird.game.object.Bird;
import com.art.zok.flappybird.game.object.Land;
  
public class WorldController {  
    private static final String TAG =   
            WorldController.class.getName();  
    
    public Bird bird;
    public Land land;
    
    public WorldController() { 
    	init();
    }  
      
    private void init() { 
    	initBird();
    	initLand();
    }  
    
    private void initBird() {
    	if(bird == null) {
    		bird = new Bird();
    	} else {
    		bird.init((int) (Math.random() * 3));
    	}
    }
      
    private void initLand() {
    	if(land == null) {
    		land = new Land();
    	} else {
    		land.init();
    	}
    }
    
    public void update(float deltaTime) {
    	bird.update(deltaTime);
    	land.update(deltaTime);
    }  
}
  和Bird对象完全相同,我们为land对象添加了类似的代码,这里就不多解释了。接下来WorldRenderer.renderWorld()方法:
private void renderWorld (SpriteBatch batch) { 
    	batch.setProjectionMatrix(camera.combined);
		batch.begin();
		worldController.bird.render(batch);
		worldController.land.render(batch);
		batch.end();
    }
   可以看到我们在renderWorld中添加了land对象的渲染。现在可以启动应用观察并测试,下面是桌面平台运行画面截图: LibGDX重建Flappy Bird——(4) 创建游戏对象
  我们可以看到如我们所期,小鸟和地面都正常运行。 创建Pipes和Pipe对象
package com.art.zok.flappybird.game.object;

import com.art.zok.flappybird.game.Assets;
import com.art.zok.flappybird.util.Constants;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;

public class Pipes extends AbstractGameObject {
	public static final float PIPE_DISTANCE = 17.24f;
	
	Pipe curPipe;
	Array<Pipe> pipes;
	private float viewWidth;
	
	public Pipes() {
		viewWidth = Constants.VIEWPORT_HEIGHT *
				Gdx.graphics.getWidth() / Gdx.graphics.getHeight();
		pipes = new Array<Pipes.Pipe>();
		init();
	}
	
	public void init() {
		pipes.clear();
		pipes.add(curPipe = new Pipe());
	}
	
	private void testPipeNumberIsTooLarge(int amount) {
		if (pipes != null && pipes.size > amount) {
			pipes.removeIndex(0);
		}
	}
	
	private void wrapPipe() {
		if (curPipe.position.x <= viewWidth - PIPE_DISTANCE) {
			pipes.add(curPipe = new Pipe());
		}
	}

	@Override
	public void update(float deltaTime) {
		wrapPipe();
		for(Pipe pipe : pipes) 
			pipe.update(deltaTime);
		testPipeNumberIsTooLarge(8);
	}
	
	@Override
	public void render(SpriteBatch batch) {
		for(Pipe pipe : pipes) {
			pipe.render(batch);
		}
	}
	
	public class Pipe extends AbstractGameObject {
		private static final float PIPE_WIDTH = 5.72f;
		private static final float CHANNEL_HEIGHT = 10.56f;
		public static final float MIN_PIPE_HEIGHT = 5.27f;
		private static final float PIPE_VELOCITY = -15f; 	// 恒定向左以15m/s的速度移动
		
		private AtlasRegion pipe;
		private float dnPipeHeight;
		private boolean isCollected;
		public Pipe() {
			pipe = Assets.instance.pipe.pipeDownGreen;
			init();
		}
		
		public void init() {
			isCollected = false;
			position.set(viewWidth, Land.LAND_HEIGHT - Constants.VIEWPORT_HEIGHT / 2);
			dimension.set(PIPE_WIDTH, Constants.VIEWPORT_HEIGHT - Land.LAND_HEIGHT);
			
			dnPipeHeight = MathUtils.random(dimension.y - 
					2 * MIN_PIPE_HEIGHT - CHANNEL_HEIGHT) + MIN_PIPE_HEIGHT;
		}
		
		public int getScore() {
			if(isCollected) return 0;
			else {
				isCollected = true;
				return 1;
			}
		}
		
		@Override
		public void render(SpriteBatch batch) {
			// down
			batch.draw(pipe.getTexture(), position.x, position.y + dnPipeHeight - Constants.VIEWPORT_HEIGHT/1.5f,
					origin.x, origin.y, dimension.x, Constants.VIEWPORT_HEIGHT/1.5f, scale.x, scale.y,
					rotation, pipe.getRegionX(), pipe.getRegionY(), pipe.getRegionWidth(), 
					pipe.getRegionHeight(), false, true);
			
			// up
			batch.draw(pipe.getTexture(), position.x, position.y + dnPipeHeight + CHANNEL_HEIGHT,
					origin.x, origin.y, dimension.x, dimension.y/1.5f,
					scale.x, scale.y, rotation, pipe.getRegionX(), pipe.getRegionY(),
					pipe.getRegionWidth(), pipe.getRegionHeight(), false, false);
		}
	}
}
  首先我们创建了一个Pipes类,并且为该类创建了一个Pipe内部类,两个类都继承于AbstractGameObjec。然而Pipes继承该类只是为了公共方法的统一,实际上Pipes并不是游戏中的对象,游戏中真实的对象是Pipe内部类,而Pipes只是管理一组Pipe对象的功能类。   首先我们看Pipe内部类,该类中定义Pipe对象的宽度(PIPE_WIDTH)常量、Pipe对象中间的通道高度(CHANNEL_HEIGHT)常量和最小Pipe高度(MIN_PIPE_HEIGHT)以及Pipe对象左移的速度(PIPE_VELOCITY)常量,大部分都很容易理解。只有一个MIN_PIPE_HEIGHT需要解释一下,我们将游戏中x轴位置相同的上下两个管子定义为一个Pipe对象,而Pipe对象的高度尺寸(dimension.y)我们定义为上下两个管子的高度和中间通道高度三者之和,此处我们定义了一个dnPipeHeight成员变量表示下面管子的随机高度,因为我们知道三者之间的总高度和下面管子的高度,则很容易计算上面管子的高度,所以无需再定义上面管子高度的变量。因为每个管子的长度是随机的,但是为了让每个Pipe对象上下两个管子的高度都不为零,我们为每个管子定义了最小高度MIN_PIPE_HEIGHT。  同样,在Pipe的构造函数中我们首先保存纹理资源,这里我们只使用一个资源即可。然后再init()方法中,我们将成员变量isCollected初始化为false,这里我们将Pipe对象看作道具使用,对原始游戏的分析可知,每次通过一个Pipe对象都会获得1个加分,所以后面我们的计分系统就是将Pipe对象看作道具处理的。接下来设定位置和尺寸。其中viewWidth值我们在外部类中初始化,Pipe为非静态内部类,可以放心使用该值。接下来我们获得Pipe对象中下面管子的随机高度dnPipeHeight,获取随机数首先需要确定随机数的范围,下图可以解释这一点: LibGDX重建Flappy Bird——(4) 创建游戏对象   上面图中只是举了一个极端的场景,当下面管子取得最小高度,上面管子则是最小高度加上最大随机高度。从上图中我们就能得出Pipe对象的dnPipeHeight随机高度计算方法了。  getScore()方法很容易理解,如果没有被收集则返回1分,表示通过了一个Pipe对象,如果收集了则返回0。  分析原始游戏的Pipe对象,在没有开始仿真前,Pipe对象是不会移动的,所以我们这里就先不需要重写父类的update()方法了。  render()方法绘制了上下两个管子。大部分都很简单,需要解释一点,因为pipe纹理资源只有一个,所以我们必须将纹理绘制的高度和宽度统一起来,看起来才能一致,所以,这里我们将其绘制的高度设定为Constants.VIEWPORT_HEIGHT/1.5f。   接下来我们需要分析一下Pipes类,如果你搞清楚了Pipe类的原理,Pipes非常容易理解。他只是管理一组Pipe对象,在构造函数计算viewWidth,然后再init()中创建一个Pipe对象的列表对象,并添加第一个对象。wrap()方法的原理是如果最后创建的那个Pipe对象向左移动了PIPE_DISTANCE距离,我们就在x轴等于viewWidth处创建一个新的Pipe对象,因为viewWith处已经超出了相机的视口范围,所以一开始我们是看不到,只有当游戏真正开始时才能观察到。至于testPipeNumberIsTooLarge()方法是为了及时释放超出范围的Pipe对象所占用的内存。update()和render()分别对管理的Pipe对象进行了更新和渲染。 测试Pipes和Pipe对象   因为我们还没有添加BOX2D模拟,所以暂时Pipe对象是看不到的,所以为了能测试Pipe对象的代码是否正常工作,我们重写一下Pipe内部类的update()方法:
@Override
		public void update(float deltaTime) {
			super.update(deltaTime);
			if(body == null) {
				position.x += PIPE_VELOCITY * deltaTime;
			}
		}
  这里其实和Bird一样,如果没有开始模拟则手动让他向左移动。   接下来为WorldCotroller添加相应的成员变量并修改方法:
package com.art.zok.flappybird.game;  

import com.art.zok.flappybird.game.object.Bird;
import com.art.zok.flappybird.game.object.Land;
import com.art.zok.flappybird.game.object.Pipes;
  
public class WorldController {  
    private static final String TAG =   
            WorldController.class.getName();  
    
    public Bird bird;
    public Land land;
    public Pipes pipes;
    
    public WorldController() { 
    	init();
    }  
      
    private void init() { 
    	initBird();
    	initLand();
    	initPipes();
    }  
    
    private void initBird() {
    	if(bird == null) {
    		bird = new Bird();
    	} else {
    		bird.init((int) (Math.random() * 3));
    	}
    }
      
    private void initLand() {
    	if(land == null) {
    		land = new Land();
    	} else {
    		land.init();
    	}
    }
    
    private void initPipes() {
    	if(pipes == null) {
    		pipes = new Pipes();
    	} else {
    		pipes.init();
    	}
    }
    
    public void update(float deltaTime) {
    	bird.update(deltaTime);
    	land.update(deltaTime);
    	pipes.update(deltaTime);
    }  
}
  完全类似的方法,所以不用再解释。下面为WorldRenderer.renderWorld()方法添加Pipes对象的渲染过程:
private void renderWorld (SpriteBatch batch) { 
    	batch.setProjectionMatrix(camera.combined);
		batch.begin();
		worldController.bird.render(batch);
		worldController.land.render(batch);
		worldController.pipes.render(batch);
		batch.end();
    }
  现在启动桌面应用并测试: LibGDX重建Flappy Bird——(4) 创建游戏对象   在上图中可以看到Pipe对象覆盖了Land和Bird对象。很明显,这里我们将渲染的顺序搞错了,首先应该渲染Pipes对象,然后是Land,最后是Bird,所以修改WorldRenderer.renderWorld()方法如下:
private void renderWorld (SpriteBatch batch) { 
    	batch.setProjectionMatrix(camera.combined);
		batch.begin();
		worldController.pipes.render(batch);
		worldController.land.render(batch);
		worldController.bird.render(batch);
		batch.end();
    }
LibGDX重建Flappy Bird——(4) 创建游戏对象   现在看起来不错哦,所有对象都能正确渲染了,但是上述Pipe的update()方法只是为测试修改的,所以测试完成后应该删除。 添加背景   我们有两张可用的纹理背景,所以每次开始都需要随机选取一张然后满屏显示在整个窗口最里面。   为了统一起见,我们还是应该将背景纹理作为一个对象放在WorldController中进行管理。所以修改WorldController类获得背景资源然后再次修改WorldRenderer.renderWorld()方法添加背景渲染代码:
public Bird bird;
    public Land land;
    public Pipes pipes;
    public AtlasRegion background; 
    private void init() { 
    	background = Assets.instance.decoration.bg.random();
    	initBird();
    	initLand();
    	initPipes();
    }
  首先我们为WorldController添加了一个成员变量background,然后再init()方法中初始化他,这里我们使用了Array<AtlasRegion>.random()方法随机获得一个背景纹理。接下来修改WorldRenderer.renderWorld()方法:
private void renderWorld (SpriteBatch batch) { 
    	batch.setProjectionMatrix(camera.combined);
		batch.begin();
		batch.draw(worldController.background,
				-camera.viewportWidth / 2, -camera.viewportHeight / 2, 
				camera.viewportWidth, camera.viewportHeight);
		worldController.pipes.render(batch);
		worldController.land.render(batch);
		worldController.bird.render(batch);
		batch.end();
    }
  因为背景是最底层的对象,所以我们需要将背景放在最前面绘制。这里draw()方法的参数分别是视口的左下角和视口的宽高,这样做总能使背景平铺到整个窗口。最后测试运行应用,下图是测试截图: LibGDX重建Flappy Bird——(4) 创建游戏对象   本章内容到现在就全部介绍完了,我们成功的创建了三个对象,并成功的将三个对象和背景渲染到场景内,下一章我们将详细介绍BOX2D的物理仿真和碰撞检测过程。
 






转载于:https://my.oschina.net/u/2432369/blog/610412

上一篇:LibGDX重建Flappy Bird——(8)屏幕切换与播放声音(终结)


下一篇:LibGDX重建Flappy Bird——(2) 创建游戏框架