Статьи

Разработка игр для Android — Дизайн игровых сущностей — Государственный паттерн

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

Внутреннее состояние лучше всего описать как душу и разум сущности. В первой части я описал, почему композиция лучше, чем наследование. В двух словах композиция предоставляет средства для обмена алгоритмами, связанными с их поведением. С другой стороны, состояние помогает объектам контролировать свое поведение.

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

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

Разбивая его, что можно сделать с винтовкой? Конечно, нажмите на курок и перезагрузите (вставьте, извлеките зажим для патронов).

Теперь подумайте обо всех сценариях. Что произойдет, если я нажму на спусковой крючок и не будет клип Или когда есть, но он пуст? Видимо, простая вещь стала немного сложнее. Давайте посмотрим на это на бумаге:

Начальная Диаграмма Состояния Винтовки

Давайте рассмотрим диаграмму и выясним, что это такое и чего мы хотим.

Диаграмма выше называется диаграммой состояний .

Каждый из этих кругов является государством, а каждая из этих стрелок является переходным состоянием .

Каждое состояние — это просто разные конфигурации винтовки. Винтовка ведет себя определенным образом, в зависимости от того, в каком состоянии она находится, и необходимо предпринять определенные действия, чтобы перевести ее в другое состояние. Итак, чтобы перейти в другое состояние, вам нужно будет что-то сделать, например, нажать на курок или вставить клип. Смотрите стрелки на диаграмме.

Мы выделяем следующие состояния :

  • Без обрезки
  • Имеет клип
  • Боеприпасы уволены
  • Нет патронов

и действия (переходы) :

  • вставляет клип
  • извлекает клип
  • спусковой крючок
  • боеприпасы

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

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

Наша первая реализация

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

Создайте класс Rifle.java .

01
02
03
04
05
06
07
08
09
10
11
public class Rifle {
 
    // defining the states
    final static int NO_CLIP        = 0;
    final static int HAS_CLIP       = 1;
    final static int AMMO_FIRED     = 2;
    final static int OUT_OF_AMMO    = 3;
 
    int state = NO_CLIP;    // instance variable holding the current state
    int ammoCount = 0;      // we count the ammo
}

Большой! Мы создали структуру винтовки. Мы также хотим знать, когда у нас закончатся боеприпасы, поэтому мы добавили переменную ammoCount . Они установлены на значения по умолчанию. Винтовка не держит клип, таким образом, патроны равны 0, когда они созданы.

Теперь давайте добавим действия. Но будьте осторожны! Подвергать все действия тому, кто обращается с оружием, опасно. Что произойдет, если кто-то попытается вытащить клип во время стрельбы? Мы должны принять это во внимание при инициировании действий.

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
// **********************
// Creating the actions
// **********************
 
public void insertClip() {
    // We check each possible state and act according to them
    if (state == HAS_CLIP) {
        System.out.println("There is already a clip loaded.");
    } else if (state == AMMO_FIRED) {
        System.out.println("You'll hurt yourself!!!");
    } else if (state == OUT_OF_AMMO) {
        System.out.println("You need to take out the empty clip first.");
    } else if (state == NO_CLIP) {
        state = HAS_CLIP;
        ammoCount = 10;
        System.out.println("You have loaded a clip with " + ammoCount + " bulletts.");
    }
}
 
public void ejectClip() {
    if (state == NO_CLIP) {
        System.out.println("The magazine is empty.");
    } else if (state == AMMO_FIRED) {
        System.out.println("You'll hurt yourself!!!");
    } else if (state == HAS_CLIP) {
        // You could still eject it if you want but for the sake of
        // simplicity let's use up the ammo first
        System.out.println("Use up all your ammo first.");
    } else if (state == OUT_OF_AMMO) {
        state = NO_CLIP;
        System.out.println("You have unloaded a clip.");
    }
}
 
public void pullTrigger() {
    if (state == NO_CLIP) {
        System.out.println("Empty Click!");
    } else if (state == AMMO_FIRED) {
        System.out.println("Jammed!");
    } else if (state == OUT_OF_AMMO) {
        System.out.println("Click! Out of ammo.");
    } else if (state == HAS_CLIP) {
        System.out.println("BANG!!!");
        state = AMMO_FIRED;
        fireAmmo();
    }
}
 
public void fireAmmo() {
    if (state == NO_CLIP) {
        System.out.println("Empty magazine.");
    } else if (state == AMMO_FIRED) {
        System.out.println("Bullet already on its way to kill someone!");
    } else if (state == OUT_OF_AMMO) {
        System.out.println("Out of ammo.");
    } else if (state == HAS_CLIP) {
        state = AMMO_FIRED;
        ammoCount--;
        System.out.println("Bullet on its way!");
        // we check if the clip is empty
        if (ammoCount == 0) {
            // yes, it's empty
            System.out.println("Darn! Out of ammo");
            state = OUT_OF_AMMO;
        } else {
            state = HAS_CLIP;
        }
    }
}

Давайте проверим винтовку.

Создайте класс RifleTest.java .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class RifleTest {
 
    public static void main(String[] args) {
        Rifle rifle = new Rifle();
        System.out.println(rifle);
 
        rifle.insertClip();
        rifle.pullTrigger();
        rifle.pullTrigger();
        rifle.pullTrigger();
        rifle.pullTrigger();
 
        System.out.println(rifle);
 
        rifle.insertClip();
        rifle.ejectClip();
        rifle.pullTrigger();
    }
}

Это очень простой сценарий, который выполняет некоторые действия с винтовкой. Некоторые могут быть хорошими, некоторые плохими. Как при попытке ввести второй клип или нажать на спусковой крючок, когда на нем нет патронов.

Проверьте результат и внимательно его изучите. Как это круто? Мы Калашниковы нашего века.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
<< RIFLE [state=Empty Magazine (No Clip), ammo=0] >>
> You have loaded a clip with 3 bullets.
> BANG!!!
> Bullet on its way!
> BANG!!!
> Bullet on its way!
> BANG!!!
> Bullet on its way!
> Darn! Out of ammo
!* Click! Out of ammo.
<< RIFLE [state=Out of Ammo, ammo=0] >&g;t
!* You need to take out the empty clip first.
> You have unloaded a clip.
!* Empty Click! – No clip!

Обратите внимание, что я опустил метод toString () для винтовки .

Отлично сработано! У нас есть наш первый конечный автомат.

Но

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

Ах, это легко, правда? У нас будет еще несколько состояний и еще несколько переходов.

Но подождите, нам нужно немного переделать класс Rifle .

Давайте посмотрим, что нам нужно изменить, чтобы поддерживать автоматический и ручной огонь:

  • нам нужно добавить два состояния
  • но тогда нам нужно изменить каждый метод для обработки состояний, добавив условные операторы
  • Функция pullTrigger () усложняется, так как ей нужно будет знать, в каком она состоянии, проверять маркеры и запускать их как таковые

Это много работы. Это должен быть другой путь.

Решение

Что если мы дадим каждому государству поведение и поместим его в свой класс? Таким образом, каждое государство будет осуществлять только свои собственные действия. У нас будет класс винтовки, делегирующий действие объекту состояния, представляющему текущее состояние.

Посмотрим как это выглядит?

Диаграмма состояния финальной винтовки

Что вы должны заметить, так это то, что Has Clip теперь имеет ручной / автоматический режим и имеет переключатель переключения . У него сейчас есть подсостояние. Переключение переключателя меняет поведение. Оба состояния будут иметь триггерное действие, но каждое состояние будет вести себя по-разному. Это легко достигается с помощью интерфейсов, верно?

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

Методы отображаются непосредственно на все действия, которые могут произойти с винтовкой

1
2
3
4
5
6
7
8
public interface RifleState {
 
    public void insertClip();
    public void ejectClip();
    public void swithManualAuto();
    public void pullTrigger();
    public void fireAmmo();
}

Новый класс Rifle не будет иметь ни одного из реализованных действий, но будет иметь все состояния и делегировать действия его текущему состоянию.

Rifle.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
public class Rifle {
 
    // the states of the rifle
    RifleState emptyState;
    RifleState autoFireState;
    RifleState manualFireState;
    RifleState outOfAmmoState;
    RifleState roundFiredState;
    RifleState ammoFiredState;
 
    RifleState state = emptyState;
    int ammoCount = 0;
 
    // constructor
    public Rifle() {
        // creating states
        this.emptyState         = new NoClipState(this);
        this.autoFireState      = new AutoFireState(this);
        this.manualFireState    = new ManualFireState(this);
        this.outOfAmmoState     = new OutOfAmmoState(this);
        this.roundFiredState    = new RoundFiredState(this);
        this.ammoFiredState     = new AmmoFiredState(this);
 
        this.state      = this.emptyState;
        this.ammoCount  = 0;
    }
    // convenience methods - delegating only
    public void insertClip() {
        this.state.insertClip();
    }
    public void ejectClip() {
        this.state.ejectClip();
    }
    public void switchManualAuto() {
        this.state.switchManualAuto();
    }
    public void pullTrigger() {
        this.state.pullTrigger();
    }
 
    // getters and setters
    // ... omitted
}

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

Также отсутствует fireAmmo (), так как это внутреннее действие состояния.

Теперь давайте сопоставим состояния с реальными классами. Они такие же, как в оригинальном классе винтовки.

Я перечислю один полный класс, а для остальных только методы, которые изменяют состояние. Изучите исходный код для полных списков.

OutOfAmmoState.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
public class OutOfAmmoState implements RifleState {
 
    private Rifle rifle;
    public OutOfAmmoState(Rifle rifle) {
        this.rifle = rifle;
    }
 
    @Override
    public void ejectClip() {
        rifle.setState(rifle.getEmptyState());
        System.out.println("> Clip ejected.");
    }
 
    @Override
    public void fireAmmo() {
        System.out.println("!* You can't fire with no ammo.");
    }
 
    @Override
    public void insertClip() {
        System.out.println("!* There is an empty clip inserted already!");
    }
 
    @Override
    public void pullTrigger() {
        System.out.println("!* Out of ammo!");
    }
 
    @Override
    public void switchManualAuto() {
        System.out.println("!* Plesea reload first");
    }
}

Обратите внимание, что единственный выход из OutOfAmmoStateудаление клипа.

Любая другая попытка ничего не даст.

Также обратите внимание на конструктор. Мы передаем ссылку на винтовку там.

Проверьте и другие классы тоже:

AmmoFiredState

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class AmmoFiredState implements RifleState {
 
    private Rifle rifle;
    public AmmoFiredState(Rifle rifle) {
        this.rifle = rifle;
    }
 
    @Override
    public void fireAmmo() {
        rifle.setAmmoCount(rifle.getAmmoCount() - 1);
        System.out.println("> Fired 1 bullet.");
        if (rifle.getAmmoCount() == 0) {
            rifle.setState(rifle.getOutOfAmmoState());
        } else {
            rifle.setState(rifle.getManualFireState());
        }
    }
    // ... ommited
}

AutoFireState

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
public class AutoFireState implements RifleState {
 
    private Rifle rifle;
    public AutoFireState(Rifle rifle) {
        this.rifle = rifle;
    }
 
    @Override
    public void ejectClip() {
        rifle.setAmmoCount(0);
        rifle.setState(rifle.getEmptyState());
        System.out.println("> Clip ejected. Please reload.");
    }
 
    @Override
    public void pullTrigger() {
        System.out.println("> Pulled trigger.");
        rifle.setState(rifle.getRoundFiredState());
        rifle.getState().fireAmmo();
    }
 
    @Override
    public void switchManualAuto() {
        rifle.setState(rifle.getManualFireState());
        System.out.println("> Switched to manual. Hope they are slow and few!");
    }
    // ... ommited
}

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

ManualFireState

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
public class ManualFireState implements RifleState {
 
    private Rifle rifle;
    public ManualFireState(Rifle rifle) {
        this.rifle = rifle;
    }
 
    @Override
    public void ejectClip() {
        rifle.setAmmoCount(0);
        rifle.setState(rifle.getEmptyState());
        System.out.println("> Clip ejected. Please reload.");
    }
 
    @Override
    public void pullTrigger() {
        System.out.println("> Pulled trigger.");
        rifle.setState(rifle.getAmmoFiredState());
        rifle.getState().fireAmmo();
    }
 
