<?xml version="1.0" encoding="utf-8" ?><rss version="2.0" xmlns:tt="http://teletype.in/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:media="http://search.yahoo.com/mrss/"><channel><title>Иван</title><generator>teletype.in</generator><description><![CDATA[Меня зовут Иван, я 10 лет работаю в игровой индустрии.

Я старший с++ геймплей программист. 

Это мой маленький блог о геймдеве и программировании]]></description><image><url>https://img4.teletype.in/files/32/b1/32b1851c-5a2b-478b-b4ff-14daa8027d71.png</url><title>Иван</title><link>https://teletype.in/@jazzyjohn</link></image><link>https://teletype.in/@jazzyjohn?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><atom:link rel="self" type="application/rss+xml" href="https://teletype.in/rss/jazzyjohn?offset=0"></atom:link><atom:link rel="next" type="application/rss+xml" href="https://teletype.in/rss/jazzyjohn?offset=10"></atom:link><atom:link rel="search" type="application/opensearchdescription+xml" title="Teletype" href="https://teletype.in/opensearch.xml"></atom:link><pubDate>Wed, 13 May 2026 16:24:51 GMT</pubDate><lastBuildDate>Wed, 13 May 2026 16:24:51 GMT</lastBuildDate><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/GHEmMHmyJbz</guid><link>https://teletype.in/@jazzyjohn/GHEmMHmyJbz?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/GHEmMHmyJbz?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Руководство по анимациям в Unreal для программистов</title><pubDate>Tue, 12 May 2026 06:54:18 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/9f/8e/9f8e66d0-ccdc-4680-aae9-b1d2ed5d1ccf.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/c8/2c/c82c8459-426c-4780-862c-ce61442f085a.png"></img>В Unreal Engine 5 существует обширный набор инструментов и систем для анимации. В этом небольшом руководстве я постараюсь дать общий обзор на то, какие есть системы и как их использовать и откуда начинать копать код в случае багов и расширений. Это руководство скорее пригодится программистам и техническим аниматорам, но и остальные разработчики могут найти его полезным.]]></description><content:encoded><![CDATA[
  <h1 id="Pfcv">Основы</h1>
  <p id="KlK7">В Unreal Engine 5 существует обширный набор инструментов и систем для анимации. В этом небольшом руководстве я постараюсь дать общий обзор на то, какие есть системы и как их использовать и откуда начинать копать код в случае багов и расширений. Это руководство скорее пригодится программистам и техническим аниматорам, но и остальные разработчики могут найти его полезным.</p>
  <p id="MCIk">В unreal система координат достаточно уникальная: левая система координат с Z вверх.</p>
  <figure id="SfN0" class="m_custom">
    <img src="https://img4.teletype.in/files/b3/f6/b3f6c556-9729-4562-9eb0-b126b3558ea1.png" width="403" />
    <figcaption>Системы отчета разных движков и инструметов</figcaption>
  </figure>
  <p id="IhkT">Эта особенность вызывает некоторые сложности при экспорте анимаций и скелетов. В большей части программ для анимаций это уже учтено, и достаточно включить галочку “Unreal Export” или похожую, и проблем не будет, но если скелет выглядит странно и кости вывернуты - это первое, что стоит проверять.</p>
  <p id="QYFd">В Unreal  существуют два основных ассета. связанных со скелетной анимацией: непосредственно скелет (Skeleton) и скелетный меш (Skeletal Mesh).</p>
  <h2 id="uQBa">Skeleton  (USkeleton)</h2>
  <p id="FZUR">Основой всей анимационной логики Unreal служит skeleton ассет, содержащий иерархию костей и их расположение в базовой (неанимированной) позе. Чаще всего в Unreal используется A поза, но в большинстве случаев базовая поза не так важна, так как большая часть инструментов, для которых она используется (например аддитивные анимации), позволяет указывать базовую позу напрямую, например кадр из определенной анимации.</p>
  <figure id="jau6" class="m_custom">
    <img src="https://img1.teletype.in/files/89/d8/89d8e6eb-c3d4-48c3-9e6a-e8e60cb94201.png" width="602" />
    <figcaption>Окно редактора скелета</figcaption>
  </figure>
  <p id="ronS">В скелете определены несколько важных дополнительных сущностей. важных для анимационной системы:</p>
  <ul id="Yjrr">
    <li id="het8">Curve (Кривые) - это целочисленные значения, которые можно задавать внутри анимации и монтажей, и которые можно считывать в любом месте анимационной системы. Кривые позволяют переключать разные опции, менять параметры материалов и управлять анимационными графами. Если кривая помечена как material curve, её значение будет автоматически передаваться во все материалы, которые используется skeletal mesh. (подробнее о кривых можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/animation-curves-in-unreal-engine" target="_blank">тут</a>)</li>
    <li id="eWAA">Animation Notifies - набор стандартных событий для этого скелета. Они делятся на два типа Notify (буквально событие, на которое можно отреагировать в BP) и SyncMarker - точки синхронизации, использующиеся для бленда между анимациями.</li>
    <li id="rrp6">Blend Profiles - это маска, которая показывает какие кости и насколько активны в этом профиле. С помощью них можно проигрывать анимации только на определенных костях или конечностях.</li>
    <li id="aMTB">Retargeting – параметры ретаргетинга. Поскольку большинство анимационных ассетов привязано к конкретному скелету, для использования анимаций на совместимых скелетах необходимо указать их соответствие. Здесь задаются как явно совместимые скелеты, так и правила Retargetting.</li>
    <li id="Y5e6">Socket - это дополнительные кости, созданые внутри Unreal. Чаще всего они используются для присоединения дополнительных компонентов (например другого меша или VFX), либо для определения положения некоторой части скелета в пространстве (например - куда смотрит голова персонажа) (подробнее о Socket можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/skeletal-mesh-sockets-in-unreal-engine" target="_blank">тут</a>)</li>
  </ul>
  <p id="aG13">Если вы не можете найти какие-то настройки скелета, обратите внимание на меню Window. в котором могут быть нужные вам окна настроек. Подробное описание скелетов можно найти <a href="https://dev.epicgames.com/documentation/unreal-engine/skeletons-in-unreal-engine" target="_blank">тут</a></p>
  <h2 id="DCps">Skeletal Mesh (USkeletalMesh)</h2>
  <p id="2jll">Skeleton Mesh - это уже непосредственно меш с треугольниками, материалами и скинингом, который использует определенный Skeleton для анимации. В основном тут живут настройки, связанные с визуальной функцией модели, но есть и несколько анимационных:</p>
  <ul id="puPv">
    <li id="Jnpn">Curve (Кривые) - те же кривые,. что и для скелета, но существуют только для этого конкретного меша.</li>
    <li id="2Hqk">Morph Target - настройки, позволяющие деформировать меш.</li>
    <li id="gctz">Cloth Settings - настройки ткани.</li>
  </ul>
  <figure id="MVcW" class="m_column">
    <img src="https://img2.teletype.in/files/56/fd/56fdb40c-e5cf-49c4-bc44-0a5fc70978b7.png" width="1906" />
    <figcaption>Окно редактора Skeletal Mesh</figcaption>
  </figure>
  <p id="ZTuD">При работе с анимацией основное внимание обычно уделяется скелету, а не мешу.</p>
  <h2 id="yBmM">Skeletal Mesh Component (USkeletalMeshComponent)</h2>
  <p id="GsV5">Поскольку основой всех игровых объектов в Unreal является Actor, чтобы добавить Skeleton Mesh на Actor используется Skeleton Mesh Component. В этом компоненте соединяются все настройки. необходимые для правильного функционирования SkeletalMesh. Их достаточно много и большая их часть имеет описание, или можно понять их функции из исходников, но часть стоит упомянуть отдельно.</p>
  <ul id="pdja">
    <li id="eFDR">ComponentTick - настройка того, когда и как тикает наш компонент. Она стандартная для ActorComponent, но все равно важная для SkeletalMesh. В ней можно указать, нужно ли мешу обновляться на сервере (в некоторых играх - да, в некоторых - нет), когда тикать (чаще всего до физики, но меши, которые присоединены к другому компоненту, должны тикать в конце, чтобы не отставать на кадр от своего родителя)</li>
    <li id="T0Yy">Visibility Based Anim Tick Option позволяет настраивать то, будет ли обновляться поза в зависимости от видимости скелета. Очень важная настройка для оптимизации, так как тяжеловесные анимационные графы очень сильно бьют по производительности проекта.</li>
    <li id="7aNs">Animation Mode отвечает за то, как анимирован компонент с помощью Blueprint (о которых пойдет речь дальше), или с помощью простой анимации, или вообще каким-нибудь необычным образом из кода (например копированием поз в <a href="https://dev.epicgames.com/documentation/unreal-engine/animation-sharing-plugin-in-unreal-engine" target="_blank">Animation Sharing</a>).</li>
  </ul>
  <p id="fwm9">Этот компонент еще интересен тем, что, не смотря на режим анимации, этот компонент все равно отвечает за финальное обновление позы и Transform конкретного меша. Даже при многопоточной логике  обновления анимационных графов, о которых мы поговорим позже, финальное положение костей все равно обновляется в этом компоненте, что делает его идеальной точкой старта для поиска багов. Сам компонент унаследован от USkinnedMeshComponent, которйы в свою очередь унаследован от UMeshComponent. Эти два класса и сам USkeletalMeshComponent позволяют достаточно быстро определить стандартные проблемы, связанные с анимациями.  Например: передается ли кривая в материалы (USkeletalMeshComponent::ApplyAnimationCurvesToComponent), или обновляется ли поза (USkinnedMeshComponent::ShouldTickPose и USkinnedMeshComponent::TickPose), или вообще тикает ли наш скелет (USkeletalMeshComponent::TickComponent).</p>
  <h1 id="m76G">Анимации</h1>
  <p id="gniI">Базовым абстрактным ассетом, содержащим анимационную информацию является UAnimationAsset. От него унаследованы все остальные ассеты, которые отвечают за анимации.</p>
  <h2 id="iG7l">Animation Sequence (UAnimSequence)</h2>
  <p id="RUtN">Каждая анимация представлена в Unreal как Animation Sequence. Она жестко привязана к Skeleton и не может существовать без конкретного скелета. Тут живут все движения костей, которые запекались в анимационной программе, также тут можно добавлять анимации кривых и аддитивных треков. Кроме того тут определены настройки того как эту анимацию воспринимать остальной анимационной системе: аддитивная ли это анимация или обычная (Additive Anim Type), есть ли у этой анимации Root Motion, и если есть, то от какой позы он считается (Все эти настройки расположены в секции Root Motion) . В анимации можно добавлять нотификаторы: как определенные раньше в Skeleton, так и любые доступные. Нотификаторы могут варьироваться от событий до полноценных блюпринтов, выполняющих некоторую игровую логику или вызывающих выполнение такой логики в других системах. (Подробнее про нотификаторы будет ниже)</p>
  <figure id="r2ab" class="m_custom">
    <img src="https://img2.teletype.in/files/53/e9/53e98186-2c44-4d33-9000-b6c06c901969.png" width="602" />
    <figcaption>Окно редактора  анимаций</figcaption>
  </figure>
  <p id="qJLk">Обычно этот класс не расширяют в c++, а просто создают ассеты из него, но часто бывает так, что необходимо добавить какую-нибудь важную информацию в анимацию. Для этого в Unreal UAssetUserData и UAnimMetaData можно заводить дополнительные типы данных и хранить их прямо в анимации. Затем их может использовать либо игровая логика, либо узлы AnimationBlueprint. Эти данные присутствуют у всех анимационных ассетов, унаследованных от UAnimationAsset, в том числе и у UAnimSequence.</p>
  <figure id="YPAV" class="m_custom">
    <img src="https://img4.teletype.in/files/ff/34/ff3412e4-7544-4641-9a41-ffd2ebe3bbff.png" width="574" />
    <figcaption>User Data и Meta Data</figcaption>
  </figure>
  <p id="bNQV">Подробнее про анимации можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/animation-sequences-in-unreal-engine" target="_blank">тут</a></p>
  <h2 id="2Rrc">Animation Montage (UAnimMontage)</h2>
  <p id="kRVV">Animation Montage - это склейки и комбинации нескольких анимаций. Они используются для того, чтобы комбинировать анимации. С помощью монтажей можо легко реплицировать RootMotion и проигрывание анимаций по сети (Такие плагины как Game Ability System очень сильно зависят от монтажей и вокруг них строят свою систему репликации анимаций).</p>
  <p id="GsMZ">Монтаж разбит на слоты (в дальнейшем они будут полезны при проигрывании разных анимаций на разных частях скелета или Animation Blueprint). Каждый слот состоит из последовательности анимаций.</p>
  <figure id="9XiG" class="m_custom">
    <img src="https://img4.teletype.in/files/75/94/759429bc-ec03-47aa-8c24-922596e68f69.png" width="602" />
    <figcaption>Таймлайн анимационного монтажа </figcaption>
  </figure>
  <p id="vdaa">Монтажи разбиты на секции. Секции можно настраивать, чтобы добиться циклов или переходов в зависимости от команд из игровой логики. В монтажи также можно добавлять нотификаторы и кривые.</p>
  <figure id="1kvv" class="m_custom">
    <img src="https://img4.teletype.in/files/f3/c2/f3c287fa-3741-4179-b4e7-d17524170c82.png" width="432" />
    <figcaption>Настройки секций монтажа</figcaption>
  </figure>
  <p id="Nazd">Настройки BlendIn и BlendOut позволяют настраивать бленды при старте монтажа и его окончания. Чаще всего монтажи играют роль некоего действия, которое совершает персонаж. Такое действие должно начать проигрываться сразу и желательно реплицироваться по сети. Кроме того, как будет видно ниже, в AnimationBlueprint монтажи живут обособленно в анимационном графе и чаще всего нет отдельного перехода в монтаж, поэтому. чтобы позы не переключались резко, логика проигрывания монтажа берет на себя контроль за блендом поз.</p>
  <p id="ZdCa">Подробнее про монтажи можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/animation-montage-in-unreal-engine" target="_blank">тут</a></p>
  <h2 id="ZIx4">Blend Space(UBlendSpace)</h2>
  <p id="et0q">Blend Space позволяет смешивать разные анимации в зависимости от двух параметров: горизонтальная и вертикальная оси. Стандартным примером использования является создание красивой анимации передвижения, где по горизонтальной оси откладывают направление перемещения (угол относительно прямого направления), а по вертикальной оси - скорость передвижения. Анимации располагают в разных точках образовавшейся плоскости. Когда надо выбрать позу, BlendSpace по входящим параметрам определяет точку на плоскости, находит ближайшие анимации и. в зависимости от расстояния от них до текущей точки, смешивает их в одну позу.</p>
  <figure id="wcgS" class="m_custom">
    <img src="https://img1.teletype.in/files/c0/41/c041d0c3-7704-4c65-b3d6-c416ed70edb8.png" width="602" />
    <figcaption>Окно редактора Blend Space</figcaption>
  </figure>
  <p id="G27y">Подробнее про Blend Space можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/blend-spaces-in-unreal-engine" target="_blank">тут</a></p>
  <h1 id="Tswm">Anim Notify</h1>
  <p id="p5pI">Нотификаторы (наследники класса UAnimNotify и UAnimNotifyState) - главный механизм передачи информации из анимационной системы в остальные подсистемы игры. Нотификаторы делятся на два типа:</p>
  <ul id="cqLR">
    <li id="uf3S">Наследники UAnimNotify - единоразовое событие. Если при проигрывании анимации или монтажа встречается нотификатор, то вызывается функция UAnimNotify::Notify.</li>
    <li id="4o7G">Наследники UAnimNotifyState - нотификатор с длительностью, активный на протяжение определенного числа кадров. Анимационная система вызывает UAnimNotifyState::NotifyBegin, UAnimNotifyState::NotifyEnd, UAnimNotifyState::NotifyTick на старте, при завершении и тике нотификатора соответственно.</li>
  </ul>
  <p id="WFjA">Нотификаторы удобно использовать для вызова геймплейных событий, проигрывания звуков, или визуальных эффектов непосредственно из анимации.</p>
  <figure id="2adp" class="m_custom">
    <img src="https://img4.teletype.in/files/f5/1a/f51af086-7d53-4851-861c-67c1284f7add.png" width="602" />
    <figcaption>Пример нескольких нотификаторов</figcaption>
  </figure>
  <p id="kVmA">Подробнее про нотификаторы можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/animation-notifies-in-unreal-engine" target="_blank">тут </a></p>
  <h1 id="8vcv">Animation Blueprint (ABP)</h1>
  <p id="O9i9">Анимационный блюприт (иногда анимационный граф) является основным и стандартным способом управления анимациями в Unreal. С точки зрения редактора, ABP привязан к конкретному скелету и состоит из двух частей. Первая часть - это стандартный для любого Blueprint EventGraph: здесь можно создавать логику, заполнять некоторые важные флаги и параметры для работы ABP. Вторая часть - это AnimationGraph (анимационный граф), роль которого  собрать финальную позу скелета со всеми кривыми, морфами и прочими метаданными.</p>
  <h2 id="UuKo">Animation Graph</h2>
  <p id="sNIh">Анимационный граф состоит из последовательности AnimNode (FAnimNode_Base) (анимационных узлов), которые выполняют логику, чтобы получить анимационную позу, повлиять на метаданные, либо переключить между разными частями графа.</p>
  <figure id="MeEP" class="m_custom">
    <img src="https://img2.teletype.in/files/de/26/de26e1de-6952-4e4b-83e4-9a126211c348.png" width="602" />
    <figcaption>Анимационный граф</figcaption>
  </figure>
  <p id="TLdy">У каждого AnimNode есть один выход, в который передается результат работы узла, но может быть несколько входов. Какой из них использовать определяет сам узел на основе своей внутренней логики.</p>
  <p id="zNFX">В Unreal создано очень много готовых узлов, с помощью которых можно создать почти любое поведение. Я остановлюсь на нескольких самых важных и часто используемых.</p>
  <h2 id="kqvZ">Sequence Player (FAnimNode_SequencePlayerBase)</h2>
  <figure id="rMLS" class="m_custom">
    <img src="https://img2.teletype.in/files/94/01/94018554-098b-4d38-a1a7-207bc83b5729.png" width="208" />
  </figure>
  <p id="xqTE">Узел, который проигрывает Animation Sequence. Самый простой и атомарный элементы любого ABP. В этом узле можно задать какую анимацию надо играть и указать настройки того, как надо играть её.</p>
  <h2 id="W2Ty">State Machine (FAnimNode_StateMachine)</h2>
  <figure id="nBVC" class="m_custom">
    <img src="https://img3.teletype.in/files/e8/2f/e82f047e-5cdd-484a-9241-ef448f2c19bc.png" width="220" />
  </figure>
  <figure id="qEAK" class="m_custom">
    <img src="https://img1.teletype.in/files/cc/62/cc62c245-c189-44bd-88a6-d273b5cdcb93.png" width="526" />
    <figcaption>Пример простого анимационного автомата</figcaption>
  </figure>
  <p id="Qhm1">Стандартный конечный автомат для описания конкретных анимационных состояний. Каждое состояние представляет из себя Animation Graph, что позволяет вкладывать в состояние еще один State Machine. Переходы между состояниями осуществляются с помощью условий или событий. У состояний существует набор событий, на которые можно повесить функции или события из EventGraph</p>
  <figure id="rkYb" class="m_custom">
    <img src="https://img2.teletype.in/files/9f/3a/9f3a2676-c527-4714-bddf-4e6d9c93ee3d.png" width="424" />
  </figure>
  <p id="Mxt4">Побольше почитать про State Machine можно <a href="https://dev.epicgames.com/documentation/unreal-engine/state-machines-in-unreal-engine" target="_blank">тут </a></p>
  <h2 id="NcZ6">Slot (FAnimNode_Slot)</h2>
  <figure id="ar3r" class="m_custom">
    <img src="https://img1.teletype.in/files/88/80/88803172-3fce-488a-bce9-fc0b84ac18b7.png" width="262" />
  </figure>
  <p id="WrIB">Узел, который играет слот из монтажа. ABP позволяет играть неограниченное количество монтажей параллельно, но с точки зрения Анимационного графа это не значит ничего для финальной позы, монтаж просто помещается в ABP, начинается, проигрывается и заканчивается. Для того, чтобы анимация из него попала в финальную позу, как раз и существует узел Slot. Этот узел использует позу одного из слотов активного сейчас монтажа, и если монтажа нет, то он просто прокидывает входящую позу без изменений.</p>
  <p id="Q7XB">Слоты разбиты на группы и редактируются для конкретного скелета в Animation Slot Manager, который можно открыть с помощью меню Window.</p>
  <figure id="Izjf" class="m_custom">
    <img src="https://img2.teletype.in/files/58/ec/58ec093e-c9d4-4c7e-8327-24ec57b7f4fc.png" width="364" />
  </figure>
  <h2 id="Vo22">BlendByBool (FAnimNode_BlendListByBool)</h2>
  <figure id="9u5N" class="m_custom">
    <img src="https://img2.teletype.in/files/1d/f8/1df85b18-039c-4134-b7ba-d647850bd7af.png" width="277" />
  </figure>
  <p id="PpGA">Узел, который выбирает либо одну входящую позу, либо вторую, в зависимости от булевого флага. По аналогии работают и остальные BlendBy узлы</p>
  <h2 id="5w0a">Apply Additive (FAnimNode_ApplyAdditive)</h2>
  <figure id="RMJP" class="m_custom">
    <img src="https://img1.teletype.in/files/07/e8/07e8511a-cdaa-412c-855f-16db1d43a2f1.png" width="237" />
  </figure>
  <p id="hqmQ">Добавляет в позу аддитивную позу. Обычно используется для проигрывания дополнительных небольших анимаций или поз смещения, например анимации реакции на удар или blendspace поворота головы за целью.</p>
  <h2 id="lKJg">Layered blend per bone (FAnimNode_LayeredBoneBlend)</h2>
  <figure id="7pzA" class="m_custom">
    <img src="https://img1.teletype.in/files/40/e1/40e19914-8996-4294-b8a4-630cfe2a38bd.png" width="297" />
  </figure>
  <p id="c1LZ">Позволяет разбить скелет на части и указать для каждой части позу, которую надо будет использовать. Чаще всего используется для анимаций верхней части тела (например анимации стрельбы, которая играется независимо от анимации ног). Для разбиения скелета на части можно указывать непосредственно кости, либо BlendProfile, заданные в Skeleton.</p>
  <p id="31l4">Подробнее про узлы можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/animation-blueprint-nodes-in-unreal-engine" target="_blank">тут</a></p>
  <h2 id="1tzW">Многопоточность</h2>
  <p id="ehMz">ABP в Unreal может обновлять свои узлы и позы многопоточно, что очень положительно влияет на производительность графа и игры в целом. Для того, чтобы ваш граф использовал многопоточность, в ABP стоит использовать Blueprint Thread Safe Update Animation и только Thread Safe функции.</p>
  <h2 id="0OTy">Fast Update</h2>
  <p id="bz7a">Как видно из скришнотов выше, некоторые узлы отмечены белой молнией. Эти узлы помечены для быстрой обработки. Такие узлы позволяют компилятору blueprint копировать значения переменных и тем самым избегать вызовов блюпринтовых методов и функций. Это улучшает производительность всего ABP. Для этого ни один пин узла не должен содержать никаких вычислений кроме как чтения переменных( а лучше всего вообще bind переменные в параметры узла).</p>
  <p id="AZHF">Подробнее про многопоточность и Fast Update можно почитать <a href="https://dev.epicgames.com/documentation/unreal-engine/animation-optimization-in-unreal-engine?application_version=5.7&utm_source=editor&utm_medium=docs&utm_campaign=rich_tooltips" target="_blank">тут </a></p>
  <h2 id="cT6Z">FAnimNode_Base</h2>
  <p id="APnT">В определенный момент разработки может наступить момент, когда уже существующих AnimNode недостаточно и нужно реализовать новую логику. Для этого нужно создать два класса: класс, содержащий непосредственно логику изменения позы, и класс, необходимый для редактора графов.</p>
  <ul id="exee">
    <li id="RdC4">Runtime узел - наследуется от FAnimNode_Base, и должен существовать в c++ проекте с типом &quot;Runtime&quot;. Этот класс будет использоваться во время игры и выполнять нужную логику.</li>
    <li id="SUJm">Узел редактора - наследуется от UAnimGraphNode_Base., он содержит необходимые для редактора настройки и логики нового узла, и он должен существовать в c++ проекте с типом &quot;UncookedOnly&quot;.</li>
  </ul>
  <p id="9S6D">Выполнение узла разбито в ABP на несколько этапов, и для них всех в FAnimNode_Base существуют виртуальные функции. Из интересных стоит отметить следующие:</p>
  <ul id="a3uS">
    <li id="FYQC">Initialize_AnyThread функция для инициализации вашего узла</li>
    <li id="C4J8">Update_AnyThread функция для обновления логики узла.</li>
    <li id="vAwK">PreUpdate функция, которая выполняется до вызова Update в GameThread; позволяет сохранить данные, которые могут быть не threadsafe.</li>
    <li id="ka0Q">Evaluate_AnyThread функция для обновления и создания финальной позы.</li>
  </ul>
  <p id="jZRQ">Чтобы добавить вход в новый узел, ему надо задать поле следующего вида</p>
  <pre id="Lwb7" data-lang="cpp">UPROPERTY(EditAnywhere, BlueprintReadWrite, Category=Links)
FPoseLink BasePose;</pre>
  <p id="ON7T">При этом в Update_AnyThread  стоит вызвать GetEvaluateGraphExposedInputs().Execute(Context);. чтобы входящие узлы тоже получили шанс выполнить свою логику, и вызвать FPoseLinkBase::Update(const FAnimationUpdateContext&amp; InContext) на самой BasePose, передав соответствующий контекст. Примером того, как работать с входящими позами, может служить класс FAnimNode_BlendListBase, который блендит список входящих поз с разными весами. Создавая новый узел, полезно поглядеть что и как делается в этом классе.</p>
  <h2 id="HEVM">UAnimInstance и FAnimInstanceProxy</h2>
  <p id="Rqyi">Программно Animation Blueprint разделён на две составляющие:</p>
  <ul id="x3FP">
    <li id="jOuP">UAnimInstance – базовый класс экземпляра ABP. Может быть расширен в Blueprint или C++.</li>
    <li id="e6RM">FAnimInstanceProxy – прослойка, обеспечивающая потокобезопасный доступ к данным во время обновления графа и скелета. Именно с ней взаимодействуют FAnimNode. Результирующая поза и метаданные (например, Root Motion) сохраняются в прокси, затем считываются UAnimInstance и передаются в USkeletalMeshComponent.</li>
  </ul>
  <h1 id="viFg">Debug</h1>
  <p id="xWds">В Unreal есть несколько инструментов для дебага анимаций и скелетов.</p>
  <p id="mT8l">Существуют флаги, позволяющие рендерить кости и их положение в пространстве: консольная команда Show Bones. Если открыть окно редактирования какого-нибудь ABP, то, как и с любым Bluerprint, если у вас запущена PIE, вы можете выбрать экземпляр работающего в мире ABP и посмотреть, что с ним происходит с помощью редактора.</p>
  <figure id="bL28" class="m_custom">
    <img src="https://img1.teletype.in/files/ca/ad/caaded69-7d28-47fa-9946-6cbb279ef973.png" width="602" />
    <figcaption>Выбор экземпляра графа для дебага</figcaption>
  </figure>
  <p id="GHh1">Основным инструментом является конечно же Rewinder Debbuger, который записывает все события, происходящие с актором и с ABP, и позволяет, используя timeline, подробно кадр за кадром просматривать, что происходит. Для того. чтобы открыть Rewinder, нужно воспользоваться меню Tools -&gt; Debug</p>
  <figure id="fDug" class="m_custom">
    <img src="https://img1.teletype.in/files/40/c4/40c4fc38-d04c-47c5-ba0d-cbabe4e34925.png" width="532" />
  </figure>
  <p id="Ef8s">После этого выбрать нужного актора и нажать запись. <br />N.B. я не уверен, насколько эта проблема зависит от версии Unreal или от сборки, но у меня Rewinder работает только в netmode PIE Play As Standalone. После этого вы можете спокойно анализировать, что происходит с мешем или ABP</p>
  <figure id="1GLd" class="m_custom">
    <img src="https://img2.teletype.in/files/53/c9/53c91de3-33a3-4403-ae52-f522211da408.png" width="602" />
    <figcaption>Окно Rewinder</figcaption>
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/XHjpMySMARU</guid><link>https://teletype.in/@jazzyjohn/XHjpMySMARU?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/XHjpMySMARU?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Игровой ИИ. State Tree</title><pubDate>Tue, 24 Mar 2026 07:55:01 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/e7/66/e766661f-6094-4851-85e7-2d103bf6d3c3.png"></media:content><description><![CDATA[<img src="https://img1.teletype.in/files/c0/98/c098348d-73ac-4bb4-a62e-35fa991f1c9e.png"></img>В прошлый раз мы познакомились с State Tree. Сегодня посмотрим как передавать данные между разными модулями дерева.Также не забывайте подписываться на мой канал, чтобы не пропустить следующие части.]]></description><content:encoded><![CDATA[
  <p id="hrrU">В <a href="https://teletype.in/@jazzyjohn/n54wgKTJvXq" target="_blank">прошлый раз</a> мы познакомились с State Tree. Сегодня посмотрим как передавать данные между разными модулями дерева.Также не забывайте подписываться <a href="https://t.me/Vaniagramming" target="_blank">на мой канал</a>, чтобы не пропустить следующие части.</p>
  <h2 id="F1Xi">Evaluator (Вычислитель)</h2>
  <figure id="kciQ" class="m_custom">
    <img src="https://img1.teletype.in/files/c0/98/c098348d-73ac-4bb4-a62e-35fa991f1c9e.png" width="510" />
    <figcaption>Пример вычислителя для поиска подходящей цели</figcaption>
  </figure>
  <p id="XOyJ">Evaluator - это особый тип узлов в StateTree, который активен все время выполнения конкретного дерева, и который позволяет дополнять или получать данные, необходимые для работы дерева. Отличие от глобальных задач скорее архитектурное, чем в логике выполнения: вычислители отвечают за сбор и вычисление данных, а не изменение состояния агента или какие-то действия. Простым примером может служить вычислитель поиска цели, который из списка известных Акторов выбирает лучшую цель, или вычислитель, который считает расстояние от текущей цели до агента и сохраняет его в переменную.</p>
  <h1 id="Sxws">Data Flow</h1>
  <p id="9BxF">Давайте разберемся, как с помощью параметров и полей можно передавать данные между разными состояниям. Начнем с полей любых задач и вычислителей: если задача  глобальна или актуальна для текущего состояния (например, эта задача существует на родительском поле), то это поле можно передать в другую задачу или вычислитель. Для этого в StateTree используется binding полей (по аналогии с полями в Animation Blueprint).</p>
  <figure id="FSyq" class="m_custom">
    <img src="https://img3.teletype.in/files/ea/42/ea422e6d-f88a-4770-892a-effdba015a10.png" width="602" />
    <figcaption>Пример Binding векторного поля к переменной Актора</figcaption>
  </figure>
  <p id="ZG1V">Если поле определено в Blueprint, то оно по умолчанию доступно для binding. Для полей в c++ существует несколько правил: поле должно иметь макрос UPROPERTY, также у него должны быть настройки EditAnywhere и BlueprintReadWrite, и поле должно быть публичным:</p>
  <pre id="MzGk" data-lang="cpp">//State tree expose data
UPROPERTY(EditAnywhere, BlueprintReadWrite)
float ParticipantsCount;</pre>
  <h2 id="c5jo">Input, Output, Context</h2>
  <p id="tz4k">Определяя поля возможно  разместить их для StateTree. Если поле находится в категории Input, значит поле содержит входящие данные для работы вашего класса. Если поле находится в категории Output, то ваш класс запишет результат своей работы в это поле. Поле из категории Context содержит необходимый конекст для работы класса, поэтому StateTree попробует заполнить его из вашей схемы автоматически. Поля без категории расцениваются как настройки, но их все равно можно привязывать к полям и параметрам. <br /><strong>N.B.</strong> стоит помнить, что даже поле должно быть видимым для настройки в редакторе, чтобы была возможность его редактировать в редакторе StateTree. Blueprint поле должно быть публичное и Instance Editable, а поле из c++ должно иметь макроc EditAnywhere.</p>
  <figure id="ucff" class="m_custom">
    <img src="https://img2.teletype.in/files/59/61/596192a3-f510-4340-8127-061d3272499a.png" width="571" />
    <figcaption>Пример разметки полей Blueprint задачи</figcaption>
  </figure>
  <h2 id="4QfC">Parameters (Параметры)</h2>
  <figure id="C24L" class="m_custom">
    <img src="https://img1.teletype.in/files/0b/d8/0bd88317-2bf7-43f4-a40f-8885064a027c.png" width="602" />
    <figcaption>Пример глобального параметра </figcaption>
  </figure>
  <p id="sWX5">Параметры являются набором переменных, которые обладают конкретной зоной видимости. Например: глобальные параметры существуют все время выполнения дерева, параметры состояния определены, пока дерево находится в этом состоянии. Простым примером использования параметров может служить глобальный таймер между атаками: в такой параметр записывают данные либо состояния и таски, выполняющие атаки, и он не сбрасывается при смене состояний.</p>
  <figure id="OYls" class="m_custom">
    <img src="https://img3.teletype.in/files/25/00/25009c36-f26a-498d-8c5e-d38c5c59cd47.png" width="602" />
    <figcaption>Пример параметра определенного в состоянии</figcaption>
  </figure>
  <p id="LC32">Примером параметра для состояния может служить точка назначения условного состояния передвижения. Родительское состояние содержит параметр-вектор точки назначения. Первое состояние - потомок находит подходящую точку для перемещения с помощью задачи и записывает его в параметр родителя, а второе состояние - с помощью задачи передвижения перемещается в заданную точку.</p>
  <p id="OiNb">Параметры также передаются в поля классов с помощью binding.</p>
  <figure id="Eoyv" class="m_custom">
    <img src="https://img3.teletype.in/files/24/15/2415e5e5-4675-40dd-aec7-95d2ae4a7c06.png" width="602" />
    <figcaption>Пример Binding параметра в поле задачи</figcaption>
  </figure>
  <h2 id="ZnyW">Property Function</h2>
  <p id="AxES">По умолчанию невозможно использовать функции (даже BlueprintCallable) для назначения полей (binding), но существует подход, который это исправляет. Существует возможность определить класс, унаследованный от <strong>FStateTreePropertyFunctionBase</strong>.</p>
  <p id="Uo8l">Такие классы выполняются перевычеслением назанченного поля и позволяют поменять логику выполнения назначения, или поменять результат назанчения. Примером может служить <strong>FStateTreeGetActorLocationPropertyFunction</strong>, которая позволяет назначать в поля позицию Актора напрямую.</p>
  <figure id="6nJ3" class="m_custom">
    <img src="https://img2.teletype.in/files/97/ea/97ea7f8b-cb35-437c-a639-41912ca4a4d9.png" width="502" />
    <figcaption>Пример Binding Property Function</figcaption>
  </figure>
  <h1 id="aLy6">Дебаг и Тестирование</h1>
  <p id="0tDJ">Одной из удобных возможностей работы со StateTree является то, что почти любой узел дерева может быть выключен или переключен в фиксированный режим во время работы в редакторе. Это очень сильно облегчает разработку, настройку и тестирование деревьев.</p>
  <p id="3MDG">Например любое условие может быть переключено в режим Force True или Force False.</p>
  <figure id="nDGc" class="m_custom">
    <img src="https://img3.teletype.in/files/2d/ae/2dae72b1-033f-45d4-a65d-dfa1e91de342.png" width="602" />
  </figure>
  <p id="jo3j">Любая задача может быть выключена.</p>
  <figure id="yrH0" class="m_custom">
    <img src="https://img1.teletype.in/files/0e/52/0e52d191-23c2-436a-a5d0-11e501c73f18.png" width="602" />
  </figure>
  <p id="P6qg">Любой переход тоже может быть выключен</p>
  <figure id="No6D" class="m_custom">
    <img src="https://img2.teletype.in/files/d9/b5/d9b5bf19-a20d-44bf-b3c6-cad0f44d4832.png" width="602" />
  </figure>
  <p id="rx0N">В Unreal Engine 5.7 для дебага используется Rewinder, который записывает все происходящее с Aкторами во время PIE, но у State Tree есть свое окно дебага, которое позволяет более подробно отслеживать что происходит с деревом и, например, какие переходы выполнялись с какими условиями.</p>
  <figure id="yjXu" class="m_custom">
    <img src="https://img4.teletype.in/files/b1/58/b15834a7-9ebd-43bb-bbb7-18564116cc61.png" width="602" />
    <figcaption>Пример дебага StateTree</figcaption>
  </figure>
  <p id="bYoD">В следующий раз мы попробуем собрать простой боевой ИИ, используя StateTree</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/JM_NVCOfGe0</guid><link>https://teletype.in/@jazzyjohn/JM_NVCOfGe0?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/JM_NVCOfGe0?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Лента Мёбиуса часть 3.</title><pubDate>Mon, 09 Feb 2026 09:53:41 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/61/7d/617db61a-2b9e-4c7c-a159-830907557156.png"></media:content><category>Unreal Engine 5</category><description><![CDATA[<img src="https://img3.teletype.in/files/6e/2a/6e2a358d-761d-436e-8367-de3d375e5e99.png"></img>В прошлый раз я рассказал, как организовать удобное вычисление направления гравитации для нашего персонажа. Сегодня попробуем научить контроллер персонажа поворачиваться по гравитации и использовать наш кастомный вектор гравитации.]]></description><content:encoded><![CDATA[
  <p id="JO1A">В прошлый раз я рассказал, как организовать удобное вычисление направления гравитации для нашего персонажа. Сегодня попробуем научить контроллер персонажа поворачиваться по гравитации и использовать наш кастомный вектор гравитации.</p>
  <h2 id="C5WZ">UCRCharacterMovementComponent</h2>
  <p id="dHFb">Для начала нам понадобится расширить компонент движка, чтобы мы могли дополнить его функционал.</p>
  <p id="O7ky">Начнем с создания класса,  унаследованного от <strong>UCharacterMovementComponent</strong>. Для начала добавим поле, отвечающие за “вверх” нашего персонажа, добавим указатель на контроллер гравитации, и создадим функцию, с помощью которой можно назначить указатель.</p>
  <p id="LuTJ">N.B. Создание таких Setter может быть не самым правильным, так как мы создаем зависимости между классами, создающими контроллеры гравитации, и нашим компонентом. В общем случае лучше использовать интерфейсы или события. Но, поскольку мы сейчас концентрируемся на тестировании механики и простой реализации ленты Мёбиуса, такой подход допустим. В дальнейшем мы сможем легко его переписать.</p>
  <pre id="tIX9" data-lang="cpp">UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class FPSGAME_API UCRCharacterMovementComponent : public UCharacterMovementComponent
{
	GENERATED_BODY()

public:
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
	FVector GetUpOrientation() const;
	UFUNCTION(BlueprintCallable)
	void SetGravityController(UObject* InGravityController);

	TOptional&lt;FVector&gt; UpOverride;
	UPROPERTY()
	TScriptInterface&lt;class ICRGravityController&gt; GravityController;
};</pre>
  <pre id="l2BL" data-lang="cpp">FVector UCRCharacterMovementComponent::GetUpOrientation() const
{
	return UpOverride.Get(FVector::UpVector);
}

void UCRCharacterMovementComponent::SetGravityController(UObject* InGravityController)
{
	if (InGravityController-&gt;Implements&lt;UCRGravityController&gt;())
	{
		GravityController = TScriptInterface&lt;ICRGravityController&gt;(InGravityController);
	}
}</pre>
  <p id="tOzT"></p>
  <p id="avti">Во время тика нашего компонента получим у гравитационного контроллера (если он есть) ориентацию и гравитацию, сохраним ориентации и проставим как направление гравитации.</p>
  <p id="zudK"></p>
  <pre id="fdoA" data-lang="cpp">void UCRCharacterMovementComponent::TickComponent(float DeltaTime, ELevelTick TickType,
	FActorComponentTickFunction* ThisTickFunction)
{
	Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
	UE_VLOG(GetOwner(), LogCRCharacterMovementComponent, VeryVerbose,  TEXT(&quot;Mode %s CustomMode %s&quot;), *GetMovementName(), *GetCustomMovementName());
	if (GravityController)
	{		
		FVector ControllerUp;
		FVector Gravity;	
		GravityController-&gt;GetGravityAndOrientation(GetActorLocation(), Gravity, ControllerUp);
		SetGravityDirection(Gravity);
		UpOverride = ControllerUp;
	}
}</pre>
  <h2 id="EwPV">ACRPlayerController</h2>
  <p id="iiiQ">Второй класс, который нам необходимо расширить - это класс нашего контроллера. В данном примере мы используем базовые настройки Unreal Engine 5.6 для шаблона FPS. Ориентация персонажа следует за камерой, контроллер поворачивает камеру. Для третьего лица (или если у вас сложный контроллер, который выполняет некоторые дополнительные функции) может потребоваться дополнительная логика.</p>
  <p id="jrst">Унаследуем наш от <strong>APlayerController</strong>. Переопределим функцию <strong>UpdateRotation </strong>и создадим пару функций для удобного вычисления ориентации, плюс добавим поле, чтобы кешировать предыдущую ориентацию “вверх”.</p>
  <pre id="3yKH" data-lang="cpp">UCLASS()
class FPSGAME_API ACRPlayerController : public APlayerController
{
	GENERATED_BODY()

	virtual void UpdateRotation(float DeltaTime) override;
public:
	static FRotator GetUpRelativeRotation(FRotator Rotation, FVector GravityDirection);
	static FRotator GetUpWorldRotation(FRotator Rotation, FVector GravityDirection);
 
private:
	FVector LastFrameUp = FVector::ZeroVector;
};
</pre>
  <p id="xm1s">Функции <strong>GetUpRelativeRotation </strong>и <strong>GetUpWorldRotation </strong>достаточно простые: мы вычисляем итоговый поворот ориентации относительно того, насколько у нас гравитационный “вверх” отличается от мирового. Разница в том, что в одном случае мы считаем, что поворот локальный, а в другом - мировой. </p>
  <pre id="0SSO" data-lang="cpp">FRotator ACRPlayerController::GetUpRelativeRotation(FRotator Rotation, FVector UpDirection)
{
	if (!UpDirection.Equals(FVector::UpVector))
	{
		FQuat GravityRotation = FQuat::FindBetweenNormals(UpDirection, FVector::UpVector);
		return (GravityRotation * Rotation.Quaternion()).Rotator();
	}
 
	return Rotation;
}
 
FRotator ACRPlayerController::GetUpWorldRotation(FRotator Rotation, FVector UpDirection)
{
	if (!UpDirection.Equals(FVector::UpVector))
	{
		FQuat GravityRotation = FQuat::FindBetweenNormals(FVector::UpVector, UpDirection);
		return (GravityRotation * Rotation.Quaternion()).Rotator();
	}
 
	return Rotation;
}</pre>
  <p id="5tsk">Функция поворота <strong>UpdateRotation </strong>требует более подробного рассмотрения.</p>
  <p id="Shlm">Для начала получим требуемое направление “вверх” из <strong>UCRCharacterMovementComponent</strong></p>
  <pre id="JJ3i" data-lang="cpp">	FVector UpDirection = FVector::UpVector;
	if (ACharacter* PlayerCharacter = Cast&lt;ACharacter&gt;(GetPawn()))
	{
		if (UCRCharacterMovementComponent* MoveComp = PlayerCharacter-&gt;GetCharacterMovement&lt;UCRCharacterMovementComponent&gt;())
		{
			UpDirection = MoveComp-&gt;GetUpOrientation();
		}
	}</pre>
  <p id="HtWp">Получим требуемую игроком ориентацию контроллера.</p>
  <pre id="Y0eW" data-lang="cpp">FRotator ViewRotation = GetControlRotation();</pre>
  <p id="hhQj">Эту ориентацию надо повернуть так, чтобы она соответствовала новой гравитации. Например если игрок смотрел на 45 градусов вверх относительно мировых координат, теперь мы должны повернуть эту ориентацию так, чтобы эти 45 градусов сохранились от нашего локального “вверх”.</p>
  <pre id="IIeC" data-lang="cpp">
	if (!LastFrameUp.Equals(FVector::ZeroVector))
	{
		const FQuat DeltaGravityRotation = FQuat::FindBetweenNormals(LastFrameUp, UpDirection);
		const FQuat WarpedCameraRotation = DeltaGravityRotation * FQuat(ViewRotation);
 
		ViewRotation = WarpedCameraRotation.Rotator();	
	}
	LastFrameUp = UpDirection;</pre>
  <p id="dawt">Теперь нам надо взять управление от игрока, чтобы добавить поворот, который пытается сделать игрок, и передать в <strong>PlayerCameraManager</strong>, чтобы он применил модификаторы и правила поворота камеры. После чего убрать Roll, перевести нашу локальную ориентацию в мировую, и выставить в контроллер.</p>
  <p id="Ekuu">N.B. Так как мы делаем абстрактную систему, не очень хочется вклиниваться в работу других систем. Если мы собрали или настроили какие-то модификаторы или ограничители поворота, они должны работать с нашей гравитацией тоже, поэтому важно чтобы <strong>PlayerCameraManager </strong>работал с нашей логикой</p>
  <pre id="YITI" data-lang="cpp">	ViewRotation = GetUpRelativeRotation(ViewRotation, UpDirection);

	FRotator DeltaRot(RotationInput);
	if (PlayerCameraManager)
	{
		PlayerCameraManager-&gt;ProcessViewRotation(DeltaTime, ViewRotation, DeltaRot);

		ViewRotation.Roll = 0;
 
		SetControlRotation(GetUpWorldRotation(ViewRotation, UpDirection));
	}
</pre>
  <p id="MeyI">В конце убираем Roll и Pitch у нашей ориентации, переводим в мировые координаты и передаем это нашей пешке.</p>
  <pre id="vfwy" data-lang="cpp">	ViewRotation.Roll = 0.0f;
	ViewRotation.Pitch = 0.0f;
	APawn* const P = GetPawnOrSpectator();
	if (P)
	{
		P-&gt;FaceRotation(GetUpWorldRotation(ViewRotation, UpDirection), DeltaTime);
	}</pre>
  <h2 id="wtnE">ApplyGravityController</h2>
  <p id="Tz2H">Для теста нам понадобится реализовать простой способ назначить нашему персонажу контролер гравитации. В реальной игре это может быть сделано через уровень, систему способностей или GameMode. Для тестирования нам подойдет и просто триггер, который будет просто проставлять выбранного Actor в <strong>UCRCharacterMovementComponent</strong></p>
  <figure id="utiT" class="m_column">
    <img src="https://img4.teletype.in/files/7e/94/7e940d64-f813-4718-8e20-fe96e899717c.png" width="1460" />
  </figure>
  <figure id="GnJi" class="m_column">
    <img src="https://img3.teletype.in/files/65/cf/65cf6d6b-06b9-4d30-98d7-2de3ebe84e66.png" width="787" />
  </figure>
  <p id="sVzp">Теперь наш код готов к полноценному тестированию.</p>
  <figure id="2nif" class="m_column">
    <iframe src="https://vk.com/video_ext.php?oid=-230701223&id=456239019&autoplay=0"></iframe>
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/n54wgKTJvXq</guid><link>https://teletype.in/@jazzyjohn/n54wgKTJvXq?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/n54wgKTJvXq?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Игровой ИИ. State Tree</title><pubDate>Wed, 03 Dec 2025 08:19:39 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/18/5e/185e8e53-f662-450a-bf88-438a7b572c69.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/6a/81/6a81ce04-0342-4413-87b4-63b59d200689.png"></img>В прошлый раз мы закончили обсуждать Goal Oriented Action Planning (GOAP), сегодня же мы обсудим комбинированные решения.]]></description><content:encoded><![CDATA[
  <p id="P6JO"><a href="https://teletype.in/@jazzyjohn/WK23iWoyZ1w" target="_blank">В прошлый раз</a> мы закончили обсуждать Goal Oriented Action Planning (GOAP), сегодня же мы обсудим комбинированные решения.Также не забывайте подписываться <a href="https://t.me/Vaniagramming" target="_blank">на мой канал</a>, чтобы не пропустить следующие части.</p>
  <h2 id="TBDA">FSM + Behaviour Tree</h2>
  <p id="qVug">Один из самых простых подходов в сочетании разных решениях - это прямой гибрид Конечного Автомата и Дерева Поведения. Автомат принимает высокоуровневые решения: запускается боевое поведение или мирное, стоит NPC  убегать от игрока или разговаривать с ним. Каждое состояние Автомата реализовано как самостоятельное дерево поведения. Деревья поведения подробно на низком уровне реализуют каждые состояния. Такой подход позволяет не перегружать FSM переходами низкоуровневых поведений (такими как неудачная навигация или выбор способности для текущего состояния).</p>
  <p id="DQTe">Насколько нам известно, Дерево Поведения чаще всего использует BlackBoard для хранения информации. Поскольку наши деревья в случае сочетания с Автоматом не должны покрывать все поведение нашего агента, то, следовательно, наши Blackboard должны хранить информацию только для текущего состояния Автомата, что упрощает его структуру  и облегчает работу с ним. Кроме того мы получаем возможность использовать шаблонные деревья. Например дерево ближнего боя может выглядеть как навигация и использование способности, в котором настройки навигации и способность храниться в Blackboard. Blackboard настраивается уже автоматом, в зависимости от того, какое поведение он описывает текущим состоянием.</p>
  <p id="ZPyF">Одна из проблем такого подхода заключается в том, что передача данных между Деревьями и Автоматами может быть нетривиальной. Например если дизайн вашего агента требует, чтобы агенту нужно было убегать от игрока, если у него нет подходящих предметов для активации. Но такой запрос про предметы может быть частью низкоуровневой логики, за которую отвечает Дерево поведений. Получается Дерево должно сообщить более высокоуровневой логике о своей неудаче. Такое направление потока данных в ИИ выглядит тяжелым для поддержки: в Автомате будут условия, которые скрыты где-то в дебрях Деревьев поведений, причем не обязательно,что всех.</p>
  <p id="tax6">Главным преимуществом совмещения FSM и Дерева Поведения является прирост производительности. Количество переходов в Автомате ограничено высокоуровневыми состояниями, и их частоту обновления можно понизить, так как такая задержка не будет заметна игроку. Дерево Поведений отвечает за одно состояние, соответственно количество декораторов и условий тоже ограничено, и Дерево работает быстрее.</p>
  <h2 id="qjF3">Behaviour Tree + Utility AI</h2>
  <p id="461A">Другой популярный подход к сочетанию моделей ИИ - это сочетание шаблонных деревьев поведений и подхода полезности для выбор конкретных действий и способностей.</p>
  <p id="682j">Дерево поведений может описывать абстрактное поведение с модулями и узлами, которые выбираются и настраиваются с помощью Utility AI. Например Дерево Поведения дальнего боя будет включать перемещение в точку для атаки и использование оружия дальнего боя, а  выбор точки и выбор оружия осуществляется с помощью полезности.</p>
  <p id="S5ny">Такой ИИ отлично работает с дизайном агентов, у которых дизайн отличается способностями и предметами, но общее поведение, позиционирование и схожая логика. Например РПГ, где разнообразие ИИ кроется именно в сборке или инвентаре противников, а не в их поведении.</p>
  <h2 id="bhY4">GOAP + FSM/BehaviorTree</h2>
  <p id="96ox">Основным подходом к работе с GOAP является реализация конкретных действий в плане с помощью других моделей поведений. Такой подход использовался при создании GOAP в F.E.A.R.: каждое действие реализовывалось через Конечный Автомат, состоящий из 3 действий: передвижение, использование smart object и  проигрывание анимации. А куда перемещаться, что за объект использовать, и какую анимацию играть, определялось типом действием и задавалось в настройках действий.</p>
  <p id="lNA7">Такой подход позволяет убрать из планов тривиальные действия и сочетания и сфокусироваться на высокоуровневых поведениях. Также  это сокращает размер планов, а следовательно и размер пространства, в котором мы ищем план.</p>
  <h1 id="yPnq">StateTree</h1>
  <p id="b13V">Один из новых и реализованных гибридов двух моделей - это StateTree в Unreal Engine 5. Подход StateTree - это попытка получить бонусы и удобства контролируемых переходов в разные состояния с понятной системой последовательностей и селекторов.</p>
  <h2 id="wrMa">Schema и Context</h2>
  <p id="BP2H">State Tree является абстрактной моделью, и, хотя естественное его использование - это ИИ, сама структура состояний и переходов может быть использована для чего угодно: от низкоуровневой системы комбо до высокоуровневой системы миссий и заданий. Поэтому в основе state tree, в отличие от дерева поведения, лежит идея, что state tree может запускаться на любом объекте и с любым контекстом. Для того чтобы код понимал, с чем он работает и какие структуры обязательные для данного дерева, существует schema</p>
  <pre id="uYMe" data-lang="cpp">UStateTreeAIComponentSchema::UStateTreeAIComponentSchema(const FObjectInitializer&amp; ObjectInitializer /*= FObjectInitializer::Get()*/)
	: AIControllerClass(AAIController::StaticClass())
{
	check(ContextDataDescs.Num() == 1 &amp;&amp; ContextDataDescs[0].Struct == AActor::StaticClass());
	// Make the Actor a pawn by default so it binds to the controlled pawn instead of the AIController.
	ContextActorClass = APawn::StaticClass();
	ContextDataDescs[0].Struct = ContextActorClass.Get();
	ContextDataDescs.Emplace(UE::GameplayStateTree::Private::Name_AIController, AIControllerClass.Get(), FGuid(0xEDB3CD97, 0x95F94E0A, 0xBD15207B, 0x98645CDC));
}</pre>
  <p id="kmVb">Схема содержит набор полей и правил, с помощью которых остальные части state tree могут получить необходимую информации и доступ к нужным объектам.</p>
  <p id="1D8B">Для запуска дерева поведения и его обновления необходимо использовать контекст и внешней контролер (которым может быть компонент, актор или подсистема).</p>
  <pre id="3tnm" data-lang="cpp">FStateTreeExecutionContext Context(*GetOwner(), *StateTreeRef.GetStateTree(), InstanceData);
if (SetContextRequirements(Context))
{
    const EStateTreeRunStatus PreviousRunStatus = Context.GetStateTreeRunStatus();
    const EStateTreeRunStatus CurrentRunStatus =  Context.Tick(DeltaTime);

    if (CurrentRunStatus != PreviousRunStatus)
    {
        OnStateTreeRunStatusChanged.Broadcast(CurrentRunStatus);
    }
}</pre>
  <p id="9aIt">Именно контролер и схема заземляют абстрактное дерево до конкретной реализации. В дальнейшем мы будем рассматривать State Tree в контексте ИИ и brain component Unreal Engine. Но зона его применения намного шире.</p>
  <h2 id="aFax">State (Состояние)</h2>
  <p id="qrIP">Можно догадаться из названия, что основой StateTree является состояние (state). Состояние совмещает в себе сразу несколько моделей. Первое - это аналог состояния из конечного автомата: структура, у которой есть понятие входа/старта и выхода/завершения. Она описывает некоторое поведение и выполняет некоторую логику, также может иметь условия и правила выполнения или входа.</p>
  <p id="mRqj">Вторая модель - это управляющий узел из дерева поведения: структура обладает набором потомков, которые она запускает и обрабатывает в соответствии с конкретным набором правил.</p>
  <figure id="gQiE" class="m_custom">
    <img src="https://img1.teletype.in/files/c2/c8/c2c88f36-6985-4b87-8126-35c498e1cfd0.png" width="599" />
  </figure>
  <p id="OZZb">Например мы можем указать состоянию правила TrySelectChildrenInOrder</p>
  <p id="Rn4K">, для того чтобы наше состояние вело себя как селектор, либо как селектор с выбором по весам(кусочек utility based ai).</p>
  <h2 id="AaRx">Transition (Переход)</h2>
  <p id="GgeF">В отличие от конечного автомата, состояния в State Tree чаще обладают циклом “выполняю  -&gt; завершил (успешно или нет)” , что делает их схожими с конечными узлами деревьев поведения. После завершения же состояние обладает свойствами уже конечного автомата: вы может свободно определить, куда переходит дерево по завершению конкретного действия. Можно совершить переход в соседнее состояние, либо прыгнуть в другое часть дерева, либо завершить дерево, либо перезапустить его. Также такие переходы можно совершать в любой момент времени, как и в конечном автомате: по событию или во время тика по условию. Кроме того сохраняется иерархическая структура дерева: если переход совершает родительское состояние, то все его потомки завершаются автоматически.</p>
  <p id="Se3i">Такой подход кажется сложным, но на практике позволяет легко реализовывать сложные и банальные вещи. Например у вас может быть сложное дерево мирного поведения с кучей состояний правил, но вам надо будет создать одно условия для перехода из него в боевое. Аналогично сложное дерево комбо может по команде от конкретного состояния совершить переход в другую ветку, и такую проверку не надо настраивать вне этого состояния. Например у вас может быть сложное дерево боя и ветка комбо-ударов: в момент апперкота вы можете совершить переход в другую ветку, и за этот переход отвечает только состояние апперкота, в другие моменты никаких проверок или переходов совершать не надо. Переходы настраиваются в состояниях и могут обладать набором условий.</p>
  <figure id="kp3H" class="m_custom">
    <img src="https://img4.teletype.in/files/72/34/7234c92f-7cbf-44f3-a0d8-018161b1bcf5.png" width="602" />
  </figure>
  <h2 id="Fqb0">Condition (Условия)</h2>
  <p id="Mm41">Условия в State Tree встречаются в переходах из состояний и в самих состояниях при старте состояния. Условия при входе позволяют реализовать модель селектор из дерева последней: потомки конкретного состояния обладают набором условий, и он будет перебирать их при входе пока её найдет то, у которого условие выполнено.</p>
  <p id="BiCo">Условия обычно наследуются от FStateTreeConditionBase. Однако, если в вашем проекте несколько StateTree, которые используются с разными схемами, может быть удобно создать промежуточные базовые классы и указать в схеме, какие классы условий можно использовать в какой схеме.</p>
  <pre id="khBH" data-lang="cpp">bool UStateTreeAIComponentSchema::IsStructAllowed(const UScriptStruct* InScriptStruct) const
{
	return Super::IsStructAllowed(InScriptStruct)
		|| InScriptStruct-&gt;IsChildOf(FStateTreeAITaskBase::StaticStruct())
		|| InScriptStruct-&gt;IsChildOf(FStateTreeAIConditionBase::StaticStruct());
}</pre>
  <p id="Bqfg">Условия можно комбинировать по правилам булевых операций. Для этого в unreal Engine 5.6 есть интерфейс создания условий. Конечно же можно создавать blueprint условия.</p>
  <figure id="090o" class="m_custom">
    <img src="https://img2.teletype.in/files/d4/68/d4686125-18c6-43d3-b28e-681d1446c0a5.png" width="602" />
    <figcaption>Пример комбинированого условия</figcaption>
  </figure>
  <p id="JsuF">Для удобства рекомендую всегда реализовывать функцию FText FStateTreeNodeBase::GetDescription, чтобы ваше дерево было читаемым и удобным.</p>
  <figure id="dpiV" class="m_custom">
    <img src="https://img3.teletype.in/files/27/c9/27c91f84-e98a-44fd-9083-985468a1ccd5.png" width="602" />
    <figcaption>Удобное описание условия</figcaption>
  </figure>
  <h2 id="oLpR">Task (задачи)</h2>
  <p id="9PMW">Задачи являются той частью состояния, которая позволяет им выполнять логику. Задача - это кирпичики, из которых собирается поведение в State Tree. Задачи могут простыми (например навигация в пространстве), и сложными действиями (например проиграть последовательность атак). В принципе, как и в дереве поведения, задачи в StateTree лучше делать атомарными и простыми. В каждом состоянии может быть сколько угодно задач.Состояние определяет само, когда оно должно завершиться, с помощью настройки (по окончанию любой задачи или всех).</p>
  <p id="knf2">Задачи наследуются от класса FStateTreeTaskBase, но, по аналогии с условиями, удобно будет завести промежуточный базовый класс, который вы разрешите использовать вашей схеме.</p>
  <figure id="xv4h" class="m_custom">
    <img src="https://img4.teletype.in/files/be/da/beda6d8b-8630-4288-ac53-15bd23aad038.png" width="602" />
    <figcaption>Пример состояние с нексколькими задачами</figcaption>
  </figure>
  <p id="kvFc">Бесконечные задачи могут выполнять роль сервисов и декораторов из дерева поведения. Такую задачу можно добавить в любое состояние, либо добавить в список глобальных задач всего дерева. Глобальные задачи чаще всего собирают и анализируют состояние игрового мира и агента и сводят его к простым флагам или полям, которые в свою очередь могут использоваться остальными состояниями.</p>
  <h2 id="0fwi">Data Flow</h2>
  <p id="wUnJ">При работе и проектировании StateTree важно сразу понять как работает видимость данных в дереве. Данные доступны иерархически, т.е. любая задача или условие может получить данные от всех задач текущего состояния и его предка в иерархии. Такой подход позволяет организовать удобную передачу данных: задача родительского состояния выполняет запрос, например, к систему EQ, получает цель для перемещения, а потомки состояния запускают задачи навигации используя результаты задачи родительского состояния.</p>
  <figure id="3Yue" class="m_custom">
    <img src="https://img3.teletype.in/files/6a/81/6a81ce04-0342-4413-87b4-63b59d200689.png" width="452" />
  </figure>
  <p id="E5q8">В следующий раз мы разберемся какие переменные и функции доступны для State Tree и попробуем собрать простой боевой ИИ.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/qpEQhv4pLJg</guid><link>https://teletype.in/@jazzyjohn/qpEQhv4pLJg?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/qpEQhv4pLJg?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Лента Мёбиуса часть 2.</title><pubDate>Thu, 25 Sep 2025 13:19:20 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/a0/df/a0df6472-92c1-422d-937d-0526b5d143d6.png"></media:content><category>Unreal Engine 5</category><description><![CDATA[<img src="https://img3.teletype.in/files/29/d0/29d03e6e-f615-404b-a725-73c28a70039e.png"></img>В прошлый раз я рассказал, как собрал процедурный меш в форме Ленты Мебиуса для своего pet проекта. В этот раз разберемся, как менять гравитацию и адаптировать контроллер под случайную гравитацию.]]></description><content:encoded><![CDATA[
  <p id="huWn">В прошлый раз я рассказал, как собрал процедурный меш в форме Ленты Мебиуса для своего pet проекта. В этот раз разберемся, как менять гравитацию и адаптировать контроллер под случайную гравитацию.</p>
  <h2 id="8v0b">ICRGravityController</h2>
  <p id="aVj8">Очевидно, раз мой пет проект начинает заигрывать с гравитацией, то на одной ленте Мебиуса останавливаться я не буду, поэтому надо организовывать все так, чтобы я мог делать какие угодно локальные изменения гравитации. Лучший подход для этого - создать интерфейс. Это позволит мне выбрать любой подход к изменению гравитации игрока в будущем: и если Лента Мебиуса это Actor, то в дальнейшем за изменение гравитации может отвечать, например, компонент.</p>
  <pre id="5E5j" data-lang="cpp">// This class does not need to be modified.
UINTERFACE(BlueprintType)
class UCRGravityController : public UInterface
{
	GENERATED_BODY()
};

/**
 * 
 */
class FPSGAME_API ICRGravityController
{
	GENERATED_BODY()

	// Add interface functions to this class. This is the class that will be inherited to implement this interface.
public:

	virtual void GetGravityAndOrientation(FVector WorldLocation, FVector&amp; Gravity, FVector&amp; Orientation) {};
};
</pre>
  <p id="cqoV">Я специально разделил направление гравитации и ориентацию для ситуаций, когда игрок сошел с нашей ленты в сторону, и мы хотим вернуть его на ленту, но не поворачивать его ориентацию. В общем же случае гравитация и ориентация совпадают с нормалью поверхности нашей ленты Мебиуса. Здесь и далее под ориентацией я понимаю вектор “вверх” капсулы игрока (в каком то смысле Pitch). Так как мой пет проект - это FPS, то остальные вращения либо не важны (Roll), либо управляются игроком (Yaw)</p>
  <figure id="sQnG" class="m_custom">
    <img src="https://img3.teletype.in/files/64/af/64afd044-bd8b-42ec-8ded-44935fe2e857.png" width="737" />
  </figure>
  <p id="PV2G">Теперь надо слегка модифицировать наш класс ленты.</p>
  <p id="tKUa">Первым делом добавим реализацию нашего интерфейса:</p>
  <pre id="GrSQ" data-lang="cpp">class FPSGAME_API ACRMobiusStrip : public AActor, public ICRGravityController
.
.
.
virtual void GetGravityAndOrientation(FVector WorldLocation, FVector&amp; Gravity, FVector&amp; Orientation) override;</pre>
  <p id="48iU">Также добавим еще один сплайн, в котором для удобства будем хранить все точки и ориентации, которые мы использовали для генерации</p>
  <pre id="BXDP" data-lang="cpp">	UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	class USplineComponent* MobiusSplineComponent</pre>
  <pre id="La7O" data-lang="cpp">	FSplinePoint Point(index, Location, ESplinePointType::Curve, Rotator);
	//setup additional spline for storing Rotation
	MobiusSplineComponent-&gt;AddPoint(Point);</pre>
  <h2 id="1g3X">Считаем гравитацию</h2>
  <p id="Vkbk">Теперь приступим к реализации самой гравитации. Мы хотим, чтобы наша гравитация менялась плавно, а не скачками между точками сплайна. Для этого получим текущую точку на сплайне и два узла.</p>
  <pre id="xvCq" data-lang="cpp">const FVector LocationOnSpline = MobiusSplineComponent-&gt;FindLocationClosestToWorldLocation(WorldLocation, ESplineCoordinateSpace::Type::World);

const float Key = MobiusSplineComponent-&gt;FindInputKeyClosestToWorldLocation(WorldLocation);
	
const FQuat QuatAtStart = MobiusSplineComponent-&gt;GetRotationAtSplineInputKey(FMath::Floor(Key),ESplineCoordinateSpace::Type::World).Quaternion();
FQuat QuatAtEnd = MobiusSplineComponent-&gt;GetRotationAtSplineInputKey(FMath::CeilToFloat(Key),ESplineCoordinateSpace::Type::World).Quaternion();
</pre>
  <p id="cRSK">Дальше надо учесть, что наша ориентация ведет себя не гладко, и в точке соединения начала и конца ленты у нас разрыв. Учтём его.</p>
  <pre id="SBOU" data-lang="cpp">if ( NumberOfHalfTurns % 2 != 0 &amp;&amp; FMath::CeilToFloat(Key) == MobiusSplineComponent-&gt;GetNumberOfSplinePoints())
{
	QuatAtEnd = QuatAtEnd * FQuat(FRotator(0.0f, 0.0f , -180.f));
}</pre>
  <p id="IKwv">Получаем итоговое значение ориентации в данной точке сплайна:</p>
  <pre id="VbLn" data-lang="cpp">	const FQuat Rotation = FQuat::Slerp(QuatAtStart, QuatAtEnd, FMath::Frac(Key));</pre>
  <p id="JRPp">N.B. Я намеренно использую Кватернионы, а не ротаторы, т.к. поскольку мы крутимся достаточно рандомно и непредсказуемо, шансы получить шарнирный замок (gimbal lock) очень высоки, поэтому лучше использовать Кватернионы.</p>
  <p id="OpfM">Сохраним расстояние от оси нашей ленты:</p>
  <pre id="8dfF" data-lang="cpp">FVector Distance = WorldLocation - LocationOnSpline;
const float WidthOffset = Rotation.GetAxisY().Dot(Distance);</pre>
  <p id="iXIB">Теперь у нас 3 варианта: мы находимся “над”(где под вертикалью мы понимаем ориентацию ленты)  плоскостью нашей ленты, сбоку от ней, или в области ребра.</p>
  <p id="Y5k9">Разберем все 3 случая отдельно.</p>
  <h3 id="WOiZ">Мы над лентой</h3>
  <p id="Ar9V">Этому соответствует условие</p>
  <pre id="8QPz" data-lang="cpp">FMath::Abs(WidthOffset) &lt; SegmentWidth/2</pre>
  <figure id="tIea" class="m_custom">
    <img src="https://img3.teletype.in/files/69/21/6921be0e-a960-44b7-8052-8ea9d44a37a2.png" width="733.2872340425532" />
  </figure>
  <p id="Ws0P">Тут все просто: если направление до нас совпадает с направлением нормали, то гравитация противоположна направлению нормали (тянет нас к ленте). Если мы находимся под лентой, то гравитация должна совпадать с нормалью (опять же  тянуть нас к ленте). Ориентация капсулы игрока противоположна направлению гравитации:</p>
  <pre id="jVLL" data-lang="cpp">FVector MobiusUp = Rotation.GetAxisZ();
if ((Distance).GetSafeNormal().Dot(MobiusUp) &lt; 0.0f)
{
	MobiusUp = MobiusUp * -1.0f;
}	
		
Orientation =  MobiusUp;
Gravity = MobiusUp * -1.0f;</pre>
  <h3 id="N593">Мы сбоку от ленты</h3>
  <p id="9RgG">Этому соответствует условие</p>
  <pre id="qhkZ" data-lang="cpp">FMath::Abs(WidthOffset) &gt; SegmentWidth/2</pre>
  <figure id="9UxF" class="m_custom">
    <img src="https://img2.teletype.in/files/91/d8/91d809ab-6303-4ef7-9f94-ff8248f2c5f0.png" width="783.023569023569" />
  </figure>
  <p id="w5Gg">Для начала поймем, что мы не на грани.</p>
  <pre id="F2D4" data-lang="cpp">const float HeightOffset = Rotation.GetAxisZ().Dot(Distance);
	
if (FMath::Abs(HeightOffset) &gt; SegmentDepth/2)
{</pre>
  <p id="1Y1S">Мы хотим, чтобы игрок вернулся на ленту, но при этом не хотим крутить ему ориентацию, так как такое возвращение будет скорее небольшим улучшением UX, а не полноценной механикой.</p>
  <p id="4vYJ">Тянем его к точке на поверхности, на которой он может стоять. Ориентация соответствует предыдущему случаю.</p>
  <pre id="NwZf" data-lang="cpp">const FVector FloorPoint = LocationOnSpline + Distance.ProjectOnToNormal(Rotation.GetAxisZ()).GetSafeNormal() * SegmentDepth/2;
FVector ToFloorPointDirection = Gravity = (FloorPoint - WorldLocation).GetSafeNormal();
FVector MobiusUp = Rotation.GetAxisZ();
if ((Distance).GetSafeNormal().Dot(MobiusUp) &lt; 0.0f)
{
	MobiusUp = MobiusUp * -1.0f;
}
Orientation =  MobiusUp;
Gravity = ToFloorPointDirection;</pre>
  <h3 id="idmx">Мы на грани</h3>
  <p id="JHbA">Условия:</p>
  <pre id="VFcs" data-lang="cpp">FMath::Abs(WidthOffset) &gt; SegmentWidth/2
FMath::Abs(HeightOffset) &lt; SegmentDepth/2</pre>
  <figure id="Q61N" class="m_custom">
    <img src="https://img3.teletype.in/files/ed/a9/eda9d73b-cf44-48e7-a569-0aacf8c05e12.png" width="770.151832460733" />
  </figure>
  <p id="hIFS">В последний случае мы на грани ленты. Он самый интересный, тк мы хотим дать игроку две возможности. Первая - поменять сторону ленты, т.е. игрок должен иметь возможность плавно менять ориентацию. Вторая - это при правильном управлении балансировать на грани ленты.</p>
  <p id="qWJC">Для начала посчитаем “вверх” относительно поверхности ленты и относительно  грани:</p>
  <pre id="DFoZ" data-lang="cpp">FVector MobiusUp = Rotation.GetAxisZ();
if ((Distance).GetSafeNormal().Dot(MobiusUp) &lt; 0.0f)
{
	MobiusUp = MobiusUp * -1.0f;
}
			
FVector SideUp = Rotation.GetAxisY();		
		
if (WidthOffset &lt; 0.0f)
{
	SideUp = SideUp * -1.0f;
}</pre>
  <p id="Qsje">Теперь в зависимости от положения по “вертикали”, получим значение, насколько глубоко игрок зашел “в грань”</p>
  <pre id="rIYn" data-lang="cpp">	const float LerpAlpha = 1.0f - FMath::Abs(HeightOffset / (SegmentDepth/2));</pre>
  <p id="WhDq">Теперь интерполируем между двумя векторами получая вектор “вверх” для игрока</p>
  <pre id="pUnX" data-lang="cpp">Orientation = FVector::SlerpVectorToDirection(MobiusUp, SideUp, LerpAlpha);		</pre>
  <p id="krhQ">Для гравитации сделаем проще: пытаемся вернуть игрока туда откуда он прошел на грань, плюс в направлении к ленте - это позволит ему поймать момент и остаться на грани</p>
  <pre id="0X59">Gravity = MobiusUp;
FVector SideDown = SideUp * -1.0f;
Gravity = FVector::SlerpVectorToDirection(Gravity, SideDown, LerpAlpha);		</pre>
  <figure id="IQ0M" class="m_custom">
    <img src="https://img3.teletype.in/files/29/d0/29d03e6e-f615-404b-a725-73c28a70039e.png" width="782" />
  </figure>
  <p id="N0KX">В  третьей части настроим наш контролер и персонажа, чтобы он мог двигаться по ленте.</p>
  <p id="ANF7">P.S.  Финальный код функции вычисления гравитации:</p>
  <pre id="etYY" data-lang="cpp">void ACRMobiusStrip::GetGravityAndOrientation(FVector WorldLocation, FVector&amp; Gravity, FVector&amp; Orientation)
{
	const FVector LocationOnSpline = MobiusSplineComponent-&gt;FindLocationClosestToWorldLocation(WorldLocation, ESplineCoordinateSpace::Type::World);

	const float Key = MobiusSplineComponent-&gt;FindInputKeyClosestToWorldLocation(WorldLocation);
	
	const FQuat QuatAtStart = MobiusSplineComponent-&gt;GetRotationAtSplineInputKey(FMath::Floor(Key),ESplineCoordinateSpace::Type::World).Quaternion();
	FQuat QuatAtEnd = MobiusSplineComponent-&gt;GetRotationAtSplineInputKey(FMath::CeilToFloat(Key),ESplineCoordinateSpace::Type::World).Quaternion();

	if ( NumberOfHalfTurns % 2 != 0 &amp;&amp; FMath::CeilToFloat(Key) == MobiusSplineComponent-&gt;GetNumberOfSplinePoints())
	{
		QuatAtEnd = QuatAtEnd * FQuat(FRotator(0.0f, 0.0f , -180.f));
	}

	const FQuat Rotation = FQuat::Slerp(QuatAtStart, QuatAtEnd, FMath::Frac(Key));
	FVector Distance = WorldLocation - LocationOnSpline;
	const float WidthOffset = Rotation.GetAxisY().Dot(Distance);
	if (FMath::Abs(WidthOffset) &gt; SegmentWidth/2)
	{
		//we outside spline
		const float HeightOffset = Rotation.GetAxisZ().Dot(Distance);
	
		if (FMath::Abs(HeightOffset) &gt; SegmentDepth/2)
		{		
			//we not inside spline depth		
			//DrawDebugSphere(GetWorld(), FloorPoint, 100.0f, 32, FColor::Blue, false);
			const FVector FloorPoint = LocationOnSpline + Distance.ProjectOnToNormal(Rotation.GetAxisZ()).GetSafeNormal() * SegmentDepth/2;
			FVector ToFloorPointDirection = Gravity = (FloorPoint - WorldLocation).GetSafeNormal();
			FVector MobiusUp = Rotation.GetAxisZ();
			if ((Distance).GetSafeNormal().Dot(MobiusUp) &lt; 0.0f)
			{
				MobiusUp = MobiusUp * -1.0f;
			}
			Orientation =  MobiusUp;
			Gravity = ToFloorPointDirection;
		}
		else
		{
			FVector MobiusUp = Rotation.GetAxisZ();
			if ((Distance).GetSafeNormal().Dot(MobiusUp) &lt; 0.0f)
			{
				MobiusUp = MobiusUp * -1.0f;
			}
			
			FVector SideUp = Rotation.GetAxisY();		
		
			if (WidthOffset &lt; 0.0f)
			{
				SideUp = SideUp * -1.0f;
			}

			const float LerpAlpha = 1.0f - FMath::Abs(HeightOffset / (SegmentDepth/2));
			
			Orientation = FVector::SlerpVectorToDirection(MobiusUp, SideUp, LerpAlpha);			
			//DrawDebugDirectionalArrow(GetWorld(), LocationOnSpline, LocationOnSpline + MobiusUp *200.f, 20, FColor::Green, false);
			Gravity = MobiusUp;
			FVector SideDown = SideUp * -1.0f;
			Gravity = FVector::SlerpVectorToDirection(Gravity, SideDown, LerpAlpha);		
		}
	}
	else
	{
		FVector MobiusUp = Rotation.GetAxisZ();
		if ((Distance).GetSafeNormal().Dot(MobiusUp) &lt; 0.0f)
		{
			MobiusUp = MobiusUp * -1.0f;
		}	
		
		Orientation =  MobiusUp;
		Gravity = MobiusUp * -1.0f;
	}

	//DrawDebugDirectionalArrow(GetWorld(), WorldLocation, WorldLocation + Orientation *200.f, 20, FColor::Green, false);
}</pre>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/ZkTYJ6oMwnl</guid><link>https://teletype.in/@jazzyjohn/ZkTYJ6oMwnl?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/ZkTYJ6oMwnl?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Расшифровка FHitResult в UE 5</title><pubDate>Wed, 10 Sep 2025 11:03:18 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/5d/06/5d06fdda-e7bc-4373-889a-4413d3f1d783.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/a6/cd/a6cd0be4-6b17-422d-ac9d-004e4e210992.png"></img>Очередной раз споткнулся об расшифровку FHitResult в UE 5 решил написать небольшую заметку для себя и поделиться с вами.]]></description><content:encoded><![CDATA[
  <p id="b0oQ">Очередной раз споткнулся об расшифровку FHitResult в UE 5 решил написать небольшую заметку для себя и поделиться с вами.</p>
  <p id="AiHP"><strong>FHitResult </strong>- это структура, которая проскакивает в любом взаимодействии с физикой в UE, будь то касты или попытки пошевелить какого-нибудь хитрого актора (потому что там внутри все равно касты и свипы). В ней все поля интересные, но начнем с простого.</p>
  <p id="jfqF"><strong>bBlockingHit </strong>отвечает за определение - было ли это столкновение(Blocking Hit)  или просто пересечение (Overlap).По факту тут возвращаются настройки реакции sweep на встречный объект.</p>
  <p id="IxF6"><strong>bStartPenetrating </strong>отвечает за то, начали ли мы наш sweep из физического объекта.</p>
  <p id="reJb"><strong>PhysMaterial, HitObjectHandle, Component, PhysicsObjectOwner, PhysicsObject,BoneName, MyBoneName</strong> : в этих полях хранится информация об объектах, с которыми наш sweep столкнулся или пересекся. В основном назначение поля понятно из названия, но есть момент: <strong>PhysMaterial </strong>по умолчанию не передается , а чтобы его передать надо выставить флаг в  <strong>FCollisionQueryParams::bReturnPhysicalMaterial</strong> (эта структура тоже полна чудес)</p>
  <p id="Raau"><strong>TraceStart, TraceEnd</strong> отвечают за стартовую и конечную точку нашего sweep.</p>
  <p id="cP3c"><strong>Distance,Time</strong> отвечают за время (параметр от 0 до 1) и расстояние от <strong>TraceStart </strong>до точки столкновения</p>
  <p id="58bd">И вот мы подходим к интересному:</p>
  <p id="4xh6"><strong>ImpactPoint </strong>точка столкновения, т.е. точка в пространстве, где два коллайдера коснулись.</p>
  <p id="Ib0C"><strong>Location  </strong>координаты в пространстве, в которых, если поместить форму, с помощью которой мы совершаем sweep, форма коснется коллайдера в точке <strong>ImpactPoint</strong></p>
  <p id="mZ3X"><strong>Normal </strong>это условная нормаль: направление прямой, соединяющей центр нашей формы, помещенной в <strong>Location </strong>и точку <strong>ImpactPoint</strong></p>
  <p id="irob"><strong>ImpactNormal </strong>это реальная нормаль поверхности, о которую стукнулась наша форма.</p>
  <p id="eFTc">Вот небольшая схема для визуализации.</p>
  <figure id="sJh7" class="m_custom">
    <img src="https://img3.teletype.in/files/a6/cd/a6cd0be4-6b17-422d-ac9d-004e4e210992.png" width="602" />
  </figure>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/WK23iWoyZ1w</guid><link>https://teletype.in/@jazzyjohn/WK23iWoyZ1w?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/WK23iWoyZ1w?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Игровой ИИ. GOAP</title><pubDate>Tue, 02 Sep 2025 12:07:42 GMT</pubDate><media:content medium="image" url="https://img3.teletype.in/files/af/8e/af8ee916-f47e-43c5-8c51-5430f492ffa3.png"></media:content><category>Игровой ИИ</category><description><![CDATA[<img src="https://img1.teletype.in/files/02/0f/020fb0e1-f01a-43f9-9353-255a5b6f8eef.png"></img>В прошлый раз мы начали обсуждать Goal Oriented Action Planning (GOAP), сегодня же мы обсудим несколько нюансов работы с планировщиками. Также не забывайте подписываться на мой канал, чтобы не пропустить следующие части.]]></description><content:encoded><![CDATA[
  <p id="ixVb"><a href="https://teletype.in/@jazzyjohn/DIpSWX2rK7o" target="_blank">В прошлый раз</a> мы начали обсуждать Goal Oriented Action Planning (GOAP), сегодня же мы обсудим несколько нюансов работы с планировщиками. Также не забывайте подписываться <a href="https://t.me/Vaniagramming" target="_blank">на мой канал</a>, чтобы не пропустить следующие части.</p>
  <h2 id="oRo8">Перепланирование.</h2>
  <p id="oHng">Как говорилось раньше, в саму архитектуру планировщиков не заложена идея, что план может стать неактуальным во время его выполнения, а значит внешняя система должна следить за актуальностью плана. Один из методов - это проверка актуальности цели и актуальности всех умных объектов, которые задействованы. Очевидно, что такие проверки надо проводить периодически. Так или иначе, в определенный момент времени нам будет необходимо прервать текущий план и создать новый план. Такой подход вызывает большую нагрузку на производительность, при чем это позволяет отследить только потерю актуальности у плана, а для того чтобы оценить, нет ли у агента поведения оптимальнее, придется постоянно генерировать планы и смотреть, не появилось ли более оптимальное действие.</p>
  <h2 id="M2Fc">Коллизии планов</h2>
  <p id="M7vX">Планы могут обладать коллизиями. Например, если оба агента хотят открыть одну и ту же дверь, первой попыткой решить такую коллизию будет создание запрета на доступность объекта (двери), как только агент начал выполнять план. К счастью, все объекты, с которыми взаимодействует наш ИИ, умные объекты, поэтому реализовать такой запрет не составит труда. Но такой подход генерирует другую проблему: если агент не справился со своим планом (например его убил игрок), а план был актуальный, высока вероятность того, что и другой агент посчитает этот план оптимальным. В итоге у нас получится поведение, где один агент за другим пытаются использовать объект, а их убивает игрок. Такое поведение выглядит глупым, поэтому логично добавить не просто флаг, а тайм аут на использование объекта.</p>
  <h2 id="vpoZ">Заскриптованное поведение.</h2>
  <p id="JlS5">Одним из бонусов GOAP является то, что поведение выглядит динамичным и не заскриптованным, но иногда это является проблемой. Очень часто в играх мы хотим каких-то фиксированных реакций на какие-то события. Игрок проводит добивание, соседние рядом противники начинают разбегаться, командир отряда используют способность, остальные участники отряда пытаются его прикрыть и т.д. В архитектуру GOAP не заложены простые способы реализации таких поведений. Можно, конечно, делать скриптованные цели и под них простые планы с реакциями, но такие вещи быстро уничтожают все изящество и удобство GOAP. Одним из способов такой проблемы может быть использование среднеуровневой логики.</p>
  <figure id="I50n" class="m_custom">
    <img src="https://img1.teletype.in/files/02/0f/020fb0e1-f01a-43f9-9353-255a5b6f8eef.png" width="602" />
  </figure>
  <h2 id="lxft">Среднеуровневый логика</h2>
  <p id="nwGZ">Со многими вышеуказанными проблемами столкнулась команда Monolith, которая и придумала GOAP для своей игры F.E.A.R. Одним из подходов, к которым пришла <a href="https://www.youtube.com/watch?v=gm7K68663rA" target="_blank">команда Monolith</a>, было отвести планеру место в середине архитектурной иерархии. Под этим подразумевается, что не стоит перегружать планировщика информацией о низкоуровневых вещах. Например какую позу или анимацию сейчас играет персонаж, и добавлять в план действие смены позы. Такие действия усложняют планы и делают их создание более медленным с точки зрения производительности. Анимационная система может сама добавить смену позы или анимацию экипировки оружия перед атакой или началом плана. Это позволяет плану концентрироваться именно на действиях и поведении, а не на навигации, анимациях и подготовке. С другой стороны, не стоит перегружать планировщик и верхнеуровневой информацией. Если один ИИ может выполнять несколько ролей, или у него может быть разное состояние (отравлен, сломлен, напуган), не стоит пытаться поместить это все в состояние мира и заставлять планировщик решать такие задачи. Напишите более высокоуровневую систему, которая будет подменять список задач и действий у вашего агента на соответствующий его роли/состоянию. Это упростит и выбор целей, и построение плана.</p>
  <h1 id="xeOd">Hierarchical Task Network (HTN) Planning</h1>
  <p id="TWYp">Иерархическая сеть задач является смежными с GOAP подходом. В отличие от GOAP, в котором мы ищем план, перемещаясь по графу действий, пока не получим план, удовлетворяющий наши цели, HTN использует декомпозицию задач превращая их в простые атомарные действия.</p>
  <figure id="NPWN" class="m_custom">
    <img src="https://img3.teletype.in/files/ec/4b/ec4b7dcc-aa25-4344-acc5-3d5fc1de2fcf.jpeg" width="602" />
    <figcaption>Пример HTN  из плагина  Hierarchical Task Network Planning AI</figcaption>
  </figure>
  <h2 id="oTRM">Задача (task)</h2>
  <p id="aVFh">В HTN задачи могут быть двух типов: примитивные и составные. Примитивные задачи - это одно атомарное действие, схожее с действиями в GOAP. Например, атаковать игрока или подобрать оружие. Целью работы модели является создание последовательности примитивных задач. Примитивная задача состоит из оператора и набора эффектов и условий. Составные задачи состоят из набора методов. Каждый метод состоит из условия и набора задач.</p>
  <h2 id="Vnzx">Оператор</h2>
  <p id="D2ZN">Оператор - это простое действие, лишенное контекста. Например, атаковать или переместиться куда-то. Именно задача дает оператору контекст, чтобы стать полноценным действием для ИИ агента. Разные задачи могут использовать одинаковые операторы, но разные контексты, условия и эффекты приводят к разнообразному поведению</p>
  <h2 id="cAn4">Эффекты и Условия</h2>
  <p id="Aei6">По аналогии с GOAP, эффекты - это то, как изменяется состояние мира при выполнении этого действия, а условия - это то, каким должно быть состояние мира, чтобы эта задача имела смысл.</p>
  <h2 id="5dcd">Построение плана.</h2>
  <p id="QCa9">HTN начинает с высокоуровневой составной задачи, которая последовательно разбивается на подзадачи, используя методы и условия в них. В результате мы получаем план, который выполняет агент. Например, задача “Выковать меч” может быть разбита на подзадачи “Достать Железо”, “Перейти В Кузню”, “Обработать Железо”. В свою очередь, “Достать Железо” может быть разбито на “Перейти на склад”, “Взять Железо” или  “Перейти в шахты”, “Добыть Железо”, в зависимости от состояния мира, в частности - есть ли железо на складе.</p>
  <h2 id="0BZG">Особенности HTN</h2>
  <p id="TfJc">HTN обладает такими же минусами и плюсами, как и GOAP, так как по сути этот тоже планировщик, только с подходом сверху вниз к поиску плана. Если вас заинтересовала подробная работа HTN, есть отличная<a href="https://www.gameaipro.com/GameAIPro/GameAIPro_Chapter12_Exploring_HTN_Planners_through_Example.pdf" target="_blank"> статья на эту тему</a></p>
  <h1 id="Tidi">Групповые Планы.</h1>
  <p id="fHmu">В заключение, хотел бы упомянуть подход кооперативных планов. Про этот подход рассказывали <a href="https://gdcvault.com/play/1028889/AI-Summit-Let-Your-Agents" target="_blank">на GDC </a>. Идея подхода в том, чтобы позволить одному агенту попросить помощи у другого. При составлении плана один агент может использовать другого агента как действие, которое поменяет состояние мира, как полезно для плана первого агента. В этот момент планирование останавливается, и мы запрашиваем другого агента составить план, где целью является то состояние мира, которое первый агент хочет получить в результате действия. Если такой план возможен, и агент не занят чем-то другим, мы сохраняем второй план и продолжаем планировать первый план. Если все получилось, у нас есть два плана, которые мы можем выполнить.</p>
  <p id="QSFs">Простым примером может служить просьба одного агента получить лечение от второго, или открыть дверь с помощью кнопки, недоступной первому. Такие агенты создают еще больше иллюзии интеллекта, так как начинают приходить друг другу на помощь и действовать сообща, даже если речь идет о коротких простых планах.</p>
  <h1 id="SWIF">Готовые решения</h1>
  <p id="3TDs">Самостоятельная реализация такой модели выбора поведения может быть нетривиальной задачей, требующей понимания архитектуры современных игр. Реализация должна взаимодействовать с множеством других подсистем для создания правильного состояния мира. Часто встает вопрос оптимизации и организации удобной настройки поведения. Поэтому чаще всего лучше использовать готовые решения. Например, для Unreal Engine 5 есть плагин <a href="https://www.fab.com/ru/listings/1423ad9b-9c53-43be-b4c8-af1b655377bf" target="_blank">Hierarchical Task Network Planning AI</a> , позволяющий создавать поведения, используя иерархический планировщик.</p>
  <p id="865E">В следующий раз мы с вами рассмотрим гибридные ИИ, т.е. ИИ, которые сочетают в себе две модели (например, FSM и Behaviour Tree).</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/qcXxOeLSIcM</guid><link>https://teletype.in/@jazzyjohn/qcXxOeLSIcM?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/qcXxOeLSIcM?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Лента Мёбиуса. Часть 1.</title><pubDate>Tue, 19 Aug 2025 08:19:33 GMT</pubDate><media:content medium="image" url="https://img4.teletype.in/files/fb/18/fb18a77c-2156-4bde-9400-d7c1179ff809.png"></media:content><category>Unreal Engine 5</category><description><![CDATA[<img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcRGlGjvXkSDSbKSAx8r2Pi8q3RG7OCB8Nj15wwAzkoo9NgMg9l97jE8pdOuZXQPa_NZIG5DbWGwccTJ0Rx7LhZNggO89L3lZ-UCj3ot8cIA_1dZAK4WSZB1H6BTuGdrikxA-YGSA?key=Vx-soB7s9vU6MTK5w50UIQ"></img>В свой  pet-проект я захотел добавить процедурный уровень на основе ленты Мёбиуса. Пока я собирал функционал и исследовал возможности движка, я узнал много полезных вещей, в частности связанных с кастомной гравитацией и с динамическими мешами в UE. Решил поделиться с вами этими находками.]]></description><content:encoded><![CDATA[
  <p id="shOB">В свой  pet-проект я захотел добавить процедурный уровень на основе ленты Мёбиуса. Пока я собирал функционал и исследовал возможности движка, я узнал много полезных вещей, в частности связанных с кастомной гравитацией и с динамическими мешами в UE. Решил поделиться с вами этими находками.</p>
  <figure id="q4Pk" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcRGlGjvXkSDSbKSAx8r2Pi8q3RG7OCB8Nj15wwAzkoo9NgMg9l97jE8pdOuZXQPa_NZIG5DbWGwccTJ0Rx7LhZNggO89L3lZ-UCj3ot8cIA_1dZAK4WSZB1H6BTuGdrikxA-YGSA?key=Vx-soB7s9vU6MTK5w50UIQ" width="602" />
  </figure>
  <h2 id="KIo4">Уровень лента</h2>
  <p id="eOse">.Для начала я определился с требованиями к генератору:</p>
  <ol id="Ik4F">
    <li id="qa4Q">Размер и положение ленты должно быть удобно для настройки.</li>
    <li id="uAT3">Лента должна быть процедурно генерируемая.</li>
    <li id="Hgmg">Алгоритм должен быть простой и достаточно быстрый, чтобы я мог использовать её в рантайме со случайными параметрами, чтобы генерировать новые уровни.</li>
  </ol>
  <p id="n8QH">С такими требованиями я принял пару решений. За основу взял зацикленный сплайн в USplineComponent.  Для отображения сначала попробовал использовать UProceduralMeshComponent, но потом поменял свое решение в пользу UDynamicMeshComponent. Во-первых, потому что динамические меши добавили в 5.0 и их сейчас активно развивают. Во вторых, для них написано много реализации в UGeometryScriptLibrary_MeshPrimitiveFunctions, куда я могу обращаться за примерами.</p>
  <h2 id="ZIGA">Генерация Ориентации.</h2>
  <p id="8Nj0">Способом генерации я выбрал вращение вертикального вектора, в частности:</p>
  <ol id="qlxE">
    <li id="WFHB">Разбиваем весь сплайн на равные участки нужной длины.(это позволит мне контролировать детализацию ленты)</li>
    <li id="5sR9">Проходим все участки в цикле, каждый раз поворачивая вертикальный вектор на угол равный 360.0/количество участков.</li>
    <li id="zfof">Генерируем вершины нашей ленты, и соединяем их в треугольники, которые отправляем в dynamicmesh.</li>
  </ol>
  <p id="mR0a">Для проверки теории я решил быстро реализовать первые два пункта и проверить, что я правильно разобрался в интерфейсе USplineComponent.</p>
  <pre id="Dkl1" data-lang="cpp">UCLASS()
class FPSGAME_API ACRMobiusStrip : public AActor
{
	GENERATED_BODY()

public:
	// Sets default values for this actor&#x27;s properties
	ACRMobiusStrip();

	UFUNCTION(CallInEditor, BlueprintCallable)
	void  GenerateStrip();

protected:
	UPROPERTY(EditAnywhere)
	float SegmentLenght = 100;
	UPROPERTY(EditAnywhere)
	float SegmentWidth = 500;

	UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	class UDynamicMeshComponent* MobiusMesh;
	UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	class USplineComponent* SplineComponent;
	UPROPERTY(VisibleAnywhere,BlueprintReadWrite)
	class USplineComponent* MobiusSplineComponent;
};</pre>
  <p id="8qgo">N.B. полезный атрибут функции в UE CallInEditor. Он добавляет кнопку в редактор, которая и вызывает функцию. Это очень удобно для тестирования генераторов и неигровых логик.</p>
  <figure id="WsCo" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcFWbpoVlgtzNZKge_oiHsbAQYfIsu01l9hhaZXCSQMLiux4tX_pE3dUK0QWzX4Fs9xBaPSQ-jeK9wmEgpy7r0SX5LVLTbCqKW7UP9E2n9gqgMs4TAi4GCkxPpWZ5yuiA?key=Vx-soB7s9vU6MTK5w50UIQ" width="499" />
  </figure>
  <pre id="LIfe" data-lang="cpp">void ACRMobiusStrip::GenerateStrip()
{
   float splineLenght = SplineComponent-&gt;GetSplineLength();
   int numOfSteps = FMath::FloorToInt(splineLenght/SegmentLenght);
   FVector UpVector = FVector::UpVector;
  
   for (int index = 0; index &lt;= numOfSteps; ++index )
   {
      FVector Location = SplineComponent-&gt;GetLocationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::World);
      FRotator Direction = SplineComponent-&gt;GetRotationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
      FVector DirectionUp = UpVector.RotateAngleAxis(360.0/numOfSteps *index, Direction.Vector());


      DrawDebugDirectionalArrow(GetWorld(), Location, Location + DirectionUp *200.f, 20, FColor::Green, false);
   }
}
</pre>
  <p id="iq77">Я убрал из статьи инициализации компонентов и конструктор, ибо они совершенно обычные и я ничего интересного там не делал.</p>
  <p id="9d9x">В результате получаем следующую картину:</p>
  <figure id="wbRl" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfSq9AxBJ4IVKCZu56m7XBsvppBo3QWE8B1ODBwh06XQ1geM4TRxnNz8GNB9gmyfYk628avvxpDXRbL4rKF1dca-Owf1zDiUqXHtVd49R9WJ4FvMRBhS2w4U11vql1JDycjv2y0Lg?key=Vx-soB7s9vU6MTK5w50UIQ" width="602" />
  </figure>
  <h2 id="Yq5t">Треугольники</h2>
  <p id="wGaK">Теперь осталось построить прямоугольники на каждом участке, и лента готова.</p>
  <p id="rJuV">Подглядев на то, как с UDynamicMeshComponent работают в движке, я увидел два способа: собрать свою версию FMeshShapeGenerator , либо взять меш из компонента и настраивать вершины и треугольники напрямую. Я выбрал второй путь и остался доволен, так как это позволило мне контролировать материалы и UV в одном месте.</p>
  <p id="f6xr">Давайте начнем создавать вершины. На каждом участке делаем две вершины в плоскости, для которой наш повернутый вектор будет нормалью, сделав отступ вправо и влево от точки на сплайне.</p>
  <pre id="6l7B" data-lang="cpp">FVector Vert0 = Location + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
FVector Vert1 = Location + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
</pre>
  <p id="61dz">Добавим эти вершины в наш меш, сохраним индекс первой вершины:</p>
  <pre id="MwXS" data-lang="cpp">int StartVertIndex = MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert0));
MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert1));</pre>
  <p id="DY9f">Теперь для всех шагов, кроме первого (на первом у нас всего 2 вершины, треугольник не сделать), возьмем вершины с прошлого шага и сделаем из них 2 треугольника.</p>
  <pre id="hAvd" data-lang="cpp">if (index != 0)
{
   UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
   MobiusMesh3-&gt;AppendTriangle(Tri);       
   Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);       
   MobiusMesh3-&gt;AppendTriangle(Tri);
}


PrevStartVertIndex = StartVertIndex;</pre>
  <p id="aOkQ">Получаем следующий меш:</p>
  <figure id="TcYT" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXf4hzsVYiAEZkcKDhx4sjCMYwxt5O1Jng2NFMR-PiPi7igKhyi9XbfIT50TrLUk_HuusdqHj888nKrUcVA23rIHRisqlqABOQCjT9XtlrUJBoSea3VUgfr7kfLZlz_VznK8uQyb?key=Vx-soB7s9vU6MTK5w50UIQ" width="602" />
  </figure>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="FUwU">N.B. Есть несколько нюансов работы с UDynamicMeshComponent , которые я узнал, работая с этой лентой, и о которых хотел бы рассказать.</p>
    <p id="wKpT">Чтобы получить меш внутри компонента и работать, есть удобная функция  </p>
    <pre id="atAg" data-lang="cpp">DynamicMesh3* MobiusMesh3 = MobiusMesh-&gt;GetDynamicMesh()-&gt;GetMeshPtr(); </pre>
    <p id="ObAY">Не забудьте включить &quot;DynamicMesh&quot; в публичные зависимости вашего проекта, иначе линтер не даст вам собрать проект. </p>
    <p id="lxyM">Чтобы применить изменения в меше, нужно вызвать NotifyMeshUpdated на компоненте.</p>
  </section>
  <p id="QdRS"></p>
  <p id="WkhJ">Сразу видны три проблемы: первая - это не лента мебиуса, а просто перекрученная лента. И вправду - мой вектор нормали делает полный оборот, и поэтому получается две поверхности. Заменим нашу константу на настраиваемый параметр количества полуоборотов, что даст мне возможность генерировать любые ленты. Вторая проблема - это отсутствие последних треугольников, которые замкнут петлю. С этой проблемой мы разберемся в конце. Последняя проблема - это то, что лента видна только с одной стороны. Эту проблему можно решить с помощью шейдера, но я все равно хотел добавить толщины ленте (так как игрок будет двигаться по ней, он должен иметь возможность увидеть грань).</p>
  <p id="jQ7R">Прежде чем добавлять толщину, приведу полный код с исправленной проблемой полного оборота.</p>
  <pre id="LPDG" data-lang="cpp">UCLASS()
