Иной раз, когда читаешь технического задание и ставишь сроки по выполнению, недооцениваешь количество затраченного времени и усилий на решение той или иной задачи. Бывает так, что пункт, который оценивается по времени в неделю, выполняется в один час, а бывает и наоборот. Но эта статья не об этом. Это демонстрация эволюции решения задачи. От самого ее зарождения, до реализации.   Марка или маркер — изображение, загруженное в AR-движок, которое распознается камерой устройства (планшет или смартфон) и может быть однозначно идентифицировано  Найден — состояние маркера, когда он был обнаружен в поле зрения камеры Потерян — состояние маркера, когда он был потерян из поля зрения камеры Может быть отображено — когда маркер найдет, мы отображаем контент, прикрепленный к маркеру Не может быть отображено — когда маркер найдем, не отображаем контент — Контент, прикрепленный к маркеру — любой объект (3D модель, спрайт, система частиц и т.п.), который может быть прикреплен к маркеру и, который, соответственно, будет отображаться на экране если маркер найден Марка, маркер, найден, потерян — базовые состояния присущие всем движкам предоставляющие функционал распознавания  Может быть отображено и не может быть отображено — состояние, используемые для решения данной задачи Пример:  Есть одна большая открытка размером с лист А4. Она разделена на 4 равных части (формат одной части А5), на каждой из этих частей есть:  Если вы работали с любыми движками по распознаванию, например, Vuforia, то наверняка знаете, что не существует такого понятия как “качество распознавания”. Марка либо распознана, либо не распознана. Соответственно, если движок “видит” марку, он меняет состояние на Find и вызывается метод OnSuccess(), если он ее “потерял”, то состояние меняется на Lost и вызывается метод OnLost(). Соответственно из имеющихся условий и вводных данных, возникла ситуация, когда имея часть открытки (половину или четверть) можно было распознать марку.  Дело все в том, что по техническому заданию планировалось постепенная разблокировка персонажей. В данной же ситуации, постепенная разблокировка возможна, но с учетом того что не найдутся люди, которые попытаются распознать четверть или половину марки. Необходимо реализовать логику в виде программного кода, которая обеспечивает постепенную разблокировку контента прикрепленного к маркерам. Из расположения элементов на открытке известно, что маркера 1, 2, 3, 4 доступны для отображения изначально.  Если считан и отображен контент на 2 маркерах, например, 2 и 3 то разрешаем отобразить контент на маркере 6. Если маркер 1 ещё не считан, то доступ к маркеру 5 закрыт. Далее по аналогии. Мы как бы даем разрешение на отображение контента у боковых маркеров только тогда, когда у нас считаны соседние угловые маркеры.  Если доступны и были найдены маркеры от 1 до 8, то открываем к отображению контент на маркере 9. У каждого маркера есть 2 состояния — доступен и не доступен контент к отображению, за которое отвечает поле public bool IsActive;  Сразу понятно, что это должен быть либо конечный автомат с переходом между состояниями, либо реализация паттерна “Состояние”. В итоге получилось не то, не другое. Не скажу, что это костыль потому что решение полностью удовлетворяло поставленным требованиям в начале статьи. Но вы можете поспорить со мной. На этом, предоставляю вам возможность самим немного подумать над возможными решениями и реализациями данной задачи. У меня на осознание и закреплении в голове картины решения ушло около 5 часов.  Для наглядности записал видео на котором запечатлен уже конечный результат работы алгоритма (если это можно таковым назвать). Первое что пришло в голову это представить взаимодействия между маркерами от углового к центральному. В графическом виде это выглядит так:  Проблемы: Не сумев уложить у себя в голове изложенное выше решение из-за множества краевых случаев и сложности восприятия, я изменил подход к выбору маркера от которого начинают распространяться зависимости. Раздумывая над решением 3 пункта предыдущего подхода, пришла идея изменить тип маркера, от которых начинают меняться состояния других маркеров. Как основные были приняты боковые маркеры. При таком раскладе связи (зависимости) выглядят таким образом:  Отсюда сразу становится понятно, что связи от бокового к центральному лишние, потому что боковому маркеру не нужно ничего знать о центральном маркере, потому этот подход сразу трансформировался в конечный.  Конечное решение, когда боковой маркер знает об угловых, угловые “живут своей жизнью”, а центральный знает о состоянии всех маркеров.  Работать с представлением в виде открыток не очень удобно. Связи между сущностями выглядят не достаточно наглядно чтобы легко преобразовать это в код. Попытка интерпретации в виде бинарного дерева может внести некоторую двузначность. Но тут нарушено одно из свойств бинарного дерева, потому двузначность сразу отпадает. Из чего можно сделать вывод, что данное представление можно однозначно трактовать и использовать для графического представления решения задачи. Исходя из этих выводов, будем использовать нотацию графов, а именно: Преимущества: Создадим интерфейс, который содержит в себе элементы присущие каждой сущности (имя, состояние): Далее опишем сущность каждой ноды: Почему IsActive = true? Из условия задачи контент угловых маркеров изначально доступен к распознаванию. Теперь, когда у нас созданы все элементы открытки у нас сделаны (все виды маркеров), мы можем начать описывать сущность самой открытки. Я не привык начинать создание класса с конструктора. Я начинаю всегда с базовых методов, которые присущи конкретной сущности. Начнем с приватных полей и приватных методов. С полями все достаточно просто. 2 списка с угловыми, боковыми нодами и одно поле центральной ноды. Дальше нужно немного пояснить. Дело в том, что сам маркер имеет тип Trackable и он понятия не имеет (и не должен иметь) о том, что он является частью какой-то там другой логики. Потому все что мы можем использовать для того чтобы управлять отображением это его имя. Соответственно, если сам маркер не хранит в себе тип ноды, к которой он принадлежит, то мы должны перенести эту обязанность на наш OpenCard класс. Исходя из этого первым делом опишем 3 приватных метода, которые отвечают за определение типа ноды. Но эти методы нет смысла использовать напрямую. Не удобно оперировать булевыми значениями, когда работаешь с объектами другого уровня абстракции. Потому создадим простенький enum NodeType и приватный метод GetNodeType(), который инкапсулирует в себе всю логику, связанную с определением типа ноды. Когда все карты на столе, мы наконец-то можем перейти к конструктору. Подходов к инициализации может быть несколько. Но я решил максимально избавить OpenCard класс от лишних телодвижений. Он у нас должен отвечать доступен ли контент к отображению или нет. Потому мы просто попросим на вход списки нод 2-ух типов и центральную ноду.  Заметим, что поскольку, центральной ноде нужно проверить только условие, что все остальные ноды true нам достаточно неявно привести пришедшие в конструктор угловые и центральные ноды к типу INode. Какой самый удобный способ создавать объекты, которые не требуют прикрепления (как MonoBehaviour компоненты) к GameObject? — Правильно, ScriptableObject. Так же для удобства добавим MenuItem атрибут, который упросит создание новых открыток.  Финальным аккордом в нашей композиции будет являться проход по массиву добавленных (если они вообще есть) ScriptableObject и созданием из них открыток. После чего нам остается в методе Update просто проверить можем ли мы отображать контент или нет. Лично для меня выводы были такие: Знаете другое решение? — Пишите в комментариях. Разработчик игр Unity3D