Статьи

Сила конечных государственных машин: концепция и создание

Этот учебник, состоящий из двух частей, будет посвящен созданию автомобиля с несколькими состояниями с использованием конечного автомата . Мы начнем с процедурного FSM и перейдем к шаблону разработки State Pattern. Концепция и творчество будут нашей главной целью во время этой первой части; затем мы перейдем к применению и расширению во второй части.


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


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

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

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

Хорошим примером может служить пистолет-пулемет 1970 года HK VP70Z , который имеет три режима стрельбы: безопасный, одиночный выстрел и полуавтоматический трехсторонний выстрел. В зависимости от текущего режима, в котором он установлен (состояние), результат (выход) будет отличаться при нажатии триггера (вход).

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


Пришло время начать новый проект. С FlashDevelop создайте новый проект AS3. Для названия поставь CarFSM. Нажмите «Обзор …» и сохраните его в нужном месте. Зайдите в слот пакета и введите «com.activeTuts.fsm». Убедитесь, что установлен флажок «Создать каталог для проекта», затем нажмите «ОК» для завершения.

описание изображения

После загрузки проекта в FlashDevelop нажмите «Просмотр» и выберите «Диспетчер проектов». Видите папку «src»? Щелкните правой кнопкой мыши и выберите «Исследовать».

описание изображения

Когда эта папка открыта, вы должны увидеть папку «com», созданную ранее. Откройте исходный код, который я включил в этот учебник, и перетащите папку «assets» в «src»; убедитесь, что вы не поместили его в папку «com».

Затем перейдите в папку с исходным кодом «com» ​​и перетащите папку «bit101» в папку «com» ​​внутри «src». Вы также можете скачать minimalComps здесь, если вы хотите получить его прямо из источника.

И, наконец, разверните папку «com» ​​(внутри «src») до «fsm» и дважды щелкните Main.as. Теперь он должен быть открыт внутри FlashDevelop (при условии, что у вас есть FD в качестве приложения по умолчанию для расширения .as).


Мы начнем с рассмотрения двух состояний еще более простого примера: флажка MinimalComps.

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

описание изображения

Теперь для кода. Внутри Main.as, на одну строку ниже класса import, добавьте метаданные, показанные ниже.

1
[SWF (width = 500, height = 350, frameRate = 60, backgroundColor = 0xFFFFFF)]

Затем, войдите в метод init() и установите курсор ниже, где он говорит «точка входа». Затем добавьте вызов метода simpleExample() . Затем убедитесь, что курсор активен где-то внутри вызова метода, и нажмите клавиши «CTRL + SHIFT + 1». Подсказка покажет; выберите «Создать частную функцию» и нажмите клавишу «Ввод».

описание изображения

Теперь просто скопируйте и вставьте приведенный ниже код во вновь созданный метод. Затем поместите курсор в слово «CheckBox» и нажмите «CTRL + SHIFT + 1», чтобы автоматически импортировать нужный класс. После этого нажмите «CTRL + ENTER», чтобы запустить приложение.

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

1
2
3
4
5
6
7
var checkBox:CheckBox = new CheckBox (this, 240, 160, ‘false’, showValue);
checkBox.scaleX = checkBox.scaleY = 2;
 
function showValue (e:Event):void
{
    CheckBox (e.target).label = Boolean (e.target.selected).toString ();
}

У вас должно быть что-то похожее на то, что вы видите над этой строкой. Есть два состояния: ON и OFF . Каждый раз, когда вы нажимаете, флажок переключает состояния, а также меняет свою метку как форму вывода.

На реальный проект «Автомобиль» FSM. Убедитесь, что проект настроен на запуск в режиме «отладки» для включения операторов trace() .


Хорошо, давайте забудем предварительный просмотр вверху страницы и запустим автомобиль FSM с нуля. В Main.as выделите метод init() вместе с методом simpleExample() и нажмите клавишу «НАЗАД», чтобы удалить их.

Перейдите на одну строку выше конструктора и добавьте переменные ниже.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
private var _past:Number;
private var _present:Number;
 