class FPSGAME_API ACRMobiusStrip : public AActor
{
	GENERATED_BODY()
	.
	.
	.
	UPROPERTY(EditAnywhere)
	int NumberOfHalfTurns = 1;

	float TotalAngle = 180.0f;
	.
	.
	.
}</pre>
  <pre id="mMhC" data-lang="cpp">void ACRMobiusStrip::GenerateStrip()
{
	float splineLenght = SplineComponent-&gt;GetSplineLength();
	int numOfSteps = FMath::FloorToInt(splineLenght/SegmentLenght);
	FVector UpVector = FVector::UpVector;

	TotalAngle = NumberOfHalfTurns * 180.0f;
	
	FDynamicMesh3* MobiusMesh3 = MobiusMesh-&gt;GetDynamicMesh()-&gt;GetMeshPtr();
	MobiusMesh3-&gt;Clear();

	int PrevStartVertIndex = 0;

	for (int index = 0; index &lt;= numOfSteps; ++index )
	{
		FVector Location = SplineComponent-&gt;GetLocationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FRotator Direction = SplineComponent-&gt;GetRotationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FVector DirectionUp = UpVector.RotateAngleAxis(360.0/numOfSteps *index, Direction.Vector());
		FRotator Rotator = FRotationMatrix::MakeFromZX(DirectionUp, Direction.Vector()).Rotator();

		FVector Vert0 = Location +  Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
		FVector Vert1 = Location - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
		
		int StartVertIndex = MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert0));
		MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert1));
		
		if (index != 0)
		{
			UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
			MobiusMesh3-&gt;AppendTriangle(Tri);			
			Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);			
			MobiusMesh3-&gt;AppendTriangle(Tri);
		}

		PrevStartVertIndex = StartVertIndex;
	}
