libGDX: Часть 6. Работа с Box2D

Box2D Test

Прошлые статьи в целом были вводными. Но только при помощи них уже можно написать свою игру. Правда в этом случае придётся всю физику самому продумывать. Для написания многих типов игр идеально бы подошёл встроенный в LibGDX движок — Box2D. О нём и пойдёт речь в этой статье.

Box2D является физическим движком реального времени и предназначен для работы с двухмерными физическими объектами. Если вы разрабатывает какую-нибудь игру с видом сбоку/платформер, то это идеальное решение. Все необходимые классы находятся в пакете com.badlogic.gdx.physics.box2d.

За основу берём исходники из статьи про архитектуру игры. Сама архитектура в целом не сильно будет изменена. Довольно большие изменения коснуться объектов (моделей), в частности класс World.

World

В пакете Box2D есть свой класс World, поэтому изменим наименование нашего класса на MyWorld. Функцию он будет одну реализовывать, а именно — быть контейнером для объектов. Будем в нём хранить ссылку на экземпляр класса World из пакета, в котором будем создавать объекты.

World world;

В конструкторе нашего класса MyWorld создаём экземпляр World.

public MyWorld(){
  platforms = new Array<MovingPlatform>();
  width = 30;
  height = 8;
  world = new World(new Vector2(0, -20), true);	
  world.setContactListener(new MyContactListener(world));
  createWorld();
}

Код этот используется в статье про ContactListener. Там же и узнаете, что означает строка world.setContactListener(new MyContactListener(world));.

Конструктор World принимает два параметра: вектор направления силы тяготения (в нашем случае сила тяготения действует вертикально вниз, как и в реальном мире) и флаг, указывающий на то, стоит ли симулировать бездействующие тела. Если с первым параметром всё ясно, то со вторым не очень. В принципе, этот параметр всегда можно в true ставить, для большей продуктивности.

Теперь необходимо создать объекты. Тип объекта задаётся с помощью BodyType. Тела бывают трёх типов: static — нулевая масса, нулевая скорость, передвинуть можно лишь программно; kinematic — нулевая масса, ненулевая скорость, может быть сдвинут; dynamic — положительная масса, ненулевая скорость, может быть сдвинут.

/**
* Создание объектов мира
*/
private void createWorld(){
  //создание игрока
  BodyDef def = new BodyDef();
  //установить тип тела
  def.type = BodyType.DynamicBody;
  //создать объект с определёнными заранее параметрами
  Body boxP = world.createBody(def);
  player = new Player(boxP);	
  //переместить объект по указанным координатам	
  player.getBody().setTransform(1.0f, 4.0f, 0);
  player.getBody().setFixedRotation(true);	
	
  //создание платформы
  platforms.add(new MovingPlatform(world, 3F, 3, 1,0.25F, 2, 0, 2)); 
		
  //создание блоков
  for(int i=0;i<width; ++i){
    Body boxGround = createBox(BodyType.StaticBody, 0.5F, 0.5F, 2);
    boxGround.setTransform(i,0,0);
    boxGround.getFixtureList().get(0).setUserData("bd");
    boxGround = createBox(BodyType.StaticBody, 0.5F, 0.5F, 0);
    boxGround.setTransform(i,height-1,0);
    boxGround.getFixtureList().get(0).setUserData("b");
  }
}

/**
 * Создание блока.
 * @param type тип
 * @param width ширина
 * @param height высота
 * @param density плотность
 * @return
 */
private Body createBox(BodyType type, float width, float height, float density) {
  BodyDef def = new BodyDef();
  def.type = type;
  Body box = world.createBody(def);
  PolygonShape poly = new PolygonShape();
  poly.setAsBox(width, height);		
  box.createFixture(poly, density);
  poly.dispose();
 
  return box;
}

Body, собственно, само тело. Обекты Body можно передвигать, поворачивать с помощью метода setTransform().

Все объекты в мире создаются с помощью метода public Body createBody(BodyDef def) класса World. Возвращаемое значение и будет нашим объектом. В дальнейшем все манипуляции над объектом будут с помощью этой возвращённой ссылки.

Body

Так как класс Body является одним из ключевых при работе с Box2D, то стоит остановится на работе с ним чуток поподробнее. Ниже кратко рассмотрю наиболее часто используемые методы.

setUserData(java.lang.Object userData) позволяет для объекта задать какие-то пользовательские данные, так сказать метаданные по объекту. Для этого можно создать какой-то класс и добавлять его этим методом к объекту, а в дальнейшем как-то использовать. Как пример, можно туда пихать данные для рендеринга (Sprite, TextureRegion…).

