Давайте будем проще. Проверьте следующую диаграмму.
Основной игровой цикл |
Мы обрабатываем ввод, обновляем состояние наших внутренних объектов и отображаем текущее состояние. Обновление и визуализация сгруппированы логически. Они связаны друг с другом и, как правило, выполняются один за другим.
Все в 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 Введение
- Разработка игр для Android — Идея игры
- Разработка игр для Android — Создать проект
- Разработка игр для Android — базовая игровая архитектура
- Разработка игр для Android — Отображение изображений с Android
- Разработка игр для Android — перемещение изображений на экране
- Разработка игр для Android — The Game Loop
- Разработка игр для Android — Измерение FPS
- Разработка игр для Android — Sprite Animation
- Разработка игр для Android — Particle Explosion
- Разработка игр для Android — Дизайн игровых объектов — Стратегия
- Разработка игр для Android — Использование растровых шрифтов
- Разработка игр для Android — переход с Canvas на OpenGL ES
- Разработка игр для Android — отображение графических элементов (примитивов) с помощью OpenGL ES
- Разработка игр для Android — OpenGL Texture Mapping
- Разработка игр для Android — Дизайн игровых сущностей — Государственный паттерн
- Серия игр для Android