Кастомні рандомогенератори та їх реалізація в Unity

Рандом в іграх зустрічається постійно. Що випаде у кейсі, який світ згенерується з сіду, критане персонаж та скільки саме шкоди він нанесе.

І часто виникає питання: «а яким саме чином зробити деякі аспекти цього рандому?» Гайда сьогодні розберемося з цим)

*Традиційний дисклеймер: рішення та код можуть бути не найкращими, тож для когось це буде гайд, а для когось план «як робити не треба». Також головна ціль топіка — показати, що і як просто можна створити засобами Unity та C#. Тож не судіть строго)*

Рандомні числа

Почнемо з чогось цілком логічного та звичного.

В юніті ми маємо зручний метод Random.Range(min, max). Невелика особливість, про яку часто забувають — як мінімальне, так і максимальне значення включається в послідовність. За допомогою цього статичного методу ми можемо легко отримувати будь-яке число типу int або float.

Також можна створювати рандомні числа через System.Random, використовуючи метод Random.Next(...).

Все просто, рухаємось далі)

Рандомний єнам

Це вже цікавіше і залежить від самої реалізації єнаму.

Уявімо, що ми маємо таку структуру:

enum Level 
{
  Low,
  Medium,
  High
}

Варто згадати, що кожній константі єнаму відповідає числове значення від 0 до n-1, де n — кількість елементів. Тобто той же ж самий єнам можна представити у вигляді:

enum Level 
{
    Low = 0,
    Medium = 1,
    High = 2
}

Відповідно для такої ситуації ми можемо зробити генератор рандомних чисел і кастувати отримане значення до enum:

public Level GetRandomLevel()
{
    return (Level) Random.Range(0, Enum.GetValues(typeof(Level)).Length);
}

Краса, чи не так? Проте не все так просто.

Уявімо ситуацію, коли значення єнаму йтимуть не послідовно, наприклад, коли треба через інспектор обирати одночасно декілька. Цей випадок виглядатиме так:

public enum Level
{
    Low = 0,
    Medium = 2,
    High = 4
}

В такому разі будуть значення, які не можуть перетворитися на константи. Неприємно =( Тому треба придумати щось інше... Або не дуже далеко відходити)

У попередньому шматочку я вже використав статичний метод Enum.GetValues(T). Як можна зрозуміти з назви, він повертає структуру даних, Array, з усіма константами. Відповідно в цьому випадку задача рандомного єнаму зводиться до рандомного елементу в масиві, яку можна вирішити, наприклад, так:

public Level GetRandomLevel()
{
    var random = new Random();



    var values = Enum.GetValues(typeof(Level));
    return (Level) values.GetValue(random.Next(values.Length));
}

Або ж рішення, яке мені подобається більше:

public static T NextEnumValue<T>(this System.Random rng) where T : Enum
{
    var values = Enum.GetValues(typeof(T));
    return (T) values.GetValue(rng.Next(values.Length));
}

Тут ми використовуємо generic extension method, обмежуючи тип дженерика єнамом. Таким чином можна цей метод використовувати для абсолютно будь-якого enum, який зустрінеться в проєкті. Краса)

Але і це не все. Іноді єнами мають такий вигляд:

public enum Level
{
    None,
    Low,
    Medium,
    High
}

Тобто мають дефолтне значення, None, яке ми хочемо скіпнути.

*загалом будь-яке значення, що стоїть першим (або має найнижчий числовий відповідник) буде дефолтним*

І такий випадок також легко зробити, використовуючи LINQ, наприклад:

public static T NextEnumValueSkipDefault<T>(this System.Random rng) where T : Enum
{
    var values = Enum.GetValues(typeof(T)).Cast<T>().Where(e => !e.Equals(default(T)));
    return values.ElementAt(rng.Next(values.Count()));
}

Тепер насолоджуємося красивими рандомними єнамами в нашому житті)

Рандомні класи

Не частий випадок, і завжди можна знайти краще рішення, проте знати про такий вид генераторів також не буде зайвим.

Основна ідея полягає в тому, щоб через System.Reflection отримувати класи-спадкоємці й звідти повертати якийсь рандомний інстанс. Реалізація ідеї у мене вийшла приблизно такою:

public static T NextClassValue<T>(this System.Random rng) where T : class
{
    var allTypes = Assembly.GetExecutingAssembly().GetTypes();
    var inheritors = allTypes.Where(t => t.IsSubclassOf(typeof(T)) && !t.IsAbstract).ToList();
    if (inheritors.Count == 0)
        throw new InvalidOperationException($"No concrete classes inheriting from {typeof(T).Name} were found.");
    
    int index = rng.Next(inheritors.Count);
    var inheritorType = inheritors[index];
    return (T)Activator.CreateInstance(inheritorType);
}

Activator.CreateInstance викликає дефолтний конструктор класу, тож якщо нам треба заповнити внутрішні поля класу рандомними значеннями, то можна явно його вказати і викликати менші генератори (чисел, єнамів, інших класів).

От і вся логіка)

В кінці хочу сказати, що будь-яку задачу повернення рандомного типу після нехитрих роздумів можна перетворити або на каст з числа в необхідний тип, або на отримання рандомного елементу в масиві, приблизно так, як це робив я.

Тому най на вашій стороні буде сила рандому, качайте параметри удачі, та зустрінемося у наступній статті)

Підписуйтеся на Telegram-канал @gamedev_dou, щоб не пропустити найважливіші статті і новини про геймдев

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Ого, це дуже круто. Я тільки почав вчити C# та Unity, то ж подібне мені дуже цікаво. Підписався. Пиши ще :)

Гарний топік. Знову ж таки — побільше б таких.

Топ коментар! Напишіть про це блог :)

дякую за такий крутий комент!

звучить як дуже цікава тема, сам би таке почитав😅
як ви сказали, контролювати рандом завжди треба, бо інакше гра буде здаватися аж занадто не чесною по відношенню до гравця

Я думав що в теперішні часи це все в геймдеві настільки спростилось до полуфабрикатів, що відкривається якийсь діалог в стилі no-code де малюється графік вірогідності і налаштовуються параметри :)

Підписатись на коментарі