Уроки

Скриптинг Dota 2 — руководство для начинающих

Вторая часть статьи  С чего начать создание мода Dota 2? 

В этом уроке мы попытаемся объяснить основы программирования пользовательских модов (кастомок/аддонов) Dota 2.

Скриптинг

Для начала вам понадобится уже созданная кастомная игра, с которой вы уже провели некоторые опыты. Самое время начать учиться программировать на Lua и вообще разобраться со всем этим дерьмом.

Зайдите в

dota 2 beta\game\dota_addons\название_вашего_мода\scripts

 
Две основные папки cо скриптами — это NPC и vscripts. Первая может иметь следующие файлы с расширением .txt (если таковых нет, их можно создать самому):

  • npc_abilities_custom.txt — содержит все измененные и созданные способности в пользовательской игре.
  • npc_heroes_custom.txt — герои со своими способностями и статистикой.
  • npc_items_custom.txt — способности предметов, которые носят в инвентаре.
  • npc_units_custom.txt — все данные для не-героев единиц, здания или существ.
  • npc_abilities_override.txt  — измененные Dota 2 способности / детали с измененными значениями.
  • herolist.txt — список героев, доступных для выбора.

Эти файлы используют систему KeyValues ​​(KV, ключ-значение) и являются ядром в системе DataDriven. Это таблицы, содержащие всевозможные данные и они помогают движку Source 2 понять, что есть что. В этих файлах очень простой синтаксис. Всё что там есть — это таблицы (начинаются и заканчиваются фигурными скобочками), в которых содержатся ключ и его параметр (значение).

KV задают базовые параметры способностей, предметов, юнитов. А вот с Lua, уже будет немного сложней.

Каждый .txt файл содержит свои особые значения key-value, и когда начинается игра, движок Source 2 будет загружать их. Например, чтобы понять, какой юнит с какими параметрами должен заспавниться. Изменения в этих файлах не вступят в силу до тех пор, пока игра не начнется снова (только после перезапуска), так что будьте очень аккуратны с синтаксисом, в случае лишней или отсутствующей скобки: »  {  } , все KeyValues, которые идут после этой ошибки не будут учтены. KV чувствительны к регистру, поэтому также обратите внимание, чтобы вы написали всё правильно, и движок не выдаст ошибку.

На этом подготовку закончим. Сейчас самое время, чтобы попробовать написать Dota 2 скрипт (сценарий). Лучшим редактором я считаю Sublime Text Editor с этими 2 сниппетами (плагинами), которые добавляют дополнения для некоторых часто используемых функций и дают подсветку синтаксиса для KV и Lua.

Это будет просто вводный пример системы DataDriven, чтобы понять, что и как устроено, где что нужно менять.

Создайте новый документ в Sublime Text Editor и убедитесь, что вы используете Dota KV синтаксис (нажмите Ctrl + Shift + P и впишите Dota KV, чтобы выбрать его быстро).

Мы будем делать очень простую способность, которая наносит один урон для одной цели. Начните с написания имени способности между « « и без пробелов. Затем напишите BaseClass… и нажмите Enter, чтобы вставить авто завершение (вот зачем нужны были сниппеты выше). Перемещайтесь с помощью клавиши Tab.

«BaseClass» имеет важное значение для каждого DataDriven определения, заставляет игру интерпретировать, что это способность DataDriven. Предметы, юниты и герои имеют свои собственные базовые классы (BaseClass).

AbilityTextureName — иконка вашей способности или любое внутреннее название способности доты, например lina_laguna_blade.

Другое важное KV — AbilityBehavior, запишите AbilityB и используйте автозаполнение

Теперь нам нужно событие (event) способности — это триггер, когда определенное событие происходит с владельцем способности. Самый основной из них — OnSpellStart, добавьте его с автозаполнением, и вы увидите новый «уровень» созданным в {  }, это называется блок. В [ДЕЙСТВИЯХ], напишите «Damage» действие, какой-нибудь ключ и %AbilityDamage. Знак процента % представляет значение, которое будет принято где-то еще, в этом случае, в ключе-значении AbilityDamage. Добавить этот последний ключ, и первая основа заклинания должна выглядеть так:

