
في gamedev ، تحتاج غالبًا إلى ربط شيء ما في منزل عشوائي: لدى Unity Random خاص بها لهذا ، ويوجد System.Random في موازاة ذلك. ذات مرة في أحد المشاريع ، بدا أن كليهما يمكن أن يعملا بشكل مختلف (على الرغم من أنه ينبغي أن يكون لهما توزيع موحد).
ثم لم يخوضوا في التفاصيل - لقد كان كافيا أن الانتقال إلى System.Random حل جميع المشاكل. الآن قررنا أن نفهم بمزيد من التفصيل وإجراء القليل من البحث: كيف RNGs "منحازة" أو يمكن التنبؤ بها ، وأي واحد للاختيار. علاوة على ذلك ، كثيراً ما سمعت آراء متضاربة حول "صدقهم" - دعنا نحاول معرفة مدى ارتباط النتائج الحقيقية بالنتائج المذكورة.
برنامج تعليمي موجز أو RNG هو في الواقع PRNG
إذا كنت على دراية بالفعل بمولدات الأرقام العشوائية ، فيمكنك المتابعة فورًا إلى قسم "الاختبار".الأرقام العشوائية (MF) هي سلسلة من الأرقام التي تم إنشاؤها باستخدام بعض العمليات العشوائية (الفوضوية) ، وهي مصدر للأنتروبيا. وهذا هو ، هذا تسلسل ، عناصره ليست مرتبطة بأي قانون رياضي - ليس لديهم علاقة سببية.
ما يخلق المدى المتوسط يسمى مولد الأرقام العشوائية (RNG). يبدو أن كل شيء أساسي ، ولكن إذا انتقلنا من النظرية إلى التطبيق ، فإن تنفيذ خوارزمية البرنامج فعليًا لإنشاء مثل هذا التسلسل ليس بهذه البساطة.
يكمن السبب في غياب العشوائية في الإلكترونيات الاستهلاكية الحديثة. بدون ذلك ، تتوقف الأرقام العشوائية عن أن يكون عشوائيًا ، ويتحول مولدها إلى وظيفة عادية للحجج المحددة عمداً. بالنسبة لعدد من التخصصات في مجال تكنولوجيا المعلومات ، هذه مشكلة خطيرة (على سبيل المثال ، للتشفير) ، أما بالنسبة للبقية فهناك حل مقبول تمامًا.
نحتاج إلى كتابة خوارزمية ستعود ، حتى لو لم تكن أرقامًا عشوائية حقًا ، ولكن أقرب ما يمكن إليها - ما يسمى بالأرقام العشوائية المزيفة (PSN). تسمى الخوارزمية في هذه الحالة مولد الأرقام العشوائية الزائفة (PRNG).
هناك العديد من الخيارات لإنشاء PRNG ، ولكن بالنسبة للجميع ، فإن الأمور التالية ستكون ذات صلة:
- الحاجة إلى ما قبل التهيئة.
PRNG يخلو من مصدر الانتروبيا ، وبالتالي ، قبل الاستخدام ، فإنه يحتاج إلى الإشارة إلى الحالة الأولية. يتم تحديده كرقم (أو متجه) ويسمى بذرة (بذرة ، بذرة عشوائية). في كثير من الأحيان ، يتم استخدام عداد ساعة المعالج أو المكافئ الرقمي لوقت النظام كبذرة. - استنساخ التسلسل.
تعتبر PRNG حتمية تمامًا ، لذا فإن البذرة المحددة أثناء التهيئة تحدد بشكل فريد تسلسل الأرقام بالكامل في المستقبل. وهذا يعني أن ورقة استراتيجية الحد من الفقر واحدة ، التي تمت تهيئتها بنفس بذرة (في أوقات مختلفة ، في برامج مختلفة ، على أجهزة مختلفة) سوف تولد نفس التسلسل.
تحتاج أيضًا إلى معرفة توزيع الاحتمالات الذي يميز PRNG - ما هي الأرقام التي سينشئها وما هو الاحتمال. في معظم الأحيان ، يكون هذا إما توزيع عادي أو توزيع موحد.
التوزيع الطبيعي (اليسار) والتوزيع الموحد (يمين)دعنا نقول لدينا النرد صادق مع 24 وجوه. إذا قمت بإسقاطها ، فإن احتمال سقوط وحدة سيكون 1/24 (وكذلك احتمال سقوط أي عدد آخر). إذا قمت بالكثير من الرميات وسجلت النتائج ، ستلاحظ أن كل الوجوه تسقط بنفس التردد. في الواقع ، يمكن اعتبار هذا النرد RNG مع توزيع موحد.
وإذا رميت على الفور 10 عظام وحساب المبلغ الإجمالي للنقاط؟ هل سيتم الحفاظ على التوحيد لها؟ لا. في أغلب الأحيان ، سيكون المبلغ قريبًا من 125 نقطة ، أي إلى قيمة متوسطة. ونتيجة لذلك - حتى قبل القيام برمية ، يمكنك تقدير النتيجة المستقبلية تقريبًا.
والسبب هو أنه للحصول على متوسط كمية النقاط ، يوجد أكبر عدد من المجموعات. أبعد من ذلك ، وعدد أقل من مجموعات - وبالتالي ، فرصة أقل للخسارة. إذا قمت بتصور هذه البيانات ، فستشبه عن بعد شكل الجرس. لذلك ، مع بعض الامتداد ، يمكن استدعاء نظام من 10 عظام RNG مع توزيع طبيعي.
مثال آخر ، فقط بالفعل في الطائرة - اطلاق النار الهدف. سيكون مطلق النار هو RNG الذي يقوم بإنشاء زوج من الأرقام (س ، ص) ، والذي يتم عرضه على الرسم البياني.

