Кастомні рандомогенератори та їх реалізація в 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, то ж подібне мені дуже цікаво. Підписався. Пиши ще :)

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

Особисто мені було б цікаво побачити не стільки реалізацію кодом, скільки можливі підходи до керування рандомом. Бо навіть самий чистий рандом (не кажучи вже про той що в Юніті), створює чимало питань вже на рівні геймдизайну.
До речі, недавно вирішував пов’язану задачу із шансами критичного удару. Кому цікава лаконічна формула, що повинна динамічно визначати шанси на true або false, можете спробувати наступне:
P = 1 / (1 + 2^(k*(n-m)))
де Р — вірогідність (буде в межах 0 до 1),
^ - знак ступеня,
n — кількість спроб здійснених гравцем (обнуляємо після кожного «успіху»),
m — кількість спроб, при яких вірогідність досягає 50%,
k — коефіцієнт, що визначає швидкість зростання ймовірності.
Дана формула задає шанс не через фіксований відсоток, а через налаштовану криву, що мінімізує шанси на «успіх» з перших спроб, але поступово їх нарощує до гарантії. Це дозволяє збалансувати удачу і виключити ймовірність «трьох крітів підряд» або «жодного рар-дропу з 30 кейсів».

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

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

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

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

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