Больше всего в играх я люблю лут. Еще со времен Lineage 2, когда я играл на фришках с лоу онлайном и выфармливал Б даггер на споте из 5 мобов с шансом дропа 0.0000002, я задавался вопросом почему шанс такой маленький и как с таким шансом можно выбить предмет с первого раза. Сегодня, конечно, ситуация намного понятнее и с точки зрения игрока и с точки зрения разработчика.
Создание системы генерации лута очень индивидуальная задача и сильно зависит от идеи проекта. Общего рецепта нет, но если упростить, все будет подчиняться логике:
Определение концепта системы, как и в большинстве прочих случаев, является одним из ключевых решений. Нужно понимать все текущие и возможные точки взаимодействия с системой которую мы делаем. Конечно, идеала не существует и никто не застрахован от того, что через неделю работы над системой может прилететь правка, которая поделит всю логику на 0, но нужно постараться быть на шаг впереди с помощью понимания проекта в целом.
Например, для простых игр типа волкинг сим не обязательно придумывать что-то сложное, генерировать случайные предметы и прочее, но стоит продумать общую систему взаимодействия с предметами, чтобы можно было легко масштабировать количество и иметь возможность изменять общую логику без лишних усилий. Я упоминаю это не просто так, а потому, что уже на начинающем уровне столкнулся с чужой работой, где логика взаимодействия с предметами (в том числе лутом) была реализована индивидуально для каждого предмета. Возможно, для тех кто не знаком с принципами ООП это не покажется грубой ошибкой, но для меня это был культурный шок. А что если завтра потребуется добавить еще 10 предметов?
Этой историей я лишь хочу обозначить то, что даже над простейшей системой нужно задумываться и задавать самому себе вопросы.
При создании своей системы, я руководствовался такими принципами как:
В итоге, в голове сложился план, где я создаю UActorComponent который контролирует логику генерации лута для каждого отдельно взятого триггера. Внутри компонента я могу подстраивать шансы для выпадения требуемых предметов. Саму информацию о луте я храню в разделенных по типам DT, чтобы иметь возможность добавлять/редактировать генерируемые базы предметов.
#include <ULootComponent.h>;
// Trigger
AMonster::OnDeath(){
GenerateLoot(1);
}
// LootComponent
ULootComponent::GenerateLoot(int GenerateAmount){
// For each GenerateAmount unit
for (int i; i < GenerateAmount; i++){
// Choose loot type
// Spawn item
}
}
В моем случае, как триггер можно использовать любой эвент внутри Актора, система самодостаточна. Для своих экспериментов я сделал куб, при наведении камеры и нажатии E, вызывается генерация.
CharacterBP:
TriggerBP:
Для расчета шанса выпадения лута я использовал систему весов. В открытом доступе есть куча материалов о том как она работает, но я все же уделю минутку расчетам.
Представим, что из монстра может выпасть 4 типа предметов – Оружее, Броня, Бижутерия и Расходуемые. Оружее и броня падает примерно с одинаковым шансом, а бижутерия в два раза реже. При этом самый частый дроп это обычные расходники, допустим в половине случаев. Исходя из этого составим таблицу:
Weapon | 20 |
Armor | 20 |
Jewelry | 10 |
Consumables | 50 |
Для наглядности я использовал сумму весов в 100, но для более тонких настроек можно использовать как 10 так и 100000. Используя таблицу мы фактически говорим, что в среднем на 100 вызовов генерации лута мы получим соответствующее таблице количество типов.
Реализуя гибкую систему, я складываю информацию о возможном выпадении лута в TMap состоящую из ENum типов лута и float как репрезентацию веса. При таком подходе я могу легко настраивать типы лута из единого реестра типов, а также вес для отдельных триггеров. Чтобы расчитать рандом пользуюсь следующим методом:
TMap <MyEnum, float> LootTable; // Represent moderatable loot table
int WeightSum = 100; // Float sum from LootTable simplified w/o calculations
MyEnum Key; // Result loot name represented as ENum key
int RandomRoll = FMath::RandRange(1,WeightSum); // Rolling random
float CumulativeWeight = 0.0f;
for (auto& a: LootTable)
{
CumulativeWeight += a.Value;
// Check if the random roll is within the current cumulative weight
if (RandomRoll <= CumulativeWeight)
{
// Found the correct entry based on random roll
Key = a.Key;
break; // Exit the loop since we found the correct entry
}
}
В результате переменная Key содержит Enum тип лута, который дальше используется для создания предмета. Переложеная на блупринты логика выглядит примерно так:
LootComponentBP weight sum:
LootComponentBP random result calculations:
Следущие итерации являются вариациями приведенных расчетов. Когда я определяю тип предмета я, таким же образом, обращаясь к следующей ENum структуре выбираю тип предмета (Sword/Dagger/Bow и т.п.). На этом моменте шанс выпадения нужного типа снаряжения начинает резко уменьшаться.
Sword | 50 |
Dagger | 100 |
Two-handed Sword | 10 |
Bow | 45 |
Например, чтобы расчитать веротяность выпадения даггера мы должны посчитать вероятность примерно так: ((LootType/WeightSum)*(LootType2/WeightSum2)) * 100 = N%
((20/100) * (100/205))*100 = 9,7% шанс выпадения даггера по данным приведенным в таблицах.
После определения типов я обращаюсь к зараннее подготовленной DT, в которую внесена базовая информация о вещи (Min-max attack/speed/defence, ассет, описание и прочее). Если в DT содержиться больше чем одна вещь искомого типа, мы также можем применить расчет веса (если структура DT предусматривает веса), но для упрощения и разнообразия я использую равноценный рандом:
Теперь шанс выпадения нужного дагера уменьшился еще в N раз количества других его видов, если они есть.
На выходе мы имеем структуру данных с информацией о базовой болванке предмета и можем продолжать манипуляции с ней.
Мой метод не кажется самым эффективным, но точно должен хорошо работать для небольших и средних игр.
Следующим шагом я собираюсь генерировать характеристики предметов в зависимости от их типа и редкости, расскажу об этом в следующей статье, а пока bp версию проекта можно найти на гитхабе: