
Në zhvillimin e lojërave shpesh nevojitet të lidhet diçka me rastësinë: për këtë, Unity ka Random-in e tij, ndërsa paralelisht ekziston edhe System.Random. Disa kohë më parë, në një nga projektet, u krijua përshtypja se të dyja mund të funksiononin ndryshe (ndonëse duhet të kishin shpërndarje të barabartë).
AtĂ«herĂ« nuk u thelluam nĂ« detaje â mjaftonte qĂ« kalimi te System.Random zgjidhi tĂ« gjitha problemet. Tani vendosĂ«m tĂ« shpĂ«rfaqim mĂ« parĂ« dhe tĂ« kryejmĂ« njĂ« kĂ«rkim tĂ« vogĂ«l: sa "tĂ« preferuar" ose tĂ« parashikueshĂ«m janĂ« gjeneratorĂ«t e numrave tĂ« rastĂ«sishĂ«m, dhe cilin duhet tĂ« zgjedhim. VeçanĂ«risht, kam dĂ«gjuar disa mendime kontradiktore mbi "ndershmĂ«rinĂ«" e tyre â do tĂ« provojmĂ« tĂ« kuptojmĂ« se si rezultatet reale pĂ«rputhen me ato tĂ« shpallura.
Një e shkurtër mbi gjeneratorët e numrave të rastësishëm ose çfarë janë në të vërtetë GSH.
Nëse tashmë jeni të njohur me gjeneratorët e numrave të rastësishëm, mund të kaloni menjëherë në seksionin "Testimi".
Numrat e rastĂ«sishĂ«m (NR) janĂ« njĂ« seri numrash tĂ« gjeneruar pĂ«rmes njĂ« procesi tĂ« caktuar rastĂ«sor (kaotik), njĂ« burimi entropie. Kjo do tĂ« thotĂ« se kjo Ă«shtĂ« njĂ« seri, elementet e sĂ« cilĂ«s nuk janĂ« tĂ« lidhur mes tyre nga ndonjĂ« ligj matematikor â ato nuk kanĂ« lidhje shkaku-pasojĂ«.
Ajo që krijon NR-në quhet gjenerator i numrave të rastësishëm (GNR). Duket e thjeshtë, por kur kalon nga teoria në praktikë, realizimi i një algoritmi programor për gjenerimin e një të tillë nuk është aq i lehtë.
Arsyeja ndodhet në mungesën e asaj kaoticiteti në elektronikën moderne të konsumit. Pa të, numrat e rastësishëm ndalojnë së qeni të tillë, dhe gjeneratori i tyre kthehet në një funksion të zakonshëm të argumenteve të njohura. Për një sërë specialitetesh në fushën e IT-së, kjo është një problem serioz (për shembull, për kriptografinë), por për të tjerët ka një zgjidhje të pranueshme.
Duhet tĂ« shkruhet njĂ« algoritĂ«m qĂ« do tĂ« kthejĂ«, ndoshta jo numra tĂ« vĂ«rtetĂ« tĂ« rastĂ«sishĂ«m, por sa mĂ« afĂ«r tyre â tĂ« ashtuquajturit, numra pseudorastĂ«sorĂ« (PR). Algoritmi nĂ« kĂ«tĂ« rast quhet gjenerator i numrave pseudorastĂ«sorĂ« (GPR).
Ka disa opcione për krijimin e GPR, por për të gjitha do të jetë relevante kjo e dhënë:
- Nevoja për inicializim të përparshëm.
GPCH është pa një burim entropie, kështu që para përdorimit duhet t'i jepet një gjendje fillestare. Kjo caktim jepet si një numër (ose si një vektor) dhe quhet farë (seed, random seed). Shpesh, si farë përdoret numri i cikleve të procesorit ose ekuivalenti numerik i kohës sistemike.
- Riprodhueshmëria e sekuencës.
GPCH është plotësisht determinuese, kështu që farë e caktuar gjatë inicializimit përcakton në mënyrë unike tërë sekuencën e numrave të ardhshëm. Kjo do të thotë se një GPCH i vetëm, i inicializuar me të njëjtën farë (në kohë të ndryshme, në programe të ndryshme, në pajisje të ndryshme) do të gjenerojë të njëjtën sekuencë.
Duhet gjithashtu tĂ« dihet shpĂ«rndarja e probabiliteteve qĂ« karakterizon GPCH â cilat numra do tĂ« gjenerojĂ« ai dhe me çfarĂ« probabiliteti. MĂ« shpesh kjo Ă«shtĂ« ose shpĂ«rndarja normale (normal distribution), ose shpĂ«rndarja e barabartĂ« (uniform distribution).

