Русский
Русский
English
Статистика
Реклама

State machine

Из песочницы Самые простые конечные автоматы или стейт-машины в три шага

01.07.2020 18:14:54 | Автор: admin
image

Привет, Хабр!

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


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


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


Что сделал тогда?


Так как задачу требовалось решить быстро (ну как обычно), то мой конечный автомат был реализован с помощью словарей, то есть:


  • есть список состояний (Enum)
  • список сигналов (замена классическому входному алфавиту)
  • словарь (map): состояние-сигнал-состояние

Таким образом, в нужном месте функция издает сигнал и, в зависимости от текущего состояния и сигнала, происходит переход (устанавливается следующее состояние)


Но что дальше?


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


Решение: библиотека и графический редактор.
Подробнее о реализации каждого из них расскажу в следующих статьях, а пока что покажу что получилось.


Спустя год разработки...


Графический редактор


Реализован на wpf с использованием ReactiveUI.


Тут у нас структура конечного автомата в виде графа и в виде таблицы переходов.
Накидываем узлы, соединяем их и сохраняем в xml файл.


image


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


Для тех, кому неохота клацать на ссылку (лень переходить в репозиторий)

Возможности


Две темы


image


Два представления конечного автомата:


  • в виде графа
  • в виде таблицы переходов

Валидация


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

Добавление узлов и соединений


image


Отмена действий


image


Сворачивание и перемещение узлов


image


Масштабирование


image


Выделение элементов


image


Наименования для состояний и переходов


image


Перемещение переходов


image


Удаление переходов


image


Импорт/Экспорт из/в xml



<?xml version="1.0" encoding="utf-8"?><StateMachine>  <States>    <State Name="Start" Position="37, 80" IsCollapse="False" />    <State Name="State 1" Position="471, 195.54" IsCollapse="False" />    <State Name="State 2" Position="276, 83.03999999999999" IsCollapse="False" />  </States>  <StartState Name="Start" />  <Transitions>    <Transition Name="Transition 2" From="State 2" To="State 1" />    <Transition Name="Transition 1" From="Start" To="State 2" />  </Transitions></StateMachine>

Сохранение схемы в PNG/JPEG


image


Библиотека


Реализуем конечный автомат в три шага:


  1. Создаем конечный автомат и инициализируем его структуру сохраненным из редактора файлом.

    StateMachine stateMachine = new StateMachine("scheme.xml");<br>
    
  2. Основную логику описываем в методах, которые затем навешиваем на события, которых предоставляется достаточно.

    stateMachine.GetState("State1").OnExit(Action1);stateMachine.GetState("State2").OnEntry(Action2);stateMachine.GetTransition("Transition1").OnInvoke(Action3);stateMachine.OnChangeState(Action4);
    
  3. Запускаем.

    stateMachine.Start(parameters);
    

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


А что с переходами?


Для перехода внутри функции, которая обрабатывает Entry/Exit в состояние, вызываем:


StateMachine.InvokeTransition("Transition1", parameters);

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


Что есть ещё?


  • Параметры для переходов и состояний.
  • Data словарь с данными, которые хранятся внутри StateMachine и используются для обмена данными между состояниями.

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


Для тех, кому неохота клацать на ссылку (лень переходить в репозиторий)

Возможности:


  • Начальное состояние
  • События входа и выхода для состояния
  • Событие выполнение для перехода
  • Параметры для перехода
  • Параметры для входа/выхода состояния
  • Событие изменения состояния
  • Данные для обмена между состояниями
  • Событие изменения данных
  • Импорт/Экспорт из/в xml
  • Логирование

Будущее проекта


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


Библиотека. Возможные улучшения:


  1. Асинхронность
  2. Таймеры
  3. Вложенные конечные автоматы
  4. Магия работы с элементами из схемы

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


И работа с ними происходит так:


stateMachine.GetState("State1");

А хотелось бы так


stateMachine.State1;

Сразу скажу, что dynamic не подойдет так как все равно нужно помнить название.
Тут наверное нужна какая-то кодо-генерация, но решение пока что не нашел.