private var _tick:Number;
 
private var _car:Car;
 
private var _initiatedTest1:Boolean;
private var _initiatedTest2:Boolean;
private var _initiatedTest3:Boolean;
private var _initiatedTest4:Boolean;
private var _initiatedTest5:Boolean;
private var _initiatedTest6:Boolean;
 
private var _finalActions:Boolean;
 
private var _counter:Number = 0;

Переменные _past, _present, _tick и _counter будут использоваться для выполнения по времени. Я объясню больше об этом в ближайшее время. Переменная _car будет содержать ссылку на класс Car, который будет инкапсулировать процедурные действия Car FSM. Остальные — логические свойства, используемые для запуска синхронизированных действий.

Давайте работать на время выполнения. Добавьте код ниже внутри конструктора.

1
2
3
4
5
6
7
_present = getTimer ();
 
trace (‘Start of constructor = ‘ + _present * 0.001 + ‘ seconds\n’);
 
_past = _present;
 
addEventListener (Event.ENTER_FRAME, update);

Подведите курсор к слову «обновить», нажмите «CTRL + SHIFT + 1» и выберите «Создать обработчик события». При тестировании приложения вы увидите распечатку, похожую на «Начало конструктора = 2,119 секунды» (это может быть меньше, если у вас более быстрый ПК). Это то же самое, что делить значение getTimer() на 1000, но они говорят, что умножение происходит быстрее.

Давайте перейдем к методу update() . Добавьте код ниже в это.

01
02
03
04
05
06
07
08
09
10
11
_present = getTimer ();
_tick = (_present — _past) * .001;
_past = _present;
 
_counter += _tick;
 
if (_counter >= 2)//2 seconds
{
    _counter -= 2;
    trace (_counter + ‘ 2 seconds’);
}

Теперь, когда вы протестируете его снова, вы увидите, что выражение trace() появляется каждые две секунды. Затем _counter сбрасывается на любое перекрытие, необходимое для поддержания точности синхронизации.

Попробуйте использовать другое значение, отличное от двух секунд, и запустите его еще пару раз.

В классе автомобилей. Прежде чем продолжить, удалите оператор if() внутри метода update() .


Как я упоминал ранее, мы начинаем с новой идеи создания автомобиля с несколькими состояниями. Допустим, мы решили иметь машину, которую можно включать и выключать, а также двигать вперед и на ней может закончиться бензин. Это даст нам четыре разных состояния — ON , OFF , DRIVE_FORWARD и OUT_OF_FUEL .

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

описание изображения

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

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

Теперь мы готовы кодировать класс.

Внутри метода конструктора в Main.as перейдите на одну строку перед слушателем события ENTER_FRAME и добавьте приведенный ниже код.

1
2
_car = new Car;
addChild (_car);

Теперь, когда класс Car отсутствует, поместите курсор в слово «Car» и нажмите «CTRL + SHIFT + 1», выберите «Создать новый класс» и нажмите клавишу «ENTER».

Используйте ту же информацию, как показано ниже. Нажмите «ОК», чтобы закончить.

описание изображения

Теперь у вас должен быть открыт класс Car во FlashDevelop.


Добавьте код под одной строкой над конструктором класса.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public static const ONE_SIXTH_SECONDS:Number = 1 / 6;
 
private const IDLE_FUEL_CONSUMPTION:Number = .0055;
private const DRIVE_FUEL_CONSUMPTION:Number = .011;
 
///CAR STATES
private static const ENGINE_OFF:String = ‘off’;
private static const ENGINE_ON:String = ‘on’;
private static const ENGINE_DRIVE_FORWARD:String = ‘driving forward’;
private static const ENGINE_OUT_OF_FUEL:String = ‘out of fuel’;
 
private var _currentState:String = ENGINE_OFF;
 
private var _engineTimer:Number = 0;
private var _fullCapacity:Number = 1;
private var _fuelSupply:Number = _fullCapacity;