</pre>
  <h2 id="PJCh">Толщина</h2>
  <p id="JMEU">Теперь добавим толщину: нам понадобятся еще две вершины. Наши старые вершины поднимем на половину заданной высоты (SegmentDepth) вдоль нормали нашей плоскости, а новые опустим вниз.</p>
  <pre id="RXAQ" data-lang="cpp">FVector Vert2 = Location - DirectionUp * SegmentDepth/2 + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
FVector Vert3 = Location - DirectionUp * SegmentDepth/2 - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
.
.
.
MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert2));
MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert3));</pre>
  <p id="9r77">Теперь у нас 4 узла и мы можем собрать верхнюю, нижнюю и боковые плоскости нашей “ленты”.</p>
  <pre id="hXLC" data-lang="cpp">//up plane
UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
MobiusMesh3-&gt;AppendTriangle(Tri);
Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);			
MobiusMesh3-&gt;AppendTriangle(Tri);
			
//bottom plane
Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 3, StartVertIndex + 3, StartVertIndex + 2 );
MobiusMesh3-&gt;AppendTriangle(Tri);
Tri = UE::Geometry::FIndex3i(StartVertIndex + 2, PrevStartVertIndex + 2, PrevStartVertIndex + 3 );
MobiusMesh3-&gt;AppendTriangle(Tri);

