
MĂ€ngude arenduses on tihti vajalik seostada midagi juhuslikkusega: Unity'l on selleks oma Random, samas eksisteerib paralleelselt ka System.Random. Ajal, mil töötasin ĂŒhel projektil, tekkis mul mulje, et mĂ”lemad vĂ”ivad toimida erinevalt (kuigi nad peaksid omama ĂŒhtlast jaotust).
Siis ei sĂŒĂŒvinud me detailidesse â piisav oli see, et ĂŒleminek System.Randomile lahendas kĂ”ik probleemid. NĂŒĂŒd otsustasime sĂŒveneda ja teha vĂ€ikese uurimuse: kui palju on RNG-d "eelistatud" vĂ”i ettearvatavad ning millist valida. Eriti kuna olen korduvalt kuulnud vastuolulisi arvamusi nende "aususe" kohta â pĂŒĂŒame vĂ€lja selgitada, kuidas reaalsed tulemused seonduvad lubatutega.
LĂŒhike sissejuhatus vĂ”i RNG on tegelikult GPSC
Kui olete juba tuttav juhuslike arvude generaatoritega, vÔite kohe edasi liikuda jaotisse "Testimine".
Juhuslikud numbrid (JN) on numbrite jada, mis genereeritakse mĂ”ne juhusliku (kaootilise) protsessi, entropia allika abil. See tĂ€hendab, et tegemist on sellise jadaga, mille elemendid ei ole omavahel seotud mingisuguse matemaatilise seadusega â neil puudub pĂ”hjuslik seos.
Seda, mis loob JN, nimetatakse juhuslike numbrite generaatoriks (JNG). NÀiliselt on kÔik elementaarne, kuid kui teooriast praktikas liikuda, siis tegelikult ei ole sellise jada genereerimise programmialgoritmi rakendamine mitte nii lihtne.
Probleem peitub kaasaegse tarbielektroonika juhuslikkuse puudumises. Ilma selleta lĂ”petavad juhuslikud numbrid juhuslikkuse ja nende generaator muutub tavaliseks funktsiooniks eelnevalt mÀÀratletud argumentide suhtes. Teatud IT-erialade puhul on see tĂ”sine probleem (nĂ€iteks krĂŒptograafias), samas kui teiste jaoks on tĂ€iesti vastuvĂ”etav lahendus.
Tuleb kirjutada algoritm, mis tagastaks ehk mitte tĂ”eliselt juhuslikud numbrid, kuid maksimaalselt lĂ€hedased neile â nn pseudojuhuslikud numbrid (PJ). Sellist algoritmi nimetatakse pseudojuhuslike numbrite generaatoriks (PJG).
On olemas mitmeid vÔimalusi PJG loomiseks, kuid kÔigile on oluline jÀrgmine:
- Eelneva initsialiseerimise vajadus.
PJG-l puudub entropiaallikas, seega peab enne kasutamist sellele nĂ€itama algseisundi. See mÀÀratakse numbrina (vĂ”i vektorina) ja seda nimetatakse seeme (seed, random seed). Sageli on seemena kasutusel protsessori taktsĂŒkli loendur vĂ”i sĂŒsteemi aja numbriline ekvivalent.
- JĂ€rjestuse reprodutseeritavus.
PJG on tĂ€ielikult mÀÀratud, seega mÀÀrab initsialiseerimise ajal antud seeme ĂŒheselt kogu tulevase numbriseeria. See tĂ€hendab, et eraldi vĂ”etud PJG, mis on initsialiseeritud sama seemnega (erinevatel aegadel, erinevates programmides, erinevates seadmetes), genereerib sama jĂ€rjestuse.
Oluline on teada ka GSHD tĂ”enĂ€osuste jaotust â milliseid numbreid ta genereerib ja millise tĂ”enĂ€osusega. See on enamasti kas normaaljaotus (normal distribution) vĂ”i ĂŒhtlane jaotus (uniform distribution).