Графический редактор. Возможные улучшения:


  1. Локализация
  2. Шейдеры для отрисовки элементов схемы.
  3. Вложенные конечные автоматы
  4. Автораспределение узлов
    волшебная кнопка автокомпоновки элементов на канвасе
  5. Кроссплатформенность
    Перевод проекта на AvaloniaUI

Выводы


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

Ссылки


Графический редактор, исходники на GitHub: SimpleStateMachineNodeEditor
Библиотека, исходники на GitHub: SimpleStateMachineLibrary

Подробнее..

Разделяй и властвуй Использование FSM в Unity

23.04.2021 22:09:05 | Автор: admin

Грамотная архитектура играет ключевую роль при разработке любого программного продукта. Корни большинства распространенных проблем с производительностью, расширяемостью или понятностью кода растут именно из ее отсутствия. Отсутствие строго определенной структуры проекта лишает разработчиков возможности мыслить абстракциями, понимать написанный коллегой код с первого взгляда и предугадывать место возникновения ошибки. А в некоторых случаях человек может запутаться даже в своем собственном коде, перенасыщенном сущностями и компонентами. Но практически каждый программист рано или поздно, сам ли или с помощью умной книжки, знакомится с решениями, которые хороши вне зависимости от контекста. Они настолько эффективны и универсальны, что находят место в решении множества задач, и... Да, знаю, можно не продолжать, все уже и так поняли, что я о шаблонах проектирования. Одни на них молятся, другие находили среди них свои велосипеды. Некоторые утверждают на собеседовании, что изучили их вдоль и поперек и уличили в полной бесполезности. Но все, так или иначе, слышали о них. Сегодня речь пойдет об одном из паттернов - "Состояние". А точнее, о конечных автоматах. Даже если вы относитесь к последней из перечисленных выше групп, вы наверняка сталкивались со следующим инструментом:

Минимальный аниматор главного героя в платформереМинимальный аниматор главного героя в платформере

Аниматоры в Unity построены как раз на конечных автоматах. Каждая анимация группы объектов представлена в виде состояния. Условия и порядок переходов между ними определяется в аниматоре, который является конечным автоматом. Также, неоднократно поднималась тема использования конечных автоматов для описания логики работы объектов со сложным поведением. AI ботов, управление главным героем, вот это все.

Я бы хотел сделать акцент на менее изъезженной теме. Конечный автомат позволяет эффективно управлять состоянием игры в целом. Более того, с его помощью можно однажды описать бизнес-логику абстрактной игры, и отталкиваться в дальнейшем от этой заготовки при разработке прототипов. Так, многие ГК игры, построенные по такому принципу могут отличаться друг от друга на полсотни строк и структуру сцены, реализуя всю логику на автомате. А в сложнейших и запутанных проектах новый функционал может быть внедрен максимально просто путем добавления в существующий автомат нового состояния и добавления переходов в него, исключая возможность возникновения багов и неожиданного поведения. Также, среди плюсов построения бизнес-логики приложения на автоматах можно выделить следующее:

  • Каждое состояние описывает свою логику максимально просто. Нет никаких условных блоков, каждое состояние представляет собой последовательность действий на входе и выходе и список сигналов, которые автомат обрабатывает в текущем состоянии. Таким образом, мы отображаем джойстик на входе в состояние Play и скрываем на выходе. Но даже если каким-то чудом игрок увидел джойстик на экране паузы, он не сможет двигать персонажа. И не потому, что активен еще один флаг isPaused, а просто потому, что в состоянии паузы не предусмотрено принимать сигналы с джойстика.

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

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

  • Многие баги становятся просто невозможны, потому что мы строго определяем условия переходов. Мы точно не попадем в состояние Play, пока состояние WaitMatch не получит сигнал "match_ready", а если мы захотим вернуться в лобби, мы сначала отправим серверу команду об этом, и только после сигнала "room_left" выполним переход.

  • Сама идея логики на автоматах максимально прозрачна. Видя ТЗ мы сразу понимаем, каким будет список состояний, логика, реализованная в каждом из них и граф переходов. Причем, как было отмечено выше, "внешняя" часть графа в большинстве игр будет оставаться неизменной.

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

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

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