Автомобиль настроен на потребление топлива только 6 раз в секунду. Это представлено константой класса ONE_SIXTH_SECONDS . Кроме того, объем потребления зависит от того, находится ли автомобиль на холостом ходу или движется вперед. Для этих целей мы будем использовать IDLE_FUEL_CONSUMPTION и DRIVE_FUEL_CONSUMPTION .

Четыре состояния представлены строковыми константами с установленным по умолчанию ENGINE_OFF .

Свойство _engineTimer будет использоваться для запуска consumeFuel() каждые 1/6 секунды, но только если состояние ENGINE_ON или ENGINE_DRIVE_FORWARD .

Наконец, _fuelSupply (то, что consumeFuel() ) принимает значение _fuelCapacity для полного бака.


Оставьте конструктор Car пустым. Перейдите ниже и добавьте метод update() показанный ниже.

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
public function update ($tick:Number):void
{
     
    switch (_currentState)
    {
        case ENGINE_OFF:
        //nothing
        break;
         
        case ENGINE_ON:
        _engineTimer += $tick;
         
        //gas consumption and trace statement
        if (_engineTimer >= ONE_SIXTH_SECONDS) //6 times per second interval
        {
            trace (‘vm’);//you may comment this out if you like
            _engineTimer -= ONE_SIXTH_SECONDS;
             
            consumeFuel (IDLE_FUEL_CONSUMPTION);///30 seconds gas supply
        }
        break;
         
        case ENGINE_DRIVE_FORWARD:
        _engineTimer += $tick;
         
        if (_engineTimer >= ONE_SIXTH_SECONDS)
        {
            trace (‘vroomm’);//you may comment this out if you like
            _engineTimer -= ONE_SIXTH_SECONDS;
             
            consumeFuel (DRIVE_FUEL_CONSUMPTION);///15 seconds gas supply
        }
        break;
         
        case ENGINE_OUT_OF_FUEL:
        //nothing
        break;
    }
}

Main.as будет вызывать этот метод при каждом ENTER_FRAME события ENTER_FRAME за истекшее время между кадрами. После вызова он проверяет текущее состояние автомобиля и выполняет соответствующее действие.

Если оставить его в покое, то переход состояния может происходить только с помощью consumeFuel() который устанавливает его в OUT_OF_FUEL когда _fuelSupply .

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


Добавьте код ниже после метода update() .

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 function turnKeyOn ():void
{
    trace («attempting to turn the car on…»);
     
    switch (_currentState)
    {
        case ENGINE_OFF:
        trace («Turning the car on…the engine is now running!»);
        _currentState = ENGINE_ON;
        break;
         
        case ENGINE_ON:
        trace («the engine’s already running you didn’t have to crank the ignition!»);
        break;
         
        case ENGINE_DRIVE_FORWARD:
        trace («you’re driving so don’t crank the ignition!»);
        break;
         
        case ENGINE_OUT_OF_FUEL:
        trace («no fuel — the car will not start, get some fuel before anything. Returning the key to the off position.»);
        break;
    }
}

Как и метод update() , проверяется _currentState и выполняется соответствующее действие. Это в значительной степени объясняет себя.


То же самое касается выключения автомобиля. Добавьте это дальше.

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 function turnKeyOff ():void
{
    trace («attempting to turn the car off…»);
     
    switch (_currentState)
    {
        case ENGINE_OFF:
        trace («The car’s already off, you can’t turn the key counter-clockwise any further…»);
        break;
         
        case ENGINE_ON:
        trace («click… the engine has been turned off from park.»);
        _currentState = ENGINE_OFF;
        break;
         
        case ENGINE_DRIVE_FORWARD:
        trace («nvrm…click… rolling to a stop…the engine has been turned off.»);
         
        _currentState = ENGINE_OFF;
        break;
         
        case ENGINE_OUT_OF_FUEL:
        trace («you already did this when the fuel ran out earlier…»);
        break;
    }
}

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


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

Добавьте код ниже turnKeyOff() .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public function driveForward ():void
{
    trace («attempting to drive forward…»);
    switch (_currentState)
    {
        case ENGINE_OFF:
        trace («click, changing the gear to drive doesn’t do anything…the car is not running, returning the gear to park…»);
        break;
         
        case ENGINE_ON:
        trace («click, changing gears to drive …now were going somewhere…»);
        _currentState = ENGINE_DRIVE_FORWARD;
        break;
         
        case ENGINE_DRIVE_FORWARD:
        trace («already driving — no need to change anything…»);
        break;
         
        case ENGINE_OUT_OF_FUEL:
        trace («click, changing the gear to drive won’t do anything…the car has no fuel, returning the gear to park…»);
        break;
    }
}

Этот метод является частным, поскольку доступ к нему требуется только автомобилю. Он вызывается шесть раз в секунду из update() .

Поместите код после driveForward() .

01
02
03
04
05
06
07
08
09
10
private function consumeFuel ($consumption:Number):void
{
     
    if ((_fuelSupply -= $consumption) <= 0)
    {
        _fuelSupply = 0;
        trace («phit…phit — the engine has stopped, no more fuel to run, returning the gear to park…»);
        _currentState = ENGINE_OUT_OF_FUEL;
    }
}

Теперь вы видите, как работает код, как описано ранее в разделе переменных класса.


Этот метод является частным, поскольку доступ к нему требуется только автомобилю.

Поместите код дальше.

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
public function reFuel ():void
{
    trace («attempting to refuel…»);
     
    if (_fuelSupply == 1)
    {
        trace («no need to refuel right now, the tank is full.»);
        return;
    }
     
    switch (_currentState)
    {
        case ENGINE_OFF:
        trace («getting out of the car and…»);
        break;
         
        case ENGINE_ON:
        trace («turning the key to off position, getting out of the car and…»);
        _currentState = ENGINE_OFF;
        break;
         
        case ENGINE_DRIVE_FORWARD:
        trace («changing gear from drive to park and turning the key to off position, getting out of the car and…»);
        _currentState = ENGINE_OFF;
        break;
         
        case ENGINE_OUT_OF_FUEL:
        trace («turning the key to the off position, getting out of the car and…»);
        _currentState = ENGINE_OFF;
        break;
    }
     
    var neededSupply:Number = _fullCapacity — _fuelSupply;
     
    _fuelSupply += neededSupply;
    trace («filling up to » + _fuelSupply + » gallon(s) of fuel.»);
}

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

С другой стороны, если у автомобиля нет полного бака, он проходит через знакомый оператор case / switch и запускает правильный оператор trace() .

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


Этот метод пришлось переопределить, поскольку Car наследуется от Sprite, который, в свою очередь, наследуется от EventDispatcher.

Все, что он делает, это возвращает оператор String, показанный ниже. Добавьте его как последний метод для класса Car.

1
2
3
4
override public function toString ():String
{
    return «The car is currently » + _currentState + » with a fuel amount of » + _fuelSupply.toFixed (2) + » gallon(s).»;
}

Так что теперь, всякий раз, когда вы вызываете trace(_car) из Main.as, вместо того, чтобы получать «[объектный автомобиль]», вы получаете утверждение типа «Автомобиль в настоящее время выключен с количеством топлива 1,00 галлонов».

Давайте вернемся к Main.as для тестирования. Обязательно сохраните свою работу, прежде чем двигаться вперед.


Внутри конструктора Main, сразу после того, где вы добавили ENTER_FRAME событий ENTER_FRAME . Введите код ниже.

1
2
3
4
5
6
7
///test 0
_car.turnKeyOff ();
trace (_car);
_car.driveForward ();
trace (_car);
_car.turnKeyOn ();
trace (_car);

В этот момент автомобиль выполнит все шесть действий без каких-либо промежутков времени. Событие ENTER_FRAME еще не началось.

Затем _tick метод update() чуть ниже, где _tick добавлен в _counter и вставьте следующий код.

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
_car.update (_tick);
 