Normaaljaotus (vasakul) ja ĂŒhtlane jaotus (paremal)
Oletame, et meil on aus mĂ€ngukuubik, millel on 24 kĂŒlge. Kui seda visata, siis ĂŒhe tulemuse tĂ”enĂ€osus on 1/24 (samuti iga teise numbri tĂ”enĂ€osus). Kui visata palju kordi ja tulemusi kirja panna, siis vĂ”ib tĂ€heldada, et kĂ”ik kĂŒlgedel on umbes sama sagedusega tulemusi. Sisuliselt vĂ”ib seda mĂ€ngukuubikut pidada GSHD-ks ĂŒhtlase jaotusega.
Kuidas siis, kui visata kohe 10 sellist kuubikut ja arvutada kokkupunktide summa? Kas see sĂ€ilitab ĂŒhtsuse? Ei. Enamasti on summa lĂ€hedane 125 punktile, st mingile keskmisele vÀÀrtusele. Seega, juba enne viskamist saab tulevast tulemust umbkaudu hinnata.
PĂ”hjus on see, et keskmise skoori saavutamiseks on kĂ”ige rohkem kombinatsioone. Mida kaugemal ollakse, seda vĂ€hem on kombinatsioone â ja seega ka vĂ€iksem tĂ”enĂ€osus esinemiseks. Kui neid andmeid visualiseerida, meenutavad need kaugel kujundit, mis sarnaneb kellakujule. SeetĂ”ttu vĂ”ib natuke venitatuna 10 tĂ€ringu sĂŒsteemi nimetada normaaljaotusega juhuslikuks arvuks.
Veel ĂŒks nĂ€ide, kuid juba tasapinnal â sihtmĂ€rkide tulistamine. SihtmĂ€rgi laskuriks on juhuslik arv, mis genereerib paari numbreid (x, y), mis kuvatakse graafikul.

Kokkuleppel, et vasakpoolne variant on rohkem lĂ€hedane reaalsusele â see on normaaljaotusega juhuslik arv. Kuid kui on vaja tĂ€hti pimedas taevas hajutada, sobib parempoolne variant, mis on saadud ĂŒhtlaselt jaotatud juhusliku arvu abil. Ăldiselt valige generaator vastavalt ĂŒlesandele.
NĂŒĂŒd rÀÀgime PSE jada entropiast. NĂ€iteks on olemas jada, mis algab nii:
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, âŠ
Kuidas need numbrid esmapilgul nÀivad? Alustame jaotuse kontrollimisega.

Need nĂ€ivad peaaegu ĂŒhtlased, kuid kui lugeda jĂ€rjestust kahte numbrit korraga ja tĂ”lgendada neid koordinaatidena tasandil, siis tuleb vĂ€lja selline pilt:

Mustrid hakkavad selgelt vÀlja paistma. Kuna andmed jÀrjestuses on teatud viisil korraldatud (st neil on madal entropia), vÔib see tekitada just seda "ebamugavust". Igatahes ei sobi selline GPХJ koordinaatide genereerimiseks tasandil vÀga hÀsti.
Teine jÀrjestus:
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, âŠ
Taas tundub, et siin on kÔik korras isegi tasandil:

Vaadakem ruumis (loeme kolme numbrit korraga):

