К сожалению, не все MIDP устройства корректно обрабатывают удерживание клавиш. А между тем, обработка этого события очень важна для разработчиков игр (особенно если речь идет об аркадах). Необходимость многократного нажатия клавиши может убить самую хорошую игру. Не забывайте, что Вы пишете для людей, и пользователь вряд ли будет пользоваться Вашей программой, если у него сведет пальцы.
Некоторые эмуляторы телефонов (и все эмуляторы Palm) все же умеют обрабатывать удерживания. Чтобы убедиться в том, что устройство поддерживает режим удерживания, необходимо воспользоваться методомhasRepeatEvents(). Его обычно помещают внутрь методаkeyRepeated():
protected void keyRepeated(int keyCode)
{
if (hasRepeatEvents())
{
keyPressed(keyCode);
}
}
Приведенный код проверяет, поддерживается ли устройством обработка удерживания клавиш, и если поддерживается, вызывается метод обработки действияkeyPressed.
Допустим, что режим удерживания не реализован на данном устройстве, тогда мы должны самостоятельно эмулировать его. Давайте решим, что нам вообще нужно. Обработка удерживания клавиши подразумевает многократное выполнение какого-либо действия с момента ее нажатия до момента освобождения. Другими словами, нам нужно:
Что может быть проще.
private KeyRepeat m_repeat=new KeyRepeat(this); protected void keyPressed(int keyCode) { // Если режим удерживания не поддерживается, // будем его эмулировать. Запускаем повторяющееся действие. if(!hasRepeatEvents()) { m_repeat.startRepeat(getGameAction(keyCode)); } } protected void keyReleased(int keyCode) { // Если режим удерживания не поддерживается, // будем его эмулировать. Останавливаем повторяющееся действие. if(!hasRepeatEvents()) { m_repeat.stopRepeat(getGameAction(keyCode)); } } //Метод performAction() вызывается из метода keyPressed(). public void performAction(int gameAction) { // Обработка game action кода нажатой клавиши. switch(gameAction) { case UP: if(m_y>0) m_y--; break; case LEFT: if(m_x>0) m_x--; break; case DOWN: if(m_y< m_maxY) m_y++; break; case RIGHT: if(m_x< m_maxX) m_x++; break; } // Для обновления экрана вызываем метод repaint repaint(); }
С точки зрения оптимизации приложения выгодно создать один объект KeyRepeater, который затем многократно использовать. В результате можно значительно сэкономить системные ресурсы, однако программа будет способно обработать удерживание только одной клавиши. Для большинства игр этого вполне достаточно.
Создаваемый нами объект очень прост. Эмулятор удерживания должен помнить объект, который выполняет повторяющееся действие и соответствующую команду. Объект определяется уже на этапе создания эмулятора:
private KeyRepeater m_repeat = new KeyRepeater(this);
Объект пользовательского интерфейса (объект, являющийся потомком Canvas) реализует интерфейс KeyRepeater, который вызывает выполнение метода performAction().
Второе значение (выполняемую в данный момент команду) объект получает при вызове методаstartRepeat(). Это значение изменяется и при вызове методаstopRepeat(). Зачем нам понадобилось значение "game action" для остановки повторяющегося действия? Представьте ситуацию, Вы нажали кнопку ВВЕРХ перед тем как отпустили ВЛЕВО. Вам нужно чтобы игровой объект двигался вверх, даже после того как отпущена кнопка ВЛЕВО. Метод stopRepeat() прекращает действие, если код клавиши (код game action) совпал с кодом клавиши вызвавшей начало действия.
private int m_gameAction; private KeyRepeater m_ui; public KeyRepeat(KeyRepeater ui) { m_ui= ui; } public void startRepeat(int gameAction) { m_gameAction= gameAction; } public void stopRepeat(int gameAction) { if(gameAction== m_gameAction) { m_gameAction=0; } }
Удерживание реализуется с помощью потоков. Метод startRepeat() запускает поток, реализующий повторяющееся действие, а stopRepeat() останавливает его. Весь необходимый код помещается в run(). Этот метод достаточно прост: спать заданное время, а потом выполнить требуемое действие.
В программе мы будем пользоваться всего одним потоком. Это возможно, поскольку он остается всегда активным, и если в данный момент нечего обрабатывать, он, не тратя процессорное время, передает управление другим потокам. Вызов метода cancel() останавливает выполнение потока.
public void run() { while(!m_cancel) { // передаем управление другому потоку, если клавиши нет. while(m_gameAction==0&&!m_cancel) { Thread.yield(); } // Выполняем действие, в зависимости от последней нажатой кнопки. m_ui.performAction(m_gameAction); // немного ждем. try { sleep(10); } catch(InterruptedException e) { } } }
Полный код приложения Вы можете найтиздесь.
Иногда бывает нужно, чтобы действие клавиши продолжалось и после того как она отпущена. Для того чтобы добиться этого эффекта достаточно закомментировать строку
m_repeat.stopRepeat(getGameAction(keyCode));
Идея проста, при удерживании клавиши нам нужно увеличивать скорость движения игрового объекта до тех пор, пока его скорость не достигнет некоторого максимального значения. В нашем примере будем поддерживать скорость ускоряющегося спрайта, когда кнопка отпущена. Таким образом, наш спрайт не остановится до тех пор, пока его скорость не будет сброшена удерживанием противоположной клавиши.
Нам нужно знать горизонтальную скорость, вертикальную скорость и направление. Нам понадобятся новые переменные внутри класса интерфейса пользователя:
private int m_horizSpeed=0; private int m_vertSpeed=0; private int m_horizDir=0; private int m_vertDir=0;
Переменные m_horizSpeed и m_vertSpeed описывают горизонтальную и вертикальную составляющие скорости. m_horizDir и m_vertDir - горизонтальное и вертикальное направление. При нажатии клавиши устанавливаются соответствующие значения переменных направления и запускается повторяющееся действие.
protected void keyPressed(int keyCode) { int gameAction= getGameAction(keyCode); switch(gameAction) { case UP: m_horizDir=-1; break; case DOWN: m_horizDir=1; break; case LEFT: m_vertDir=-1; break; case RIGHT: m_vertDir=1; break; default: return; } m_repeat.startRepeat(gameAction); }
Когда клавиши отпущены переменные направления устанавливаются в 0.
protected void keyReleased(int keyCode) { int gameAction= getGameAction(keyCode); if(gameAction== m_repeat.getGameAction()) { switch(gameAction) { case UP: case DOWN: m_horizDir=0; break; case LEFT: case RIGHT: m_vertDir=0; break; } } }
Скорость игрового объекта вычисляется внутри метода performAction(). Вычисление вертикальной и горизонтальной скоростей аналогичны, поэтому ниже рассмотрим только горизонтальную скорость. Новое значение скорости вычисляется как сумма старого значение и соответствующей переменной направления (не забывайте, что переменные направления мы задаем внутри методов keyPressed() и keyReleased()). Новая горизонтальная координата вычисляется, как старая координата плюс половина скорости. Почему половина? Просто в этом случае ускорение получится более плавным. Если переменная направления равна 0, то скорость не изменяется.
public void performAction(int gameAction) { // вычисляем новое горизонтальное положение m_horizSpeed+= m_horizDir; if(m_horizSpeed<-MAX_SPEED) { m_horizSpeed=-MAX_SPEED; } elseif(m_horizSpeed> MAX_SPEED) { m_horizSpeed= MAX_SPEED; } m_y+= m_horizSpeed/2; // Если горизонтальная координата превысила некоторый предел, // отображаем объект с другой стороны экрана if(m_y> m_maxY) { m_y=0; } elseif(m_y<0) { m_y= m_maxY; } // Вычисляем новую вертикальную позицию. m_vertSpeed+= m_vertDir; if(m_vertSpeed<-MAX_SPEED) { m_vertSpeed=-MAX_SPEED; } elseif(m_vertSpeed> MAX_SPEED) { m_vertSpeed= MAX_SPEED; } m_x+= m_vertSpeed/2; // Если вертикальная координата превышает предел, // рисуем объект с другой стороны экрана. if(m_x> m_maxX) { m_x=0; } elseif(m_x<0) { m_x= m_maxX; } // производим перерисовку экрана. repaint(); }
Полный код программы можно найтиздесь.
На практике наиболее часто встречаются именно два рассмотренных нами типа движения, все другие типы легко сводятся к этим двум. Предложеннго кода вполне достаточно для тог чтобы вы самостоятельно реализовали любой тип движения.
В обоих случаях метод repaint() вызывается в конце performAction. Этот способ не всегда оптемален. Например, если у Вас много игровых объектов, то обнослять экран после изменения положения каждого из объектов не обязательно.