(一)— 雏形
这个系列主要讲述了如何使用Cocos2D编写简单的游戏。稍微读读感觉不错,所以想写个libgdx版本的。
本篇文章主要讲述基本内容的编写,包括显示人物、怪兽和飞镖。
最终效果如下图:

获取libgdx
你可以从libgdx的官网下载打包好的代码,我下载的是0.98版本。

当然,你也可以从git代码仓库获取最新的版本的,或者你习惯使用的以前版本,比如0.97。
创建项目
libgdx项目的创建可以有多种方式,我推荐使用setup-ui。方便易用还可以省去很多麻烦,特别是ADT升级以后的ClassNotFound问题。
如果是下载打包好的,那么就默认包含了gdx-setup-ui,双击就可以打开。

填写一些基本信息,然后选中你下载的0.98.zip那个压缩文件。这里我只生成一个桌面项目和Android项目。
桌面项目是方便调试,而Android项目是最后发布的。在整个开发中我始终用桌面项目调试,因为速度快,容易排错。同时周期性的在Android真机上测试。

点击生成项目,然后在Eclipse中导入。

一般导入进去以后Android项目会有一些问题,修改project.properties文件和AndroidManifest.xml配置文件。
运行效果如下:

准备工作
本例子中用到的图片如下:

用gdx-texturepacker打包成一张大图。

