Box2d и Libgdx

в 15:21, , рубрики: android, box2d, Gamedev, libgdx, метки: , , ,

День добрый.

Не так давно начал изучать работу Libgdx и обнаружил, что не так много статей по сему фреймворку на русском языке. Встречал, конечно, статьи на русском, но они были лишь переводами офф. мануалов. Были правда и статьи, где разработчики пытались рассказать о своих творениях. Но в одной статье всё не обхватить, в итоге в подобных статьях было всего понамешано.

Я задался целью поэтапно писать статьи и различных аспектах/частях LibGDX, чтобы в итоге, любой человек мог более-менее работоспособную версию своей собственной игрушки сделать. В этой статье хотел бы рассказать про Box2D, который присутствует в LibGDX.


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

За основу я взял архитектуру, которую описывал в одной из своих статей. Используется шаблон MVC.

GameScreen

GameScreen реализует Screen, InputProcessor, что позволяет в нём обрабатывать все действия пользователя: нажатия и т.д.

В нём создаются необходимые классы: MyWorld, WorldController, WorldRenderer.
В статье, которую взял за основу вместо MyWorld используется World. Пришлось изменить, чтобы не конфликтовал с одноимённым классом из Box2D пакета.

    @Override
	public void show() {
		//высота камеры стабильна, а ширину подгоняем в зависимости от экрана усростйва, чтобы объекты не были растянуты
		MyWorld.CAMERA_WIDTH =  MyWorld.CAMERA_HEIGHT* Gdx.graphics.getWidth()/Gdx.graphics.getHeight();
		world = new MyWorld();
	
		renderer = new WorldRenderer(world, MyWorld.CAMERA_WIDTH, MyWorld.CAMERA_HEIGHT,true);
		controller = new WorldController(world);


		Gdx.input.setInputProcessor(this);	 //позволяет нам обрабатывать действия юзера
	}

Естественно надо переопределить всё методы по обработке: touchDown, touchUp и прочие.

Как пример, переопределим touchDown, touchUp так, чтобы наш персонаж мог двигаться влево, вправо и прыгать.

private void ChangeNavigation(int x, int y){
 
		controller.resetWay();
        //если кликнули выше перса, то указываем, чтобы игрок двигался вверх (прыгнул)
		if(height-y >  world.getPlayer().getPosition().y * renderer.ppuY)
			controller.upPressed();
		
       //если левее выше перса, то указываем, чтобы игрок двигался влево
		if ( x< world.getPlayer().getPosition().x * renderer.ppuX) 
			controller.leftPressed();
       //если кликнули правее перса, то указываем, чтобы игрок двигался вправо
		if (x> (world.getPlayer().getPosition().x )* renderer.ppuX)
			controller.rightPressed();
			
	}
	
	@Override
	public boolean touchDown(int x, int y, int pointer, int button) {

		if (!Gdx.app.getType().equals(ApplicationType.Android))
			return false;
		ChangeNavigation(x,y);
		return true;
	} 
	
	@Override
	public boolean touchUp(int x, int y, int pointer, int button) {
		if (!Gdx.app.getType().equals(ApplicationType.Android))
			return false;
	
		controller.resetWay();
		return true;
	}
	@Override
	public boolean touchDragged(int x, int y, int pointer) {
		ChangeNavigation(x,y);
		return false;
	}

В GameScreen ведётся обработка действий юзера и сигнализация контроллера. Ключевой метод здесь render(), который срабатывает через определённый промежуток и обрабатывает логику и отрисовывает объекты.

@Override
	public void render(float delta) {

		Gdx.gl.glClearColor(0, 0, 0, 232F/255);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
	
		controller.update(delta);
		renderer.render(delta);
	}

MyWorld

MyWorld будет являться всего лишь контейнером объектов. Так же в нём будет ссылка на экземпляр класса World из пакета. Необходимо определить объекты.

World world;
//персонаж
Player player;
//двигающиемя платформы
Array<MovingPlatform> platforms = new Array<MovingPlatform>();
//земля
public MovingPlatform groundedPlatform = null;

Ну и создать объекты:

public MyWorld(){
		width = 30;
		height = 8;
    //вектор указывает направление силы, действующей в мире (гравитация)
		world = new World(new Vector2(0, -20), true);	
		createWorld();
	}

