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.

  Категории: java, libgdx, Коддинг
  • http://winners-games.com Vitalik

    Довольно таки удобный движок, сразу можно писать игру и смотреть в реальном времени.

    • http://suvitruf.ru Suvitruf

      Вся прелесть в том, что он интуитивно понятен. К тому же, по нему самому оригинальный мануал есть, правда он на C++, но принцип работы по ним понять можно.

      Так что, для написания более-менее простого платформера (тот же Марио) не сильно заморачиваться надо.

      • http://winners-games.com Vitalik

        Да, сейчас очень много развелось клонов Марио, или они уже были просто их никто не видел? Особенно хотелось бы подметить игру Memes Mario, ну просто не реально сложная игра.

        • http://suvitruf.ru Suvitruf

          Многие не видели, многие были забанены, если речь о Гугл Маркете.

          Сам я в Memes Mario не гамал, но игра зачётная =D

  • артур

    помогите пожалуйста разобраться с vector2,для чего он нужен, и что обозначает этот код
    Vector2 vel = box.getLinearVelocity();
    velocity.y = vel.y;

    и как на примере этого кода изменить направление импульса,например влево
    Vector2 vel = box.getLinearVelocity();
    velocity.y = vel.y;
    box.setLinearVelocity(velocity);
    if(isJump) {box.applyLinearImpulse(0, 14, box.getPosition().x, box.getPosition().y);

    • http://suvitruf.ru Suvitruf

      Изменить velocity.x надо на отрицательное число. Это, по сути, шаг, с которыми будет двигаться перс в этом направлении.

      • артур

        а velocity.y каким должен быть? пробывал с 0, но тогда персонаж чуть поднимается вверх, а потом движется влево, а мне нужно,чтобы он сразу резко отправился влево по прямой траектории,то есть как прыжок вверх,только в другом направлении

        • http://suvitruf.ru Suvitruf

          Вверх он двигается из-за этого
          if(isJump) {box.applyLinearImpulse(0, 14, box.getPosition().x, box.getPosition().y);
          applyLinearImpulse задаёт импульс телу.
          В данном случая по оси X – не двигается. По оси Y – вверх.
          Если надо вместо этого двигаться влево, то вот код
          if(isJump) {box.applyLinearImpulse(-14, 0, box.getPosition().x, box.getPosition().y);

          • артур

            спасибо,теперь разобрался

  • tstr

    защем тебе “float deltaTime” в “isPlayerGrounded” ?

    • http://suvitruf.ru Suvitruf

      Для кое-каких экспериментов использовал. Можете убрать)

  • Oleg

    Довольно интересно, но могли бы Вы привести этот самый пример на архитектуре Scene2d?

    • http://suvitruf.ru Suvitruf

      Можно попробовать.

      • Oleg

        буду ждать с нетерпением =)

        P.S. чек-бокс “Я не бот” порадовал)

  • Иван

    можете ли вы объяснить что надо удалять в коде чтоб убрать статистику (friction, grounded, velocity), которое стоит возле актера ?

    • http://suvitruf.ru Suvitruf

      font.drawMultiLine

  • Алексей

    Скажите, пожалуйста можно ли box2d использовать отдельно от libgdx?
    Хочу освоить box2d, пишу на java.
    по вашим статьям же в принципе можно смотреть? или все таки box2d можно как то отдельно использовать?

    • http://suvitruf.ru Suvitruf

      Box2D – физический движок. Он встроен во многие игровые движки. В тот же cocos2x.

  • lalka

    Автор, зачем здесь передавать дельтатайм? Это же неправильно. По крайней мере на десктопах симуляция физического мира будет плыть от любого чиха. Запустил фотошоп – снаряд скорость поменял.
    world.getWorld().step(delta, 4, 4);
    Сюда вместо дельты константу надо или как-то накапливать дельту, если это ОЧЕНЬ необходимо….

    • http://suvitruf.ru Suvitruf

      В Box2D физика по своему обновляется, поэтому в плане дельты не совсем уверен. Не так много времени провёл, работая с Box2D, чтобы что-то наверняка сказать.

  • Чайник

    Я вот начал разбираться и понял наверное 30% от всего написанного. Просто много слов непонятных. Можно значение их посмотреть где нибудь? и в чём измеряется скорость размер и MAX_VELOCITY 3f , 0.8f.

    • http://suvitruf.ru Suvitruf

      Вы напишите, что не понятно, я объясню (:
      Скорость измеряется в относительных единицах. В данном случае VELOCITY = 0.8f означает, что скорость равна 0.8 клеток за единицу времени.

  • Vetl

    Недавно начал изучать gamedev и многое не понятно. Вот, например, никак не могу понять как связывать графику и физику box2d.
    Как я понял необходимо получать координаты и углы поворота объектов используя данные box2d, а затем картинкам присваивать их значения и отрисовывать?
    Есть ли возможность вставить текстуру на этапе создания и добавления объекта в world и, например, жестко привязать его к fixture – так чтобы при перемещении или повороте fixture поворачивалось и перемещалось графическое отображение объекта?

    • http://suvitruf.ru Suvitruf

      Я обычно отдельно отрисовывываю, беря данные о координатах из объектов Box2D.

      • Vetl

        На этапе обучения тяжело так делать. Подскажите способ попроще?

        • http://suvitruf.ru Suvitruf

          Я думал, что так наоборот проще О_о

  • YuryKlmchuk

    Не могу понять следующие: вот например есть body
    this.bodyDef = new BodyDef();
    this.bodyDef.type = BodyType.DynamicBody;
    this.bodyDef.position.set(this.getX()/100, this.getY()/100);
    this.boxBody = this.gameWorld.createBody(this.bodyDef);
    this.shape = new PolygonShape();
    this.shape.setAsBox(0.5f, 0.5f);
    this.fixtureDef = new FixtureDef();
    this.fixtureDef.shape = this.shape;
    this.boxBody.createFixture(fixtureDef);

    Координаты этого body находиться в центре квадрата, или в нижнем левом углу???

  • Игорь

    Спасибо. Не могу понять, что изменяет метод setBullet(true), который рассматривает тело при столкновение как пулю. Вроде не чего не меняется. Кстати метод Сontact.isTouching() не всегда корректно работает, на пересечение объектов или частей сложного объекта не фиксирует контакт, лучше использовать интерфейс ContactListener, он все фиксирует.
    Не плохой метод applyLinearImpulse(float impulseX, float impulseY, float pointX, float pointY), но если ваш игрок соединяет внутри себя разные объекты с помощью разных соединений (например DistanceJointDef с амортизацией DistanceJointDef.frequencyHz типа пружины и без), то игрок подпрыгивает на разную высоту, так как к импульсу прибавляется сила отталкивания соединения (DistanceJointDef). Как вы думаете, как в таком случае лучше реализовать прыжок на одинаковую высоту?

    • http://suvitruf.ru Suvitruf

      setBullet(true) указывает, чтоб для объекта более точные вычисления применялись.

      А про второе…в обработчике прыжка задавать вектор, с учётом уже имеющегося импульса.

      • Игорь

        цитата “А про второе…в обработчике прыжка задавать вектор, с учётом уже имеющегося импульса.”
        Вы имеете ввиду Body.setLinearVelocity(0, 5f-x*Body.getLinearVelocity().y);или так applyLinearImpulse(new Vector2(0,3.85f), Body.getPosition());? У меня игрок на колесах, и с места он прыгает ниже чем с разгона, и когда подвеска сжимается в момент импульса, он ниже подскакивает, а когда наоборот – выше. Если я правильно понимаю нужно отнять/прибавить не достающий/избыточный импульс при столкновение, для этого нужно где то получить величину этого импульса по вертикальной оси. Как учитывать скорость имеющегося импульса, с помощью метода Body.getLinearVelocity().y, а потом корректировать с помощью Body.setLinearVelocity(0, x);? Или как то по другому?

  • Sergey

    Как убрать круг и квадрат которые выводятся вместе с актером?? или сделать их не видимыми??

  • Dmitry

    Как я понимаю, угол поворота телу напрямую задать нельзя? Делал машинку с видом сверху, управляемую со стрелок клавиатуры, простые тригонометрические формулы, спрайт вполне себе ездит, а вот с телом сложности. Передать угол от спрайта к телу не получается, только наоборот.
    А через силы и угловую скорость как-то не очень понятно, как это адекватно сделать.

  • Евгений Кузьмин

    А BodyDef def = new BodyDef();
    def.type = BodyType.DynamicBody;
    Body boxP = world.createBody(def); почему в стеке (в локальной области видимочти) объявлены и определены? В createWorld? Они копируются в класс player по видимому и уничтожаются при выходе из области видимости для того чтобы куча не больно тормозила?