Статьи

Разработка игр для Android — основной игровой цикл

После серии пока у вас есть понимание архитектуры игры. Даже если ненадолго, но мы знаем, что нам нужно принять ввод в той или иной форме, обновить внутреннее состояние игры и, наконец, отобразить его на экране, а также произвести некоторые звуки и / или вибрации. Кроме того, мы создали пример проекта Android для нашей первой игры. В этой статье мы собираемся обсудить и реализовать базовый игровой цикл.

Давайте будем проще. Проверьте следующую диаграмму.

Основной игровой цикл

Мы обрабатываем ввод, обновляем состояние наших внутренних объектов и отображаем текущее состояние. Обновление и визуализация сгруппированы логически. Они связаны друг с другом и, как правило, выполняются один за другим.

Все в Android происходит внутри Activity . Деятельность создаст представление. Вид — это то, где все происходит. Это место, где происходит касание, и полученное изображение отображается. Думайте об Деятельности как о таблице, содержащей лист бумаги ( представление ), позволяющий нам что-то нарисовать. Мы будем использовать наш карандаш, чтобы нарисовать что-то на бумаге. Это будет наше прикосновение, и фактическая химия происходит на бумаге, так что результат нашего взаимодействия с View создает изображение. То же самое с Активностью и Видом . Что-то вроде следующей диаграммы:

Android Game Loop

Давайте откроем DroidzActivity.java из нашего проекта. Мы видим линию

1
setContentView(R.layout.main);

Это не более чем назначает представление по умолчанию (R) действию при его создании. В нашем случае это происходит при запуске.

Давайте создадим новый вид, который мы будем использовать. View — это простой класс, который предоставляет нам обработку событий (например, onTouch) и видимое прямоугольное пространство для рисования. Самый простой способ — расширить собственный SurfaceView для Android. Мы также реализуем SurfaceHolder.Callback, чтобы получить доступ к изменениям поверхности, например, когда она разрушена или изменилась ориентация устройства.

MainGamePanel.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
package net.obviam.droidz;
 
import android.content.Context;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
public class MainGamePanel extends SurfaceView implements
  SurfaceHolder.Callback {
 
 public MainGamePanel(Context context) {
  super(context);
  // adding the callback (this) to the surface holder to intercept events
  getHolder().addCallback(this);
  // make the GamePanel focusable so it can handle events
  setFocusable(true);
 }
 
 @Override
 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
 }
 
 @Override
 public void surfaceCreated(SurfaceHolder holder) {
 }
 
 @Override
 public void surfaceDestroyed(SurfaceHolder holder) {
 }
 
 @Override
 public boolean onTouchEvent(MotionEvent event) {
  return super.onTouchEvent(event);
 }
 
 @Override
 protected void onDraw(Canvas canvas) {
 }
}

Приведенный выше код является простым классом, который переопределяет интересующие нас методы.
Ничего особенного, кроме строк 15 и 17.

1
getHolder().addCallback(this);

Эта строка устанавливает текущий класс ( MainGamePanel ) в качестве обработчика событий, происходящих на фактической поверхности.

1
setFocusable(true);

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

Будут использоваться все переопределенные методы (строка 20 и далее), но в настоящее время они остаются пустыми.

Давайте создадим нить, которая будет нашим реальным игровым циклом.

MainThread.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package net.obviam.droidz;
 
public class MainThread extends Thread {
 
 // flag to hold game state
 private boolean running;
 public void setRunning(boolean running) {
  this.running = running;
 }
 
 @Override
 public void run() {
  while (running) {
   // update game state
   // render state to the screen
  }
 }
}

Как вы можете видеть, это мало что значит. Он переопределяет метод run () и, в то время как флаг выполнения установлен в true, он выполняет бесконечный цикл.

В настоящее время поток не создан, поэтому давайте запустим его при загрузке экрана.
Давайте посмотрим на модифицированный класс MainGamePanel .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
package net.obviam.droidz;
 
import android.content.Context;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
 