Shpërndarja normale (majtas) dhe shpërndarja e barabartë (djathtas)
Supozoni se kemi një zarf të ndershëm me 24 faqe. Nëse e hedhim, probabiliteti i rënies së numrit një do të jetë 1/24 (ashtu si probabiliteti i rënies së ndonjë numri tjetër). Nëse bëjmë shumë hedhje dhe regjistrojmë rezultatet, do të vëmë re se të gjitha faqet bien afërsisht me të njëjtën frekuencë. Në thelb, kjo zarf mund të konsiderohet si GPà me shpërndarje të barabartë.
Dhe nĂ«se hedhim menjĂ«herĂ« 10 tĂ« tillĂ« dhe llogarisim shumĂ«n totale tĂ« pikĂ«ve? A do tĂ« ruajĂ« kjo barazpeshĂ«n? Jo. MĂ« shpesh, shuma do tĂ« jetĂ« afĂ«r 125 pikĂ«ve, qĂ« do tĂ« thotĂ« njĂ« mesatare tĂ« caktuar. Si pasojĂ« â edhe para hedhjes, mund tĂ« vlerĂ«sojmĂ« pĂ«rafĂ«rsisht rezultatin e ardhshĂ«m.
Arsyeja Ă«shtĂ« se pĂ«r tĂ« marrĂ« shumĂ«n mesatare tĂ« pikĂ«ve ekziston numri mĂ« i madh i kombinimeve. Sa mĂ« larg tĂ« jetĂ« nga ajo, aq mĂ« pak kombinime do tĂ« jenĂ« â dhe pĂ«r rrjedhojĂ«, probabiliteti i rĂ«nies do tĂ« jetĂ« mĂ« i vogĂ«l. NĂ«se kĂ«to tĂ« dhĂ«na vizualizohen, ato do tĂ« duken disi si forma e njĂ« zilje. Prandaj, me njĂ« minimum tĂ« tĂ«rheqjes, sistemi i 10 zarfĂ«ve mund tĂ« quhet GPĂ me shpĂ«rndarje normale.
NjĂ« shembull tjetĂ«r, por tashmĂ« nĂ« njĂ« plan â qĂ«llimi nĂ« njĂ« qĂ«llim. Shigjetari do tĂ« jetĂ« GPĂ, duke gjeneruar njĂ« çift numrash (x, y), qĂ« shfaqet nĂ« grafik.

Pranon dhe se varianti nĂ« tĂ« majtĂ« Ă«shtĂ« mĂ« afĂ«r jetĂ«s reale â ky Ă«shtĂ« njĂ« gjenerator numrash tĂ« rastĂ«sishĂ«m me shpĂ«rndarje normale. Por nĂ«se ka nevojĂ« tĂ« shpĂ«rndajmĂ« yjet nĂ« qiellin e errĂ«t, atĂ«herĂ« varianti i djathtĂ«, i marrĂ« me njĂ« gjenerator numrash tĂ« rastĂ«sishĂ«m me shpĂ«rndarje uniforme, Ă«shtĂ« mĂ« i pĂ«rshtatshĂ«m. NĂ« pĂ«rgjithĂ«si, zgjidhni gjeneratorin sipas detyrĂ«s sĂ« vendosur.
Tani do të flasim për entropinë e sekuencës së NRR-së. Për shembull, egziston një sekuencë që fillon kështu:
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, âŠ
Sa të rastësishme duken këto numra me të parën që shikojmë? Le të fillojmë me kontrollin e shpërndarjes.