"test_ability"
{
        "BaseClass"             "ability_datadriven"
        "AbilityTextureName"    "lina_laguna_blade"
        "MaxLevel"                      "1"

        "AbilityBehavior"       "DOTA_ABILITY_BEHAVIOR_UNIT_TARGET"
        "AbilityUnitTargetTeam" "DOTA_UNIT_TARGET_TEAM_ENEMY"
        "AbilityUnitTargetType" "DOTA_UNIT_TARGET_HERO | DOTA_UNIT_TARGET_BASIC"
        "AbilityUnitDamageType" "DAMAGE_TYPE_MAGICAL"

        “AbilityDamage”         "500"

        "OnSpellStart"
        {
                "Damage"
                {
                        "Target"        "TARGET"
                        "Type"          "DAMAGE_TYPE_MAGICAL"
                        "Damage"        "%AbilityDamage"
                }
        }
}

Теперь, эту способность нужно добавить ​​в npc_abilities_custom.txt файл для героя или юнита, чтобы иметь возможность использовать её.

После добавления test_ability, которую вы только что создали, пора добавить способность и герою. В папке вашего мода есть папка heroes, которая имеет различные файлы с описанием способностей героя. Откройте любого героя и измените стандартную способность на test_ability.

Всякий раз, когда вам необходимо создать «маникен» (в игрострое это dummy unit), используйте в консоли -createhero (unit_name) enemy.

unit_name — имя, доступного для пика, героя или любое имя юнита, который также доступен. Можно использовать сокращенные имена, например «ancient», а не «ancient_apparition». Быстрая команда -createhero kobold enemy создаст противника — нейтрала кобольда. Полное имя юнита «npc_dota_neutral_kobold», но короткая команда будет работать. Вы также можете отключить перезарядки способностей путем написания -wtf (и -unwtf будет включать её).

Обширную документацию и углубленные примеры системы DataDriven можно найти на страницах Dota 2 Workshop Tools Wiki.


Lua скриптинг

Возвращаясь к папке game/scripts, мы увидим папку vscripts. Это как раз то место, где находятся все скрипты Lua. Lua довольно легко изучить и синтаксис очень прост. По-большей степени, программирование в Dota, на Lua — это просто знания готовых функций API, которые необходимо использовать (подробнее об этом позже).

Вот 4 главных разработок на Lua в Dota:

  • Игровая Логика
  • DataDriven RunScript
  • Hammer I/O (ввод / вывод)
  • Кастомный UI (пользовательский интерфейс)

Игровая Логика — Структура

В каждой пользовательской игре, должен обязательно присутствовать файл с именем addon_game_mode.lua . По крайней мере, до тех пор, пока логика игры находится именно в этом файле (и в самом деле, Valve сделали точно также в своём примере holdout), рекомендуется, чтобы вы использовали в этом файле только эти 3 функции:

  • Require (подключение файлов), через нее подключаем все необходимые файлы, которые будут использоваться в игровой логике, располагаются как библиотеки, то есть все функции внутри этих файлов можно использовать в любой точке.
  • Precache (предварительно кэшированные), когда игра начинается и игроки забирают своих героев, движок будет пытаться загрузить связанные модели / частицы / звуки этих героев. Если мы динамически используем ресурсы в Lua, перед предзагрузкой — то скорей всего будет какие-либо ошибки.
  • Activate , создает основу пользовательской игры и вызывает функцию инициализации.
Функция Precache была свёрнута в Sublime Text Editor (там где троеточие).

После того как выполнится Precache и Activate, первая функция, которая выполнится в файле lua — GameMode: InitGameMode ().

И здесь начинается игра с инициализацией всех видов правил и функций, которые зарегистрированы во всех GameRules и GameMode файлах. Для этого, многие переменные определены на начало файла, чтобы нормально организовать параметры, такие как настройки золота, убийства, кастомные уровни, и т.д.

Это синтаксис функции, примененной на GameRules, с одним параметром BOOL:

GameRules: SetHeroRespawnEnabled (ENABLE_HERO_RESPAWN)

