有时,当您阅读技术任务并设定实施期限时,会低估解决特定问题所花费的时间和精力。 碰巧一个点(按每周的时间估算)在一个小时完成,有时反之亦然。 但是这篇文章不是关于这个的。 这是对问题解决方案演变的演示。 从成立到实施。

使用条款
标记或标记-上传到AR引擎的图像,可由设备的相机(平板电脑或智能手机)识别,并且可以唯一地标识
找到-在相机视野中检测到的标记状态
丢失-从相机视图丢失时的标记状态
它可以显示-找到标记后,我们将显示标记所附加的内容
无法显示-找到标记时,不显示内容-标记所附加的内容-可以附加到标记上并因此将被显示的任何对象(3D模型,子画面,粒子系统等)在屏幕上是否找到标记
标记,标记,发现,丢失-提供识别功能的所有引擎固有的基本状态
它可以显示而不能显示-用于解决此问题的状态
一个例子:
- 下载应用程序=>可以识别所有下载的品牌
- 我们正在尝试识别=>标记的状态更改为“找到”
- 如果标记可以显示=>状态,则标记为“找到”,我们将显示附加到标记的模型
- 如果无法显示标记=>标记的状态为“找到”,但不显示附加的模型
- 品牌从相机的视野中消失了=>我们将状态更改为“丢失”
引言
有一张A4大小的大明信片。 它分为4个相等的部分(A5格式的一部分),在每个这些部分上有:
- 1个完整的角标记(1)
- 底边标记的一半(5)
- 顶部标记的一半(8)
- 四分之一中心马克(9)

如果您使用任何识别引擎(例如Vuforia),那么您可能知道不存在“识别质量”之类的东西。 标记被识别或未被识别。 因此,如果引擎“看到”品牌,则将状态更改为Find
并OnSuccess()
方法;如果引擎“丢失”,状态将更改为Lost
并OnLost()
方法。 因此,根据现有条件和输入数据,当拥有一部分卡(一半或四分之一)时出现了一种情况,便有可能识别该品牌。
事实是,根据技术任务,计划逐步解锁角色。 在这种情况下,可以逐步解锁,但是鉴于没有人试图识别该品牌的四分之一或一半。
有必要以程序代码的形式实现逻辑,以确保逐渐释放附加在标记上的内容。 从卡上元素的位置可以知道,标记1、2、3、4最初可用于显示。

如果已读取内容并将其显示在2个标记(例如2和3)上,则我们允许在标记6上显示内容。如果尚未读取标记1,则将关闭对标记5的访问。 进一步类推。 我们仅在读取相邻的角标记时才允许在侧标记处显示内容。

如果找到并找到了1到8个标记,则打开标记9上的内容以进行显示,每个标记具有2个状态-内容可用,不可用于显示,由public bool IsActive;
字段负责public bool IsActive;
马上很清楚,这应该是在状态之间进行转换的状态机,或者是“状态”模式的实现。
扰流板结果不是一个,不是另一个。 我不能说这是一个拐杖,因为该解决方案完全满足了本文开头的要求。 但是你可以和我争论。
在此基础上,我让您有机会自己考虑一下此任务的可能解决方案和实现。 我花了大约5个小时才意识到并确定了决定的图片。
为了清楚起见,我录制了一段视频,上面已经捕获了算法的最终结果(如果可以调用它的话)。
解决方法
1.从角标记到中心
首先想到的是呈现从角落到中心的标记之间的相互作用。 在图形形式中,它看起来像这样:

问题:
- 如何确定要更改状态的哪一侧标签? 左边还是右边的那个? 我们还强迫每个标记“知道”中心标记的存在。
- 有必要从类别中添加非显而易见的依赖项:侧面标记订阅了IsChangedEventCallback()角标记事件,必须对中央标记执行类似的操作。
- 如果我们将每种类型的标记都视为一个实体,则在这些实体的层次结构中,我们将从下至上转发状态更改命令。 这不是很好,因为我们将数字与角度标记紧密绑定在一起,在这种情况下,角度标记失去了缩放的能力。
由于存在许多极端情况和感知的复杂性,无法将上述解决方案付诸实践,我改变了选择依赖项开始扩散的标记的方法。
2.侧面知道中心和角落
考虑到前一种方法的第3段的解决方案,想法是改变标记的类型,其他标记的状态也从此开始改变。 作为主要的侧面标记。 在这种情况下,通信(依赖项)如下所示:

从这里可以立即清楚地看到,从横向到中心的连接是多余的,因为横向标记不需要了解任何有关中心标记的信息,因此该方法立即转变为最终方法。
3.中央的人知道每个人,侧面的人知道角落

最终的解决方案是,当侧面标记知道拐角时,这些拐角“过自己的生活”,而中心标记知道所有标记的状态。

使用明信片视图不是很方便。 实体之间的关系看起来不够清晰,无法轻松地将其转换为代码。 尝试以二叉树的形式进行解释可能会带来一些歧义。 但是这里二叉树的属性之一被破坏了,因此歧义立即消失了。 从中我们可以得出结论,可以清楚地解释此表示形式,并以图形方式表示问题的解决方案。 基于这些结论,我们将使用图形符号,即:
- 角度标记-角度节点(级别3)
- 侧面标记-侧面节点(级别2)
- 中心标记-中心节点(级别1)
优点:
- 标记之间的依赖性是显而易见的。
- 每个级别可以以3个实体的形式表示,每个实体都由基本部分组成,但是每个级别都具有其固有的附加功能
- 要扩展,您只需要添加具有自己特征的新型节点
- 这种解决方案很容易以OO(面向对象)风格来想象
实作
基础实体
让我们创建一个包含每个实体(名称,状态)固有元素的接口:
public interface INode { string Name { get; set; } bool IsActive { get; set; } }
接下来,我们描述每个节点的本质:
- CornerNode-一个角度节点。 只需实现
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
?
- SideNode-侧节点。 我们实现了
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; } }
- CenterNode是中央节点。 和前面的一样,我们实现了
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
类。 基于此,我们首先描述三种负责确定节点类型的私有方法。
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
类不必要的手势。 它应该向我们回答内容是否可以显示。 因此,我们只要求输入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
构造函数中INode
成角度的中央节点隐式INode
为INode
类型就足够了。
初始化
创建不需要附加到GameObject的对象(例如MonoBehaviour
组件)的最便捷方法是什么? -对, ScriptableObject
。 另外,为方便起见,添加MenuItem
属性,该属性将简化新卡的创建。
[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); } }
结论
对我个人而言,结论如下:
- 在尝试解决问题时,您需要尝试将其元素分解为原子部分。 此外,考虑这些原子部分之间相互作用的所有可能选择,您需要从可能会产生更多连接的对象开始。 换句话说,它可以表述为:努力开始使用可能不太可靠的元素来解决问题
- 如果可能,您应尝试以其他形式显示源数据。 就我而言,图形表示对我有很大帮助。
- 每个实体彼此之间的间隔可能来自于该实体之间的连接数。
- 可以通过OO样式表示许多更习惯于通过编写算法来解决的应用任务
- 具有环依赖性的解决方案是一个糟糕的解决方案
- 如果很难将所有物体之间的连接保持在脑海中,这是一个错误的决定
- 如果您不能记住对象交互的逻辑,这是一个错误的决定
- 拐杖并不总是一个错误的决定
您知道其他解决方案吗? -在评论中写。