Статьи

Сборка игр с использованием MVC Pattern — Учебное пособие и введение

Одним из полезных архитектурных паттернов в разработке игр является паттерн MVC (модель-представление-контроллер).

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

Следующая диаграмма является простейшим логическим представлением концепции контроллера представления модели.

Модель Модель-Вид-Контроллер

Пример использования

В примере игры, где игрок управляет роботом, может произойти следующее:

  • 1 — Пользователь щелкает / нажимает где-то на экране.
  • 2Контроллер обрабатывает нажатие / нажатие и преобразует событие в соответствующее действие. Например, если местность занята противником, создается действие атаки, если это пустая местность, то создается действие перемещения и, наконец, если место, где пользователь коснулся, занято препятствием, ничего не делать.
  • 3Контроллер соответствующим образом обновляет состояние робота ( модели ). Если действие перемещения было создано, то оно меняет позицию, если атака, то она стреляет.
  • 4 — Средство визуализации ( представление ) получает уведомление об изменениях состояния и отображает текущее состояние мира.

Все это означает, что модели (роботы) ничего не знают о том, как нарисовать себя или как изменить свое состояние (положение, очки жизни). Они тупые сущности. В Java их также называют POJO (простые старые объекты Java).

Контроллер отвечает за изменение состояния моделей и уведомляет об этом средство визуализации.

Рендерер должен иметь ссылку на модели (роботы и любые другие объекты) и их состояние, чтобы нарисовать их.
Из типичной игровой архитектуры мы знаем, что основной цикл действует как суперконтроллер, который обновляет состояния и затем выводит объекты на экран много раз в секунду. Мы можем поместить все обновления и рендеринг в основной цикл вместе с роботами, но это было бы грязно.
Давайте определим различные аспекты (проблемы) наших игр.

Модели
  • Дроид, управляемый игроком
  • Арена, на которой может двигаться дроид
  • Некоторые препятствия
  • Некоторые враги, чтобы стрелять в
Контроллеры
  • Основной цикл и обработчик ввода
  • Контроллер для обработки ввода игрока
  • Контроллер для выполнения действий над роботом игрока (перемещение, атака)
Виды
  • World Renderer — рендеринг объектов на экран

Создание проекта

Для простоты я выбрал апплет на этот раз и постараюсь сделать его очень коротким. Проект имеет следующую структуру:

MVC — Структура проекта

Файл Droids.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
package net.obviam.droids;
 
import java.applet.Applet;
import java.awt.Color;
import java.awt.Event;
import java.awt.Graphics;
import java.awt.image.BufferedImage;
 
public class Droids extends Applet implements Runnable {
 
 private static final long serialVersionUID = -2472397668493332423L;
 
 public void start() {
  new Thread(this).start();
 }
 
 public void run() {
 
  setSize(480, 320); // For AppletViewer, remove later.
 
  // Set up the graphics stuff, double-buffering.
  BufferedImage screen = new BufferedImage(480, 320, BufferedImage.TYPE_INT_RGB);
  Graphics g = screen.getGraphics();
  Graphics appletGraphics = getGraphics();
 
  long delta = 0l;
 
  // Game loop.
  while (true) {
   long lastTime = System.nanoTime();
 
   g.setColor(Color.black);
   g.fillRect(0, 0, 480, 320);
 
   // Draw the entire results on the screen.
   appletGraphics.drawImage(screen, 0, 0, null);
 
   // Lock the frame rate
   delta = System.nanoTime() - lastTime;
   if (delta < 20000000L) {
    try {
     Thread.sleep((20000000L - delta) / 1000000L);
    } catch (Exception e) {
     // It's an interrupted exception, and nobody cares
    }
   }
   if (!isActive()) {
    return;
   }
  }
 }
 
 public boolean handleEvent(Event e) {
  return false;
 }
}

Запуск вышеприведенного кода в виде апплета не делает ничего, кроме как настроить основной цикл и закрасить экран в черный цвет.
В структуре 3 пакета, и соответствующие компоненты пойдут туда.

net.obviam.droids.model будет содержать все модели
net.obviam.droids.view будет содержать все рендеры
net.obviam.droids.controller будет содержать все контроллеры

Создание моделей

