Циклическое передвижение объекта по экрану
В листинге 7.5 был нарисован синий квадрат и перемещен один раз вдоль оси X горизонтально слева направо. Но иногда в играх необходимо циклично передвигать объект, используя его, например, в качестве мишени.
Возьмем за основу код из листинга 7.5, где перемещается квадрат, и сделаем так, чтобы после пересечения всего экрана и исчезновения, он снова появлялся с другой стороны, создавая подобие циклического перемещения. Алгоритм решения этой задачи заключается в том, чтобы узнать, когда квадрат выйдет из области видимости и в тот же момент нарисовать его с другой стороны экрана и вновь переместить по экрану. Для этого создадим переменную end и присвоим ей значение окончания экрана найденное методом getwidth () (движение происходит по ширине экрана).
int.end = getwidth();
В методе run () в самом начале цикла while будем производить постоянное сравнение позиции квадрата с окончанием экрана:
if (position > end) { position = 0; }
Как только квадрат будет выходить из области экрана, его позиция обнулится и квадрат снова будет нарисован в первоначальной позиции, что зациклит движение квадрата. В листинге 7.6 представлен исходный код решающий эту задачу.
/** Листинг 7.6 Класс Main и luiacq.Draw */ import javax.microedition.lcdui.*; import javax.microedition.midlet.*; public class Main extends MIDlet implements CommandListener { // команда выхода из программы private Command exitMidlet = new Command("Выход",Command.EXIT, 0); public void startApp() { // создаем объект класса Draw Draw dr = new Draw(); // запускаем поток dr.start(); // добавляем команду выхода dr.addCommand(exitMidlet); dr.setCommandListener(this); Display.getDisplay(this).setCurrent(dr); } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void commandAction(Command c, Displayable d) { if (c == exitMidlet) { destroyApp(false); notifyDestroyedt) ; } } } /** класс Draw определен в файле Draw.Java циклическое появление квадрата */ import javax.microedition.Icdui.*; public class Draw extends Canvas implements Runnable { // позиция для перемещения квадрата int position = 10; // узнаем ширину экрана int end = getwidth(); // конструктор public Draw() { super(); } public void start () { // создаем и запускаем поток Thread t = new Thread(this); t.start(); } // метод run интерфейса Runnable public void run() { // бесконечный цикл while (true) { // сравниваем позицию квадрата if(position > end) { // обнуляем позицию квадрата position = 0; } // увеличиваем позицию на 1 position ++; // обновляем экран repaint() ; // останавливаем цикл на 20 миллисекунд try { Thread.sleep(20); } catch (Java. lang. InterruptedException zxz) {} ) } public void paint(Graphics g) { // вычисляем область для перерисовки экрана int x = g.getClipWidth(); int у = g.getClipHeight(); // устанавливаем белый цвет фона g.setColor(0xffffff); // назначаем перерисовку всему экрану g.fillRect(0,0,х,у); //устанавливается синий цвет квадрата g.setColor(0, 0, 200); // рисуем квадрат g.fillRect(position,40, 20, 20); } }
Программирование графики
В этой главе...
7.1. Класс Canvas 7.1.1. Методы класса Canvas 7.2. Класс Graphics 7.2.1. Методы класса Graphics 7.3. Рисование линий 7.4. Рисование прямоугольников 7.5. Рисование дуг 7.6. Вывод текста 7.7. Механизм создания игрового цикла 7.8. Перемещение квадрата 7.9. Циклическое передвижение объекта по экрану 7.10. Столкновение 7.11. Перемещение объекта с помощью клавиш
Класс Canvas
Класс Canvas - это абстрактный класс, поэтому необходимо создавать подклассы для работы с классом Canvas. Абстрактный класс Canvas представляет некий обобщенный графический контекст, что позволяет программе производить прорисовку графики при помощи класса Graphics. Кроме этого класс Canvas предоставляет возможность в обработке событий полученных с клавиш телефона. Если классы высокоуровневого интерфейса, рассмотренные в главах 5 и 6, обрабатывают команды перехода, то-с помощью класса Canvas можно получать события с любой нажатой клавиши телефона.
Существует ряд так называемых «ключевых кодов» в виде заданных констант, с помощью которых можно назначать игровые действия для клавиш телефона. Все ключевые коды соответствуют стандарту ITU-T и заданны в виде следующих констант:
static int DOWN — движение вниз; static int FIRE - обычно используется в играх и реализует стрельбу из оружия; static int GAME_A — игровая клавиша А; static int GAME_B - игровая клавиша В; static int GAME_C - игровая клавиша С; static int GAME_D - игровая клавиша D; static int KEY_NUMO - клавиша 0; static int KEY_NUM1-клавиша 1; static int KEY_NUM2 - клавиша 2; static int KEY_NUM3 - клавиша 3; static int KEY_NUM4 - клавиша4; static int KEY_NUM5 - клавиша 5; static int KEY_NUM6 - клавиша 6; static int KEY_NUM7 - клавиша 7; static int KEY_NUM8 - клавиша 8; static int KEY_NUM9 - клавиша 9; static int KEY_POUND - клавиша #; static int KEY_STAR - клавиша *; static int LEFT - движение влево; static int RIGHT - движение вправо; static int UP - движение вверх.
Ключевые коды GAME_A, GAME_B, GAME_C, GAME_D и FIRE предназначены специально для игровых действий и обычно задаются клавишам с цифрами соответственно 2,4,8,6 и 5, но зависят от реализации конкретных моделей телефонов.
Класс Graphics
При помощи класса Graphics осуществляется двухмерное представление графики на экране телефона. Класс Graphics существует также в составе Java 2 SE, но в платформе Java 2 ME он сильно урезан, всвязи с ограниченными системными ресурсами телефонов. Поэтому имеется возможность рисовать только линии, прямоугольники, дуги и текст. Для окрашивания этих примитивов предусмотрена работа с цветовой компонентой.
Система координат, используемая для представления графики в Java 2 ME, перевернута по отношению к обычной Декартовой системе координат. Начало системы координат, находится в левой верхней точке экрана телефона. Положительная ость X проходит по верхней кромке экрана слева направо, а положительная ось Y- сверху вниз по левой стороне экрана, как изображено на рис. 7.1.
Такая система координат отнюдь не новшество в программировании двухмерной графики. Идентичная модель координат применяется так же в DirectX и OpenGL для представления двухмерных сцен, но уже в компьютерной графике.
Рис. 7.1. Система координат в Java 2 ME
Механизм создания игрового цикла
Для создания цикла классом Canvas используется интерфейс Runnable и его единственный метод run (), в котором реализуется цикл прорисовки графики. Рассмотрим в качестве примера класс DemoGraphics и проанализируем его.
public class DemoGraphics extends Canvas implements Runnable
{
public void run()
{
while(true)
{
// обновление графических элементов repaint();
// задержка цикла на 20 миллисекунд для обновления состояния дисплея
Thread.sleep(20);
} }
public void paint( Graphics g )
{
// код, создающий графические элементы
}
public void keyPressed( int keyCode )
{
// обработка событий с клавиш телефона при помощи ключевых кодов
}
}
В классе DemoGraphics для упрощения опущен основной код, связанный с прорисовкой графики и обработкой событий, поступающих с клавиш телефона. Весь цикл прорисовки графики состоит из трех методов: run (), paint () и keyPressed().
В методе paint () происходит создание и отрисовка графических элементов программы на экране телефона, а метод run () создает цикл, в котором происходит постоянное обновление экрана телефона, связанное, например, с движением графического элемента. События, полученные в результате нажатия клавиш, поступают в метод key Pressed (), где обрабатываются заданным вами способом. На основании этих событий также может происходить обновление цикла прорисовки графики.
В методе run () создается бесконечный цикл while (true). В реальной программе необходимо, конечно, предусмотреть выход из бесконечного цикла. Метод repaint () постоянно обновляет графические элементы. Метод sleep () класса Thread останавливает системный поток на двадцать миллисекунд, для того чтобы отреагировать на все произошедшие изменения, а именно, произвести обработку событий с клавиш телефона и перерисовать графический элемент на экране телефона. Механизм прорисовки графики в профиле MIDP 1.0 строится именно по такому принципу , связанному с изменением состояния графических элементов и называмому игровым циклом.
Теперь давайте рассмотрим как можно больше примеров, реализующих вывод графики на экран, перемещение графических элементов, обработку событий с клавиш телефона, столкновение графического элемента с препятствием и других хитростей, из которых состоят мобильные игры.
Методы класса Canvas
Большинство методов класса Canvas обеспечивают обработку низкоуровневых событий. Абстрактный метод void paint (Graphics g) является основным методом, с помощью которого происходит прорисовка графики на экране телефона. Класс Graphics определяет, что именно необходимо рисовать на экране телефона. Разберем основную часть методов класса Canvas:
int getGameAction(int keyCode) - связывает игровые действия с заданным ключевым кодом; int getKeyCode(int gameAction) - получает ключевой код игровых действий; String getKeyName (int keyCode) - получает ключевой код для клавиши; boolean hasPointerMotionEvents () - проверяет поддержку устройством перемещение указателя; protected void keyPressed( int keyCode) - вызывается при нажатии клавиши; protected void keyReleased(int keyCode) - вызывается при отпускании нажатой клавиши; protected void keyRepeated(int keyCode) - повторное нажатие клавиши; protected abstract void paint (Graphics g) - прорисовка графики на экране телефона; protected void pointerDragged(int x, int у) - определяет перемещение курсора; protected void pointerPressed(int x, int у) -определяет позицию курсора, при которой должно производится нажатие определенной клавиши; protected void pointerReleased(int x, int у) -определяет Позицию курсора в момент отпускания определенной клавиши; void repaint () - повторяет прорисовку; void repaint(int x, int у, int width, int height)-повторяет прорисовку заданной области.
Методы класса Graphics
Основные методы класса Graphics обеспечивают прорисовку двухмерной графики. Есть еще несколько методов, с помощью которых можно произвести перемещение системы координат и произвести отсечение (clipping). Основные методы класса Graphics:
void copyArea(int x_src, int y_src, int width, int height, int x_dest, int y_dest, int anchor)- копирует прямоугольную область из установленных значений в параметрах (x_src, y_src, width, height), в новую область (x_dest, y_dest); void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) - рисует контур дуги в виде эллипса; void drawChar(char character, int x, int y, int anchor) -рисует символ; void drawChars(char[] data, int offset, int length, int x, int y, int anchor) - рисует массив символов: void drawlmage (Image img, int x, int y, int anchor) -рисует изображение; void drawLine(int x1, int y1, int x2, int y2) - рисует линию из точки x1и y1, до точки х2 и у2; void drawRegion(Image src, int x_src, int y_src, int width, int height, int transform, int x_dest, int y_dest, int anchor) - копирует изображения в заданную область на экран телефона; void drawRoundRect(int x, int у, int width, int height, int- arcWidth, int arcHeight) — рисует контур прямоугольника, используя закругленные углы; void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) - рисует заполненную цветом дугу; void fillRect(int x, int у, int width, int height)- рисует заполненный цветом прямоугольник; void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) - рисует заполненный прямоугольник, используя закругленные углы; void fillTriangle(int x1, int y1, int x2, int y2, int x3, int y3) - рисует заполненный цветом треугольник; int getBlueComponent () - получает синий компонент цвета; int getClipHeight () -получает высоту для текущей области отсечения; int getClipWidth() -получает ширину для текущей области отсечения; int getColor() - получает текущий цвет; Font getFont() - получает текущий шрифт; int getGreenComponent () -получает зеленный компонент цвета; int getRedComponent ()- получает красный компонент цвета; void setClip(int x, int y, int width, int height)-устанавливает отсечение заданной области экрана; void setColor (int RGB) - устанавливает цвет при помощи значения RGB; void setColor (int red, int green, int blue) - назначает цвет при помощи трех цветовых компонентов red, green и blue; void setFont (Font font) - устанавливает заданный шрифт; void setStrokeStyle (int style) - задает штриховой стиль рисуемому контексту, используя константы SOLID и DOTTED; void translate (int x, int у) - перемещает систему координат в точку х и у.
При использовании некоторых методов, очень часто используется параметр int anchor. С помощью этого параметра задаются различные значения для выбора позиции. Посмотрите на рис. 7.2, где изображен механизм прорисовки текста с выбором определенной позиции.
Рис. 7.2. Техника прорисовки текста
Для этих целей в классе Graphics имеются константы, с помощью которых происходит выбор позиции:
static int BASELINE - задает базовую линию; static int BOTTOM - сдвигает вниз; static int HCENTER - центрирует; static int LEFT - сдвигает влево; . static int RIGHT - сдвигает вправо; static int TOP - сдвигает вверх; static int VCENTER - используется только при прорисовке изображений, производит вертикальную центровку всего изображения.
Можно использовать две константы для выбора позиции. Например, для того чтобы сдвинуть текст влево и вверх, используется комбинация Graphics. LEFT I Graphics .TOP.
Далее мы перейдем к практике и изучим модель программирования графики в приложении на Java 2 ME, рассмотрим создание и отрисовку линий, прямоугольников, дуг и текста. Главное о чем надо помнить при использовании графических элементов - это о размере экрана телефона. Разные модели телефонов имеют свои размеры дисплея, и если вы будете использовать большой по площади экран, например 128x128 пикселей, то на экране с разрешением 101У80, некоторые части графических элементов будут срезаны. Чтобы этого избежать, надо использовать методы класса Canvas, getwidth () и getHeight (), которые возвращают размеры ширины и высоты экрана и уже на основании этих данных производить построение графических элементов, производя тем самым адаптацию графического контекста к конкретной модели телефона. Например, чтобы нарисовать, горизонтальную линию, не выходящую из зоны видимости, можно воспользоваться следующим кодом:
int w = getWidth.{); drawLine(20, 20, w-20, w-20);
В своих примерах к этой главе я специально не использую оптимизации графики, для того чтобы исходный код был более понятен. Поэтому после компиляции всех примеров обязательно запустите получившиеся программы на максимальном количестве имеющихся эмуляторов. Особенно попробуйте работу этих программ на эмуляторе DefaultColorPhone из состава среды программирования J2ME Wireless Toolkit 2.1, меня лично очень сильно позабавил результат работы этого эмулятора, а вам, я думаю, предоставит некоторую пищу для размышлений.
Перемещение квадрата
Начнем с самого простого - выведем на экран синий квадрат, прорисованный с помощью метода fillRect() и заставим переместиться его через весь экран по горизонтали слева на право. Посмотрите на код из листинга 7.5, производящий перемещение квадрата на экране.
/ * * Листинг 7.5 Класс Main и класс Draw */ import javax.micro-edition. Icdui .*; import javax.microedition.midlet.*; public class Main extends MIDlet implements CommandListener { // команда выхода из программы private Command exitMidlet = new Command("Выход", Command.EXIT, 0); public void startApp() { // создаем объект класса Draw Draw dr = new Draw(); // запускаем поток dr.start(); // добавляем команду выхода dr.addCommand(exitMidlet); dr.setCommandListener(this); Display.getDisplay(this).setCurrent(dr); } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void commandAction(Command c, Displayable d) { if (с == exitMidlet) { destroyApp(false) ; notifyDestroyed() ; } } /** класс Draw определен в файле Draw.Java перемещает квадрат по экрану */ import javax.microedition.lcdui.*; public class Draw extends Canvas implements Runnable { // позиция для перемещения квадрата int position =10; // конструктор public Draw() { super(); } public void start() { // создаем и запускаем, поток Thread t = new Thread(this); t.start(); } // метод run() интерфейса Runnable public void run() { // бесконечный цикл while (true) { // увеличиваем позицию на 1 position ++; // обновляем экран repaint(); // останавливаем цикл на 20 миллисекунд try { Thread.sleep(20); } catch (java.lang.InterruptedException zxz) {} } } public void paint(Graphics g) { // вычисляем область для перерисовки экрана int x = g.getClipWidth(); int у = g.getClipHeight () // устанавливаем белый цвет фона g.setColor(0xffffff); // назначаем перерисовку всему экрану g.fillRect(0,0,х,у); // устанавливается синий цвет квадрата vg.setColor(0, 0, 200); // рисуем квадрат g.fillRect(position,40, 20, 20); } }
Листинг 7.5 содержит два класса Main и Draw, находящиеся в файлах Main.java и Draw.Java. Основной класс мидлета Main содержит код создающий объект dr класса Draw. Затем он запускает системный поток с помощью метода start (), добавляет команду выхода из программы и отображает текущий дисплей с объектом dr. Класс Draw содержит код, создающий синий квадрат и перемещающий его по экрану. Прежде чем мы начнем рассмотрение кода класса Draw, давайте подумаем, как можно произвести перемещение объекта на экране. Самый простейший способ перемещения объекта по экрану вдоль оси X, заключается в постоянном увеличении, допустим на единицу, позиции этого объекта по оси X. При создании квадрата методом fillRect () задаются две координаты по оси X и по оси Y для первоначального вывода квадрата на экран. Поэтому достаточно создать переменную для координаты по оси X и затем в цикле прорисовки увеличивать ее на единицу, перемещая тем самым объект по экрану.
В листинге 7.5 класс Draw наследуется от класса Canvas, что дает возможность воспользоваться Методом paint (). для рисования графических элементов на экране и. реализации метода run () интерфейса Runnable. В методе run () создается цикл, постоянно обновляющий состояние графических элементов.
В начале кода класса Draw создается переменная position, отвечающая за координату по оси X для точки вывода квадрата на экран. Конструктор класса Draw вызывает метод super () для использования конструктора своего суперкласса Canvas.
В методе start () создается системный поток, который будет запущен в методе run (). Кстати, было бы не плохо предусмотреть выход из потока. Этот пример небольшой и проблем с ним не возникнет, а после разбора листинга 7.5, мы рассмотрим простой механизм, останавливающий системный поток и предусматривающий выход из бесконечного цикла while метода run (). В самом методе run () создается бесконечный цикл, в котором происходит постоянное увеличение переменной position на единицу, благодаря чему квадрат перемещается по оси X слева направо.
В методе paint () вас должны заинтересовать следующие строки кода:
int х = g.getClipWidthf); int у = g.getClipHeight(); g.setColor(0xffffff); g.fillRect(0,0,x,y);
Здесь используется отсечение (clipping) необходимое для корректной прорисовки графики. Если не использовать отсечение, то после того как вы нарисуете квадрат и начнете передвигать его, на экране будет рисоваться слева на право одна толстая синяя линия. То есть будет происходить постоянная перерисовка квадрата в новой позиции по оси X, но при этом будет оставаться и предыдущий нарисованный квадрат. Чтобы этого избежать имеется два способа: можно стирать определенный участок экрана, а можно просто перерисовывать цвет фона всего экрана. Такая операция в Java 2 ME называется отсечением, и для произведения этой операции используются методы: getClipWigthf) и getClipHeight (), производящие отсечение поверхности всего экрана и методы getClipX () и getClipY () для более точного указания координат заданной области экрана для отсечения. По сути, использование этих методов приводит к простому обновлению всего экрана либо заданной области экрана. В этом примере мы будем использовать перерисовку всего фона экрана. Ширина и высота экрана узнается с помощью методов getClipWigth () и getClipHeight (), полученные значения сохраняются в переменных х и у. Вызовом метода setColor () устанавливается белый цвет фона и в следующей строке кода задается прямоугольник для области отсечения:
g.fillRect(0, 0, х, у );
В конце метода paint () рисуем синий квадрат:
g.fillRect(position, 40, 20, 20);
С каждым проходом через графический цикл, цвет фона будет перерисовываться и стирать прошлую позицию квадрата, а квадрат постоянно перерисовывается на новом месте в соответствии со значением переменной position, что и создает иллюзию передвижения объекта по экрану телефона.
После компиляции примера из листинга 7.5, на экране появится синий квадрат и пересечет один раз дисплей телефона слева направо. Теперь что касается бесконечного цикла while. В методе run () в реальном приложении необходимо предусмотреть выход из него, а так же позаботиться о прекращений работы потока. Самый простой способ заключается в создании переменной отвечающей за состояние цикла в начале класса Draw, например:
boolean z;
Дальше в методе start () присвоить этой переменной значение true.
public void start() { z = true; Thread t = new Thread(); t.start(); }
А в методе run () использовать значение переменной z для входа в цикл
while while(z) { // код цикла }
Для логического конца создается новый метод stop () в классе Draw, назначая переменной z значения false.
public void stop() { z = false; }
Вызовем этот метод в классе Main в методе destroyApp ():
public void destroyApp(boolean unconditional) { z.stop (); }
Перемещение объекта с помощью клавиш
Перемещение объекта по экрану телефона с помощью клавиш телефона, это, пожалуй, самое главное действие в играх. Для передвижения нужно воспользоваться методом key Pressed () и описать код производящий обработку событий получаемых с клавиш телефона. При реализации метода keyPressed (), сначала необходимо вызвать метод getGameAction () для получения ключевого код»! с последующей его обработкой. Для обработки полученного ключевого кода можно применить оператор switch и тогда метод keyPressed () может принять следующий вид:
protected void keyPressed(int keyCode) { // получаем игровые события int act = getGameAction{keyCode); // обработка событий switch(act) { case Canvas.LEFT: // движение влево break; case Canvas .RIGHT: // движение вправо break; case Canvas.UP: // движение вверх break; case Canvas.DOWN: // движение вниз break; default: break; } } }
Код обработки для клавиш зависит от того, что именно вы желаете сделать с объектом. Давайте возьмем за основу код из листинга 7.5, где был нарисован квадрат, и модернизируем его. Выведем синий квадрат на экран, для этого создадим две переменные positionX и positionY, отвечающие за точку вывода квадрата на экран и присвоим им значения близкие к центру экрана.
int positionX. = getWidth()/2; int positionY = getHeight()/2;
При нажатии клавиш можно воспользоваться значениями переменных positionX и positionY для перемещения квадрата по экрану, увеличивая или уменьшая значения этих двух переменных. Значение, на которое вы будете увеличивать или уменьшать переменные, фактически будет обозначать скорость перемещения квадрата по экрану. В листинге 7.8 дается полный код программы перемещения элемента по экрану с помощью клавиш.
/** Листинг 7.8 Класс Main и класс Draw */ import javax.microedition.Icdui.*; import javax.microedition.midlet.*; public class Main extends MIDlet implements CommandListener . { // команда выхода из программы private Command exitMidlet = new Command(«Выход», Command.EXIT, 0); public void startApp() { // создаем объект класса Draw Draw dr = new Draw(); // запускаем поток dr.start(); // добавляем команду выхода dr.addCommand(exitMidlet); dr.setCommandListener(this); Display.getDisplay(this).setCurrent(dr); } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void сommandAction(Command c, Displayable d) { if (c = = exitMidlet) { destroyApp(false); notifyDestroyed() } } } /** класс Draw определен в файле Draw.Java передвижение квадрата с помощью клавиш телефона */ import javax.microedition.Icdui.*; public class Draw extends Canvas implements Runnable { // устанавливаем квадрат в центр экрана int positionX = getwidth()/2; // устанавливаем квадрат в центр экрана int positionY = getHeight()/2; // конструктор public Draw() { super(); } public void start() { // создаем и запускаем поток Thread t = new Thread(this); t.start(); } // метод run интерфейса Runnable public void run() { // бесконечный цикл while (true) { // обновляем экран repaint(); // останавливаем цикл на 20 миллисекунд try { Thread, sleep (20) ,- } catch (Java.lang.InterruptedException zxz) {} } } public void paint(Graphics g) { // вычисляем область для перерисовки экрана int x = g.getClipWidth(); int у = g.getClipHeight(); // устанавливаем белый цвет фона g.setColor(0xffffff); // назначаем перерисовку всему экрану g.fillRect(0,0,х,у); // устанавливается зеленный цвет квадрату g.setColor(0, 0, 200); 7/ рисуем квадрат g.fillRect(positionX, positionY, 20, 20); projected void keyPressed(int keyCode) // скорость передвижения int speed = 3 ; // получаем игровые события int act = getGameAction(keyCode); // обработка событий switch(act) { // движение влево case Canvas.LEFT: positionX -= speed; break; // движение вправо case Canvas.RIGHT: positionX += speed; break; // движение вверх case Canvas.UP: positionY -= speed; break; // движение вниз case Canvas.DOWN: positionY += speed; break; default: break; } } }
В следующей главе рассматриваются игровые классы, доступные в профиле MIDP 2.0 и значительно упрощающие создание игрового цикла, а также игровом графики.
Рисование дуг
В английском языке слово arc означает дугу, и именно это слово применяется в документации по Java 2 ME. Используя методы drawArc () и fillArc () можно нарисовать как дугу, так и полноценную окружность. Используя оба метода, как вы уже наверно заметили можно нарисовать контур дуги и закрашенную цветом дугу. Методы drawArc () и fillArc () имеют одинаковое количество параметров со сходными действиями. Рассмотрим один из методов - fillArc ().
public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle)
Параметры метода fill Arc():
x - расстояние, откладываемое от оси X до мнимой вертикальной касательной к окружности; у - расстояние, откладываемое от оси Y до мнимой горизонтальной касательной к окружности; wight - ширина рисуемой дуги (горизонтальный радиус); height - высота рисуемой дуги (вертикальный диаметр); startAngle - стартовый угол для начала рисования дуги; arcAngle - протяженность дуги (значение 360 замкнет дугу в окружность).
На рис. 7.5 изображена схематично техника создания дуги.
Рис. 7.5. Техника создания дуги
В листинге 7.3 приведен пример кода создающего три разноцветных сегмента круга, наложенных друг на друга, и дугу в виде контура.
/** Листинг 7.3
Класс Main и класс Arc */ import javax.microedition. Icdui.*; import javax.microedition.midlet.*; public class Main extends MIDlet implements CommandListener { // команда выхода из программы private Command exitMidlet = new Command("Выход", Command.EXIT, 0) public void startApp() { // создаем объект класса Arc Arc myarc = new Arc(); // добавляем команду выхода myarc.addCommand(exitMidlet); myarc.setCommandListener(this); Display.getDisplay(this).setCurrent(myarc); } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void commandAction(Command c, Displayable d) { if (c = = exitMidlet) { destroyApp(false); notifyDestroyed(); } } /** класс Arc определен в файле Arc.Java рисует дуги */ import javax.microedition.Icdui.*; public class Arc extends Canvas { // конструктор public Arc(){ super(); } public void paint(Graphics g) { // устанавливается красный цвет g.setColor(255, 0, 0); // рисуем первую заполненную цветом дугу g.fillArc(15, 15, 60., 60, 45, 360); // устанавливается зеленый цвет g.setColor(0, 255, 0) ; // накладываем вторую дугу поверх первой g.fillArc(15, 15, 60, 60, 45, 180); // устанавливается синий цвет g.setColor(0, 0, 255); // накладываем третью дугу поверх первых двух g.fillArc(15, 15, 60, 60, 45, 90); // устанавливается синий цвет для дуги в виде контура g.setColor(0, 0, 255); // рисуем контур дуги g.drawArc(5, 5, 80, 80, 30, 180); } }
В листинге 7.3 используется тот же самый механизм, что и в примерах из листингов 7.2 и 7.1 - создаются два класса: Main и Arc, находящиеся в файлах Main.java и Arc.java. Все действия по прорисовке дуг осуществляются в методе
paint().
g.setColor(255, 0,0); g.fillArc(15, 15, 60, 60, 45, 360);
В этих строках кода происходит установка цвета для рисуемой дуги и происходит прорисовка самой дуги. Первые два значения в методе fillArc () - 15 и 15 пикселей задают координаты точки в пространстве, относительно которой будет происходить прорисовка дуги. Значения 60 и 60 пикселей задают ширину и высоту дуги. Значением 45 устанавливается угол для начала рисования дуги (со значением 360 будет нарисована замкнутая окружность). Затем в примере рисуются еще два сегмента зеленого и синего цвета, наложенные поверх первой нарисованной дуги.
Рисование линий
Для того чтобы нарисовать линию нужно воспользоваться методом draw-Line () класса Graphics. Рассмотрим прототип этого метода:
public void drawLine(int x1, int y1, int x2, int y2)
Параметры метода drawLine ():
x1 - координата начальной точки отрезка по оси X; у1 - координата начальной точки отрезка по оси Y; х2 - координата конечной точки отрезка по оси X; у2 - координата конечной точки отрезка по оси Y.
Задавая целочисленные координаты точек в методе drawLine () можно нарисовать любую линию заданного размера. Также можно воспользоваться методом setColor() для закрашивания линий цветом и методом setStrokeSty le () -для рисования линии в виде пунктира.
Рассмотрим пример кода находящийся в листинге 7.1, где с помощью линий рисуется система координат используемая в Java 2 ME.
/ * * Листинг 7.1 Класс Main и класс Line */ import javax..microedition.lcdui .*; import javax.microedition.midlet.*; public class Main extends MIDlet implements CommandListener { // команда выхода из программы private Command exitMidlet = new Command("Выход",Command.EXIT, 0); public void startApp() { // создаем объект класса Line Line myline = new Line(); // добавляем команду выхода myline.addCommand(exitMidlet); myline.setCommandListener(this); Display.getDisplay(this).setCurrent(myline); } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void commandAction(Command c, Displayable d) { if (c = = exitMidlet) { destroyApp(false); notifyDestroyed(); } } } /** класс Line определен в файле Line.Java */ import javax.microedition.Icdui.*; public class Line extends Canvas { // конструктор public Line(){ super(); } public void paint(Graphics g) { // устанавливается синий цвет для линий g.setColor(0x0000ff); // рисуется система координат g.drawLine(20, 20, 90, 20); g.drawLine(20, 20, 20, 90) g.drawLine(90, 20, 80, 10) g.drawLine(90, 20, 80, 30) g.drawLine(20, 90, 10, 80) g.drawLine(20, 90, 30, 80) // устанавливается красный цвет для трех линий g.setColor(0xffff0000); // рисуем толстую линию, в три пикселя g.drawLine(30, 30, 70, 30); g.drawLine(30, 31, 70, 31); g.drawLine(30, 32, 70, 32); // устанавливаем пунктир g.setStrokeStyle(Graphics.DOTTED) ; // рисуем линию в два пикселя пунктиром g.drawLine(30, 50, 70, 50); g.drawLine(30, 51, 70, 51); } }
В листинге 7.1 используются два класса: Main, являющийся основным классом мидлета приложения, и класс Line, в котором происходит работа с графикой. Подразумевается, что оба класса находятся в файлах Main.java и Line.java, это характерно для объектно-ориентированного программирования и в дальнейшем мы будем придерживаться именно такой модели построения изучаемых примеров. Основной класс мидлета Main очень прост. В этом классе создается объект класса Line, добавляется команда выхода из приложения и отражается текущий экран. Класс Line, находящийся в файле Line.java листинга 7.1, рисует набор различных линий. Сам класс Line наследуется от абстрактного класса Canvas. В более сложных программах может использоваться интерфейс Run-nable и метод run (). Такая техника программирования обычно используется при создании игр, и будет обсуждаться в конце этой главы.
Конструктор класса Line использует метод super () позволяющий обратиться к конструктору своего суперкласса Canvas. Основные же события происходят в методе paint () класса Canvas.
В строке кода:
g.setColor(OxOOOOff)
происходит назначение цвета для любого последующего отрисованного примитива. То есть вызов метода setColor () с заданным цветом действителен до момента последующего вызова метода setColor (), устанавливающего другой цвет. В этом примере используется метод setColor (RGB), поэтому значение цвета задается восьмеричным значением с помощью нулей и букв. В следующем примере из раздела 7.4 при рисовании прямоугольников будет показана работа метода setColor () с тремя целочисленными параметрами, задающими значение цвета.
В строках кода
g.drawLine(20, 20, 90, 20) g.drawLine(20, 20, 20, 90) g.drawLine(90, 20, 80, 10) g.drawLine(90, 20, 80, 30) g.drawLine(20, 90, 10, 80) g.drawLine(20, 90, 30, 80)
рисуется шесть синих линий, образующих систему координат. Толщина всех линий равна одному пикселю - это значение по умолчанию и изменить его нельзя. Для того чтобы нарисовать широкую линию, придется рисовать несколько соприкасающихся одинаковых по размеру линий.
g.drawLine(30, 30, 70, 30); g.drawLine 30, 31, 70, 31); g.drawLine(30, 32, 70, 32);
Этими строками кода рисуется одна толстая линия шириной в три пикселя.
В конце в методе paint () рисуется линия толщиной в два пикселя в виде пунктирной линии. Для этого используется метод setStrokeStyle () и константа DOTTER. На рис. 7.3 изображен эмулятор телефона с результатом работы программы из листинга 7.1.
Рис. 7.3. Рисование разноцветных линий
Рисование прямоугольников
При создании прямоугольников можно использовать два метода класса Graphics- это drawRect() и fillRect (). При помощи метода drawRect () рисуется только контур прямоугольника, а метод fillRect () позволяет нарисовать прямоугольник уже закрашенным каким-либо цветом (изначально по умолчанию цвет черный). Оба метода абсолютно идентичны по количеству и назначению параметров, поэтому рассмотрим прототип одного из них, а именно метода drawRect ().
public void drawRect(int x, int y, int width, int height)
Параметры метода drawRect ():
x - координата точки по оси X для левого верхнего угла прямоугольника; у - координата точки по оси Y для левого верхнего угла прямоугольника; width - ширина рисуемого прямоугольника; height - высота рисуемого прямоугольника.
В составе класса Graphics имеется еще один метод рисующий прямоугольник, но с закругленными углами - drawRoundRqct (). Этот метод имеет уже шесть параметров, где первые четыре параметра работают в том же ключе что и методы drawRect () и fillRect (), а два последних параметра определяют степень закругленности углов. В листинге 7.2 рисуется три разных по размеру прямоугольника, и закрашиваются тремя цветами: красным, зеленым и синим. Для закрашивания прямоугольников применяется метод setColor (int red, int green, int blue) с тремя параметрами. Выставляя любое целочисленное значение от 0 до 255 для каждого параметра можно создавать разнообразную цветовую гамму, естественно учитывая при этом цветовые возможности телефона, на котором будет работать эта программа. Для того чтобы определить доступную цветность дисплея телефона, необходимо воспользоваться методами класса Display:
isColor () - если телефон поддерживает цветовую гамму, то возвращает значение true; numColor() - определяет количество доступных цветов.
/** Листинг 7.2 Класс Main и класс Rectangles */ import javax. micro-edit ion. Icdui.*; import javax.microedition.midlet.*; public class Main extends MIDlet implements CommandListener { // команда выхода из программы private Command exitMidlet = new Command("Выход", Command.EXIT, 0); public void startAppO { // создаем объект класса Rectangles Rectangles myrec = new Rectangles(); // добавляем команду выхода myrec. addCommand( exitMidlet); myrec.setCommandListener(this); Display.getDisplay(this).setCurrent(myrec); } public void pauseApp() {} public void destroyApp(boolean unconditional){} } public void coiranandAction(Command c, Displayable d) { if (c == exitMidlet) { destroyApp (false) ,notifyDestroyed(); } } } / * * класс Rectangles определен в файле Rectangles.Java, */ import javax..microedition. Icdui ; public class Rectangles extends Canvas { // конструктор public Rectangles(){ super(); } public void paint(Graphics g) { // устанавливается красный цвет g.setColor(255, 0,0); // рисуем первый прямоугольник g.fillRect(/*x*/ 15,/*у*/ 30,/*ширина*/ 15,/*высота*/ 20); // устанавливается зеленный цвет g.setColor(0, 255, 0); // рисуем второй прямоугольник g.fillRect(30, 30, 15, 45); // устанавливается синий цвет g.setColor(0, 0, 255); // рисуем третий прямоугольник g.fillRect(45, 30, 15, 60); // устанавливается синий цвет g.setColor(255, 0, 0); // рисуем прямоугольник с закругленными углами g.drawRoundRect(70, 30, 40, 40, 10, 10); } }
В этом примере также используются два класса - класс Main, играющий роль основного класса мидлета и класс Rectangles, где происходит отрисовка графики. Оба класса разделены на два файла Main.java и Rectangles.java. В классе Main создается объект класса Rectangles, добавляется команда выхода и показывается текущий экран. Класс Rectangles является подклассом класса Canvas. Прорисовка прямоугольников происходит в методе paint () класса Graphics.
g.setColor(255, 0, 0) ;
В этой строке кода устанавливается красный цвет для прямоугольника размером 15 на 20 пикселей, который рисуется с помощью метода fillRect ().
g.fillRect(/*x*/ 15,/*у*/30,/*ширина*/15,/*высота*/20);
Прямоугольник рисуется закрашенным в красный цвет. Дальше происходит прорисовка еще двух закрашенных в зеленый и синий цвет прямоугольников с размерами соответственно 15x45 и 15x60 пикселей.
В конце всего кода в классе Rectangles рисуется контур прямоугольника с закругленными углами. На рис. 7.4 изображен эмулятор телефона с четырьмя нарисованными прямоугольниками.
Рис. 7.4. Рисование четырех прямоугольников
Столкновение
В предыдущем разделе 7.9 мы выведи на экран синий квадрат, задали ему вектор движения и перемещали квадрат горизонтально через весь экран. После того как квадрат исчезал, достигнув конца экрана, он циклично появлялся вновь с другой стороны. Следующий пример иллюстрирует столкновение круга и квадрата с препятствием, а именно, окончанием экрана телефона. Оба объекта рисуются независимо друг от друга по высоте экрана и перемещаются параллельно по горизонтали слева на право. По достижению конца экрана оба объекта отталкиваются от конца экрана и начинают движение в обратном направлении. Посмотрите на листинг 7.7, где приводится код программы осуществляющий эти действия. Эта программа состоит из двух файлов Main.java и Draw.java./** Листинг 7.7 Класс Main и класс Draw */ import javax.microedition.Icdui.*; import javax.microedition.midlet.*; public class Main extends MIDlet implements. CommandListener { //команда выхода из программы private Command exitMidlet = new Command("Выход", Command.EXIT, 0); publicvoid startApp() { // создаем объект класса Draw Draw dr = new Draw(); // запускаем поток dr.start(); // добавляем команду выхода dr.addCommand(exitMidlet) ; dr.setCommandListener(this); Display.getDisplay(this).setCurrent(dr); } public void pauseApp() {} public void destroyApp(boolean unconditional)} public void сommandAction(Command c, Displayable d) { if (c == exitMidlet) { destroyApp(false); notifyDestroyed(); } } } /** класс Draw определен в файле Draw.Java рисует круг и квадрат */ import javax.microedition.Icdui.*; public class Draw extends Canvas implements Runnable { // позиция для перемещения квадрата и круга int position = 0; // узнаем ширину экрана int endX = getWidth(); // конструктор public Draw() { super(); } public void start() { // создаем и запускаем поток Thread t = new Thread(this); t.start(); } //метод run интерфейса Runnable public void run() { // бесконечный цикл while (true) { // сравниваем позицию if(position > endX) { //уменьшаем позицию position = endX; } // увеличиваем позицию на 1 position ++; // обновляем экран repaint(); // останавливаем цикл на 20 миллисекунд try { Thread.sleep(20); } catch (java.lang.InterruptedException zxz) {} } } publie void paint(Graphics g) { // вычисляем область для перерисовки экрана int х = g.getClipWidth() ; int у = g.getClipHeight () ; // устанавливаем желтый цвет фона g.setColor(0xffff00); // назначаем перерисовку всему экрану g.fillRect(0,0,x,y); // устанавливается синий цвет квадрата g.setColor(0, 0, 200); // рисуем квадрат g.fillRect(position, 40, 20, 20); // устанавливается красный цвет круга g.setColor(250, 0,0); // рисуем круг fillArc(position, 10, 20, 20, 45, 360); } }
В файле Main.java создается объект класса Draw, запускается системный поток и отражается текущий экран. Файл Draw.java содержит полную реализацию класса Draw. Начальные позиции квадрата и круга определяются переменной position, которой задано нулевое значение. Для того чтобы определить момент столкновения, необходимо знать точку или координаты препятствия. В этом примере как препятствие применяется конец экрана с правой стороны, поэтому с помощью метода getWidth () находится конец экрана.
int endX = getWidth();
В последствии значение переменной endx будет использовано для определения столкновения.
Конструктор, класса Draw обращается к конструктору своего суперкласса Canvas. В методе run() и цикле while с помощью оператора отношения if происходит сравнение текущей позиции двух объектов и конца экрана.
if (position > endX) { position = endX -; }
После того как позиция обоих объектов становится больше значения переменной endx, то есть квадрат и круг достигают конца экрана, то происходит уменьшение переменной position. Это приводит к движению объектов в обратном направлении.
После компиляции примера на экране появятся два движущихся слева направо объекта. Мы использовали препятствие в виде одной стороны экрана, но точно таким же образом можно определить столкновение для всех сторон экрана или просто для статических объектов. Выполните эти упражнения в качестве домашнего задания, а также внимательно посмотрите на процесс работы примера из листинга 7.7, квадрат и круг все-таки не отталкиваются от кромки экрана, а исчезают. Это происходит, потому что опорная позиция объектов находится в левом верхнем углу и столкновение происходит именно с позицией объектов* Вы знаете размер круга и квадрата, подумайте как можно определить более точную точку столкновения объектов и кромки экрана.
в главе 6, дают возможность
Высокоуровневые классы, изученные в главе 6, дают возможность создавать пользовательский интерфейс приложения. По сути, эти классы выполнены в виде шаблонов, используя которые вы мoжете создавать списки, формы, шрифт, группы элементов, бегущие строки. Но использование таких классов-шаблонов несколько упрощает интерфейс программы, лишая возможности использования графики в программах на Java 2 ME. Иногда в приложении необходимо нарисовать таблицу, линию, квадрат, то есть воспользоваться графикой для создания насыщенной и красочной программы. Для этих целей в платформе Java 2 ME существуют так называемые классы низкоуровневого интерфейса - это классы Canvas и GameCanvas (класс GameCanvas будет рассмотрен в следующей главе), а так же класс Graphics, с помощью которого осуществляется непосредственная прорисовка графики на экране телефона. В самом начале этой главы дается характеристика классам Canvas и Graphics, попутно рассматриваются имеющиеся в составе обоих классов методы, после чего мы приступим к практической части и создадим ряд примеров, иллюстрирующих работу обоих классов.
Вывод текста
Для вывода текста на экран телефона можно воспользоваться методами drawstring () и drawChar (), рисующими соответственно строка-текста и любой назначенный символ. Текст можно выводить с любым цветом, а также использовать стили начертания, изученные в главе 6. Прототип метода drawstring () выглядит следующим образом:
public void drawstring (String str, int x, int y, int anchor)
Параметры метода drawstring ():
str - строка текста; x и у - задают размер невидимого прямоугольника, в котором происходит расположение текста; anchor - в этом параметре задается выбор позиции текста внутри невидимого прямоугольника. Здесь используются константы класса Graphics, рассмотренные в разделе 7.2.
В листинге 7.4 показан пример вывода текста на экран телефона. Код довольно прост и я думаю, вам не составит труда разобраться в нем самостоятельно.
/** Листинг 7.4 Класс Main и класс Text */ import javax.microedition.Icdui.*; import javax.microedition.midlet.*; public class Main extends MIDlet implements CommandListener { // команда выхода из программы private Command exitMidlet = new Command(«Выход», Command.EXIT, 0); public void startApp() { // создаем объект класса Text Text mytext = new Text(); // добавляем команду выхода mytext.addCommand(exitMidlet); mytext.setCommandListener(this); Display.getDisplay(this).setCurrent(mytext); } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void сommandAction(Command c, Displayable d) { if (c = = exitMidlet) { destroyApp(false) ; notifyDestroyedf); } } } /** класс Text определен в файле Text.Java рисует текст */ import javax.microedition.lcdui.*; public class Text extends Canvas { // конструктор public Text(){super(); } public void paint(Graphics g) { // устанавливается цвет g.setColor(10, 80, 200); // рисуем строку текста g.drawstring(«Java 2 Micro Edition», 80, 40, Graphics.TOP | Graphics.HCENTER); } }
Анимация в игровом процессе
Анимация в игровом процессе строится на основе последовательной цепочки рисунков. Как вы уже знаете, отдельно взятый рисунок из анимационной последовательности в Java 2 ME называется фреймом. Для того чтобы осуществить плавную анимацию в игре, необходимо выполнить ряд сменяющих друг друга рисунков. Посмотрите на рис. 8.3, где изображен матрос с флажками.
Рис. 8.3. Анимационная последовательность
На рис. 8.3 все фреймы выполнены в виде горизонтальной цепочки, но это не обязательное условие, можно расположить фреймы любым удобным образом. Не забывайте о том, что отсчет начинается с нуля и идет слева направо и сверху вниз.
Анимация повсеместно используется в компьютерных и мобильных играх. Вы наверно замечали, что при перемещении в игре персонажа, он производит ряд повторяющихся движений, создавая видимость игрового цикла. Такие элементарные движения следствие перехода" по имеющимся фреймам исходного изображения. Для этих целей в Java 2 ME имеются специальные методы класса Sprite.
Метод nextFrame () позволяет осуществить переход по всем имеющимся фреймам исходного изображения. Как только достигается последний фрейм, то автоматически происходит переход к первому фрейму с последующим переходом по всей имеющейся последовательности, что обеспечивает цикличность анимации.
Давайте рассмотрим пример использующий анимационную последовательность, изображенную на рис. 8.3. В этом примере на экран выводится изображение матроса и осуществляется цикличный переход По всем имеющимся фреймам, создавая эффект движения матроса, который с помощью семафорной азбуки передает слово «анимация».
В листинге 8.3, а так же на компакт-диске в папке \Code\Listing8_3\src дается код примера иллюстрирующего работу анимационной последовательности.
/** Листинг 8.3 класс MainGame */ import javax.microedition.lcdui.*; import javax.microedition.midlet. * ; public class MainGame extends MIDlet implements CornmandListener { // команда выхода private Command exitMidlet = new Command(«Выход», Command.EXIT, 0); // объект класса MyGameCanvas private MyGameCanvas mr; public void startApp() { // обрабатываем исключительную ситуацию try{ // инициализируем объект класса MyGameCanvas mr = new MyGameCanvas(); // запускаем поток mr .start(); // добавляем команду выхода mr.addCommand(exitMidlet); mr:setCommandListener{this); /7 отражаем текущий дисплей Display.getDisplay(this).setCurrent(mr); }catch (Java.io.lOException zxz) {}; } public void pauseApp() {} public void destroyApp(boolean unconditional) { // останавливаем потоку if(mr != null) mr.stop(); } public void commandAction(Command c, Displayable d) { if (с == exitMidlet) { destroyApp(false); notifyDestroyedO ; } } } /** файл MyGameCanvas.java класс MyGameCanvas */ import java.io. import javax.microedition.Icdui.*; import javax.microedition.Icdui.game.*; public class MyGameCanvas extends GameCanvas implements Runnable { // создаем объект класса MySprite private Matros matros; // создаем объект класса LayerManager private LayerManager lm; // логическая переменная boolean z; public MyGameCanvas() throws IOException { // обращаемся к конструктору суперкласса Canvas super(true); // загружаем изображение Image im = Image.createlmage(«/matros.png»); // инициализируем объект matros matros = new Matros(im, 94, 100); // выбираем позицию matros.setPosition(30, 30); // инициализируем менеджер уровней 1m = new LayerManager(); // добавляем объект матроса к уровню lm.append(matros); } public void start() { { z= true; // создаем и згшускаем поток Thread t = new Thread(this); t.start () ; } // останавливаем поток public void stop() { z = false; } public void run() { // получаем графический контекст Graphics g = getGraphics(); while (z) { // рисуем графические элементы init(g) // останавливаем цикл try { Thread.sleep(250); } catch (Java.lang.InterruptedException zxz) {}; } } private void init(Graphics g) { // белый цвет фона g.setColor(0xffffff); // перерисовываем экран g.fillRect(0, 0, getWidth(),getHeight()); // рисуем уровень в точке 0,0 lm.paint(g, 0 , 0) ; // рисуем анимацию matros.Animation(); // двойная буферизация iluGhGraphics(); } } /* * файл Matros.Java класс Matros * / import- javax .rnicroedition. Icdui .* ; import javax.microedition.lcdui.game.*; public class Matros extends Sprite { // конструктор public Matros(Image image, int fw, int fh) { // обращаемся к конструктору суперкласса super(image, fw, fh) ; } // метод осуществляющий анимацию public void Animation!) { // вызываем следующий фрейм nextFrame(); } }
В листинге 8.3 нам интересно несколько моментов. В классе Matros, являющимся подклассом класса Sprite, создается метод Animation (), который выглядит следующим образом:
public void Animation() { nextFrame(); }
Метод Animation () осуществляет тот самый последовательный переход по имеющимся фреймам исходного изображения. В классе MyGameCanvas происходит создание объекта класса Matros:
private Matros matros;
Затем в конструкторе класса MyGameCanvas загружается изображение матроса и инициализируется объект matros.
Image im = Image.createlmage(«/matros.png»); matros = new Matros(im, 94, 100);
Размер одного фрейма с матросом равен 94x100 пикселей, поэтому указывается размер именно одного фрейма. По умолчанию загружается самый первый фрейм изображения, но можно использовать метод setFrame () для установки необходимого фрейма из анимационной последовательности. В методе Graphics () класса MyGameCanvas происходит вызов метода Animation ():
matros.Animation();
Это в итоге приводит к цикличному перебору всех имеющихся фреймов. Откомпилируйте код из листинга 8.3 и посмотрите работу этого примера. На экране телефона матрос с помощью семафорной азбуки передает слово «анимация».
Техника создания игр
В этой главе... 8.1. Класс GameCanvas 8.2. Класс Layer 8.3. Класс TiledLayer 8.4. Класс LayerManager 8.5. Класс Sprite 8.6. Создание фонового изображения 8.7. Обработка событий с клавиш телефона 8.8. Анимация в игровом процессе 8.9. Столкновение объектов
Класс GameCanvas
Абстрактный класс GameCanvas составляет основу интерфейса всей создаваемой игры. Этот класс отвечает за прорисовку экрана, улучшая механизм работы игрового цикла и не используя при этом входные системные потоки. Весь игровой процесс может быть сосредоточен в одном цикле метода run () интерфейса Runnable.
Игровой цикл, используемый классом GameCanvas, отличается от аналогичного цикла применяемого классом Canvas. Типичная проблема игрового цикла класса Canvas заключается в том, что он разбит на несколько потоков. Вывод графики происходит в одном методе, обработка событий с клавиш — в другом, а изменение состояния игрового процесса в третьем. Такая модель работы может давать сбои, поэтому в классе GameCanvas была придумана более изящная конструкция игрового цикла, располагающаяся в одном единственном цикле.
public void run() { Graphics g = getGraphics(); while(true) { // метод, обрабатывающий нажатия клавиш с телефона inputKey(); // метод, прорисовывающий графику GameGraphics(); // копирует графические элементы на экран из внеэкранного буфера flushGraphics(); }
Система прорисовки игровой графики построена на использовании двойной буферизации, без .которой невозможно создать ни одной хорошей компьютерной игры. Смысл механизма двойной буферизации заключается в использовании внеэкранного буфера. Вся игровая графика рисуется во вторичном или внеэкранном буфере и только после полного цикла отрисовки, копируется непосредственно на экран с помощью метода flushGraphics (). При этом улучшается качество самой графики и, что самое главное, до тех пор, пока игровой процесс не обновится, обработка событий с клавиш телефона является недоступной.
Сама же обработка событий с клавиш упрощена с помощью метода getKeyState (). Вы просто определяете нажатую клавишу пописываете соответствующие игровые действия для нее. Изменены некоторые названия констант для клавиш телефона. Обработка событий с клавиш телефона происходит с помощью следующих констант:
static int DOWN_PRESSED - движение.вниз; static int FIRE_PRESSED - реализует стрельбу из оружия; static int GAME_A_PRESSED - игровая клавиша A; static int GAME_B_PRESSED - игровая клавиша В; static int GAME_C_PRES.SED - игровая клавиша С; static int GAME_D_PRESSED - игровая клавиша D; static int LEFT_PRESSED - движение влево; static int RIGHT_PRESSED-движение вправо;. static int UP_PRESSED - движение вверх.
Использование констант значительно упрощает работу с объектом, а с помощью метода getKeyStates () класса GameCanvas, можно определить, какая именно клавиша нажата в данный момент. Проанализируем методы класса GameCanvas:
void f lushGraphics () - копирует изображение из внеэкранного буфера на экран; void flushGraphics(int x, int y, int width, int height) -копирует изображение из внеэкранного буфера на экран в заданный по размеру прямоугольник; protected Graphics getGraphics () - получает графические элементы для представления их в последствии классом GameCanvas; int getKeyStates ()-определяет какая из клавиш нажата; void paint (Graphics g) - рисует графические элементы, представленные классом GameCanvas.
Класс GameCanvas создает основной цикл игрового процесса в одном потоке, наблюдает за событиями, получаемыми с клавиш телефона на основе которых производит постоянное обновление экрана.
Класс Layer
Абстрактный класс Layer задает основные свойства для всех созданных уровней игры.
Класс Layer имеет два подкласса TiledLayer и Sprite. При создании любых других подклассов класса Layer необходимо реализовать метод paint () в этих классах, чтобы иметь возможность рисовать созданные уровни на экране телефона, представляемом классом Graphics. С помощью методов класса Layer можно задавать размер и позицию уровня.
int getHeight() - получает высоту экрана; int getwidth() - получает ширину экрана; int getX () - получает горизонтальную координату уровня; int getY () - получает вертикальную координату уровня; void move (int dx, int dy) — перемещает уровень на dx и dy координаты; abstract void paint (Graphics g) - рисует уровень; void setPosition (int x, int у) - устанавливает уровень в позицию, обозначенную в координатах х и у.
Класс LayerManager
Менеджер уровней представлен классом LayerManager. Это класс осуществляет представление любого количества уровней на игровом поле. Для создания объекта нужно воспользоваться конструктором класса LayerManager.
LayerManager () - создает уровень.
А чтобы добавить уровни в игру необходимо использовать следующие методы:
void append (Layer 1) - добавляет уровень в менеджер уровнен; Layer getLayerAt (int index) - получает уровень с заданным индексом; int getsize ()-получает общее количество уровней; void insert (Layer 1, int index) - подключает новый уровень в заданный индекс; void paint (Graphics g, int x, int у)- представляет текущий менеджер уровней в заданных координатах; void remove (Layer 1) - удаляет уровень из менеджера уровней.
Предположим, у вас имеется четыре уровня: фон, игрок, препятствия и артефакты. Для того чтобы связать все четыре уровня воедино, создается объект класса LayerManager и вызывается метод append (). Например:
LayerManager im = new LayerManager(); im.append(fon); im.append(igrok); im.append(prep); . im. append (artf.) ;
И все! Четыре перечисленных уровня отражается на игровом поле.
Класс Sprite
Механизм работы с объектом класса Sprite идентичен модели работы с классом TiledLayer. Но если класс TiledLayer в основном отвечает за фоновое изображение, то с помощью класса Sprite рисуются на экране основные анимированные герои, космические корабли, машины, люди, звери, артефакты и так далее. Изображение, загружаемое в игру, может быть выполнено в виде анимационной последовательности. Количество рисунков в анимационной последовательности неограниченно, а отсчет происходит от нуля. Располагаться рисунки могут как в виде столбца, так и в виде колонки, в зависимости от ваших предпочтений. Каждый рисунок анимационной последовательности называется фреймом. Фрейм может быть любого размера по ширине и высоте, но все фреймы должны быть одинаковых размеров. Размер фрейма должен быть известен, потому что он используется при создании объекта класса Sprite. Есть три конструктора класса Sprite каждый из которых можно использовать при создании объекта класса Sprite.
Sprite (Image image) - создает не анимированный спрайт; Sprite(Image image, int frameWidth, int frameHeight)—создает анимационный спрайт, взятый из заданного фрейма; Sprite (Sprites)- создает спрайт из другого спрайта.
Для перехода по фреймам исходного изображения, а также определения столкновения между объектами используются методы класса Sprite.
boolean collidesWith(Sprite s, boolean pixelLevel) — определяет столкновение между спрайтами; boolean collidesWith (TiledLayer t, boolean. pixelLevel) - определяет столкновение между спрайтом и препятствием, нарисованным при помощи класса TiledLayer; int getFrame () - получает текущий фрейм; void next Frame ()- осуществляет переход наследующий фрейм; void paint (Graphics g) - рисует спрайт; void prevFrame () - осуществляет переход на предыдущий фрейм; void setFrame(int sequencelndex) - устанавливает заданный фрейм; void setFrameSequence (int [ ] sequence) - устанавливает определенную фреймовую последовательность; void set Image(Image img, int frameWidth, int frameHeight)-изменяет изображение спрайта на новое изображение; void setTransform( int transform) - производит трансформацию спрайта. public void defineReferencePixel (int x, int у)- изменяет опорную позицию спрайта, перенося ее в точку с координатами х и у.
Метод defineReferencePixel () изменяет опорную позицию спрайта, но для чего это нужно? Опорная позиция для спрайта задается левым верхним углом, но не всегда это удобно, поэтому опорную позицию можно перенести, в центр спрайта. Например, если спрайт сделан в виде машины, то при вращении вокруг своей оси, если опорная позиция перенесена в центр, вращение будет правдоподобным. Но если не переопределить позицию, то вращение произойдет в точке левого верхнего угла спрайта и выглядеть это будет не вполне естественно, как будто у машины пробито одно из колес. Для этого вызывается метод def ineReferencePixel () с заданными координатами и переопределяет опорную позицию, например в центр спрайта:
defineReferencePixel(frameWidth / 2, frameHeight / 2);
Метод setTransform() производит трансформацию спрайта, вращая или зеркально отображая спрайт вокруг своей оси с помощью следующих констант:
static1int TRANS_MIRROR; static int TRANS_MIRROR_ROT180; static int TRANS_MIRROR_ROT270; static int TRANS_MIRROR_ROT90; static int TRANS_NONE; static int TRANS_ROT180; static int TRANS_ROT270; static int TRANS_ROT90.
Посмотрите на рис. 8.2, где очень привлекательно показаны константы для различных состояний спрайта.
Рассмотрев классы GameCanvas, Layer, Sprite, TiledLayer и LayerManager, перейдем к практическим занятиям этой главы.
Класс TiledLayer
С помощью класса TiledLayer создается фон игровой сцены. Фоновое изображение выполняется в виде одинаковых по размеру ячеек как показано на рис. 8.1.
Рис 8.1. Ячейки фонового изображения
Количество и расположение ячеек может варьироваться как угодно, но нумерация ячеек следует от единицы, слева направо и сверху вниз. Построение сцены происходит путем загрузки исходного изображения, разбитого на ячейки и указания индекса необходимой ячейки на игровом поле. Но прежде нужно создать объект класса TiledLayer с помощью конструктора, прототип которого выглядит следующим образом:
TiledLayer(int columns, int rows, Image image, int tileWidth, int tileHeight)
columns-количество столбцов; rows - количество строк; image - исходное изображение; tileWidth - размер ширины ячейки в пикселях; tileHeigh - размер высоты ячейки в пикселях.
Размеры одной ячейки по ширине и высоте могут быть разными, но все ячейки исходного изображения должны быть одинаковыми по размеру. В качестве примера, возьмем за основу изображение на рис. 8.1 с шестью ячейками и предположим, что размер одной ячейки по ширине равен 10 пикселям, а по высоте 15 пикселям, тогда загрузка изображения и создание объекта TiledLayer может выглядеть следующим образом:
Image im = Image.createlmage("/fon.png"); TiledLayer tl =new TiledLayer(3, 2, im, 10, 15);
Загрузив изображение и создав объект класса TiledLayer, вы можете приступить к разметке фона па игровом ноле. Допустим, каждая из перечисленных по номеру ячеек обладает следующими характеристиками:
1) камни;
2) трава;
3) вода;
4) песок;
5) воздух;
6) заграждение.
А игровое поле разбито на пятнадцать столбцов и десять строк, тогда создав массив данных, очень легко выполнить разметку всего поля, например таким образом:
int[] pole = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 5, 5, 5, 1, 1, .5, 1, 1, 5, 5, 5, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 1, 1, 1,1, 1, 1, 4, 4, 4, 4, 6, 6, 6, 6, 6, 4, 4, 4,4,4, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 3, 3/3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 3, 3, 3, 3,,3, 3, 3, 3, 3, }
Затем весь имеющийся массив данных считывается с помощью цикла и рисуется методом paint () на экране телефона.
Познакомимся с методами класса TiledLayer:
int createAnimatedTilefint staticTilelndex) - создает анимационный фон и возвращает следующий индекс ячейки; int getCellHeight () - получает высоту ячейки в пикселях; int getCellWidth() - получает ширину ячейки в пикселях; int getColumns () - получает количество колонок, на которое разбито изображение фона; int getRows () - получает количество строк, на которое разбито изображение фона; void paint (Graphics g) - рисует фон; void setCellfint col, int row, int tilelndex) - рисует заданную ячейку.
Обработка событий с клавиш телефона
В профиле MIDP 2.0 предусмотрена улучшенная обработка событий получаемых с клавиш телефона. Используя метод getKeyState () можно определять состояние клавиш телефона и действовать адекватным образом. 'В демонстрационном примере к этому разделу мы выведем на экран мячик, созданный при помощи класса MySprite являющимся подклассом класса Sprite. В листинге 8.2 представлен код примера, в котором на экране рисуется синий мяч, а с помощью клавиш телефона Up, Down, Left и Right этот мяч можно передвигать по экрану. Листинг 8.2 состоит из трех классов: MainGame. MyGameCanvas и MySprite, расположенных в трех разных файлах MainGame.java, MyGameCanvas.java и MySprite.java.
/** Листинг 8.2 класс MainGame */ import javax.microedition.Icdui.*; import javax.microedition.midlet.*; public class MainGame extends MIDlet implements CommandListener { // команда выхода private Command exitMidlet = new Command("Выход", Command.EXIT, 0); // объект класса MyGameCanvas private MyGameCanvas mr; public void startApp() { // обрабатываем исключительную ситуацию try { // инициализируем объект класса MyGameCanvas mr = new MyGameCanvas(); // запускаем поток mr.start(); // добавляем команду выхода mr.addCommand(exitMidlet); mr.setCommandLis'tener (this) ; // отражаем текущий дисплей Display.getDisplay(this).setCurrent(mr); } catch (Java.io.lOException zxz) {} ; } public void pauseApp() {} public void destroyApp(boolean unconditional) { // останавливаем поток if(mr != null) mr.stopt); } public void commandAction (Command c, Displayable d;) { if (с == exitMidlet) { destroyApp(false); notifyDestroyed(); } } } /** файл MyGameCanvas.Java класс MyGameCanvas */ import java.io.*; import javax.microedition.Icdui.*; import javax.microedition. Icdui .game .*; public class MyGameCanvas extends GameCanvas implements Runnable { // создаем объект класса MySprite private MySprite bol; // создаем объект класса Lay'erManager private LayerManager lm; // логическая переменная boolean z; public MyGameCanvas() throws IOException { // обращаемся к конструктору суперкласса Canvas super(true); // загружаем изображение Image im = Image.createlmage("/bol.png"); // инициализируем объект bol bol = new MySprite (itn, 23, 23); // выбираем позицию в центре экрана bol.setPosition(getWidth()/2, getHeight()/2); // инициализируем менеджер уровней 1m = new LayerManager(); // добавляем объект bol к уровню lm.append(bol); } public void start() { z = true; // создаем и запускаем поток Thread t = new Thread(this); t.start(); } // останавливаем поток public void stop() { z = false; } public void run() { // получаем графический контекст Graphic's g = getGraphics () ; while (z) { // обрабатываем события с клавиш телефона inputKey(); // рисуем графические элементы init(g); // останавливаем цикл на 20 миллисекунд try { Thread.sleep(20); } catch (Java.lang.InterruptedException zxz) {}; } } private void inputKey() { // определяем нажатую клавишу int keyStates = getKeyStates(); // код обработки для левой нажатой клавиши -if ((keyStates & LEFT_PRESSED) != 0) bol.moveLeft(); // код обработки для правой нажатой клавиши if ((keyStates & RIGHT_PRESSED) != 0) bol.moveRight(); // код обработки для клавиши вверх if ((keyStates & UP_PRESSED) != 0) bol.moveUp(); // код обработки для клавиши вниз if ((keyStates & DOWN_PRESSED) != 0) bol.moveDown(); } private void init(Graphics g) { // белый цвет фона g.setColor(0xffffff); // перерисовываем экран g.fillRect (0 , 0, getWi.dth () , getHeight()); // рисем уровень в точке 0,0 lm.paint(g, 0, 0) ; // двойная буферезация flushGraphics(); }; } /** файл MySprite.Java класс MySprite */ import javax.microedition.Icdui.*; import javax.microedition.Icdui.game.*; public class MySprite extends Sprite { // конструктор public MySprite(Image image, int fw, int fh) { // обращаемся к конструктору суперкласса super (image, fw, fh); } // метод для клавиши Left public void moveLeft() ; { // передвигаем спрайт move(-1,0); } // метод для клавиши Right public void moveRight() { / / передвигаем спрайт move(1,0); } // метод для клавиши Up public void moveUp() { // передвигаем спрайт move(0,-1); } // метод для клавиши Down public void moveDown() { // передвигаем спрайт move(0,1); '} }
В файле MySprite.java находится класс MySprite, с которого и начнем рассмотрение листинга. Конструктор класса MySprite обращается к конструктору своего суперкласса Sprite!
super(image, fw, fh);
Этот конструктор имеет три параметра - это исходное изображение спрайта, ширина и высота фрейма. Спрайт, как вы помните, может иметь анимационную последовательность, поэтому необходимо точно знать ширину и высоту одного фрейма, при обязательном условии, что все фреймы спрайта должны быть одного размера. В этом примере мы не используем анимационной последовательности, и можно было обойтись и более простым конструктором суперкласса Sprite.
Для передвижения спрайта по экрану телефона созданы методы moveLef t (), moveRight (), moveUp () и moveDowri (), в основу которых положены вызовы метода move (). Метод move () имеет два параметра - это координаты по осям X и Y. Задавая необходимое смещение на 1, 2, 3 и более пикселей по одной из осей, вы будете производить движение объекта по экрану.
Класс MyGameCanvas работает по схеме, использованной в разделе 8.6, с той лишь разницей, что вместо фонового изображения загружается спрайт в виде мячика. В конструкторе класса MyGameCanvas происходит загрузка исходного изображения bol.png, это и есть наш мячик или спрайт. Спрайт размером 23x23 пикселя. При создании объекта bol класса MySprite
bol = new MySprite(im, 23, 23)
используется изображение bol.png, затем, в конструкторе класса MyGameCanvas происходит выбор позиции для первоначальной отрисовки спрайта на экране с помощью метода setPosition (). Мячик рисуется в центре экрана и добавляется методом append () к уровню.
В методе run () в игровом цикле происходит вызов двух методов. Метод init () производит рисование всех графических элементов с помощью менеджера уровней, а метод input Key () осуществляет обработку нажатий клавиш телефона.
private void inputKey() { int keyStates = getKeyStates(); if ((keyStates & LEFT_PRESSED) != 0) bol.moveLeft(); if ((keyStates & RIGHT_PRESSED) !=0) bol.moveRight(); if ((keyStates & UP_PRESSED) != 0) bol.moveUp(); if ((keyStates & DOWN_PRESSED) != 0) bol.moveDown (); }
В методе inputKey () происходит определение нажатой клавиши посредством метода getKeyState (). Весь остальной код в методе inputKey () использует оператор if для обработки нажатых клавши, вызывая соответствующие методы moveLeftO, moveRight (), moveUp ()или moveDown () для перемещения объекта по экрану.
В классе MainGame из файла MainGame.java создается объект класса MyGameCanvas, запускается системный поток и отражается текущий экран.
В этом примере использовался спрайт файла ресурса bol.png состоящий из одного фрейма размером 23x23 пикселя. В следующем разделе мы рассмотрим технику анимации спрайтов в играх, и будем исполВзовать спрайт, состоящий уже из нескольких фреймов.
Создание фонового изображения
С помощью класса TiledLayer можно создавать любое количество уровней, накладывая их друг на друга, а с помощью менеджера уровней, представленного классом LayerManager, отслеживать все имеющиеся уровни. В качестве примера будет создан фон на основе элементов разметки игрового поля. Фоновое изображение загружается из файла fon.png. Само изображение выполнено в виде шести ячеек размером 15x15 пикселей.
В листинге 8.1 находится код примера создающего фоновое изображение. Ознакомьтесь с листингом, а потом мы перейдем к анализу этого примера.
Рис. 8.2. Константы трансформации
/ * * Листинг 8.1 класс MainGame */ import javax.microedition.lcdui.* ; import javax.microedition.midlet.*; public class MainGame extends MIDlet implements CommandListener { //команда выхода private Command exitMidlet = new Command!"Выход", Command.EXIT, 0) ; // объект класса MyGameCanvas private MyGameCanvas mr; public void startApp.{} { // обрабатываем исключительную ситуацию try{ // инициализируем объект класса MyGameCanvas; mr = new MyGameCanvas() ; // запускаем поток mr.start(); // добавляем команду выхода mr.addCommand(exitMidlet); mr.setCommandListener(this) ; // отражаем текущий дисплей Display .getDisplay (this) . setCurrent (mr-) ; } catch (Java . io. ID-Exception zxz) {} ; } public void pauseApp() {} public void destroyApp(boolean unconditional) { // останавливаем поток if(mr != null) mr.stop'O; } public void coramandAction(Command c, Displayable d) { if (c == exitMidlet) { destroyApp(false) ; notifyDestroyed() ; } } } / * * Файл MyGameCanvas.Java класс MyGameCanvas */ import Java.io.IOException; import javax.microedition.Icdui.*; import javax.microedition Nlcdui.game.* ; public class MyGameCanvas extends GameCanvas implements Runnable- { // создаем объект класса TiledLayer private TiledLayer fonPole; // создаем объект класса LayerManager private LayerManager im; // логическая переменная для выхода из цикла boolean z; public MyGameCanvas(), throws IOException { // обращаемся к конструктору суперклассаNCanvas super(true); // инициализируем fonPole fonPole = Fon () ; // создаем менеджер уровней 1m = new LayerManager(); // добавляем объект fonPole к уровню im.append (fonPole); } public void start() { z = true; // создаем и запускаем поток Thread t = new Thread(this); t.start(); } //* метод, создающий объект класса TiledLayer и загружающий фоновое изображение */ public TiledLayer Fon()throws IOException { // загрузка изображения из файла ресурса Image im = Image.createlmage("/fon.png"); // создаем объект класса TiledLayer fonPole = new TiledLayer(/*столбцы*/10,/*строки*/10, /*изображение*/lm,/*ширина*/15,/*высота*/15); // разметка игрового поля int[] pole = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 5, 5, 5, 1, 1, 5, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 2, 4, 4, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 4, 4, 4, 3, 3, 3, 3 }; // цикл, считывающий разметку поля for(int i = 0; i < pole.length; i + + ) { /* присваиваем каждому элементу игрового поля определенную ячейку изображения im*/ fonPole.setCell(i % 10, i / 10, pole[i]); } return fonPole; } public void stop(){ z = false; } public void run() { // получаем графический контекст Graphics g = getGraphics(); while (z) { // рисуем графические элементы init(g) ; // останавливаем цикл на 20 миллисекунд try { Thread.sleep(20); } catch (Java.lang.InterruptedExceptlori zx.z) {}; } } private void init(Graphics g) { // белый цвет фона для перерисовки экрана g.setColor(0xffffff); // размер перерисовки экрана g.fillRect(0, 0, getWidth(), getHeight()); // рисуем уровень с левого верхнего угла дисплея 1m.paint(g, 0, 0) ; // двойная буферизация flushGraphics(); } }
Листинг 8.1 состоит из двух классов MainCanvas и MyGameCanvas, находящихся в файлах MainCanvas.java и MyGameCanvas.java. Анализ листинга начнем с класса MyGameCanvas. В первых строках кода этого класса объявляются два объекта классов TiledLayer и LayerManager, атак же логическая переменная z.
private TiledLayer fonPole; private LayerManager lm, boolean z;
Объект fonPole класса TiledLayer будет отвечать за фоновое изображение. Объект im класса LayerManager является менеджером уровней. Логическая переменная z необходима для прерывания цикла в методе run () и для окончания системного потока, в котором происходит работа всего игрового цикла.
В конструкторе MyGameCanvas происходит инициализация объекта fonPole класса TiledLayer и объект im класса LayerManager. Инициализированный объект fonPole добавляется менеджером уровней к текущему уровню для представления на экране телефона. Обратите внимание, объект fonPole инициализируется с помощью метода Fon ().
Image im = Image.createlmage("/fon.png"); fonPole= new TiledLayer(/*столб*/10,/*строки*/10,im, /*ширина*/15,/*высота*/15);
В этих двух строках происходит загрузка исходного изображения из файла ресурса и создание объекта fonPole с помощью конструктора класса TiledLayer.
Вся разметка игрового поля выполнена в виде десяти строк и десяти столбцов. Первые два параметра конструктора класса TiledLayer как раз и отвечают за количество столбцов и строк. Третий параметр конструктора - это исходное изображение, выполненное в виде шести ячеек, каждая размером 15x15 пикселей. Два последних параметра конструктора класса TiledLayer определяют ширину и высоту ячейки. При создании объекта класса TiledLayer необходимо быть внимательным и указывать реальные размеры одной ячейки. Если размер одной ячейки будет, предположим 20x20 пикселей, а вы обозначите как 15x15 пикселей, то в итоге ячейки изображения загружены не будут.
Дальше в методе Fon () происходит разметка игрового поля выполненного в виде десяти столбцов и десяти строк.
int[] pole = { 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 5, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 1, 1, 5, 5, 5, 1, 1, 5, 1, 1, 1, 1, 1, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6, 2, 4, 4, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 4, 4, 4, 3, 3, 3, 3, 2, 2, 2, 4, 4, 4, 3, 3, 3, 3, };
Все строки и столбцы состоят из элементов. Отсчет ячеек происходит от единицы и выше. Присвоение номера ячейки исходного изображения одному из элементов и организует разметку игрового поля, которое в последствии будет рисоваться на экране. Единственное О5чем нельзя забывать - это о размере дисплеев реальных телефонов. Если вы имеете десять столбцов и размер каждой ячейки 15 пикселей по ширине, то в итоге ваше игровое поле в ширину будет 10x15 = - 150 пикселей. Не каждый телефон может похвастаться таким разрешением, поэтому при создании игрового поля нужно учитывать эти нюансы. Вслед за разметкой игрового поля в методе Fon () происходит считывание всех элементов с помощью цикла for.
forfint i = 0; i < pole.length; i++) { fonPole.setCell(i % 10, i / 10, pole[i]); }
Присвоение номера ячейки определенному элементу происходит при помощи метода setCell (). В этом методе первый параметр-номер столбца, второй - номер строки и последний - номер ячейки изображения. Здесь главное не запутаться, потому что номера столбцов и строк начинаются с нуля из-за того, что это обычный массив данных, а все массивы, как вы знаете, ведут свой отсчет с нуля, тогда как ячейка исходного изображения начинается с единицы. Сразу возникает вопрос, а почему не произвести отсчет тоже с нуля, чтобы не было путаницы? Дело в том, что отсчет и производится с нуля, но число ноль - это своего рода зарезервированное значение для ячеек изображения. Нулевое значение может использоваться, но оно не. загружает ничего, поэтому отсчет ячеек ведется с единицы. С методом Fon () мы разобрались, перейдем к методу init ().
g.setColor(0xffffff); g.fillRect(0, 0, getWidth0, getHeight());
В этих строках кода происходит постоянная перерисовка фона экрана. Эти действия вы производили в главе 7, когда разбирали механизм отсечения.
С помощью метода paint () рисуется уровень. Начало точки вывода уровня задано точкой 0,0, что соответствует началу, системы координат.
И последний метод flushGraphics () осуществляет двойную буферизацию, копируя графические элементы из внеэкранного буфера на экран.
В методе run () происходит остановка игрового цикла. Перед тем как цикл создается с помощью оператора while, методом getGraphics () происходит получение графического контекста, что и является одним из достоинств механизма игрового цикла в профиле MIDP 2.0.
В файле MainGame.java создается основной класс мидлета MainGame. В методе startApp() производится создание объекта рассмотренного класса MyGameCanvas, добавляется команда выхода, запускается системный поток и отражается текущий дисплей. В методе destroyApp () происходит остановка потока методом stop () класса MyGameCanvas.
Столкновение объектов
Практически во всех играх приходится обрабатывать события связанные со столкновением двух объектов или с препятствием. В профиле MIDP 2.0 существует три отличных метода, отслеживающих факт столкновения. Все три метода определены в классе Sprite.
collidesWith(Image image, int x, int y, Boolean pixelLevel) - определяет факт столкновения со статическим изображением; collidesWith (Sprite s, Boolean pixelLevel) - определяет столкновение между двумя объектами класса Sprite; collidesWith(TiledLayer t. Boolean pixelLevel) -отслеживает столкновение между объектом класса Sprite и объектом класса TiledLayer.
Используя эти методы можно обрабатывать всевозможные ситуации, связанные со столкновением. Как правило, при столкновении должны произойти какие-то события, чаще всего связанные с изменением первоначального состояния объекта. Это может быть уничтожение объекта, его перемещение, уменьшение или увеличение. В основе изменения состояния объекта положен перебор имеющихся фреймов в анимационной последовательности или перерисовка изображения на новом месте. Для этих операций в классе Sprite имеется несколько методов. С одним из методов nextFrame (), вы уже познакомились в разделе 8.8, рассмотрим оставшиеся методы.
prevFrame () - с помощью этого метода происходит переход к предыдущим фреймам изображения. Этот метод подобен методу nextFrame (), только переход осуществляется в обратном порядке; setFrame() - производит установку при помощи индекса заданного фрейма из всей имеющейся последовательности фреймов. setFrameSequence () - устанавливает определенную фреймовую последовательность при помощи массива индексов в анимационной последовательности; getFrame () - узнает, какой из фреймов исходного изображения используется в текущий момент; set Image () - заменяет исходное изображение на новое. Можно использовать этот метод, например в том случае, если объект уничтожен, и его необходимо перерисовать заново.
Набора этих методов вполне достаточно для обработки различных ситуаций возникающих в игровом процессе. Однако необходимо очень тщательно разобраться в действии всех вышеперечисленных методов. Для этого был написан исходный код примера, иллюстрирующего работу методов nextFrame (), prevFrame (), setFrame () и setFrameSequence (). В этой программе на экран выводятся четыре бомбы и синий шарик, перемещая который в разные стороны можно произвести столкновение с четырьмя бомбами. Все бомбы являются объектами класса MySprite, являющимся подклассом класса Sprite. Метод, обрабатывающий столкновение мяча и бомбы, использует один из четырех методов nextFrame (), prevFrame.(), setFrame () и setFrameSequence () для каждой из бомб, что красочно иллюстрирует работу каждого метода. Исходные, изображения бомбы и мяча выполнены в виде последовательности четырех фреймов размером 23x23 пикселя. Первый фрейм мяча и бомбы является исходным изображением, а три последующих фрейма реализованы в виде взрыва. Переход по этим фреймам создает иллюзию взрыва мячика или бомбы. Но оттого, что для каждой бомбы как мы договорились, используются различные методы, то результат может оказаться неожиданным. Поэтому внимательно посмотрите на код примера в листинге 8.4 и запустите программу с помощью любого эмулятора, поддерживающего профиль MIDP 2.0. Либо откомпилируйте исходный код из листинга 8.4 и внимательно ознакомьтесь с работой этой программы. Я уверен, что вы без труда разберетесь, что именно нужно сделать для логического завершения взрывов бомб и мяча. Алгоритм действий очень прост и может быть следующим после столкновения мяча с одной из мин. Необходимо произвести взрыв, последовательно переходя по всем фреймам, после чего, например, нарисовать бомбу и мячик заново. В листинг 8.4 предложен исходный код примера.
/ * * листинг 8.4 класс MainGame */ import javax.microedition.Icdui.*; import javax.microedition.midlet.*; public class MainGame extends MIDlet implements CommandListener { // команда выхода private Command exitMidlet = new Command(«Выход»,Command.EXIT, 0); // объект класса MyGameCanvas private MyGameCanvas mr; public void startApp() { // обрабатываем исключительную ситуацию. try{ // инициализируем'объект класса MyGameCanvas mr = new MyGameCanvas(); // запускаем поток mr.start{}; // добавляем команду выхода mr.addCommand(exitMidlet) ; mr.setCommandListener(this); // отражаем текущий дисплей Display.getDisplay(this).setCurrent(mr) ; } catch (Java . io. lOException zxz) {} ; } public void pauseApp() {} public void destroyApp(boolean unconditional) { // останавливаем поток if(mr != null) mr.stopf); } public void commandAction(Command c, Displayable d) { if (c = = exitMidlet) { destroyApp (false); notifyDestroyed(); } } } } /** файл MyGameCanvas.Java класс MyGameCanvas */ import java.io.*; import javax.microedition.Icdui.*; import javax.microedition.Icdui.game.*; public class MyGameCanvas extends GameCanvas implements Runnable { // создаем объект класса MySprite private MySprite bol; // создаем объект класса LayerManager private LayerManager lm; // создаем бомбы private MySprite bombal, bomba2, bombaB, bomba4; // логическая переменная boolean z; public MyGameCanvas() throws lOException { // обращаемся к конструктору суперкласса Canvas super(true); // загружаем изображение мяча linage bollmage = Image.createlmage («/bol.png») // инйциалтзируем объект bol bol = new MySprite(bollmage, 23, 23); // выбираем позицию в центре экрана bol.setPosition(getWidth() /2, getHeight. () /2) ; // загрузка изображения бомбы Image bombalmage = Image.createlmage(«/bomba.png»); // инициализируем первую бомбу bombal = new MySprite(bombalmage, 23, 23); // выбираем позицию для первой бомбы bombal.setPosition(10, 10); // инициализируем вторую бомбу bomba2 = new MySprite(bombalmage, 23, 23); // выбираем позицию для второй бомбы bomba2 . setPosition( getwidth()-30, 10); // инициализируем третью-бомбу bоmbа3 = new MySprite(bombalmage, 23, 23); // выбираем позицию для третьей бомбы bоmbа3.setPosition(10, getHeight()-40); // инициализируем четвертую бомбу bomba4 = new MySprite(bombalmage, 23, 23); // выбираем позицию для четвертой бомбы bomba4.setPosition(getWidth()-30, getHeight 0-40); // инициализируем менеджер уровней lm = new LayerManager(); // добавляем мяч к уровню lm.append(bol); // добавляем бомбы к уровню lm.append(bombal); lm.append(bomba2); lm.append(ЬотЬаЗ); lm.append(bomba4); } // обрабатываем столкновение public void stolknovenie() { // столкновение с первой бомбой if (bol. collidesWith (bombal, true)) { bol.nextFrame(); bombal.nextFrame(); } // столкновение со второй бомбой if(bol.collidesWith(bomba2, true)) { bol.prevFrame(); bomba2.prevFrame(); } // столкновение с третьей бомбой if (bol .collidesWith(bоmbа3 , true)) { bol.setFrame (2) ; vbomba3.setFrame(0); } // столкновение с четвертой бомбой if(bol.collidesWith(bomba4, true)) { int[] i = {2,3}; bol.setFrame(0); bomba4 . setFrameSequence (i) ; } } public void start() { z = true; // создаем и запускаем поток Thread t = new Thread(this); t.start(); } // останавливаем поток public void stop() { z = false; } public void run() { // получаем графический контекст Graphics g = getGraphics(); while (z) { // столкновение с препятствием stolknovenie (); // обрабатываем события с клавиш телефона inputKey(); // рисуем графические элементы init(g); // останавливаем цикл на 20 миллисекунд try { Thread.sleep(20); } catch (Java.lang.InterruptedException zxz) {}; } } private void inputKey() { //-определяем нажатую клавишу int keyStates = getKeyStates(); // код обработки для левой нажатой клавиши if ((keyStates & LEFT_PRESSED) != 0) bol.moveLeft(); // код обработки для правой нажатой клавиши if ((keyStates & RIGHT_PRESSED) != 0) bol.moveRight(); // код обработки для клавиши вверх if ((keyStates & UP_PRESSED) != 0) bol.moveUp(); // код обработки для клавиши вниз if ({keyStates & DOWN_PRESSED) !=-0) bol.moveDown(); } private void init(Graphics g) { // желтый цвет фона g.setColor(0xffff00); // перерисовываем экран g.fillRect(0, 0, getWidth(), getHeight()); // рисем уровень в точке 0,0 lm.paint(g, 0, 0); // двойная буферизация flushGraphics(); } } /** файл MySprite.Java класс MySprite */import javax.microedition.Icdui.*; import javax.microedition.Icdui.game.*; public class MySprite extends Sprite // констоуктор oublic MySprite(Image image, int fw, int fh) // обращаемся к конструктору суперкласса super( image , fw, fh); }//метод для левой нажатой клавиши public void moveLeft() //передвигаем спрайт move(-1,0); //метод для правой нажатой клавиши publiс void moveRight() //передвигаем спрайт move(1,0); } // метод для клавиши вверх public void rnoveUp () // передвигаем спрайт // метод для клавиши вниз public void moveDown() //передвигаем спрайт move(0,1); } }
В листинге 8-4 содержатся три класса MainGame, MyGameCanvas и My Sprite. Основной код обработки столкновений бомб и мяна находится в классе Мус игпйСа n vas - этому классу мы и уделим особое внимание при разборе - листинга.
В конструкторе класса MyGameCanvas происходит загрузка изображения мяча из файла ресурса bol.png, инициализация объекта bol, класса MySprite и устанавливается позиция прорисовки на экране объекта bol.
ImagebolImage = Image.createlmage(«/bol.png») ; bol= new KySprioe(bollmage, 23, 23); bol.setPosition(getWidth()/2 ,getHeight()/2 ) ;
Заметьте, что позиция вывода мяча на экран установлена в центре экрана, но эта позиция определена для левой верхней точки мяча, поэтому именно левый верхний угол изображения спрайта мяча будет находиться в центре экрана. Для того чтобы нарисовать сам спрайт в центре экрана, нужно переопределить опорную позицию мяча с помощью метода def ineRef erencePixel ().
Затем в конструкторе класса MyGameCanvas загружается изображение бомбы.
Image bombalmage=Image.createlmage(«/bomba.png»);
После этого происходит инициализация четырех объектов bombal, bomba2, bomba3 и bomba4 класса MySprite и устанавливается позиция для вывода всех четырех бомб на экран телефона.
bombal = new MySprite(bombalmage, 23, 23); bombal.setPosition(10,10); bomba2 = new MySprite(bombalmage, 23, 23); bomba2.setPosition( getwidth()-30, 10); bomba3 = new MySprite(bombalmage, 23, 23); bonba3.setPosition(10, getHeight()-40); bomba4 = new MySprite(bombalmage, 23, 23);. bomba4.setPosition(getWidth()-30, getHeight()-40);
Все четыре бомбы рисуются в разных углах экрана слева направо и сверху вниз.
Произведя загрузку необходимых изображений и инициализируя все объекты класса MySprite, можно добавить их к уровню с помощью менеджера уровней.
lm.append(bol); lm.append(bombal); lm.append(bomba2); lm.append(bomba3); lm. append (bomba4);
Наша задача в этом примере - это определение столкновения бомб и мячика, для этого создан метод stolknovenie (), где при помощи конструкции if/ else происходит обработка столкновения объектов bol и bombalbomba4. Сейчас было бы очень хорошо, если бы вы могли запустить программу из листинга 8.4 и попробовали осуществить столкновение мяча с четырьмя бомбами. Как мы договорились, при столкновении будет обсуждаться работа четырех разных методов.
В столкновении с первой бомбой, находящейся в левом верхнем углу экрана, используется метод next Frame (). Если вы переместите мячик по экрану, то при наезде на первую бомбу произойдет взрыв бомбы и мяча. То есть начнется перебор всех имеющихся фреймов обоих объектов в цикличном порядке. Как только вы уберете мячик с бомбы, взрыв обоих объектов прекратится, потому что закончится перебор фреймов изображений. А состояние бомбы и мяча будет соответствовать одному из фреймов всей анимационной последовательности, при этом возможности повлиять на остановку- перехода по фреймам в методе nextFrame () нет.
Вторая бомба, находящаяся в правом верхнем углу экрана, при обработке столкновения использует практически идентичный предыдущему Метод prevFrame (), отличающийся лишь тем, что переход по всем существующим фреймам бомбы и мяча происходит в обратном порядке. Остановить работу метода на нужном фрейме также невозможно.
Третья бомба рисуется в нижнем углу экрана и для обработки столкновения мяча с бомбой используется метод setFrame (). Этот метод производит переход по заданным фреймам всей анимационной последовательности по номеру или индексу фрейма. В этом примере используется следующий код при столкновении мяча и третьей бомбы.
bol, setFrame (2); bomba3.setFrame(0);
Когда вы передвинете мячик на третью бомбу, то увидите, что изображение мячика изменится, и будет соответствовать третьему фрейму всей анимационной последовательности мячика. Состояние бомбы останется неизменным, потому что используется индекс 0, а это первый фрейм бомбы.
Четвертая бомба при столкновении задействует метод setFrameSequenсе (), благодаря которому можно использовать фреймовые последовательности в виде массива индексов.
Используя эти методы, я думаю, вы вполне сможете создать любую цепочку событий для перемещения, столкновения и других возможных действий объекта В следующей главе будет рассматриваться мультимедийная библиотека, необходимая для создания звукового сопровождения в играх.
Рынок мобильных телефонов развирается стремительными
Рынок мобильных телефонов развирается стремительными темпами. Все больше телефонов имеют поддержку технологии Java. Веянье игровой индустрии захватило и мобильные телефоны, поэтому платформа Java 2 ME позиционируется в большей степени как игровая платформа Для мобильных телефонов. При разработке игр под профиль MIDP 1.0 программист сталкивается с массой проблем в виде написания большого количества собственных классов для создания игрового процесса, рисование графики, уровней и так далее. В профиль MIDP 2.0 добавлено пять игровых классов, значительно упрощающих создание мобильных игр, это:
GameCanvas - абстрактный класс, содержащий основные элементы игрового интерфейса; Layer — абстрактный класс, отвечающий за уровни представляемые в игре; LayerManager - менеджер уровней; sprite - представляет на экране спрайтовое изображение; TiledLayer - отвечает за создание фоновых изображений.
Все вышеперечисленные классы доступны только в профиле MIDP 2.0. Также можно воспользоваться этими классами и в программировании телефонов Siemens под профиль MIDP 1.0, импортировав пакет com.siemens.mp.color_game. Компания Siemens входит в экспертную группу MIDP Expert Group и, по всей видимости, на базе уже имеющихся игровых классов от компании Siemens были созданы игровые классы для профиля MIDP 2.0.
При использовании игровых классов профиля MIDP 2.0, построение мобильной игры основано на системе уровней. Формируя игру, программист создает необходимое ему количество уровней. Каждый из уровней содержит набор графических элементов, например; можно создать уровень с фоновым изображением, уровень с препятствиями, уровень с игровыми бонусами, уровень с главным персонажем игры. После этого все имеющиеся уровни компонуются воедино, накладываются один уровень на другой и прорисовываются на экране телефона. Контроль над всеми уровнями, осуществляется при помощи класса LayerManager -менеджера уровней. Обилие методов предоставляемых игровыми классами позволяет отслеживать всевозможные столкновения, перемещения, анимацию и множество других функций, что значительно упрощает процесс создания мобильных игр.
В этой главе, сначала вы познакомитесь с устройством всех игровых классов, а затем будет создан ряд примеров иллюстрирующих работу и взаимодействие игровых классов профиля MIDP 2.0.
Мобильная мультимедиа-библиотека
В этой главе... 9.1. Пакет javax.microedition.media 9.1.1. Интерфейс Control 9.1.2. Интерфейс Controllable 9.1.3. Интерфейс Player 9.1.4. Интерфейс PlayerListener 9.1.5. Класс Manager 9.2. Пакет javax.microedition.media.control 9.2.1. Интерфейс ToneControl 9.2.2. Интерфейс VolumeControl 9.3. Воспроизведение wav-файлов 9.4. Воспроизведение тональных звуков
Интерфейс Control
Интерфейс Control - это самый главный интерфейс, с его помощью осуществляется контроль над всеми имеющимися ресурсами, также от этого интерфейса наследуются еще два интерфейса ToneControl и VolumeControl.
Интерфейс Controllable
С помощью интерфейса Controllable можно получить управление над воспроизведением, посредством использования двух методов:
Control getControl (String controlType) - определяет тип управления; Control[] getControls () - получает управление.
Интерфейс Player
Интерфейс Player наследуется от интерфейса Controllable и необходим для реализации процесса воспроизведения звуковых данных на основе формирования проигрывателей. Проигрыватели создаются методом createPlayer () класса Manager, например:
Player player1 = Manager.createPlayer();
После создания проигрывателя можно производить воспроизведения звука, для этого необходимо воспользоваться методами интерфейса Player.
Методы интерфейса Player
void addPlayerListener(PlayerListener playerListener)-осуществляет обработку событий от определенного проигрывателя; void close () — закрывает проигрыватель; void deallocate () - освобождает ресурс, занятый проигрывателем; String getContentType () - получает тип звуковых данных воспроизводимых проигрывателем; long getDuration () — получает размер звукового файла; long getMediaTime () - получает время воспроизведения звуковых данных; int getState() — определяет состояние проигрывателя; void removePlayerListener(PlayerListener playerListener) — удаляет установленный обработчик событий; void setLoopCount (int count) - устанавливает цикличное воспроизведение звуковых данных; long setMediaTime(long now) - устанавливает время воспроизведения; void start () - дает команду на воспроизведение; void stop () - останавливает воспроизведение.
Большинство методов направленно на работу со звуковыми данными, позже в разделе 9.3 мы разберем подробнее работу с методами интерфейса Player.
Интерфейс PlayerListener
Интерфейс PlayerListener позволяет осуществлять обработку событий полученных от проигрывателя. Помните в главе 5 мы разбирали работу интерфейса CommandListener? Интерфейс PlayerListener функционирует почти по такой же схеме, но ориентирован на работу с проигрывателем. В составе интерфейса PlayerListener .имеется всего один метод:
void playerUpdate (Player player, String event, Object eventData) — обновляет состояние проигрывателя.
C помощью констант интерфейса Player в методе playerUpdate (), нужно задавать тип необходимых событий в параметрах eventData и event:
static String CLOSED - уведомляет о закрытии проигрывателя; static String DEVICE_AVAILABLE - уведомляет о доступности проигрывателя; static String DEVICE_UNAVAILABLE - уведомляет о недоступности проигрывателя; static String DURATIONJJPDATED - обновляет состояние; static String END_OF_MEDIA - уведомляет о конце воспроизведения данных проигрывателем; static String ERROR - уведомляет об ошибке; static String STARTED - уведомляет о начале работы проигрывателя; static String STOPPED - уведомляет о конце работы проигрывателя; static String VOLUME_CHANGED - уведомляет о выборе громкости для воспроизведения.
Интерфейс ToneControl
С помощью интерфейса ToneControl происходит настройка и построение блока тональных звуков для воспроизведения. Это достигается путем использования метода void setSequence (byte [ ] sequence), который устанавливает тональные звуки для воспроизведения и набора следующих констант:
static byte BLOCK_END - конец блока воспроизведения; static byte BLOCK_START - стартовая позиция в блоке; static byte C4 - нота До; static byte PLAY_BLOCK - воспроизвести блок; static byte REPEAT - повторить воспроизведение блока; static byte SET_VOLUME - установить громкость; static byte SILENCE - без звука; static byte TEMPO — темп или скорость воспроизведения; static byte VERSION - версия атрибута воспроизведения. С помощью перечисленных констант производится настройка блока тональных звуков для воспроизведения, о которых мы поговорим подробно в разделе 9.4.
Интерфейс VolumeControl
Интерфейс VolumeControl имеет методы, на основе которых можно реализовать управление громкостью воспроизведения:
int getLevel() - возвращает текущий уровень громкости; boolean isMutedf) - определяет состояние сигнала; int setLevel(int level) -устанавливает уровень громкости. Значение может находиться в пределах от 0 до 100; void setMute (boolean mute) - устанавливает состояние сигнала.
Сейчас мы вкратце рассмотрели имеющиеся интерфейсы, классы, методы и константы двух пакетов javax.microedition.media и javax.microedition.media.control. Теперь давайте подытожим все полученную информацию и рассмотрим примеры, иллюстрирующие работу со звуком в мобильных телефонах.
Класс Manager
Класс Manager создает проигрыватель для воспроизведения звуков, а также отслеживает доступные протоколы звуковых данных, с помощью нескольких методов.
static Player createPlayer(InputStream stream, String type) - создает проигрыватель для воспроизведения звуковых данных из потока; static Player createPlayer(String locator) - создает проигрыватель для воспроизведения звуковых данных из определенного файла; static String[] getSupportedProtocols(String content_type) - возвращает список доступных протоколов для мобильного устройства; static void playTone(int note, int duration, int volume) -воспроизводит различные тональные звуки.
Пакет javaxmicroeditionmedia
Пакет javax.microedition.media необходим для работы со звуком и содержит четыре основных интерфейса и один класс, на базе которых и происходит воспроизведение звуков в телефоне.
Пакет javaxmicroeditionmediacontrol
Пакет javox.microedition.media.control небольшой по своему составу и производит контроль над процессами, связанными с воспроизведением и регулировкой звука. В разделе 9.4 этой главы очень подробно рассматривается схема контроля.
в играх создает более насыщенную-
Воспроизведение звуков в играх создает более насыщенную- атмосферу. Хорошая звуковая дорожка к игре - это 30-40% успеха! Но, к сожалению, в мобильных играх, нет возможности воспроизведения мощной полноценной звуковой дорожки, как в компьютерных или приставочных играх, в виду ограничения системных ресурсов телефона. Поэтому, в основном, все звучание в играх сводится к воспроизведению так называемых тональных звуков. Каждый тональный звук соответствует определенной ноте, выстроив необходимую последовательность нот для воспроизведения можно получить определенную звуковую дорожку.
В профиле MIDP 1.0 возможность работы со звуком отсутствует, и все строится на использовании классов, предоставляемых производителями мобильных телефонов. В профиле MIDP 2.0 такая возможность имеется, поскольку появилась мобильная мультимедиа библиотека (MMAPI), разработаная экспертной группой, в состав которой входят известные компании:
Nokia (Specification Lead); Aplix Corporation; Beatnik. Inc.; France Telecom; Insignia Solutions; Mitsubishi Electric Corp.; Motorola; Netdecisions Holdings United; NTT DoCoMo, Inc.; Openwave Systems Inc.; PacketVideo Corporation; Philips; Siemens AG ICM MP TI; Smart Fusion; Sun Microsystems, Inc.; Symbian Ltd; Texas Instruments Inc.; Vodafone; Yamaha Corporation; Zucotto Wireless.
На данный момент существуют две мобильные мультимедиа-библиотеки, различающиеся по своему назначению и спецификации, это:
Mobile Media API - предназначена для работы с устройствами имеющими боле мощные системные ресурсы. Это, как правило, карманные портативные устройства; MIDP 2.0 Media API - эта библиотека направлена на поддержку мобильных устройств с ограниченными ресурсами.
В этой главе будет представлена мобильная мультимедиа библиотека MIDP 2.0 Media API, которая используется при программировании звука в приложениях написанных под профиль MIDP 2.0. Работа со звуком строится по принципу блочной конструкции состоящей из трех ключевых блоков:
Менеджер - это основной диспетчер, при помощи которого создаются все проигрыватели. Также менеджер имеет возможность в воспроизведении простых тональных звуков на телефоне. Менеджер в профиле MIDP 2.0 представлен классом Manager; Проигрыватель - осуществляет непосредственное воспроизведение звуков и представлен интерфейсом Player
Bee взаимодействие построено на использовании нескольких интерфейсов и класса Manager, содержащихся в библиотеке MIDP 2.0 Media API, которая состоит из двух пакетов:
javax.microedition.media; javax.microedition.media.control.
Эти два пакета содержат ряд интерфейсов и всего один класс Manager. Рассмотрим подробно оба пакета библиотеки MIDP 2.0 Media API, давая попутно краткую характеристику каждому интерфейсу, классу Manager и всем имеющимся методам. А потом, на основе полученного материала, создадим несколько примеров исходного кода, иллюстрирующих мoдель работы со звуком.
Воспроизведение тональных звуков
При создании звукового сопровождения к играм и приложениям в основном используются так называемые тональные звуки, генерируемые телефоном. Мобильные телефоны ограничены в системных ресурсах, поэтому воспроизведение wav, mp3 файлов не всегда возможно.
Воспроизведение тональных звуков происходит примерно тем же способом, что и воспроизведение wav-файлов, рассмотренное в предыдущем разделе. Создание тональных звуков строится на основе секвенсора, используемого музыкантами. То есть, в вашем распоряжении имеются семь нот, которые могут быть сыграны в любой тональности. Указав определенную последовательность нот в заданном массиве данных, вы сможете их впоследствии последовательно воспроизвести. В принципе, аналогичные действия можно произвести в любом телефоне, где в музыкальном редакторе вы выстраиваете некую последовательность определенных Символов, обозначающих ноты. Указав нужную последовательность нот, вы получаете готовую мелодию, созданную при помощи тональных звуков.
Перейдем к практике. Первым делом необходимо создать те самые семь нот. В классе ToneControl пакета javax.microedition.media.control.*, доступна константа С4, которая по своему звучанию соответствует ноте «До». Для того чтобы создать, например ноту Ре, можно воспользоваться следующей конструкцией кода:
byte Re = (byte)(ToneControl.C4+1);
Для создания последующей ноты Ми нужно прибавить к ноте До (то есть С4), число два и так далее. Когда закончатся все семь нот, то вы переходите к следующей октаве, что и предопределяет разные тональности звукового сопровождения. Всего можно использовать значение от 0 до 127.
Затем создается массив данных, можно назвать его секвенсором, в котором указывается последовательность нот. Синтаксис, используемый в секвенсоре, строго определен, и его необходимо правильно использовать. Например, имеется следующий массив данных, характеризующийся как секвенсор:
byte[] Nota = {...};
В этом массиве данных первой строкой кода должно идти указание версии используемого атрибута.
ToneControl.VERSION,1,
Затем задается скорость воспроизведения с помощью целочисленного значения, которое может варьироваться от 5 до 127. Например:
ToneControl.TEMPO,30,
Далее необходимо дать команду, указывающую начало блока последовательности нот для воспроизведения, например:
ToneControl.BLOCK_START, 0,
И только после этого идет последовательность нот. Между нотами ставится обязательно длина ноты обычно заданная переменной, и ее диапазон может быть от 2 до 16. Например:
byte d = 4; Re,d,Mi,d,Re,d,
Между воспроизведением нот можно использовать паузы для создания выразительной мелодии. Пауза задается с помощью константы SELENCE, например:
byte stop = ToneControl.SELENCE; byte d = 4;
Тогда последовательность нот может быть следующей:
Re,d,stop,d,Mi,d,stop,d,stop,d,Re,d,
После того как вы задали всю последовательность нот, необходимо четко указать конец блока с помощью константы BLOCK_END следующим образом:
ToneControl.BLOCK_END, 0,
На каждую константу BLOCK_START, должна присутствовать константа BLOCK_END. Иначе возникнет ошибка при компиляции.
В конце нужно воспользоваться константой PLAY_BLOCK для воспроизведения блока последовательности нот. После этого созданный секвенсор можно использовать для воспроизведения проигрывателем тональных звуков. Посмотрите на листинг 9.2, где показана демонстрационная программа, воспроизводящая все семь нот одной октавы.
/ * * листинг 9.2 класс TonMIDlet воспроизводит тональные звуки */ import javax.microedition.Icdui.*; import javax.microedition.midlet.*; import javax.microedition.media.*; import javax.microedition.media.control.*; import java.io.*; public class TonMIDlet extends MIDlet implements CommandListener // команда выхода private Command exitMidlet = new Command("Выход",Command.EXIT, 0); // команда воспроизведения private Command pi = new Command("Играть", Command.OK, 1); // объект mydisplay представляет экран телефона private Display mydisplay; public TonMIDlet() { mydisplay = Display.getDisplay(this); } public void startApp() Form Is = new Form("Тональные звуки"); // добавляем команду выхода ls.addCommand(exitMidlet); // добавляем команду воспроизведения ls.addCoiranand(pl); ls.setCommandListener(this); // отражаем текущий дисплей my.display. setCurrent (Is) ; } private void TonPlay() { // нота До byte Do = ToneControl.C4;, // нота Ре byte Re = (byte)(ToneControl.C4 + 1); // нота Ми byte Mi = (byte)(ToneControl.C4 + 2) ; // нота Фа byte Fa = (byte)(ToneControl.C4 + 3); // нота Соль byte So = (byte)(ToneControl.C4 + 4); // нота Ля byte Lj = (byte)(ToneControl.C4 + 5); // нота Си byte Si = (byte)(ToneControl.C4 + 6); // пауза byte stop = ToneControl.SILENCE; // скорость воспроизведения тональных звуков byte speed = 30; // продолжительность воспроизведения ноты byte pr = 4; // секвенсор byte[] Nota = { // атрибут, задающий номер версии ToneControl.VERSION, 1, // скорость воспроизведения ToneControl.TEMPO, speed, // начало блока ToneControl.BLOCK_START, 0, // последовательность нот для воспроизведения Do,pr,stop,pr,Re,pr,stop,pr,Mi,pr,stop,pr, Fa,pr,stop,pr,So,pr,stop,pr,Lj,pr,stop,pr,Si,pr, // конец блока ToneControl.BLOCK_END, 0, // воспроизведение блока ToneControl.PLAY_BLOCK, 0, } ; // воспроизводим тональные звуки из секвенсора try{ Player player = Manager.createPlayer(Manager.TONE_DEVICE_LOCATOR); player.realize(); ToneControl toncontrl = (ToneControl)player.getControl(«ToneControl»); toncontrl.setSequence(Nota); player.start(); } catch (IOException zxz){} catch (MediaException zmz){} } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void commandAction(Command c, Displayable d) { if (c = = exitMidlet) { destroyApp(false); notifyDestroyed(); } if (c == pi) { TonPlay(); } } }
В основе программы из листинга 9.2 лежит модель, используемая в предыдущем разделе при .воспроизведении wav-файла. В классе TonMIDlet создается пустая форма классом Form и назначаются две команды: выход из программы и воспроизведение тональных звуков. При нажатии на кнопку Играть, задействуется метод TonPlay (), где создаются ноты, секвенсор, после чего происходит воспроизведение последовательности нот.
Воспроизведение wav-файлов
Воспроизведение wav-фа.йлов в телефоне задача не сложная. Wav-файл должен быть размещен в каталоге создаваемого приложения. Если вы используете J2ME Wireless Toolkit 2.1, то расположите wav-файл в папке res. Впоследствии, после компиляции и установки программы, wav-файл будет находиться в JAR- архиве, и доступен для воспроизведения.
Для того чтобы воспроизвести необходимый wav-файл создается объект класса Inputstream для связывания потока данных с ресурсом, а именно wav-файлом,например:
Inputstream input = getClassf).getResourceAsStream(«файл.wav»);
Затем создается проигрыватель:
Player player = Manager.createPlayer(input,«audio/X-wav»);
Проигрыватель формируется с помощью метода createPlayer() класса Manager. Количество создаваемых проигрывателей регламентируется только системными ресурсами телефона. После чего используется метод start() для воспроизведения wav-файла.
В листинге 9.1 вы найдете пример исходного кода, в котором происходит загрузка и воспроизведение wav-файла из JAR-архива. В примере используется класс Form, с помощью которого создается пустой экран, и добавляются две команды: выход из приложения и воспроизведение wav-файла. Основные действия разворачиваются в методе WavPlay (), где создается проигрыватель и воспроизводится wav-файл. Обратите также внимание на подключаемые пакеты.
/** листинг 9.1. класс WavMIDlet воспроизводит wav-файл */ import javax.microedition.Icdui.*; import javax.microedition.midlet.*; import javax.microedition.media.*; import javax.microedition.media.control.*; import java.io.*; public class WavMIDlet extends MIDlet implements CommandListener { // команда выхода private Command exitMidlet = new Command("Выход", Command.EXIT, 0); // команда воспроизведения private Command pi = new Command("Играть", Command.OK, 1); // объект mydisplay представляет экран телефона private Display mydisplay; public WavMIDlet() { mydisplay = Display.getDisplay(this); } public void startApp() { Form Is = new Form("Воспроизведение wav"); // добавляем команду выхода ls.addCommand(exitMidlet); // добавляем команду воспроизведения ls.addCommand(pl); ls.setCommandListener(this); // отражаем текущий дисплей mydisplay.setCurrent(Is); } private void WawPlay() { try { // ищем ресурс с именем melod.wav InputStream input =getClass().getResourceAsStream("melod.wav"); // создаем проигрыватель Player player = Manager.createPlayer(input, "audio/X-wav"); // воспроизводим player.start(); } catch (lOException zxz) {} catch (MediaException zmz) {} } public void pauseApp() {} public void destroyApp(boolean unconditional){} public void commandAction(Command c, Displayable d) { if (c == exitMidlet) { destroyApp(false); notifyDestroyed() ; } if (c = = pi) { WawPlay(); } } }