//test 1 after 5 seconds of running the car and
//only if _initiatedTest1 has a value of false.
if (_counter >= 5 && ! _initiatedTest1)
{
    _initiatedTest1 = true;
     
    _car.reFuel ();
    _car.reFuel ();
    _car.driveForward ();
    _car.turnKeyOff ();
    _car.turnKeyOff ();
    _car.driveForward ();
    _car.turnKeyOn ();
    _car.driveForward ();
    trace (_car);
}
 
//test 2 after 11 seconds
if (_counter >= 11 && ! _initiatedTest2)
{
    _initiatedTest2 = true;
     
    _car.turnKeyOff ();
    _car.turnKeyOn ();
    _car.driveForward ();
    trace (_car);
}
 
//test 3 after 30 seconds
if (_counter >= 30 && ! _initiatedTest3)
{
    _initiatedTest3 = true;
     
    _car.turnKeyOn ();
    _car.turnKeyOff ();
    _car.turnKeyOn ();
    _car.driveForward ();
    _car.turnKeyOn ();
    _car.turnKeyOff ();
    _car.turnKeyOn ();
    trace (_car);
}
 
//test 4 after 35 seconds
if (_counter >= 35 && ! _initiatedTest4)
{
    _initiatedTest4 = true;
     
    _car.reFuel ();
    _car.reFuel ();
    trace (_car);
     
    _car.turnKeyOff ();
    _car.driveForward ();
    _car.turnKeyOff ();
    _car.turnKeyOff ();
    _car.turnKeyOn ();
    trace (_car);
}
 
//test 5 after 42 seconds
if (_counter >= 42 && ! _initiatedTest5)
{
    _initiatedTest5 = true;
     
    _car.driveForward ();
    trace (_car);
}
 
//test 6 after 45 seconds
if (_counter >= 45 && ! _initiatedTest6)
{
    _initiatedTest6 = true;
     
    _car.turnKeyOn ();
    _car.turnKeyOn ();
    _car.driveForward ();
    trace (_car);
}
 
///stop the car after 60 seconds
if (_counter >= 60 && ! _finalActions)
{
    _finalActions = true;
     
    trace (‘elapsed ‘ + getTimer () / 1000);
     
    _car.turnKeyOff ();
    trace (_car);
}

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

Если вы получаете ошибки, сравните ваш код с классами, которые я включил в этот урок.

Попробуйте изменить _fuelCapacity в Car, перепутать методы в некоторых или во всех разделах теста и запустить его снова. Вы увидите, что код надежный и этот процедурный FSM эффективен. Вот и все! Были сделаны.

Подожди минуту. Поскольку все хорошо, почему бы нам не добавить возможность ездить назад и турбо? В то же время, мы могли бы добавить анимацию и звук. Теперь представьте, насколько раздутым будет класс Car, если вы заставите его делать все то, что делает машина в верхней части страницы. Мы смотрим примерно на 2000 строк кода — по крайней мере. ЛОЛ! Я бы, наверное, сказал, да, конечно, я могу взломать это. Но код становится очень хрупким и его легко взломать. Так что было бы неплохо использовать другую технику.

Если объект FSM имеет простое поведение, обязательно используйте эту технику. Но если у вас есть сложный объект, который может потребоваться добавить новые функции в будущем. Может быть, даже добавьте еще несколько штатов — ну, вот тут и вступает Паттерн штатов.


Передай привет «старшему брату» процедурного ФСМ. Использование этого шаблона проектирования позволит легко поддерживать ваши состояния и вносить в них изменения, но, что самое приятное, теперь можно добавлять другие состояния без риска испортить код.

