LibGdx练习-像素鸟(五)
游戏界面元素
资源加载完成后我们进入游戏界面。GameScreen继承BaseScreen,拥有一个stage和一个MainGame实例。
先来创建一下我们游戏界面所需要的组成元素。
// 背景
private ImageActor bgImage;
// 地板
private FloorActor floorImage;
// 点击提示
private ImageActor tapTipImage;
// 准备提示
private ImageActor getReadyActor;
// 鸟
private BirdActor birdActor;
// 分
private int score;
// 分数显示
private Label scoreLable;
// 水管集合
private CopyOnWriteArrayList<BarActor> barImageList = new CopyOnWriteArrayList<>();
// 对象池
private Pool<BarActor> barImagePool;
// 初始鸟高度
private float birdStartPositionY;
// 下水管最小值
private float minDownBarTopY;
// 下水管最大值
private float maxDownBarTopY;
// 距离下次生成水管的时间累加器
private float generateBarTimeCounter;
// 游戏状态
private GameState gameState;
// 暂停按钮
private Button button;
// 结束界面
private OverGroup overGroup;
在界面元素中可以看到我们所需要用到的鸟、水管、地板都是我们独立封装过的对象,我们先来看下。
BirdActor
BirdActor描述了鸟的对象,鸟采用了动画的展示形式,那么鸟在飞行和死亡的状态下要有一个神么样的动画展示形式呢?看下源码。
public class BirdActor extends BaseAnimationActor {
/** 当前游戏状态 */
private GameState gameState;
/** 小鸟竖直方向上的速度 */
private float velocityY;
/** 小鸟竖直方向上的重力加速度 */
private float gravity = Res.Physics.GRAVITY;
public BirdActor(MainGame mainGame) {
super(mainGame);
// 创建小鸟动画
Animation animation = new Animation(
0.2F,
AssetsUtil.atlas.findRegions(Res.Atlas.IMAGE_BIRD_YELLOW_01_TO_03)
);
// 动画循环播放
animation.setPlayMode(Animation.PlayMode.LOOP);
// 设置小鸟动画
setAnimation(animation);
// 初始化为准备状态
refreshFrameAndRotation(GameState.ready);
}
@Override
public void act(float delta) {
super.act(delta);
// 在 飞翔状态 或 状态水管后掉落到地板前 才应用物理效应
if (gameState == GameState.fly || gameState == GameState.die) {
/*
* 应用物理效应(简单模拟物理效果, 帧率较低时物理效果的误差可能较大)
* v = v0 + a * t
* s = v0 * t + a * t^2
*/
// 递增速度
velocityY += gravity * delta;
// 递增位移
setY(getY() + velocityY * delta);
}
// 正在飞翔状态时改变小鸟的角度
if (gameState == GameState.fly) {
changeBirdRotation(delta);
}
}
/**
* 根据游戏状态刷新小鸟状态
* @param gameState
*/
public void refreshFrameAndRotation(GameState gameState) {
if (gameState == null || this.gameState == gameState) {
return;
}
this.gameState = gameState;
switch (this.gameState) {
case ready: {
// 准备状态循环播放动画, 帧持续时间为 0.2 秒
setPlayAnimation(true);
setRotation(0);
getAnimation().setFrameDuration(0.2F);
break;
}
case fly: {
// 准备状态循环播放动画, 帧持续时间为 0.18 秒
setPlayAnimation(true);
getAnimation().setFrameDuration(0.18F);
break;
}
case die: {
break;
}
case gameOver: {
// 游戏结束状态停止播放动画, 并固定显示第1帧
setPlayAnimation(false);
setFixedShowKeyFrameIndex(1);
setRotation(-90);
break;
}
case pause:{
setPlayAnimation(false);
break;
}
case resume:{
setPlayAnimation(true);
break;
}
}
}
public float getVelocityY() {
return velocityY;
}
public void setVelocityY(float velocityY) {
this.velocityY = velocityY;
}
/**
* 根据数值方向速度变化值改变小鸟的旋转角度
* @param delta
*/
private void changeBirdRotation(float delta) {
float rotation = getRotation();
rotation += (velocityY * delta);
if (velocityY > 0) {
// 向上飞时稍微加大角度旋转的速度
rotation += (velocityY * delta) * 1.5F;
} else {
// 向下飞时稍微减小角度旋转的速度
rotation += (velocityY * delta) * 0.2F;
}
// 校准旋转角度: -75 <= rotation <= 45
if (rotation < -75) {
rotation = -75;
} else if (rotation > 45) {
rotation = 45;
}
// 设置小鸟的旋转角度
setRotation(rotation);
}
}
我们采用switch语句来进行鸟状态的判断,不同的状态会有不同的动画展现形式。
再者,我们需要对于鸟的飞行轨迹进行判定,虽然相对模型世界来讲鸟一直在向前飞,但是对我们开过过程来说其实不然,相对于屏幕来讲,鸟的横坐标并没有移动过,只是纵坐标的变换。
我们对于actor的判断不应该以逻辑为基准,而是应该以父容器为基准。
我们固定鸟的横坐标,并给与鸟一个始终向下的固定的偏移速度,以此达到我们的预期效果。
除此,为了动画更加流畅真实我们还需要对于鸟的角度变换进行描述,根据不同的速度我们继续小鸟不同的角度变换。
注意,一定要给鸟设定最大最小角度,不然鸟转一圈可就尴尬咯。
BarActor
BarActor用以描述水管actor。
public class BarActor extends BaseImageActor implements Poolable {
/** 水平移动速度, px/s */
private float moveVelocity;
/** 是否是上方水管 */
private boolean isUpBar;
/** 是否已被小鸟通过 */
private boolean isPassByBird;
/** 水管是否在移动 */
private boolean isMove;
public BarActor() {
super(null);
}
public BarActor(MainGame mainGame) {
super(mainGame);
}
@Override
public void act(float delta) {
super.act(delta);
// 如果水管正在移动状态, 递增水管的水平位移(水平移动)
if (isMove) {
setX(getX() + moveVelocity * delta);
}
}
public float getMoveVelocity() {
return moveVelocity;
}
public void setMoveVelocity(float moveVelocity) {
this.moveVelocity = moveVelocity;
}
public boolean isUpBar() {
return isUpBar;
}
public void setUpBar(boolean isUpBar) {
this.isUpBar = isUpBar;
if (this.isUpBar) {
setRegion(AssetsUtil.atlas.findRegion(Res.Atlas.IMAGE_BAR_UP));
} else {
setRegion(AssetsUtil.atlas.findRegion(Res.Atlas.IMAGE_BAR_DOWN));
}
}
public boolean isPassByBird() {
return isPassByBird;
}
public void setPassByBird(boolean isPassByBird) {
this.isPassByBird = isPassByBird;
}
public boolean isMove() {
return isMove;
}
public void setMove(boolean isMove) {
this.isMove = isMove;
}
@Override
public void reset() {
setMove(false);
setPassByBird(false);
}
}
BarActor在移动上很好理解,水平左移即可。
它的核心在于上下水管的判断,空位补充以及碰撞检测。这些东西将影响我们游戏失败或者加分。
FloorActor
再来看FloorActor。
public class FloorActor extends BaseImageActor {
/** 水平移动速度, px/s */
private float moveVelocity;
/** 地板纹理区域 */
private TextureRegion region;
/** 水平偏移量 */
private float offerX;
/** 地板是否在移动 */
private boolean isMove;
public FloorActor(MainGame mainGame) {
super(mainGame);
region = AssetsUtil.atlas.findRegion(Res.Atlas.IMAGE_GAME_FLOOR);
setBounds(0, 0, region.getRegionWidth(), region.getRegionHeight());
}
public float getMoveVelocity() {
return moveVelocity;
}
public void setMoveVelocity(float moveVelocity) {
this.moveVelocity = moveVelocity;
}
public boolean isMove() {
return isMove;
}
public void setMove(boolean isMove) {
this.isMove = isMove;
}
@Override
public void act(float delta) {
super.act(delta);
if (isMove) {
offerX += (delta * moveVelocity);
offerX %= getWidth();
if (offerX > 0) {
offerX -= getWidth();
}
}
}
@Override
public void draw(Batch batch, float parentAlpha) {
// 绘制两次以达到视觉上的循环移动效果
batch.draw(region, getX() + offerX, getY(),Res.FIX_WORLD_WIDTH*2,getHeight());
if (Math.abs(offerX) > 0.001F) {
batch.draw(region, getX() + (getWidth() + offerX), getY(),Res.FIX_WORLD_WIDTH*2,getHeight());
}
}
}
地板的绘制相对来说是比较复杂的,向左根据时间戳移动没什么说的,但是跟水管不同的是水管出现的是完整的,而地板游戏不结束就要一直出现。
当然,我们可以找一个非常长的图片作为地板,但是显然这种方式很蠢笨而且不能一劳永逸。
我们通过地板偏移量和屏幕宽度取余的方式让地板反复横跳,同时重写draw方法进行两次绘制,这样地板就用之不竭啦。
当然,我们的地板还是需要比屏幕宽度大的哦,不然初始就铺不满就谈不到后续绘制了。