- PVSM.RU - https://www.pvsm.ru -
Иной раз, когда читаешь технического задание и ставишь сроки по выполнению, недооцениваешь количество затраченного времени и усилий на решение той или иной задачи. Бывает так, что пункт, который оценивается по времени в неделю, выполняется в один час, а бывает и наоборот. Но эта статья не об этом. Это демонстрация эволюции решения задачи. От самого ее зарождения, до реализации.
Есть одна большая открытка размером с лист А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 часов.
Для наглядности записал видео [1] на котором запечатлен уже конечный результат работы алгоритма (если это можно таковым назвать).
Первое что пришло в голову это представить взаимодействия между маркерами от углового к центральному. В графическом виде это выглядит так:
Проблемы:
Не сумев уложить у себя в голове изложенное выше решение из-за множества краевых случаев и сложности восприятия, я изменил подход к выбору маркера от которого начинают распространяться зависимости.
Раздумывая над решением 3 пункта предыдущего подхода, пришла идея изменить тип маркера, от которых начинают меняться состояния других маркеров. Как основные были приняты боковые маркеры. При таком раскладе связи (зависимости) выглядят таким образом:
Отсюда сразу становится понятно, что связи от бокового к центральному лишние, потому что боковому маркеру не нужно ничего знать о центральном маркере, потому этот подход сразу трансформировался в конечный.
Конечное решение, когда боковой маркер знает об угловых, угловые “живут своей жизнью”, а центральный знает о состоянии всех маркеров.
Работать с представлением в виде открыток не очень удобно. Связи между сущностями выглядят не достаточно наглядно чтобы легко преобразовать это в код. Попытка интерпретации в виде бинарного дерева может внести некоторую двузначность. Но тут нарушено одно из свойств бинарного дерева, потому двузначность сразу отпадает. Из чего можно сделать вывод, что данное представление можно однозначно трактовать и использовать для графического представления решения задачи. Исходя из этих выводов, будем использовать нотацию графов, а именно:
Преимущества:
Создадим интерфейс, который содержит в себе элементы присущие каждой сущности (имя, состояние):
public interface INode
{
string Name { get; set; }
bool IsActive { get; set; }
}
Далее опишем сущность каждой ноды:
INode
:public class CornerNode : INode
{
public string Name { get; set; }
public bool IsActive { get; set; }
public Node(string name)
{
Name = name;
IsActive = true;
}
}
Почему IsActive = true
?
Из условия задачи контент угловых маркеров изначально доступен к распознаванию.
INode
, но добавляем еще поля LeftCornerNode
и RightCornerNode
. Тем самым боковая нода хранит в себе свое состояние и знает только о существовании боковых нод.public class SideNode : INode
{
public string Name { get; set; }
public bool IsActive { get; set; }
public CornerNode LeftCornerNode { get; }
public CornerNode RightCornerNode { get; }
public SideNode(string name, CornerNode leftNode, CornerNode rightNode)
{
Name = name;
IsActive = false;
LeftCornerNode = leftNode;
RightCornerNode = rightNode;
}
}
INode
. Добавляем поле типа List<INode>
.public class CentralNode : INode
{
public List<INode> NodesOnCard;
public string Name { get; set; }
public bool IsActive { get; set; }
public CentralNode(string name)
{
Name = name;
IsActive = false;
}
}
Теперь, когда у нас созданы все элементы открытки у нас сделаны (все виды маркеров), мы можем начать описывать сущность самой открытки. Я не привык начинать создание класса с конструктора. Я начинаю всегда с базовых методов, которые присущи конкретной сущности. Начнем с приватных полей и приватных методов.
private List<CornerNode> cornerNodes;
private List<SideNode> sideNodes;
private CentralNode centralNode;
С полями все достаточно просто. 2 списка с угловыми, боковыми нодами и одно поле центральной ноды.
Дальше нужно немного пояснить. Дело в том, что сам маркер имеет тип Trackable
и он понятия не имеет (и не должен иметь) о том, что он является частью какой-то там другой логики. Потому все что мы можем использовать для того чтобы управлять отображением это его имя. Соответственно, если сам маркер не хранит в себе тип ноды, к которой он принадлежит, то мы должны перенести эту обязанность на наш OpenCard
класс. Исходя из этого первым делом опишем 3 приватных метода, которые отвечают за определение типа ноды.
private bool IsCentralNode(string name)
{
return name == centralNode.Name;
}
private bool IsSideNode(string name)
{
foreach (var sideNode in sideNodes)
if (sideNode.Name == name)
return true;
return false;
}
private bool IsCornerNode(string name)
{
foreach (var sideNode in cornerNodes)
if (sideNode.Name == name)
return true;
return false;
}
Но эти методы нет смысла использовать напрямую. Не удобно оперировать булевыми значениями, когда работаешь с объектами другого уровня абстракции. Потому создадим простенький enum NodeType
и приватный метод GetNodeType()
, который инкапсулирует в себе всю логику, связанную с определением типа ноды.
public enum NodeType
{
CornerNode,
SideNode,
CentralNode
}
private NodeType? GetNodeType(string name)
{
if (IsCentralNode(name))
return NodeType.CentralNode;
if (IsSideNode(name))
return NodeType.SideNode;
if (IsCornerNode(name))
return NodeType.CornerNode;
return null;
}
IsExist
— метод, который возвращает булевое значение, говорящее о том, принадлежит ли наша марка открытке. Это вспомогательный метод, который сделан для того, чтобы в случае, если маркер не принадлежит никакой открытке мы могли отобразить контент на ней.public bool IsExist(string name)
{
foreach (var node in centralNode.NodesOnCard)
if (node.Name == name)
return true;
if (centralNode.Name == name)
return true;
return false;
}
CheckOnActiveAndChangeStatus
— метод (как можно понять из названия) в котором мы проверяем текущее состояние ноды и меняем его состояние.public bool CheckOnActiveAndChangeStatus(string name)
{
switch (GetNodeType(name))
{
case NodeType.CornerNode:
foreach (var node in cornerNodes)
if (node.Name == name)
return node.IsActive = true;
return false;
case NodeType.SideNode:
foreach (var node in sideNodes)
if (node.LeftCornerNode.IsActive && node.RightCornerNode.IsActive)
return true;
return false;
case NodeType.CentralNode:
foreach (var node in centralNode.NodesOnCard)
if (!node.IsActive)
return false;
return centralNode.IsActive = true;
default:
return false;
}
}
Когда все карты на столе, мы наконец-то можем перейти к конструктору. Подходов к инициализации может быть несколько. Но я решил максимально избавить OpenCard
класс от лишних телодвижений. Он у нас должен отвечать доступен ли контент к отображению или нет без необходимости дополнительно обрабатывать входные данные. Проверку этого стоит вынести в отдельный класс. Но, это не Open Source библиотека, чтобы об этом беспокоиться. Потому мы просто попросим на вход списки 2 типов и центральную ноду.
public OpenCard(List<CornerNode> listCornerNode, List<SideNode> listSideNode, CentralNode centralNode)
{
CornerNodes = listCornerNode;
SideNodes = listSideNode;
CentralNodes = centralNode;
CentralNodes.NodesOnCard = new List<INode>();
foreach (var node in CornerNodes)
CentralNodes.NodesOnCard.Add(node);
foreach (var node in SideNodes)
CentralNodes.NodesOnCard.Add(node);
}
Заметим, что поскольку, центральной ноде нужно проверить только условие, что все остальные ноды true
нам достаточно неявно привести пришедшие в конструктор угловые и центральные ноды к типу INode
.
Какой самый удобный способ создавать объекты, которые не требуют прикрепления (как MonoBehaviour
компоненты) к GameObject? — Правильно, ScriptableObject
. Так же для удобства добавим MenuItem
атрибут, который упросит создание новых открыток.
// todo добавить статью о ScriptableObject
[CreateAssetMenu(fileName = "Open Card", menuName = "New Open Card", order = 51)]
public class OpenCardScriptableObject : ScriptableObject
{
public string leftDownName;
public string rightDownName;
public string rightUpName;
public string leftUpName;
public string leftSideName;
public string rightSideName;
public string downSideName;
public string upSideName;
public string centralName;
}
Финальным аккордом в нашей композиции будет являться проход по массиву добавленных (если они вообще есть) ScriptableObject
и созданием из них открыток. После чего нам остается в методе Update
просто проверить можем ли мы отображать контент или нет.
public OpenCardScriptableObject[] openCards;
private List<OpenCard> _cardList;
void Awake()
{
if (openCards.Length != 0)
{
_cardList = new List<OpenCard>();
foreach (var card in openCards)
{
var leftDown = new CornerNode(card.leftDownName);
var rightDown = new CornerNode(card.rightDownName);
var rightUp = new CornerNode(card.rightUpName);
var leftUp = new CornerNode(card.leftUpName);
var leftSide = new SideNode(card.leftSideName, leftUp, leftDown);
var downSide = new SideNode(card.downSideName, leftDown, rightDown);
var rightSide = new SideNode(card.rightSideName, rightDown, rightUp);
var upSide = new SideNode(card.upSideName, rightUp, leftUp);
var central = new CentralNode(card.centralName);
var nodes = new List<CornerNode>() {leftDown, rightDown, rightUp, leftUp};
var sideNodes = new List<SideNode>() {leftSide, downSide, rightSide, upSide};
_cardList.Add(new OpenCard(nodes, sideNodes, central));
}
}
}
void Update()
{
var isNotPartCard = false;
foreach (var card in _cardList)
{
if (card.IsExist(trackableName))
isNotPartCard = true;
if (card.CheckOnActiveAndChangeStatus(trackableName))
imageTrackablesMap[trackableName].OnTrackSuccess(trackable);
if (!isNotPartCard)
imageTrackablesMap[trackableName].OnTrackSuccess(trackable);
}
}
Лично для меня выводы были такие:
Знаете другое решение? — Пишите в комментариях.
Автор: Алексей Козорезов
Источник [2]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/321652
Ссылки в тексте:
[1] видео: https://youtu.be/ZcXxVQG2Rgk
[2] Источник: https://habr.com/ru/post/454452/?utm_source=habrahabr&utm_medium=rss&utm_campaign=454452
Нажмите здесь для печати.