A játékfejlesztésben gyakrabban fordul elő, mint a programozás többi területén, hogy szükségünk van valamiféle véletlen viselkedésre.
Láttuk, hogy a legbonyolultabb objektumok mélységében lebontva is csak nagyon egyszerű adatokból, leginkább számokból épülnek fel, int-ekből és float-okból. Ebből következik, hogy a véletlen működés is véletlenül generált számokból épül fel.
Ha láttunk már játékban bármiféle véletlen működést (, márpedig ritkán fordulnak elő játékok enélkül), akkor biztosak lehetünk benne, hogy az sok elemi véletlenszám generálásból lett megoldva.
Felhasználási szempontból alapvetően 2 külön módja van a véletlenszámok generálásának.
- Determinisztikus véletlen szám
- Nem determinisztikus véletlen szám
Ha egy folyamat determinisztikus, az azt jelenti, hogy újra végrehajtva pontosan ugyanazt az eredményt fogja adni, azaz az egyes elemi lépések egyike sem véletlenszerű.
A nem determinisztikus véletlenek
A nem determinisztikus véletlenek megértése könnyű. Ez az, ami teljesen össze-vissza, megjósolhatatlan és előre jelezhetetlen. Ha a számítógép teheti, akkor ezen szám generálásához számára elérhető fizikai adatokat is felhasznál, mint az idő és a hőmérséklet.
Bizonyos chip-ek, amik talán már a te számítógépedben is jelen vannak már kvantumeseményeket használnak arra, hogy véletlenszámokat generáljanak. Ezen chipeket titkosításra szokás használni és lényegük, hogy elméleti szinten is előre jelezhetetlenek még egy képzeletbeli lény számára sem, aki minden tud az univerzum jelen állapotáról.
A videojátékok véletlen számainak nagy része nem kell, hogy determinisztikus legyen.
Mikor például egy fegyverrel lövök és azt akarom, hogy egy bizonyos véletlen szórása legyen a lövésemnek, tökéletesen megfelel a nem determinisztikus módszer:
int randomInt = Random.Range(5,10); // Ez egy véletlen egész számot ad 5-tól 9-ig.
// EZ NEM ELÍRÁS VOLT!
// A fenti példa 5,6,7,8,9 számot adhat, 10-et nem.
// (Ennek optimalizációs és kényelemi oka van.)
float randomFloat = Random.Range(2.4f, 3.12f);
// Ez egy véletlen számot ad vissza 2.4-től 3.12-ig.
// Float esetben ha nagyon valószínűtlen is,
// de lehet, hogy pont a szélső értékeket adja.
A System és a Unity Engine névtér is tartalmaz Random osztályt. Ebben az esetben a fordító nem tudja eldönteni melyikre gondolunk. Ez fordítási idejű hiba és a Random osztálynevet pirossal aláhúzza az IDE.
using System;
using UnityEngine;
// Ha mindkettő névtér be van using-olva, tisztázni kell a file elején,
// hogy melyiket szeretnénk használni, mikor leírjuk, hogy Random:
using Random = UnityEngine.Random;
// Másik megoldás lehet hogy amikor használjuk a random osztály elé írjuk a névteret is:
float randomFloat = UnityEngine.Random.Range(2.4f, 3.12f);
A determinisztikus véletlenek
Felmerülhet a kérdés, hogy hogyan lehet a véletlenszám-generálás determinisztikus, mikor azt állítottam, hogy egy determinisztikus folyamatban semmi véletlen nem lehet.
Vannak esetek, amikor nem teljes véletlenre van szükségünk, hanem úgynevezett álvéletlenekre vagy semi-véletlenekre. Ebben az esetben egy bonyolult algoritmus felel a véletlen számok legenerálásáért, ami újra lefuttatva ugyanazokat a számokat fogja adni.
Ezek nem véletlen számok olyan szempontból, hogy megismételhetőek és előre jelezhetőek. De a logikus mintázatot ezen számok egymásutánjában egy ember számára nagyon nehéz, vagy inkább azt mondom, lehetetlen észre venni.
Determinisztikus számok generálására mindig egy magot, azaz seed-et használunk. Ez egy kiinduló szám, amivel elindítjuk a véletlenszámok generálásának folyamatát.
Ha két determinisztikus számgenerálási folyamatot ugyanattól a seed-től indítunk, az összes többi generált szám egyezni fog.
Erre például akkor lehet szükség a játékfejlesztésben, ha azt szeretnénk, hogy a véletlenül generált pályánkat a játékos ha kedve tartja újra tudja generálni egy seed alapján.
Javaslom, hogy erre a célra ne a Unity hanem a System random osztályát használjuk.
int seed = 123456;
System.Random myRandom = new System.Random(seed);
int randomInt = myRandom.Next(5, 10); // Ez egy véletlen egész számot ad 5-tól 9-ig.
double randomDouble myRandom.NextDouble();
// Ez egy véletlen számot ad 0-tól 1-ig.
// A double olyan lebegőpontos típus, mint a float, csak pontosabb
// Átkasztolható float-ba
float randomFloat = (float) randomDouble
Sorozatok keverése
Gyakori probléma a játékfejlesztésben, hogy egy sorozatból akarunk véletlenszerűen sorsolni ezt meg tudjuk tenni azáltal, hogy használjuk a lista vagy tömb hosszát és az int alapú randomszám generátort.
Card[] cardArray; // Kártyalapok tömbje
List<Card> cardList; // Kártyalapok listája
// ...
Card randomFromArray = cardArray[Random.Range(0, cardArray.Length)];
Card randomFromList = cardArray[Random.Range(0, cardList.Count)];
Gyakran azonban szeretnénk, hogy sorsolás után az eltávolított elem törlődjön is a listából:
int randomIndex = Random.Range(0, cardArray.Length);
Card randomFromArray = cardList[randomIndex];
cardList.RemoveAt(randomIndex);
Más esetekben arra van szükségünk, hogy megkeverjünk egy sorbarendezett listát, akár egy kártyapaklit. Ez elérhető azáltal, hogy véletlenszerűen cserélgetünk elemeket a listában. Na de hány csere garantálja, hogy tényleg véletlen lesz a lista sorrendje és nem lesz felfedezhető az eredeti sorminta. (Meddig érdemes keverni egy új kártyapaklit?)
Biztosan teljesen véletlenszerű lesz egy lista, ha annyiszor cserélünk, ahány elemű a pakli, és sorban haladunk rajta végig, ezzel garantálva, hogy minden indexen egyszer legalább történjen csere.
// Fisher-Yates keverő algoritmus
void Shuffle(List<Card> deck)
{
for (int i = deck.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
Card temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;
}
}
Ha nem keverni, hanem sorba rendezni szeretnél egy listát: Sorozatok rendezése