В играх на 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.