applyAngularImpulse(float impulse) задаёт угловой импульс в единицах kg*m*m/s. Чаще всего используется по отношению к самому игроку в ответ на нажатия по экрану. Импульс, очевидно, имеет затухающий характер. Он постепенно гасится силой трения и силой тяготения. Я обычно задаю импульс при движении по вертикали (при прыжке например), а при движения по горизонтали изменяю скорость. При задании импульса персонаж ещё будет двигаться некоторое время после того, как вы уберёте палец с экрана. Логично для движения по вертикали, пока не погасится сила инерции. В принципе, иногда можно и applyLinearImpulse() для движения влево/вправо использовать. Как пример, если персонаж двигается по скользкой поверхности (лёд, как пример).

setLinearVelocity(float vX, float vY) — задать скорость движения по осям. В отличии от импульса, скорость будет постоянной, пока вы её не измените.

setBullet(boolean flag) указывает, должно ли тело рассматриваться как пуля при обнаружении коллизий.

setAwake(boolean flag) устанавливает спит тело или нет. Спящее тело потребляет меньше ресурсов процессора.

setActive(boolean flag) устанавливает активно тело или нет. Бездействующее тело не моделируются и не участвует в столкновениях. Бездействующее тела все еще принадлежит объекту World и остается в списке тела.

setGravityScale(float scale) устанавливает коэффициент для силы тяжести. Банальный пример, если вы установите значение -1, то тело начнёт взлетать, то есть сила тяготения будет действовать в обратном направлении.

Для игрока создадим свой класс Player.

public class Player {
	final static float MAX_VELOCITY = 3f;
	public final static float SPEED = 5f;
	public final static float SIZE = 0.8f;
	
	public Fixture playerPhysicsFixture;
	public Fixture playerSensorFixture;
	Body box;
	public  Player(Body b){ 
		box = b;		
		PolygonShape poly = new PolygonShape();		
		poly.setAsBox(0.4f, 0.4f);
		playerPhysicsFixture = box.createFixture(poly,0);
		poly.dispose();			
		
		CircleShape circle = new CircleShape();		
		circle.setRadius(0.4f);
		circle.setPosition(new Vector2(0, -0.05f));
		playerSensorFixture = box.createFixture(circle, 0);
		//трение
		//setFriction(200F);
		circle.dispose();		
		box.setBullet(true);
		
	}
	public float getFriction(){
		return playerSensorFixture.getFriction();
	}
	
	public Body getBody(){
		return box;
	}
	
	public void setFriction(float f){
		playerSensorFixture.setFriction(f); 
		playerPhysicsFixture.setFriction(f); 
	}
	
	public Vector2 getPosition(){
		return box.getPosition();
	}
	
	public Vector2 getVelocity() {
		return velocity;
	}
	
	Vector2 velocity = new Vector2();
	public void update(float delta) {
		Vector2 vel = box.getLinearVelocity();
		velocity.y = vel.y;
		box.setLinearVelocity(velocity);
		if(isJump) {box.applyLinearImpulse(0, 14, box.getPosition().x,  box.getPosition().y);	isJump = false;}
	}
	boolean isJump = false;
	public void jump(){
		isJump = true;
	}
	public void resetVelocity(){
		getVelocity().x =0;
		getVelocity().y =0;
	}
}

Все методы по работе с Body я уже рассмотрел выше. Стоит лишь остановиться на одном моменте — игрок состоит из двух частей. Действительно, в Box2D можно создавать объекты из кинематических пар. В данном случае игрок состоит из склеенных прямоугольника и круга. Это очень интересная особенность, которая позволяет собирать комплексные объекты и задавать поведение и параметры для каждой части в отдельности.

Код класса по созданию платформы тут приводить не буду, кому надо может посмотреть в исходниках внизу статьи. Принцип там в целом такой же.

Обработка коллизий

Как я уже сказал, про обработку коллизий можно почитать в отдельной статье про ContactListener. Здесь же частично затрону этот вопрос.

Для определения того, может ли персонаж прыгнуть, необходимо выяснить, находится ли он сейчас на земле/платформе. Для этого необходимо посмотреть список его контактов. Целиком метод ниже:

