...
В копилку Игрока
Буфер – место в памяти, в котором можно создавать графику. В традиционной буферной анимации роль буфера выполняет экран, а при создании анимации с двойной буферизацией к экрану добавляется область памяти.
На рис. 5.6 показано, как используется буфер в памяти для выполнения всех необходимых действий. Это может показаться хитрым приемом программирования, однако все делается очень просто (благодаря MIDP 2.0 API).
Кроме стандартного класса Canvas в MIDP API существует класс GameCanvas, поддерживающий графику с двойной буферизацией. Чтобы воспользоваться преимуществами класса GameCanvas, образуйте игровой класс холста от класса GameCanvas, после чего вы сможете работать с этим объектом в обычном режиме. Однако теперь все построения будут производиться в буфере. Чтобы вывести результат на экран, воспользуйтесь методом flushGraphics().
Давайте посмотрим, как работает буфер в памяти класса GameCanvas. Когда вы делаете что-либо с объектом Graphics, ассоциированным с холстом, все построения выполняются в буфере, на экране изменения не будут видны. Вызов метода flushGraphic() позволяет отобразить все, что было построено в памяти, на экране. При этом содержимое буфера не изменяется и не стирается, а просто выводится на экран.
...
В копилку Игрока
Когда вы создаете объект класса GameCanvas, при инициализации буфер заполняется пикселями белого цвета.
Класс GameCanvas реализует ряд интересных методов, которые можно применять при создании мобильных игр, например, эффективная обработка ввода, с которой вы познакомитесь в следующей главе. А пока – вы достаточно знаете для того, чтобы создавать плавную анимацию в мидлете.
Построение программы UFO
Хотя можно привести массу примеров написания программы с применением спрайтовой анимации, UFO (НЛО) – актуален всегда. Если вы когда-нибудь столкнетесь с тем, что в вашей игре чего-то не хватает, добавьте неопознанный летающий объект – это помогает всегда! В этом разделе вы научитесь использовать возможности MIDP для создания мидлета, в котором неопознанный летающий объект будет перемещаться по экрану. Пример UFO демонстрирует основы спрайтовой анимации, показывает, как применять эту методику на практике. По мере изучения материала книги вы будете знакомиться с новыми более интересными возможностями классов анимации MIDP.
Пример UFO использует спрайт НЛО, который хаотично перемещается по черному экрану. Спрайт НЛО – это изображение летающего объекта, в программе используются средства класса Sprite для изменения положения спрайта на экране. Вероятно, самый важный аспект программы UFO – это создание потока анимации, который обновляет и выводит спрайт НЛО через заданные промежутки времени. Поток анимации называется игровым циклом – это сердце и душа любой мобильной игры, в которой используется анимация.
Написание программного кода
Как и при написании большинства мидлетов, самое интересное начинается при создании специального класса холста, который в данном случае называется UFOCanvas. Ниже приведены члены-переменные класса UFOCanvas:
private Display display;
private boolean sleeping;
private long frameDelay;
private Random rand;
private Sprite ufoSprite;
private int ufoXSpeed, ufoYSpeed;
Переменная display уже знакома вам по предыдущим примерам, она используется для работы с дисплеем. Переменная sleeping определяет, запущен ли игровой цикл. Вы можете приостановить выполнение анимации, присвоив этой переменной значение true. Переменная frameDelay тесно связана с игровым циклом, поскольку она контролирует частоту его выполнения. Если быть более точным, то эта переменная содержит число миллисекунд между итерациями цикла. Вы можете с легкостью пересчитать это число в количество фреймов, для чего нужно на него разделить 1. Например, если величина framedelay равна 40 мс (или 0.04 с), то частота кадров будет равна 25. Аналогично, вы можете преобразовать частоту кадров во временной интервал между ними, для чего нужно поделить 1 на частоту и умножить на 1000. Например:
1/30 кадр/с = 0.333333 с = 33 мс
Член-переменная класса rand – это экземпляр стандартного генератора случайных чисел MIDP, он используется для создания произвольного движения объекта по экрану. Спрайт НЛО хранится в переменной ufoSprite, которая является объектом класса Sprite. Скорость спрайта хранится отдельно от самого спрайта – в переменной ufoSpeed.
...
Совет Разработчику
С практической точки зрения, обычно лучше создавать спрайты как отдельные классы, производные от класса Sprite. Однако поскольку спрайт НЛО достаточно прост, нет необходимости создавать для него отдельный класс. Большинство спрайтов, с которыми вы столкнетесь в книге, будут созданы как отдельные классы, производные от Sprite.
Чуть раньше я упомянул, что переменная frameDelay определяет частоту кадров анимации мидлета UFO. Эта переменная инициализируется в конструкторе мидлета:
frameDelay = 33;
Возвращаясь к указанному ранее уравнению, вы можете посчитать, что этот интервал времени соответствует частоте 30 кадров/с. Если вы вспомните, о чем говорилось в начале главы, то исходя из того, что современные мобильные телефоны поддерживают такую частоту, она обеспечивает плавную анимацию. Это тот случай, когда тестирование на реальном устройстве важно. Большая часть инициализирующего кода мидлета UFO расположена в методе start(). Ниже приведена часть кода, инициализирующего спрайт:
ufoXSpeed = ufoYSpeed = 3;
try {
ufoSprite = new Sprite(Image.createImage("/Saucer.png"));
ufoSprite.setPosition(0, 0);
}
catch (IOException e) {
System.err.println("Failed loading image!");
}
Компоненты X и Y скорости спрайта равны 3, поэтому спрайт будет перемещаться на три пикселя вниз и вправо за одну итерацию. Отрицательные значения компонента скорости говорят о том, что спрайт перемещается вверх и влево. Чтобы создать объект класса Sprite, передайте созданное изображение конструктору этого класса. Начальное положение спрайта равно (0,0) – верхний левый угол экрана. Поток анимации устанавливается также в методе start():
sleeping = false;
Thread t = new Thread(this);
t.start();
Сначала переменной sleeping присваивается значение false, это означает, что выполнение цикла разрешено. Затем создается объект класса Thread, которому передается холст с помощью параметра this. Поток вызывает метод start(), который запускает и приостанавливает выполнение игрового цикла. Важно предусмотреть средства остановки игрового цикла, для чего предусмотрен метод stop():
public void stop() {
// остановить анимацию
sleeping = true;
}
Как вы можете видеть, приостановить выполнение цикла очень легко, для этого достаточно присвоить переменной sleeping значение true. Игровой цикл расположен в методе run():
while (!sleeping) {
update();
draw(g);
try {
Thread.sleep(frameDelay);
}
catch (InterruptedException ie) {}
}
Игровой цикл – это цикл while, который выполняется до тех пор, пока значение переменной sleeping ложно. Внутри цикла вызывается метод update(), который обновляет анимацию, после чего вызывается метод draw(), обновляющий изображение. Статический метод sleep() класса Thread используется, чтобы установить задержку потока анимации, которая определяется переменной frameDelay. Эта часть кода управляет анимацией мидлета. Метод update() вызывается в цикле один раз, а следовательно, отвечает за обновление каждого фрейма анимации. Иначе говоря, метод update() вызывается 30 раз в секунду, поскольку частота смены кадров в мидлете UFO равна 30 кадров/с. В данном случае метод update() отвечает за случайное изменение скорости летающего объекта, а следовательно, и за изменение его положения. Приведенный ниже код изменяет скорость объекта:
if (rand.nextInt() % 5 == 0) {
ufoXSpeed = Math.min(Math.max(ufoXSpeed + rand.nextInt() % 2, -8), 8);
ufoYSpeed = Math.min(Math.max(ufoYSpeed + rand.nextInt() % 2, -8), 8);
}
...
Совет Разработчику
Метод update() – это самый важный метод, который вы будете разрабатывать, создавая игры в среде J2ME. Одно выполнение метода update() составляет один игровой цикл, этот метод контролирует каждый стук сердца вашей игры. Поэтому вы должны быть уверены, что каждая строка кода этого метода хорошо продумана и тщательно проработана, а также оптимизирована с точки зрения эффективности. С некоторыми приемами оптимизации игр вы познакомитесь в главе 17.
Метод nextInt() класса Random() используется для случайной генерации случайного целого числа. Если число делится на 5, то скорость летающей тарелки изменяется. Это может показаться странным, но идея заключается в том, что скорость НЛО не должна изменяться на каждой итерации. Проверяя делимость числа на 5 (%), в среднем скорость объекта изменяется один раз за пять фреймов. Чтобы изменять скорость чаще, необходимо уменьшить число, стоящее при проверке делимости. Например, если вы хотите изменять скорость на каждом третьем кадре, то измените код так: rand.nextInt() % 3.
Скорость летающей тарелки также изменяется на случайное число. Скорость может изменяться на значение из диапазона от -2 до 2. Более того, методы Math.min() и Math.max() используются для ограничения скорости, по модулю она не должна превосходить 8. При этом отрицательные значения скорости говорят о том, что спрайт перемещается вверх или вправо.
...
Совет Разработчику
Вы можете использовать и другой порог для ограничения скорости, число 8 – это не магическое число.
После того как скорость была изменена случайно, метод update() перемещает НЛО в новое положение:
ufoSprite.move(ufoXSpeed, ufoYSpeed);
Метод move() класса Sprite() перемещает спрайт на указанное число пикселей. В этом случае значения компонент скорости спрайта – это именно то, что необходимо для смещения. Но здесь есть подводный камень. Что делать, когда НЛО достигает края экрана? Хотя вы можете сделать так, что он будет отталкиваться от стенок, намного лучше, если он будет появляться с другой стороны экрана, как в игре Asteroids. Ниже приведен код реализации этого:
if (ufoSprite.getX() < -ufoSprite.getWidth()) //По достижении НЛО края экрана при движении по горизонтали переместить его к противоположному
ufoSprite.setPosition(getWidth(), ufoSprite.getY());
else if (ufoSprite.getX() > getWidth())
ufoSprite.setPosition(-ufoSprite.getWidth(), ufoSprite.getY());
if (ufoSprite.getY() < -ufoSprite.getHeight()) //По достижении НЛО края экрана при движении по вертикали переместить его к противоположному
ufoSprite.setPosition(ufoSprite.getX(), getHeight());
else if (ufoSprite.getY() > getHeight())
ufoSprite.setPosition(ufoSprite.getX(), -ufoSprite.getHeight());
В этом коде нет ничего волшебного, он просто проверяет, не вышел ли НЛО за пределы экрана. Если да, то летающий объект появится у противоположного края.
...
Совет Разработчику
Если вы не хотите идти по стопам игры Asteroids, то поступите так же, как в игре Pong. Пусть НЛО отражается от краев экрана. Вместо того чтобы изменять положение спрайта на экране, измените знак его скорости. Изменение знака компонента ufoXSpeed позволит отражаться НЛО от левой и правой границ, а ufoYSped – от верхней и нижней.
Последний элемент головоломки с названием UFOCanvas – это метод draw(), который вызывается для рисования анимации:
private void draw(Graphics g) {
// очистить экран
g.setColor(0x000000);
g.fillRect(0, 0, getWidth(), getHeight());
// нарисовать спрайт UFO
ufoSprite.paint(g);
// сменить буфер
flushGraphics();
}
В этом методе экран сначала очищается и заполняется черным цветом, а затем вызывается метод paint(), который и рисует спрайт. В завершении созданная графика выводится на экран, для чего вызывается метод flushGraphics(). В этом и состоит вся прелесть двухбуферной анимации: вы создаете графику, а затем выводите ее на экран. Без этого игры были бы не столь привлекательными, поверьте.
Теперь, чтобы объединить все вышесказанное, посмотрите листинг 5.1.
Листинг 5.1. Класс UFOCanvas – это класс холста мидлета UFO
import javax.microedition.lcdui.*;
import javax.microedition.lcdui.game.*;
import java.util.*;
import java.io.*;
public class UFOCanvas extends GameCanvas implements Runnable {
private Display display;
private boolean sleeping;
private long frameDelay;
private Random rand;
private Sprite ufoSprite;
private int ufoXSpeed, ufoYSpeed;
public UFOCanvas(Display d) {
super(true);
display = d;
// установить частоту кадров 30 кадров/с
frameDelay = 33;
}
public void start() {
// установить холст как текущий экран
display.setCurrent(this);
// инициализация генератора случайных чисел
rand = new Random();
// инициализация спрайта НЛО
ufoXSpeed = ufoYSpeed = 3;
try {
ufoSprite = new Sprite(Image.createImage("/Saucer.png"));
ufoSprite.setPosition(0, 0); //НЛО стартует в верхнем левом углу экрана
}
catch (IOException e) {
System.err.println("Failed loading image!");
}
// запуск потока анимации
sleeping = false;
Thread t = new Thread(this);
t.start();
}
public void stop() {
// Stop the animation
sleeping = true;
}
public void run() {
Graphics g = getGraphics();
// The main game loop
while (!sleeping) {
update();
draw(g);
try {
Thread.sleep(frameDelay);
}
catch (InterruptedException ie) {}
}
}
private void update() {
// Randomly alter the UFO\'s speed
if (rand.nextInt() % 5 == 0) { //Изменить случайным образом компоненты скорости по осям Х и Y в интервале от -8 до 8
ufoXSpeed = Math.min(Math.max(ufoXSpeed + rand.nextInt() % 2, -8), 8);
ufoYSpeed = Math.min(Math.max(ufoYSpeed + rand.nextInt() % 2, -8), 8);
}
// Move the sprite
ufoSprite.move(ufoXSpeed, ufoYSpeed);
// Wrap the UFO around the screen if necessary
if (ufoSprite.getX() < -ufoSprite.getWidth())
ufoSprite.setPosition(getWidth(), ufoSprite.getY());
else if (ufoSprite.getX() > getWidth())
ufoSprite.setPosition(-ufoSprite.getWidth(), ufoSprite.getY());
if (ufoSprite.getY() < -ufoSprite.getHeight())
ufoSprite.setPosition(ufoSprite.getX(), getHeight());
else if (ufoSprite.getY() > getHeight())
ufoSprite.setPosition(ufoSprite.getX(), -ufoSprite.getHeight());
}
private void draw(Graphics g) {
// Clear the display
g.setColor(0x000000);
g.fillRect(0, 0, getWidth(), getHeight());
// Draw the UFO sprite //Спрайт очень просто вывести на экран, используя метод paint()
ufoSprite.paint(g);
// Flush the offscreen graphics buffer
flushGraphics();
}
}
Когда код UFOCanvas полностью разработан, можно перейти к встраиванию этого класса в мидлет. В листинге 5.2 приведен код класса UFOMIDlet. Листинг 5.2. Код класса UFOMIDlet, хранящийся в файле UFOMIDlet.java
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;
public class UFOMIDlet extends MIDlet implements CommandListener {
private UFOCanvas canvas;
public void startApp() {
if (canvas == null) {
canvas = new UFOCanvas(Display.getDisplay(this)); //Настраиваемый холст – это то, что отличает класс мидлета от созданных ранее примеров
Command exitCommand = new Command("Exit", Command.EXIT, 0);
canvas.addCommand(exitCommand);
canvas.setCommandListener(this);
}
// Start up the canvas
canvas.start();
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {
canvas.stop();
}
public void commandAction(Command c, Displayable s) {
if (c.getCommandType() == Command.EXIT) {
destroyApp(true);
notifyDestroyed();
}
}
}
Как видно из приведенного кода, класс UFOMIDlet состоит из стандартного кода мидлета, который вы видели ранее. Класс мидлета отвечает за создание холста, запуск и остановку выполнения программы. Вы должны привыкнуть к тому, что большая часть специального игрового кода ваших игр будет реализовываться в классе холста и других обслуживающих классах.
Тестирование программы
Как только мидлет UFO собран, вы можете протестировать его в эмуляторе J2ME. В результате летающая тарелка немедленно начинает перемещаться по экрану (рис. 5.7).