Duket si afër uniformes, por nëse lexojmë sekuencën me dy numra dhe i interpretojmë ata si koordinata në një dyanshëm, atëherë do të rezultojë kjo:

Patternet bëhen të dukshme. Dhe pasi të dhënat në sekuencë janë të rregulluara në një mënyrë të caktuar (domethënë kanë entropi të ulët), kjo mund të shkaktojë atë që quhet 'preferencë'. Të paktën, ky NRR nuk është shumë i përshtatshëm për gjenerimin e koordinatave në një dyanshëm.
Një sekuencë tjetër:
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, âŠ
Duket se gjithçka është në rregull edhe në planin dyanshëm:

Le të shikojmë në volum (lexojmë tre numra):

Dhe përsëri patternet. Të ndërtojmë një vizualizim në katër dimensione nuk do të funksionojë më. Por patternet mund të ekzistojnë edhe në këtë dimension dhe në më të mëdhenj.
Në të njëjtën fushë të kriptografisë, ku kërkesat për NRR janë më të rrepta, një situatë e tillë është kategorikisht e papranueshme. Prandaj, për të vlerësuar cilësinë e tyre, janë zhvilluar algoritme të veçanta, për të cilët nuk do të flasim tani. Tema është e gjerë dhe kërkon një artikull të veçantë.
Testimi
Nëse nuk dimë diçka me siguri, si të punojmë me të? A duhet të kalojmë rrugën nëse nuk e dimë se cili sinjal i semaforit e lejon këtë? Pasojat mund të jenë të ndryshme.
E njĂ«jta gjĂ« vlen pĂ«r rastĂ«sinĂ« e njohur nĂ« Unity. ĂshtĂ« mirĂ« nĂ«se dokumentacioni zbulohet me detajet e nevojshme, por historia e pĂ«rmendur nĂ« fillim tĂ« artikullit ndodhi pikĂ«risht pĂ«r shkak tĂ« mungesĂ«s sĂ« saktĂ«sive tĂ« dĂ«shiruara.
Dhe pa e ditur si funksionon mjeti, nuk mund ta aplikoni atë siç duhet. Në përgjithësi, ka ardhur koha për të kontrolluar dhe për të kryer një eksperiment, për t'u siguruar së paku për shpërndarjen.
Zgjidhja ishte e thjeshtë dhe efektive - të mbledhim statistikë, të marrim të dhëna objektive dhe të shohim rezultatet.
Subjekti i hulumtimit
Në Unity ekzistojnë disa mënyra për të gjeneruar numra të rastësishëm - ne testuam pesë.
- System.Random.Next(). Gjeneron numra të tërë (integer) brenda një diapazoni të caktuar.
- System.Random.NextDouble(). Gjeneron numra me pikë çift (double) brenda diapazonit nga [0; 1).
- UnityEngine.Random.Range(). Gjeneron numra me pikë të vetme (float) brenda një diapazoni të caktuar.
- UnityEngine.Random.value. Gjeneron numra me pikë të vetme (float) brenda diapazonit nga [0; 1).
- Unity.Mathematics.Random.NextFloat(). Pjesë e bibliotekës së re Unity.Mathematics. Gjeneron numra me pikë të vetme (float) brenda një diapazoni të caktuar.
Përveç në disa raste, dokumentacioni tregonte shpërndarje të barabartë, përveç UnityEngine.Random.value (ku shpërndarja nuk ishte e specifikuar, por duke pasur parasysh UnityEngine.Random.Range() gjithashtu pritej e barabartë) dhe Unity.Mathematics.Random.NextFloat() (ku bazohet në algoritmin xorshift, kështu që përsëri duhet pritur shpërndarje të barabartë).
Në parim, rezultatet e pritura ishin ato që janë të specifikuara në dokumentacion.
Metodologjia
Ne shkruam një aplikacion të vogël që gjeneronte sekuenca të numrave të rastësishëm me secilën nga mënyrat e paraqitura dhe ruante rezultatet për përpunim të mëtejshëm.
Gjatësia e secilës sekuencë - 100,000 numra.
Diapazoni i vlerave të numrave të rastësishëm - [0, 100).
Të dhënat u mbledhën nga disa platforma të targetuara:
- Windows
â Unity v2018.3.14f1, MĂ«nyra editori, Mono, .NET Standard 2.0 - macOS
â Unity v2018.3.14f1, MĂ«nyra editori, Mono, .NET Standard 2.0
â Unity v5.6.4p4, MĂ«nyra editori, Mono, .NET Standard 2.0 - Android
â Unity v2018.3.14f1, ndĂ«rtim pĂ«r pajisje, Mono, .NET Standard 2.0 - iOS
â Unity v2018.3.14f1, ndĂ«rtim pĂ«r pajisje, il2cpp, .NET Standard 2.0
Implementimi
Kemi disa mënyra të ndryshme për të gjeneruar numra të rastësishëm. Për secilën prej tyre do të shkruajmë një klasë mbështjellëse të veçantë, e cila duhet të ofrojë:
- Mundësinë për të caktuar diapazonin e vlerave [min/max). Do të caktohet përmes konstruktorit.
- Një metodë që kthen numrin e rastësishëm. Si tip do të zgjedhim float, si më të përgjithshëm.
- Emri i metodës së gjenerimit për etiketimin e rezultateve. Për lehtësi, do të kthejmë emrin e plotë të klasës + emrin e metodës që përdoret për të gjeneruar numra të rastësishëm.
Fillimisht, do të shpallim një abstraksion që do të paraqitet nga intervista IRandomGenerator:
namespace RandomDistribution
{
public interface IRandomGenerator
{
string Emri { get; }
float Gjenero();
}
}Implementimi i System.Random.Next()
Ky metodë lejon të përcaktohet një gamë vlerash, por ajo kthen numra të plotë (integer), ndërsa neve na duhen float. Mund të interpretojmë thjesht integer si float, ose mund të zgjasim gamën e vlerave disa rendesh, duke e kompensuar atë në çdo gjenerim të numrave të rastësishëm. Do të rezultojë diçka si fixed-point me saktësi të caktuar. Do ta përdorim këtë variant, pasi është më afër vlerave reale float.
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 Emri => "System.Random.Next()";
public SystemIntegerRandomGenerator(float min, float max, int factor = DefaultFactor)
{
_min = (int)min * factor;
_max = (int)max * factor;
_factor = factor;
}
public float Gjenero() => (float)_generator.Next(_min, _max) / _factor;
}
}Implementimi i System.Random.NextDouble()
KĂ«tu Ă«shtĂ« njĂ« gamĂ« fikse vlerash [0; 1). PĂ«r ta projektuar atĂ« nĂ« gamĂ«n e pĂ«rcaktuar nĂ« konstruktor, pĂ«rdorim aritmetikĂ« tĂ« thjeshtĂ«: X * (max â min) + min.
using System;
namespace RandomDistribution
{
public class SystemDoubleRandomGenerator : IRandomGenerator
{
private readonly Random _generator = new Random();
private readonly double _factor;
private readonly float _min;
public string Emri => "System.Random.NextDouble()";
public SystemDoubleRandomGenerator(float min, float max)
{
_factor = max - min;
_min = min;
}
public float Gjenero() => (float)(_generator.NextDouble() * _factor) + _min;
}
}Implementimi i UnityEngine.Random.Range()
Ky metodë e klasës statike UnityEngine.Random lejon të përcaktohet një gamë vlerash dhe kthen numra të rastësishëm të llojit float. Nuk do të ketë nevojë për konvertime të tjera.
using UnityEngine;
namespace RandomDistribution
{
public class UnityRandomRangeGenerator : IRandomGenerator
{
private readonly float _min;
private readonly float _max;
public string Emri => "UnityEngine.Random.Range()";
public UnityRandomRangeGenerator(float min, float max)
{
_min = min;
_max = max;
}
public float Gjenero() => Random.Range(_min, _max);
}
}Implementimi i UnityEngine.Random.value
Vlera e pronës së klasës statike UnityEngine.Random kthen një numër të rastësishëm të tipit float nga një gamë e caktuar vlerash [0; 1). Ta projektojmë atë në gamën e caktuar në të njëjtën mënyrë siç është realizuar 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;
}
}Implementimi i Unity.Mathematics.Random.NextFloat()
Metoda NextFloat() e klasës Unity.Mathematics.Random kthen një numër të rastësishëm të tipit float dhe lejon caktimin e një gamë vlerash. Detaji është se çdo ekzemplar i Unity.Mathematics.Random duhet të inicializohet me një seed për të shmangur gjenerimin e sekuencave të përsëritura.
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);
}
}Implementimi i MainController
Disa implementime të IRandomGenerator janë gati. Tani duhet të gjenerojmë sekuenca dhe të ruajmë datasetin rezultat për përpunim. Për këtë, do të krijojmë një skenë në Unity dhe një skript të vogël MainController, i cili do të kryejë gjithë punën e nevojshme dhe do të përgjigjet gjithashtu për ndërveprimin me UI.
Do të caktosh madhësinë e datasetit dhe gamën e vlerave të numrave të rastësishëm, si dhe do të kem një metodë që kthen një array të gjeneruesve të konfiguruar dhe të gatshëm për punë.
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)
};
}
...
}
}Tani tani jemi duke formuar një dataset. Në këtë rast, gjenerimi i të dhënave do të kombinohet me regjistrimin e rezultateve në një rrjedhë teksti (në formatin csv). Për ruajtjen e vlerave të secilit IRandomGenerator i jepet një kolonë e veçantë, ndërsa rreshti i parë përmban emrin e gjeneratorit.
namespace RandomDistribution
{
public class MainController : MonoBehaviour
{
...
private void GenerateCsvDataSet(TextWriter writer, int dataSetSize, params IRandomGenerator[] generators)
{
const char separator = ',';
int lastIdx = generators.Length - 1;
// shkruaj titullin
for (int j = 0; j <= lastIdx; j++)
{
writer.Write(generators[j].Name);
if (j != lastIdx)
writer.Write(separator);
}
writer.WriteLine();
// shkruaj të dhënat
for (int i = 0; i <= dataSetSize; i++)
{
for (int j = 0; j <= lastIdx; j++)
{
writer.Write(generators[j].Generate());
if (j != lastIdx)
writer.Write(separator);
}
if (i != dataSetSize)
writer.WriteLine();
}
}
...
}
}Tani duhet të thërrasim metodën GenerateCsvDataSet dhe të ruajmë rezultatin në një skedar, ose ta dërgojmë menjëherë të dhënat përmes rrjetit nga pajisja përfundimtare në atë pranuese. server.
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();
}
}
...
}
}Burimet e projektit ndodhen në .
Rezultatet
Nuk ndodhi ndonjĂ« mrekulli. Ajo qĂ« pritĂ«m, ishte ajo qĂ« morĂ«m â nĂ« tĂ« gjitha rastet njĂ« shpĂ«rndarje e barabartĂ« pa ndonjĂ« aluzion pĂ«r komplot. Nuk shoh ndonjĂ« kuptim nĂ« shtimin e grafikĂ«ve tĂ« veçantĂ« pĂ«r platforma â tĂ« gjitha tregojnĂ« rezultate pĂ«rafĂ«rsisht tĂ« njĂ«jta.
Realiteti është:

Vizualizimi i sekuencave në planin e të gjitha pesë mënyrave të gjenerimit:

Dhe vizualizimi në 3D. Do ta lë vetëm rezultatin e System.Random.Next(), për të mos krijuar një mori përmbajtjeje të njëjtë.

Historia e treguar në hyrje mbi shpërndarjen normale të UnityEngine.Random nuk u përsërit: ndoshta fillimisht ka qenë e gabuar, ose diçka ka ndryshuar që nga atëherë në motor. Por tani jemi të sigurt.
Burimi: habr.com
