注意:在1.0正式版中box2d被作为一个扩展独立了出来。因为作者感觉有的开发者不一定用到,这样可以减少核心包的大小,详细情况参考官方介绍:http://www.badlogicgames.com/wordpress/?p=3404
因为最近工作原因有段时间没写学习笔记了。最近心血来潮,看了看box2D这个东西。这里就简单介绍下Box2D。
Box2D是一个用于模拟2D刚体物体的C++引擎。作者为Erin Catto。Bullet Physics之类咱就先不了解了,这玩意太高级了。
为什么要提到box2D,前几篇中的模拟物理的操作感觉很是繁琐。Box2D给我们提供了偷懒的机会。libgdx中使用jni封装了box2D,不得不说。libgdx的作者也太有毅力了。
下面我们看看什么是box2d。
box2d官方的使用手册:http://box2d.org/manual.pdf
这里我们使用libgdx的文档中的介绍来一步一步创建一个物理世界。
libgdx文档中关于box2d的部分。
https://github.com/libgdx/libgdx/wiki/box2d#creating-a-world
创建物理世界
World world = new World(new Vector2(0, -10), true);
解释下参数new Vector2(0, -10)世界中的重力系统,第一个是x水平方向的,第二个是y值垂直方向的。true指的是对象是否可以睡眠。其实也就是文档中介绍的让某些物理世界中的物体睡眠以节约cpu资源。box2d中的物理模拟都是通过cpu进行计算模拟的。
还有需要注意的是物理世界中的单位都是以米,秒等标准单位进行计算的。
调试渲染器
Box2DDebugRenderer debugRenderer = new Box2DDebugRenderer();
这没什么好说的,就是为了调试方便。
world.step(1/60f, 6, 2);
这是为了世界中的物体帧率同步,在render的最后调用。
可以参看这篇文章http://gafferongames.com/game-physics/fix-your-timestep/
物理世界渲染
debugRenderer.render(world, camera.combined);
现在物理世界中什么都没有,box2d世界中的对象称为Body。有人称为刚体。
我们就用body好了,可以用演员的概念来理解box2d的对象。
先看第一种body
Dynamic Bodies
动态的body.
这种body的特性就是参与碰撞,可以进行移动。它适合任何需要移动的对象。
创建一个动态的body
// First we create a body definition
BodyDef bodyDef = new BodyDef();
// We set our body to dynamic, for something like ground which doesn't move we would set it to StaticBody
bodyDef.type = BodyType.DynamicBody;
// Set our body's starting position in the world
bodyDef.position.set(100, 300);
// Create our body in the world using our body definition
Body body = world.createBody(bodyDef);
// Create a circle shape and set its radius to 6
CircleShape circle = new CircleShape();
circle.setRadius(6f);
// Create a fixture definition to apply our shape to
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = circle;
fixtureDef.density = 0.5f;
fixtureDef.friction = 0.4f;
fixtureDef.restitution = 0.6f; // Make it bounce a little bit
// Create our fixture and attach it to the body
Fixture fixture = body.createFixture(fixtureDef);
// Remember to dispose of any shapes after you're done with them!
// BodyDef and FixtureDef don't need disposing, but shapes do.
circle.dispose();
这段代码很好理解。
body由两部分组成:
1.它本身的状态描述(位置,类型,阻尼大小,是否可以休眠等等)。类似于定义对象的属性
2.它的物理状态(形状,密度,质量,摩擦力等物理特性)。这是box2d对此body模拟物理现象的依据。
下面的Static Bodies和Kinematic Bodies大家自己看下就好了。
只介绍下区别。Static Bodies 顾名思义就是静态的body,适合做一些边界和无法破坏的障碍。Kinematic Bodies 这是介于动态和静态之间的。简单理解就是可以移动的静态body。
物理世界如何和我们的游戏世界进行绑定交互呢
body.setUserData(Object);
例如:body.setUserData(girlActor);
直接绑定演员。
然后在循环方法render之前修改演员的状态。
Iterator<Body> bi = world.getBodies();
while (bi.hasNext()){
Body b = bi.next();
// Get the body's user data - in this example, our user
// data is an instance of the Entity class
Entity e = (Entity) b.getUserData();
if (e != null) {
// Update the entities/sprites position and angle
e.setPosition(b.getPosition().x, b.getPosition().y);
// We need to convert our angle from radians to degrees
e.setRotation(MathUtils.radiansToDegrees * b.getAngle());
}
}
看出来了吗,这跟前几篇的介绍控制演员动作是一个完全相反的。我们如果使用物理世界,可以直接控制物理世界中的对象,改变对象的状态,位置信息,然后传递给我们的游戏中的演员并修改它的状态,位置等信息。
也就是说,我们直接控制物理世界中的对象,而不是实际的演员,就可以实现前几篇同样的效果。在我们前几篇的示例中,可以更为简单的处理。
girlActor.setPosition(body.getPosition());
这样来更新演员的位置信息。当然这句要放到一个不停执行的方法中。来实现实时刷新。
使用box2d的好处就是,我们不用考虑物理计算了,box2d会给我们自动处理。例如重力影响,碰撞检测,阻力等等。
box2d非常适合模拟一些物理操作比较多的游戏。例如最著名的愤怒的小鸟。重力,弹力,摩擦,碰撞基本都涉及到了。
实际上在我们这个学习示例中倒不是必须使用的。毕竟需要模拟的物理操作不多,只有一个跳起操作。使用box2d是为了接下来把我们上篇的地图跟box2d结合起来使用,来实现障碍物,碰撞检测等方面的东西。
这里只是为了对box2d物理引擎进行下简单的了解。如果想做物理类的游戏就有必要深入研究下了。
修改下我们上篇的代码,这里我把包名变化了下。
资源加载那块感觉还是很麻烦,干脆独立出来好了,新建一个专门负责加载资源的类。
MyAssetManager
package com.me.box2d.common;
import com.badlogic.gdx.assets.AssetManager;
import com.badlogic.gdx.assets.loaders.resolvers.InternalFileHandleResolver;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.maps.tiled.TiledMap;
import com.badlogic.gdx.maps.tiled.TmxMapLoader;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.*;
public class MyAssetManager {
private static final String GIRL_RUN_ATLAS = "data/image/girlRun.atlas";
private static final String TILEDMAP_TMX = "data/map/map.tmx";
public static AssetManager assetManager;
public static TextureAtlas textureAtlas;
public static TextureRegion[] girlRunFrameLeft;
public static TextureRegion[] girlRunFrameRight;
public static Animation animationRight;
public static Animation animationLeft;
public static TiledMap map;
private boolean firstLoad = true;
public MyAssetManager() {
assetManager = new AssetManager();
assetManager.load(GIRL_RUN_ATLAS, TextureAtlas.class);
assetManager.setLoader(TiledMap.class
, new TmxMapLoader(new InternalFileHandleResolver()));
assetManager.load(TILEDMAP_TMX, TiledMap.class);
girlRunFrameLeft = new TextureRegion[16];
girlRunFrameRight = new TextureRegion[16];
}
public boolean load() {
if (assetManager.update()) {
if (firstLoad) {
textureAtlas = assetManager.get(GIRL_RUN_ATLAS, TextureAtlas.class);
map = assetManager.get(TILEDMAP_TMX, TiledMap.class);
createAnimations();
loadTextures();
loadSounds();
loadFonts();
firstLoad = false;
}
return true;
}
return false;
}
private static void loadTextures() {
}
private static void loadSounds() {
}
private static void loadFonts() {
}
private static void createAnimations() {
for (int i = 0; i < 16; i++) {
girlRunFrameRight[i] = textureAtlas.findRegion(String.valueOf(i + 1));
}
for (int i = 0; i < 16; i++) {
girlRunFrameLeft[i] = new TextureRegion(girlRunFrameRight[i]);
girlRunFrameLeft[i].flip(true, false);
}
animationLeft = new Animation(0.06f, girlRunFrameLeft);
animationRight = new Animation(0.06f, girlRunFrameRight);
}
public static void playSound(Sound sound) {
sound.play(1);
}
public static Body getGirlBody(World world) {
CircleShape girlShape = new CircleShape();
girlShape.setRadius(1f);
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyDef.BodyType.DynamicBody;
bodyDef.position.set(new Vector2(2f, 2f));
Body girlBody = world.createBody(bodyDef);
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.friction = 1f;
fixtureDef.density = 1f;
fixtureDef.shape = girlShape;
girlBody.createFixture(fixtureDef);
girlShape.dispose();
return girlBody;
}
}
}
这里增加了个获取演员body的静态方法,为了方便,所以也放到了资源管理里边。
修改下地图文件。
这里除了背景图层,我添加了一个障碍物图层,和一个障碍物对象的对象图层。
对象图层使用矩形工具围绕障碍图层画出一个正方形矩形来框选住狮子。背景图层暂时不让它显示,这是为了方便我们查看效果。
修改我们的World代码
package com.me.box2d.actor;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.*;
import com.me.box2d.common.MyAssetManager;
/**
* 我们的游戏世界
*/
public class World {
//相当于我们游戏世界的大小。只是为了方便控制人物大小,移动速度等
//手机屏幕划分为10*6方格,具体每个方格大小不知道
private final float WORLD_WIDTH = 10f;
private final float WORLD_HEIGHT = 6f;
//也就是把屏幕分成10*6网格后每个单元格的x,y轴的像素数
//下面的方法中会根据屏幕大小自动计算出
private float pixelsX;
private float pixelsY;
private int width;
private int height;
private GirlActor girlActor;
//物理世界
private com.badlogic.gdx.physics.box2d.World box2dWorld;
private Body girlBody;
public World() {
//创建物理世界
this.box2dWorld =
new com.badlogic.gdx.physics.box2d.World(new Vector2(0, -9.8f), true);
//创建演员的body对象
this.girlBody = MyAssetManager.getGirlBody(this.box2dWorld);
//创建世界的边界,这个我没放到资源管理里边
BodyDef bodyDef = new BodyDef();
bodyDef.type = BodyDef.BodyType.StaticBody;
bodyDef.position.set(0, 0);
Body worldBounds = box2dWorld.createBody(bodyDef);
//世界边界是一条线的形状
EdgeShape edgeShape = new EdgeShape();
FixtureDef fixtureDef = new FixtureDef();
fixtureDef.shape = edgeShape;
//注意是4个边
edgeShape.set(new Vector2(0f, 0f), new Vector2(28.8f, 0f));
worldBounds.createFixture(fixtureDef);
edgeShape.set(new Vector2(0f, 0f), new Vector2(0f, 6f));
worldBounds.createFixture(fixtureDef);
edgeShape.set(new Vector2(28.8f, 0f), new Vector2(28.8f, 6f));
worldBounds.createFixture(fixtureDef);
edgeShape.set(new Vector2(0f, 6f), new Vector2(28.8f, 6f));
worldBounds.createFixture(fixtureDef);
//释放资源
edgeShape.dispose();
}
public com.badlogic.gdx.physics.box2d.World getBox2dWorld() {
return this.box2dWorld;
}
public Body getGirlBody() {
return this.girlBody;
}
public GirlActor getGirlActor() {
return girlActor;
}
public void setGirlActor(GirlActor girlActor) {
this.girlActor = girlActor;
}
//计算出每个方格的像素数
//例如800*480的屏幕 800/10 480/6 每单位x,y像素为80的方格
//这样方便我们控制世界中的物体的大小和比例
public void setPixelsXY(int width, int height) {
this.width = width;
this.height = height;
pixelsX = (float) width / WORLD_WIDTH;
pixelsY = (float) height / WORLD_HEIGHT;
}
/**
* 屏幕的实际宽度
*
* @return width
*/
public int getWidth() {
return width;
}
/**
* 屏幕的实际高度
*
* @return height
*/
public int getHeight() {
return height;
}
/**
* @return 每单元格的像素数
*/
public float getPixelsX() {
return this.pixelsX;
}
public float getPixelsY() {
return this.pixelsY;
}
public float getWorldWidth() {
return WORLD_WIDTH;
}
public float getWorldHeight() {
return WORLD_HEIGHT;
}
}
接下来修改我们view类
package com.me.box2d.view;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.maps.tiled.TiledMapRenderer;
import com.badlogic.gdx.maps.tiled.renderers.OrthogonalTiledMapRenderer;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.utils.Disposable;
import com.me.box2d.actor.GirlActor;
import com.me.box2d.actor.World;
import com.me.box2d.common.MapBodyBuilder;
import com.me.box2d.common.MyAssetManager;
/**
* view层
* 主要负责渲染场景,演员
*/
public class WorldView implements Disposable {
private GirlActor girlActor;
private SpriteBatch spriteBatch;
private TextureRegion currentFrame;
private OrthographicCamera camera;
private World world;
//自定义资源管理器专门用来管理资源加载
private MyAssetManager myAssetManager;
private Box2DDebugRenderer debugRenderer = new Box2DDebugRenderer();
private com.badlogic.gdx.physics.box2d.World box2dWorld;
private boolean first = true;
/**
* 构造方法 初始化相关的参数。
*
* @param world 游戏世界
*/
public WorldView(World world) {
this.world = world;
box2dWorld = world.getBox2dWorld();
// box2dWorld.setContactListener(new ListenerClass());
girlActor = world.getGirlActor();
spriteBatch = new SpriteBatch();
currentFrame = new TextureRegion();
myAssetManager = new MyAssetManager();
this.camera = new OrthographicCamera(world.getWorldWidth(), world.getWorldHeight());
this.camera.position.set(world.getWorldWidth() / 2, world.getWorldHeight() / 2, 0);
}
/**
* 演员显示
*/
public void render(float deltaTime) {
camera.update();
//演员走到正中间的时候移动正交相机
//往回走的时候重置正交相机位置
if (girlActor.getPosition().x > world.getWorldWidth() / 2
- girlActor.getBounds().getWidth() / 2) {
this.camera.position.set(girlActor.getPosition().x
+ girlActor.getBounds().getWidth() / 2,
world.getWorldHeight() / 2, 0
);
} else if (girlActor.getPosition().x < world.getWorldWidth() / 2
- girlActor.getBounds().getWidth() / 2) {
this.camera.position.set(world.getWorldWidth() / 2,
world.getWorldHeight() / 2, 0);
}
if (myAssetManager.load()) {
//执行一次创建
if (first) {
//根据地图创建静态body障碍物
MapBodyBuilder.buildShapes(MyAssetManager.map,
box2dWorld, world.getPixelsY());
first = false;
}
spriteBatch.setProjectionMatrix(this.camera.combined);
//物理世界显示
debugRenderer.render(box2dWorld, this.camera.combined);
drawMap();
spriteBatch.begin();
drawGirl();
spriteBatch.end();
box2dWorld.step(1 / 60f, 6, 2);
}
}
/**
* Girl的渲染逻辑
*/
private void drawGirl() {
//获取演员的当前状态,是站立还是跑动状态
switch (girlActor.getState()) {
//站立状态
case IDLE:
//根据演员的当前面朝方向获取演员的纹理。因为图片中没有站立的,所以我们取第一个为站立图片使用
currentFrame = girlActor.isFacingRight() ? MyAssetManager.girlRunFrameRight[0]
: MyAssetManager.girlRunFrameLeft[0];
break;
//跑动状态,当然是获取跑动动画了
case RUNING:
currentFrame = girlActor.isFacingRight()
? MyAssetManager.animationRight.getKeyFrame(girlActor.getStateTime(), true)
: MyAssetManager.animationLeft.getKeyFrame(girlActor.getStateTime(), true);
break;
default:
break;
}
spriteBatch.draw(currentFrame
, girlActor.getPosition().x
, girlActor.getPosition().y
, girlActor.getBounds().getWidth()
, girlActor.getBounds().getHeight());
}
/**
* 地图渲染
*/
private void drawMap() {
TiledMapRenderer tiledMapRenderer = new OrthogonalTiledMapRenderer(MyAssetManager.map, 0.06f, spriteBatch);
tiledMapRenderer.setView(this.camera);
tiledMapRenderer.render();
}
/**
* 资源回收
*/
@Override
public void dispose() {
spriteBatch.dispose();
}
}
这个也没什么需要解释的。
最重要的就是修改我们的控制类,原来的控制类控制的是演员,现在我们把控制类修改为控制body
package com.me.box2d.control;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.me.box2d.actor.GirlActor;
import com.me.box2d.actor.World;
import java.util.HashMap;
import java.util.Map;
/**
* control层,演员的控制
*/
public class GirlBodyControl {
private GirlActor girlActor;
private Body girlBody;
/**
* 枚举类型的按键
*/
public enum KEY {
LEFT, RIGTH, JUMP
}
/**
* 定义个map来存放按键状态。这样就可以支持多按键了。
* 并且每个按键的状态只能存在一种,这样比较符合实际需要
*/
private static Map<KEY, Boolean> keys = new HashMap<>();
static {
keys.put(KEY.LEFT, false);
keys.put(KEY.RIGTH, false);
keys.put(KEY.JUMP, false);
}
//定义一组改变按键状态的方法
public void leftDown() {
keys.put(KEY.LEFT, true);
}
public void leftUp() {
keys.put(KEY.LEFT, false);
}
public void rightDown() {
keys.put(KEY.RIGTH, true);
}
public void rigthUp() {
keys.put(KEY.RIGTH, false);
}
public void jumpDown() {
keys.put(KEY.JUMP, true);
}
public void jumpUp() {
keys.put(KEY.JUMP, false);
}
/**
* 构造器
*
* @param world 游戏世界
*/
public GirlBodyControl(World world) {
girlActor = world.getGirlActor();
girlBody = world.getGirlBody();
}
/**
* 不断执行的方法
* 在MyGame的render方法中调用来不断判断和更新演员状态
*/
public void update(float deltaTime) {
state();
girlActor.setPosition(new Vector2(girlBody.getPosition().x - 1f,
girlBody.getPosition().y - 1f));
girlActor.update(deltaTime);
if (girlBody.getLinearVelocity().y == 0 && (girlActor.getPosition().y < 3)) {
if (girlActor.getState().equals(GirlActor.State.JUMPING)) {
girlActor.setState(GirlActor.State.IDLE);
}
}
}
/**
* 判断当前按下的按键状态
* 跳跃和左右方向是可以同时按下的
*/
private void state() {
if (keys.get(KEY.JUMP)) {
if (!girlActor.getState().equals(GirlActor.State.JUMPING)) {
girlActor.setState(GirlActor.State.JUMPING);
girlBody.setLinearVelocity((new Vector2(girlBody.getLinearVelocity().x, girlActor.getSpeed_y())));
}
}
if (keys.get(KEY.LEFT)) {
if (!girlActor.getState().equals(GirlActor.State.JUMPING)) {
girlActor.setState(GirlActor.State.RUNING);
girlActor.setFacingRight(false);
girlBody.setLinearVelocity(new Vector2(-girlActor.getSpeed_x(), girlBody.getLinearVelocity().y));
} else {
girlActor.setFacingRight(false);
girlBody.setLinearVelocity(new Vector2(-girlActor.getSpeed_x(), girlBody.getLinearVelocity().y));
}
} else if (keys.get(KEY.RIGTH)) {
if (!girlActor.getState().equals(GirlActor.State.JUMPING)) {
girlActor.setState(GirlActor.State.RUNING);
girlActor.setFacingRight(true);
girlBody.setLinearVelocity(new Vector2(girlActor.getSpeed_x(), girlBody.getLinearVelocity().y));
} else {
girlActor.setFacingRight(true);
girlBody.setLinearVelocity(new Vector2(girlActor.getSpeed_x(), girlBody.getLinearVelocity().y));
}
} else {
if (!girlActor.getState().equals(GirlActor.State.JUMPING)) {
girlActor.setState(GirlActor.State.IDLE);
girlBody.setLinearVelocity(new Vector2(0, girlBody.getLinearVelocity().y));
}
}
}
}
注意把演员类GirlActor中的update方法中的计算坐标的代码注释掉
public void update(float deltaTime) {
stateTime += deltaTime;
//根据时间和速度来运算当前的演员位置信息。
//这里进行一次拷贝操作,不改变原有的velocity值
//因为在匀加速运动中,我们需要它作为初始速度值进行计算
// position.add(velocity.cpy().scl(deltaTime));
this.bounds.x = position.x;
this.bounds.y = position.y;
}
setLinearVelocity();
这其实就是设置一个线性的x,y的速率。和我们原来的没什么不同。
只是需要注意的是不要改变我们没有控制的速率,比如向左走的时候,
girlBody.setLinearVelocity(new Vector2(-girlActor.getSpeed_x(), girlBody.getLinearVelocity().y));
如果演员正好在空中,那么Y的速率我们就要设置为它本身现在Y的速率,不要去改变它。因为现在是box2d在控制速率的计算。我们只改变x的速率就可以了。
if (girlBody.getLinearVelocity().y == 0 && (girlActor.getPosition().y < 3)) {
if (girlActor.getState().equals(GirlActor.State.JUMPING)) {
girlActor.setState(GirlActor.State.IDLE);
}
}
这段其实就是当人物落地,或者落到狮子的头上的时候,修改为站立状态,因为速率为0的情况有两种,一种就是落到某个物体上,还有就是在跳到最高的那个点的时候,速率会变为0,所以我就直接写了个位置<3来防止获取到第二种状态。还有没有更好的办法。有想到的通知我
最终的动画效果,有点模糊http://shouyou.aipai.com/c14/PD4lKColJy1qJWQqLA.html