Комплексная система — это логическая структура, включающая в себя множество других логических блоков.
Например, такой системой может являться система способностей персонажа, которая включает в себя не только эффекты всех способностей, но и систему их вызова.
В такой системе крайне важно не просто писать логику её работы, но и организовывать ее структуру для сохранения понятности и читабельности. В достижении этой цели могут помочь следующие подходы к организации системы:
— Атомарность — Визуальность — Сегментированность — Модульность и Универсальность — Масштабируемость
Визуальность
Несмотря на многие минусы блюпринтов, у них есть один значительный плюс — это визуальное программирование.
Программисты активно утверждают, что это неудобно, занимает слишком много места и усложняет навигацию по логике. Однако подобные рассуждения являются лишь следствием факта, что людям всегда свойственно отторгать то, что им кажется непонятным, а также не стоит забывать, что порог входа у блюпринтов намного ниже, чем у C++ и, как следствие, гораздо больше примеров плохо написанной логики, которую создают новички в этом деле.
На самом деле возможность визуально выстраивать логику очень сильно упрощает работу с ней.
По сути сами программисты сами прибегают к подобному, составляя блок-схемы будущего кода перед стартом работы. Здесь же визуальность сохраняется на всем протяжении разработки.
Лучшее объяснение этого пункта дал один мой знакомый программист:
Визуальные системы должны оперировать семантически наполненными действиями: иди туда, потом подбери вещь, потом используй ее, ударь врага и так далее. Как только в визуальных системах появляются уравнения, или ненаполненный семантикой код, это превращается в нагромождение логических блоков. Блюпринты хороши, когда под ними уже лежат системы и тебе надо ими управлять из блюпринтов.
Но чтобы эта визуальность работала, должна присутствовать понятность в том, как она организована.
Приведу простой пример (который также является обоснованием мысли, почему блюпринты и классический код требуют разных подходов в написании, и из-за этого, если человек является хорошим программистом, это вовсе не значит, что он будет хорошим блюпринтистом, даже если будет писать код визуально).
Программисты пишут код для игр в рамках концепции объектно-ориентированного программирования. Весь код организуется в функции.
Однако при использовании точно такого подхода образуются цепочки функций. То есть одна функция вызывает другую (или даже несколько других в зависимости от условий). Таким образом появляется иерархия функций и включений, и, как следствие, чтобы ориентироваться в них, необходимо постоянно перемещаться по иерархии и держать всю общую структуру логики у себя в голове.
При этом блюпринтист бы, в таком случае, делал бы основу системы логики в EventGraph.
То есть в одном пространстве, где человек сразу может видеть всю структуру логики и понимать, какие её куски по каким условиям запускаются, что позволит вам легко отслеживать баги и нарушения в системе вызова тех или иных кусков логики, т.к вы будете знать, что за все вызовы отвечает одна система.
Пример структуры логики в EventGraph
Вывод: Основной граф логики создается в EventGraph и строится как одна логическая система для удобства визуальной работы с ней.
Сегментированность
Однако, продолжая предыдущий пример, если писать всю игровую логику в EventGraph, как делают многие начинающие блюпринтисты, легко превратить его в нечитаемое нагромождение из операций, которое также не будет следовать принципам Визуальности. И тогда на помощь приходит Сегментированность.
Предположим, что мы разрабатываем систему способностей персонажа. Следуя предыдущему принципу, основу логики активации способностей мы создаем в EventGraph (то есть функционал активации сущности под названием «способность»). Например, создаем CustomEvent, который на вход берет тип способности из Enumeration и затем через набор Switcher-ов, вызывает активацию логики нужной способности.
Пример структуры логики активации магических способностей
Однако логика работы самой способности, в действительности не относится к дереву логики ее активации. Это атомарная сущность, которая вызывает какой-либо эффект.
Таким образом мы можем вынести ее код в отдельную функцию (например, FireSpell или SwordAttack), а в общий граф логики системы способностей вынести лишь вызов этой функции, тем самым оставив его лаконичным и читабельным.
Структура логики активации магических способностей с вынесенными функциями
К тому же это облегчит вам процесс поиска ошибок, потому как если у вас некорректно работает конкретная способность, вам не придётся искать куски ее логики в большом графе, и вы сможете быстрее локализовать проблему, проверив отдельную функцию.
Вывод: Основной граф логики создается в EventGraph и строится как одна логическая система для удобства визуальной работы с ней, однако логики отдельных вызываемых этим графом блоков выносятся в функции для сохранения читабельности кода.
Модульность и Универсальность
Итак, мы определились с тем, как создавать общую структуру логики. Однако дальше остается вопрос, как же писать те блоки, что мы выносим в функции.
Самым простым ответом тут будет являться — создаем новую функцию для каждого отдельного блока.
Однако, такой подход скорее всего обернется большим количеством копирования кода и дублирования логики, потому как функции могут работать очень похожим образом. Для того, чтобы этого избежать, можно использовать один из подходов (а на самом деле их комбинацию, поскольку в разных ситуациях релевантен один или второй):
— Универсальность
Предположим, в нашей системе способностей герой может кастовать огненные, ледяные и кислотные шары. Общая суть работы способностей одна и та же, отличается лишь то, какой проджектайл и с какой скоростью создается. В такой ситуации нет никакого смысла создавать функцию на каждый вид заклинания. Гораздо практичнее создать одну функцию, которая в зависимости от принимаемых в себя параметров будет выдавать нужный эффект. Таким образом в общем графе логики эту функцию можно будет подставить во все места каста заклинания, лишь указав нужные параметры.
— Модульность
Предположим, наш персонаж получает светящуюся ауру в моменты, когда на него наложен бафф, а также когда он получил критический урон или находится в процессе создания заклинания. Мы можем прописать создание ауры в каждом из этих пунктов. Но также мы можем создать не просто функцию, а модуль. То есть функцию, которую можно легко встраивать в другие функции и графы, и которая не зависит от внешних систем и параметров. Таким образом мы сможем использовать эту функцию во всех местах, где нам будет удобно и вызывать нужный эффект.
Пример использования функции-модуля
К тому же, не будет возникать такого эффекта, что одна и та же логика в одном блоке работает корректно, а в другом нет. Все проблемы локализуются в рамках одной универсальной функции.
Вывод: Основной граф логики создается в EventGraph и строится как одна логическая система для удобства визуальной работы с ней, однако логики отдельных вызываемых этим графом блоков выносятся в функции для сохранения читабельности кода. При этом функции проектируются как универсальные и модульные, чтобы можно было эффективно переиспользовать их в рамках графа и других функций.
Масштабируемость
Однако в конце концов мы приходим к главной проблеме систем с централизованной логикой (в данном случае основного Графа) — отсутствие у нее гибкости. Поэтому, с самого начала необходимо заложить в нее возможности для масштабирования.
Даже если изначально четко известно, что у персонажа есть десять конкретных способней, это вовсе не означает, что в процессе разработки не появится одиннадцатая. Для этого необходимо закладывать в ваш основной граф вызова способностей способ легко добавлять слот для новой функции способностей.
Однако, даже такой подход не убережет вас от самой частой проблемы таких систем. Когда вы в самом начале закладываете структуру логики, то появившийся в будущем элемент, работающий по иной логике, может серьезно нарушить общую структуру работы.
Пример: вся система строится на вызове одноразовых действий (удар, каст заклинания, прием зелья и т. д.), однако в процессе у нас возникает необходимость внедрить способность с продолжительностью действия, что нарушает устоявшуюся структуру работы основного графа.
Самым простых шагом в такой ситуации кажется отойти от системы и добавить этой функции некую иную логику, или попытаться встроить в устоявшуюся логику новую систему используя надстройку другой логики.
Однако оба этих подхода ведут к одному исходу: вы нарушите общую стройность логики и усложните не только будущую работу с ней, но и процесс поиска ошибок, поскольку через некоторое время в переплетении надстроек вы перестанете понимать, как в целом должна работать ваша система.
Поэтому, самым грамотным решением в таком случае будет провести рефакторинг. То есть заново рассмотреть то, какой должна быть основа вашей системы, учесть все новые факторы и создать новую, которая будет учитывать их все без необходимости создавать надстройки.
Это тяжелый и долгий процесс (и чем больше кода было написано, тем сложнее его осуществить), однако если его не проводить каждый раз, когда система перестает выполнять свои функции, то рано или поздно вы все равно придете к тому, что будете ее переписывать, но тогда уже разобраться в том, что именно происходит в коде будет намного сложнее.
К тому же система деления на функции позволит вам провести этот процесс намного проще, поскольку вы будете менять лишь логику их вызова, а не суть всех процессов, практически не трогая то, что вы уже написали внутри этих функций.
Вывод из первой главы:
Основной граф логики создается в EventGraph и строится как одна логическая система, которая может масштабироваться при появлении новых функций, однако логики отдельных вызываемых этим графом блоков выносятся в функции для сохранения читабельности кода.
При этом, функции проектируются как универсальные и модульные, чтобы можно было эффективно переиспользовать их в рамках графа и других функций.
В случае, если граф логики перестает удовлетворять потребностям, он должен быть оперативно переработан для создания новой системы, учитывающей все необходимые условия.