Чтобы применить этот шаблон, мы снова обратимся к нашей доверенной таблице переходов состояний. См. Шаг 4. Шаблон состояния состоит из трех частей. Первый — это интерфейс состояний, он будет содержать все действия, которые вы видите в таблице переходов состояний. Кроме того, этот интерфейс состояний может также содержать методы, которые являются общими для всех классов состояний. Во-вторых, классы состояний, соответствующие каждому состоянию, указанному в таблице переходов между штатами. И в-третьих, конечный автомат, который обычно является вашим преобразованным процедурным объектом FSM (класс Car). После преобразования Автомобиль предоставит общедоступные средства доступа и модификаторы, позволяющие осуществлять внешнее управление всеми классами штата. Автомобиль будет делегировать действия в текущее активное состояние.


Нажмите «Просмотр» и выберите «Менеджер проектов». В пределах «src» разверните до тех пор, пока не увидите папку «fsm». Щелкните правой кнопкой мыши и выберите «Добавить> Новый интерфейс …», затем нажмите «ENTER».

описание изображения

Назовите это «IState». Интерфейсы начинаются с «I» для обозначения имен.

Когда FlashDevelop откроет класс, добавьте в него код ниже.

1
2
3
4
5
6
function turnKeyOff ():void;
function turnKeyOn ():void;
function driveForward ():void;
function reFuel ():void;
function update ($tick:Number):void;
function toString ():String;

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

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


Выполните ту же процедуру при создании интерфейса IState, но вместо этого выберите «Добавить новый класс».

описание изображения

Назовите это «EngineOff». В слоте интерфейса нажмите кнопку «Добавить» и введите «IState», класс IState должен находиться в той же папке. Также должен быть установлен флажок «Генерировать реализации методов интерфейса». Нажмите «ОК» для завершения.

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

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
package com.activeTuts.fsm
{
    public class EngineOff implements IState
    {
         
        public function EngineOff()
        {
             
        }
         
        /* INTERFACE com.activeTuts.fsm.IState */
         
        public function turnKeyOff():void
        {
             
        }
         
        public function turnKeyOn():void
        {
             
        }
         
        public function driveForward():void
        {
             
        }
         
        public function reFuel():void
        {
             
        }
         
        public function update($tick:Number):void
        {
             
        }
         
        public function toString():String
        {
             
        }
         
    }
 
}

Эти классы состояний не должны расширять Sprite, поскольку все медиа-ресурсы (вторая часть) будут добавляться и контролироваться через автомобиль. Состояния будут создаваться через класс Car, передавая себя в качестве ссылки. Двухсторонняя структура композиции будет использоваться для обеспечения связи между Автомобилем и государственными классами.


Измените метод конструктора, чтобы он соответствовал приведенному ниже коду.

1
2
3
4
5
6
private var _car:Car;
 
public function EngineOff ($car:Car)
{
    _car = $car;
}

Я включил переменную _car над модифицированным конструктором. Теперь мы можем управлять классом автомобилей из этого состояния.

Давайте перейдем к реализации метода интерфейса.

Получить внутри turnKeyOff() . Проверьте таблицу переходов между штатами, чтобы увидеть, что здесь происходит. Затем сравните это с процедурным turnKeyOff() внутри класса Car. Помните, у нас все еще есть класс автомобилей в процедурном FSM. Как только вы увидите матч. Скопируйте действие для состояния ENGINE_OFF в пустой метод. Метод turnKeyOff() должен отражать то, что вы видите ниже.

1
2
3
4
public function turnKeyOff ():void
{
    _car.print («The car’s already off, you can’t turn the key counter-clockwise any further…»);
}

Оператор trace() был заменен на print() который мы добавим в класс Car позже.

Теперь войдите в метод turnKeyOn() и добавьте код, указанный далее.

1
2
_car.print («Turning the car on…the engine is now running!»);
_car.changeState (_car.getEngineOnState ());

turnKeyOn() его с таблицей переходов состояний и процедурным turnKeyOn() для состояния ENGINE_OFF чтобы убедиться, что оно одинаковое. Метод changeState() делегируется обратно автомобилю, передающемуся в восстановленном состоянии, в которое он должен измениться.

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public function driveForward ():void
{
    _car.print («click, changing the gear to drive doesn’t do anything…the car is not running, returning the gear to park…»);
}
 