/**
	 * Опреелить, нахоится ли игрок на земле/платформе.
	 * @param deltaTime 
	 * время, прошешее с прошлой прорисовки.
	 * @return
	 */
	private boolean isPlayerGrounded(float deltaTime) {				
		world.groundedPlatform = null;
		List<Contact> contactList = world.getWorld().getContactList();
		for(int i = 0; i < contactList.size(); i++) {
			Contact contact = contactList.get(i);
			if(contact.isTouching() && (contact.getFixtureA() == world.getPlayer().playerSensorFixture ||
			   contact.getFixtureB() == world.getPlayer().playerSensorFixture)) {				
 
				Vector2 pos = world.getPlayer().getPosition();
				WorldManifold manifold = contact.getWorldManifold();
				boolean below = true;
				for(int j = 0; j < manifold.getNumberOfContactPoints(); j++) {
					below &= (manifold.getPoints()[j].y < pos.y - 0.4f);
				}
 
				if(below) {
					if(contact.getFixtureA().getUserData() != null && contact.getFixtureA().getUserData(). equals("p")) {
						world.groundedPlatform =  (MovingPlatform)contact.getFixtureA(). getBody().getUserData();	
						if (!keys.get(Keys.LEFT) && !keys.get(Keys.RIGHT)) 	
							contact.setFriction(200F);
						else
							contact.setFriction(0F); 
					}
 
					if(contact.getFixtureB().getUserData() != null && contact.getFixtureB().getUserData(). equals("p")) {
						world.groundedPlatform = (MovingPlatform)contact.getFixtureB(). getBody().getUserData();
						if (!keys.get(Keys.LEFT) && !keys.get(Keys.RIGHT)) 	
							contact.setFriction(200F);
						else
							contact.setFriction(0F); 
					}											
					return true;			
				}
 
				return false;
			}
		}
		return false;
	}

В целом метод прост: получаем список всех контактов в мире world.getWorld().getContactList(), затем определяем столкновения именно персонажа с другими объектами. Если есть объект, с которым контактирует персонаж, и этот объект находится ниже персонажа, то очевидно, что игрок стоит на земле/платформе.

Тут разве что стоит остановиться на одном моменте, а именно на строках:

if (!keys.get(Keys.LEFT) && !keys.get(Keys.RIGHT)) 	
  contact.setFriction(200F);
else
  contact.setFriction(0F); 

Если бы этих строк не было, то для нашей изначальной задачи (для последующего прыжка) это не было бы проблемой. Но без этих строк персонаж, находясь на движущейся платформе, будет как бы скользить.

Логично предположить, что надо выставить трение побольше для персонажа. Я смотрел мануалы забугорные…Не всегда данный метод работает. Поэтому можно в таком случае пойти другим путём: менять силу трения не у персонажа, а у контакта. То есть, в методе isPlayerGrounded менять трения у контакта.

Тогда, в случае, если персонаж просто стоит, то он будет двигаться вместе с платформой из-за большого трения. Если же персонажу необходимо двигаться, то уменьшаем трение, чтоб персонаж мог двигаться.

WorldRenderer

Как и раньше отрисовка происходит в классе WorldRenderer в методе render(float delta). Тут стоит остановится на одном удобстве Box2D для разработки. В нём есть класс Box2DDebugRenderer, который позволяет отрисовывать границы объектов. Довольно удобно для отладки.

Теперь метод по отрисовке объектов будет выглядеть так:

/** * Отрендерить всё. * @param delta * время, прошешее с прошлой прорисовки. */ public void render(float delta) { renderer.render(world.getWorld(), cam.combined); world.getWorld().step(delta, 4, 4); }

Это позволяет разрабатывать игру на первоначальном этапе, не думаю об анимации. world.getWorld().step(delta, 4, 4) указывает частоту, с которой обновлять физику. Для моделирования движения тел Box2D использует численное дифференцирование (метод Эйлера). Метод step(float timeStep, int velocityIterations, int positionIterations) имеет три аргумента: время, которое необходимо смоделировать в мире и количество итераций для решателей скоростей и позиций. Задача этих решателей — просчёт ограничений для правильного поведения тел при столкновениях. При просчёте одного ограничения не возникает особых трудностей. Однако при просчете нескольких ограничений, разрешение одного ограничения слегка нарушает остальные. Поэтому для получения хорошего результата необходимо произвести просчет всех ограничений несколько раз (т.е. сделать несколько итераций).

Чем больше таких итераций, тем точнее происходит моделирование, но только одновременно с этим возрастают и временные затраты, что тянет за собой больший расход ресурсов процессора. Так что, подбирайте эти значения с умом.

Исходники

Можете скачать исходники Libgdxtutorial-lesson6.rar.