Статьи

Разработка игр для Android с помощью libgdx — Прототип за день, часть 1b

Создание игры и отображение мира

Чтобы отобразить мир на экране, нам нужно создать для него экран и сказать ему, чтобы он отображал мир. В libgdx есть вспомогательный класс Game и мы переписываем класс StarAssault подклассом класса Game предоставляемого libgdx.

О экранах

Игра может состоять из нескольких экранов. Даже наша игра будет иметь 3 основных экрана. Экран « Начать игру» , « Играть» и « Игра окончена» . Каждый экран связан с тем, что происходит на нем, и они не заботятся друг о друге. Например, экран « Начать игру» будет содержать пункты меню « Играть» и « Выйти» . У него есть два элемента (кнопки), и он обеспокоен обработкой щелчков / прикосновений к этим элементам. Он рендерит эти две кнопки неутомимо и, если нажать кнопку « Воспроизвести» , он уведомляет основную игру о загрузке экрана воспроизведения и избавлении от текущего экрана. Экран Play запустит нашу игру и обработает все, что касается игры. Как только состояние Game Over достигнуто, он сообщает основной игре о том, чтобы перейти к экрану Game Over , единственной целью которого является отображение высоких результатов и прослушивание щелчков по кнопке воспроизведения.

Давайте сделаем рефакторинг кода и пока создадим только главный экран для игры. Мы пропустим запуск и игру поверх экранов.

GameScreen.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
41
package net.obviam.starassault.screens;
 
import com.badlogic.gdx.Screen;
 
public class GameScreen implements Screen {
 
