في بعض الأحيان ، عندما تقرأ مهمة فنية وتحدد المواعيد النهائية للتنفيذ ، قلل من مقدار الوقت والجهد المبذول في حل مشكلة معينة. يحدث أن نقطة ، والتي يتم تقديرها حسب الوقت في الأسبوع ، يتم تنفيذها في ساعة واحدة ، وأحيانًا العكس. لكن هذا المقال ليس عن ذلك. هذا دليل على تطور حل للمشكلة. من بدايتها إلى التنفيذ.

الشروط المستخدمة
وضع علامة أو علامة - صورة تم تحميلها على محرك AR ، والتي يتم التعرف عليها بواسطة كاميرا الجهاز (الكمبيوتر اللوحي أو الهاتف الذكي) ويمكن التعرف عليها بشكل فريد
تم العثور - علامة الحالة عندما تم اكتشافها في مجال رؤية الكاميرا
حالة علامة المفقودة عند فقدها من عرض الكاميرا
يمكن عرضها - عند العثور على العلامة ، نعرض المحتوى المرفق بالعلامة
لا يمكن عرضها - عندما نعثر على العلامة ، لا نعرض المحتوى - المحتوى المرفق بالعلامة - أي كائن (نموذج ثلاثي الأبعاد ، شبح ، نظام جسيم ، إلخ) يمكن إرفاقه بالعلامة ، وبالتالي ، سيتم عرضه على الشاشة إذا تم العثور على علامة
علامة ، علامة ، وجدت ، مفقودة - الحالات الأساسية المتأصلة في جميع المحركات التي توفر وظائف التعرف
يمكن عرضها ولا يمكن عرضها - الحالة المستخدمة لحل هذه المشكلة
مثال:
- تنزيل التطبيق => يمكن التعرف على جميع العلامات التجارية التي تم تنزيلها
- نحن نحاول التعرف على => حالة العلامة تتغير إلى "موجود"
- إذا كان من الممكن عرض العلامة => الحالة ، فسيتم "العثور" على العلامة ونعرض النموذج المرفق بالعلامة
- إذا تعذر عرض العلامة => يتم العثور على حالة العلامة ، ولكن لا يتم عرض النموذج المرفق
- اختفت العلامة التجارية من مجال رؤية الكاميرا => نغير الحالة إلى "ضائعة"
مقدمة
هناك بطاقة بريدية كبيرة بحجم ورقة A4. وهي مقسمة إلى 4 أجزاء متساوية (شكل جزء واحد A5) ، يوجد في كل جزء من هذه الأجزاء:
- علامة زاوية واحدة كاملة (1)
- نصف علامة الجانب السفلي (5)
- نصف علامة الجانب العلوي (8)
- ربع مركز مارك (9)

إذا كنت تعمل مع أي محركات التعرف ، على سبيل المثال ، Vuforia ، فأنت على الأرجح تعلم أنه لا يوجد شيء مثل "جودة الاعتراف". العلامة معترف بها أو غير معترف بها. وفقًا لذلك ، إذا كان المحرك "يرى" العلامة التجارية ، فإنه يغير الحالة إلى " Find
ويتم OnSuccess()
طريقة OnSuccess()
، وإذا "فقدها" ، OnLost()
الحالة إلى Lost
OnLost()
طريقة OnLost()
. وفقًا لذلك ، من الظروف الحالية وبيانات الإدخال ، نشأ موقف عند امتلاك جزء من البطاقة (نصف أو ربع) كان من الممكن التعرف على العلامة التجارية.
الشيء هو أنه وفقا للمهمة الفنية ، تم التخطيط لفتح تدريجي للشخصيات. في هذه الحالة ، يمكن فتح القفل التدريجي ، ولكن بالنظر إلى عدم وجود أشخاص يحاولون التعرف على ربع العلامة التجارية أو نصفها.
من الضروري تطبيق المنطق في شكل رمز البرنامج ، والذي يضمن فتح تدريجي للمحتوى المتصل بالعلامات. من موقع العناصر الموجودة على البطاقة ، من المعروف أن العلامات 1 و 2 و 3 و 4 متاحة للعرض في البداية.

إذا تم قراءة المحتوى وعرضه على علامتين ، على سبيل المثال ، 2 و 3 ، فإننا نسمح بعرض المحتوى على العلامة 6. إذا لم يتم قراءة العلامة 1 بعد ، فسيتم إغلاق الوصول إلى العلامة 5. مزيد من القياس. نحن نمنح إذنًا لعرض المحتوى في العلامات الجانبية فقط عندما نقرأ علامات الزاوية المجاورة.

إذا توفرت علامات من 1 إلى 8 وتم العثور عليها ، فافتح المحتوى الموجود في العلامة 9. للعرض ، ولكل علامة public bool IsActive;
- المحتوى متاح وغير متاح للعرض ، public bool IsActive;
حقل public bool IsActive;
مسؤولاً عنه public bool IsActive;
من الواضح على الفور أن هذا يجب أن يكون إما جهاز دولة مع انتقال بين الدول ، أو تنفيذ نمط "الدولة".
المفسدوكانت النتيجة ليست واحدة ، وليس أخرى. لا يمكنني القول أن هذا عكاز لأن الحل يفي بالكامل بالمتطلبات في بداية المقال. ولكن يمكنك أن تجادل معي.
في هذا ، أعطيك الفرصة للتفكير لنفسك قليلاً حول الحلول والتطبيقات الممكنة لهذه المهمة. استغرق الأمر مني حوالي 5 ساعات لإدراك وإصلاح صورة القرار في رأسي.
من أجل الوضوح ، قمت بتسجيل مقطع فيديو يتم فيه تسجيل النتيجة النهائية للخوارزمية (إذا كنت تستطيع تسمية ذلك).
نهج الحل
1. من علامات الزاوية إلى المركز
أول ما يتبادر إلى الذهن هو تقديم التفاعلات بين العلامات من الزاوية إلى المركز. في شكل رسومية ، يبدو كما يلي:

المشاكل:
- كيفية تحديد التسمية الجانبية لتغيير الدولة؟ واحد على اليسار أو على اليمين؟ نحن أيضًا نجبر كل علامة على "معرفة" وجود علامة مركزية.
- من الضروري إضافة التبعيات غير الواضحة من الفئة: تشترك العلامة الجانبية في حدث علامة الزاوية IsChangedEventCallback () ، ويجب اتخاذ إجراءات مماثلة للعلامة المركزية.
- إذا نظرنا إلى كل نوع من العلامات ككيان ، في التسلسل الهرمي لهذه الكيانات سنقوم بإعادة توجيه الأمر state-change من الأسفل إلى الأعلى. هذا ليس جيدًا جدًا ، لأننا نربط أنفسنا بإحكام بالعدد ، في هذه الحالة ، علامات الزاوي ، ونفقد القدرة على القياس.
غير قادر على وضع الحل أعلاه في رأسي بسبب العديد من حالات الحافة وتعقيد الإدراك ، لقد غيرت النهج لاختيار العلامة التي تبدأ التبعيات في الانتشار.
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
واجهة 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
بنا. بناءً على ذلك ، نصف أولاً 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
من الإيماءات غير الضرورية قدر الإمكان. يجب أن يجيب معنا ما إذا كان المحتوى متاحًا للعرض أم لا. لذلك ، نحن ببساطة نطلب قوائم إدخال العقد من نوعين وعقدة مركزية.
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
.
التهيئة
ما هي الطريقة الأكثر ملاءمة لإنشاء كائنات لا تحتاج إلى إرفاقها (مثل مكونات MonoBehaviour
) إلى GameObject؟ - الحق ، 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; }
سيكون الوتر النهائي في تكويننا عبارة عن مرور عبر مجموعة ScriptableObjects المضافة (إن وجدت) وإنشاء بطاقات بريدية منها. بعد ذلك ، يبقى لنا في طريقة 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
- الحل الذي يحتوي على تبعيات الحلقة هو حل سيء
- إذا كان من الصعب الاحتفاظ بجميع الاتصالات بين الكائنات الموجودة في رأسك ، فهذا قرار سيء
- إذا كنت لا تستطيع أن تضع في الاعتبار منطق تفاعل الكائنات - فهذا قرار سيء
- العكازات الخاصة بك ليست دائما قرارا سيئا
هل تعرف حل آخر؟ - اكتب في التعليقات.