libGDX: Часть 5. Работа с сенсорным экраном и управление

В играх на Android очень важно организовать управление удобное. Я пока Bomberman’а делал, перепробовал различные варианты. Рассмотрим в этой статье возможные альтернативы.

За основу для статьи берём исходники из статьи про архитектуру игры на основе scene2d. Сам только сейчас оценил прелесть работы с scene2d.

Определение направление движения в зависимости от координат нажатия по экрану

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

public void ChangeNavigation(float x, float y){
		
  resetWay();
  if(y > getY())
    upPressed();
		
  if(y <  getY())
    downPressed();
		
  if ( x< getX()) 
    leftPressed();
			
  if (x> (getPosition().x +SIZE)* world.ppuX)
    rightPressed();
			
  processInput();
}

В общем-то, для стратегии это было бы удобно. Но если это какой-нибудь шутер/аркада, то кликать по экрану в разных точках неразумно. Удобнее бы было сделать виртуальный джойстик и при кликах по нему уже выбирать направление.

Управление джойстиком

Модифицируем нашу игру. Для начала надо изменить наш атлас, добавить в него спрайты джойстика. Затем изменить метод loadTextures() по загрузке текстур.

private void loadTextures() {
  texture  = new Texture(Gdx.files.internal("images/atlas.png"));
  TextureRegion tmpLeftRight[][] = TextureRegion.split(texture, texture.getWidth()/ 2, texture.getHeight()/2 );
  TextureRegion left2[][] = tmpLeftRight[0][0].split(tmpLeftRight[0][0].getRegionWidth()/2, tmpLeftRight[0][0].getRegionHeight());
  TextureRegion left[][] = left2[0][0].split(left2[0][0].getRegionWidth()/4, left2[0][0].getRegionHeight()/8);
  textureRegions.put("player",  left[0][0]);
  textureRegions.put("brick1",  left[0][1]);
  textureRegions.put("brick2",  left[1][0]);
  textureRegions.put("brick3",  left[1][1]);
  textureRegions.put("navigation-arrows", tmpLeftRight[0][1]);
  TextureRegion rightbot[][] = tmpLeftRight[1][1].split(tmpLeftRight[1][1].getRegionWidth()/2,tmpLeftRight[1][1].getRegionHeight()/2);
  textureRegions.put("khob",   rightbot[0][1]); 	
}     

Прелесть scene2d, как я уже говорил ранее, в том, что можно работать с актёрами независимо друг от друга и в относительных координатах. Поэтому создадим класс, который будет олицетворять собой контроллер и унаследуем его от Actor.

public class WalkingControl extends Actor{

	//размер джоя
	public static  float SIZE = 4f;
	//размер движущейся части (khob)
	public static  float CSIZE = 3f;
	
	public static  float CIRCLERADIUS = 1.5f;
	public static float  CONTRLRADIUS = 3F;
	//public static float Coefficient = 1F;

	//угол для определения направления
	float angle;
	//public static int Opacity = 1;
	 World world;
	
	//координаты отклонения khob
	protected Vector2 offsetPosition = new Vector2(); 
	
	protected Vector2 position = new Vector2();
	protected Rectangle bounds = new Rectangle();
	