 @Override
 public void render(float delta) {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void resize(int width, int height) {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void show() {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void hide() {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void pause() {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void resume() {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void dispose() {
  // TODO Auto-generated method stub
 }
}

StarAssault.java станет очень простым.

01
02
03
04
05
06
07
08
09
10
11
12
13
package net.obviam.starassault;
 
import net.obviam.starassault.screens.GameScreen;
 
import com.badlogic.gdx.Game;
 
public class StarAssault extends Game {
 
 @Override
 public void create() {
  setScreen(new GameScreen());
 }
}

GameScreen реализует интерфейс Screen который очень похож на ApplicationListener но в него добавлено 2 важных метода.
show() — вызывается, когда основная игра делает этот экран активным
hide() — вызывается, когда в основной игре активен другой экран

StarAssault только один метод. GameScreen create() делает только активацию недавно GameScreen . Другими словами, он создает его, вызывает метод show() и впоследствии будет вызывать его метод render() каждый цикл.

GameScreen становится нашей целью для следующей части, так как именно там будет жить игра. Помните, что игровой цикл — это метод render() . Но чтобы что-то сделать, нам нужно сначала создать мир. Мир можно создать с помощью метода show() как у нас нет других экранов, которые могут прервать наш игровой процесс. В настоящее время GameScreen отображается только при запуске игры.

Мы добавим два члена в класс и реализуем метод render(float delta) .

01
02
03
04
05
06
07
08
09
10
11
private World world;
private WorldRenderer renderer;
 
/** Rest of methods ommited **/
 
@Override
public void render(float delta) {
 Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1);
 Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
 renderer.render();
}

Атрибут world — это экземпляр World который содержит блоки и Боба.
renderer — это класс, который рисует / рендерит мир на экране (я вскоре расскажу об этом).
render(float delta)
Давайте создадим класс WorldRenderer .
WorldRenderer.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
41
42
43
44
45
46
47
package net.obviam.starassault.view;
 
import net.obviam.starassault.model.Block;
import net.obviam.starassault.model.Bob;
import net.obviam.starassault.model.World;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Rectangle;
 
public class WorldRenderer {
 
 private World world;
 private OrthographicCamera cam;
 
 /** for debug rendering **/
 ShapeRenderer debugRenderer = new ShapeRenderer();
 
 public WorldRenderer(World world) {
  this.world = world;
  this.cam = new OrthographicCamera(10, 7);
  this.cam.position.set(5, 3.5f, 0);
  this.cam.update();
 }
 
 public void render() {
  // render blocks
  debugRenderer.setProjectionMatrix(cam.combined);
  debugRenderer.begin(ShapeType.Rectangle);
  for (Block block : world.getBlocks()) {
   Rectangle rect = block.getBounds();
   float x1 = block.getPosition().x + rect.x;
   float y1 = block.getPosition().y + rect.y;
   debugRenderer.setColor(new Color(1, 0, 0, 1));
   debugRenderer.rect(x1, y1, rect.width, rect.height);
  }
  // render Bob
  Bob bob = world.getBob();
  Rectangle rect = bob.getBounds();
  float x1 = bob.getPosition().x + rect.x;
  float y1 = bob.getPosition().y + rect.y;
  debugRenderer.setColor(new Color(0, 1, 0, 1));
  debugRenderer.rect(x1, y1, rect.width, rect.height);
  debugRenderer.end();
 }
}

WorldRenderer имеет только одну цель. Взять текущее состояние мира и отобразить его текущее состояние на экране. Он имеет единственный публичный метод render() который GameScreen основным циклом ( GameScreen ). Рендер должен иметь доступ к world поэтому мы передадим его, когда создадим экземпляр. Для первого шага мы будем визуализировать ограничивающие рамки элементов (блоков и Боба), чтобы увидеть, что мы имеем до сих пор. Рисование примитивов в OpenGL довольно утомительно, но libgdx поставляется с ShapeRenderer, который делает эту задачу очень простой.
Важные строки объяснены.

# 14 — Объявляет world как переменную-член.
№ 15 — Мы объявляем OrthographicCamera. Мы будем использовать эту камеру, чтобы «смотреть» на мир с точки зрения орфографии. В настоящее время мир очень маленький, и он умещается на одном экране, но когда у нас будет обширный уровень, и Боб перемещается по нему, нам придется перемещать камеру вслед за Бобом. Это аналог реальной камеры. Больше об орфографических проекциях можно найти здесь .
№ 18ShapeRenderer объявлен. Мы будем использовать это для рисования примитивов (прямоугольников) для сущностей. Это вспомогательный рендер, который может рисовать примитивы, такие как линии, прямоугольники, круги. Для всех, кто знаком с графикой на холсте, это должно быть легко.
# 20 — Конструктор, который принимает world в качестве параметра.
# 22 — Мы создаем камеру с областью просмотра 10 единиц в ширину и 7 единиц в высоту. Это означает, что заполнение экрана единичными блоками (width = height = 1) приведет к отображению 10 блоков по оси X и 7 по оси Y.
Важно: Это не зависит от разрешения. Если разрешение экрана составляет 480 × 320, это означает, что 480 пикселей представляют 10 единиц, поэтому поле будет иметь ширину 48 пикселей. Это также означает, что 320 пикселей представляют 7 единиц, поэтому поля на экране будут иметь высоту 45,7 пикселей. Это не будет идеальный квадрат. Это связано с соотношением сторон. Соотношение сторон в нашем случае составляет 10: 7.
# 23 — Эта линия позиционирует камеру, чтобы смотреть на середину комнаты. По умолчанию он смотрит на (0,0), который является углом комнаты. Камера (0,0) находится посередине, как и следовало ожидать от обычной камеры. На следующем рисунке показаны координаты мира и настройки камеры.

# 24 — Обновлены внутренние матрицы камеры. Метод обновления должен вызываться каждый раз при воздействии на камеру (перемещение, масштабирование, поворот и т. Д.). OpenGL спрятан красиво.
Метод render() :
# 29 — Мы применяем матрицу от камеры к визуализатору. Это необходимо, так как мы установили камеру и хотим, чтобы они были одинаковыми.
# 30 — Мы говорим визуализатору, что хотим нарисовать прямоугольники.
# 31 — Мы нарисуем блоки, чтобы перебрать все их в мире.
# 32 — # 34 — Извлечение координат ограничивающего прямоугольника каждого блока. OpenGL работает с вершинами (точками), поэтому для рисования прямоугольника необходимо знать координаты начальной точки и ширину. Обратите внимание, что мы работаем с координатами камеры, которые совпадают с мировыми координатами.
# 35 — Установите цвет прямоугольников на красный.
# 36 — Нарисуйте прямоугольник в x1, y1 с заданной width и height .
# 39 — # 44 — Мы делаем то же самое с Бобом, но на этот раз прямоугольник зеленый.
# 45 — Мы даем визуализатору знать, что мы закончили рисовать прямоугольники.

Нам нужно добавить renderer и world в GameScreen (основной цикл) и увидеть его в действии.
Измените GameScreen следующим образом:

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
package net.obviam.starassault.screens;
 
import net.obviam.starassault.model.World;
import net.obviam.starassault.view.WorldRenderer;
 
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL10;
 
public class GameScreen implements Screen {
 
 private World world;
 private WorldRenderer renderer;
 
 @Override
 public void show() {
  world = new World();
  renderer = new WorldRenderer(world);
 }
 
 @Override
 public void render(float delta) {
  Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1);
  Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
  renderer.render();
 }
 
 /** ... rest of method stubs omitted ... **/
 
}

Метод render(float delta) имеет 3 строки. Первые 2 строки очищают экран с черным, а 3-я строка просто вызывает метод render() рендерера.
World и WorldRenderer создаются при отображении экрана.

Чтобы протестировать его как на рабочем столе, так и на Android, мы должны создать пусковые установки для обеих платформ. Создание рабочего стола и запуска Android

Мы создали еще 2 проекта в начале.
star-assault-desktop и star-assault-android , последний является проектом Android.
Для настольного проекта это очень просто. Нам нужно создать класс с main методом, который создает приложение, предоставляемое libgdx.
Создайте класс StarAssaultDesktop.java в настольном проекте.

1
2
3
4
5
6
7
8
9
package net.obviam.starassault;
 
import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
 
public class StarAssaultDesktop {
 public static void main(String[] args) {
  new LwjglApplication(new StarAssault(), "Star Assault", 480, 320, true);
 }
}

Это оно. Строка № 7 , где все происходит. Он создает новое приложение LwjglApplication передавая новый экземпляр StarAssault который является реализацией Game . 2-й и 3-й параметры определяют размер окна. Я выбрал 480 × 320, потому что это разрешение поддерживается на многих телефонах Android, и я хочу напомнить его на рабочем столе. Последний параметр указывает libgdx использовать OpenGL ES 2.
Запуск приложения как обычной Java-программы должен привести к следующему результату:

Если вы получаете какие-то ошибки, отследите и убедитесь, что установка правильная и все шаги выполнены, включая проверку gdx.jar на вкладке экспорта в свойствах проекта star-guard -> Build Path.


Версия для Android

В проекте star-assault-android есть единственный класс Java, называемый StarAssaultActivity .
Измените это на:
StarAssaultActivity.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
package net.obviam.starassault;
 
import android.os.Bundle;
 
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
 
public class StarAssaultActivity extends AndroidApplication {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
  AndroidApplicationConfiguration config = new AndroidApplicationConfiguration();
  config.useAccelerometer = false;
  config.useCompass = false;
  config.useWakelock = true;
  config.useGL20 = true;
  initialize(new StarAssault(), config);
    }
}

Обратите внимание, что новое действие расширяет AndroidApplication .
В строке # 13 создается объект AndroidApplicationConfiguration . Мы можем установить все типы конфигураций, касающихся платформы Android. Они говорят сами за себя, но учтите, что если мы хотим использовать Wakelock , файл AndroidManifest.xml также необходимо изменить. Это требует разрешения от Android, чтобы оставить устройство включенным и предотвратить затемнение экрана, если мы не касаемся его.
Добавьте следующую строку в файл AndroidManifest.xml где-то внутри тегов <manifest> .

1
<uses-permission android:name="android.permission.WAKE_LOCK"/>

Также в строке № 17 мы говорим Android использовать OpenGL ES 2. Это означает, что мы сможем протестировать его только на устройстве, так как эмулятор не поддерживает OpenGL ES 2. В случае возникновения проблем с ним, установите значение false ,
Строка № 18 инициализирует приложение Android и запускает его.
Когда устройство подключено к затмению, оно сразу развертывается, и ниже вы можете увидеть фотографию приложения, запущенного на нексусе. Это выглядит идентично настольной версии.

Шаблон MVC

Впечатляет, как далеко мы зашли за такое короткое время. Обратите внимание на использование шаблона MVC. Это очень эффективно и просто. Модели — это объекты, которые мы хотим отобразить. Представление — рендерер. Представление рисует модели на экране. Теперь нам нужно взаимодействовать с сущностями (особенно Бобом), и мы также представим некоторые контроллеры.
Чтобы узнать больше о паттерне MVC, посмотрите мою другую статью или поищите ее в сети. Это очень полезно.

Добавление изображений

Пока все хорошо, но мы определенно хотим использовать правильную графику. Сила MVC пригодится, и мы изменим рендер, чтобы он рисовал изображения вместо прямоугольников.
В OpenGL отображение изображения является довольно сложным процессом. Сначала его нужно загрузить, превратить в текстуру, а затем сопоставить с поверхностью, которая описывается некоторой геометрией. libgdx делает это чрезвычайно легко. Превратить изображение с диска в текстуру — это один слой.
Мы будем использовать 2 изображения, следовательно, 2 текстуры. Одна текстура для Боба и одна для блоков. Я создал два изображения, блок и Боб. Боб — подражатель главы Звездной Стражи. Это простые файлы png, и я скопирую их в каталог assets/images . У меня есть два изображения: block.png и bob_01.png . В конце концов Боб станет анимированным персонажем, поэтому я добавил к нему номер (панорамирование на будущее).
Сначала давайте немного WorldRenderer , а именно, чтобы извлечь рисунок прямоугольников в отдельный метод, поскольку мы будем использовать его для целей отладки.
Нам нужно будет загрузить текстуры и отобразить их в соответствии с экраном.
Взгляните на новый WorldRenderer.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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
package net.obviam.starassault.view;
 
import net.obviam.starassault.model.Block;
import net.obviam.starassault.model.Bob;
import net.obviam.starassault.model.World;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType;
import com.badlogic.gdx.math.Rectangle;
 
public class WorldRenderer {
 
 private static final float CAMERA_WIDTH = 10f;
 private static final float CAMERA_HEIGHT = 7f;
 
 private World world;
 private OrthographicCamera cam;
 
 /** for debug rendering **/
 ShapeRenderer debugRenderer = new ShapeRenderer();
 
 /** Textures **/
 private Texture bobTexture;
 private Texture blockTexture;
 
 private SpriteBatch spriteBatch;
 private boolean debug = false;
 private int width;
 private int height;
 private float ppuX; // pixels per unit on the X axis
 private float ppuY; // pixels per unit on the Y axis
 public void setSize (int w, int h) {
  this.width = w;
  this.height = h;
  ppuX = (float)width / CAMERA_WIDTH;
  ppuY = (float)height / CAMERA_HEIGHT;
 }
 
 public WorldRenderer(World world, boolean debug) {
  this.world = world;
  this.cam = new OrthographicCamera(CAMERA_WIDTH, CAMERA_HEIGHT);
  this.cam.position.set(CAMERA_WIDTH / 2f, CAMERA_HEIGHT / 2f, 0);
  this.cam.update();
  this.debug = debug;
  spriteBatch = new SpriteBatch();
  loadTextures();
 }
 
 private void loadTextures() {
  bobTexture = new  Texture(Gdx.files.internal("images/bob_01.png"));
  blockTexture = new Texture(Gdx.files.internal("images/block.png"));
 }
 
 public void render() {
  spriteBatch.begin();
   drawBlocks();
   drawBob();
  spriteBatch.end();
  if (debug)
   drawDebug();
 }
 
 private void drawBlocks() {
  for (Block block : world.getBlocks()) {
   spriteBatch.draw(blockTexture, block.getPosition().x * ppuX, block.getPosition().y * ppuY, Block.SIZE * ppuX, Block.SIZE * ppuY);
  }
 }
 
 private void drawBob() {
  Bob bob = world.getBob();
  spriteBatch.draw(bobTexture, bob.getPosition().x * ppuX, bob.getPosition().y * ppuY, Bob.SIZE * ppuX, Bob.SIZE * ppuY);
 }
 
 private void drawDebug() {
  // render blocks
  debugRenderer.setProjectionMatrix(cam.combined);
  debugRenderer.begin(ShapeType.Rectangle);
  for (Block block : world.getBlocks()) {
   Rectangle rect = block.getBounds();
   float x1 = block.getPosition().x + rect.x;
   float y1 = block.getPosition().y + rect.y;
   debugRenderer.setColor(new Color(1, 0, 0, 1));
   debugRenderer.rect(x1, y1, rect.width, rect.height);
  }
  // render Bob
  Bob bob = world.getBob();
  Rectangle rect = bob.getBounds();
  float x1 = bob.getPosition().x + rect.x;
  float y1 = bob.getPosition().y + rect.y;
  debugRenderer.setColor(new Color(0, 1, 0, 1));
  debugRenderer.rect(x1, y1, rect.width, rect.height);
  debugRenderer.end();
 }
}

Я укажу на важные строки:
# 17 и # 18 — Объявлены константы для размеров области просмотра. Используется для камеры.
# 27 и # 28 — Объявите 2 текстуры, которые будут использоваться для Боба и блоков.
# 30SpriteBatch объявлен. SpriteBatch заботится обо всем отображении текстур, отображении и так далее для нас.
# 31 — Это атрибут, установленный в конструкторе, чтобы знать, нужно ли нам отображать экран отладки тоже или нет. Помните, что отладочный рендеринг просто отображает блоки для элементов игры.
# 32 — # 35 — эти переменные необходимы для правильного отображения элементов. width и height содержат размер экрана в пикселях и передаются из операционной системы на шаге resize . ppuX и ppuY — это количество пикселей на единицу.
Поскольку мы установили для камеры порт просмотра 10 × 7 в мировых координатах (то есть мы можем отобразить 10 блоков по горизонтали и 7 блоков по вертикали), и мы имеем дело с пикселями в конечном результате, нам необходимо сопоставить эти значения с фактическими пиксельные координаты. Мы решили работать в разрешении 480 × 320. Это означает, что 480 пикселей по горизонтали эквивалентны 10 единицам, то есть единица будет состоять из 48 пикселей на экране.
Если мы попытаемся использовать ту же единицу для высоты (48 пикселей), мы получим 336 пикселей (48 * 7 = 336). Но у нас есть только 320 пикселей, и мы хотим показать всю высоту 7 блоков. Делая то же самое для вертикальной части, мы получаем, что 1 единица по вертикали будет 320/7 = 45,71 пикселей. Нам нужно немного исказить каждое изображение, чтобы оно вписалось в наш мир.
Это прекрасно, и OpenGL делает это очень легко. Это происходит, когда мы меняем соотношение сторон на нашем телевизоре, и иногда изображение удлиняется или сжимается, чтобы уместить все на экране, или мы просто выбираем вариант обрезания изображения, но сохраняем соотношение сторон.
Примечание: для этого мы используем float , даже если разрешение экрана связано с целыми числами, OpenGL предпочитает float, и мы тоже. OpenGL определит размеры и место для размещения пикселей.
# 36 — Метод setSize (int w, int h) будет вызываться каждый раз при изменении размера экрана, и он просто (пере) вычисляет единицы измерения в пикселях.
# 43 — Конструктор немного изменился, но он делает очень важные вещи. Он создает экземпляр SpriteBatch и загружает текстуры (строка № 50 ).
# 53loadTextures() делает то, что говорит: загружает текстуры. Посмотрите, как это невероятно просто. Чтобы создать текстуру, нам нужно передать обработчик файла, и он создаст из нее текстуру. Обработчики файлов в libgdx очень полезны, так как мы не различаем Android или десктоп, мы просто указываем, что хотим использовать внутренний файл и он знает, как его загрузить. Обратите внимание, что для пути мы пропустили assets потому что assets используются в качестве исходного каталога, то есть все из этого каталога копируется в корень окончательного пакета. Таким образом, assets действуют как корневой каталог.
# 58 — новый метод render() содержит всего несколько строк.
# 59 и # 62 — заключить блок рисования / сеанс SpriteBatch . Каждый раз, когда мы хотим визуализировать изображения в OpenGL через SpriteBatch мы должны вызывать begin() , рисовать наши вещи и end() когда мы закончим. Это важно сделать, иначе это не сработает. Вы можете прочитать больше о SpriteBatch здесь .
# 60 и # 61 — просто вызовите 2 метода, чтобы отобразить сначала блоки, а затем Боба.
# 63 & # 64 — если включена debug , вызовите метод для визуализации блоков. Метод drawDebug был подробно описан ранее.
# 67 — # 76 — методы drawBlocks и drawBob похожи. Каждый метод вызывает метод spriteBatch с текстурой. Это важно понимать.
Первый параметр — это текстура (изображение, загруженное с диска).
Второй и третий параметры сообщают spriteBatch где отображать изображение. Обратите внимание, что мы используем преобразование координат из мировых координат в экранные координаты. Вот где используются ppuX и ppuY . Вы можете сделать расчеты вручную и посмотреть, где изображения отображаются. SpriteBatch по умолчанию использует систему координат с началом координат (0, 0) в левом нижнем углу.

Вот и все. Просто убедитесь, что вы GameScreen класс GameScreen так, чтобы resize GameScreen для рендерера, а также для установки debug рендеринга в значение true .
Модифицированные биты GameScreen

01
02
03
04
05
06
07
08
09
10
/** ... omitted ... **/
 public void show() {
  world = new World();
  renderer = new WorldRenderer(world, true);
 }
 
 public void resize(int width, int height) {
  renderer.setSize(width, height);
 }
/** ... omitted ... **/

Запуск приложения должен дать следующий результат:
без отладки

и с отладочным рендерингом

Большой! Попробуйте на Android тоже и посмотрите, как это выглядит.

Обработка ввода — на рабочем столе и Android

Мы прошли долгий путь, но пока мир статичен и ничего интересного не происходит. Чтобы сделать это игрой, нам нужно добавить обработку ввода, перехватывать клавиши и касания и создавать на их основе некоторые действия.
Схема управления на рабочем столе очень проста. Клавиши со стрелками переместят Боба влево и вправо, z заставит Боба прыгнуть, а x выстрелит из оружия. На Android у нас будет другой подход. Мы назначим некоторые кнопки для этих функций и положим их на экран, и, коснувшись соответствующих областей, мы рассмотрим одну из нажатых клавиш.

Чтобы следовать шаблону MVC , мы отделим класс, который управляет Бобом и остальным миром, от классов модели и представления. Создайте пакет net.obviam.starassault.controller и все контроллеры перейдут туда.
Для начала мы будем управлять Бобом нажатием клавиш. Чтобы играть в игру, мы должны отслеживать состояние 4 клавиш: двигаться влево, двигаться вправо, прыгать и стрелять. Поскольку мы будем использовать 2 типа ввода (клавиатура и сенсорный экран), фактические события должны поступать в процессор, который может инициировать действия.
Каждое действие инициируется событием.
Действие «Влево» запускается событием, когда нажимается клавиша со стрелкой влево или касаются определенной области экрана.
Действие прыжка запускается при нажатии клавиши z и т. Д.
Давайте создадим очень простой контроллер под названием WorldController .
WorldController.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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
package net.obviam.starassault.controller;
 
import java.util.HashMap;
import java.util.Map;
import net.obviam.starassault.model.Bob;
import net.obviam.starassault.model.Bob.State;
import net.obviam.starassault.model.World;
 
public class WorldController {
 
 enum Keys {
  LEFT, RIGHT, JUMP, FIRE
 }
 
 private World  world;
 private Bob  bob;
 
 static Map<Keys, Boolean> keys = new HashMap<WorldController.Keys, Boolean>();
 static {
  keys.put(Keys.LEFT, false);
  keys.put(Keys.RIGHT, false);
  keys.put(Keys.JUMP, false);
  keys.put(Keys.FIRE, false);
 };
 
 public WorldController(World world) {
  this.world = world;
  this.bob = world.getBob();
 }
 
 // ** Key presses and touches **************** //
 
 public void leftPressed() {
  keys.get(keys.put(Keys.LEFT, true));
 }
 
 public void rightPressed() {
  keys.get(keys.put(Keys.RIGHT, true));
 }
 
 public void jumpPressed() {
  keys.get(keys.put(Keys.JUMP, true));
 }
 
 public void firePressed() {
  keys.get(keys.put(Keys.FIRE, false));
 }
 
 public void leftReleased() {
  keys.get(keys.put(Keys.LEFT, false));
 }
 
 public void rightReleased() {
  keys.get(keys.put(Keys.RIGHT, false));
 }
 
 public void jumpReleased() {
  keys.get(keys.put(Keys.JUMP, false));
 }
 
 public void fireReleased() {
  keys.get(keys.put(Keys.FIRE, false));
 }
 
 /** The main update method **/
 public void update(float delta) {
  processInput();
  bob.update(delta);
 }
 
 /** Change Bob's state and parameters based on input controls **/
 private void processInput() {
  if (keys.get(Keys.LEFT)) {
   // left is pressed
   bob.setFacingLeft(true);
   bob.setState(State.WALKING);
   bob.getVelocity().x = -Bob.SPEED;
  }
  if (keys.get(Keys.RIGHT)) {
   // left is pressed
   bob.setFacingLeft(false);
   bob.setState(State.WALKING);
   bob.getVelocity().x = Bob.SPEED;
  }
  // need to check if both or none direction are pressed, then Bob is idle
  if ((keys.get(Keys.LEFT) && keys.get(Keys.RIGHT)) ||
    (!keys.get(Keys.LEFT) && !(keys.get(Keys.RIGHT)))) {
   bob.setState(State.IDLE);
   // acceleration is 0 on the x
   bob.getAcceleration().x = 0;
   // horizontal speed is 0
   bob.getVelocity().x = 0;
  }
 }
}

# 11 — # 13 — определить enum для действий, которые Боб будет выполнять. Каждое нажатие / касание может вызвать одно действие.
№ 15 — объявить World который находится в игре. Мы будем контролировать сущности, найденные в мире.
# 16 — объявить Bob как частного участника, и это всего лишь ссылка на Bob в игровом мире, но он нам понадобится, так как ссылаться на него проще, чем извлекать его каждый раз, когда он нам нужен.
# 18 — # 24 — это статический HashMap ключей и их статусов. Если клавиша нажата, это true , иначе false . Статически инициализирован. Эта карта будет использоваться в методе update контроллера, чтобы понять, что делать с Бобом.
# 26 — Это конструктор, который принимает World в качестве параметра и также получает ссылку на Боба.
# 33 — # 63 — Эти методы являются простыми обратными вызовами, которые вызываются всякий раз, когда нажимается кнопка действия или происходит касание назначенной области. Эти методы вызываются из любого ввода, который мы используем. Они просто устанавливают значение соответствующих нажатых клавиш на карте. Как видите, контроллер также является конечным автоматом, и его состояние задается картой keys .
# 66 — # 69 — метод update который вызывается каждый цикл основного цикла. в настоящее время он делает 2 вещи: 1 — обрабатывает ввод и 2 — обновляет Боба. У Боба есть специальный метод update который мы увидим позже.
# 72 — # 92 — метод processInput опрашивает карту keys для ключей и соответственно устанавливает значения для Боба. Например, строки # 73 — # 78 проверяют, нажата ли клавиша для движения влево, и если да, то устанавливают State.WALKING сторону для Боба влево, его состояние в State.WALKING и его скорость в значение скорости Боба, но с отрицательным знак. Знак есть потому, что на экране слева отрицательное направление (начало координат находится внизу слева и указывает вправо).
То же самое за право. Есть несколько дополнительных проверок, нажаты ли обе клавиши или нет, и в этом случае Боб становится State.IDLE и его горизонтальная скорость будет равна 0 .

Посмотрим, что изменилось в Bob.java .

1
2
3
4
5
6
7
8
9
public static final float SPEED = 4f; // unit per second
 
public void setState(State newState) {
 this.state = newState;
}
 
public void update(float delta) {
 position.add(velocity.tmp().mul(delta));
}

Просто изменил константу SPEED до 4 единиц (блоков) в секунду.
Также добавлен метод setState потому что я забыл его раньше.
Наиболее интересным является недавно приобретенный метод update(float delta) , который вызывается из WorldController . Этот метод просто обновляет позицию Боба в зависимости от его скорости. Для простоты мы делаем только это без проверки его состояния и потому, что контроллер заботится о том, чтобы установить скорость для Боба в соответствии с его лицом и состоянием. Здесь мы используем векторную математику, и libgdx очень помогает.
Мы просто добавляем пройденное расстояние в delta секундах к текущей position Боба. Мы используем velocity.tmp() потому что tmp() создает новый объект с тем же значением, что и velocity и мы умножаем значение этого объекта на истекшее время. В Java мы должны быть осторожны с тем, как мы используем ссылки, поскольку скорость и положение являются объектами Vector2 . Подробнее о векторах здесь http://en.wikipedia.org/wiki/Euclidean_vector .

У нас есть почти все, нам просто нужно называть правильные события, когда они происходят. libgdx имеет процессор ввода, который имеет несколько методов обратного вызова. Поскольку мы используем GameScreen в качестве игровой поверхности, имеет смысл использовать его и в качестве обработчика ввода. Для этого GameScreen будет реализовывать libgdx InputProcessor .
Новый GameScreen.java

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package net.obviam.starassault.screens;
 
import net.obviam.starassault.controller.WorldController;
import net.obviam.starassault.model.World;
import net.obviam.starassault.view.WorldRenderer;
 
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL10;
 
public class GameScreen implements Screen, InputProcessor {
 
 private World    world;
 private WorldRenderer  renderer;
 private WorldController controller;
 
 private int width, height;
 
 @Override
 public void show() {
  world = new World();
  renderer = new WorldRenderer(world, false);
  controller = new WorldController(world);
  Gdx.input.setInputProcessor(this);
 }
 
 @Override
 public void render(float delta) {
  Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1);
  Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
 
  controller.update(delta);
  renderer.render();
 }
 
 @Override
 public void resize(int width, int height) {
  renderer.setSize(width, height);
  this.width = width;
  this.height = height;
 }
 
 @Override
 public void hide() {
  Gdx.input.setInputProcessor(null);
 }
 
 @Override
 public void pause() {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void resume() {
  // TODO Auto-generated method stub
 }
 
 @Override
 public void dispose() {
  Gdx.input.setInputProcessor(null);
 }
 
 // * InputProcessor methods ***************************//
 
 @Override
 public boolean keyDown(int keycode) {
  if (keycode == Keys.LEFT)
   controller.leftPressed();
  if (keycode == Keys.RIGHT)
   controller.rightPressed();
  if (keycode == Keys.Z)
   controller.jumpPressed();
  if (keycode == Keys.X)
   controller.firePressed();
  return true;
 }
 
 @Override
 public boolean keyUp(int keycode) {
  if (keycode == Keys.LEFT)
   controller.leftReleased();
  if (keycode == Keys.RIGHT)
   controller.rightReleased();
  if (keycode == Keys.Z)
   controller.jumpReleased();
  if (keycode == Keys.X)
   controller.fireReleased();
  return true;
 }
 
 @Override
 public boolean keyTyped(char character) {
  // TODO Auto-generated method stub
  return false;
 }
 
 @Override
 public boolean touchDown(int x, int y, int pointer, int button) {
  if (x < width / 2 && y > height / 2) {
   controller.leftPressed();
  }
  if (x > width / 2 && y > height / 2) {
   controller.rightPressed();
  }
  return true;
 }
 
 @Override
 public boolean touchUp(int x, int y, int pointer, int button) {
  if (x < width / 2 && y > height / 2) {
   controller.leftReleased();
  }
  if (x > width / 2 && y > height / 2) {
   controller.rightReleased();
  }
  return true;
 }
 
 @Override
 public boolean touchDragged(int x, int y, int pointer) {
  // TODO Auto-generated method stub
  return false;
 }
 
 @Override
 public boolean touchMoved(int x, int y) {
  // TODO Auto-generated method stub
  return false;
 }
 
 @Override
 public boolean scrolled(int amount) {
  // TODO Auto-generated method stub
  return false;
 }
}

Перемены:
# 13 — класс реализует InputProcessor
# 19 — ширина и высота экрана, используемого сенсорными событиями Android.
# 25 — создать экземпляр WorldController с миром.
# 26 — установить этот экран в качестве текущего процессора ввода для приложения. libgdx рассматривает это как глобальный процессор ввода, поэтому каждый экран должен устанавливать разные, если они не разделяют одно и то же. В этом случае сам экран обрабатывает ввод.
# 47 и # 62 — мы устанавливаем активный глобальный процессор ввода в null только для очистки.
# 68 — метод keyDown(int keycode) запускается при каждом нажатии клавиши на физической клавиатуре. Параметр keycode — это значение нажатой клавиши, и таким образом мы можем опрашивать его, и, если это желаемая клавиша, что-то сделать. Это именно то, что происходит. Основываясь на ключах, которые мы хотим, мы передаем событие контроллеру. Метод также возвращает true чтобы процессор ввода знал, что ввод был обработан.
# 81keyUp является точной инверсией метода keyDown . Когда ключ освобождается, он просто делегируется WorldController .
# 111 — # 118 — это то, где это становится интересным. Это происходит только на сенсорных экранах, и координаты передаются вместе с указателем и кнопкой. Указатель предназначен для мультитач и представляет идентификатор касания, которое он захватывает.
Элементы управления очень просты и сделаны только для простых демонстрационных целей. Экран делится на 4, и если касание падает до нижнего левого квадранта, он обрабатывается как триггер движения влево и передает в controller то же событие, что и рабочий стол.
Точно то же самое для TouchUp.

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

Запуск приложения как на рабочем столе, так и на Android покажет элементы управления. На рабочем столе клавиши со стрелками и на Android при прикосновении к нижним углам экрана будет двигаться Боб.
На рабочем столе вы заметите, что использование мыши для имитации прикосновений также будет работать. Это потому, что touchXXX также обрабатывает ввод мыши на рабочем столе. Чтобы исправить это, добавьте следующую строку в начало методов touchDown и touchUp :

1
2
if (!Gdx.app.getType().equals(ApplicationType.Android))
 return false;

Возвращается, falseесли приложение не является Android и не выполняет остальную часть метода. Помните, что это falseозначает, что ввод не был обработан.

Как мы видим, Боб переехал.

Краткий обзор

До сих пор мы довольно много рассказывали о разработке игр, и нам уже есть, что показать.
Постепенно мы внедрили рабочие части в наше приложение и шаг за шагом мы чего-то достигли.

Нам еще нужно добавить:

  • Взаимодействие с землей (столкновение блоков, прыжок)
  • Анимация
  • Большой уровень и камера, чтобы следовать за Бобом
  • Враги и пистолет, чтобы взорвать их
  • Звуки
  • Изысканные элементы управления и тонкая настройка
  • Больше экранов для игры окончено
  • Больше веселья с libgdx

Убедитесь, что вы ознакомились с частью 2 (которая все еще выполняется) , чтобы отметить вышеупомянутый список. Но сделайте это сами, во что бы то ни стало, и любые отзывы очень ценятся.

Также ознакомьтесь с libgdx и его замечательным сообществом . Исходный код этого проекта можно найти здесь: https://github.com/obviam/star-assault.

Чтобы проверить это с помощью git:
git clone git@github.com:obviam/star-assault.git

Вы также можете скачать его в виде zip-файла .

Проверьте следующую часть этого урока здесь .

Ссылка: Начало работы в разработке игр для Android с помощью libgdx — создайте рабочий прототип за день — учебное пособие, часть 1, от нашего партнера JCG Impaler в блоге Against the Grain .