public class MainGamePanel extends SurfaceView implements
  SurfaceHolder.Callback {
 
 private MainThread thread;
 
 public MainGamePanel(Context context) {
  super(context);
  getHolder().addCallback(this);
 
  // create the game loop thread
  thread = new MainThread();
 
  setFocusable(true);
 }
 
 @Override
 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
 }
 
 @Override
 public void surfaceCreated(SurfaceHolder holder) {
  thread.setRunning(true);
  thread.start();
 }
 
 @Override
 public void surfaceDestroyed(SurfaceHolder holder) {
  boolean retry = true;
  while (retry) {
   try {
    thread.join();
    retry = false;
   } catch (InterruptedException e) {
    // try again shutting down the thread
   }
  }
 }
 
 @Override
 public boolean onTouchEvent(MotionEvent event) {
  return super.onTouchEvent(event);
 }
 
 @Override
 protected void onDraw(Canvas canvas) {
 }
}

Мы добавили следующие строки:
Строка 12 объявляет поток как частный атрибут.

1
private MainThread thread;

В строке 19 мы создаем поток.

1
thread = new MainThread();

В методе surfaceCreated мы устанавливаем флаг выполнения в значение true и запускаем поток (строки 30 и 31). К тому времени, когда этот метод называется, поверхность уже создана, и игровой цикл может быть безопасно запущен.

Взгляните на метод surfaceDestroyed .

01
02
03
04
05
06
07
08
09
10
11
12
13
public void surfaceDestroyed(SurfaceHolder holder) {
 // tell the thread to shut down and wait for it to finish
 // this is a clean shutdown
 boolean retry = true;
 while (retry) {
  try {
   thread.join();
   retry = false;
  } catch (InterruptedException e) {
   // try again shutting down the thread
  }
 }
}

Этот метод вызывается непосредственно перед разрушением поверхности. Это не место для установки флага выполнения, но код, который мы ввели, гарантирует, что поток завершит работу корректно. Мы просто блокируем поток и ждем его смерти.

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

Добавить взаимодействие с экраном

Мы выйдем из приложения, когда коснемся нижней части экрана. Если мы коснемся его где-нибудь еще, мы просто запишем координаты.

В классе MainThread мы добавляем следующие строки:

1
2
3
4
5
6
7
8
private SurfaceHolder surfaceHolder;
private MainGamePanel gamePanel;
 
public MainThread(SurfaceHolder surfaceHolder, MainGamePanel gamePanel) {
 super();
 this.surfaceHolder = surfaceHolder;
 this.gamePanel = gamePanel;
}

Мы объявили переменные gamePanel и surfaceHolder и конструктор, принимающий экземпляры в качестве параметров.
Важно иметь их обоих, а не только gamePanel, так как нам нужно заблокировать поверхность, когда мы рисуем, и это можно сделать только через SurfaceHolder .

Измените строку в конструкторе MainGamePanel, который создает поток

1
thread = new MainThread(getHolder(), this);

Мы передаем текущий держатель и панель его новому конструктору, чтобы поток мог получить к ним доступ. Мы создадим метод обновления игры на игровой панели, и мы запустим его из цепочки, но в настоящее время просто оставим его как есть.

Добавьте константу TAG в класс MainThread . Каждый класс будет иметь свою собственную строковую константу с именем TAG . Значением константы будет имя класса, в котором она находится. Мы используем собственную платформу логирования Android, которая принимает два параметра. Первый — это тег, который представляет собой просто строку для определения источника сообщения журнала, а второй — это сообщение, которое мы хотим зарегистрировать. Хорошей практикой является использование имени класса для тега, поскольку это упрощает поиск журналов.

Примечание о регистрации

Чтобы открыть средство просмотра журнала, перейдите в Windows -> Показать представление -> Другое … и в диалоговом окне выберите Android -> LogCat

Показать представление -> LogCat

Теперь вы должны увидеть представление LogCat. Это не более чем консоль, где вы можете следить за журналом Android. Это отличный инструмент, поскольку вы можете фильтровать журналы, содержащие определенный текст, или журналы с определенным тегом, что весьма полезно.

Давайте вернемся к нашему коду. Класс MainThread.java выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package net.obviam.droidz;
 
import android.util.Log;
import android.view.SurfaceHolder;
 
public class MainThread extends Thread {
 
 private static final String TAG = MainThread.class.getSimpleName();
 
 private SurfaceHolder surfaceHolder;
 private MainGamePanel gamePanel;
 private boolean running;
 public void setRunning(boolean running) {
  this.running = running;
 }
 
