Программирование двухмерных игр на J2ME
ОГЛАВЛЕНИЕ
Введение
J2ME – это интересная среда для игр. Имея базовые знания Java, заранее установленные NetBeans и J2ME Wireless Toolkit (беспроводной инструментарий J2ME), вы можете создавать простые, забавные двухмерные игры, которые можно запускать на ваших собственных мобильных устройствах.
В этой статье показано использование интерфейса прикладного программирования (API) 5-класса Game,скомпонованного в пакет javax.microedition.lcdui.game.
Предпосылки
Здесь предполагается, что вы имеете базовые знания Java, знакомы с NetBeans и прочитали статью “Введение в программирование на Java ME”. Создание игр также требует определенных знаний физики, включая динамику Ньютона, движения, столкновения, и так далее.
Недавно мы принимали участие в проекте под названием “Интенсивные программы разработки приложений для мобильных устройств”, организованном нашим университетом и его партнерами. Проект финансировался Uramus - программой поддержки студенческого обмена между европейскими странами. Мы начали работать с J2ME, что в итоге привело к написанию этой статьи.
Использование GameBuilder облегчает процесс разработки игры. Однако он не будет подробно описываться в данной статье. Подробную информацию о разработке игр с помощью GameBuilder можно найти здесь.
Использование кода
MainMidlet (главный мидлет)
Как и Midlet, MainMidlet должен расширять абстрактный класс Midlet, который можно найти в пакете javax.microedition.midlet. Midlet требует переопределения трех методов:
- startApp(), вызываемого для запуска игры
- pauseApp(), вызываемого для временной остановки приложения, например, при приеме вызова. Приложение должно прекратить отображение анимации и освободить ресурсы, которые больше ему не нужны. Работу приложения можно возобновить с помощью вызова resumeMIDlet()
- destroyApp(boolean unconditional), вызываемого при выходе из приложения. Мидлет может вызвать notifyDestroyed(), чтобы прервать работу
(Эти методы автоматически создаются при создании Визуального мидлета в NetBeans.)
Однако, нам всего лишь нужно реализовать методы startApp() путем создания экземпляра класса GameCanvas и добавления CommandListener для выхода из Midlet (мидлета). Конечно же, это не лучший способ программирования, но на этом этапе нам только лишь нужно, чтобы приложение выполнялось. В качестве текущего средства вывода изображения можно установить GameCanvas в конце или внутри GameCanvas с помощью метода setCurrent. Этот метод принимает любые объекты Displayable (отображаемые) в качестве аргумента.
public class MainMidlet extends MIDlet implements CommandListener {
private SSGameCanvas gameCanvas ;
private Command exitCommand ;
public void startApp() {
try {
//создание нового потока игры
gameCanvas = new SSGameCanvas();
gameCanvas.start(); // запуск потока игры
exitCommand = new Command("Exit",Command.EXIT,1);
gameCanvas.addCommand(exitCommand);
gameCanvas.setCommandListener(this);
Display.getDisplay(this).setCurrent(gameCanvas);
}
catch (java.io.IOException e) { e.printStackTrace();}
}
public void pauseApp() {}
public void destroyApp(boolean unconditional) {}
public void commandAction(Command command, Displayable displayable) {
if (command == exitCommand) {
destroyApp(true);
notifyDestroyed();
}
}
}
GameCanvas («игровой холст»)
Как элемент низкоуровневой библиотеки пользовательского интерфейса, при объединении с GraphicsGameCanvas предоставляет нам гибкие инструменты создания нашего собственного игрового экрана. С помощью Graphics вы, по сути, можете рисовать то, что вы обычно можете создать в двухмерном Java, включая рисование фигур, строк или изображений. GameCanvas – это расширение исходного Canvas (холста), предоставляющее больше возможностей контроля над рисованием и повышающее скорость реакции игры на нажатия кнопок. (графика)
Используя GameCanvas, вы можете заметить, что его возможности включают в себя скрытую буферизацию. Когда вы рисуете что-либо, скорее всего, вы выполняете это за пределами экрана и с помощью вызова метода flushGraphics() выводите содержимое буфера на экран.
GameCanvas также упрощает процесс получения входных данных, позволяя нам запрашивать состояние ключа, используя метод getKeyState(). Обработка состояния кнопки, однако, передается GameManager (средство управления игрой) для облегчения управления игрой.
Начало системы координат игры расположено в верхнем левом углу экрана, как показано на рисунке.
В методе render (отображение) внеэкранный буфер очищается, и графика отображается с помощью вызова метода paint, принадлежащего GameManager.
public void render(Graphics g) {
……..
// Очистка холста.
g.setColor(0, 0, 50);
g.fillRect(0,0,WIDTH-1,HEIGHT-1);
….….
gameManager.paint(g);
}
В данном примере SSGameCanvas реализует интерфейс Runnable (работоспособный), что приводит к созданию методов run(), в которых мы создаем замкнутый цикл (петлю) игры, выполняющийся до тех пор, пока не будет достигнуто определенное условие завершения.
Замкнутые циклы игры
public void run() {
while (running) {
// рисование графики
render(getGraphics());
// продвижение к следующему циклу (такту)
advance(tick++);
// отображение
flushGraphics();
try { Thread.sleep(mDelay); }
catch (InterruptedException ie) {}
}
}
Расчет времени в игре контролируется целым числом, названным tick. tick упрощает задачу задания времени в игре, такую как темп стрельбы корабля, или то, как долго будет сверкать звезда, или то, как долго спрайт будет входить в следующий кадр. Если расчет времени выполняется в GameCanvas путем реализации Runnable, tick имеет значение mDelay + время, необходимое для завершения одного игрового цикла (миллисекунды). Если мы создаем поток, который берет на себя управление tick, мы, скорее всего, будем иметь tick = mDelay. Вероятно, нам понадобится только 24 -30 кадров в секунду, поэтому мы ограничиваем mDelay таким образом, чтобы иметь требуемый анимационный эффект и при этом потреблять меньше энергии. Во время каждого цикла игры мы вызываем метод продвижения GameManager, который расширяет LayerManager, чтобы проверить ввод пользователя, столкновения и нарисовать графику.
public void advance(int ticks) {
// продвижение к следующему игровому холсту
gameManager.advance(ticks);
this.paint(getGraphics());
}
}
tick может иметь ограничение: он ограничивает продолжительность игры предельным значением целого числа, что составляет примерно 590 часов в 32-разрядной системе.
Спрайт
Спрайты выполняют роли действующих субъектов в играх. Это могут быть наши герои Марио, утки и снаряды в играх про Марио или космические корабли в игре про звездные войны. Как базовый изобразительный элемент, он может отображать непрерывное действие на экране с помощью нескольких кадров, сохраненных в Изображении. Файл Изображения должен хранить в себе все кадры Спрайта, чтобы затем их отобразить. Все кадры должны иметь заранее заданную одинаковую width (ширину) и height (высоту).
public SpaceShip(Image image, int w, int h) throws java.io.IOException {
super(image,w ,h);
WIDTH = w;
HEIGHT= h;
setFrameSequence(SEQUENCE);
defineReferencePixel(WIDTH/2,HEIGHT/2);
setTransform(this.TRANS_MIRROR_ROT270);
}
Чтобы инициализировать спрайт “Корабль”, мы вызываем конструктор из базового класса Sprite: super(image, w, h); где w и h – это ширина и высота каждого кадра. Изображение состоит из 8 кадров, поэтому мы устанавливаем последовательность кадров {0,1,2,3,4,5,6,7}, используя метод setFrameSequence.
Затем мы вызываем метод defineReferencePixel, чтобы установить точку отсчета в центре кадра. Эта точка отсчета будет использоваться для установки корабля в заданное положение на экране. И наконец, мы вращаем (поворачиваем) все кадры с помощью метода setTransform.
public void advance(int ticks) {
if ((ticks%RATE==0))
nextFrame();
}
Метод advance меняет кадр корабля, чтобы создать анимации в соответствии с параметром RATE (частота). Постоянно вызывая метод nextFrame(), мы отображаем на экране последовательность кадров от 0 до 8 и затем в обратном порядке до 0: 0,1,2…7,0,1,2…. Следующие методы moveLeft(), moveRight(), moveUp(), moveDown() изменяют положение корабля на экране в зависимости от значений его speedX (скорости по оси x) и speedy (скорости по оси y).
public void moveLeft () {
if (this.getRefPixelX()>0)
this.move(-speedX, 0);
}
public void moveRight (int m) {
if (this.getRefPixelX() < m)
this.move(speedX, 0);
}
public void moveUp () {
if (this.getRefPixelY()>0)
this.move(0, -speedY);
}
public void moveDown (int m) {
if (this.getRefPixelY()<m)
this.move(0, speedY);
}
Когда кораблю дается команда выстрелить снаряд, мы проверяем, прошло время охлаждения или нет, сравнивая текущее время со временем предыдущего выстрела.
public Bullet fire (int ticks) {
if (ticks- fireTick > SHOOT_RATE) {
fireTick = ticks;
bullet.setSpeed(BULLET_SPEED);
bullet.shot(this.getRefPixelX(), this.getRefPixelY()+HEIGHT/2);
return bullet;
}
else
return null;
}
Чтобы проверить, не произошло ли столкновение между спрайтами, изображениями или TitledLayer (о нем будет сказано позже), мы используем метод collidesWith. Мы используем этот метод в GameManager, чтобы проверить, не произошло ли столкновение между кораблем и астероидами, чтобы уменьшить «показатели здоровья» корабля, и чтобы проверить, не произошло ли столкновение между снарядом и астероидами, чтобы увеличить число очков и разрушить снаряд и астероид.
GameManager
Как подкласс LayerManager , GameManager может управлять группой Слоев, автоматически отображая каждый Слой в назначенном порядке. Метод append (добавить) вызывается для добавления конкретного слоя в LayerManager (средство управления слоями).
private Image shipImage;
private static final String SHIP_IMAGE = "/resource/blue_ship.png";
private static final int SHIP_WIDTH = 40;
private static final int SHIP_HEIGHT = 33;
shipImage = Image.createImage( SHIP_IMAGE );
// создание космического корабля
ship = new SpaceShip(shipImage, SHIP_WIDTH, SHIP_HEIGHT);
// задание его местоположения
ship.setRefPixelPosition(height/2, width/2);
this.append(ship);
Чтобы реагировать на ввод пользователя, мы запрашиваем состояния кнопок с помощью упоминаемого нами экземпляра GameCanvas, используя метод getKeyStates(). Мы реагируем соответствующим образом на каждое состояние кнопки, перемещая корабль в требуемом направлении.
int keyState = gameCanvas.getKeyStates();
// перемещение вправо
if ((keyState & GameCanvas.RIGHT_PRESSED)!= 0){
ship.moveRight(width);
}
// перемещение влево
if ((keyState & GameCanvas.LEFT_PRESSED)!= 0){
ship.moveLeft();
}
// перемещение вверх
if ((keyState & GameCanvas.UP_PRESSED) != 0){
ship.moveUp();
}
// перемещение вниз
if ((keyState & GameCanvas.DOWN_PRESSED) != 0){
ship.moveDown(height);
}
В GameManager мы также проверяем, произошло ли столкновение между кораблем, снарядом и случайным образом созданным врагом (астероидами). Если столкновение произошло, мы изменяем состояние игры в соответствии с этим столкновением.
private Obstacle checkCollisionWithAsteroids(Sprite t) {
for (int i =0; i < MAX_OBS;i++) {
if (obs[i]!=null)
if (obs[i].collidesWith(t, true)) {
return obs[i];
}
}
return null;
}
В случае, если мы достигли условия завершения игры, мы отображаем самое большое число набранных очков и останавливаем поток GameCanvas, используя метод stop (остановить):
protected void endGame(Graphics g) {
GameOver=true;
gameCanvas.stop();
Font f = g.getFont();
int h = f.getHeight()+40;
int w = f.stringWidth("High score")+40;
g.setColor(250,250,250);
g.drawString("Score " + score,(width-w)/2,(height-h)/2,g.TOP | g.LEFT);
}
Что дальше
Используя данные знания, вы сможете создавать простые игры, например: “отправиться на рыбалку”, “лягушка переходит дорогу”. Мы опишем TitledLayer и звук в следующих статьях. Они, несомненно, улучшат внешний вид игры и впечатление от нее.