//left  side
Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 2, StartVertIndex+ 2 , StartVertIndex );
MobiusMesh3-&gt;AppendTriangle(Tri);
Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex, PrevStartVertIndex+ 2 );
MobiusMesh3-&gt;AppendTriangle(Tri);			

//right side
Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex +1, StartVertIndex + 3 );
MobiusMesh3-&gt;AppendTriangle(Tri);			
Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, PrevStartVertIndex + 3, PrevStartVertIndex + 1);
MobiusMesh3-&gt;AppendTriangle(Tri);</pre>
  <p id="M2DJ">Запускаем и получаем результат</p>
  <figure id="y2iO" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe8alOXMOPNlssgU0KCnQ8rCHxO-JQ0HQe5O_4ZQulC66AMeqj16UX9DsKae2H8XcqGAhPXzRiht3_S8adOqjx31GnLGKUB7FHHIx8POjlNpgmb9cdBSZfTlJ_eegDSVnguHHkklA?key=Vx-soB7s9vU6MTK5w50UIQ" width="602" />
  </figure>
  <p id="RE1E">Теперь давайте замкнем нашу петлю. С помощью блокнота и ручки я сумел понять, какие вершины с какими образовывают треугольник, и написал отдельную логику для последней секции.</p>
  <pre id="6VHi" data-lang="cpp">//we did half full  turn we need consider it in indexes
