Кастомні рандомогенератори та їх реалізація в 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 викликає дефолтний конструктор класу, тож якщо нам треба заповнити внутрішні поля класу рандомними значеннями, то можна явно його вказати і викликати менші генератори (чисел, єнамів, інших класів).
От і вся логіка)
В кінці хочу сказати, що будь-яку задачу повернення рандомного типу після нехитрих роздумів можна перетворити або на каст з числа в необхідний тип, або на отримання рандомного елементу в масиві, приблизно так, як це робив я.
Тому най на вашій стороні буде сила рандому, качайте параметри удачі, та зустрінемося у наступній статті)
7 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів