Перевод статьи «How to Code Monster Loot Drops»
Рассказывает Kyatric
В экшн-играх обычной механикой является выпадение из убитого врага какой-то вещи или награды. Игрок может собрать эту добычу и получить какое-то преимущество. В этом руководстве мы поговорим об устройстве и реализации этой системы.
Демки созданы в Construct 2, HTML5-инструменте для разработки игр, но вы можете реализовать эту механику, используя любой язык или инструмент. Я использовал версию r167.2. Вы можете скачать свежую версию здесь и разобраться с примерами. Их исходники можно найти здесь.
Базовая механика
После смерти врага (когда его HP меньше или равны нулю) вызывается функция. Задачей функции является определение наличия и типа лута.
Функция также может управлять визуальным отображением лута, показывая его на месте, где был убит враг.
Вот пример:
Нажмите кнопку Slay 100 Beasts. Это запустит процесс, который создаст 100 случайных существ, убьёт их и выведет результат для каждого их них (выпал ли лут, и если выпал, то какой). Статистика внизу экрана показывает количество существ, с которых выпал лут, и количество предметов каждого типа).
Этот пример показывает логику функции и демонстрирует тот факт, что эту систему можно применить для игры любого жанра.
Давайте разберемся, как работает демка. Во-первых, существа и предметы содержатся в массивах. Вот массив beast
:
Номер (X) |
Название (Y-0) |
Вероятность выпадения (Y-1) |
Редкость предмета (Y-2) |
0 | Кабан | 100 | 100 |
1 | Гоблин | 75 | 75 |
2 | Сквайр | 65 | 55 |
3 | Зог-Зог | 45 | 100 |
4 | Сова | 15 | 15 |
5 | Мастодонт | 35 | 50 |
А вот массив drops
:
Номер (X) |
Название (Y-0) |
Редкость предмета (Y-1) |
0 | Леденец | 75 |
1 | Золото | 50 |
2 | Камни | 95 |
3 | Бриллиант | 25 |
4 | Благовония | 35 |
5 | Снаряжение | 15 |
Величина X
— это уникальный идентификатор существа или предмета. Например, существо номер 0 — это Кабан. Предмет номер 3 — это Бриллиант.
Эти массивы мы используем как справочник. В массиве существ есть ещё два столбца:
- Вероятность падения определяет шанс дропа лута после смерти существа. Например, с Кабана точно что-то выпадет, а вот с Совы — только с вероятностью 15%.
- Редкость предмета определяет степень ценности выпадаемого предмета (чем больше значение, тем менее ценным является предмет — так уж я написал функцию).
Это удобно с точки зрения баланса: мы не хотим дать игроку много топовых предметов в начале игры, ведь так он потеряет интерес.
Эти таблицы — лишь пример, и вы можете и должны настроить их самостоятельно, подогнав под свою систему. Если вы хотите узнать больше о балансе в играх, ознакомьтесь с этой серией статей (прим перев.: мы уже переводили одну статью, посвящённую балансу в играх, можете прочитать и её).
Давайте взглянем на псевдокод демки:
CONSTANT BEAST_NAME = 0
CONSTANT BEAST_DROPRATE = 1
CONSTANT BEAST_RARITY = 2
CONSTANT DROP_NAME = 0
CONSTANT DROP_RATE = 1
//Those constants are used for a better readability of the arrays
On start of the project, fill the arrays with the correct values
array aBeast(6,3) //The array that contains the values for each beast
array aDrop(6,2) //The array that contains the values for each item
array aTemp(0) //A temporary array that will allow us what item type to drop
array aStats(6) //The array that will contain the amount of each item dropped
On button clicked
Call function "SlainBeast(100)"
Function SlainBest (Repetitions)
int BeastDrops = 0 //The variable that will keep the count of how many beasts did drop item
Text.text = ""
aStats().clear //Resets all the values contained in this array to make new statistics for the current batch
Repeat Repetitions times
int BeastType
int DropChance
int Rarity
BeastType = Random(6) //Since we have 6 beasts in our array
Rarity = aBeast(BeastType, BEAST_RARITY) //Get the rarity of items the beast should drop from the aBeast array
DropChance = ceil(random(100)) //Picks a number between 0 and 100)
Text.text = Text.text & loopindex & " _ " & aBeast(BeastType,BEAST_NAME) & "is slain"
If DropChance > aBeast(BeastType,BEAST_DROPRATE)
//The DropChance is bigger than the droprate for this beast
Text.text = Text.text & "." & newline
//We stop here, this beast is considered to not have dropped an item.
If DropChance <= aBeast(BeastType,BEAST_DROPRATE)
Text.text = Text.Text & " dropping " //We will put some text to display what item was dropped
//On the other hand, DropChance is less or equal the droprate for this beast
aTemp(0) //We clear/clean the aTemp array in which we will push entries to determine what item type to drop
For a = 0 to aDrop.Width //We will loop through every elements of the aDrop array
aDrop(a,DROP_RATE) >= Rarity //When the item drop rate is greater or equal the expected Rarity
Push aTemp,a //We put the current a index in the temp array. We know that this index is a possible item type to drop
int DropType
DropType = random(aTemp.width) //The DropType is one of the indexes contained in the temporary array
Text.text = Text.text & aDrop(DropType, DROP_NAME) & "." & newline //We display the item name that was dropped
//We do some statistics
aStats(DropType) = aStats(DropType) + 1
BeastDrops = BeastDrops + 1
TextStats.Text = BeastDrops & " beasts dropped items." & newline
For a = 0 to aStats.width //Display each item amount that was dropped
and aStats(a) > 0
TextStats.Text = TextStats.Text & aStats(a) & " " & aDrop(a,DROP_NAME) & " "
Начнём с пользовательского действия: нажатия на кнопку. Кнопка вызывает функцию с параметром 100. В реальности вы скорее всего будете убивать врагов по одному 🙂
После этого вызывается функция SlainBeast
. Её задачей является вывод сообщения для игрока. Она очищает переменную BeastDrops
и массив aStats
, которые используются для статистики. В реальной игре они вам не понадобятся. Функция также очищает Text
, чтобы для вывода результата использовались 100 новых строк. В самой функции создаются три переменные: BeastType
, DropChance
и Rarity
.
BeastType
будет номером, по которому мы будем обращаться к строке в массиве aBeast
; по сути, это тип существа. Rarity
также берется из массива aBeast
; это редкость выпавшего предмета, и значение мы будем брать из поля “Редкость предмета” массива aBeast
.
И наконец, DropChance
— это случайное число от 0 до 100. В большей части языков программирования есть функция, возвращающая случайное число от 0 до 1, мы просто умножим его на 100.
Теперь мы можем выводить информацию в объект Text
: нам уже известен тип существа. Поэтому мы конкатенируем значение Text.text
и BEAST_NAME
текущего BeastType
из массива aBeast
.
Затем нам нужно определить, выпал ли предмет. Для этого мы сравним DropChance
с величиной BEAST_DROPRATE
из массива aBeast
. Если DropChance
меньше или равен этой величине, предмет выпал.
Итак, процесс выпадения предмета задаётся двумя строками. Первая:
DropChance > aBeast(BeastType,BEAST_DROPRATE)
В этом случае DropChance
больше DropRate
, и это значит, что предмет не выпал. После этого мы выводим только завершающую предложение “[BeastType] was slain.” точку и переходим к следующему существу..
Вторая:
DropChance <= aBeast(BeastType,BEAST_DROPRATE)
В этом случае DropChance
меньше или равен DropRate
текущего BeastType
, поэтому мы считаем, что предмет должен выпасть. Для этого мы сравниваем редкость предмета, которая задана текущему существу, и несколько значений редкости, заданных в таблице aDrop
.
Мы просматриваем всю таблицу aDrop
, занося каждый подходящий номер в массив aTemp
. В результате там должен быть как минимум один номер. После этого мы создаём переменную DropType
, которая случайным образом выбирает один из номеров в массиве aTemp
; это и есть выпавший предмет.
Мы добавляем имя предмета в предложение, которое становится таким: “BeastType
was slain, dropping a DROP_NAME
.”. Затем мы изменяем статистику (в массиве aStats
и в BeastDrops
).
Наконец, после 100 повторов, мы выводим статистику, количество существ, с которых выпал лут, и количество предметов каждого типа.
Ещё один пример: отображаем предметы
Давайте разберем ещё один пример:
Нажмите Пробел для запуска фаербола, который убьёт врага.
Как видно, создаётся случайный враг. Игрок может совершить дистанционную атаку. Когда снаряд попадает во врага, тот умирает.
Здесь используется аналогичная прошлой функция, но в этот раз она вдобавок создаёт визуальное отображение выпавшего предмета и изменяет статистику в нижней части экрана.
Вот псевдокод:
CONSTANT ENEMY_NAME = 0
CONSTANT ENEMY_DROPRATE = 1
CONSTANT ENEMY_RARITY = 2
CONSTANT ENEMY_ANIM = 3
CONSTANT DROP_NAME = 0
CONSTANT DROP_RATE = 1
//Constants for the readability of the arrays
int EnemiesSpawned = 0
int EnemiesDrops = 0
array aEnemy(11,4)
array aDrop(17,2)
array aStats(17)
array aTemp(0)
On start of the project, we roll the data in aEnemy and aDrop
Start Timer "Spawn" for 0.2 second
Function "SpawnEnemy"
int EnemyType = 0
EnemyType = random(11) //We roll an enemy type out of the 11 available
Create object Enemy //We create the visual object Enemy on screen
Enemy.Animation = aEnemy(EnemyType, ENEMY_ANIM)
EnemiesSpawned = EnemiesSpawned + 1
txtEnemy.text = aEnemy(EnemyType, ENEMY_NAME) & " appeared"
Enemy.Name = aEnemy(EnemyType, ENEMY_NAME)
Enemy.Type = EnemyType
Keyboard Key "Space" pressed
Create object Projectile from Char.Position
Projectile collides with Enemy
Destroy Projectile
Enemy start Fade
txtEnemy.text = Enemy.Name & " has been vanquished."
Enemy Fade finished
Start Timer "Spawn" for 2.5 seconds //Once the fade out is finished, we wait 2.5 seconds before spawning a new enemy at a random position on the screen
Function "Drop" (Enemy.Type, Enemy.X, Enemy.Y, Enemy.Name)
Function Drop (EnemyType, EnemyX, EnemyY, EnemyName)
int DropChance = 0
int Rarity = 0
DropChance = ceil(random(100))
Rarity = aEnemy(EnemyType, ENEMY_RARITY)
txtEnemy.text = EnemyName & " dropped "
If DropChance > aEnemy(EnemyType, ENEMY_DROPRATE)
txtEnemy.text = txtEnemy.text & " nothing."
//Nothing was dropped
If DropChance <= aEnemy(EnemyType, ENEMY_DROPRATE)
aTemp.clear/set size to 0
For a = 0 to aDrop.Width
and aDrop(a, DROP_RATE) >= Rarity
aTemp.Push(a) //We push the current index into the aTemp array as possible drop index
int DropType = 0
DropType = Random(aTemp.Width) //We pick what is the drop index amongst the indexes stored in aTemp
aStats(DropType) = aStats(DropType) + 1
EnemiesDrops = EnemiesDrops + 1
Create Object Drop at EnemyX, EnemyY
Drop.AnimationFrame = DropType
txtEnemy.Text = txtEnemy.Text & aDrop.(DropType, DROP_NAME) & "." //We display the name of the drop
txtStats.text = EnemiesDrops & " enemies on " & EnemiesSpawned & " dropped items." & newline
For a = 0 to aStats.width
and aStats(a) > 0
txtStats.text = txtStats.Text & aStats(a) & " " & aDrop(a, DROP_NAME) & " "
Timer "Spawn"
Call Function "SpawnEnemy"
Давайте взглянем на таблицы aEnemy
и aDrop
:
Номер (X) | Название (Y-0) | Вероятность выпадения (Y-1) | Редкость предмета (Y-2) | Название анимации (Y-3) |
0 | Healer Female | 100 | 100 | Healer_F |
1 | Healer Male | 75 | 75 | Healer_M |
2 | Mage Female | 65 | 55 | Mage_F |
3 | Mage Male | 45 | 100 | Mage_M |
4 | Ninja Female | 15 | 15 | Ninja_F |
5 | Ninja Male | 35 | 50 | Ninja_M |
6 | Ranger Male | 75 | 80 | Ranger_M |
7 | Townfolk Female | 75 | 15 | Townfolk_F |
8 | Townfolk Male | 95 | 95 | Townfolk_M |
9 | Warrior Female | 70 | 70 | Warrior_F |
10 | Warrior Male | 45 | 55 | Warrior_M |
Номер (X) | Название (Y-0) | Редкость предмета (Y-1) |
0 | Apple | 75 |
1 | Banana | 50 |
2 | Carrot | 95 |
3 | Grape | 85 |
4 | Empty potion | 80 |
5 | Blue potion | 75 |
6 | Red potion | 70 |
7 | Green potion | 60 |
8 | Pink Heart | 65 |
9 | Blue pearl | 15 |
10 | Rock | 100 |
11 | Glove | 25 |
12 | Armor | 30 |
13 | Jewel | 35 |
14 | Mage Hat | 65 |
15 | Wood shield | 85 |
16 | Iron axe | 65 |
В отличие от прошлого примера, массив, содержащие данные о врагах, называется aEnemy
и содержит ещё один столбец данных, в котором хранятся названия анимаций. Таким образом, при появлении врага автоматически создаётся его анимация.
aDrop
теперь содержит 16 элементов, и каждый номер относится с анимации объекта — но я также мог создать отдельную анимацию для каждого предмета.
В этот раз врагов у нас гораздо больше. Тем не менее, единственным изменением является то, что мы разделили появление врагов и функцию, определяющую наличие выпавшего лута. Вы сделали это, потому что в реальной игре враги не будут просто ждать, пока их убьют.
Теперь у нас есть функция SpawnEnemy
и функция Drop
. Drop
очень похожа на вариант из прошлого примера, но теперь принимает несколько параметров: координаты врага на экране (там будет отображён предмет), тип врага и его название из таблицы aEnemy
.
Логика функции Drop
не изменилась; мы добавили лишь способ отображения информации.
Замечание: для размещения врагов в разных местах экрана я использовал невидимый объект
Spawn
, постоянно двигающийся влево-вправо. При вызове функцииSpawnEnemy
она создаёт врага в текущих координатах объектаSpawn
.
Осталось только решить, когда вызывать Drop
. Я запускаю её не сразу после смерти врага, а после исчезновения его модели. Вы же можете настроить это на свой вкус.
Заключение
На уровне гейм-дизайна, наличие системы лута даёт игроку стимул сражаться с врагами для получения усилений, снаряжения и выполнения задач, прямым или косвенным образом.
На уровне реализации, выпадение предметов задано функцией, проверяющей уровень редкости предмета в зависимости от убитого врага и отображающей нужную информацию на экране. Необходимые данные хранятся в структурах данных наподобие массивов, которые доступны функции.
Не смешно? А здесь смешно: @ithumor