Дроид

Droid.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.droids.model;
 
public class Droid {
 
 private float x;
 private float y;
 private float speed = 2f;
 private float rotation = 0f;
 private float damage = 2f;
 
 public float getX() {
  return x;
 }
 public void setX(float x) {
  this.x = x;
 }
 public float getY() {
  return y;
 }
 public void setY(float y) {
  this.y = y;
 }
 public float getSpeed() {
  return speed;
 }
 public void setSpeed(float speed) {
  this.speed = speed;
 }
 public float getRotation() {
  return rotation;
 }
 public void setRotation(float rotation) {
  this.rotation = rotation;
 }
 public float getDamage() {
  return damage;
 }
 public void setDamage(float damage) {
  this.damage = damage;
 }
}

Это простой объект Java без каких-либо знаний об окружающем мире. Имеет позицию, вращение, скорость и урон. Эти состояния определяются переменными-членами и доступны через методы получения и установки.
В игре требуется еще несколько моделей: препятствия и враги на карте. Для простоты у препятствий будет только положение на карте, и враги будут постоянными объектами. Карта будет двухмерным массивом, содержащим врагов, препятствия и дроида. Карта будет называться Arena чтобы отличаться от стандартной карты Java, и будет заполнена препятствиями и врагами при ее создании.
Obstacle.java

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package net.obviam.droids.model;
 
public class Obstacle {
 
 private float x;
 private float y;
 
 public Obstacle(float x, float y) {
  this.x = x;
  this.y = y;
 }
 
 public float getX() {
  return x;
 }
 public float getY() {
  return y;
 }
}

Enemy.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
package net.obviam.droids.model;
 
public class Enemy {
 
 private float x;
 private float y;
 private int hitpoints = 10;
 
 public Enemy(float x, float y) {
  this.x = x;
  this.y = y;
 }
 
 public float getX() {
  return x;
 }
 public float getY() {
  return y;
 }
 public int getHitpoints() {
  return hitpoints;
 }
 public void setHitpoints(int hitpoints) {
  this.hitpoints = hitpoints;
 }
}

Arena.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
package net.obviam.droids.model;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
 
public class Arena {
 
 public static final int WIDTH = 480 / 32;
 public static final int HEIGHT = 320 / 32;
 
 private static Random random = new Random(System.currentTimeMillis());
 
 private Object[][] grid;
 private List<Obstacle> obstacles = new ArrayList<Obstacle>();
 private List<Enemy>  enemies = new ArrayList<Enemy>();
 private Droid droid;
 
 public Arena(Droid droid) {
  this.droid = droid;
 
  grid = new Object[HEIGHT][WIDTH];
  for (int i = 0; i < WIDTH; i++) {
   for (int j = 0; j < HEIGHT; j++) {
    grid[j][i] = null;
   }
  }
  // add 5 obstacles and 5 enemies at random positions
  for (int i = 0; i < 5; i++) {
   int x = random.nextInt(WIDTH);
   int y = random.nextInt(HEIGHT);
   while (grid[y][x] != null) {
    x = random.nextInt(WIDTH);
    y = random.nextInt(HEIGHT);
   }
   grid[y][x] = new Obstacle(x, y);
   obstacles.add((Obstacle) grid[y][x]);
   while (grid[y][x] != null) {
    x = random.nextInt(WIDTH);
    y = random.nextInt(HEIGHT);
   }
   grid[y][x] = new Enemy(x, y);
   enemies.add((Enemy) grid[y][x]);
  }
 }
 
 public List<Obstacle> getObstacles() {
  return obstacles;
 }
 public List<Enemy> getEnemies() {
  return enemies;
 }
 public Droid getDroid() {
  return droid;
 }
}

Arena — более сложный объект, но чтение кода должно облегчить понимание. Он в основном объединяет все модели в единый мир. Наш игровой мир — это арена, которая содержит все элементы, такие как наш дроид, враги и препятствия.