Ja jÀlle mustrid. Neli mÔÔtmetes visualiseerimine ei toimi. Kuid mustrid vÔivad eksisteerida ka selle mÔÔtme ja suuremate puhul.
Samal krĂŒptograafial, kus PRNG-dele esitatavad nĂ”uded on kĂ”ige rangemad, on selline olukord kategooriliselt vastuvĂ”etamatu. SeetĂ”ttu on nende kvaliteedi hindamiseks vĂ€lja töötatud spetsiaalsed algoritmid, mille kohta me praegu ei rÀÀgi. Teema on ulatuslik ja vÀÀrib eraldi artiklit.
Testimine
Kui me ei tea midagi kindlalt, kuidas sellega siis ĂŒmber kĂ€ia? Kas on mĂ”tet teed ĂŒletada, kui sa ei tea, milline valgusfoori signaal seda lubab? TagajĂ€rjed vĂ”ivad olla erinevad.
Sama kehtib ka kurikuulsa juhuslikkuse kohta Unitys. Hea on, kui dokumentatsioon toob esile vajalikud ĂŒksikasjad, kuid artikli alguses mainitud lugu juhtus just selle puudumise tĂ”ttu.
Ilma teadmata, kuidas tööriist töötab, ei saa sa seda Ă”igesti rakendada. ĂhesĂ”naga, on aeg kontrollida ja katsetada, et lĂ”puks veenduda vĂ€hemalt jaotuses.
Lahendus oli lihtne ja efektiivne â koguda statistikat, saada objektiivseid andmeid ja vaadata tulemusi.
Uuringu teema
Unitys on mitmeid viise juhuslike numbrite genereerimiseks â testisime viit.
- System.Random.Next(). Genereerib tÀisarve (integer) mÀÀratud vÀÀrtuste vahemikus.
- System.Random.NextDouble(). Genereerib kahekordse tÀpsusega (double) arve vahemikus [0; 1).
- UnityEngine.Random.Range(). Genereerib ĂŒheainsa tĂ€psusega (float) arve mÀÀratud vÀÀrtuste vahemikus.
- UnityEngine.Random.value. Genereerib ĂŒheainsa tĂ€psusega (float) arve vahemikus [0; 1).
- Unity.Mathematics.Random.NextFloat(). Osa uuest Unity.Mathematics raamatukogust. Genereerib ĂŒheainsa tĂ€psusega (float) arve mÀÀratud vÀÀrtuste vahemikus.
Peaaegu igal pool dokumentatsioonis on mĂ€rgitud ĂŒhtlane jaotumine, vĂ€lja arvatud UnityEngine.Random.value (kus jaotumist ei ole mainitud, kuid sarnaselt UnityEngine.Random.Range() oodati samuti ĂŒhtlast jaotumist) ja Unity.Mathematics.Random.NextFloat() (kus pĂ”hineb algoritmil xorshift, seega tuleb jĂ€lle oodata ĂŒhtlast jaotumist).
Vaikimisi vÔeti oodatud tulemused, mis on dokumentatsioonis mÀrgitud.
Metoodika
Kirasime vÀikese rakenduse, mis genereeris juhuslike arvude jÀrjestusi iga esindatud meetodi abil ja salvestas tulemused edasiseks töötlemiseks.
Iga jÀrjestuse pikkus on 100 000 arvu.
Juhuslike arvude vÀÀrtuste vahemik on [0, 100).
Andmed koguti mitmelt sihtplatvormilt:
- Windows
â Unity v2018.3.14f1, redigeerimise reĆŸiim, Mono, .NET Standard 2.0 - macOS
â Unity v2018.3.14f1, redigeerimise reĆŸiim, Mono, .NET Standard 2.0
â Unity v5.6.4p4, redigeerimise reĆŸiim, Mono, .NET Standard 2.0 - Android
â Unity v2018.3.14f1, seadmele versioon, Mono, .NET Standard 2.0 - iOS
â Unity v2018.3.14f1, seadmele versioon, il2cpp, .NET Standard 2.0
Rakendus
Meil on mitu erinevat viisi juhuslike arvude genereerimiseks. IgaĂŒhe jaoks kirjutame eraldi wrapper-klassi, mis peaks pakkuma:
- VÔimalust mÀÀrata vÀÀrtuste vahemik [min/max). See mÀÀratakse konstruktoris.
- Meetod, mis tagastab juhusliku arvu. TĂŒĂŒbiks valime float, kuna see on ĂŒldisema iseloomuga.
- Generatsiooni meetodi nimetus tulemuste mÀrgistamiseks. Mugavuse huvides tagastame vÀÀrtusena klassi tÀisnime + meetodi nime, mida juhusliku arvu genereerimiseks kasutatakse.
Alustame abstraktsiooni vÀljakuulutamisest, mille esindab IRandomGenerator liides:
namespace RandomDistribution
{
public interface IRandomGenerator
{
string Name { get; }
float Generate();
}
}Realiseerimine System.Random.Next()
See meetod vĂ”imaldab mÀÀrata vÀÀrtuste vahemikku, kuid tagastab tĂ€isarvud (integer), samas kui vajalikud on ujukomaarvud (float). Ăks vĂ”imalus on tĂ”lgendada tĂ€isarve kui ujukomaarve, teine vĂ”imalus on laiendada vÀÀrtuste vahemikku mitme jĂ€rjekorra vĂ”rra, kompenseerides neid iga kord juhuslikku arvu genereerimisel. Saame midagi sarnast fikseeritud punktiga, millel on mÀÀratud tĂ€psus. Kasutame seda varianti, kuna see on lĂ€hemal tĂ”elise ujukomaarvulise vÀÀrtuse mĂ”istele.
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() rakendamine
Siin on fikseeritud vÀÀrtuste vahemik [0; 1). Et projitseerida see konstruktoris mÀÀratud vahemikule, kasutame lihtsat aritmeetikat: X * (max â min) + min.
kasutades System;
nimeserver RandomDistribution
{
avalik klass SystemDoubleRandomGenerator : IRandomGenerator
{
privaatne lÔplik Random _generator = uus Random();
privaatne lÔplik double _factor;
privaatne lÔplik float _min;
avalik string Nimi => "System.Random.NextDouble()";
public SystemDoubleRandomGenerator(float min, float max)
{
_factor = max - min;
_min = min;
}
avalik float Generate() => (float)(_generator.NextDouble() * _factor) + _min;
}
}UnityEngine.Random.Range() rakendamine
See staatilise klassi UnityEngine.Random meetod vĂ”imaldab mÀÀrata vÀÀrtuste vahemiku ja tagastab float tĂŒĂŒpi arvu. TĂ€iendavaid teisendusi tegema ei pea.
kasutades UnityEngine;
nimeserver RandomDistribution
{
avalik klass UnityRandomRangeGenerator : IRandomGenerator
{
privaatne lÔplik float _min;
privaatne lÔplik float _max;
avalik string Nimi => "UnityEngine.Random.Range()";
public UnityRandomRangeGenerator(float min, float max)
{
_min = min;
_max = max;
}
avalik float Generate() => Random.Range(_min, _max);
}
}UnityEngine.Random.value rakendamine
Staatilise klassi UnityEngine.Random omadus value tagastab float tĂŒĂŒpi arvu fikseeritud vÀÀrtuste vahemikust [0; 1). Projekteerime selle antud vahemikku samamoodi nagu System.Random.NextDouble() rakendamisel.
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() rakendus
Unity.Mathematics.Random.NextFloat() meetod tagastab float tĂŒĂŒpi juhusliku arvu ja vĂ”imaldab mÀÀrata vÀÀrtuste vahemiku. Ainus nĂŒanss on see, et iga Unity.Mathematics.Random eksemplar tuleb initsialiseerida mĂ”ne seed'iga â nii saame vĂ€ltida korduvate jadade genereerimist.
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 rakendus
Mitmed IRandomGenerator'i rakendused on valmis. Edasi tuleks genereerida jÀrjestusi ja salvestada tulemuste andmestik töötlemiseks. Selle jaoks loome Unity's stseeni ja vÀikese skripti MainController, mis teostab kogu vajaliku töö ja vastutab samal ajal UI-ga suhtlemise eest.
MÀÀrame andmestiku suuruse ja juhuslike arvude vÀÀrtuste vahemiku ning loome meetodi, mis tagastab töötamiseks seadistatud ja valmis generaatorite massiivi.
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)
};
}
...
}
}NĂŒĂŒd loome andmestiku. Antud juhul koosneb andmete genereerimine tulemuste kirjutamisest tekstivoolu (csv-formaadis). Iga IRandomGenerator'i vÀÀrtuste salvestamiseks on ettenĂ€htud oma eraldi veerg, samas kui esimene rida sisaldab generaatori nime.
namespace RandomDistribution
{
public class MainController : MonoBehaviour
{
...
private void GenerateCsvDataSet(TextWriter writer, int dataSetSize, params IRandomGenerator[] generators)
{
const char separator = ',';
int lastIdx = generators.Length - 1;
// write header
for (int j = 0; j <= lastIdx; j++)
{
writer.Write(generators[j].Name);
if (j != lastIdx)
writer.Write(separator);
}
writer.WriteLine();
// write data
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();
}
}
...
}
}Peab ainult kutsuda vÀlja meetod GenerateCsvDataSet ja salvestada tulemus faili vÔi edastada andmed otse vÔrgus lÔppseadmest vastuvÔtjale. 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();
}
}
...
}
}Projekti lÀhtekoodid on saadaval aadressil .
Tulemused
Ihme ei juhtunud. Ootused ja saadud tulemused olid samad â kĂ”igis juhtumites ĂŒhtlane jaotus ilma mingite vandenĂ”ude mĂ€rkideta. Eraldi graafikute lisamine platvormide kaupa ei tundu mĂ”istlik, kuna need nĂ€itavad kĂ”ik enam-vĂ€hem samu tulemusi.
TÔde on selline:

Seeriate visualiseerimine tasapinnal kÔigi viie genereerimise viisi puhul:

Ja visualiseerimine 3D-s. JĂ€tan alles ainult System.Random.Next() tulemuse, et mitte toota liiga palju sarnast sisu.

Sissejuhatuses rÀÀgitud normaaljaotuse lugu UnityEngine.Random kohta ei kordunud: kas see oli algselt vale vĂ”i on mootoris sellest ajast midagi muutunud. Kuid nĂŒĂŒd oleme kindlad.
Allikas: habr.com