if (index == numOfSteps &amp;&amp; NumberOfHalfTurns % 2 != 0)
{
	//up plane
	UE::Geometry::FIndex3i Tri(PrevStartVertIndex + 3, StartVertIndex, StartVertIndex + 1 );
	MobiusMesh3-&gt;AppendTriangle(Tri);
	Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 2,PrevStartVertIndex + 3);			
	MobiusMesh3-&gt;AppendTriangle(Tri);				
				
	//bottom plane
	Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, StartVertIndex + 2,  PrevStartVertIndex + 1);
	MobiusMesh3-&gt;AppendTriangle(Tri);
	Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1 , PrevStartVertIndex , StartVertIndex + 3);
	MobiusMesh3-&gt;AppendTriangle(Tri);			
				
	//left  side			
	Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex+ 2 , StartVertIndex );
	MobiusMesh3-&gt;AppendTriangle(Tri);	
	Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex+3, PrevStartVertIndex+ 1 );
	MobiusMesh3-&gt;AppendTriangle(Tri);	
}
</pre>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="s6ZQ">Я не стал подробно объяснять, как я собирал треугольники из вершин. Там был простой и прямолинейный подход. Но если это кажется полезным, оставьте комментарий, и я подробно распишу как я собирал треугольники.</p>
  </section>
  <p id="vW0a">Теперь наша петля замкнута:</p>
  <figure id="YH8L" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfxNtCouw0x211WTtzHTLjqI6RtMPzbb_qkByPXLmPUvZ8de-4dQYumlAHCAP3gaVcBFebngJHZOxF7CC8CSIiNG53A1gLznOHZStIgFsaTE8KXANek6EzDYDvA0NfSGC4JQVjd?key=Vx-soB7s9vU6MTK5w50UIQ" width="602" />
  </figure>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="arF9">Первое, что я сделал - попробовал запрыгнуть на неё персонажем. Естественно коллизий у нее не было. Теоретически можно было завести кучу коробочек для моей ленты, но с учетом того, что она одна на уровне, и меш достаточно простой, я доверился оптимизации UE и просто включил сложные коллизии:</p>
    <pre id="AfTU" data-lang="cpp">MobiusMesh-&gt;SetComplexAsSimpleCollisionEnabled(true);</pre>
  </section>
  <p id="nKD4"></p>
  <h2 id="PW6Z">UV и материалы.</h2>
  <section style="background-color:hsl(hsl(0, 0%, var(--autocolor-background-lightness, 95%)), 85%, 85%);">
    <p id="LpXu">Для того, чтобы включить UV развертку и материалы на меше, на нем надо активировать атрибуты с помощью функции EnableAttributes().</p>
    <pre id="ziGx" data-lang="cpp">MobiusMesh3-&gt;EnableAttributes();</pre>
    <p id="Swqq">Для того, чтобы добавлять UV к конкретным треугольникам, удобно взять ссылки на два объекта FDynamicMeshUVOverlay и  FDynamicMeshMaterialAttribute.</p>
    <pre id="vBJz" data-lang="cpp">UE::Geometry::FDynamicMeshUVOverlay* UVOverlay 
                           = MobiusMesh3-&gt;Attributes()-&gt;PrimaryUV();
UE::Geometry::FDynamicMeshMaterialAttribute* MaterialAttribute
                           = MobiusMesh3-&gt;Attributes()-&gt;GetMaterialID();</pre>
    <p id="mvbq">Для того, чтобы проставить UV или материал, надо знать индекс треугольника (его возвращает функция SetTriangle)</p>
    <pre id="osFC" data-lang="cpp">UVOverlay-&gt;SetTriangle(tid, UVUpTri);
MaterialAttribute-&gt;SetNewValue(tid, 1);</pre>
    <p id="NGdW">Для того, чтобы назначить конкретный материал на меш, нужно его передать уже в компонент.</p>
    <pre id="oYJ2" data-lang="cpp">MobiusMesh-&gt;SetMaterial(0, BaseMaterial);</pre>
  </section>
  <p id="U3n3">Я завел поле для материала, в моем классе.</p>
  <pre id="Wm0V" data-lang="cpp">UPROPERTY(EditAnywhere)
UMaterialInterface* BaseMaterial;
</pre>
  <p id="L3Ad">Для UV развертки я решил, что у меня будет очень длинная лента в UV пространстве, которую я переадресую на мои треугольники равномерно. Будем добавлять точки к нашему UVOverlay  на каждом шаге цикла, генерируя небольшую полоску в UV пространстве, на которую будем проецировать все наши треугольники в этом шаге цикла.</p>
  <p id="iveE">Добавляем две вершины.</p>
  <pre id="lsCT" data-lang="cpp">float UVYOffest = SegmentLenght * index/ SegmentWidth;
int StartUVIndex = UVOverlay-&gt;AppendElement(FVector2f(0.0f, UVYOffest));
UVOverlay-&gt;AppendElement(FVector2f(1.0f,UVYOffest));
</pre>
  <p id="Ap1E">Создаем два треугольника в UV пространстве.</p>
  <pre id="wri8" data-lang="cpp">UE::Geometry::FIndex3i UVUpTri(PrevStartUVIndex, StartUVIndex, StartUVIndex + 1 );
UE::Geometry::FIndex3i UVDownTri(StartUVIndex+1, PrevStartUVIndex+1, PrevStartUVIndex);</pre>
  <p id="Gfc4">Сохраняем индексы наших треугольников и добавляем адресацию между треугольниками меша и UVOverlay</p>
  <pre id="lc1I" data-lang="cpp">UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
int tid = MobiusMesh3-&gt;AppendTriangle(Tri);      
UVOverlay-&gt;SetTriangle(tid, UVUpTri);


Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);        
tid = MobiusMesh3-&gt;AppendTriangle(Tri);
UVOverlay-&gt;SetTriangle(tid, UVDownTri);
</pre>
  <p id="Ajgy">Повторяем для всех треугольников. Назначаем тестовый материал для дебага UV.</p>
  <p id="ygnD">N.B. для последнего шага цикла, когда мы замыкаем петлю, мне понадобилась дополнительная магия с UV, чтобы все красиво сошлось в месте начала петли. Я честно добился правильной развертки методом проб и ошибок, но наверное это можно было вычислить как-то заранее.</p>
  <p id="1mhI">В итоге я получил вот такой меш.</p>
  <figure id="4Q6g" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe1SNjHWCr6-IudfQ_e4fu-QoDPhhAIjO-d5LbBJOAO663oWJKFjBwBVV4cjiT6SOEYY7UbJR5D8b9A5tzaGciKwoBF57JXif6pKJjed0a7ExM18Xi2Q5VA0wM_7P6YEwUjIS7hPw?key=Vx-soB7s9vU6MTK5w50UIQ" width="602" />
  </figure>
  <h2 id="54F1">P.S.</h2>
  <p id="4Ms5">Финальный код если кто то решит его прочитать</p>
  <pre id="XFhe" data-lang="cpp">void ACRMobiusStrip::GenerateStrip()
{
	float splineLenght = SplineComponent-&gt;GetSplineLength();
	int numOfSteps = FMath::FloorToInt(splineLenght/SegmentLenght);
	FVector UpVector = FVector::UpVector;

	TotalAngle = NumberOfHalfTurns * 180.0f;
	
	FDynamicMesh3* MobiusMesh3 = MobiusMesh-&gt;GetDynamicMesh()-&gt;GetMeshPtr();
	MobiusMesh3-&gt;Clear();
	MobiusMesh3-&gt;EnableAttributes();
	MobiusMesh3-&gt;Attributes()-&gt;EnableMaterialID();
	UE::Geometry::FDynamicMeshUVOverlay* UVOverlay = MobiusMesh3-&gt;Attributes()-&gt;PrimaryUV();
	UE::Geometry::FDynamicMeshMaterialAttribute* MaterialAttribute = MobiusMesh3-&gt;Attributes()-&gt;GetMaterialID();
	
	int PrevStartVertIndex = 0;
	int PrevStartUVIndex = 0;
	

	
	for (int index = 0; index &lt;= numOfSteps; ++index )
	{
		FVector Location = SplineComponent-&gt;GetLocationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FRotator Direction = SplineComponent-&gt;GetRotationAtDistanceAlongSpline(SegmentLenght * index, ESplineCoordinateSpace::Type::Local);
		FVector DirectionUp = UpVector.RotateAngleAxis(TotalAngle/numOfSteps *index, Direction.Vector());
		FRotator Rotator = FRotationMatrix::MakeFromZX(DirectionUp, Direction.Vector()).Rotator();
		int StartVertIndex = 0;
		if (index != numOfSteps)
		{
			FVector Vert0 = Location + DirectionUp * SegmentDepth/2 + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
			FVector Vert1 = Location + DirectionUp * SegmentDepth/2 - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
			FVector Vert2 = Location - DirectionUp * SegmentDepth/2 + Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
			FVector Vert3 = Location - DirectionUp * SegmentDepth/2 - Rotator.RotateVector(FVector::RightVector) * SegmentWidth /2;
		
			StartVertIndex = MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert0));
			MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert1));
			MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert2));
			MobiusMesh3-&gt;AppendVertex(UE::Geometry::FVertexInfo(Vert3));
		}

		float UVYOffest = SegmentLenght * index/ SegmentWidth;
		int StartUVIndex = UVOverlay-&gt;AppendElement(FVector2f(0.0f, UVYOffest));
		UVOverlay-&gt;AppendElement(FVector2f(1.0f,UVYOffest));
		
		if (index != 0)
		{
			UE::Geometry::FIndex3i UVUpTri(PrevStartUVIndex, StartUVIndex, StartUVIndex + 1 );
			UE::Geometry::FIndex3i UVDownTri(StartUVIndex+1, PrevStartUVIndex+1, PrevStartUVIndex);
			//we did half full  turn we need consider it in indexes
			if (index == numOfSteps &amp;&amp; NumberOfHalfTurns % 2 != 0)
			{
				//up plane
				UE::Geometry::FIndex3i Tri(PrevStartVertIndex + 3, StartVertIndex, StartVertIndex + 1 );
				int tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);
		
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 2,PrevStartVertIndex + 3);			
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);
				
				//bottom plane
				UVUpTri = UE::Geometry::FIndex3i  (StartUVIndex, StartUVIndex + 1, PrevStartUVIndex+1 );
				UVDownTri =  UE::Geometry::FIndex3i(PrevStartUVIndex+1, PrevStartUVIndex, StartUVIndex);
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, StartVertIndex + 2,  PrevStartVertIndex + 1);
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);
			
				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1 , PrevStartVertIndex , StartVertIndex + 3);
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);
				
				//left  side
				UVUpTri = UE::Geometry::FIndex3i  (PrevStartUVIndex , StartUVIndex, StartUVIndex + 1);
				UVDownTri =  UE::Geometry::FIndex3i(StartUVIndex + 1, PrevStartUVIndex+1, PrevStartUVIndex);

				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex+ 2 , StartVertIndex);
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex+3, PrevStartVertIndex+ 1 );
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);

				//right side
				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 2, StartVertIndex +1, StartVertIndex + 3 );
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, PrevStartVertIndex , PrevStartVertIndex + 2);
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);		
				
			}
			else
			{
				//up plane
				UE::Geometry::FIndex3i Tri(PrevStartVertIndex, StartVertIndex, StartVertIndex + 1 );
				int tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);
		
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 1 , PrevStartVertIndex + 1,PrevStartVertIndex);			
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);
				//bottom plane
				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 3, StartVertIndex + 3, StartVertIndex + 2 );
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);
			
				Tri = UE::Geometry::FIndex3i(StartVertIndex + 2, PrevStartVertIndex + 2, PrevStartVertIndex + 3 );
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);
				//left  side

				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 2, StartVertIndex+ 2 , StartVertIndex );
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex, PrevStartVertIndex, PrevStartVertIndex+ 2 );
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);

				//right side

				Tri = UE::Geometry::FIndex3i(PrevStartVertIndex + 1, StartVertIndex +1, StartVertIndex + 3 );
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVUpTri);

				Tri = UE::Geometry::FIndex3i(StartVertIndex + 3, PrevStartVertIndex + 3, PrevStartVertIndex + 1);
				tid = MobiusMesh3-&gt;AppendTriangle(Tri);			
				UVOverlay-&gt;SetTriangle(tid, UVDownTri);
			}		
		}
		PrevStartUVIndex = StartUVIndex;
		PrevStartVertIndex = StartVertIndex;
	}

	MobiusMesh-&gt;NotifyMeshUpdated();
	MobiusMesh-&gt;SetMaterial(0, BaseMaterial);
}
</pre>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/4GkBH9Vinpd</guid><link>https://teletype.in/@jazzyjohn/4GkBH9Vinpd?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/4GkBH9Vinpd?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Как я нейро-джунов игру делать учил.</title><pubDate>Tue, 12 Aug 2025 09:44:57 GMT</pubDate><media:content medium="image" url="https://img1.teletype.in/files/06/73/067373c3-df66-4c59-abcc-19ffe5c24b84.png"></media:content><description><![CDATA[<img src="https://img3.teletype.in/files/60/13/6013c82a-4922-474b-a58c-525c9a80bd73.png"></img>Пару дней назад мне надо было собрать простой прототип мобильной игры на Unity. Это были последние дни моего отпуска, а с Юнити я не работал уже года 3, поэтому огромного желания бросаться и писать нужные мне системы особо не было. Я решил совместить две интересные вещи: написать нужный мне код, и протестить кодогенерацию LLM в чем-то сложнее геометрических алгоритмов. Вышло неплохо, и я решил поделиться парой интересных находок.]]></description><content:encoded><![CDATA[
  <p id="Tbe4">Пару дней назад мне надо было собрать простой прототип мобильной игры на Unity. Это были последние дни моего отпуска, а с Юнити я не работал уже года 3, поэтому огромного желания бросаться и писать нужные мне системы особо не было. Я решил совместить две интересные вещи: написать нужный мне код, и протестить кодогенерацию LLM в чем-то сложнее геометрических алгоритмов. Вышло неплохо, и я решил поделиться парой интересных находок.</p>
  <figure id="Ed1n" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcMRp6gkkr1IfofaFtX8zPkJeDYbAKd0bLTWoHflgjKVYFGZZOCxvk1YBVB_GJFKkEKx98I3UcbFzoGxc7i-glZ3JR-0Y7Ezf_6zwxAD1lhd2EBWGAFW0INPOXVlTWuTvNvSlBR2Q?key=i7semuRDWuN3Un2R7wBknw" width="602" />
    <figcaption>То с чего начиналась работа над прототипом ( Почему я использую женский род в обращениях к Deepseek, тайна даже для меня)</figcaption>
  </figure>
  <h3 id="X5el">Если вы джун или мидл.</h3>
  <p id="BeTK">Если вы только начали путь в программировании, или еще не до конца уверены в своих навыках, я настоятельно не рекомендую генерировать код с помощью LLM. Когда их обучают, перед компаниями не особо стоит цель отфильтровать материалы, объем данных должен статистически выправить ошибки. А открытые репозитории github - это на 90 процентов неизвестные студенческие или джуновские проекты, качество кода в которых оставляет желать лучшего. Я тут не исключение, мой гитхаб - это кошмар, ибо я заботился о нем только во времена поиска своей первой работы и с тех пор не трогал. Даже страшно представить, что за “чудо-код” я у себя там найду.</p>
  <p id="8rrQ">Поэтому LLM будет очень часто генерировать сомнительный код.Так что новичкам я рекомендую использовать LLM как личного репетитора или ментора: задавайте вопросы, просите провести ревью вашего кода, но старайтесь избегать именно генерации кода. В целом LLM - это отличный инструмент для обучения, жаль его не было у меня, когда я учил PHP и KOHANA</p>
  <figure id="grz8" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeGa3F2zT_my1n_Wecklc3qpNuDGyJhhaOTzn1wacNlp9R6ubrm41ak811h0zPBWoMzBE0iYWYyQvDcN5ElIpAFFfLCTRUJXuJ-8spFVTfC--9MzBw89ZZhMzg9iywSP5kEgme7GA?key=i7semuRDWuN3Un2R7wBknw" width="443" />
    <figcaption>Что-то в этом цикле не то, но вот что, да, DeepSeek?</figcaption>
  </figure>
  <h3 id="DaOk">Если вы сеньор или лид.</h3>
  <p id="lG44">Ситуация кардинально меняется, если у вас уже есть интуиция на плохой код, и вы уже умеете делать код ревью. Тут можно оттянуться по-полной. Я  собрал прототип за 5 дней с UI, главным меню и всем игровым циклом, при этом потратив раз в 5 меньше времени на код. В основном я рефакторил слишком простые решения (например атрибуты для моего аналога GAS GPT сгенерировала прямо в лоб, поэтому я их переписал и усложнил, как того требовал дизайн), интегрировал их вместе и настраивал ассеты в Юнити.</p>
  <p id="okBn">Я открыл пару вкладок Deepseek и параллельно отдавал на реализацию разные части прототипа, давая фидбэк и внося корректировки. Эдакий мясной лид нейро-джунов. Кстати, это забавно, но мне кажется, что для сеньора работа с LLM может стать хорошей практикой для общения с джунами.</p>
  <figure id="tbXi" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdcQ7DZZVwTuGXl_yDylb_iNYyJ0swUO7xRow_Tw5drQiBgmPfVzXdDBOBMiIknS278W9CE58LoYC_KBPwaM3_6IdBWfmIRyq0H7JK4Q6yRFoDc1muk8U54cyb87MoGUsUZTs4sQQ?key=i7semuRDWuN3Un2R7wBknw" width="457" />
    <figcaption>Успешные декомпозиция прототипа для нейро комнады</figcaption>
  </figure>
  <p id="um9Y">Я для себя понял пару правил, которые позволили мне получать полезный код от GPT</p>
  <ol id="qHrJ">
    <li id="NVf3">Расписывайте подробно по пунктам, что нужно, короткими предложениями. Представьте, что вы делаете чеклист для Jira или QA. Тогда шансы на то, что LLM что-то не поймет или нафантазирует, сокращаются.</li>
    <li id="ksme">Декомпозируйте задачу. Несмотря на то, что сейчас в LLM можно закинуть много контекста, LLM очень часто игнорируют или понижают в приоритете середину запроса. Поэтому лучше работать в несколько этапов: просить что-то, а потом просить дорабатывать что вышло. Например. я сначала попросил собрать мне инвентарь со слотами, а потом попросил сделать систему сохранений, и вышло неплохо и без багов. Я старался держать размер запроса в 3-4 простых пункта для реализации.</li>
    <li id="MJPT">Задавайте примеры. Если вы знаете, что именно вам нужно и что оно где-то было реализовано (в другом движке или фреймворке), прямо укажите это как пример. На удивление, это срабатывает очень хорошо.</li>
    <li id="2R1k">Не мучайте LLM багофиксом. Она не сможет, она будет галлюцинировать и оверинженирить. Пофиксите это сами, когда будете интегрировать уже в проект.</li>
    <li id="sh01">Помогайте им с архитектурными решениями. Например, скажите, что использовать, как, что будет взаимодействовать.</li>
    <li id="1RUy">Не доверяйте им интеграцию систем друг с другом. У меня не получилось заставить DeepSeek нормально поженить две системы без глупостей.</li>
  </ol>
  <p id="PYEy">В принципе для прототипа мои нейро-джуны справились на ура, игра бегает, багов было мало, и у меня была куча свободного времени. Да, местами код кривоват, и не самый оптимальный, но я знаю кучу проектов, которые релизнулись с кодом хуже. Я был приятно удивлен результатом и рекомендую всем иногда отдавать генерацию кода LLM.</p>
  <figure id="wo79" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfN8WxGnPLpCVnx3cxelTLzYIavFsbGTYT_M_6JGSLQw89hfdDtv8_LbQ98bpFuyMkIQtswwTChtGvil2XoDc5mur_fuw5Z3OUj-XYp-a16HmyI13ZWndPOYTbTAtVWtrogO-iUYw?key=i7semuRDWuN3Un2R7wBknw" width="602" />
    <figcaption>От работы в час ночи даже нейро-джун может выгореть</figcaption>
  </figure>
  <p id="QEcJ">P.S. А еще я скормил Deepseek правила синтаксиса для <a href="https://dev.epicgames.com/documentation/en-us/unreal-engine/using-shape-grammar-with-pcg-in-unreal-engine" target="_blank">PCG Gramar</a>, и она с первого раза сгенерила мне код для грамматической генерации в юнити.</p>

]]></content:encoded></item><item><guid isPermaLink="true">https://teletype.in/@jazzyjohn/DIpSWX2rK7o</guid><link>https://teletype.in/@jazzyjohn/DIpSWX2rK7o?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn</link><comments>https://teletype.in/@jazzyjohn/DIpSWX2rK7o?utm_source=teletype&amp;utm_medium=feed_rss&amp;utm_campaign=jazzyjohn#comments</comments><dc:creator>jazzyjohn</dc:creator><title>Игровой ИИ. GOAP</title><pubDate>Wed, 02 Jul 2025 10:31:10 GMT</pubDate><media:content medium="image" url="https://img2.teletype.in/files/94/1c/941cf296-050d-4564-9d61-fd4ad18ea43e.png"></media:content><description><![CDATA[<img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXd1FhXpoB9saGKOjfo4gJzJ2h_KJJlFpvJkrn15ValRVf8Rx6na__u15w_uboMegKnO-n6rNscPUtTwT_8J9Ed53q1cNKVYNxXUo4cd90Szim60LRq-UcpbsmgDzR-y9BqD-_rX1g?key=T9eDSqP8dXxYeiPCHDYL_A"></img>В прошлый раз мы закончили с ИИ, основанным на полезности (utilityAI), и сегодня мы начинаем обсуждать новый тип игрового ИИ Goal Oriented Action Planning (GOAP).Также не забывайте подписываться на мой канал, чтобы не пропустить следующие части.]]></description><content:encoded><![CDATA[
  <p id="8umu">В <a href="https://teletype.in/@jazzyjohn/awvu3FvhvYG" target="_blank">прошлый раз</a> мы закончили с ИИ, основанным на полезности (utilityAI), и сегодня мы начинаем обсуждать новый тип игрового ИИ Goal Oriented Action Planning (GOAP).Также не забывайте подписываться <a href="https://t.me/Vaniagramming" target="_blank">на мой канал</a>, чтобы не пропустить следующие части.</p>
  <figure id="OcVD" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXd1FhXpoB9saGKOjfo4gJzJ2h_KJJlFpvJkrn15ValRVf8Rx6na__u15w_uboMegKnO-n6rNscPUtTwT_8J9Ed53q1cNKVYNxXUo4cd90Szim60LRq-UcpbsmgDzR-y9BqD-_rX1g?key=T9eDSqP8dXxYeiPCHDYL_A" width="602" />
  </figure>
  <h2 id="N7ah">Планирование</h2>
  <p id="sthO">Основой GOAP является создание и выполнение плана, состоящего из простых действий, с определенной и конкретной целью. Получающиеся из такого планирования поведения очень динамичны, чаще всего соответствуют ожиданиям игрока с учетом текущей обстановки в игре, и в целом отличаются от заскриптованных поведений разнообразием и иллюзией интеллекта.</p>
  <h2 id="cInj">Состояние Мира</h2>
  <p id="Fmsw">Главным и важным компонентом GOAP является структурированное состояние мира. Наши предыдущие системы ИИ тоже анализировали что происходило в мире (где противник, ранен ли агент, видна ли текущая цель). В случае GOAP состояние мира является не абстрактной концепцией информации, необходимой для работы ИИ, а полноценным объектом в памяти, содержащим информацию о мире в понятном остальному алгоритму формате.</p>
  <pre id="BWoN">class GOAPWorldState