 public MainThread(SurfaceHolder surfaceHolder, MainGamePanel gamePanel) {
  super();
  this.surfaceHolder = surfaceHolder;
  this.gamePanel = gamePanel;
 }
 
 @Override
 public void run() {
  long tickCount = 0L;
  Log.d(TAG, "Starting game loop");
  while (running) {
   tickCount++;
   // update game state
   // render state to the screen
  }
  Log.d(TAG, "Game loop executed " + tickCount + " times");
 }
}

В строке 08 мы определяем тег для регистрации.
В методе run () мы определяем tickCount, который увеличивается каждый раз, когда выполняется цикл while (игровой цикл).
Мы регистрируем результаты.

Вернемся к классу MainGamePanel.java, где мы изменили метод onTouchEvent, чтобы обрабатывать касания на экране.

01
02
03
04
05
06
07
08
09
10
11
public boolean onTouchEvent(MotionEvent event) {
 if (event.getAction() == MotionEvent.ACTION_DOWN) {
  if (event.getY() > getHeight() - 50) {
   thread.setRunning(false);
   ((Activity)getContext()).finish();
  } else {
   Log.d(TAG, "Coords: x=" + event.getX() + ",y=" + event.getY());
  }
 }
 return super.onTouchEvent(event);
}

В строке 02 мы проверяем, является ли событие на экране началом нажатого жеста ( MotionEvent.ACTION_DOWN ). Если это так, мы проверяем, произошло ли касание в нижней части экрана. То есть координата Y жеста находится в нижних 50 пикселях экрана. Если это так, мы устанавливаем состояние выполнения потока в false и вызываем finish () для основного действия, которое в основном выходит из приложения.

Примечание. Экран представляет собой прямоугольник с верхними левыми координатами в (0,0) и нижними правыми координатами в ( getWidth () , getHeight () ).

Я также изменил класс DroidzActivity.java, чтобы мы регистрировали его жизненный цикл.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package net.obviam.droidz;
 
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.Window;
import android.view.WindowManager;
 
public class DroidzActivity extends Activity {
    /** Called when the activity is first created. */
 
 private static final String TAG = DroidzActivity.class.getSimpleName();
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // requesting to turn the title OFF
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // making it full screen
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        // set our MainGamePanel as the View
        setContentView(new MainGamePanel(this));
        Log.d(TAG, "View added");
    }
 
 @Override
 protected void onDestroy() {
  Log.d(TAG, "Destroying...");
  super.onDestroy();
 }
 
 @Override
 protected void onStop() {
  Log.d(TAG, "Stopping...");
  super.onStop();
 }
}

Строка 20 делает дисплей полноэкранным.
Методы onDestroy () и onStop () были переопределены только для регистрации жизненного цикла действия.

Давайте запустим приложение, щелкнув правой кнопкой мыши по проекту и выбрав Run As -> Android application
Вы должны увидеть черный экран. Если вы щелкнете несколько раз в верхней части, а затем щелкните в нижней части экрана вашего эмулятора, приложение должно выйти.
На этом этапе стоит проверить логи.

LogCat

Выделенные строки являются наиболее интересными, так как если вы соответствуете, посмотрите журналы в коде, вы увидите точно порядок вызовов методов. Вы также должны увидеть, сколько раз выполнялся цикл while в потоке. Это очень большое число, но в следующий раз мы будем более внимательны к циклам, поскольку представим FPS и UPS. Это количество кадров в секунду и количество обновлений в секунду . Мы создадим игровой цикл, который будет рисовать что-то на экране, и он будет делать это столько раз в секунду, сколько мы укажем.

То, что мы сделали до сих пор:

  • Создать полноэкранное приложение
  • Иметь отдельный поток, управляющий приложением
  • Перехват основных жестов, таких как нажатие жестов
  • Завершение приложения любезно

Загрузите исходный код здесь .

Импортируйте его в затмение, и оно должно работать сразу.

Ссылка: Базовый цикл игры от нашего партнера по JCG Тамаса Яно из блога « Против зерна ».

Не забудьте проверить нашу новую Android игру ArkDroid (скриншоты ниже) . Ваш отзыв будет более чем полезным!
Статьи по Теме: