Шаблон Стратегия

Tulenber 8 May, 2020 ⸱ Beginner ⸱ 6 min ⸱ 2019.3.13f1 ⸱

Все стратегии хороши лишь до того момента, пока не выпущена первая стрела. Мэт Коутон. Огни Небес. Роберт Джордан.

Продолжаем наш цикл посвящённый шаблонам проектирования и поговорим про очень полезный шаблон под названием Стратегия.

Теория

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

Теоретическая часть шаблонов описывается очень часто, так что за ней можно обратиться к этой замечательной статье.

Практика

В качестве практического примера можно привести поведение искусственного интеллекта юнитов в стратегических играх.

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

  1. Проверка на возможность выполнения действия - если это выстрел из пистолета, то проверяются наличие патронов и расстояние до цели. Для лечения проверяется доступность аптечки и текущий уровень хитпоинтов
  2. Вводная анимация - юнит поднимает руку с пистолетом для выстрела или достаёт аптечку для лечения
  3. Действие - выстрел(создание летящей пули или снятие хитпоинтов с противника) в случае с пистолетом и начисление хитпоинтов в случае с лечением
  4. Выходная анимация - юнит опускает руку с оружием или прячет аптечку, после лечения

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

  1. Действие - высокоуровневая стратегия, описывает последовательность проверка-анимация-действие-анимация
  2. Проверка наличия цели - она должна быть на подходящем расстоянии до выстрела и т.п.
  3. Проверка хитпоинтов - не стоит лечить себя, без необходимости
  4. Выстрел - если в игре есть выстрелы, то они будут не сильно различаться между собой
  5. Лечение - также довольно однотипная механика

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

Реализация

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

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

Выбор заклинания происходит при помощи Immediate Mode GUI (IMGUI), подробнее о его применении можно почитать в нашей статье про чит панели. Работа с анимациями производится через Mecanim, базовую информацию о котором можно подчерпнуть из другой статьи "Шаблон Состояние".

  1. В качестве интерфейса используется абстрактный класс:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using UnityEngine;

public abstract class Spell
{
    // Триггер для запуска анимации
    protected int TriggerName;
    // Префаб заклинания
    protected GameObject Prefab;
    // Место спауна префаба зависит от типа заклинания
    protected Vector3 ProjectileOffset;

    public void Fire(Vector3 magePosition)
    {
        // Объект заклинания создаётся с учётом сдвига точки появления
        Object.Instantiate(Prefab, magePosition + ProjectileOffset, Quaternion.identity);
    }

    public int GetAnimationTriggerName()
    {
        // Информация об анимации для данного типа заклинания
        return TriggerName;
    }
}
  1. Стратегии в нашем случае будут отвечать за вид и оффсета генерируемого объекта заклинания, а также триггер анимации:
  1. Базовое заклинание
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
using UnityEngine;

public class BasicSpell : Spell
{
    public BasicSpell(GameObject prefab)
    {
        Prefab = prefab;
        // Сдвиг относительно положения мага
        ProjectileOffset = new Vector3(0.6f, 0, 0);
        // Тригер анимации базового заклинания
        TriggerName = Animator.StringToHash("Attack");
    }
}
  1. Продвинутое заклинание
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
using UnityEngine;

public class AdvancedSpell : Spell
{
    public AdvancedSpell(GameObject prefab)
    {
        Prefab = prefab;
        // Сдвиг относительно положения мага
        ProjectileOffset = new Vector3(0.8f, 0.2f, 0);
        // Тригер анимации продвинутого заклинания
        TriggerName = Animator.StringToHash("Attack_Extra");
    }
}
  1. Управляющим объектом будет маг:
 1
 2
 3
 4
 5
 6
 7
 8
 9
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
using UnityEngine;

public class Mage : MonoBehaviour
{
    // Объект родитель для всех противников
    public Transform enemyList = null;
    // Префаб основного заклинания
    public GameObject basicSpellPrefab = null;
    // Префаб продвинутого заклинания
    public GameObject advancedSpellPrefab = null;
    // Анимация мага
    private Animator _animator;
    // Флаг для контроля каста заклинания
    public static bool ProjectileFlag = false;

    // Объект для текущего заклинания
    private Spell _spell = null;
    // Базовое заклинание
    private BasicSpell _basicSpell = null;
    // Продвинутое заклинание
    private AdvancedSpell _advancedSpell = null;

    // Настройки выбора заклинания
    private int _spellSelection = 0;
    private readonly string[] _spellNames = {"Basic", "Advanced"};

    void Start()
    {
        _animator = GetComponent<Animator>();
        
        _basicSpell = new BasicSpell(basicSpellPrefab);
        _advancedSpell = new AdvancedSpell(advancedSpellPrefab);

        // Изначально выбрано базовое заклинание
        _spell = _basicSpell;
    }
    
    void Update()
    {
        // Если нет противников или уже есть скастованное заклинание
        if (enemyList.childCount == 0 || ProjectileFlag)
        {
            return;
        }

        // Запуск анимации мага для каста текущего заклинания
        _animator.SetTrigger(_spell.GetAnimationTriggerName());

        ProjectileFlag = true;
    }

    private void OnGUI()
    {
        // Прячем выбор заклинания во время каста и полёта текущего заклинания
        if (ProjectileFlag)
        {
            return;
        }

        // Отрисовываем панель выбора заклинания
        GUI.Box(new Rect (10, 10, 100, 100), "Spell panel");
        _spellSelection = GUI.SelectionGrid (new Rect (20, 40, 80, 60), _spellSelection, _spellNames, 1);

        // При отсутствии изменений ничего не делаем
        if (!GUI.changed)
        {
            return;
        }

        // При выборе базового заклинания делаем его активным
        if (_spellSelection == 0)
        {
            _spell = _basicSpell;
            return;
        }

        // При выборе продвинутого заклинания делаем активным его
        _spell = _advancedSpell;
    }

    // Создание текущего заклинания в подходящий момент из анимации каста
    public void Fire()
    {
        _spell.Fire(transform.position);
    }
}

Результат

В зависимости от выбранного заклинания маг запускает разные анимации заклинаний
Result

Заключение

Сама по себе реализация шаблона стратегия не является чем-то сложным, по сути, это всего лишь использование наследования объектов. Самым большим вызовом станет выделение механик игры в структуру, которая была бы эффективна для конкретного проекта и скорее всего прежде, чем это случится, пройдёт несколько итераций рефакторинга, но чем раньше вы начнёте ответственно подходить к созданию этой структуры, тем проще вам будет поддерживать проект на достойном уровне. Пока! =)



Privacy policyCookie policyTerms of service
Tulenber 2020