Так же, как KV, Lua чувствителен к регистру. Расположение функций в основном файле Lua как правило, не имеет значения. Все строки скрипта внутри вызова функции будет выполняться один за другим, потенциально в том же кадре; один кадр в Dota — 1/30 секунды.

Обратите внимание на использование : двоеточия перед функцией. В Lua, от этого зависит доступ функции API игры. Мы говорим, что GameRules является HScript или handle (дескриптор — число, с помощью которого можно идентифицировать объект) . Дескрипторы в основном огромные таблицы, со всей соответствующей информацией. На странице Scripting API вы увидите много различных типов функций, которые можно использовать различные дескрипторы.

Глобальным функциям не нужны префиксы : в дескрипторах. Герои, юниты, способности и предметы имеют свои различные классы дескрипторов и пытаясь вызвать функцию несовместимого класса вызовет ошибку VScript — розовый текст в консоли и красным текстом на игровом экране.


Консоль

Вы можете получить доступ к игровой консоли, нажав клавишу `.

Это обеспечит тонну полезной информации для отладки. Различные цвета представляют различные «каналы» информации. По умолчанию все каналы находятся в том же журнале: вкладка по умолчанию. Очень рекомендуется делать свои собственные вкладки, чтобы разделять просмотр журнала.

В основном для Lua скриптинга, нам понадобится вкладка VScript. Сообщения о системе DataDriven в General Channel, в желтом канале что-то ещё, сделайте отдельный просмотр для него тоже.

img

Новые вкладки:

Консоль будет уведомлять, когда происходит ошибка в скрипте Lua, либо когда игра загружается (ошибка синтаксиса компиляции) или во время выполнения. В этой ошибке, я написал GameRules.SetHeroRespawnEnabled с   .   вместо   :   

img

Вы можете проследить ошибку и попытаться ее решить, прописать script_reload в консоли, чтобы перезагрузить скрипт (подгрузить пересохраненный файл) и проверить, была ли она исправлена.

Синтаксическая ошибка DataDriven, как правило, выглядит следующим образом:

img


События в пользовательской игре Source 2

Вторым сегментом функции InitGameMode являются Слушатели:

ListenToGameEvent('dota_player_gained_level', Dynamic_Wrap(GameMode, 'OnPlayerLevelUp'), self)

Структура этого ListenToGameEvent читается так:

Всякий раз, когда событие dota_player_gained_level срабатывает, выполнить функцию OnPlayerLevelUp (всё то что описано в ней).

OnPlayerLevelUp и GameMode просто имена функций и основного класса, как правило, вам не нужно беспокоиться о них, это просто слушатели. Dynamic_Wrap — функция для того, чтобы script_reload команда также перезагружала слушателей. script_reload перезагружает Lua скрипты во время игры, в отличие от DataDriven файлов, которые требуют, чтобы игра была полностью перезапущена заново.

3-й и последний главный элемент в InitGameMode, который сам определяет переменные для отслеживания информации. Они используют приставку self., которая является локальной ссылкой на GameMode, сквозь все функции внутри основного файла Lua. Добавление информации к объекту entity. называется «индексация» и в основном добавляет еще одну запись к большой таблице этого объекта. Это очень полезно, потому что эта информация хранится в дескрипторе объекта и видны повсюду (можно использовать везде), и ничего не поменяется, пока мы не переназначим или уничтожим его.

Достаточно теории, давайте посмотрим, как все это приходит. Мы добавим несколько простых строк сценария в OnNPCSpawned функции, которая является слушателем к npc_spawned и вызывается каждый раз когда спавнится юнит или герой на карте.

Давайте проанализируем содержание OnNPCSpawned функции по умолчанию:

-- NPCшка заспавнилась где-то на карте. Это также работает и для героев
function GameMode:OnNPCSpawned(keys)
        print(" NPC Spawned")
        DeepPrintTable(keys)
        local npc = EntIndexToHScript(keys.entindex)

        if npc:IsRealHero() and npc.bFirstSpawned == nil then
                npc.bFirstSpawned = true
                GameMode:OnHeroInGame(npc)
        end
end

Первая команда print будет печатать всё что находится » « в VConsole. Функция print родная в Lua, и принимает несколько параметров, разделенных запятыми, и конкатенацию (взаимную связь) строк с «..» , как это:

print("[BAREBONES]".."NPC","Spawned")

DeepPrintTable является глобальной Valve-игр функцией, которая будет отображать информацию с таблицей произошедшего. Для ключей в данном случае, это будет .entindex и .splitscreenplayerИндекс объекта является очень важным номером для ссылки на объект. Игнорируйте splitscreenplayer, это просто остатки старого движка Source, они никогда не использовались в Dota 2.

img

Следующая строка определяет локальную переменную. В Lua объем локальных переменных в блоке, в котором они были объявлены, ограничивается. Это хороший стиль программирования, использовать локальные переменные, только тогда, когда нужно. Локальные переменные помогут вам избежать загромождения глобальной окружающей среды с ненужными переменными и значениями. Кроме того, доступ к локальным переменным быстрее, чем к глобальным.

local npc = EntIndexToHScript(keys.entindex)

Это в основном чтение информации, которая предоставляется в событиях, и хранится в локальной переменной внутри этого вызова функции. В этом примере все Слушатели и их функции уже были обработаны, но для справки, вы всегда можете проверить In_Engine_Events вики-страницу, чтобы точно знать, какие параметры каждого события переносятся.

Локальная переменная npc HScript, типа дескриптора. Все изменения, сделанные в переменной NPC будет отражать на заспавненном юните.

Следующая строка сначала проверяет, если npc герой true (это исключает иллюзии и других юнитов), а также проверяет, если индекс.bFirstSpawned не назначен. Если оба условия выполняются, изменяется логическое значение = true и вызывается функция OnHeroInGame.

Чтобы закончить этот базовый урок Dota Lua, давайте изменим OnNPCSpawned функцию, так, что, если юнит с именем npc_dota_neutral_kobold заспавнится, подождать 1 секунду, а затем он умрёт сам по-себе:

function GameMode:OnNPCSpawned(keys)
   local npc = EntIndexToHScript(keys.entindex)

   if npc:IsRealHero() and npc.bFirstSpawned == nil then
       npc.bFirstSpawned = true
       GameMode:OnHeroInGame(npc)
   elseif npc:GetUnitName() == "npc_dota_neutral_kobold" then
       Timers:CreateTimer( 1.0 , function()
           npc:ForceKill(true)
       end)
    end
end

Здесь мы воспользуемся библиотекой Таймеров для простой односекундной задержки. Есть много различных функций таймера и их объяснения в timers.lua. BOOL на ForceKill это для того, чтобы воспроизвести анимацию смерти.


Таблицы

Таблицы наиболее важная структура, которую мы должны использовать. Как упоминалось ранее, все данные на объекты можно рассматривать как таблицу (хотя это технически указатель C ++ объекта), тем самым, вы сможете получить и установить значения по различным функциям API игры.

Есть некоторые функции в API, которые возвращают таблицу дескрипторов объектов.

Предположим, вы хотите найти все единицы порожденных (заспавненных) вблизи кобольдов и убить их. Функция FindUnitsInRadius может быть использована для этой цели, и включает в себя много параметров с разными типами, которые стоит объяснить:

table FindUnitsInRadius(int teamNumber, Vector position, handle cacheUnit, float radius, int teamFilter, int typeFilter, int flagFilter, int order, bool canGrowCache)

Параметры должны быть в таком порядке. Эта функция глобальная, поэтому не нужен handle: (дескриптор), но мы должны держать в таблице в переменные, такие как:

local units = FindUnitsInRadius(...)

Выяснить teamNumber — какая команда объекта находится в радиусе можно сделать с помощью GetTeamNumber () с помощью информации от дескриптора NPC. Что касается других параметров фильтра, вместо реальных чисел мы используем кучу констант, которые представляют различные числовые значения. Полный список констант на этой странице вики.

Вектор представляется в виде вектора (х, у, z) координат. Функция, чтобы получить позицию конкретного блока под названием GetAbsOrigin и принимает дескриптор (handle) NPC.

Что касается параметров кэша, просто поставьте ноль и false, от них не много пользы в целом.

Завершенная функция вызывающая героя в радиусе 500 от родившегося (заспавненого) кобольда будет выглядеть так:

local units = FindUnitsInRadius( npc:GetTeamNumber(), npc:GetAbsOrigin(), nil, 500, 
                                 DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_HERO, 
                     DOTA_UNIT_TARGET_FLAG_NONE, FIND_ANY_ORDER, false)

Используйте дополнительные строки разрыва, чтобы сделать его более читабельным. Теперь мы хотим сделать итерацию (цикл, перебор) объектов в этой таблице, это осуществляется следующим образом:

for key,unit in pairs(units) do
    print(key,value)
    unit:ForceKill(true)
end

Ключ, юнита с выбранным именем относится к позиции и переопределяется его ценность внутри блоков таблицы, которые будут читать в парах (не пар из чайника). Использование ‘_’, как имя ключа поможет вам, если вы хотите, чтобы было ясно, что первый параметр не будете использоваться. Второй параметр — юниты, используется для итерации дескрипторов (handle) найденных юнитов.

Существует еще одна вещь, на которую стоит обратить внимание: проблема «подождите один кадр». Потому что все юниты фактически родились в (0,0,0) координатах, а затем переехали в нужное положение. Во многих случаях вы должны будете создать второй таймер с задержкой 0.03 сек (1 кадр) для некоторых сценариев, чтобы всё работало.

Теперь, OnNPCSpawned выглядит так:

function GameMode:OnNPCSpawned(keys)
    local npc = EntIndexToHScript(keys.entindex)

    if npc:IsRealHero() and npc.bFirstSpawned == nil then
        npc.bFirstSpawned = true
        GameMode:OnHeroInGame(npc)
    elseif npc:GetUnitName() == "npc_dota_neutral_kobold" then
        Timers:CreateTimer(0.03, function() 
            local units = FindUnitsInRadius( npc:GetTeamNumber(), npc:GetAbsOrigin(), nil, 500, 
                        DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_HERO, 
                    DOTA_UNIT_TARGET_FLAG_NONE, FIND_ANY_ORDER, false)

        for key,value in pairs(units) do
            print(key,value)
            value:ForceKill(true)
            end
        end)
    end
end

 

 


DataDriven RunScript

Вторым по важности для скриптинга на Lua в Dota 2 моддинге — это «RunScript». С помощью RunScript мы выполняем код способности в виде файла .lua. Для RunScript нужно создать новый экземпляр lua-файла, поэтому глобальные переменные не стоит применять, все переменные должны быть локальными и / или назначены дескриптору.

Разделение файлов lua под каждую способность — хорошая идея, так как это также помогает разделить скрипты способностей, как с файлами txt.

Давайте вернемся к первой супер простой способности, которая будет наносить урон единичной цели (target damage), и добавим этот блок в событие способности OnSpellStart:

Синтаксис выглядит так:img

ScriptFile направляет относительно в папку /vscripts/. AbilityName — это название функции lua внутри этого файла.

Давайте вернемся к нашей простой способности (которую мы написали выше в статье), и добавим блок RunScript, а также создадим файл с кодом, чтобы подключение произошло успешно. Рекомендуется разделить все файлы Lua для каждой функции отдельно, а также создать отдельные папки для героев, юнитов, предметов и т.д. Это не обязательный пункт — на ваше усмотрение.

Добавьте блок RunScript в DD-событие — OnSpellStart. Теперь, при нажатии способности — будет подгружен файл example_script и выполнятся все строки, определенные в функции ScriptedAbility.

"RunScript"
{
    "ScriptFile"    "heroes/example_script.lua"
    "Function"      "ScriptedAbility"
}

В папке vscripts создайте папку heroes и в ней файл example_script с расширением .lua. Я рекомендую настроить эти файлы для автоматического открытия с помощью Sublime и в нем установить синтаксис Dota Lua.

Скрипт в этом примере, при срабатывании события передаст некоторую информацию первому параметру функции, который называться как угодно, но обычно это просто keys или event.

img

Внутри функции большинство скриптов возможностей начинаются с определения локальных переменных для целевых игровых объектов, которые выполнили определенные события. Основные целевые переменные, видимые на любом скрипте:

  • .caster, объект, который использовал спелл (способность, абилку).
  • .target, цель способности (в некоторых случаях может быть такой же, как и кастер)
    Пример
function ScriptedAbility( event )
    local caster = event.caster
    local target = event.target

    if target:GetHealthPercent() < 50 then
        target:Kill(nil, caster) -- Убивает этого NPC, с некоторыми параметрами
    end
end

Этот скрипт убьет юнита, на которого направлена способность, если процент здоровья составляет менее половины, и приписывает убийство тому, кто использовал эту способность (заклинателю).

Примеры скриптинга и Исходники

Существует множество примеров, расположенных в виде репозиториев на GitHub. С помощью этого руководства, теперь, вы должны понимать как скриптить логику вашей пользовательской игры. Исходников стандартных возможностей из Dota нет. Лучшим репозиторием на GitHub для поиска скриптов стандартных переписанных абиллок на lua — SpellLibrary. Это проект сообщества, в котором переписана каждая способность Dota с использованием KV и Lua.

Если вы хотите посмотреть как работают скрипты определенной кастомки, но автор не открыл исходники, выполните следующие действия:

  1. Подпишитесь на кастомную игру. Скачайте GCFScape, если у вас до сих пор нет его. Скачать GCFScape можно здесь — http://nemesis.thewavelength.net/?p=26
  2. Посмотрите на ссылку кастомной игры, исходники которой вам нужны. steamcommunity.com/sharedfiles/filedetails/?id=скопируйте_этот_номер
  3. Зайдите  папку Steam -> SteamApps -> workshop -> content -> 570 (это папка с контентом воркшопа доты)
  4. Найдите скопированный номер
  5. Откройте .vpk файл с помощью GCFScape и разархивируйте содержимое куда угодно. Теперь у вас есть доступ к папке со скриптами и закомпилинными моделями/частицами/звуками.

img

Whenever you have a doubt about how to use a particular GameAPI function, its possible to find examples all over GitHub by just writing the name of it, additionally filtering by lua like this:

Всякий раз, когда вы сомневаетесь в том, как использовать определенную функцию GameAPI, можно найти примеры по всему GitHub, просто записав имя этой функции, а также фильтруя lua следующим образом:

Главное убедиться, что это нужные файлы и они для Dota, а не другой игры 🙂 Дело в том, что названия функций API могут иметь одинаковые названия, хоть и относиться будут совсем не к Dota 2.

На этом с основами скриптинга можно завершить. Если у вас есть какие-то вопросы — вы всегда можете уточнить информацию в комментариях или на нашем форуме.

Источник: ModDota

I_Explorer

Друзья CG
30 Июл 2016
318
16
Проект
Жизнь в тюрьме
Отличный гайд, а теперь попробуй врубить темный стиль и не ослепнуть.
 
  • Нравится
Реакции: Maximus и -ExotiC-

-ExotiC-

Какой-то ноунэйм
Команда форума
11 Авг 2014
498
56
customgames.ru
Последнее редактирование:
  • Нравится
Реакции: vulkantsk

vulkantsk

Супермодератор
Команда форума
21 Июн 2017
1,149
196
www.dotabuff.com
Проект
Roshan defense
Слишком много текста , глаза ращбегаются !
Рекомендую распределить по разделам в оглавлении !
А потом засунуть в спойлеры , по моему будет легче читать или найти нужную информацию !
 

mitfleg

Новичок
25 Фев 2019
1
0
Проект
Scripts
Добрый вечер. С Lua ничего не понятно толком. Я хоть и изучал языки программирования, но таки не смог разобраться. Кто сможет помочь только вьехать в это все. По поводу функций и т.п. логически понятно, а вот куда писать код, и где брать файлы....Спасибо.
 

danilkoo832832

Пользователь
5 Фев 2016
40
3
your_name_addon\scripts\vscripts\addon_game_mode.lua писать код сюда, можно и в другие файлы, но их придётся подключать к главному. Все луа файлы должны находится в папке vscripts. Про брать файлы немного не понял, если имеешь ввиду стандартные абилки, то на луа от валв их нет, но есть от других людей SpellLibrary есть еще варианты.