private void createWorld(){
		BodyDef def = new BodyDef();
		def.type = BodyType.DynamicBody;
		Body boxP = world.createBody(def);
		player = new Player(boxP);
		
		player.getBody().setTransform(3.0f, 4.0f, 0);
		player.getBody().setFixedRotation(true);	
		
		Body box = createBox(BodyType.StaticBody, 1, 1, 0);
		for(int i = 0; i < 10; i++) {
			box = createBox(BodyType.DynamicBody, (float)Math.random(), (float)Math.random(), 13);
			
			box.setTransform((float)Math.random() * 10f - (float)Math.random() * 10f, (float)Math.random() * 10 + 6, 0);
		}
		platforms.add(new MovingPlatform(world, 3F, 3, 1,0.25F, 2, 0, 2)); 
		
		for(int i=1;i<width; ++i){
			Body boxGround = createBox(BodyType.StaticBody, 1F, 1F, 0);
			boxGround.setTransform(i,0,0);
			
		}
		
	}
	
	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;
	}

BodyDef — определение тела, которое все данные о твёрдом теле содержит. Вы можете повторно использовать это определение для различных объектов.

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

static стоит использовать для объектов, которые не надо двигать. Как пример: пол, стены.

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

Player

Класс 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, 3);
		poly.dispose();			
 
		CircleShape circle = new CircleShape();		
		circle.setRadius(0.41f);
		circle.setPosition(new Vector2(0, 0f));
		playerSensorFixture = box.createFixture(circle, 1);
		
		setFriction(10F);
		circle.dispose();		
 
		box.setBullet(true);
				
	}

	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, 8, 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;
	}
}

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

Передвигать объекты в принципе можно по разному, вплоть до явного изменения позиции. Но в Box2D есть два специальных метода для этого:
applyLinearImpulse() — задать импульс объекту. Как и в реальном мире, если вы толкнёте объект, то в соответствии с импульсом тело будет двигать, пока трение не погасит импульс.
setLinearVelocity() — задать скорость движения. В отличии от импульса, скорость будет постоянной, пока вы её не измените.

Я лично для движения по оси X использую setLinearVelocity(). Пока вы будет держать палец на таче, перс будет двигать, когда отпустите, персонаж остановится. Если же для движения по горизонтали использовать applyLinearImpulse(), то персонаж ещё будет двигать некоторое время после того, как вы уберёте палец с экрана.
В принципе, иногда можно и applyLinearImpulse() для движения влево/вправо использовать. Как пример, если персонаж двигается по скользкой поверхности (лёд, как пример).

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

MovingPlatform

MovingPlatform представляет собой движущуюся платформу.

public class MovingPlatform {
	Body platform;		
	Vector2 pos = new Vector2();
	Vector2 dir = new Vector2();
	Vector2 vel = new Vector2();
	float maxDist = 0;		
	float width;
	//Vector2 l = new Vector2();
	public MovingPlatform(World world, float x, float y, float width, float height, float dx, float dy, float maxDist) {
		platform = createBox(world, BodyType.KinematicBody, width, height, 1);			
		
		this.width = width;
		pos.x = x;
		pos.y = y;
		dir.x = dx;
		dir.y = dy;
		
		vel.x = dx;
		vel.y = dy;
		
		this.maxDist = maxDist;
		platform.setTransform(pos, 0);
		platform.getFixtureList().get(0).setUserData("p");
		platform.setUserData(this);
		
	
	}
	public void resume(){
		dir.x= vel.x;
		dir.y = vel.y;
		
	}
	public void pause(){
		
		vel.x = dir.x;
		vel.y = dir.y;
		dir.x = 0;
		dir.y = 0;
		platform.setLinearVelocity(dir);
	}
	public void update(float deltaTime) {
		
		if(dir.x < 0 && platform .getPosition().x < pos.x-maxDist)
		{
			platform .getPosition().x  =pos.x;
			dir.mul(-1);
		}
		
		if(dir.x > 0 && platform .getPosition().x> pos.x+maxDist)
		{
			platform .getPosition().x  =pos.x+maxDist;
			dir.mul(-1);
		}
		
		platform.setLinearVelocity(dir);		 
		
	}
	
	private Body createBox(World world,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/2, height/2);
		Fixture f = box.createFixture(poly, density);
		poly.dispose();
 
		return box;
	}
}

Платформа будет двигаться на определённое расстония от изначальной позиции.

WorldController

В WorldController происходит обновление логики в ответ на действия юзера в методе update().

