LibGDX — это библиотека Java с открытым исходным кодом для создания кроссплатформенных игр и приложений. В моем последнем уроке я рассказал о настройке библиотеки и подготовке к разработке. В этом учебном пособии описывается обработка ввода с помощью LibGDX, что делает интерактивные игры более интерактивными.
Различные типы входных состояний.
Обработка ввода — простая задача. Если ключ не работает , то этот ключ должен быть зарегистрирован как true
а также, когда ключ активен . Эта простота может привести к множеству проблем, особенно для игр. То, что вы хотите, — это простой способ спросить libGDX, нажата ли конкретная клавиша, нажата ли она или отпущена.
Но каково значение этих трех разных ключевых состояний? Они описывают, как пользователь взаимодействовал со своей клавиатурой.
- Нажата : клавиша была активирована, и она срабатывает только для одного кадра.
- Вниз : клавиша в настоящее время удерживается нажатой.
- Выпущено : ключ был выпущен, и он срабатывает только для одного кадра.
Если у вас была логика в функции рендеринга, например:
if (key.pressed()) print("pressed") if (key.down()) print("down") if (key.released()) print("released")
Вы ожидаете увидеть что-то вроде:
..."pressed" //triggered one frame ..."down" //constant amongst all frames ..."down" ..."down" ..."released" //triggered one from
Та же логика может также применяться к сенсорным событиям.
Класс менеджера ввода
Существует много разных способов реализации этого класса, но я предлагаю вам стремиться быть как можно более «объектно-ориентированным», что означает, что каждое событие нажатия клавиши и касания будет его собственным объектом.
Первым классом, который нужно объявить, является сам менеджер ввода (который будет реализовывать класс InputProcessor в InputProcessor
).
public class InputManager implements InputProcessor { }
Если вы следуете из предыдущего урока и используете IntelliJ, щелкните правой кнопкой мыши внутри этого класса и выберите « Генерировать -> Переопределить методы» . (Из-за раздувания кода я собираюсь пока что сгенерировать эти методы из этих фрагментов кода.)
Затем создайте внутренний класс с именем InputState
.
ПРИМЕЧАНИЕ . Все три класса ниже будут внутренними классами InputManager
.
public class InputState { public boolean pressed = false; public boolean down = false; public boolean released = false; }
Теперь создайте KeyState
и TouchState
которые расширяют InputState
public class KeyState extends InputState{ //the keyboard key of this object represented as an integer. public int key; public KeyState(int key){ this.key = key; } } public class TouchState extends InputState{ //keep track of which finger this object belongs to public int pointer; //coordinates of this finger/mouse public Vector2 coordinates; //mouse button public int button; //track the displacement of this finger/mouse private Vector2 lastPosition; public Vector2 displacement; public TouchState(int coord_x, int coord_y, int pointer, int button){ this.pointer = pointer; coordinates = new Vector2(coord_x, coord_y); this.button = button; lastPosition = new Vector2(0, 0); displacement = new Vector2(lastPosition.x, lastPosition.y); } }
Класс TouchState
более сложен из-за не только наличия событий Pressed
, Down
и Released
, но и сохранения того, какой палец контролирует этот объект, а также сохранения координат и вектора смещения для движений жестов.
Вот базовая структура всего класса (исключая методы переопределения заглушки):
public class InputManager implements InputProcessor { public class InputState { public boolean pressed = false; public boolean down = false; public boolean released = false; } public class KeyState extends InputState{ //the keyboard key of this object represented as an integer. public int key; public KeyState(int key){ this.key = key; } } public class TouchState extends InputState{ //keep track of which finger this object belongs to public int pointer; //coordinates of this finger/mouse public Vector2 coordinates; //mouse button public int button; //track the displacement of this finger/mouse private Vector2 lastPosition; public Vector2 displacement; public TouchState(int coord_x, int coord_y, int pointer, int button){ this.pointer = pointer; coordinates = new Vector2(coord_x, coord_y); this.button = button; lastPosition = new Vector2(0, 0); displacement = new Vector2(lastPosition.x,lastPosition.y); } } }
Поскольку каждое событие нажатия клавиши / касания будет отдельным объектом, вам необходимо хранить их в массиве. Добавьте два новых поля массива в InputManager
.
public Array<KeyState> keyStates = new Array<KeyState>(); public Array<TouchState> touchStates = new Array<TouchState>();
И добавьте конструктор для InputManager
для инициализации этих двух объектов.
public InputManager() { //create the initial state of every key on the keyboard. //There are 256 keys available which are all represented as integers. for (int i = 0; i < 256; i++) { keyStates.add(new KeyState(i)); } //this may not make much sense right now, but I need to create //atleast one TouchState object due to Desktop users who utilize //a mouse rather than touch. touchStates.add(new TouchState(0, 0, 0, 0)); }
Теперь вы готовы контролировать логику этих объектов, используя сгенерированные методы переопределения. Начиная с public boolean keyDown(int keycode)
.
@Override public boolean keyDown(int keycode) { //this function only gets called once when an event is fired. (even if this key is being held down) //I need to store the state of the key being held down as well as pressed keyStates.get(keycode).pressed = true; keyStates.get(keycode).down = true; //every overridden method needs a return value. I won't be utilizing this but it can be used for error handling. return false; }
Далее следует public boolean keyUp(int keycode)
.
@Override public boolean keyUp(int keycode) { //the key was released, I need to set it's down state to false and released state to true keyStates.get(keycode).down = false; keyStates.get(keycode).released = true; return false; }
Теперь, когда вы обработали логику состояния ключей, вам нужен способ доступа к этим ключам для проверки их состояний. Добавьте эти три метода в InputManager
:
//check states of supplied key public boolean isKeyPressed(int key){ return keyStates.get(key).pressed; } public boolean isKeyDown(int key){ return keyStates.get(key).down; } public boolean isKeyReleased(int key){ return keyStates.get(key).released; }
Все обретает форму, поэтому сейчас хороший момент, чтобы попытаться объяснить, как все сходится.
libGDX представляет каждый ключ как целое число, используемое для keyStates
элемента из массива keyStates
который возвращает один объект keyState
. Вы можете проверить состояние этой клавиши ( Pressed
, Pressed
или Released
), которая является логическим значением.
Вы почти готовы протестировать InputManager
но есть еще несколько вещей для настройки. Прямо сейчас, единственное состояние, которое на самом деле является функциональным, это состояние Down
.
Когда клавиша нажата. Down
установлен на true. И когда клавиша « Up
, « Down
устанавливается в «ложь» Два других состояния, Pressed
и Released
еще не работают должным образом.
Это Trigger
клавиши, которые должны срабатывать только для одного кадра, а затем оставаться ложными. Теперь, когда они активированы, они продолжают быть True
.
Вам необходимо реализовать новый метод для InputManager
называемый update
который будет правильно обрабатывать состояния Pressed
и Released
.
public void update(){ //for every keystate, set pressed and released to false. for (int i = 0; i < 256; i++) { KeyState k = keyStates.get(i); k.pressed = false; k.released = false; } }
Метод Update
позволяет настроить InputManager
поста InputManager
. Все, что необходимо сбросить, будет соответствовать этому методу.
Чтобы использовать InputManager
, вам нужно создать его экземпляр в core
пространстве имен проекта. В MyGdxGame
добавьте InputManager
в качестве нового поля.
public class MyGdxGame extends ApplicationAdapter { InputManager inputManager; }
В конструкторе MyGdxGame
экземпляр InputManager
. Чтобы InputManager
был полезен, вам нужно передать его InputProcessor InputProcessor
и libGDX будет использовать этот новый объект для обработки входных событий. Замените текущий метод create
:
@Override public void create () { inputManager = new InputManager(); Gdx.input.setInputProcessor(inputManager); }
Теперь вам нужно вызвать InputManager
обновления InputManager
в конце метода рендеринга MyGdxGame
.
Вы должны вызывать update()
после обработки всей игровой логики, связанной с вводом. Замените текущий метод create
:
@Override public void render () { inputManager.update(); }
Вот как выглядит MyGdxGame
:
public class MyGdxGame extends ApplicationAdapter { SpriteBatch batch; Texture img; OrthographicCamera camera; InputManager inputManager; @Override public void create () { batch = new SpriteBatch(); img = new Texture("badlogic.jpg"); camera = new OrthographicCamera(Gdx.graphics.getWidth(), Gdx.graphics.getHeight()); inputManager = new InputManager(); Gdx.input.setInputProcessor(inputManager); } @Override public void render () { camera.update(); batch.setProjectionMatrix(camera.combined); Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); batch.begin(); batch.draw(img, 0, 0); batch.end(); inputManager.update(); } }
Протестируйте этот код, чтобы убедиться, что все работает как задумано.
@Override public void render () { //testing key states... if (inputManager.isKeyPressed(Input.Keys.A)) { System.out.println("A Pressed"); } if (inputManager.isKeyDown(Input.Keys.A)) { System.out.println("A Down"); } if (inputManager.isKeyReleased(Input.Keys.A)) { System.out.println("A Released"); } camera.update(); batch.setProjectionMatrix(camera.combined); Gdx.gl.glClearColor(1, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); batch.begin(); batch.draw(img, 0, 0); batch.end(); inputManager.update(); }
Теперь вы должны иметь более точные входные события.
InputManager
почти закончен, осталось только реализовать логику для обработки сенсорных событий. К счастью, KeyStates
и TouchStates
работают одинаково. Теперь вы будете использовать сгенерированные методы сенсорных событий.
Примечание : этот метод тщательно прокомментирован, поэтому я могу повториться.
@Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { //There is always at least one touch event initialized (mouse). //However, Android can handle multiple touch events (multiple fingers touching the screen at once). //Due to this difference, the input manager will add touch events on the fly if more than one //finger is touching the screen. //check for existing pointer (touch) boolean pointerFound = false; //get altered coordinates int coord_x = coordinateX(screenX); int coord_y = coordinateY(screenY); //set the state of all touch state events for (int i = 0; i < touchStates.size; i++) { TouchState t = touchStates.get(i); if (t.pointer == pointer) { t.down = true; t.pressed = true; //store the coordinates of this touch event t.coordinates.x = coord_x; t.coordinates.y = coord_y; t.button = button; //recording last position for displacement values t.lastPosition.x = coord_x; t.lastPosition.y = coord_y; //this pointer exists, don't add a new one. pointerFound = true; } } //this pointer doesn't exist yet, add it to touchStates and initialize it. if (!pointerFound) { touchStates.add(new TouchState(coord_x, coord_y, pointer, button)); TouchState t = touchStates.get(pointer); t.down = true; t.pressed = true; t.lastPosition.x = coord_x; t.lastPosition.y = coord_y; } return false; }
Одним из основных различий между KeyStates
и TouchStates
является тот факт, что все KeyStates
инициализируются в конструкторе InputManager
из-за того, что клавиатура является физическим устройством.
Вы знаете все доступные клавиши для использования, но сенсорное событие — это кроссплатформенное событие. Касание на рабочем столе означает, что пользователь щелкнул мышью, но касание на Android означает, что пользователь коснулся экрана пальцем. Кроме того, на рабочем столе (мышь) может быть только одно касание, в то время как на Android (несколько пальцев) может быть несколько касаний.
Чтобы решить эту проблему, добавляйте новые TouchStates
на лету в зависимости от того, что делает пользователь.
Когда пользователь запускает сенсорное событие, вы сначала хотите преобразовать его значения screenX
и screenY
во что-то более удобное. Координата 0,0
находится в верхнем левом углу экрана, и ось Y перевернулась. Для этого добавьте два простых метода в InputManager
чтобы преобразовать эти координаты в 0,0
в центре экрана и по оси Y справа вверх, что будет лучше работать с объектом SpriteBatch
.
private int coordinateX (int screenX) { return screenX - Gdx.graphics.getWidth()/2; } private int coordinateY (int screenY) { return Gdx.graphics.getHeight()/2 - screenY; }
Теперь вам нужен способ проверить, является ли это новым событием касания или InputManager
уже обнаружил это событие и добавил его в список TouchStates
. Чтобы уточнить, пользователь держит свое устройство и касается экрана большим пальцем правой руки, это будет первое событие касания в пределах touchStates
и InputManager
знает, что он может обработать по крайней мере одно событие касания. Если пользователь решит коснуться экрана большими пальцами левой и правой руки, второе событие касания создаст новый TouchState
и добавит его в touchStates
на лету. Теперь InputManager
знает, что может обрабатывать два сенсорных события.
Все это работает от переменной pointer
переданной этому методу. Переменная- pointer
— это целое число, которое позволяет различать одновременные события касания.
Каждый раз, когда пользователь запускает сенсорное событие, используйте pointer
чтобы проверить, создавать ли новое TouchState
или нет.
boolean pointerFound = false;
TouchState
все доступные объекты TouchState
чтобы проверить, существует ли этот указатель, и установите состояния этого TouchState
.
//set the state of all touch state events for (int i = 0; i < touchStates.size; i++) { TouchState t = touchStates.get(i); if (t.pointer == pointer) { t.down = true; t.pressed = true; //store the coordinates of this touch event t.coordinates.x = coord_x; t.coordinates.y = coord_y; t.button = button; //recording last position for displacement values t.lastPosition.x = coord_x; t.lastPosition.y = coord_y; //this pointer exists, don't add a new one. pointerFound = true; } }
Обратите внимание, что если TouchState
не найден, вы создаете новый и добавляете его в массив touchStates
. Также обратите внимание, как получить доступ к TouchState
.
touchStates.add(new TouchState(coord_x, coord_y, pointer, button)); TouchState t = touchStates.get(pointer);
Если один палец касается экрана, pointer
равен 0
что соответствует одному пальцу. Добавление этого нового объекта TouchState
в массив автоматически устанавливает правильное значение индекса. Его позиция в массиве равна 0
что соответствует значению pointer
.
Если второй палец касается экрана, его pointer
будет 1
. Когда новый TouchState
добавляется в массив, его индексное значение равно 1
. Если вам нужно TouchSate
какое TouchSate
является каким, используйте указанное значение pointer
чтобы найти правильное TouchState
из touchStates
.
Теперь обработайте логику, когда палец больше не касается экрана.
@Override public boolean touchUp(int screenX, int screenY, int pointer, int button) { TouchState t = touchStates.get(pointer); t.down = false; t.released = true; return false; }
ПРИМЕЧАНИЕ . Я решил не удалять объекты TouchState
даже если они больше не используются. Если был обнаружен новый счетчик пальцев, я думаю, он должен остаться открытым и готовым к повторному использованию. Если вы по-другому относитесь к этому, в методе touchUp
есть место, где вы бы реализовали логику для удаления TouchState
.
Теперь вычислите вектор смещения TouchState
для обработки жестов пальцем.
@Override public boolean touchDragged(int screenX, int screenY, int pointer) { //get altered coordinates int coord_x = coordinateX(screenX); int coord_y = coordinateY(screenY); TouchState t = touchStates.get(pointer); //set coordinates of this touchstate t.coordinates.x = coord_x; t.coordinates.y = coord_y; //calculate the displacement of this touchstate based on //the information from the last frame's position t.displacement.x = coord_x - t.lastPosition.x; t.displacement.y = coord_y - t.lastPosition.y; //store the current position into last position for next frame. t.lastPosition.x = coord_x; t.lastPosition.y = coord_y; return false; }
Как и раньше с состояниями клавиш, вам нужно добавить три метода для доступа к этим состояниям касания плюс два других метода для получения координат и смещения TouchState
.
//check states of supplied touch public boolean isTouchPressed(int pointer){ return touchStates.get(pointer).pressed; } public boolean isTouchDown(int pointer){ return touchStates.get(pointer).down; } public boolean isTouchReleased(int pointer){ return touchStates.get(pointer).released; } public Vector2 touchCoordinates(int pointer){ return touchStates.get(pointer).coordinates; } public Vector2 touchDisplacement(int pointer){ return touchStates.get(pointer).displacement; }
Теперь у вас та же проблема, что и раньше с KeyStates
, когда правильно KeyStates
только состояние Down
. Но это имеет смысл, вы никогда не добавляли логику для сброса событий trigger
.
Возвращаясь к методу update
, добавьте следующий код:
for (int i = 0; i < touchStates.size; i++) { TouchState t = touchStates.get(i); t.pressed = false; t.released = false; t.displacement.x = 0; t.displacement.y = 0; }
Вот снова метод полного update
:
public void update(){ for (int i = 0; i < 256; i++) { KeyState k = keyStates.get(i); k.pressed = false; k.released = false; } for (int i = 0; i < touchStates.size; i++) { TouchState t = touchStates.get(i); t.pressed = false; t.released = false; t.displacement.x = 0; t.displacement.y = 0; } }
Примечание : вектор смещения сбрасывается обратно на (0, 0)
в каждом кадре.
Использование InputManager
Вы уже видели пример использования InputManager
для проверки KeyStates
. Теперь я объясню, используя TouchStates
.
Если приложение работает на рабочем столе и использует только мышь, используйте только TouchState
из touchStates
.
if (inputManager.isTouchPressed(0)) { System.out.println("PRESSED"); } if (inputManager.isTouchDown(0)) { System.out.println("DOWN"); System.out.println("Touch coordinates: " + inputManager.touchCoordinates(0)); System.out.println("Touch displacement" + inputManager.touchDisplacement(0)); } if (inputManager.isTouchReleased(0)) { System.out.println("RELEASED"); }
Если приложение работает на Android, вам нужно TouchStates
все доступные TouchStates
и обрабатывать каждое из них в отдельности, но проверка TouchState
напрямую подвержена ошибкам.
inputManager.touchStates.get(0);
Покройте это новым методом, добавленным в InputManager
:
public TouchState getTouchState(int pointer){ if (touchStates.size > pointer) { return touchStates.get(pointer); } else { return null; } }
Теперь у вас есть более TouchState
способ доступа к TouchState
с простой проверкой ошибок.
inputManager.getTouchState(0);
Если бы это было в цикле for для начала, проверка границ цикла for по существу была бы свободна от проверки ошибок.
for (int i = 0; i < inputManager.touchStates.size; i++) { TouchState t = inputManager.getTouchState(i); System.out.println("Touch State: " + t.pointer + " : coordinates : " + t.coordinates); }
Игра началась
Этот Input Manager был разработан для управления сущностями в игровой среде. Сначала я разработал основы этого класса для «переменных» высот прыжка для моего игрового персонажа.
Позже я расширил класс для обработки ввода для Android с несколькими сенсорными событиями, которые в настоящее время используются в игре под названием Vortel . Мне нужен был способ, позволяющий пользователю контролировать сущность своими пальцами, независимо от того, где их пальцы были на экране, и независимо от того, сколько пальцев было на экране одновременно. Я достиг этого TouchState
, накапливая все векторы смещения из каждого TouchState
, а затем применяя этот новый вектор к сущности. Если у вас есть свободное время, пожалуйста, проверьте его.
Я использовал функцию переменной высоты прыжка в другой игре под названием Ooze . Я использовал различные состояния TouchState
( Pressed,
TouchState
), чтобы точно контролировать, насколько высоко персонаж прыгает в зависимости от того, как долго пользователь касается экрана.
Я надеюсь, что этот урок был полезен для вас с вашими игровыми идеями и с нетерпением жду ваших вопросов и комментариев ниже .