	public WalkingControl(Vector2 pos, World world){ 
		this.position = pos;
		this.bounds.width = SIZE;
		this.bounds.height = SIZE;
		this.world = world;
		
		getOffsetPosition().x = 0;
		getOffsetPosition().y = 0;
		
		setHeight(SIZE*world.ppuY);
		setWidth(SIZE*world.ppuX);
		setX(position.x*world.ppuX);
		setY(position.y*world.ppuY);
	
		addListener(new InputListener() {
	        public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
	                return true;
	        }
	        
	        //при перетаскивании
	        public void touchDragged(InputEvent event, float x, float y, int pointer){
	        	
	        	withControl(x,y);
	        }
	        
	        //убираем палец с экрана
	        public void touchUp (InputEvent event, float x, float y, int pointer, int button) {
	        	
				getOffsetPosition().x = 0;
				getOffsetPosition().y = 0;
	        }
	        
		});
	}
	

	 
	//отрисовка
	@Override
	public void draw(SpriteBatch batch, float parentAlfa) {
		
		
		
		batch.draw(world.textureRegions.get("navigation-arrows"), getX(), getY(),getWidth(), getHeight());
		batch.draw(world.textureRegions.get("khob"), 
				(float)(position.x+WalkingControl.SIZE/2-WalkingControl.CSIZE/2+getOffsetPosition().x)*world.ppuX, 
				(float)(position.y+WalkingControl.SIZE/2-WalkingControl.CSIZE/2+getOffsetPosition().y)*world.ppuY, 
				WalkingControl.CSIZE * world.ppuX, WalkingControl.CSIZE * world.ppuY);
		
	}

	
	public Actor hit(float x, float y, boolean touchable) {
		//Процедура проверки. Если точка в прямоугольнике актёра, возвращаем актёра.
		return x > 0 && x < getWidth() && y> 0 && y < getHeight()?this:null;
	}
	
	
	public Vector2 getPosition() {
		return position;
	}
	public Vector2 getOffsetPosition() {
		return offsetPosition;
	}
	
	public Rectangle getBounds() {
		return bounds;
	}
	
	
	
	
	public void withControl(float x, float y){

		//точка касания относительно центра джойстика
		float calcX = x/world.ppuX -SIZE/2;
		float calcY = y/world.ppuY -SIZE/2;
		
		//определяем лежит ли точка касания в окружности джойстика
		if(((calcX*calcX + calcY* calcY)<=WalkingControl.CONTRLRADIUS*WalkingControl.CONTRLRADIUS)
				){	
			
			world.resetSelected();
			
			//пределяем угол касания
			double angle = Math.atan(calcY/calcX)*180/Math.PI;
			
			//угол будет в диапозоне [-90;90]. Удобнее работать, если он в диапозоне [0;360]
			//поэтому пошаманим немного
			if(angle>0 &&calcY<0)
					angle+=180;
			if(angle <0)
				if(calcX<0)
					angle=180+angle;
				else
					angle+=360;
			
			//в зависимости от угла указываем направление, куда двигать игрока
			if(angle>40 && angle<140)
				((Player)world.selectedActor).upPressed();
				
			if(angle>220 && angle<320)
				((Player)world.selectedActor).downPressed();
			
			
			if(angle>130 && angle<230)
				((Player)world.selectedActor).leftPressed();
			
			if(angle<50 || angle>310)
				((Player)world.selectedActor).rightPressed();
				
			
			//двигаем игрока
			((Player)world.selectedActor).processInput();

			
			angle = (float)(angle*Math.PI/180);
			getOffsetPosition().x = (float)((calcX*calcX + calcY* calcY>1F)? Math.cos(angle)*0.75F: calcX);
			getOffsetPosition().y = (float)((calcX*calcX + calcY* calcY>1F)? Math.sin(angle)*0.75F: calcY);
			
		}	
		else{
			
			world.resetSelected();
			getOffsetPosition().x = 0;
			getOffsetPosition().y = 0;
		}

	}
}

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

Теперь в конструкторе World необходимо добавить наш джойстик как актёра.

//контрол как актёр	
addActor(new WalkingControl (new Vector2(0F,0F),this));

Так же необходимо переопределить метод, который срабатывает при движении пальца по экрану:

@Override
public boolean touchDragged(int x, int y, int pointer) {
  //если предварительно выбран игрок
  if(selectedActor != null) 
    super.touchDragged(x, y, pointer);
	
  return true;
}

Необходимо изменит метод, который срабатывает при нажатии по экрану (по актёру…в нашем случае по Stage), чтобы запоминать только если кликнули по одному из персов:

public Actor hit(float x, float y, boolean touchable) {

  Actor  actor  = super.hit(x,y,touchable);
  //если выбрали актёра
  if(actor != null &&  actor instanceof Player)
    //запоминаем
    selectedActor = actor;
  return actor;
}

Управление стрелкам

В Bomberman’е я реализовал возможность выбора между двумя типами управления: джоем и стрелками. По аналогии с методом, описанным мною для управления джоем, можно сделать и управление стрелками. Но тогда WalkingControl будет наследоваться не от Actor, а от Group. Тогда весь класс будет как контейнер для 4 актёров-кнопок. Может быть в одной из статей покажу, как это реализовать.

Собственно всё. Данная реализация позволяет кастомизировать контролы для управления как угодно: менять положение, непрозрачность, размеры. Использование scene2d даёт возможность менять наш джой как угодно, не влияя на остальной код, так как всё инкапсулировано в самом классе контроллера. Можете скачать исходники урока Libgdxtutorial-lesson5.rar.