Наша первая игра

Идея игры

В этой игре игроку нужно будет стрелять по двум шарам (красному и синему), летающим в зоне апплета. Их направление и скорость будет выбираться случайным образом, а юзер сможет "стрелять", наводя на них курсор мыши и кликая. Если шар достигает границы апплета, не будучи подстреленным, игрок теряет одну из десяти жизней. Когда теряется последняя жизнь, игра заканчиваются.

Наброски классов и методов

Класс Main
Этот класс реализует все методы, касающиеся анимации объектов (шаров) и управления игрой. Сюда входят методы init(), start(), stop(), paint(), update(), run() и mouseDown() для обработки событий мыши. Также класс управляет всеми объектами (два объекта-шара и объект-игрок) и потоком исполнения, в котором запускается игра.

  1. init(): объявляет все объекты, получает звуковые данные, устанавливает курсор мыши, и берет парметр скорости игры из html-файла.
  2. start(): запускает поток
  3. stop(): останавливает поток
  4. run(): перемещает шары
  5. paint(...): вызывает paint-методы обоих шаров, отображает информацию о счете и количестве жизней на экране. Если игра закончилась, выводится текст с результатами (например: "Вы - чемпион!")
  6. update(...): реализует двойную буферизацию
  7. mouseDown (...): отслеживает события мыши. Если игра запущена, метод смотрит за тем, как "игрок лупит по шарам", если игра остановлена, ждет двойного щелчка, чтобы запустить игру снова.

Класс Player
Очень простой класс. В нем присутствуют два экземпляра переменных для хранения счета и жизней игрока. Также есть методы: один прибавляет счет (addScore(int plus)), второй убавляет количество жизней (looseLife), и еще два метода, передающих эти значения в класс Main (getScore(), getLifes()).

Класс Ball
Самый сложный класс в игре. Он реализует все важные методы для объекта-шара, включая следующие функции:

  1. Ball(...): конструктор, получает все важные атрибуты объекта-шара (цвет, скорость, y-направление, ...) и инициализирует переменные этого объекта.
  2. move(): метод, перемещающий шар и проверяющий с помощью вызова isOut(), не покинул ли шар зону апплета, после чего игрок теряет жизнь.
  3. ballWasHit(): этот метод вызывается, если юзер попал в шар. После этого после этого направление шара выбирается случайным образом, а сам он возвращается на исходную позицию.
  4. userHit (int x, int y): метод проверяет с помощью векторов, был ли подстрелен шар (детали позже).
  5. isOut(): метод тестирует, вылетел ли шар за пределы апплета. Если возвращается значение true, игрок теряет одну жизнь.
  6. DrawBall(Graphics g): этот метод рисует шар и вызывается из метода paint класса Main.

Как все это работает вместе
Всякий раз, когда мы вызываем метод run в классе Main, мы вызываем move-метод шара. Этот метод перемещает шар и проверяет с помощью isOut(), не улетел ли он за пределы апплета. Если такое произошло, метод isOut() вызывает looseLife() и количество жизней уменьшается на одну единицу.
Когда юзер кликает по апплету, в классе Main вызывается метод mouseDown. Он, в свою очередь, вызывает метод userHit(), проверяющий, попал ли игрок в шар или нет, и возвращающий соответственно значения true или false. Если возвращается true, то в действие вступает ballWasHit(). Данный метод пополняет счет игрока и возвращает шар на исходную точку.

Одна важная вещь перед тем, как мы начнем
Если вы хотите понимать все, что я делаю, скачайте исходный код и читайте его, пока я буду объяснять. Я не буду разбирать каждый шаг детально, вместо этого изучайте исходник и попробуйте понять те вещи, о которых говориться не будет, с помощью других глав или неким своим путем.
Еще более важно начать писать свои небольшие программы и пытаться самостоятельно решать неизменно возникающие проблемы. Каждая игра требует своеобразного подхода и собственных решений. Используйте эту маленькую игру и другие апплеты и исходники как "словари", помогающие создавать свои игры. Поверьте, таким образом вы научитесь гораздо большему!

Случайное направление движения двух шаров

Как я упоминал во второй главе, в этой игре мы будем перемещать шары не только по оси x, но и по оси y. ля этого нам придется добавить в апплет вектор y_speed, а x_speed, если вы помните, уже имеется. Шар будет иметь переменную x_pos, которая будет меняться при каждом вызове move(), прибавляя значение x_speed (помните: x_speed может быть отрицательной), а также у шара будет переменная y_pos, меняющаяся путем прибавления y_speed. начение y_speed в нашей игре меняться не будет. Первый шар будет двигаться вверх (y_speed = -1), а второй - вниз (y_speed = 1). Да-да, будьте внимательны: в Java y-координата становится больше при движении вниз! Значение x_speed будет выбираться случайным образом каждый раз, когда шар покидает апплет.

Генератор случайных чисел
Для начала нам нужен этот самый генератор. Импортируем java.util.* в класс Ball и объявим следующую переменную:

    Random rnd = new Random ();

Теперь мы можем генерировать случайные целые числа, вызывая rnd.nextInt(). Поскольку нам нужны числа в диапазоне от -3 до +3, следует вычислить
Благодаря генератору у нас есть возможность задавать шару случайное направление движения. Переменная x_speed получает новое случайное значение, когда шар был подстрелен или ушел за пределы апплета.

Метод move() шара
Прежде всего мы должны объявить четыре экземпляра переменных (x_speed, y_speed, x_pos, y_pos) и инициализировать их в конструкторе объекта-шара. Всякий раз, когда вызывается метод move(), мы прибавляем значение x_speed к переменной x_pos и значение y_speed к переменной y_pos. Когда в шар попадает игрок, или он покидает апплет, мы инициализируем x_speed со случайным значением, а y_speed остается постоянной. Далее представлен код:

public void move()
{

    // Прибавляем значения y_speed и x_speed к позициям по осям у и x
    pos_x += x_speed;
    pos_y += y_speed;

    /* ызывем метод isOut() и проверяем, находится ли шар внутри апплета */
    isOut();

}

Метод isOut()
Он проверяет, покинул ли шар апплет. Если одно из условий истинно, шар возвращается в исходную точку, проигрывается аудиофайл, игрок теряет жизнь, а x_speed получает новое значение (случайным образом).
Проверки этого метода почти такие же, как в главе 2, с той разницей, что здесь нам приходится проверять четыре границы вместо двух. Однако техника та же, и вы должны сами попробовать во всем разобраться, а с помощью главвы 2 это не составит абсолютно никакой проблемы.

Выстрелы по шарам

Данная игра была первой, которую я программировал, и проблема, как стрелять по шарам кликами мыши доставила мне немало головной боли. Сейчас я объясню вам мое решение: наш мяч имеет координаты x и y. Метод mouseDown главного класса, вызывает метод userHit (int x_mouse, int y_mouse) и передает туда координаты той точки, где был произведен клик. После этого методу нужно решить, был ли подстрелен шар. Но как это сделать?

Явно недостаточно проверить, совпадают ли координаты шара с координатами клика, ведь это значит, что игроку придется попадать ровно в центр шара. А это практически невозможно с учетом того, что шар постоянно движется.

Ок, идея номер 2. Мы засчитываем попадание, если координаты mouseDown находятся в определенном диапазоне вокруг координат позиции шара. Конечно, координаты x и y должны быть в этом диапазоне одновременно (&& - тест). Таким образом, мы проверяем, попадает ли игрок в прямоугольник, описанный вокруг нашего шара (например, если радиус шара равен 10, то стороны прямоугольника будут равны 20 пикселям). Это было первое, что пришло мне в голову, но при попытке реализовать это метод не работал так, как задумывалось! Иногда срабатывало, иногда нет, даже если я лепил по шарику со стопроцентной точностью! Я до сих пор не понял, почему так получилось.

Но ничего, у меня в запасе оказалась еще одна идея. Я думаю, каждый слышал о векторах. В нашей игре их два: первый - это координаты x и y шара, а второй - координаты x и y клика мыши. Если мы высчитаем длину соединительного вектора и она окажется меньше радиуса - игрок попал в шар!
Итак, вычисляем соединительный вектор:

// Вычисление соединительного вектора
double x = maus_x - pos_x;
double y = maus_y - pos_y;

Теперь эксплуатируем теорему Пифагора (c = Math.sqrt a? + b?) для нахождения его длины, которая равна расстоянию между кликом мыши и позицией шара.

// Расчет длины
double distance = Math.sqrt ((x*x) + (y*y));

Последний шаг - это проверка, меньше ли найденная длина необходимого значения (например, радиуса шара). Лично я выбрал число 15, хотя радиус равен 10. Надо немного побаловать пользователя :).

// Если расстояние меньше 15, шар подстрелен
if (distance < 15)
{

    player.addScore (10* Math.abs(x_speed) + 10);
    return true;

}
else return false;

Вот теперь мы наконец можем стрелять по мячу, кликая по нему курсором мыши.

Объект player: подсчет очков и потеря жизней