WIDTH и HEIGHT рассчитываются на основе выбранного разрешения. Ячейка (плитка) в сетке будет иметь ширину 32 пикселя и высоту, поэтому я просто вычисляю, сколько ячеек помещается в сетку.
В конструкторе (строка № 19) настроена сетка и случайным образом размещено 5 препятствий и 5 врагов. Это создаст стартовую арену и наш игровой мир.
Чтобы поддерживать основной цикл в чистоте, мы делегируем обновление и рендеринг GameEngine . Это простой класс, который будет обрабатывать пользовательский ввод, обновлять состояния моделей и отображать мир. Это крошечный клеевой каркас, чтобы все это произошло.
GameEngine.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
package net.obviam.droids.controller;
 
import java.awt.Event;
import java.awt.Graphics;
 
public class GameEngine {
 
 /** handle the Event passed from the main applet **/
 public boolean handleEvent(Event e) {
  switch (e.id) {
  case Event.KEY_PRESS:
  case Event.KEY_ACTION:
   // key pressed
   break;
  case Event.KEY_RELEASE:
   // key released
   break;
  case Event.MOUSE_DOWN:
   // mouse button pressed
   break;
  case Event.MOUSE_UP:
   // mouse button released
   break;
  case Event.MOUSE_MOVE:
   // mouse is being moved
   break;
  case Event.MOUSE_DRAG:
   // mouse is being dragged (button pressed)
   break;
  }
  return false;
 }
 
 /** the update method with the deltaTime in seconds **/
 public void update(float deltaTime) {
  // empty
 }
 
 /** this will render the whole world **/
 public void render(Graphics g) {
  // empty
 }
}

Чтобы использовать движок, необходимо Droids.java класс Droids.java . Нам нужно создать экземпляр класса GameEngine и вызвать методы update() и render() в соответствующее время. Также нам нужно делегировать обработку ввода движку.
Добавьте следующие строки:

Объявите частного члена, а также создайте его экземпляр.

1
private GameEngine engine = new GameEngine();

Модифицированный игровой цикл выглядит так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
while (true) {
 long lastTime = System.nanoTime();
 
 g.setColor(Color.black);
 g.fillRect(0, 0, 480, 320);
 
 // Update the state (convert to seconds)
 engine.update((float)(delta / 1000000000.0));
 // Render the world
 engine.render(g);
 
 // Draw the entire results on the screen.
 appletGraphics.drawImage(screen, 0, 0, null);
 
 // Lock the frame rate
 delta = System.nanoTime() - lastTime;
 if (delta < 20000000L) {
  try {
   Thread.sleep((20000000L - delta) / 1000000L);
  } catch (Exception e) {
   // It's an interrupted exception, and nobody cares
  }
 }
}

