Цель этой статьи собрать различные техники и фишки, позволяющие создать мультиверсионную карту полноценно работающую на клиенте с любым актуальным патчем (в частности, 1.26, 1.31, 1.32+ Reforged SD/HD). Показать, как внедрять механики из последних патчей, не теряя совместимости со старым клиентом.
Если у вас есть замечания по оптимальности описанных методов или другие предложения, то обязательно напишите об этом. Будем вместе дополнять статью.
Оглавление
Основы. Узнаём версию клиента
Всё описанное далее актуально для создания мультиверсионных карт в редакторе патча 1.26. Карты, созданные в более новых патчах, не будут работать в старых клиентах.
Для начала, полезно будет узнать текущую версию клиента (1.32, 1.31 или более старая версия). Для этого используются следующие функции:
//True, если версия клиента 1.32+
function Is132OrNewer takes nothing returns boolean
return GetLocalizedString("REFORGED") != "REFORGED"
endfunction
//True, если версия клиента 1.31+
function Is131OrNewer takes nothing returns boolean
return GetLocalizedString("WINDOW_MODE_WINDOWED") != "WINDOW_MODE_WINDOWED"
endfunction
Также, при игре на 1.32+ Reforged важно знать текущий графический режим (SD или HD). Для этого можно воспользоваться фактом, что у некоторых аудиофайлов разная длина в этих двух версиях:
function IsHD_Default takes nothing returns boolean
return GetSoundFileDuration("sound\\buildings\\death\\ancientuprootdeath1.flac")==4631
endfunction
Тем не менее, предпочтительнее использовать вместо стандартного аудиофайла заранее импортированную пару. Это связано с тем, что некоторые игроки ставят модификации на замену аудиофайлов (в частности, очень популярен мод на "Возвращении старой русской озвучки"). И этот мод конфликтует с этим методом. Импортируйте файл_1 с полным путём "Check_SD_HD.mp3" и файл_2 с полным путём "_HD.w3mod\Check_SD_HD.mp3".
//True, если у локального игрока включена HD графика
function IsHD takes nothing returns boolean
return GetSoundFileDuration("Check_SD_HD.mp3")==144
endfunction
Внимание! При игре по сети возможно, что у разных игроков будет включён различный режим графики. Используйте только локальный код (без создания новых объектов) при проверках на графический режим.
function GetVersion takes nothing returns nothing
local string s
if Is132OrNewer() then
set s="1.32+ Reforged"
elseif Is131OrNewer() then
set s="1.31"
else
set s="1.26-1.30.2"
endif
set s="Current version - "+s
if IsHD() then
set s=s+" (HD)"
else
set s=s+" (SD)"
endif
call DisplayTextToPlayer(GetLocalPlayer(),0,0,s)
endfunction
Карта-пример (12.6 КБ) с определением версии игры и результаты её запуска на разных клиентах:
Импорт HD моделей. Альтернативные модели
Для импорта HD-версий моделей достаточно добавить к полному пути префикс "_HD.w3mod\". Аналогично импортируются текстуры.
При замене стандартных моделей стоит учесть, что у некоторых юнитов в HD режиме меняется путь к модели (поле file в Редакторе Объектов) и масштаб (scale), в случаях, если в эти поля не были внесены изменения. Используя этот факт можно проводить замену моделей только для одного из режимов, используя в другом версию по-умолчанию. К примеру, юнит Мечник (hhes) в SD режиме имеет модель человеческого капитана "units\human\TheCaptain\TheCaptain.mdx", а в HD эльфийского солдата "Units\Human\HighElfSwordsman\HighElfSwordsman.mdx".
Если же в поле масштаба внести изменение, то может получиться так, что юниты со стандартными моделями получаются сильно несоразмерными в SD и HD режимах. Это связно с тем, что несоразмерен масштаб самих моделей.
И изменение поля scale при переходе из SD в HD как раз компенсировало эту несоразмерность. В этих случаях можно задать необходимый размер юнита в коде карты. Это не вызывает рассинхронизацию.
function Rescale takes unit u, real sclSD, real sclHD returns nothing
local real scl
if IsHD() then
set scl=sclHD
else
set scl=sclSD
endif
call SetUnitScale(u,scl,scl,scl)
endfunction
Карта-пример (4.15 МБ) с новой импортированной SD и HD моделью; только с изменённой SD моделью; с изменением масштаба юнита в зависимости от режима графики:
Исполнение новых функций через Preloader
Внимание! Описанный ниже метод подходит только для версии 1.32+ Reforged. Он не работает для 1.31.
К версии 1.32+ Reforged в Warcraft было добавлено много новых native-функций. Но при попытке обратиться к ним напрямую в редакторе 1.26 происходит ошибка компиляции (так как они не описаны в текущих common.j и blizzard.j). Обойти эту ошибку можно использовав следующую команду:
call Preloader("CustomPreloader.pld")
Эта команда выполняет функцию PreloadFiles() из импортированного и предварительно созданного файла предзагрузки "CustomPreloader.pld" (название файла произвольно). Воспользуемся здесь тем свойством этой команды, что при синтаксической ошибке в этой функции она просто игнорируется, а не вызывает вылет игры с ошибкой. Использовать эту команду можно в любой момент игры. Разрешено иметь несколько различных .pld-файлов.
Внутри функции PreloadFiles() нельзя ссылаться на пользовательские функции из карты, а также на глобальные переменные. Разрешено использование локальных переменных.
В качестве примера, используем функцию BlzSetAbilityIcon() для изменения иконки способности Самопожертвование (Adtn) у Светлячка, когда игра запущена в версиях 1.32+ Reforged. С помощью блокнота создаём новый файл с названием и разрешением "CustomPreloader.pld" и вставляем туда следующий текст:
function PreloadFiles takes nothing returns nothing
call BlzSetAbilityIcon('Adtn',"ReplaceableTextures\\CommandButtons\\BTNSacrifice.blp")
endfunction
Импортируем этот .pld-файл в карту. Вызываем выполнение этой команды через call Preloader("CustomPreloader.pld").
Карта-пример (13 КБ) с вызовом новой native-функции через Preloader:
Работа с фреймами
Для работы с фреймами в рамках мультиверсионной карты воспользуемся тем свойством функции Preloader(), что при вызове она наследует поток исполнения. И хотя внутри функции PreloadFiles() мы не можем обратиться к глобальным переменным или другим пользовательским функциям, но через функцию GetTriggeringTrigger() разрешено получить триггер, вызвавший исходный поток.
В качестве примера создадим кнопку, выводящую всем игрокам простой текст по нажатию.
Подготовим файл ButtonExample.pld, в котором будет создаваться элемент-кнопка
function PreloadFiles takes nothing returns nothing
local trigger trig = GetTriggeringTrigger()
local framehandle mainButton =BlzCreateFrame("ScriptDialogButton", BlzGetOriginFrame(ORIGIN_FRAME_GAME_UI,0), 0,0)
call BlzFrameSetSize(mainButton, 0.12,0.05)
call BlzFrameSetAbsPoint(mainButton, FRAMEPOINT_CENTER, 0.3,0.3)
call BlzFrameSetText(mainButton, "Hello, world!")
call BlzTriggerRegisterFrameEvent(trig, mainButton, FRAMEEVENT_CONTROL_CLICK)
set trig=null
set mainButton=null
endfunction
В коде карты прописываем функцию ButtonTrigger_Init():
globals
trigger udg_trig
triggeraction udg_trigact
endglobals
//Функция с действиями по нажатию кнопки
function ButtonTrigger_ClickAction takes nothing returns nothing
call DisplayTextToPlayer(GetLocalPlayer(),0,0,"Hello, world!")
call Preloader("ButtonResetFocus.pld") //Исправление залипания фокуса на кнопке после нажатия
endfunction
//Функция создания кнопки
function ButtonTrigger_CreateButton takes nothing returns nothing
call TriggerRemoveAction(udg_trig,udg_trigact) //Уберём с триггера действие по созданию кнопки
set udg_trigact=null
call Preloader("ButtonExample.pld")
call TriggerAddAction(udg_trig,function ButtonTrigger_ClickAction) //Добавим действие по нажатию кнопки
endfunction
//Создаём и запускаем триггер
function ButtonTrigger_Init takes nothing returns nothing
set udg_trig=CreateTrigger() //Создаём триггер. В итоге он будет запускаться при нажатии кнопки.
set udg_trigact=TriggerAddAction(udg_trig,function ButtonTrigger_CreateButton) //Понадобиться только для первонального создания кнопки.
call TriggerExecute(udg_trig)
endfunction
Дополнительно, для исправления фичи (или бага?) с залипанием фокуса на кнопке после нажатия, создадим и импортируем файл ButtonResetFocus.pld:
function PreloadFiles takes nothing returns nothing
call BlzFrameSetEnable(BlzGetTriggerFrame(), false)
call BlzFrameSetEnable(BlzGetTriggerFrame(), true)
endfunction
Карта-пример (15 КБ) с действующей кнопкой:
Превью и миникарта
Обычной проблемой карт созданных в редакторе 1.26 является неотображение картинки-превью в Reforged, так как стандартный метод с импортом файла "war3mapPreview.tga" не работает в новом клиенте. Роль превью теперь здесь играет изображение игровой карты, которое можно задать импортом файла с именем и форматом "war3mapMap.dds".
Далее, для того, чтобы сделать обратную замену preview на истинное изображение карты, при инициализации вызывается функция BlzChangeMinimapTerrainTex(). В картах, сделанных в старых редакторах, это можно сделать через загрузку соответствующего .pld-файла.
С помощью блокнота создаём новый файл с названием и разрешением "ChangeMinimap.pld" и вставляем туда следующий текст:
function PreloadFiles takes nothing returns nothing
call BlzChangeMinimapTerrainTex("war3mapMapTrue.dds")
endfunction
Здесь "war3mapMapTrue.dds" - полный путь к файлу с истинным изображением игровой карты, который необходимо дополнительно импортировать (название файла произвольно). Файл "ChangeMinimap.pld" сохраняем и импортируем в карту. Добавляем "Preloader("ChangeMinimap.pld")" в функцию инициализации:
function Trig_MapInit_Actions takes nothing returns nothing
call Preloader( "ChangeMinimap.pld" )
endfunction
Карта-пример (69 КБ) с рабочей миникартой и превью в разных версиях клиента:
Анимируем портреты в Reforged HD
Одной из проблем при запуске старых карт в Reforged HD является отсутствие эффекта движения губ во время роликов у портретов юнитов со стандартными HD моделями. Это связано с тем, что в Reforged HD предусмотрена синхронизация движения губ и воспроизведения звуковых дорожек. Отсутствие как самих специальных файлов с набором движения губ, так и привязки их к соответствующим звуковым дорожкам, приводят к тому, что юниты вообще перестают открывать рты, даже если параллельно идёт озвучка.
Оставим за рамками этой статьи описание способа, как самостоятельно создать файл, описывающий правильное движение губ для конкретных аудиодорожек (например, с помощью пакета FaceFX Studio). Но покажем, как можно подключить произвольную (не связанную с аудиодорожкой) анимацию движения губ, используя редактор 1.26.
За основу порядка движения губ возьмём набор анимации Акамы из 5 миссии компании Альянса из The Frothen Throne. Эта анимация длиться довольно долго (41.2 с в русской версии и 34.7 с в английской) и она совместима со всеми основными гуманоидными портретами.
Создаём в блокноте и после импортируем в карту файлы conversation.json (название файла строгое) и LipsStart.pld (название файла произвольно) со следующим содержанием:
{
"stringTablePath": "war3map.wts",
"conversation": {
"All_conversations": [
{
"conversationOrder": 0,
"animationLabel": "A05Akama11",
"animationGroupLabel": "Map-DranaiAkama",
"animationSetFilepath": "sound\\dialogue\\faceanimation\\humanx05\\facialanimation\\dranaiakama.animset_ingame",
"animationSetFilepathMapRelative": true
}
]
}
}
function PreloadFiles takes nothing returns nothing //41.225 - Rus; 34.709 - Eng
local sound soundhandle=CreateSound("sound\\ambient\\doodadeffects\\elevator.flac", false, false, false, 10, 10, "DefaultEAXON")
call SetSoundFacialAnimationLabel(soundhandle, "A05Akama11")
call SetSoundFacialAnimationGroupLabel(soundhandle, "Map-DranaiAkama")
call SetSoundFacialAnimationSetFilepath(soundhandle, "sound\\dialogue\\faceanimation\\humanx05\\facialanimation\\dranaiakama.animset_ingame")
call SetSoundChannel(soundhandle,36)
call SetSoundVolume(soundhandle,0)
call StartSound(soundhandle)
call KillSoundWhenDone(soundhandle)
set soundhandle=null
endfunction
Здесь за основу soundhandle может быть выбран любой звук произвольной длины. Если нужно добавить аудиодорожку с реальной озвучкой персонажа, то её можно запустить параллельно используя другой звуковой канал. Теперь при фоновом запуске звука soundhandle (через функцию Preloader("LipsStart.pld") ) текущий отображаемый портрет начнёт шевелить губами.
call CinematicModeBJ( true, GetPlayersAll() )
call DoTransmissionBasicsXYBJ('hrif', GetPlayerColor(Player(0)),0, 0, null, "Sniper", "Hello, Worker!", 7.0)
call Preloader( "LipsStart.pld" )
call PolledWait( 7.00 )
call DoTransmissionBasicsXYBJ( 'hpea', GetPlayerColor(Player(1)),0, 0, null, "Worker", "Hello, Sniper!", 7.0)
call Preloader( "LipsStart.pld" )
call PolledWait( 7.00 )
call CinematicModeBJ( false, GetPlayersAll() )
Карта-пример (14 КБ) с диалогом:
Два стула и табуретка. Совместимость с патчем 1.31
Хотя клиент 1.31 пользуются меньшей популярностью чем 1.26 или актуальный Reforged, здесь есть своя ненулевая аудитория. Если вам нужна совместимость с этим патчем, необходимо учесть некоторые особенности.
В 1.31 существует баг, проявляющийся после загрузок сохранённых игр. В этом случае перестают работать почти все раннее добавленные события триггеров типа Specific Unit Event (EVENT_UNIT_SPELL_EFFECT, EVENT_UNIT_USE_ITEM и т.п.) Из подобных остаются работать всего несколько (EVENT_UNIT_DAMAGED, EVENT_UNIT_DEATH).
Если ваша карта не односессионная, то необходимо либо использовать только события типа Generic Unit Event (EVENT_PLAYER_UNIT_SPELL_EFFECT, EVENT_PLAYER_UNIT_USE_ITEM и т.п.). Либо отслеживать событие загрузки игры (EVENT_GAME_LOADED) и добавлять заново потерянные события.
Статья обновлена. Добавлена глава Работа с фреймами.
Ред. SNART
О май гад, какие же потрясающие костыли! Хорошо что я на модинг варкрафта положил глаз еще давным давно. Вспоминаю кастылестроение в сладких снах.
Ред. tysch_tysch