FSM

AState

- public FSM(AState initState)

- public void Signal(string name, object data = null)

- private void ChangeState(AState newState)

- void Enter()

- void Exit()

- AState Signal()

Итак, мы имеем 2 сущности:

Конечный автомат. Интерфейс у него невелик. Начиная работу в некотором состоянии, он может принимать сигналы извне, передавая их в текущее состояние. И если оно в ответ на сигнал вернуло новое состояние, автомат выполняет переход в него. В момент перехода мы выполняем Exit для последнего состояния и Enter для следующего. Таким образом, у нас получается следующая конструкция:

 public class FSM {   private AState currentState;   public FSM(AState initState) => ChangeState(initState);      private void ChangeState(AState newState)   {     if (newState == null) return;     currentState?.Exit();     currentState = newState;     currentState.Enter();   }   public void Signal(string name, object arg = null)   {     var result = currentState.Signal(name, arg);     ChangeState(result);   } }

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

public class AState{  public virtual void Enter() => null;  public virtual void Exit() => null;  public virtual AState Signal(string name, object arg) => null;}

А в самих состояниях мы просто описываем логику

public class SLoad : AState{    public override void Enter()    {        Game.Data.Set("loader_visible",true);        var load = SceneManager.LoadSceneAsync("SceneGameplay");        load.completed+=a=>Game.Fsm.Signal("scene_loaded");    }    public override void Exit()    {        Game.Data.Set("loader_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "scene_loaded")            return new SLobby();        return null;    }    }

Как вы могли заметить, я описал логику переходов между состояниями, но даже вскользь не затронул тему хранения и изменения их списка. Дело в том, что в этом нет смысла, мы можем просто создавать новые состояния при переходе. Экземпляр состояния в большинстве случаев представляет собой 3 ссылки на функции, хранящиеся статически. И переходы между состояниями - не такое частое действие, чтобы дать сколько-то существенный оверхед. Зато мы получаем гору синтаксического сахара и статический анализ в подарок! При желании, проблема аллокаций решается очень легко, но это будет не так удобно. Кроме того, вот пример того, как такое состояние можно легко параметризовать, не усложняя при этом общий интерфейс

public class SMessage : AState{    private string msgText;    private AState next;    public SMessage(string messageText, AState nextState)    {        msgText = messageText;        btnText = buttonText;        next = nextState;    }        public override void Enter()    {        Game.Data.Set("message_text", msgText);        Game.Data.Set("window_message_visible",true);    }    public override void Exit()    {        Game.Data.Set("window_message_visible",false);    }        public override AState Signal(string name, object arg)    {        if (name == "message_btn_ok")             return next;        return null;    }}

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

...case "iap_ok":return new SMessage("Item purchased! Going back to store.", new SStore());...

В этой статье я не буду задерживаться на устройстве структуры Game.Data, используемой в состояниях, достаточно просто отметить, что это словарь, в элементах которого реализован шаблон "Наблюдатель". Объекты сцены, например, элементы UI, могут подписываться на изменения в нем и отображать гарантированно актуальные данные. Она играет ключевую роль в связи между визуализацией и логикой в моих примерах, но не является необходимым условием для работы с конечными автоматами. Сигналы в автомат же можно отправлять откуда угодно, но одним из самых универсальных примеров будет следующий скрипт.

public class ButtonFSM : MonoBehaviour, IPointerClickHandler{    public string key;        public override void OnPointerClick(PointerEventData eventData)    {        Game.Fsm.Signal(key);    }}

Иными словами, мы при клике по кнопке(на самом деле, любому CanvasRenderer) передаем соответствующий сигнал в автомат. При переходе между состояниями мы можем любым удобным нам способом включать и выключать разные Canvas, менять маски, используемые в Physics.Raycast и даже иногда менять Time.timeScale! Как бы ужасно и бескультурно это ни казалось на первый взгляд, пока сделанное в Enter отменяется в Exit, оно гарантированно не может доставить каких-либо неудобств, так что вперед! Главное - не переусердствуйте.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru