Создание мутатора Цикл статей по созданию мутаторов для Killing Floor. Минимальный шаблон, типы мутаторов и объединение мутаторов.
#1
Отправлено:
Создание мутатора
Приветствуем! Это первый тематический цикл статей от Команды 2/5.
Тема цикла: Создание мутатора
- Статья 1: Минимальный шаблон мутатора и переменные GroupName, FriendlyName, Description
- Статья 2: Типы мутаторов
- Статья 3: Объединение мутаторов
Статья 1: Минимальный шаблон мутатора
Рассмотрим минимальный шаблон любого мутатора:
unrealscript
Class TestMut extends Mutator;
defaultproperties
{
GroupName="KF-TestMut"
FriendlyName="TestMut"
Description="Тестовый мутатор. Базовый шаблон"
}
Всегда ленился поглядеть, что за GroupName и FriendlyName. Вот и повод изучить.
GroupName
Итак, в общем то уже из комментария к описанию переменной в классе Engine.Mutator
unrealscript
var() cache string GroupName; // Will only allow one mutator with this tag to be selected.
становится ясно, что данная переменная используется, чтобы дать возможность заблокировать добавление мутаторов одной и той же группы. При желании можно посмотреть и реализацию этого механизма в Engine.GameInfo.AddMutator — не буду копировать сюда код, любознательные сами глянут, остальным это и не надо.
Использование:
- Страховка от добавления мутаторов реализующих одно и то же (или идейно близкие вещи)
- Страховка от добавления конфликтующих мутаторов
- Я особо этим никогда не пользовался — жду от вас идеи, как это можно использовать
Важное замечание:
В KF есть страничка добавления мутаторов в игру — KFGui.KFMutatorPage. И если мы внимательно поглядим на функцию SetCurrentGame, то мы увидим такой вот код:
unrealscript
...
for (i=0;i<MutatorList.Length;i++)
{
if ( Left(MutatorList[i].GroupName,2)== "KF")
lb_Avail.List.Add(MutatorList[i].FriendlyName,,MutatorList[i].Description);
}
...
Который означает, что в списке доступных мутаторов будут отображаться только мутаторы GroupName которых начинается на “KF”. Поэтому не забывайте писать GroupName=“KFчтототам”, если хотите, чтобы ваш мутатор был доступен в этой менюшке.
Общепринятый подход: GroupName = "KF-НазваниеМутатора"
FriendlyName
Ну тут вроде всё просто — это название мутатора, которое отображается в окошке выбора мутаторов. Название переменной намекает нам на дружелюбие. Вот есть понятие дружелюбного пользовательского интерфейса. Так и тут — сам класс мутатора может называться как угодно неудобоваримо, а в этой переменной стоит написать лаконичное и понятное название, в идеале, чтобы даже описание Description не понадобилось для среднестатистического игрока.
Вы можете писать название мутатора и кириллицей (только не забывайте переключиться в кодировку Little Endian), например:
unrealscript
FriendlyName="Тестовый мутатор"
Более того, вы можете раскрасить название мутатора, если вам уж совсем нехрен делать. Общепринятый для анриала способ “окраски” описан в различных туториалах по Unreal Engine.
Сделаем слово “Тестовый” красного цвета, а “мутатор” зелёного:
Вы можете воспользоваться программкой ServerColor, я же воспользуюсь HEX редактором (я люблю редактор Hex Editor Neo). Итак, я возьму не чистые цвета, а просто те, что мне приглянулись. Префикс для красного цвета получится 1B D1 60 60. Префикс для зелёного цвета получится 1B 3D B7 74.
Загоним эти значения в HEX редактор и получим С``` для красного цвета и =·t` для зелёного.
Получим:
unrealscript
FriendlyName="С``Тестовый =·tмутатор"
Также можно сделать слово синим цветом.
Description
Ну с этим то в общем всё понятно. Это описание мутатора, которое отображается в окошке выбора мутаторов. Опять же можно сделать на русском и цветное.
Сделаем его синим. Префикс: X"П
unrealscript
Description="X"ПТестовый мутатор. Базовый шаблон"
Итоговый пример мутатора
В итоге получили такой вот мутатор:
unrealscript
Class TestMut extends Mutator;
defaultproperties
{
GroupName="KF-TestMut"
FriendlyName="С``Тестовый =·tмутатор"
Description="X"ПТестовый мутатор. Базовый шаблон"
}
Статья 2: Типы мутаторов
- Мутатор, который работает только на сервере (потом добавлю сюда ссылку на статью “модель клиент-сервер для unreal движка”).
- Мутатор, который работает на сервере и клиенте (при этом возможно действия выполняются только для клиентов, для сервера никакого полезного кода не выполняется).
- Мутатор, который работает на сервере, но при этом требует, чтобы некоторые ресурсы закачивались на клиент (ибо туда идёт неявная репликация).
ТИП №1: Мутатор, который работает только на сервере
Примером такого мутатора являются пожалуй большинство мутаторов. Характерной особенностью является полная независимость от того, что происходит на клиенте.
Так как такой мутатор выполняется только на сервере, нам нет необходимости закачивать его на клиент, и нет никакой необходимости добавлять bAddToServerPackages=true в defaultproperties, вызывать где-то функцию AddToPackageMap() или писать в KillingFloor.ini ServerPackages=ПакетМутатора.
Пример:
unrealscript
class ServerOnlyMut extends Mutator;
function PostBeginPlay()
{
// Ваш код здесь
}
defaultproperties
{
GroupName="KF-ServerOnlyMut"
FriendlyName="Server Only Mutator"
Description="Мутатор, работающий только на сервере"
}
Замечание:
Можно создать файл “НазваниеПакета.upkg” в папке мутатора. И в нём прописать флаги AllowDownload, ClientOptional, ServerSideOnly. Этот текстовый файл используется компилятором.
Написав AllowDownload=false, мы запретим закачивать наш пакет на клиент, независимо от того, прописал админ этот наш пакет в ServerPackages или нет.
У Марко в ServerPerksMut такой набор флагов:
ini
[Flags]
AllowDownload=False
ClientOptional=False
ServerSideOnly=True
Я раньше особо не парился по этому поводу и во все мутаторы добавлял bAddToServerPackages=true. Новичкам стоит, пожалуй, так и делать, чтобы не ломать лишний раз голову.
И ещё замечание:
Данный тип мутатора можно запускать не как мутатор, а как ServerActor. Данную тему я потом покопаю, ибо не очень понимаю плюсы и минусы данного шага. Напоминаю, ServerActor - это объект, который создаётся на сервере при загрузке карты.
ТИП №2: Мутатор, который используется на клиенте и возможно на сервере
Ниже приведены некоторые из случаев, когда они нужны:
- изменение HUD (обычно с помощью Interaction)
- обработка нажатия кнопок клавиатуры/мыши
- вызов спецэффектов на клиенте
- необходимость поменять свойства объектов, используемых только на клиенте. Например: отображение веса пушки в магазине Вообще клиентские мутаторы тема непростая. Она требует понимания того, как происходит создание объектов на сервере и клиенте. Это будет потихоньку раскрываться в других статьях. Пока просто опишу основные характерные особенности кода. 1. Чтобы мутатор работал на клиенте в defaultproperties необходимо добавить 2 строчки:
unrealscript
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
2. Мутатор должен быть закачен на клиент, поэтому нужна либо строчка:
unrealscript
bAddToServerPackages=True
либо другое альтернативное добавление мутатора в список пакетов.
3. Используемые на клиенте функции должны иметь префикс simulated
4. Проверку того, на клиенте выполняется данный экземпляр мутатора или на сервере, можно сделать такую:
Вариант 1:
unrealscript
if(Role == ROLE_Authority)
{
//КодДляСервера
}
else
{
//КодДляКлиента
}
Вариант 2:
unrealscript
local PlayerController PC;
PC=Level.GetLocalPlayerController();
if(PC!=none)
{
КодДляКлиента;
}
else
{
КодДляСервера;
}
Вариант 3:
unrealscript
if(Level.NetMode==NM_DedicatedServer)
{
//КодДляВыделенногоСервера
}
else
{
//КодДляКлиента (при условии, что мутатор установлен на выделенном сервере)
}
5. Мутатор выполняется на стороне клиента, и есть возможность получить PlayerController:
unrealscript
Level.GetLocalPlayerController()
Соответственно для каждого экземпляра мутатора будет PlayerController его владельца. Для сервера Level.GetLocalPlayerController() вернёт none. Именно на этом основана проверка в предыдущем пункте (вариант 2). Очень полезная штука этот Level.GetLocalPlayerController().
6. На клиенте нельзя использовать Level.Game
Что это значит? Это значит нельзя на клиенте так проверять идёт волна или нет:
unrealscript
if(KFGameType(Level.Game).bWaveInProgress)
{
...
}
Надо использовать KFGameReplicationInfo объект:
unrealscript
local KFGameReplicationInfo KFGRI;
KFGRI = KFGameReplicationInfo(Level.GetLocalPlayerController().GameReplicationInfo);
if(KFGRI.bWaveInProgress)
{
...
}
7. Надо чётко понимать, что, если создать клиентский мутатор с чтением информации из ini файла, то клиентские версии мутатора будут искать ini на клиенте, а серверный - на сервере. Впрочем, это тема более сложной статьи. Пример клиентского мутатора: Пусть мутатор пишет игроку приветствие в начале волны (первой волны, которую он играет):
unrealscript
class TestClientMut extends Mutator;
//Функция выполняется на клиенте и сервере благодаря пунктам 1 и 2
//Для сервера сразу зовётся Disable('Tick') и для сервера функция больше не вызывается
//Для клиента же мы ждём начала волны, постим сообщение и отключаем функцию
//В принципе можно было бы и мутатор удалить для сервера и клиента, а не только отключить Tick, ну да не суть важно
simulated function Tick(float dt)
{
local PlayerController PC;
local KFGameReplicationInfo KFGRI;
PC=Level.GetLocalPlayerController();
if(PC!=none)
{
KFGRI = KFGameReplicationInfo(PC.GameReplicationInfo);
if(KFGRI.bWaveInProgress)
{
PC.ClientMessage("Greetings Mortal, are you ready to die?");
Disable('Tick');
}
}
else
Disable('Tick');
}
defaultproperties
{
bAddToServerPackages=True
GroupName="KF-TestClientMut"
FriendlyName="TestClient"
Description="Testing base client mutator"
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
bNetNotify=True
}
Пример серверного мутатора с аналогичным функционалом:
unrealscript
class GreetingsMut extends Mutator;
function Tick(float dt)
{
local Controller C;
if(KFGameType(Level.Game).bWaveInProgress)
{
for( C = Level.ControllerList; C != None; C = C.nextController )
{
if(C.IsA('PlayerController') && C.PlayerReplicationInfo.PlayerID>0)
PlayerController(C).ClientMessage("Greetings Mortal, are you ready to die?");
}
Disable('Tick');
}
}
defaultproperties
{
GroupName="KF-GreetingsMut"
FriendlyName="Greetings"
Description="GreetingsMut"
}
В первом случае у каждого клиента есть мутатор, который лично для него пишет сообщение. Во втором случае мы на сервере просто пробегаем по всем игрокам и каждому из них пишем сообщение. Полезно сравнить эти два мутатора и чётко понять как каждый из них работает.
ТИП №3: Мутатор, который работает на сервере, но при этом требует, чтобы некоторые ресурсы закачивались на клиент
Он, вроде, как серверный, но вот беда - если его не закачать клиенту, то он не будет полноценно работать.
Например, мутатор, который на сервере вызывает функцию запускающую проигрывание звука ClientPlaySound. Если не прописать закачку ресурсов на клиент, звук проигрываться не будет.
Поэтому, если для работы мутатора на клиенте нужны ресурсы какого-либо пакета, необходимо скачивать этот мутатор на клиент. То есть добавить bAddToServerPackages=True в defaultproperties.
Статья 3: Объединение мутаторов
Сегодня будем бороться с переизбытком мутаторов. Замечали вылеты сервера при смене карты из вебадминки? Или даже при старте сервера? Всё это следствие переизбытка мутаторов.
Проблема
Когда у вас много мутаторов, каждый из них создаёт свой объект, что может привести к:
- Вылетам сервера
- Проблемам при смене карт
- Увеличению времени загрузки
Идея
Использовать один общий мутатор, преобразовать старые мутаторы в Actor объекты и делегировать им задачи.
Пример
Объединим три мутатора: AutoSpawner, AllTraderMut и KFNoDramaMut в один. 1. Переместим все мутаторы в одну папку, например, “ServerMutators”. 2. Создадим мутатор “ServerAggregatorMut”:
unrealscript
class ServerAggregatorMut extends Mutator;
var AutoSpawner autoSpawner;
var AllTraderMut allTraderMut;
var KFNoDramaMut kfNoDramaMut;
function PostBeginPlay()
{
autoSpawner = Spawn(class'AutoSpawner');
allTraderMut = Spawn(class'AllTraderMut');
kfNoDramaMut = Spawn(class'KFNoDramaMut');
}
defaultproperties
{
GroupName="KF-Aggregator"
FriendlyName="AggregatorMut"
Description="Объединённый мутатор"
}
Так как AllTraderMut и KFNoDramaMut не содержат никаких функций из класса Mutator, объединить их очень просто. Пример кода для AutoSpawner:
unrealscript
class AutoSpawner extends Actor config (Mutators);
struct PlayerData
{
var PlayerController PC;
var int WaveNum;
var bool bRespawned;
};
var array<PlayerData> PlayerDB;
var int WaveCounter;
function PostBeginPlay()
{
SetTimer(0.1, true);
}
function Timer()
{
local int ID;
local PlayerController PC;
if (Invasion(Level.Game).bWaveInProgress)
{
for (ID = 0; ID < PlayerDB.Length; ID++)
{
if (PlayerDB[ID].PC == None)
continue;
PC = PlayerDB[ID].PC;
if (PC.PlayerReplicationInfo != None && !PC.PlayerReplicationInfo.bOnlySpectator && PC.PlayerReplicationInfo.bOutOfLives)
{
Level.Game.Disable('Timer');
PC.PlayerReplicationInfo.bOutOfLives = false;
PC.PlayerReplicationInfo.NumLives = 0;
PC.PlayerReplicationInfo.Score = Max(KFGameType(Level.Game).MinRespawnCash, int(PC.PlayerReplicationInfo.Score));
PC.GotoState('PlayerWaiting');
PC.SetViewTarget(PC);
PC.ClientSetBehindView(false);
PC.bBehindView = False;
PC.ClientSetViewTarget(PC.Pawn);
Invasion(Level.Game).bWaveInProgress = false;
PC.ServerReStartPlayer();
Invasion(Level.Game).bWaveInProgress = true;
Level.Game.Enable('Timer');
}
}
}
if (Invasion(Level.Game).WaveNum != WaveCounter)
{
for (ID = PlayerDB.Length - 1; ID > -1; ID--)
{
if (PlayerDB[ID].PC == None)
PlayerDB.Remove(ID, 1);
}
WaveCounter = Invasion(Level.Game).WaveNum;
}
}
defaultproperties
{
VersionString="0.0.8"
DateString="10/July/10"
WaveCounter=-1
}
Так мы можем “упаковать” неограниченное количество мутаторов.
Мутаторы, выполняющиеся на клиенте
Все мутаторы, что были рассмотрены выше, выполняются только на сервере и не закачиваются на клиент. Кратко рассмотрим упаковку мутатора, который закачивается и выполняется на клиенте. Лучше такие мутаторы упаковывать в отдельный пакет, это по меньшей мере логично. Упакуем мутатор BrutusPatHPLeft:
unrealscript
class AggregatorMut extends Mutator;
var BrutusPatHPLeft brutusPatHPLeft;
simulated function PostBeginPlay()
{
brutusPatHPLeft = Spawn(class'BrutusPatHPLeft');
}
defaultproperties
{
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
bNetNotify=True
GroupName="KF-Aggregator"
FriendlyName="AggregatorMut"
Description="AggregatorMut"
}
unrealscript
class BrutusPatHPLeft extends Actor config (Mutators);
...
defaultproperties
{
bAffectSpectators=True
bAffectPlayers=True
DisplayString="Health: %h/%m (%p%)"
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
bNetNotify=True
}
Вся разница в 3х добавившихся в defaultproperties переменных:
unrealscript
bAddToServerPackages=True
bAlwaysRelevant=True
RemoteRole=ROLE_SimulatedProxy
И в ключевом слове simulated перед функциями в мутаторе (в данном случае перед PostBeginPlay). Остальное ничем не отличается от варианта с серверными мутаторами.
Замечания
Замечание 1: Это особенно актуально для пользователей Windows.
Замечание 2: Количество пакетов не имеет значения, проблема сохраняется даже если все мутаторы в одном пакете.
Замечание 3: Можно добавить config переменные в общий мутатор, чтобы можно было в настройках включать/выключать тот или иной “упакованный” мутатор.
Замечание 4: Изначально статья была куда больше, в частности за счёт “упаковки” большего числа мутаторов. Но потом мы решили — незачем. Если у вас есть конкретные вопросы как упаковать тот или иной мутатор — пишите.
Замечание 5: Возможно есть смысл использовать не Actor классы, а Info. Это будет более правильно в каком-то плане, но это уже тема отдельной статьи.
Замечание 6: В частности есть смысл всем нашим Actor объектам добавить свойство bHidden=true.
Замечание 7: Аналогичным образом можно поступать при создании дополнительных объектов в PlayerReplicationInfo, чтобы не выполнять весь код непосредственно в базовом классе репликации.
Замечание 8: Ссылка на результат упаковки: ServerMutators.ServerAggregatorMut, Mutators.AggregatorMut
Статьи от Команды 2/5