Чтобы вести счет и отнимать жизни, мы добавляем методы looseLife() и addScore(int plus) в класс player. аждый раз, когда шар покидает поле битвы живым, isOut() вызывает player.looseLife() и игрок теряет одну жизнь. Если же игрок попадает в шар, метод userHit() вызывает player.addScore (10* Math.abs(x_speed) + 10) и добавляет очки к счету в объекте player (чем быстрее двигался шар, тем больше очков за него дается). Методы addScore и looseLife очень просты.
Однако есть одна важная вещь: объект player инициализируется в классе Main! Это означает, что ссылка на объект player должна быть также инициализирована в объекте ball, чтобы объект ball мог вызывать методы объекта player.
Это действительно важные действия, потому что часто случается, что более одного класса нуждается в ссылке на специальный объект. Это значит, что такой объект инициализируется в одном классе (я использовал класс Main для управления всеми игровыми объектами), а ссылки на него даются в других классах.

Меняем курсор мыши на перекрестье

В данной игре курсор мыши в виде крестика выглядит намного уместнее обычного курсора. Чтобы добыть и установить его, нам нужно добавить три строчки в код класса Main. Во-первых, экземпляр класса Cursor:

// Курсор в виде крестика
Cursor c;

Следующие строки добавляются в метод init():

// Генерируем перекрестный курсор
c = new Cursor (Cursor.CROSSHAIR_CURSOR);
// Устанавливаем его в качестве стандартного в апплете
this.setCursor (c);

Есть и другие виды курсоров, которые можно найти в Java API.

Старт игры двойным кликом и окончание при потере всех жизней

В основном все закончено, осталась только одна вещь: нам нужно, чтобы игра начиналась после двойного щелчка мышью по апплету, и не ранее. Преимущество такого способа не только в том, что игрок может сам решить, когда ему начинать игру, но и в том, что в играх, использующих клавиатуру, мы можем очень легко сфокусировать ее на апплете. И, конечно же, игра должна заканчиваться, когда игрок теряет последнюю из жизней.

Первым действием мы добавляем в класс Main переменную, принимающую булевы значения, которая называется isStoped. Если значение isStoped равно true, игра не идет, а если false, то идет. Нам нужна проверка в методе run класса Main, запущена ли игра (isStoped = false) и есть ли у игрока еще жизни. Если эти два условия выполняются, шары двигаются.

// метод run
while (true)
{

    if (player.getLives() >= 0 && !isStoped)
    {
      redball.move();
      blueball.move();
    }

    ...
}

Следующим действием мы добавляем вторую проверку - в метод paint(). Пока игрок теряет жизни, этот метод занимается тем, что рисует два шара, счет и количество оставшихся жизней. Если игра остановлена, он рисует информацию, что игра может быть начата двойным щелчком мыши. Если пользователь проиграл, paint() выводит результаты и информацию, как начать игру заново. Выглядит метод так:

// Метод paint()
public void paint (Graphics g)
{

    // Оставшиеся жизни игрока
    if (player.getLives() >= 0)
    {
      // Рисуем два шара, счет...

      ...

      // Если игра остановлена
      if (isStoped)
      {
        // Выводим информацию: "Start game with a double click"
      }
    }
    // Жизни кончились
    else if (player.getLiver() < 0)
    {
      // Выводим финальный счет, задаем isStopped значение true...
      // а подробности см. в исходном коде!
    }
}

Мы почти сделали это, но... Мы не можем переключаться между двумя состояниями игры (игра остановлена, игра запущена), потому что мы не можем контролировать значение переменной isStoped. Чтобы заполучить такой контроль, нужно добавить следующий код в метод mouseDown.

// Отслеживание кликов мыши
public void mouseDown (Event e, int x, int y)
{
    // Управление событиями мыши, когда игра запущена
    if (!isStoped)
    {
      // Проверка, было ли попадание в красный шар
      if (redball.userHit (x, y))
      {
        // проигрывание звукового файла
        hitnoise.play();

        // возвращение шара на позицию
        redball.ballWasHit ();
      }
      // Проверка, было ли попадание в синий шар
      if (blueball.userHit (x, y))
      {
        // проигрывание звукового файла
        hitnoise.play();

        // возвращение шара на позицию
        blueball.ballWasHit ();
      }
      else
      {
        // проигрывание звукового файла обычно выстрела
        shotnoise.play();
      }
    }
    // Управление событиями мыши, когда игра остановлена
    else if (isStoped && e.clickCount == 2)
    {
      // Сбрасываем все важные значения!
      isStoped = false;
      init ();
    }

    return true;
}

Вот так!

Итак, вы это сделали. Надеюсь, вы все уяснили, включая те вещи, которые не рассматривались подробно. Следующие главы расскажут вам о специфичных решениях проблем, с которыми я сталкивался.
Программируйте любые игры, чувствуя себя как дома :-), используйте мощь Java, которая делает возможным почти все, удачи вам в разработке и я верю, что смог помочь вам этим туториалом! Если у вас будут проблемы, туториалы, игры... шлите письма!
А теперь, как обычно, смотрите на работающую версию апплета, скачивайте исходник и, как говорится, have fun!

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

Следующая глава:
Искусственный интеллект для Pong-игры

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