public function reFuel ():void
{
    if (_car.hasFullTank () == false)
    {
        _car.print («getting out of the car and adding » + Number (_car.refillWithFuel ()).toFixed (2) + » gallon(s) of fuel.»);
    }
}
 
public function update ($tick:Number):void { }
 
public function toString ():String
{
    return ‘off’;
}

Метод driveForward() делает то же самое, что и процедурные driveForward() с _currentState установленным как ENGINE_OFF

reFuel() просит автомобиль проверить, не заполнен ли бак. Если нет, то автомобиль заправится топливом. Позже вы увидите, как работают эти два метода, когда мы изменим класс Car.

Метод update() остается пустым, поскольку автомобиль не будет работать.

toString() работает так же, как метод toString() .

Это завершает класс EngineOff.

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


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

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

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
package com.activeTuts.fsm
{
    import flash.display.Sprite;
    import flash.events.Event;
 
    public class Car extends Sprite
    {
        ///CAR STATES
        //engine off state
        //engine on state
        //drive state
        //no gas state
         
        public static const ONE_SIXTH_SECONDS:Number = 1 / 6;
         
        public static const IDLE_FUEL_CONSUMPTION:Number = .0055;
        public static const DRIVE_FUEL_CONSUMPTION:Number = .011;
         
        private var _engineOffState:IState;
        private var _engineOnState:IState;
        private var _engineDriveForwardState:IState;
        private var _engineOutOfFuelState:IState;
         
        private var _fuelCapacity:Number = 1;
        private var _fuelSupply:Number = _fuelCapacity;
         
        private var _engineTimer:Number = 0;
         
        private var _currentState:IState;
         
        public function Car ()
        {
            init ();
        }
         
        private function init ():void
        {
            initializeStates ();
        }
         
        private function initializeStates ():void
        {
            _engineOffState = new EngineOff (this);
            _engineOnState = new EngineOn (this);
            _engineDriveForwardState = new EngineDriveForward (this);
            _engineOutOfFuelState = new EngineOutOfFuel (this);
             
            _currentState = _engineOffState;//default state
        }
         
        public function update ($tick:Number):void
        {
            _currentState.update ($tick);
        }
         
        ///car functions
        public function turnKeyOn (e:Event = null):void
        {
            _currentState.turnKeyOn ();
        }
         
        public function turnKeyOff (e:Event = null):void
        {
            _currentState.turnKeyOff ();
        }
         
        public function driveForward (e:Event = null):void
        {
            _currentState.driveForward ();
        }
         
        public function reFuel (e:Event = null):void
        {
            _currentState.reFuel ();
        }
         
        public function consumeFuel ($consumption:Number):void
        {
             
            if ((_fuelSupply -= $consumption) <= 0)
            {
                _fuelSupply = 0;
                print («the engine has stopped, no more fuel to run…»);
                 
                changeState (_engineOutOfFuelState);
            }
        }
         
        public function refillWithFuel ():Number
        {
            var neededSupply:Number = _fuelCapacity — _fuelSupply;
            _fuelSupply += neededSupply;
            return neededSupply;
        }
         
        public function hasFullTank ():Boolean
        {
            var fullTank:Boolean = _fuelCapacity == _fuelSupply ?
            if (fullTank) print («no need to refuel right now, the tank is full…»);
            return fullTank;
        }
         
        public function getEngineOffState ():IState { return _engineOffState;
         
        public function getEngineOnState ():IState { return _engineOnState;
         
        public function getEngineOutOfFuelState ():IState { return _engineOutOfFuelState;
         
        public function getEngineDriveForwardState ():IState { return _engineDriveForwardState;
         
        public function changeState ($state:IState):void
        {
            _currentState = $state;
        }
         
        public function get engineTimer ():Number { return _engineTimer;
         
        public function set engineTimer ($value:Number):void { _engineTimer = $value;
         
        public function print ($text:String):void
        {
            trace ($text);
        }
         
        override public function toString ():String
        {
            return ‘The car is currently ‘ + _currentState + ‘ with a fuel amount of ‘ + _fuelSupply + ‘ gallon(s).’;
        }
    }
}

Давайте рассмотрим изменения.

Начиная с определения переменной. Вы заметите, что четыре состояния изменили тип на IState и больше не являются статическими константами.

Затем конструктор теперь вызывает init() который, в свою очередь, вызывает initializeState() . Все классы состояний создаются с помощью этого метода.

Затем наступает легкая часть, больше никаких операторов переключения. Машина просто делегирует действия в текущее состояние. Посмотрите turnKeyOff() вниз, чтобы reFuel()

Метод consumeFuel() должен был стать публичным доступом для EngineOn и EngineDriveForward.

И затем два метода, которые мы использовали в методе reFuel() hasFullTank()hasFullTank() и refillWithFuel() .

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

changeState() делает именно то, что говорит, он меняет _currentState.

Опять же, следуя строгому правилу ООП, к свойству _engineTimer можно получить доступ и изменить его с помощью этих двух методов: get engineTimer() и set engineTimer() .

print() сейчас просто передаст параметр String в оператор trace() . И затем метод toString() .


Чтобы упростить создание трех других классов, перейдите внутрь класса Car из метода initialize() . Поместите курсор в слово «EngineOn» и нажмите «CTRL + SHIFT + 1», чтобы создать приглашение. Выберите «Создать новый класс» и нажмите «ENTER».

Сопоставьте информацию, как показано на рисунке ниже, затем нажмите «ОК».

описание изображения

Это похоже на шаг 16, когда вы создали класс EngineOff. Только на этот раз мы использовали горячие клавиши FD. Кроме того, вы заметите ссылку на объект car в конструкторе, который был передан при создании экземпляра. Не забудьте добавить этот знак «$» в параметр car для вашего конструктора и единственную переменную класса _car сверху.

Сравните это с кодом ниже.

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
package com.activeTuts.fsm
{
    public class EngineOn implements IState
    {
        private var _car:Car;
         
        public function EngineOn($car:Car)
        {
            _car = $car;
        }
         
        /* INTERFACE com.activeTuts.fsm.IState */
         
        public function turnKeyOff ():void
        {
             
        }
         
        public function turnKeyOn ():void
        {
             
        }
         
        public function driveForward ():void
        {
             
        }
         
        public function reFuel ():void
        {
             
        }
         
        public function update ($tick:Number):void
        {
             
        }
         
        public function toString ():String
        {
             
        }
         
    }
 
}

Теперь вернитесь в метод initialize() в Car и повторите процесс для двух последних оставшихся классов состояний.


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

Сделайте это, просто следуйте Шагу 17, и у вас все получится. Просто помните, что вы сейчас работаете над результатами действий для ENGINE_ON .

Отлично! Когда вы закончите, сравните ваш код с классами внутри папки «StatePatternPartial1», включенной в исходную загрузку.


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

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

Мы закончим первую часть урока здесь. Во второй части мы начнем с добавления двух других состояний «EngineDriveReallyFast» и «EngineDriveBackward». Затем мы добавим элементы управления для анимации и звука, доказывающие, как легко изменять и масштабировать.


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

Вот шаги:

  1. Нарисуйте таблицу переходов состояний для вашего объекта.
  2. Создайте свой процедурный объект FSM.
  3. После того как Procedural FSM сработает, если вам нужно добавить гораздо больше функций и / или состояний, преобразуйте их в Шаблон состояний.
  4. Сначала создайте свой интерфейс IState.
  5. Создайте первый / по умолчанию класс состояний (см. Таблицу переходов состояний и процедурные действия FSM).
  6. Дублируйте копию своего процедурного объекта FSM, затем разрешите публичный доступ ко всем свойствам, которые должны контролировать классы State.
  7. Создайте остальные государственные классы.
  8. Добавить функции / состояния в соответствии с вашими требованиями. Они обычно представляются, когда вы работаете над действиями своего государства.

Увидимся во второй части!

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

Спасибо за прочтение!