Стреляющий космический корабль

Давайте теперь поговорим о том, как сделать так, чтобы заставить звездолет (или любой другой объект игры) стрелял. Это действие используется почти во всех action-играх типа MarsAttacks или моей игры J-Rio. И хотя оно реализуется почти элементарно, я получил много писем от людей, не знающих, как сделать это (в чем нет ничего постыдного ;-). Что ж, приступим к решению задачи!

Наброски классов и важные шаги

Как всегда, я хочу начать эту главу с объяснения основ классов и главных шагов и техник, которыен я собираюсь использовать. Так как Java - объекто-ориентированный язык программирования, и выстрел должен иметь множество атрибутов и типов поведения (движение, проверка на столкновения с врагом, стенами, ...), должно быть понятно, что мы должны создать класс Shot, чтобы реализовать все типы поведения. Этот класс будет содержать все переменные атрибутов (координаты x и y, скорость...) выстрела и иметь методы, представляющие поведения типа движения. В большинстве случаев выстрел производится объектом игрока, так что у нас будет объект игрока с методом generateShot. Он будет генерировать выстрел (не удивляйтесь этому имени ;-) на позиции игрока, с правильныи направлением... и будет возвращать этот объект в вызывающий класс. В моем примере вызывающим классом будет Main. Этот класс хранит сгенерированный выстрел в массиве объектов выстрелов. Каждый раз, когда будет вызываться метод run главного класса, мы будем iterate over этот массив, чтобы перемещать выстрелы, уничтожать их, если они покинули поле игры, а также мы можем проверять наличие столкновений с врагами либо элементами уровня (не ищите, их нет в апплете, прилагающемся к главе). Метод paint апплета будет также run through the shots array и прорисовывать каждый в нем существующий выстрел.
Если вы все поняли, можете просто взять исходный код и просмотреть его самостоятельно. Классы просты, и в них нет ничего особенного. Тем не менее, вы конечно можете прочитать мои объяснения, приведенные ниже.

Класс Shot

Этот класс хранит координаты выстрела, и имеет метод moveShot для реализации поведения движения в y-направлении и метод drawShot(), рисующий изображения выстрелов на экране.

import java.awt.Graphics;
import java.awt.Color;

public class Shot
{

    // переменные
    private int x_pos;
    private int y_pos;

    // размер выстрела
    private final int radius = 3;

    // конструктор
    public Shot(int x, int y)
    {
      x_pos = x;
      y_pos = y;
    }

    // возвращает y-позицию, необходимую для проверки, покинул ли выстрел пространство игры
    public int getYPos()
    {
      return y_pos;
    }

    // движение выстрела по оси y
    public void moveShot(int speed)
    {
      y_pos += speed;
    }

    // рисуем выстрел на экране
    public void drawShot(Graphics g)
    {
      g.setColor(Color.yellow);
      g.fillOval(x_pos, y_pos, radius, radius);
    }
}

Класс Player

Этот класс также прост, как три копейки. Он также имеет метод move (чтобы двигать игрока по оси x), и метод draw, и хранит координаты космического корабля. Единственный "интересный" метод - generateShot.

import java.awt.Graphics;
import java.awt.Color;

public class Player
{

    // переменные
    private int x_pos;
    private int y_pos;

    // конструктор
    public Player(int x, int y)
    {
      x_pos = x;
      y_pos = y;
    }

    // перемещение корабля по оси x
    public void moveX(int speed)
    {
      x_pos += speed;
    }

    // генерируем выстрел на текущей позиции космического корабля
    // и возвращаем этот выстрел в вызывающий метод
    public Shot generateShot()
    {
      Shot shot = new Shot(x_pos, y_pos);

      return shot;
    }

    // рисуем игрока
    public void drawPlayer(Graphics g)
    {
      g.setColor(Color.red);
      int [ ] x_poly = {x_pos, x_pos - 10, x_pos, x_pos + 10};
      int [ ] y_poly = {y_pos, y_pos + 15, y_pos + 10, y_pos + 15};
      g.fillPolygon(x_poly, y_poly, 4);
    }
}

Класс Main

Теперь взглянем на класс Main. Я удалил все неважные части (обозначенные как "...") для поведения стрельбы корабля. Вы увидите эти части, если скачаете исходный код.

import java.applet.*;
import java.awt.*;

public class Main extends Applet implements Runnable
{

    // переменные
    ...
    private Player player;
    private Shot [] shots;

    // константы
    private final int shotSpeed = -2;
    ...

    // двойная буферизация
    private Image dbImage;
    private Graphics dbg;

    public void init()
    {
      ...
      // генерация массива shot
      shots = new Shot[5];
    }

    ...

    public void run ()
    {

      while (true)
      {
        // Iterate over the shot array and move shots,
        // проверка, ушел ли выстрел за пределы поля игры
        // вы можете добавить сюда другие проверки (проверку на столкновение...)

        for(int i=0; i<shots.length; i++)
        {
          if(shots[i] != null)
          {
            // перемещение выстрела
            shots[i].moveShot(shotSpeed);

            // проверка, покинул ли выстрел пределы поля игры
            // если значение true, удалить из массива
            if(shots[i].getYPos() < 0)
            {
              // удалить выстрел
              shots[i] = null;
            }

            // другие операции
            // ...
            // например, проверка столкновений...
            // ...
          }
        }

        // перемещение игрока

        ...

        ...
      }
    }

    public boolean keyDown(Event e, int key)
    {
      ...

      // Генерация нового выстрела при нажатии клавиши "Пробел"
      else if(key == 32)
      {
        // генерация нового выстрела и попытка сохранить его в массиве
        for(int i=0; i<shots.length; i++)
        {
        // only store shot if there is a place left in the array
          if(shots[i] == null)
          {
            shots[i] = player.generateShot();
            // вызов break, чтобы сохранить выстрел только один раз, важно!
            break;
          }
        }
      }

      ...
    }

    public void paint (Graphics g)
    {
      // рисуем игрока
      ...

      // рисуем выстрелы
      for(int i=0; i<shots.length; i++)
      {
        if(shots[i] != null)
        {
          shots[i].drawShot(g);
        }
      }
    }
}

Вывод

Надеюсь, что я смог убедить вас, что генерация выстрелов - несложное дело. Техника хранения игровых объектов одного типа в массиве и iterate over его каждый раз, когда вызываются методы run и paint, часто используется в программировании игр и других приложений, так что не забывайте о ней, если окажетесь в схожей ситуации.

Скачать исходный код апплета
Запустить апплет

Следующая глава:
Основы платформенной игры

Fabian Birzele, 2001-2003.
перевод и веб-дизайн: В.Мурзагалин, 2004.