boolean grounded ;
public void update(float delta) {
		Array<MovingPlatform> platforms = world.getPlatforms();
       //обновление состояния платформ
		for(int i = 0; i < platforms.size; i++) {
			MovingPlatform platform = platforms.get(i);
			platform.update(Math.max(1/60.0f, delta));
		}
 
		grounded = isPlayerGrounded(Gdx.graphics.getDeltaTime());
       //обработка нажатий
		processInput();
		
       //обработка состояния игрока
		world.getPlayer().update(delta);
	}

Обработать нажатия юзера и указать новое направление движения необходимо.

 //в зависимости от выбранного направления движения выставляем новое направление движения для персонажа
	private void processInput() {
		
		Player player = world.getPlayer();
       //если надо двигаться влево
	    if (keys.get(Keys.LEFT)) 
	    	player.getVelocity().x =- Player.SPEED;
  
       //если надо двигаться вправо
	    if (keys.get(Keys.RIGHT))
	     	player.getVelocity().x = Player.SPEED;
       //если надо прыгнуть
	    if (keys.get(Keys.UP)) 	    	
              //если стоим на земле
			if(grounded) 
                     //прыгаем
				 player.jump();
			
	    
       //если не на земле
		if(!grounded)
              //убираем трение
			world.getPlayer().setFriction(0f);
              //иначе задаём трение
		else{
			if(keys.get(Keys.RIGHT) || keys.get(Keys.LEFT))
				world.getPlayer().setFriction(0.2f);
			else
				world.getPlayer().setFriction(100f);
		}
	     
	    
	 }

Остаётся только написать метод по проверке того, на земле ли стоит персонаж. Для этого необходимо получить список всех соприкосновений объектов. Среди них выбрать контакты персонажа. Если среди контактов есть объект, котороый находится ниже персонажа, следовательно он находится на твёрдой поверхности.

	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(contact.getFixtureB().getUserData() != null && contact.getFixtureB().getUserData().equals("p")) {
						world.groundedPlatform = (MovingPlatform)contact.getFixtureB().getBody().getUserData();
					}											
					return true;			
				}
 
				return false;
			}
		}
		return false;
	}

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

Можно в таком случае пойти другим путём: менять силу трения не у персонажа, а у контакта. То есть, в методе isPlayerGrounded написать что-то вроде:

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

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

Собственно всё. Наш персонаж может двигать влево и вправо, а так же прыгать. Так же он может двигать случайно сгенерированные блоки.

Необходимо теперь только отрендерить объекты.

WorldRenderer

В классе WorldRenderer происходит отрисовка объектов.

public class WorldRenderer {
	Box2DDebugRenderer renderer;
	public static float CAMERA_WIDTH = 10f;
	public static  float CAMERA_HEIGHT = 15f;
	public float ppuX;
	public float ppuY;
	
	MyWorld world;
	public OrthographicCamera cam;
	
	
	public WorldRenderer(MyWorld world, float w, float h, boolean debug) {
		renderer = new Box2DDebugRenderer();
		this.world = world;
		CAMERA_WIDTH = w;
		CAMERA_HEIGHT = h;	
		ppuX = (float)Gdx.graphics.getWidth() / CAMERA_WIDTH;
		ppuY = (float)Gdx.graphics.getHeight() / CAMERA_HEIGHT;
		this.cam = new OrthographicCamera(CAMERA_WIDTH, CAMERA_HEIGHT);
		SetCamera(CAMERA_WIDTH / 2f, CAMERA_HEIGHT / 2f);
	}
	
	public void SetCamera(float x, float y){
		this.cam.position.set(x, y,0);	
		this.cam.update();
	}  

	public void dispose(){
		world.dispose();
	}
	
	public void render(float delta) {
		renderer.render(world.getWorld(), cam.combined);
		world.getWorld().step(delta, 4, 4);
	}
}

Сама отрисовка в методе render() происходит. В данном примере используется для отрисовки Box2DDebugRenderer в дебаг режиме для отображения границ объектов. В окончательном варианте вашего приложения в методе render() вам необходимо для всех объектов вывести какие-нибудь спрайты.

world.getWorld().step(delta, 4, 4); указывает частоту, с которой обновлять физику. В большинстве случаев, думаю, нет необходимости обновлять физику чаще, чем происходит прорисовка.
P.S. я сам только начал изучать Box2D, поэтому буду рад любым подсказкам и критике.

Автор: Suvitruf

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js