{

bool bPlayerDead = false;

int AliveAICount = 3;

bool AIHasWeapon = true;

int AmmoAmountInWeapon = 0;

};</pre>
  <p id="8WBc">Конечно же в реальных системах состояние мира использует контейнеры для записи информации, а не поля, по аналогии с Blackboard. Важно заметить, что состояние мира удобно сделать уникальным для каждого агента. Да, какие-то значения будут одинаковыми (например состояние Игрока) для всех агентов, но часть информации будет иметь смысл относительно конкретного ИИ агента. Такой подход связан с тем, что GOAP перекочевал в геймдев из робототехники, и там схожие алгоритмы работают непосредственно на агенте, который и будет выполнять план, поэтому все остальные агенты как бы не являются ИИ для робота, а являются внешними факторами состояния мира. В геймдеве же очевидно, что планирование и выполнение планов для всех агентов происходит внутри одной программы, а чаще всего внутри одной системы. Поэтому это порождает создание индивидуальных состояний мира для каждого агента, который хочет использовать GOAP.</p>
  <h2 id="L0JH">Цель (Goal)</h2>
  <p id="pWxG">Цель в GOAP описывается тоже с помощью состояния мира: это набор нескольких полей из состояния мира, значения которых зафиксированы. Например целью может быть поднять предмет, т.е. флаг bHasItem в состоянии мира агента должен иметь значение true. Кроме того у каждой цели есть приоритет или полезность этой цели, т.е. цифровое представление того, насколько эта цель сейчас актуальна для игрока. В роли такого представления могут выступать функции полезности из UtilityBasedAI. В GOAP вместо выбора поведения агент выбирает цель, но для выбора используются аналогичные подходы и алгоритмы как в UtilityBasedAI.</p>
  <h2 id="et2H">Действия (Action)</h2>
  <p id="ezzF">Действия в GOAP - это маленькие простые поведения. Чаще всего это комбинации перемещения и либо совершения каких-то действий самим агентом (атака, кувырок, использование способности), либо взаимодействие с каким-то объектом в игровом мире (открыть дверь, поднять оружие, включить тревогу). Кроме самой реализации у действий есть несколько характеристик, необходимых для работы в GOAP.</p>
  <p id="BP3R">Первая характеристика - это то, как это действие меняет состояние мира. Чаще всего эта характеристика описывается схожа с целями: это набор полей со значениями из состояния мира, обозначающие изменение состояния мира при выполнении этого действия. Например действие “открыть дверь” описывается полем DoorState = Open.</p>
  <p id="mIpn">Вторая характеристика - это требования к состоянию мира для выполнения этого действия. Эта характеристика аналогична первой, но описывает состояние мира, необходимое для выполнения этого действия. Например действие стрельбы требует наличие у агента оружия дальнего боя.</p>
  <p id="ubKX">Третья важная характеристика - это стоимость действия. Она описывает то, насколько затратно действие для агента. Эта характеристика появляется из-за того, что при планировании мы будем использовать алгоритмы поиска по графу, а следовательно стоимость позволит выбрать самый оптимальный план.</p>
  <h2 id="lYGE">Smart Objects</h2>
  <p id="CbKo">В GOAP используется очень удобная концепция умных объектов(smart objects). В игровом ИИ агентам очень часто приходится взаимодействовать с интерактивными объектами в мире - это могут быть кнопки, двери и т.д. Планы и действия в  GOAP чаще всего не привязаны к конкретному агенту или объекту, действие может быть абстрактным (например подойти и провзаимодействовать с предметом). Поэтому необходим ассет, в котором будет храниться информация о том, как взаимодействовать с предметом (например какую анимацию проигрывать), состояние объекта (дверь открыта или закрыта) и т.д. Роль такого асета выполняют умные объекты. Умные объекты представляют собой структуру, в которой хранится вся информация, необходимая ИИ для взаимодействия с нею. Умные объекты чаще всего содержат в себе простой конечный аппарат (FSM) для описания своего состояния и для проигрывания анимаций и эффектов, связанных с взаимодействием с ним (например анимация открытия двери). В большинстве реализаций также существует удобный интерфейс для получения списка умных объектов в заданном расстоянии или на уровне.</p>
  <p id="W6cL">Подход умных объектов позволяет легко добавлять новые сущности в геймплей, не меняя ИИ и не исправляя уже существующие объекты. N.B. умные объекты используются не только в GOAP, но и в других системах принятие решения, а также в других игровых системах: например взаимодействия игрока с объектами на карте.</p>
  <h2 id="uiQQ">План</h2>
  <p id="mgg9">Основной целью GOAP является создание и выполнения плана. Под планом подразумевается последовательность действий, которая приводит к выполнению цели агента. Планы статичны и не меняются с момента создания - в этом и основной минус GOAP: для того, чтобы отреагировать на изменившуюся обстановку, нам необходимо снова выполнить планирование, а это ресурсоемкая и не быстрая задача. А поскольку планы и цели у нескольких агентов могут совпадать, то неожиданно получается, что у нескольких агентов планы становятся неактуальны, что создает неравномерную нагрузку на систему ИИ и отрицательно влияет на производительность.</p>
  <h2 id="ygtG">Построение плана</h2>
  <p id="inEO">Для того, чтобы построить оптимальный план, мы строим граф, где роль узлов выполняют состояния мира, и действие, которое это состояние сгенерировало, за исключением стартового и конечного узла, у которых нет действий. После чего мы ищем кратчайший маршрут с помощью А* от целевого состояния мира к текущему. Конечно же в реальных имплементациях никто не строит весь граф, а мы сразу начинаем искать кратчайший маршрут с помощью А*, параллельно создавая наш граф. Каждый раз добавляя действия в наш граф, мы генерируем узел, в котором описано состояние мира. Это состояние - комбинация состояния цели и требований, существующих у действий, часть полей в состоянии мира узла совпадает с текущим реальным состоянием. Если мы посчитаем количество полей, которое не совпадает, мы получим удобную эвристическую функцию для А*. (напомню: эвристическая функция - это способ оценки того, насколько узел графа близок к цели)</p>
  <p id="SB4c">Давайте рассмотрим пример. У нас существует агент со следующими целями</p>
  <figure id="tBjL" class="m_original">
    <img src="https://img3.teletype.in/files/a1/90/a190b003-b1bf-473d-bac2-5c2d33b33a81.png" width="631" />
  </figure>
  <p id="JvbK">Также у нашего агента есть набор действия</p>
  <figure id="6sFB" class="m_original">
    <img src="https://img1.teletype.in/files/42/47/42476d64-3810-4629-9754-63e1df938686.png" width="626" />
  </figure>
  <p id="f7au">Давайте наш агент выберет цель Атаковать Врага, и у него на старте есть оружие ближнего боя.</p>
  <p id="TG4T">На первом шаге наш граф поиска плана выглядит следующим образом</p>
  <figure id="jFjk" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdUn4Sn1X-YUSDSpLnw9a-ztLs-vYVtJ0fEd8whVJxT8CELEstUPH86GDEi67B51bKmL4TR3-69QnWbkyX7Aor5j2dY5R-v6FTzBUmcRib6X5yihmmgemiZVqPIfuE_6FnrFhk4?key=T9eDSqP8dXxYeiPCHDYL_A" width="602" />
  </figure>
  <p id="mq58">Как видно, поскольку наш агент уже обладает оружием ближнего боя, эвристика для атаки в ближнем бою ниже, чем для дальнего боя. А* продолжит исследовать граф, начиная с ноды с наименьшей эвристикой. Следующий шаг будет выглядеть следующим образом</p>
  <figure id="ky4L" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXdmWrcU5ZAzBh3Pwhj8NOEOKrZoJ1FpAN5hRvVuenNJl55Dn5KFNyUZ7TzgivDh3od3HWRhrjG-Zkq6uW3rgVEwC4AcN7hYBoQasGpwkSagHFLQO4LHs5OhET60gdR6KgI7bvRG4g?key=T9eDSqP8dXxYeiPCHDYL_A" width="602" />
  </figure>
  <p id="zNGu">Теперь мы выполнили все требования, которые появились от наших действий в состоянии мира, и при этом пришли в состояние, которое соответствует текущему. Наш план готов и он состоит из двух действий: подойти к противнику и атаковать. Давайте представим, что у нас не получилось подойти к противнику, ведь наши действия не только влияют на состояние, но и проверяют возможность быть выполненными, поэтому логично предположить, что у нас может не быть пути до противника. Тогда А* пойдет разворачивать другую ветку графа, и после двух шагов мы получим следующий граф</p>
  <figure id="lwzR" class="m_custom">
    <img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfJUjBjxVd7IBsDWj9sJo_D9lUHNLZqbozbAs78eSkMePyarrec3k76P-ktv8Xk0QdYKRXH_wGQUsdmPMvk5iNBSdz35JN7w7HpkwCRfYrajf5dyKGYb7DPgs1ahFSTcqXGGU7Aqg?key=T9eDSqP8dXxYeiPCHDYL_A" width="539" />
  </figure>
  <p id="C6Gn">Теперь наш план состоит из 3 действий: найти оружие дальнего боя, подойти на расстояние выстрела, и атаковать. <br /> Этот пример хорошо демонстрирует как из простых настроек и действий, генерируется логичное и “умное” поведение.</p>
  <p id="QaOT">В следующий раз мы обсудим некоторые особенности работы с GOAP.</p>

]]></content:encoded></item></channel></rss>