توافق على أن الخيار على اليسار أقرب إلى الحياة الحقيقية - وهذا هو RNG مع التوزيع الطبيعي. ولكن إذا كنت بحاجة إلى تشتيت النجوم في سماء مظلمة ، فإن الخيار الصحيح الذي يتم الحصول عليه بمساعدة RNG بتوزيع موحد ، هو الأفضل. بشكل عام ، اختر مولدًا بناءً على المهمة.
الآن دعونا نتحدث عن إنتروبيا تسلسل PSP. على سبيل المثال ، هناك تسلسل يبدأ مثل هذا:
89 ، 93 ، 33 ، 32 ، 82 ، 21 ، 4 ، 42 ، 11 ، 8 ، 60 ، 95 ، 53 ، 30 ، 42 ، 19 ، 34 ، 35 ، 62 ، 23 ، 44 ، 38 ، 74 ، 36 ، 52 ، 18 ، 58 ، 79 ، 65 ، 45 ، 99 ، 90 ، 82 ، 20 ، 41 ، 13 ، 88 ، 76 ، 82 ، 24 ، 5 ، 54 ، 72 ، 19 ، 80 ، 2 ، 74 ، 36 ، 71 ، 9 ، ...
كيف عشوائي هذه الأرقام للوهلة الأولى؟ لنبدأ بالتحقق من التوزيع.

يبدو أقرب إلى الزي الرسمي ، ولكن إذا قرأت تسلسل رقمين وتفسيرهما على أنهما إحداثيات على المستوى ، فستحصل على هذا:

أنماط واضحة للعيان. ونظرًا لأن البيانات الموجودة في التسلسل يتم ترتيبها بطريقة معينة (أي أنها تحتوي على إنتروبيا منخفضة) ، فقد يؤدي ذلك إلى "التحيز". كحد أدنى ، فإن PRNG ليس مناسبًا جدًا لإنشاء إحداثيات على متن طائرة.
تسلسل آخر:
42 ، 72 ، 17 ، 0 ، 30 ، 0 ، 15 ، 9 ، 47 ، 19 ، 35 ، 86 ، 40 ، 54 ، 97 ، 42 ، 69 ، 19 ، 20 ، 88 ، 4 ، 3 ، 67 ، 27 ، 42 ، 56 ، 17 ، 14 ، 20 ، 40 ، 80 ، 97 ، 1 ، 31 ، 69 ، 13 ، 88 ، 89 ، 76 ، 9 ، 4 ، 85 ، 17 ، 88 ، 70 ، 10 ، 42 ، 98 ، 96 ، 53 ، ...
يبدو أن كل شيء على ما يرام هنا حتى على متن الطائرة:

دعونا نرى في المجلد (نقرأ ثلاثة أرقام لكل منهما):

ومرة أخرى الأنماط. بناء التصور في أربعة أبعاد لن ينجح. لكن الأنماط يمكن أن توجد على هذا البعد وعلى الأحجام الكبيرة.
في نفس التشفير ، حيث يتم فرض المتطلبات الأكثر صرامة على PRNG ، مثل هذا الموقف غير مقبول بشكل قاطع. لذلك ، ولتقييم جودتها ، تم تطوير خوارزميات خاصة ، والتي لن نتطرق إليها الآن. الموضوع واسع ويستند إلى مقالة منفصلة.
تجريب
إذا لم نكن نعرف شيئًا مؤكدًا ، فكيف نتعامل معه؟ هل يستحق الأمر عبور الطريق إذا كنت لا تعرف إشارة المرور التي تسمح بها؟ النتائج قد تكون مختلفة.
الشيء نفسه ينطبق على العشوائية سيئة السمعة في الوحدة. حسنًا ، إذا كانت الوثائق تكشف التفاصيل الضرورية ، إلا أن القصة المذكورة في بداية المقالة حدثت لمجرد عدم وجود التفاصيل المطلوبة.
ودون معرفة كيفية عمل الأداة ، لا يمكنك تطبيقها بشكل صحيح. بشكل عام ، حان الوقت لفحص وإجراء تجربة للتأكد أخيرًا على الأقل من حساب التوزيع.
كان الحل بسيطًا وفعالًا - لجمع الإحصاءات ، والحصول على بيانات موضوعية وإلقاء نظرة على النتائج.
موضوع البحث
هناك عدة طرق لتوليد أرقام عشوائية في الوحدة - لقد قمنا باختبار خمسة.
- System.Random.Next (). يولد أعداد صحيحة في نطاق معين من القيم.
- System.Random.NextDouble (). يولِّد أرقامًا مزدوجة الدقة (مزدوجة) في النطاق من [0؛ 1).
- UnityEngine.Random.Range (). يولد أرقام دقيقة واحدة (تعويم) في نطاق معين من القيم.
- UnityEngine.Random.value. يولِّد أرقامًا فردية دقيقة (عائمة) في النطاق من [0؛ 1).
- Unity.Mathematics.Random.NextFloat (). جزء من مكتبة Unity.Mathematics الجديدة. يولد أرقام دقيقة واحدة (تعويم) في نطاق معين من القيم.
في كل مكان تقريبًا في الوثائق ، تمت الإشارة إلى توزيع موحد ، باستثناء UnityEngine.Random.value (حيث لم يتم تحديد التوزيع ، ولكن على غرار UnityEngine.Random.Range () كان من المتوقع أيضًا أن يكون موحدًا) و Unity.Mathematics.Random.NextFloat () (حيث الأساس هو خوارزمية xorshift ، مما يعني أنك بحاجة مرة أخرى إلى انتظار توزيع موحد).
بشكل افتراضي ، تم التقاط تلك المتوقعة في الوثائق للنتائج المتوقعة.
تقنية
لقد كتبنا تطبيقًا صغيرًا أنشأ سلسلة من الأرقام العشوائية في كل طريقة من الطرق المقدمة وحفظ النتائج للمعالجة الإضافية.
طول كل تسلسل 100000 رقم.
نطاق الأرقام العشوائية هو [0 ، 100).
تم جمع البيانات من عدة منصات مستهدفة:
- نوافذ
- الوحدة v2018.3.14f1 ، وضع المحرر ، Mono ، .NET Standard 2.0 - ماك
- الوحدة v2018.3.14f1 ، وضع المحرر ، Mono ، .NET Standard 2.0
- الوحدة v5.6.4p4 ، وضع المحرر ، مونو ، .NET Standard 2.0 - الروبوت
- الوحدة v2018.3.14f1 ، التجميع على الجهاز ، Mono ، .NET Standard 2.0 - دائرة الرقابة الداخلية
- الوحدة v2018.3.14f1 ، بناء إلى جهاز ، il2cpp ، .NET Standard 2.0
تطبيق
لدينا عدة طرق مختلفة لتوليد أرقام عشوائية. لكل منهم سنكتب فئة غلاف منفصلة ، والتي ينبغي أن توفر:
- القدرة على ضبط نطاق القيم [دقيقة / كحد أقصى). سيتم تعيينه من خلال المنشئ.
- طريقة العودة المدى المتوسط. سنختار تعويم كنوع ، كنوع أكثر عمومية.
- اسم طريقة التوليد لتمييز النتائج. للراحة ، سنقوم بإرجاع اسم الفصل الكامل + اسم الطريقة المستخدمة لإنشاء المدى المتوسط كقيمة.
أولاً ، قم بتعريف التجريد ، والذي سيمثله واجهة IRandomGenerator:
namespace RandomDistribution { public interface IRandomGenerator { string Name { get; } float Generate(); } }
تنفيذ System.Random.Next ()
تتيح لك هذه الطريقة تحديد نطاق من القيم ، لكنها تُرجع أعدادًا صحيحة ، وتحتاج إلى تعويم. يمكنك ببساطة تفسير الأعداد الصحيحة كتعويم ، أو يمكنك توسيع نطاق القيم بعدة أوامر من حيث الحجم ، مع التعويض عنها في كل مرة يتم فيها إنشاء المدى المتوسط. سوف تتحول إلى شيء مثل النقطة الثابتة بالدقة المحددة. سنستخدم هذا الخيار ، لأنه أقرب إلى قيمة التعويم الحقيقية.
using System; namespace RandomDistribution { public class SystemIntegerRandomGenerator : IRandomGenerator { private const int DefaultFactor = 100000; private readonly Random _generator = new Random(); private readonly int _min; private readonly int _max; private readonly int _factor; public string Name => "System.Random.Next()"; public SystemIntegerRandomGenerator(float min, float max, int factor = DefaultFactor) { _min = (int)min * factor; _max = (int)max * factor; _factor = factor; } public float Generate() => (float)_generator.Next(_min, _max) / _factor; } }
تنفيذ System.Random.NextDouble ()
هنا مجموعة من القيم الثابتة [0؛ 1). لإسقاطها على الحساب المحدد في المُنشئ ، نستخدم حسابًا بسيطًا: X * (الحد الأقصى - دقيقة) + دقيقة.
using System; namespace RandomDistribution { public class SystemDoubleRandomGenerator : IRandomGenerator { private readonly Random _generator = new Random(); private readonly double _factor; private readonly float _min; public string Name => "System.Random.NextDouble()"; public SystemDoubleRandomGenerator(float min, float max) { _factor = max - min; _min = min; } public float Generate() => (float)(_generator.NextDouble() * _factor) + _min; } }
تطبيق UnityEngine.Random.Range ()
تسمح لك هذه الطريقة للفئة الثابتة UnityEngine.Random بتحديد نطاق من القيم وإرجاع المدى المتوسط للنوع float. لا التحولات الإضافية ضرورية.
using UnityEngine; namespace RandomDistribution { public class UnityRandomRangeGenerator : IRandomGenerator { private readonly float _min; private readonly float _max; public string Name => "UnityEngine.Random.Range()"; public UnityRandomRangeGenerator(float min, float max) { _min = min; _max = max; } public float Generate() => Random.Range(_min, _max); } }
تطبيق UnityEngine.Random.value
تقوم خاصية القيمة للفئة الثابتة UnityEngine.Random بإرجاع المدى المتوسط للنوع العائم من نطاق ثابت من القيم [0؛ 1). نحن نعرضه على نطاق معين بنفس الطريقة عند تطبيق System.Random.NextDouble ().
using UnityEngine; namespace RandomDistribution { public class UnityRandomValueGenerator : IRandomGenerator { private readonly float _factor; private readonly float _min; public string Name => "UnityEngine.Random.value"; public UnityRandomValueGenerator(float min, float max) { _factor = max - min; _min = min; } public float Generate() => (float)(Random.value * _factor) + _min; } }
تطبيق Unity.Mathematics.Random.NextFloat ()
الأسلوب NextFloat () للفئة Unity.Mathematics.Random بإرجاع المدى المتوسط من النوع float ويتيح لك تحديد نطاق من القيم. الفرق الوحيد هو أنه يجب تهيئة كل مثيل من Unity.Mathematics.Random مع بعض البذور - وبهذه الطريقة سنتجنب توليد تسلسلات متكررة.
using Unity.Mathematics; namespace RandomDistribution { public class UnityMathematicsRandomValueGenerator : IRandomGenerator { private Random _generator; private readonly float _min; private readonly float _max; public string Name => "Unity.Mathematics.Random.NextFloat()"; public UnityMathematicsRandomValueGenerator(float min, float max) { _min = min; _max = max; _generator = new Random(); _generator.InitState(unchecked((uint)System.DateTime.Now.Ticks)); } public float Generate() => _generator.NextFloat(_min, _max); } }
تنفيذ MainController
العديد من تطبيقات IRandomGenerator جاهزة. بعد ذلك ، تحتاج إلى إنشاء تسلسل وحفظ مجموعة البيانات الناتجة للمعالجة. للقيام بذلك ، قم بإنشاء مشهد في الوحدة ونص رئيسي MainController ، والذي سيؤدي جميع الأعمال الضرورية ويكون مسئولًا في وقت واحد عن التفاعل مع واجهة المستخدم.
لقد حددنا حجم مجموعة البيانات ونطاق قيم المدى المتوسط ، ونحصل أيضًا على طريقة تُرجع مجموعة من المولدات المضبوطة والمجهزة للاستخدام.
namespace RandomDistribution { public class MainController : MonoBehaviour { private const int DefaultDatasetSize = 100000; public float MinValue = 0f; public float MaxValue = 100f; ... private IRandomGenerator[] CreateRandomGenerators() { return new IRandomGenerator[] { new SystemIntegerRandomGenerator(MinValue, MaxValue), new SystemDoubleRandomGenerator(MinValue, MaxValue), new UnityRandomRangeGenerator(MinValue, MaxValue), new UnityRandomValueGenerator(MinValue, MaxValue), new UnityMathematicsRandomValueGenerator(MinValue, MaxValue) }; } ... } }
والآن نحن نشكل مجموعة بيانات. في هذه الحالة ، سيتم دمج توليد البيانات مع تسجيل النتائج في دفق نصي (بتنسيق CSV). لتخزين قيم كل IRandomGenerator ، يتم تخصيص عمود منفصل ، ويحتوي السطر الأول على اسم المولد.
namespace RandomDistribution { public class MainController : MonoBehaviour { ... private void GenerateCsvDataSet(TextWriter writer, int dataSetSize, params IRandomGenerator[] generators) { const char separator = ','; int lastIdx = generators.Length - 1;
يبقى استدعاء الأسلوب GenerateCsvDataSet وحفظ النتيجة في ملف ، أو نقل البيانات على الفور من الشبكة من الجهاز النهائي إلى خادم الاستقبال.
namespace RandomDistribution { public class MainController : MonoBehaviour { ... public void GenerateCsvDataSet(string path, int dataSetSize, params IRandomGenerator[] generators) { using (var writer = File.CreateText(path)) { GenerateCsvDataSet(writer, dataSetSize, generators); } } public string GenerateCsvDataSet(int dataSetSize, params IRandomGenerator[] generators) { using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture)) { GenerateCsvDataSet(writer, dataSetSize, generators); return writer.ToString(); } } ... } }
مصادر المشروع على
GitLab .
النتائج
لم تحدث معجزة. ما توقعوه ، حصلوا عليه - في جميع الحالات توزيعا موحدا دون إشارة إلى المؤامرات. لا أرى نقطة تطبيق رسومات منفصلة على الأنظمة الأساسية - فهي تظهر جميعها تقريبًا نفس النتائج.
الحقيقة هي:

تصور تسلسل على متن طائرة من جميع أساليب الجيل الخمسة:

والتصور في 3D. سأترك فقط نتيجة System.Random.Next () ، حتى لا ينتج عنها مجموعة من المحتوى نفسه.

القصة التي روى في المقدمة حول التوزيع الطبيعي لـ UnityEngine.Random لم تتكرر: إما أنها كانت خاطئة في البداية ، أو تغير شيء ما في المحرك منذ ذلك الحين. ولكن الآن نحن متأكدون.