Выделенные строки (# 7- # 10) содержат делегирование для методов update() и render() . Обратите внимание, что есть конвертация в секунды из нано секунд. Очень полезно работать в считанные секунды, поскольку мы можем работать с реальными мировыми ценностями.

Важное замечание : Обновление должно произойти после вычисления дельты (времени, прошедшего с момента последнего обновления). Также рендер должен вызываться после обновления, чтобы он отображал текущее состояние объектов. Обратите внимание, что экран очищается каждый раз перед визуализацией (окрашивается в черный цвет).
Последнее, что нужно сделать, это делегировать обработку ввода.

Замените текущий метод handleEvent следующим фрагментом:

1
2
3
public boolean handleEvent(Event e) {
 return engine.handleEvent(e);
}

Очень простая и простая делегация.
Запуск апплета не дает особого захватывающего результата. Просто черный экран. Это имеет смысл, поскольку все заглушки, кроме экрана, очищаемого каждый цикл.

Инициализация моделей (мир)

Нашей игре нужен дроид и несколько врагов. По замыслу мир — это наша Arena . Создавая его, мы создали мир (проверьте конструктор в Arena ).
Мы создадим мир в GameEngine как движок отвечает за GameEngine представлению, что делать.

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

01
02
03
04
05
06
07
08
09
10
private Arena arena;
private Droid droid;
 
public GameEngine() {
 droid = new Droid();
 // position droid in the middle
 droid.setX(Arena.WIDTH / 2);
 droid.setY(Arena.HEIGHT / 2);
 arena = new Arena(droid);
}

Примечание : конструктор Arena необходимо изменить, чтобы Droid добавлялся в сетку перед препятствиями и врагами.

1
2
3
4
...
  // add the droid
  grid[(int)droid.getY()][(int) droid.getX()] = droid;
...

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

Создание первого View / Renderer

Мы приложили много усилий для создания арены и мира, и мы действительно хотим увидеть это. Из-за этого мы создадим быстрый и грязный рендер, чтобы раскрыть мир. Под быстрым и грязным я подразумеваю не причудливые изображения или что-либо, кроме простых квадратов, кругов и заполнителей. Как только мы будем довольны тем, что игровые элементы включены, мы можем работать над более сложным видом, чтобы заменить квадраты и круги причудливой графикой. Это где сила разделения сияет.
Шаги, чтобы сделать мир.

  • Нарисуйте сетку, чтобы увидеть, где находятся клетки.
  • Препятствия будут нарисованы в виде синих квадратов, и они будут занимать клетки
  • Враги будут красными кругами
  • У дроида будет зеленый круг с коричневым квадратом

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

Renderer.java

1
2
3
4
5
6
7
package net.obviam.droids.view;
 
import java.awt.Graphics;
 
public interface Renderer {
 public void render(Graphics g);
}

Вот и все. Он содержит один единственный метод: render(Graphics g) . Graphics g — это холст, который передается из апплета. В идеале интерфейс должен быть независим от этого, и каждая реализация будет использовать различный сервер, но цель этого упражнения — описать MVC, а не создавать полную структуру. Поскольку мы выбрали апплет, нам нужен объект Graphics .
Конкретная реализация выглядит так:

SimpleArenaRenderer.java (в пакете view )

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
package net.obviam.droids.view;
 
import java.awt.Color;
import java.awt.Graphics;
 
import net.obviam.droids.model.Arena;
import net.obviam.droids.model.Droid;
import net.obviam.droids.model.Enemy;
import net.obviam.droids.model.Obstacle;
 
public class SimpleArenaRenderer implements Renderer {
 
 private Arena arena;
 
 public SimpleArenaRenderer(Arena arena) {
  this.arena = arena;
 }
 
 @Override
 public void render(Graphics g) {
  // render the grid
  int cellSize = 32; // hard coded
  g.setColor(new Color(0, 0.5f, 0, 0.75f));
  for (int i = 0; i <= Arena.WIDTH; i++) {
   g.drawLine(i * cellSize, 0, i * cellSize, Arena.HEIGHT * cellSize);
   if (i <= Arena.WIDTH)
    g.drawLine(0, i * cellSize, Arena.WIDTH * cellSize, i * cellSize);
  }
 
  // render the obstacles
  g.setColor(new Color(0, 0, 1f));
  for (Obstacle obs : arena.getObstacles()) {
   int x = (int) (obs.getX() * cellSize) + 2;
   int y = (int) (obs.getY() * cellSize) + 2;
   g.fillRect(x, y, cellSize - 4, cellSize - 4);
  }
 
  // render the enemies
  g.setColor(new Color(1f, 0, 0));
  for (Enemy enemy : arena.getEnemies()) {
   int x = (int) (enemy.getX() * cellSize);
   int y = (int) (enemy.getY() * cellSize);
   g.fillOval(x + 2, y + 2, cellSize - 4, cellSize - 4);
  }
 
  // render player droid
  g.setColor(new Color(0, 1f, 0));
  Droid droid = arena.getDroid();
  int x = (int) (droid.getX() * cellSize);
  int y = (int) (droid.getY() * cellSize);
  g.fillOval(x + 2, y + 2, cellSize - 4, cellSize - 4);
  // render square on droid
  g.setColor(new Color(0.7f, 0.5f, 0f));
  g.fillRect(x + 10, y + 10, cellSize - 20, cellSize - 20);
 }
}

Строки # 13 — # 17 объявляют объект Arena и проверяют, установлен ли он при построении рендерера. Я назвал это ArenaRenderer, потому что мы будем рендерить арену (мир).

Единственным методом в рендерере является метод render() . Давайте посмотрим, что делает шаг за шагом.
# 22 — Объявить размер ячейки в пикселях. Это 32. Он жестко запрограммирован, как в классе Arena .
# 23 — # 28 — Сетка рисуется. Это простая сетка. Сначала цвет устанавливается на темно-зеленый, а линии рисуются на одинаковом расстоянии.

Рисование препятствий — синие квадраты
# 31 — Установите цвет кисти на синий.
# 32 — # 36 — Перебирайте все препятствия на арене, и для каждого он рисует синий заполненный прямоугольник, немного меньший, чем ячейка на сетке.
# 39 — # 44 — Устанавливает красный цвет и, перебирая врагов на арене, рисует круг в соответствующей позиции.
# 47 — # 54 — Наконец рисует дроида в виде зеленого круга с коричневым квадратом сверху.


Обратите внимание, что арена в реальном мире имеет ширину 15 (480/32). Таким образом, дроид всегда будет находиться в одной и той же позиции (7, 5), и средство визуализации определяет свою позицию на экране, используя преобразование единиц измерения. В этом случае 1 единица в мировой координате составляет 32 пикселя на экране.
GameEngine для использования вновь созданного представления ( SimpleArenaRenderer ), мы получаем результат.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class GameEngine {
 
 private Arena arena;
 private Droid droid;
 private Renderer renderer;
 
 public GameEngine() {
  droid = new Droid();
  // position droid in the middle
  droid.setX(Arena.WIDTH / 2);
  droid.setY(Arena.HEIGHT / 2);
  arena = new Arena(droid);
 
  // setup renderer (view)
  renderer = new SimpleArenaRenderer(arena);
 }
 
 /** ... code stripped ... **/
 
 /** this will render the whole world **/
 public void render(Graphics g) {
  renderer.render(g);
 }
}

Обратите внимание на выделенные строки (5, 15, 22). Это строки, где рендерер (представление) добавляется в игру.
Результат должен выглядеть следующим образом (позиции расположены случайно, кроме дроида игрока):

Результат первого просмотра

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

Контроллер для обработки моделей ввода и обновления

Пока игра ничего не делает, но отображает текущее состояние мира (арена). Для простоты мы обновим только одно состояние дроида, его положение.

Шаги для перемещения дроида на основе пользовательского ввода:

  • При наведении мыши проверьте, пуста ли ячейка, на которую нажали, Это означает, что он содержит любые объекты, которые могут быть экземплярами Enemy или Obstacle .
  • Если ячейка пуста, контроллер создаст действие, которое будет перемещать дроида с постоянной скоростью, пока он не достигнет пункта назначения.
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
package net.obviam.droids.controller;
 
import net.obviam.droids.model.Arena;
import net.obviam.droids.model.Droid;
 
public class ArenaController {
 
 private static final int unit = 32;
 private Arena arena;
 
 /** the target cell **/
 private float targetX, targetY;
 /** true if the droid moves **/
 private boolean moving = false;
 
 public ArenaController(Arena arena) {
  this.arena = arena;
 }
 
 public void update(float delta) {
  Droid droid = arena.getDroid();
  if (moving) {
   // move on X
   int bearing = 1;
   if (droid.getX() > targetX) {
    bearing = -1;
   }
   if (droid.getX() != targetX) {
    droid.setX(droid.getX() + bearing * droid.getSpeed() * delta);
    // check if arrived
    if ((droid.getX() < targetX && bearing == -1)
      || (droid.getX() > targetX && bearing == 1)) droid.setX(targetX);
   }
   // move on Y
   bearing = 1;
   if (droid.getY() > targetY) {
    bearing = -1;
   }
   if (droid.getY() != targetY) {
    droid.setY(droid.getY() + bearing * droid.getSpeed() * delta);
    // check if arrived
    if ((droid.getY() < targetY && bearing == -1)
      || (droid.getY() > targetY && bearing == 1)) droid.setY(targetY);
   }
   // check if arrived
   if (droid.getX() == targetX && droid.getY() == targetY)
    moving = false;
  }
 }
 
 /** triggered with the coordinates every click **/
 public boolean onClick(int x, int y) {
  targetX = x / unit;
  targetY = y / unit;
  if (arena.getGrid()[(int) targetY][(int) targetX] == null) {
   // start moving the droid towards the target
   moving = true;
   return true;
  }
  return false;
 }
}

Следующая разбивка объясняет логику и важные моменты.

# 08unit представляет количество пикселей в ячейке, которое представляет 1 единицу в мировых координатах. Это жестко запрограммировано и не оптимально, но для демонстрации это достаточно хорошо.
# 09Arena контролирует контролер. Он устанавливается при создании контроллера (строка № 16).
# 12 — Целевые координаты клика в мировых единицах.
# 14 — Это true когда дроид движется. Это состояние «движения» действий. В идеале это должен быть отдельный класс, но чтобы продемонстрировать контроллер и сделать его лаконичным, мы взломаем действие внутри контроллера.
# 20 — Метод update который обновляет положение дроида в соответствии с временем, прошедшим с постоянной скоростью. Это очень просто, он проверяет позиции X и Y и, если они не совпадают с позицией цели, обновляет соответствующую позицию дроида (X или Y), учитывая его скорость. Если дроид находится в целевой позиции, то переменная состояния move обновляется, завершая действие перемещения.

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

# 52 — Метод onClick(int x, int y) будет вызываться при возникновении события «вверх». Он проверяет, пуста ли ячейка, по которой щелкнули, и если да, то запускает действие «перемещение», устанавливая для переменной состояния значение true
# 53- # 54 — Преобразует экранные координаты в мировые координаты.
Это контроллер. Чтобы использовать его, GameEngine необходимо обновить.

Обновленный GameEngine.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
package net.obviam.droids.controller;
 
import java.awt.Event;
import java.awt.Graphics;
 
import net.obviam.droids.model.Arena;
import net.obviam.droids.model.Droid;
import net.obviam.droids.view.Renderer;
import net.obviam.droids.view.SimpleArenaRenderer;
 
public class GameEngine {
 
 private Arena arena;
 private Droid droid;
 private Renderer renderer;
 private ArenaController controller;
 
 public GameEngine() {
  droid = new Droid();
  // position droid in the middle
  droid.setX(Arena.WIDTH / 2);
  droid.setY(Arena.HEIGHT / 2);
  arena = new Arena(droid);
 
  // setup renderer (view)
  renderer = new SimpleArenaRenderer(arena);
  // setup controller
  controller = new ArenaController(arena);
 }
 
 /** handle the Event passed from the main applet **/
 public boolean handleEvent(Event e) {
  switch (e.id) {
  case Event.KEY_PRESS:
  case Event.KEY_ACTION:
   // key pressed
   break;
  case Event.KEY_RELEASE:
   // key released
   break;
  case Event.MOUSE_DOWN:
   // mouse button pressed
   break;
  case Event.MOUSE_UP:
   // mouse button released
   controller.onClick(e.x, e.y);
   break;
  case Event.MOUSE_MOVE:
   // mouse is being moved
   break;
  case Event.MOUSE_DRAG:
   // mouse is being dragged (button pressed)
   break;
  }
  return false;
 }
 
 /** the update method with the deltaTime in seconds **/
 public void update(float deltaTime) {
  controller.update(deltaTime);
 }
 
 /** this will render the whole world **/
 public void render(Graphics g) {
  renderer.render(g);
 }
}

Изменения выделены.
№ 16 — Объявить контроллер.
# 28 — Создание контроллера.
# 46 — Делегирование события мыши.
# 60 — Вызвать метод update на контроллере.
Запустите апплет, и вы можете нажать на карту, и если ячейка пуста, дроид переместится туда.

Упражнения

  • Создайте представление, которое будет отображать изображения / спрайты для сущностей вместо нарисованных фигур.
    Подсказка : используйте BufferedImage для достижения этой цели.
  • Извлеките действие перемещения в новый класс.
  • Добавляйте новые действия (атаки) при нажатии на врага. Подсказка: создайте пулю, которая будет выпущена в цель. Вы можете использовать движение с большей скоростью. Когда hitpoint попадания падает до 0, враг уничтожается. Используйте другое изображение для представления разных состояний.

Исходный код

https://github.com/obviam/mvc-droids или загрузите в виде zip-файла.

Вы также можете использовать Git
$ git clone git://github.com/obviam/mvc-droids.git

Ссылка: Создание игр с использованием шаблона MVC — учебное пособие и введение от нашего партнера JCG   Импалер в блоге « Против зерна» .