我整个例子都是用的是Stage模式。所以它的坐标原点在左下角,如果是一般用Spirte直接绘制,那么原点在右上角。
首先将打包好的图册复制到assets中新建的pack文件夹。
然后我们开始动工了,首先删除setup-ui生成的多余代码,整理DartsShaSha.java文件如下:
package com.cnblogs.htynkn;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.scenes.scene2d.Stage;
public class DartsShaSha extends ApplicationAdapter {
Stage stage;
@Override
public void create() {
stage = new Stage(480, 320, true);
}
@Override
public void dispose() {
stage.dispose();
}
@Override
public void render() {
Gdx.gl.glClearColor(1, 1, 1, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
stage.act();
stage.draw();
}
}
|
这时候运行效果是一个白茫茫的画面。

注意一下这句
stage = new Stage(480, 320, true); |
因为我希望屏幕的自适应有Stage自动完成,所以坐标基本可以写死。
先不着急开工,我们先添加一个现实FPS的标签。我希望这个标签显示在屏幕右下角。
在create方法中添加
LabelStyle labelStyle = new LabelStyle(new BitmapFont(), Color.BLACK); //创建一个Label样式,使用默认黑色字体
Label label = new Label("FPS:", labelStyle); //创建标签,显示的文字是FPS:
label.setName("fpsLabel"); //设置标签名称为fpsLabel
label.setY(0); //设置Y为0,即显示在最下面
label.setX(480 - label.getTextBounds().width); //设置X值,显示为最后一个字紧靠屏幕最右侧
stage.addActor(label); //将标签添加到舞台
|
在render方法中更新fps的值
Label label = (Label) stage.getRoot().findActor("fpsLabel"); //获取名为fpsLabel的标签
label.setText("FPS:" + Gdx.graphics.getFramesPerSecond());
label.setX(480 - label.getTextBounds().width);
//更新X值以保证显示位置正确性 |
效果如下:
添加忍者
现在来添加我们的主角,我希望主角显示在屏幕左侧中央。所以它的x值必然是0,但是它的y值并不是320的一半,而是160减去图片高度的一半。
主角其实就是一张图片,并没有太多特别的效果,所以我使用Image类。
首先获取图册
TextureAtlas atlas = new TextureAtlas("pack/default.pack"); |
在从图册中获取Player.png并创建Image对象。
Image man = new Image(atlas.findRegion("Player")); //获取图册中的Player.png并创建image对象
man.setX(0);
man.setY(160 - man.getHeight() / 2); //设置Y值,以让图片在中间显示
stage.addActor(man); //将主角添加到舞台 |
效果如下:

添加怪兽
然后我们来添加几只怪兽。怪兽应该是随机从屏幕右侧出现,并直线移动到屏幕左侧。
同时我们还要检测怪兽的生命值什么的,或者其他效果,所以为了方便处理,我们专门建立一个Group来管理怪兽。
新建类TargetGroup,并集成Group类。
package com.cnblogs.htynkn;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
public class TargetGroup extends Group {
public TargetGroup(AtlasRegion region) {
super();
}
}
|
因为还需要传入怪兽的图片,所以我们的创建方法保留参数AtlasRegion
region。
怪兽的Y值因为是随机的,但是又不能超出屏幕。所以用随机数来生成。libgdx的MathUtils提供了相关方法。
int minY = 0;
int maxY = (int) (320 - region.getRegionHeight());
int tempY = MathUtils.random(minY, maxY);
|
这里还有一个问题需要注意,就是怪兽之间不应该出现遮挡,所以对于生成的Y值还需要进行判断。
假设我们要生成3只怪兽,那么代码应该如下:
int tempY = 0;
for (int i = 0; i < 3; i++) {
Image image = new Image(region);
image.setX(480 - image.getWidth());
// 开始判断Y值是否符合要求
boolean flag = false;
do {
flag = false;
tempY = MathUtils.random(minY, maxY); // 生成Y值
Actor[] actors = this.getChildren().begin(); // 获取当前已有的怪兽对象
for (int j = 0; j < this.getChildren().size; j++) {
Actor tempActor = actors[j];
if (tempY == tempActor.getY()) { // 如果Y值相等,比如重合,所以舍弃,重新生成
flag = true;
break;
} else if (tempY < tempActor.getY()) { // 如果生成的Y值小于当前怪兽的Y值,
则判断生成的Y值加上高度后是否合适
if ((tempY + region.getRegionHeight()) >= tempActor
.getY()) {
flag = true;
break;
}
} else { // 如果生成的Y值大于当前怪兽的Y值,则判断当前怪兽的Y值加上高度后是否合适
if (tempY <= (tempActor.getY() + region
.getRegionHeight())) {
flag = true;
break;
}
}
}
} while (flag);
image.setY(tempY);
this.addActor(image); //添加到组中
|
在主类的create方法中添加
TargetGroup group = new TargetGroup(atlas.findRegion("Target"));
stage.addActor(group);
|
效果如下:

目前怪兽还不能移动,这里需要一个简单的动画效果,libgdx中的Action可以办到。
考虑到怪兽是水平移动,即Y值不变,X值变小。
所以添加一个方法
public void AddMove(Actor actor, float time) {
actor.addAction(Actions.moveTo(0, actor.getY(), time));
}
|
怪兽的移动速度也随机一下,代码如下
image.setY(tempY);
this.AddMove(image, MathUtils.random(3f, 8f)); //怪兽移动效果
this.addActor(image); //添加到组中
|
效果如下:

添加武器
我们的主角自然不能赤手空拳和怪兽进行搏斗,现在添加一些飞镖。
假定用户触摸屏幕以后,主角就向触摸位置发射一个飞镖。
因为飞镖的数量不一定,所以我这里创建一个专门的类ProjectileFactory来处理。
首先是飞镖的创建,和怪兽群一样的原因,我还是希望一个专门的组来管理。
创建一个专门的方法来创建飞镖
public static Image createProjectile(AtlasRegion region, Actor man,
Vector3 target) {
Image image = new Image(region);
image.setX(man.getX() + man.getWidth() / 2);
image.setY(man.getY() + man.getHeight() / 2);
image.addAction(Actions.moveTo(target.x, target.y, 2f)); //设置飞镖的移动
return image;
}
|
在主类进行一些修改以便其可以获取屏幕的触摸。
首先修改类声明为
public class DartsShaSha extends InputAdapter implements ApplicationListener |
其实具体也可以两个都实现接口,主要是我觉得看着不舒服。
重写touchDown方法
public boolean touchDown(int screenX, int screenY, int pointer, int button) {
Vector3 vector3 = new Vector3(screenX, screenY, 0);
stage.getCamera().unproject(vector3); // 坐标转化
projectiles.addActor(ProjectileFactory.createProjectile(
atlas.findRegion("Projectile"), man, vector3)); // 添加新飞镖到飞镖组
return true;
}
|
在create方法中添加新的Group并设置Input响应。
stage.addActor(projectiles); //添加飞镖组到舞台
InputMultiplexer multiplexer = new InputMultiplexer(); //多输入接收器
multiplexer.addProcessor(this); //添加自身作为接收
multiplexer.addProcessor(stage); //添加舞台
Gdx.input.setInputProcessor(multiplexer); //设置多输入接收器为接收器
|
效果如下:

更完善的飞镖
飞镖虽然添加出来了,但是飞镖没有转动…而且飞镖没有在到达目的地后自动消失。
现在先来添加旋转效果,libgdx提供了rotateBy方法。
在创建飞镖的createProjectile方法中添加
image.addAction(Actions.repeat(50, Actions.rotateBy(360, 0.5f))); //设置飞镖的旋转 |
这个不方便截图,就不展示效果了。
现在来考虑如何让飞镖到达目的后消失。首先来看看我们的Image对象,它包含了两个Action,一个是旋转Action,另外一个移动Action。
我们可以检测Action的数量,如果只有一个Action,我们可以断定飞镖只是在旋转而已经到达目的地了。这个时候就可以把它删除了。
添加一个专门的方法来判断飞镖是否应该移除了
public static Boolean checkAlive(Actor projectile) {
if (projectile.getActions().size == 1) {
return false;
}
return true;
}
|
在render方法中添加处理代码
// 飞镖的移除判
Actor[] projectile = projectiles.getChildren().begin(); //获取Actor数组
for (int j = 0; j < projectiles.getChildren().size; j++) { //移除判断
Actor actor = projectile[j];
if (!ProjectileFactory.checkAlive(actor)) {
projectiles.removeActor(actor);
}
}
|
效果如下:

现在飞镖可以自动消失了,并且也在旋转了。不过旋转效果很奇怪,它并不是沿中心旋转,而是沿着左下角旋转的。
重新设置中心
image.setOrigin(image.getWidth() / 2, image.getHeight() / 2); |
现在一切正常了。
碰撞检测和杀敌
当然,发出飞镖的目的自然是杀敌,现在马上来添加这个功能。
我们可以把怪兽看着一个矩形,即飞镖击中任何位置都算作有效。而飞镖就以其中心为代表。
创建方法attackAlive
public static Boolean attackAlive(Actor target, Actor projectile) {
Rectangle rectangle = new Rectangle(target.getX(), target.getY(),
target.getWidth(), target.getHeight()); // 创建一个矩形
return rectangle.contains(
projectile.getX() + projectile.getWidth() / 2,
projectile.getY() + projectile.getHeight() / 2); //判断是否在矩阵中,即是否击中
}
|
在render方法中修改
// 开始处理飞镖
Actor[] projectile = projectiles.getChildren().begin();
Actor[] targets = targetGroup.getChildren().begin();
for (int i = 0; i < projectiles.getChildren().size; i++) {
Actor actor = projectile[i];
for (int j = 0; j < targetGroup.getChildren().size; j++) {
Actor target = targets[j];
if (ProjectileFactory.attackAlive(target, actor)) {
targetGroup.removeActor(target);
projectiles.removeActor(actor);
break;
}
}
}
// 如果飞镖已经飞到则刪除
projectile = projectiles.getChildren().begin();
for (int j = 0; j < projectiles.getChildren().size; j++) {
Actor actor = projectile[j];
if (!ProjectileFactory.checkAlive(actor)) {
projectiles.removeActor(actor);
}
}
|
效果如下:
写在最后
虽然实现了个大概,但是仔细看看其实问题还是很多的,后面的文章会提到进一步的修改。包括逻辑上的完善,声音效果,预加载,背景绘制,集成第三方社交和广告等等。
这片文章对应demo可以从这里下载来试试。
http://pan.baidu.com/share/link?shareid=328840&uk=4127624209
我用的2.2的sdk编译的,低版本没有测试。我的手机是ZTE V880,fps50上下。
ps:testin的测试结果是通过率 100.00%
ps: 代码上传到github上了,地址 https://github.com/htynkn/DartsShaSha
文章对应的代码的tag为page 1。
(二)— 完善
完善飞镖逻辑
现在的飞镖可以旋转可以飞行了,但是有一个问题却没有解决。
首先飞镖的速度,如果用户触摸位置很靠近左侧,那么飞镖的速度就很慢了。
其次,如果用户触摸中间位置,默认情况下飞镖应该是朝那个方向飞行,而不是飞到触摸位置就消失了。
这里的处理办法很简单,就是根据用户触摸位置,算出一个X为480的值,这样飞镖就可以飞到最右侧,同时保持相当的速度。
在createProjectile方法中添加
float r = (target.y - image.getY()) / (target.x - image.getX()); //获取斜率
float detY = r * 480; //获取Y的变动量
image.addAction(Actions.moveTo(480 + image.getX(), detY + image.getY(),
2f)); // 设置飞镖的移动
|
这样基本就解决了问题。
接下来来思考飞镖的数量和相应位置。
首先飞镖的速度一定要得到限制,不然满屏幕飞镖有什么意思。这里限制飞镖的数量为5个。
在touchDown的最开始添加
if (projectiles.getChildren().size >= 5) { //限制飞镖的数量为5个
return false;
}
|
这样当屏幕上的飞镖数量大于等于5时就不会产生新的飞镖了。
还有就是触摸的位置,如果触摸的位置太靠右的话,会出现飞镖倒飞或者速度过快的问题,所以当触摸位置太靠近左侧的时候就不释放飞镖。
在touchDown方法中添加
if (vector3.x < man.getX() + 5) { //如果触摸太靠近左侧就不响应
return false;
}
|
这里的5是我随便写的,仅仅表示个意思。测试一下,觉得5还是太小了,最后我改成10了。
更带感的对手
说实话,现在的对手一动不动,只会匀速平移。我们先改进它的外貌吧。
我从http://untamed.wild-refuge.net/rmxpresources.php?characters获取到如下图片
打包以后放入assets文件夹中。
因为libgdx只有默认Animation类,但是没法办法直接在stage中使用,所以新建一个Scythe类并继承Actor类。
public Scythe(AtlasRegion atlasRegion) {
super();
this.setWidth(titleWidth); //设置高度
this.setHeight(titleHeight); //设置宽度
TextureRegion[][] temp = atlasRegion.split(titleWidth, titleHeight); //分割图块
walkFrames = new TextureRegion[4]; //获取第二行的4帧
for (int i = 0; i < 4; i++) {
walkFrames[i] = temp[1][i];
}
animation = new Animation(0.1f, walkFrames); //创建动画,帧间隔0.1
}
|
因为原图宽200,高192,一共16张图,所以每一块的宽就是50,高48。使用Animation类需要手动提供相关帧,并通过Animation和当前时间获取的帧。
重写draw方法如下
@Override
public void draw(SpriteBatch batch, float parentAlpha) {
stateTime += Gdx.graphics.getDeltaTime(); //获取总时间
currentFrame = animation.getKeyFrame(stateTime, true); //获取当前关键帧
batch.draw(currentFrame, this.getX(), this.getY(), this.getWidth(),
this.getHeight()); //绘制
}
|
修改TargetGroup中有关region.getRegionHeight()的部分,全部除以4。同时修改Image类Scythe类。
最后完整的Scythe如下
package com.cnblogs.htynkn;
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.scenes.scene2d.Actor;
public class Scythe extends Actor {
TextureRegion[] walkFrames; // 保存每一帧
Animation animation; // 动画类
float stateTime; // 总时间
TextureRegion currentFrame; // 当前帧
int titleWidth = 50; // 声明块宽度
int titleHeight = 48; // 声明块高度
public Scythe(AtlasRegion atlasRegion) {
super();
this.setWidth(titleWidth); // 设置高度
this.setHeight(titleHeight); // 设置宽度
TextureRegion[][] temp = atlasRegion.split(titleWidth, titleHeight); // 分割图块
walkFrames = new TextureRegion[4]; // 获取第二行的4帧
for (int i = 0; i < 4; i++) {
walkFrames[i] = temp[1][i];
}
animation = new Animation(0.1f, walkFrames); // 创建动画,帧间隔0.1
}
@Override
public void draw(SpriteBatch batch, float parentAlpha) {
stateTime += Gdx.graphics.getDeltaTime(); // 获取总时间
currentFrame = animation.getKeyFrame(stateTime, true); // 获取当前关键帧
batch.draw(currentFrame, this.getX(), this.getY(), this.getWidth(),
this.getHeight()); // 绘制
}
}
|
效果如下:

添加血条
我们来试试在给予怪兽血量,即有些怪兽可以承受两次伤害。这里我们将用到比较基本的东西。
首先是血条的位置,一般来看应该在怪兽正上方,以红色显示。
绘制可以用很多种方法,我不怎么习惯mesh那套,所以这里我使用Pixmap类。
先在Sythe类中添加两个变量
int margin = 2; // 血条和人物之间的间隔
int pixHeight = 5; // 血条高度
|
然后在绘制方法中添加
Pixmap pixmap = new Pixmap(64, 8, Format.RGBA8888); //生成一张64*8的图片
pixmap.setColor(Color.BLACK); //设置颜色为黑色
pixmap.drawRectangle(0, 0, titleWidth, pixHeight); //绘制边框
Texture pixmaptex = new Texture(pixmap); //生成图片
TextureRegion pix = new TextureRegion(pixmaptex, titleWidth, pixHeight); //切割图片
batch.draw(pix, this.getX(), this.getY() + this.titleHeight
+ this.margin); //绘制
|
这样我们就有了一个黑色的边框了

然后就是血量的填充了,在添加两个变量以记录总血量和当前血量
int maxHp; // 总血量
int currentHp; // 当前血量
|
绘制血条的代码添加到绘制完黑框的语句后面
pixmap.setColor(Color.RED); // 设置颜色为红色
pixmap.fillRectangle(0, 1, titleWidth * currentHp / maxHp,
pixHeight - 2); // 绘制血条
|
最后一定要释放掉pixmap
pixmap.dispose();
这是设置总血量为2,当前血量为1的效果

控制转换
增加了血量以后我们的代码也需要修改了,在主类DartsShaSha中修改相关的逻辑。
为了方便起见我们将游戏的逻辑赋给控制器。
先新建一个IController
package com.cnblogs.htynkn.controller;
import com.badlogic.gdx.scenes.scene2d.Group;
import com.badlogic.gdx.scenes.scene2d.Stage;
public abstract class IController extends Group {
public abstract void update(Stage stage);
}L>
|
然后新建TargetController类,重写update方法,在这个方法中我们处理相关逻辑,然后在主类中只需要调用方法就可以。
首先将中已有的代码拷贝过来,然后在Sythe中添加两个方法来处理受到伤害和死亡判断。
public void beAttacked(int damage) {
if (this.currentHp > damage) { // 如果血量大于伤害就扣除响应数值
currentHp = currentHp - damage;
} else if (this.currentHp > 0) { // 如果血量小于伤害但是仍有血量就让血量归零
currentHp = 0;
}
}
public Boolean isAlive() {
return this.currentHp > 0;
}
|
然后在TargetController中添加update的相关代码
public void update(Stage stage) {
Group projectiles = (Group) stage.getRoot().findActor("projectiles"); // 获取飞镖所在Group
Actor[] projectile = projectiles.getChildren().begin();
Actor[] targets = this.getChildren().begin();
for (int i = 0; i < projectiles.getChildren().size; i++) {
Actor actor = projectile[i];
for (int j = 0; j < this.getChildren().size; j++) {
Actor target = targets[j];
if (ProjectileFactory.attackAlive(target, actor)) {
Scythe scythe = (Scythe) target;
scythe.beAttacked(1);
projectiles.removeActor(actor);
if (!scythe.isAlive()) {
this.removeActor(target);
}
break;
}
}
}
}
|
然后在主类中修改相关的实例化代码,在render中调用update方法。
targetController.update(this.stage); //调用update方法,处理怪兽的逻辑 |
效果如下:

飞镖的杀伤力和手势识别
上面的代码中每一个飞镖的杀伤力是1,每一个怪兽的血量是2。
现在我们来修改一下飞镖的控制,让飞镖也采用控制器来处理。
在这里设定为触摸一下屏幕是发射一般的飞镖,杀伤力为1。而长按以后的杀伤力是2。
libgdx中提供了一个手势识别的接口,实现它就可以了。这里我选择继承GestureAdapter。
public class DartsListener extends GestureAdapter |
同时修改飞镖的控制为控制器模式,新建控制器DartsController。
代码如下:
package com.cnblogs.htynkn.controller;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.cnblogs.htynkn.elements.Dart;
public class DartsController extends IController {
AtlasRegion region;
@Override
public void update(Stage stage) {
// 如果飞镖已经飞到则刪除
Actor[] projectile = this.getChildren().begin();
for (int j = 0; j < this.getChildren().size; j++) {
Actor actor = projectile[j];
if (!this.checkAlive(actor)) {
this.removeActor(actor);
}
}
}
public DartsController(AtlasRegion region) {
this.region = region;
}
public void AddDarts(Dart dart) {
if (this.getChildren().size >= 5) { //如果飞镖数量大于等于5个就结束
return;
}
float r = (dart.getTarget().y - dart.getY())
/ (dart.getTarget().x - dart.getX()); // 获取斜率
float detY = r * 480; // 获取Y的变动量
dart.addAction(Actions.moveTo(480 + dart.getX(), detY + dart.getY(), 2f)); // 设置飞镖的移动
this.addActor(dart);
}
public Boolean attackAlive(Actor target, Actor projectile) {
Rectangle rectangle = new Rectangle(target.getX(), target.getY(),
target.getWidth(), target.getHeight()); // 创建一个矩形
return rectangle.contains(
projectile.getX() + projectile.getWidth() / 2,
projectile.getY() + projectile.getHeight() / 2); // 判断是否在矩阵中,即是否击中
}
public Boolean checkAlive(Actor projectile) {
if (projectile.getActions().size == 1) {
return false;
}
return true;
}
public Dart createDart() {
return new Dart(region);
}
}
|
其中Dart类代码如下:
package com.cnblogs.htynkn.elements;
import com.badlogic.gdx.graphics.g2d.TextureAtlas.AtlasRegion;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
public class Dart extends Image {
Vector2 target;
public Dart(AtlasRegion region) {
super(region);
this.setOrigin(getWidth() / 2, getHeight() / 2);
this.addAction(Actions.repeat(50, Actions.rotateBy(360, 0.5f)));
}
public void setTarget(Vector2 target) {
this.target = target;
}
public void setTarget(float x, float y) {
this.target = new Vector2(x, y);
}
public Vector2 getTarget() {
return target;
}
}
|
因为我们的输入接受另外有类DartsListener来处理,所以修改主类的继承如下:
public class DartsShaSha implements ApplicationListener |
在multiplexer中添加我们新的手势识别器
GestureDetector gestureDetector = new GestureDetector(
new DartsListener(this.stage));
multiplexer.addProcessor(gestureDetector); // 添加手势识别 |
目前DartsListener中代码如下
package com.cnblogs.htynkn.listener;
import com.badlogic.gdx.input.GestureDetector.GestureAdapter;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.cnblogs.htynkn.controller.DartsController;
import com.cnblogs.htynkn.elements.Dart;
public class DartsListener extends GestureAdapter {
Stage stage;
public DartsListener(Stage stage) {
this.stage = stage;
}
@Override
public boolean touchDown(float x, float y, int pointer, int button) {
DartsController dartsController = (DartsController) stage.getRoot()
.findActor("dartsController");
if (dartsController.getChildren().size >= 5) { // 限制飞镖的数量为5个
return false;
}
Vector3 vector3 = new Vector3(x, y, 0);
stage.getCamera().unproject(vector3); // 坐标转化
Actor man = stage.getRoot().findActor("player");
if (vector3.x < man.getX() + 10) { // 如果触摸太靠近左侧就不响应
return false;
}
Dart dart = dartsController.createDart();
dart.setX(man.getX() + man.getWidth() / 2);
dart.setY(man.getY() + man.getHeight() / 2);
dart.setTarget(vector3.x, vector3.y);
dartsController.AddDarts(dart);
return true;
}
@Override
public boolean longPress(float x, float y) {
return true;
}
}
|
可能还有其他细节的修改,详细的请参考代码。
目前我们只能算是重构了一下,游戏效果并没有改变。现在来设置飞镖的杀伤力和长按的处理。
在Dart中添加属性
在实例化中添加
将TargetController中的
修改为
scythe.beAttacked(dart.getPower()); |
而中的longPress方法基本和touchDown相同,只是增加了
dart.setPower(2); //设置杀伤力为2
dart.setColor(Color.RED); //设置成红色 |
来思考一下处理流程,用户触摸屏幕,首先会触发tap事件,然后是touchDown,最后才是longPress。
也就是目前我们的游戏长按一下会发出一个普通的飞镖,然后才是我们的红色飞镖。
要处理这个问题,我们添加一个DartsDetector类,继承GestureDetector类。
因为事件的触发顺序是tap->touchdown->longpress->touchup。
所以我们的事件逻辑全部转移到touchup中,如果是longpress事件就发出红色飞镖,如果是touchdown就发出普通飞镖。
由于我们的DartsListener已经不处理任何逻辑了,所以删除其中所有代码。
GestureDetector中的代码如下
package com.cnblogs.htynkn.listener;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.input.GestureDetector;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.cnblogs.htynkn.controller.DartsController;
import com.cnblogs.htynkn.elements.Dart;
public class DartsDetector extends GestureDetector {
Stage stage;
public DartsDetector(Stage stage, GestureListener listener) {
super(listener);
this.stage = stage;
}
@Override
public boolean touchUp(float x, float y, int pointer, int button) {
DartsController dartsController = (DartsController) stage.getRoot()
.findActor("dartsController");
if (dartsController.getChildren().size >= 5) { // 限制飞镖的数量为5个
return false;
}
Vector3 vector3 = new Vector3(x, y, 0);
stage.getCamera().unproject(vector3); // 坐标转化
Actor man = stage.getRoot().findActor("player");
if (vector3.x < man.getX() + 10) { // 如果触摸太靠近左侧就不响应
return super.touchUp(x, y, pointer, button);
}
Dart dart = dartsController.createDart();
dart.setX(man.getX() + man.getWidth() / 2);
dart.setY(man.getY() + man.getHeight() / 2);
dart.setTarget(vector3.x, vector3.y);
if (this.isLongPressed()) { //如果是长按就变成红色飞镖
dart.setPower(2); // 设置杀伤力为2
dart.setColor(Color.RED); // 设置成红色
}
dartsController.AddDarts(dart);
return super.touchUp(x, y, pointer, button);
}
}
|
效果如下:

写在最后
虽然实现了个大概,但是仔细看看其实问题还是很多的,后面的文章会提到进一步的修改。包括逻辑上的完善,声音效果,预加载,背景绘制,集成第三方社交和广告等等。
这片文章对应demo可以从这里下载来试试。
http://pan.baidu.com/share/link?shareid=328840&uk=4127624209
我用的2.2的sdk编译的,低版本没有测试。我的手机是ZTE V880,fps50上下。
ps:testin的测试结果是通过率 100.00%
ps: 代码上传到github上了,地址 https://github.com/htynkn/DartsShaSha
文章对应的代码的tag为page 1。
(三)— 人性化
这一篇主要是添加一些让游戏更人性化的东西,比如音效和加载画面,菜单等等。
这其中用到了很多资源,主要出自以下几个网站,大家有需要也可以去上面寻找。
添加音效
首先是飞镖发出时候的音效,我希望是类似"bing"的一声,要短小精炼。
我使用的是http://www.freesound.org/people/BMacZero/sounds/96132/
libgdx支持的音频主要是WAV, MP3和OGG,其他的支持需要扩展支持。
libgdx中的音频有两种,一种是sound,一种是music。
一般所谓的音效使用sound,而音乐就使用music。
在DartController中添加属性
在实例化方法中添加
this.bing = Gdx.audio.newSound(Gdx.files.internal("audio/bing.wav")); |
在AddDart方法中添加
在怪兽被消灭的时候的音效我选用http://www.freesound.org/people/yottasounds/sounds/174465/
在TargetController中添加
great = Gdx.audio.newSound(Gdx.files.internal("audio/great.wav")); |
最后添加一个背景音乐。我使用的是http://www.freesound.org/people/Setuniman/sounds/167453/
大小为5.7M,个人感觉有点大。
而且我需要循环播放,这就意味着末尾的这个部分的我不需要。

使用在线转换器截取并转化成ogg。

转化以后大小变成465kb了。仔细听一下,感觉很多部分是重复,所以再一次截取,只要前4秒的。
转化好以后在主类添加
backgroundMusic = Gdx.audio.newMusic(Gdx.files
.internal("audio/background.ogg"));
backgroundMusic.setLooping(true); //循环播放
backgroundMusic.setVolume(0.4f); //设置音量
backgroundMusic.play(); //播放
|
因为music可以在resume和pause时自动播放和暂停,所以不需要其他的处理。在游戏关闭时需要将其释放掉。
backgroundMusic.dispose(); |
Game-Screen模式和资源预加载
到目前为止我们的所有操作都在一个界面上,但是一个游戏中可能有很多不同的界面和场景,比如菜单,帮助,关卡1,关卡2等等。
libgdx提供了Game-Screen模式,即只有一个Game,但是Game中有很多Screen。通过setScreen来切换。
这里注意一下Game所提供的切换功能没有任何过渡效果,后面会有添加过多效果的例子。
新建DartsGame类,继承Game类。在darts-shasha-android项目和darts-shasha-desktop项目中分别修改入口代码
package com.cnblogs.htynkn;
import android.os.Bundle;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
public class MainActivity extends AndroidApplication {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
cfg.useGL20 = false;
initialize(new DartsGame(), cfg);
}
}
|
和
package com.cnblogs.htynkn;
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
public class Main {
public static void main(String[] args) {
LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration();
cfg.title = "darts-shasha";
cfg.useGL20 = false;
cfg.width = 480;
cfg.height = 320;
new LwjglApplication(new DartsGame(), cfg);
}
}
|
新建一个类ShaScreen,实现Screen接口,其中的代码基本和原来的主类一样。唯一的区别就是create中的代码需要移动到show中。
在DartsGame中添加代码
package com.cnblogs.htynkn;
import com.badlogic.gdx.Game;
import com.cnblogs.htynkn.screen.ShaScreen;
public class DartsGame extends Game {
@Override
public void create() {
ShaScreen screen = new ShaScreen();
this.setScreen(screen);
}
}
|
这样我们就转化到Game-Screen模式。
当我们在游戏中使用了很多资源的时候直接new的话经常会卡顿,比如我们这个demo再加入音效后进入时间明显变长了。
所以我们这里需要一个预加载过程,而libgdx提供的AssetManager可以实现这个功能。
加载中使用一个小图标加文字形式。由于要使用中文,同时文字量很小,所以使用hiero作图。

新建一个LoadingScreen类作为加载页面。
因为AssetManager的作用很特殊,很多类都可以要调用它,所以我一般使用单例模式。
在DartsGame添加
public static AssetManager manager;
public static AssetManager getManager() {
if (manager == null) {
manager = new AssetManager();
}
return manager;
}
|
在LoadingScreen中通过
AssetManager manager = DartsGame.getManager(); |
获取manager。然后通过load方法添加要加载的资源
manager.load("audio/background.ogg", Music.class);
manager.load("audio/bing.wav", Sound.class);
manager.load("audio/great.wav", Sound.class);
manager.load("pack/sha/default.pack", TextureAtlas.class);
|
这里只是告诉manager要加载什么资源,并没有实际加载。
在render方法中调用
DartsGame.getManager().update() |
来启动加载并获取是否加载完成,如果返回值为true那么就表明加载完成。
其他类通过
DartsGame.getManager().get() |
来获取资源。
LoadingScreen代码如下:
package com.cnblogs.htynkn.screen;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Label.LabelStyle;
import com.cnblogs.htynkn.DartsGame;
public class LoadingScreen implements Screen {
Game game;
Stage stage;
@Override
public void render(float delta) {
Gdx.gl.glClearColor(0, 0, 0, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
if (DartsGame.getManager().update()) {
game.setScreen(new ShaScreen());
}
stage.act();
stage.draw();
}
@Override
public void resize(int width, int height) {
// TODO Auto-generated method stub
}
@Override
public void show() {
AssetManager manager = DartsGame.getManager();
manager.load("audio/background.ogg", Music.class);
manager.load("audio/bing.wav", Sound.class);
manager.load("audio/great.wav", Sound.class);
manager.load("pack/sha/default.pack", TextureAtlas.class);
stage = new Stage(480, 320, true);
BitmapFont font = new BitmapFont(
Gdx.files.internal("pack/loading/font.fnt"), false);
LabelStyle labelStyle = new LabelStyle(font, Color.WHITE);
Label label = new Label("游戏加载中...", labelStyle);
label.setPosition(160, 150);
stage.addActor(label);
Image image = new Image(
new TextureAtlas(Gdx.files
.internal("pack/loading/default.pack"))
.findRegion("ninja_attack"));
image.setOrigin(image.getWidth() / 2, image.getHeight() / 2);
image.addAction(Actions.repeat(1000, Actions.rotateBy(360, 3f)));
image.setPosition(100, 140);
stage.addActor(image);
}
@Override
public void hide() {
// TODO Auto-generated method stub
}
@Override
public void pause() {
// TODO Auto-generated method stub
}
@Override
public void resume() {
// TODO Auto-generated method stub
}
@Override
public void dispose() {
// TODO Auto-generated method stub
}
public LoadingScreen(Game game) {
this.game = game;
}
}
|
效果如下:

当我们不需要某个资源时通过
来销毁。或者使用
来清空所有资源。
添加友盟统计
统计功能对于一个应用是必须的。你可以获取到用户的信息,比如分辨率,系统版本,还可以快速获取到错误等等。
libgdx是一个游戏框架,没有自带的统计功能,这里我选用友盟统计。
首先申请key

申请完成以后获取Appkey并下载SDK,拷贝到android项目的libs文件夹中。
在AndroidManifest.xml中添加相关信息
详细的情况参考友盟的文档。
现在来看一下友盟文档,最常用的是
MobclickAgent.onResume(this);
MobclickAgent.onPause(this);
|
为了方便开发,我们新建一个接口IStatisticsService。
package com.cnblogs.htynkn.extension; public interface IStatisticsService { void onResume(); void onPause(); }
} |
然后在DartsGame类添加
public static IStatisticsService statisticsService; |
然后在Android项目中添加AndroidStatisticsService
package com.cnblogs.htynkn.extension;
import android.content.Context;
import com.umeng.analytics.MobclickAgent;
public class AndroidStatisticsService implements IStatisticsService {
Context context;
public AndroidStatisticsService(Context context) {
this.context = context;
}
@Override
public void onResume() {
MobclickAgent.onResume(context);
}
@Override
public void onPause() {
MobclickAgent.onPause(context);
}
} |
修改入口文件为
package com.cnblogs.htynkn; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.cnblogs.htynkn.extension.AndroidStatisticsService; public class MainActivity extends AndroidApplication { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration(); cfg.useGL20 = false; DartsGame dartsGame = new DartsGame(); DartsGame.setStatisticsService(new AndroidStatisticsService(this)); initialize(dartsGame, cfg); } } |
同样的,在桌面项目中添加DesktopStatisticsService
package com.cnblogs.htynkn.extension; import android.content.Context; import com.umeng.analytics.MobclickAgent; public class AndroidStatisticsService implements IStatisticsService { Context context; public AndroidStatisticsService(Context context) { this.context = context; } @Override public void onResume() { MobclickAgent.onResume(context); } @Override public void onPause() { MobclickAgent.onPause(context); } } |
修改入口文件
package com.cnblogs.htynkn; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.cnblogs.htynkn.extension.DesktopStatisticsService; public class Main { public static void main(String[] args) { LwjglApplicationConfiguration cfg = new LwjglApplicationConfiguration(); cfg.title = "飞镖杀杀"; cfg.useGL20 = false; cfg.width = 480; cfg.height = 320; DartsGame.setStatisticsService(new DesktopStatisticsService()); new LwjglApplication(new DartsGame(), cfg); } } |
因为桌面项目并没有统计功能,所以以输出log替代。
然后在游戏相应位置调用即可。
其他需要的功能办法基本相同,就是实现相应接口,然后不同的平台传入不同实现。没有实现的就用mock输出。
依照这种方法还可以添加其他第三方功能。
不过很遗憾友盟SDK升级以后我死活用不上了…现在换成百度APP统计了。原理是一样的,具体参考源码。
比如在Android的启动文件中重写onPause和onResume
package com.cnblogs.htynkn; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.cnblogs.htynkn.extension.AndroidStatisticsService; public class MainActivity extends AndroidApplication { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration(); cfg.useGL20 = false; DartsGame dartsGame = new DartsGame(); DartsGame.setStatisticsService(new AndroidStatisticsService(this)); initialize(dartsGame, cfg); } @Override protected void onResume() { super.onResume(); DartsGame.getStatisticsService().onResume(); } @Override protected void onPause() { super.onPause(); DartsGame.getStatisticsService().onPause(); } } |
统计结果


实话,友盟的统计比百度的好很多,但是不知为嘛我用不起了。
写在最后
本来这一篇写好很久了,但是我一直想加入一个菜单和游戏结束的界面,不过最近实在没有时间,先发出来这个部分吧。
如果有时间以后会补上的。
|