    @Override
    public void switchManualAuto() {
        rifle.setState(rifle.getAutoFireState());
        System.out.println("> Switched to auto. Bring'em on!!!");
    }
 
    // ... ommited
}

NoClipState

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
public class NoClipState implements RifleState {
 
    private Rifle rifle;
    public NoClipState(Rifle rifle) {
        this.rifle = rifle;
    }
 
    @Override
    public void insertClip() {
        rifle.ammoCount = 50;
        rifle.setState(rifle.getManualFireState());
    }
 
    // ...ommited
}

RoundFiredState

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
public class RoundFiredState implements RifleState {
 
    private Rifle rifle;
    public RoundFiredState(Rifle rifle) {
        this.rifle = rifle;
    }
 
    @Override
    public void fireAmmo() {
        int count = 10;
        while (count > 0 && rifle.getAmmoCount() > 0) {
            System.out.print("> BANG! ");
            rifle.setAmmoCount(rifle.getAmmoCount() - 1);
            count--;
        }
        System.out.println();
        System.out.println("> Fired a round of " + (10 - count) + " bullets. Yeah!");
        if (rifle.getAmmoCount() <= 0) {
            rifle.setAmmoCount(0);
            rifle.setState(rifle.getOutOfAmmoState());
        } else {
            rifle.setState(rifle.getAutoFireState());
        }
    }
    // ...ommited
}

Большой! Давайте вместе бросим тест на новую блестящую винтовку.

RifleTest

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RifleTest {
 
    public static void main(String[] args) {
        Rifle rifle = new Rifle();
 
        rifle.pullTrigger();
        rifle.ejectClip();
        rifle.insertClip();
        rifle.pullTrigger();
        rifle.switchManualAuto();
        rifle.pullTrigger();
        rifle.pullTrigger();
        rifle.switchManualAuto();
        rifle.pullTrigger();
        rifle.insertClip();
        rifle.switchManualAuto();
        rifle.pullTrigger();
        rifle.pullTrigger();
        rifle.pullTrigger();
        rifle.pullTrigger();
        rifle.pullTrigger();
    }

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

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
!* You can’t fire with an empty magazine.
!* The magazine is empty.
> Pulled trigger.
> Fired 1 bullet.
> Switched to auto. Bring’em on!!!
> Pulled trigger.
> BANG! >BANG! >BANG! >BANG! >BANG! >BANG! >BANG! > BANG! >BANG! >BANG!
> Fired a round of 10 bullets. Yeah!
> Pulled trigger.
>BANG! >BANG! >BANG! >BANG! >BANG! >BANG! >BANG! > BANG! >BANG! >BANG!
> Fired a round of 10 bullets. Yeah!
> Switched to manual. Hope they are slow and few!
> Pulled trigger.
> Fired 1 bullet.
!* Clip is already present!
> Switched to auto. Bring’em on!!!
> Pulled trigger.
> BANG! >BANG! >BANG! >BANG! >BANG! >BANG! >BANG! > BANG! >BANG! >BANG!
> Fired a round of 10 bullets. Yeah!
> Pulled trigger.
> BANG! >BANG! >BANG! >BANG! >BANG! >BANG! >BANG! > BANG! >BANG! >BANG!
> Fired a round of 10 bullets. Yeah!
> Pulled trigger.
> BANG! >BANG! >BANG! >BANG! >BANG! >BANG! >BANG! >BANG!
> Fired a round of 8 bullets. Yeah!
!* Out of ammo!
!* Out of ammo!

Опять же, желтые линии показывают, когда вы попробовали что-то хитрое.

Вуаля! У нас есть автоматы и легко расширяемые игровые элементы.

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

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

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

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

Загрузите исходный код и проект здесь (obviam.states.tgz).

Обратите внимание, что первая попытка была переименована в RifleOld.java и RifleOldTest.java

Справка: Дизайн внутриигровых сущностей. Стратегии составления объектов. Часть 2 — Государственный шаблон от нашего партнера по JCG Тамаса Яно из блога « Против зерна ».

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

Статьи по Теме: