Мутаторы и UnrealScript

Создание мутатора. Функции и настройки

Для достаточно хорошего понимания нижеследующего текста, важно иметь хотя бы начальные знания об ООП и языке UnrealScript в частности.

Дерево классов и свойства в KF Editor Дерево классов и свойства актора в KF Editor.

Актёр (класс) мутатора предоставляет несколько функций, которые позволяют уведомить об определенных событиях. К примеру, когда актёр появляется или игрок входит в текущую игру, мутатору позволено изменить поведение (переменные) и атрибуты (методы) других актёров в нужное время. Кроме того, мутатор наследует все функции актёра (от которого мутатор происходит), некоторые из них весьма полезны для написания даже простого мутатора.

Основные функции

PostBeginPlay

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

function PostBeginPlay()
{
    // Здесь обработка инициализации
}

Tick

Ещё одна стандартная функция актёра. Вызывается через определенный цикл игрового времени. При частоте кадров в 60 fps, он будет вызываться 60 раз в секунду (или один раз в ~16 миллисекунд). Эта функция часто используется другими актёрами, позволяя им постоянно, покадрово выполнять задачи (обновление переменных и т.д.). Аргумент delta указывает, сколько секунд потребовалось последнему циклу для выполнения задачи (например, при 2 fps значение аргумента должно быть ~0.5, так как ему потребуется полсекунды, чтобы сделать кадр.

function Tick(float Delta)
{
    // Здесь обновление логики мутатора
}

Timer

Ещё одна важная функция актёров, вызываемая сценарием (скриптом). Объявление функции SetTimer() будет планировать вызов на указанную задержку, которая может быть опционально повторена через некоторый промежуток времени.

function PostBeginPlay()
{
    SetTimer(0.5, False); // Через 0.5 с, без повтора (False)
}

function Timer()
{
    // Здесь обработка связанных с таймером вещей
}

Специфические функции

ModifyPlayer

Функция вызывается после того, когда игрок вошёл в игру. Она (функция) позволяет вносить в игрока изменения, прежде чем он появится. Для совместимости с другими мутаторами, необходимо указать на суперкласс имплементации этой функции, который позволит другим мутаторам обрабатывать этот запрос.

function ModifyPlayer(Pawn Other)
{
    Other.GiveWeapon("KFMOD.SCARMK17AssaultRifle");
    Super.ModifyPlayer(Other);
}

Mutate

Эта функция полезна как в отладке, так и практическом применении. Она вызывается, когда игрок вводит в консоли mutator <строка аргумента>. В многопользовательской игре он будет вызыватся независимо от того, запускается ли мутатор на клиентской машине или нет. Как и с функцией выше, вы должны отправить этот вызов другим мутаторам в цепи (вызов суперкласса имплементации этой функции обработает это для вас).

function Mutate(string MutateString, PlayerController Sender)
{
    if (Caps(MutateString) == "HYPER")
        Sender.Pawn.Health = 999;
    Super.Mutate(MutateString, Sender);
}

CheckReplacement

Функция вызывается всякий раз когда появляется актёр (это относится только к тем, чей атрибут bGameRelevant установлен на значение false - эта функция не вызывается для клиенто-сторонних актёров, такие как локальные эффекты). Функция позволяет изменить/заменить актёра. Это полезно когда нужно внести изменения в актёр после его появления, чтобы не пришлось беспокоится об изменениях позже. Если вы не являететсь опытным пользователем, всегда делайте возврат с вызовом суперкласса имплементации этой функции.

function bool CheckReplacement(Actor Other, out byte bSuperRelevant)
{
    if (KFMonster(Other) != None)
        KFMonster(Other).Health = 1;
    return Super.CheckReplacement(Other, bSuperRelevant);
}

Добавление настроек мутатора

FillPlayInfo

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

static function FillPlayInfo(PlayInfo PlayInfo)
{
    Super.FillPlayInfo(PlayInfo);
    PlayInfo.AddSetting(
        default.RulesGroup,
        "variableName",
        "settingDescription",
        0,
        0,
        "settingType");
}

Как вы можете увидеть выше, чтобы добавить параметр в игру, вы должны вызвать PlayInfo.AddSetting() и передать настройки в качестве аргументов. Свойство variableName должно совпадать с именем нужной для обозначения переменной. Свойство settingDescription позволяет дать краткое описание, которое непосредственно отображается рядом с полем ввода. Свойство settingType описывает настройки, которые могут быть выведены на экран в виде текста (строка или числовые данные), логической проверки (истина/ложь), строки выбора (выпадающее окно-список).

При использовании типа "Текст" можно указать длину строки и/или диапазона принимаемых значений в качестве дополнительного аргумента после settingType.

PlayInfo.AddSetting(default.RulesGroup, "bEnabled", "Enabled?", 0, 0, "check");
PlayInfo.AddSetting(default.RulesGroup, "HealthMax", "Maximum Health", 0, 0, "text");
PlayInfo.AddSetting(
    default.RulesGroup,
    "HealthMax",
    "Maximum Health",
    0,
    0,
    "text",
    "3;100:200"); // до 3 символов, диапазон 100–200

При использовании selectType необходимо указать принадлежность аргумента к settingType в следующем формате:"Значение1;Текст1;Значение2;Текст2".

PlayInfo.AddSetting(
    default.RulesGroup,
    "StartWep",
    "Starting weapon",
    0,
    0,
    "select",
    "0;Deagle;1;Bullpup;2;LAR"); // выпадающий список оружия

GetDescriptionText

static function string GetDescriptionText(string SettingName)
{
    switch (SettingName)
    {
        case "ExampleVar":
            return "This is the 'long' description for the setting.";
    }
    return Super.GetDescriptionText(SettingName);
}

Значения defaultproperties из ini-файла

Из темы «Изменение defaultproperties через ини файл» на форуме killingfloor.ru. Через ini редактируются только переменные, объявленные как config/globalconfig, поэтому, например, здоровье клота напрямую из ini не поменять. Рабочий способ, который Flame назвал идеальным: держать настройку в конфиге мутатора и в PostBeginPlay переписывать default-значения класса монстра.

class ZClot extends ZombieClot_XMas;
defaultproperties
{
}
class ZDay extends Mutator config(ZDay);
var globalconfig int HealthC;
function PostBeginPlay()
{
 SaveConfig();
 class'ZDay.ZClot'.default.Health=HealthC;
 class'ZDay.ZClot'.default.HealthMax=HealthC;
 Super.PostBeginPlay();
}
defaultproperties
{
 HealthC=1000;
 GroupName="ZDay"
 FriendlyName="ZDay"
 Description="ZDay"
 bAddToServerPackages=True
}

Без ini-файла у такого зомби будет 1000 здоровья, с ini-файлом (SaveConfig() создаст его автоматически при первом запуске) — сколько в нём написано. Мутатор прописывается как ZDay.ZDay, зомби в песочницу — как ZDay.ZClot. Для разных монстров удобнее заводить переменные с разными именами (Clot_Health, Bloat_Health и т.п.). Конфиг можно создать и вручную: у Gwyn в папке System сработал файл AngryClot.ini с секцией [AngryClotMut.AngryClot] и строкой HealthOFClot=20.

Из темы «Как отцепить ini файл?» (форум killingfloor.ru): файл конфига задаётся в объявлении класса (Config(KFPortalTurret)), а в него пишутся переменные var() globalconfig. Если ini создаётся автоматически, то, скорее всего, в коде есть вызов SaveConfig(); чтобы файл не создавался, удалите этот вызов (Flame). Чтобы получить несколько по-разному настроенных копий (в теме - турелей), Gwyn предложил унаследовать класс со своим конфигом, например class myTurret extends PortalTurret config(MyTurret);, либо сделать копии с переименованными классами, файлами и конфигами (по его словам, не проверено).

Предупреждение Flame про PreBeginPlay: в классе Mutator именно там мутатор добавляется в ServerPackages, если выставлен bAddToServerPackages:

event PreBeginPlay()
{
 if ( !MutatorIsAllowed() )
 Destroy();
 else if (bAddToServerPackages)
 AddToPackageMap();
}

Если переопределить PreBeginPlay и забыть Super.PreBeginPlay(), мутатор не будет сам загружаться на клиентах, как ни пиши bAddToServerPackages=true. Dr_Killjoy добавлял, что почти во всех переопределённых функциях желательно вызывать Super.

Небольшие мутаторы

Все коробки патронов заново в начале каждой волны (Flame, тема «Мутатор для респавна патронов» на форуме killingfloor.ru). Коробки патронов на карте (KFAmmoPickup) после подбора «засыпают»; мутатор будит все спящие коробки при старте волны. Если на карте коробок нет, заспавнить их он не сможет. Как с ними работает сама игра, Flame советовал смотреть в KFMod.KFGameType.SetupPickups.

Class SpawnAllAmmoAfterTraderMut extends Mutator;
var bool bTimeToSpawnAmmo;
function PostBeginPlay()
{
 SetTimer(1.0,true);
 bTimeToSpawnAmmo=true;
}
function Timer()
{
 if(bTimeToSpawnAmmo && KFGameType(Level.Game).bWaveInProgress)
 {
 ReSpawnAmmoPickups();
 bTimeToSpawnAmmo=false;
 }
 if(!bTimeToSpawnAmmo && !KFGameType(Level.Game).bWaveInProgress)
 {
 bTimeToSpawnAmmo=true;
 }
}
function ReSpawnAmmoPickups()
{
 local KFAmmoPickup AmmoPickupBox;
 foreach DynamicActors(class'KFAmmoPickup', AmmoPickupBox)
 {
 if(AmmoPickupBox.bSleeping)
 AmmoPickupBox.GotoState('Pickup');
 }
}
defaultproperties
{
 GroupName="KF-SpawnAllAmmoAfterTraderMut"
 FriendlyName="SpawnAllAmmoAfterTraderMut"
 Description="SpawnAllAmmoAfterTraderMut"
}

Подключается как SpawnAllAmmoAfterTraderMut.SpawnAllAmmoAfterTraderMut.

Запрет консольных команд (тема «Отловить консольные команды»). Консольные команды вроде ChangeCharacter, CreateCameraEffect или GetWeapon — это exec-функции контроллера игрока, поэтому их переопределяют в наследнике контроллера. Essence показал, как сделать это отдельным мутатором, без правки ServerPerks:

class ConComMut extends Mutator;
function PostBeginPlay()
{
 Level.Game.PlayerControllerClass=Class'ConComMut.SRPlayerController';
 Level.Game.PlayerControllerClassName="ConComMut.SRPlayerController";
 Super.PostBeginPlay();
}
defaultproperties
{
 bAddToServerPackages=True
 GroupName="KF-ConComMut"
 FriendlyName="Console Commands Mutator"
 Description="Redefines console commands."
}
class SRPlayerController extends KFPlayerController;
exec function ChangeCharacter(string newCharacter, optional string inClass);
exec function CreateCameraEffect(class<CameraEffect> EffectClass);
exec function GetWeapon(class<Weapon> NewWeaponClass);
defaultproperties
{
}

С ServerPerks те же функции переопределяют в KFPCServ (Flame). Пример tor1980 — запрет смены ника через консоль:

exec function SetName(coerce string S)
{
 ClientMessage("No way :)");
}

Поток предупреждений xTeamRoster ... Accessed None 'PlayerReplicationInfo' в консоли сервера (тема «xTeamRoster Floods into console»). Решение Flame: возьмите последний класс в цепочке наследования вашего ростера (например, TeamInfo -> UnrealTeamInfo -> xTeamRoster -> KFStoryRoster), скопируйте в него функции AddToTeam родителей под новыми именами, вызывайте их вместо Super и добавьте проверки PlayerReplicationInfo==none. Точка входа выглядит так:

class KFStoryRoster extends xTeamRoster;
function bool AddToTeam( Controller Other )
{
 if(Other.PlayerReplicationinfo == none)
 {
 return false;
 }
 return AddToTeamFromUnrealTeamInfo(Other);
}

В скопированную AddToTeamFromTeamInfo добавляются проверки if(Other.PlayerReplicationInfo==none) return false; и P.PlayerReplicationInfo!=none в цикле по Level.ControllerList. Для своего геймтайпа (наследника KFGameType) Essence объяснил подключение: создайте KFStoryRoster.uc с этим кодом в пакете геймтайпа и добавьте в defaultproperties геймтайпа DefaultEnemyRosterClass="MyPackage.KFStoryRoster", где MyPackage — этот пакет. Автор темы подтвердил, что всё заработало. Сами предупреждения безвредны, но в его случае давали скачки лага при подключении игроков.

ZED Time и камера для событий на карте (тема «Парочка нестандартных вопросов», форум killingfloor.ru). Чтобы акцентировать событие на карте (например, падение лифта), Freddy предложил в триггере в нужном месте по событию touch вызвать замедление и на время сфокусировать на триггере камеру; тот же триггер можно привязать к кнопке или двери. Замедление вызывается одной функцией DramaticEvent, пример захвата камеры есть в классе Патриарха, а запуск движения в начале карты - в стандартных картах, где вначале улетает вертолёт.

Ускорение берсерка в ZED Time (тема «Берс в Zed-time»). Убрать замедление для одного игрока Flame не знал как: ZED Time меняет скорость всей игры. Его включает KFGameType(Level.Game).DramaticEvent(1) (единица — вероятность срабатывания), а внутри вызывается SetGameSpeed(ZedTimeSlomoScale) из Engine.GameInfo. Стандартные значения: ZEDTimeDuration=3.000000 (замедление на 3 секунды) и ZedTimeSlomoScale=0.200000 (в 5 раз медленнее). Зато можно поменять параметры движения. Пример — особая катана берсерка из мутатора WTF (WTFEquipKatana.uc): по правой кнопке она включает замедление (не чаще раза в 120 секунд, за 20 HP), а в WeaponTick при смене состояния bZEDTimeActive ускоряет берсерка и смену оружия:

if (KFGameType(Level.Game).bZEDTimeActive)
{
 KFHumanPawn(Instigator).InventorySpeedModifier=500; //it is an INT
 Instigator.AccelRate=100000.0;
 KFHumanPawn(Instigator).ModifyVelocity(dt, Instigator.Velocity);
}
else
{
 ResetMovement();
}

Проверка на перк в этом коде — KFPRI.ClientVeteranSkill != Class'SRVetBerserker'.

Затемнение карты со второй волны (Flame, форум killingfloor.ru, тема «Мут гасящий освещение»). Первая версия обнуляла AmbientSaturation, KFOverlayColor и DistanceFogColor у всех ZoneInfo на сервере, и разницы видно не было: освещение нужно менять на клиенте. Рабочий вариант выполняется только при Level.NetMode==NM_Client и срабатывает, когда WaveNumber > 0:

class ZoneInfoMut extends Mutator;
simulated function PostBeginPlay()
{
 if( Level.NetMode==NM_Client )
 {
 SetTimer(2,true);
 }
}
simulated function Timer()
{
 local ZoneInfo zi;
 local KFGameReplicationInfo KFGRI;
 local PlayerController PC;
 if( Level.NetMode==NM_Client )
 {
 PC=Level.GetLocalPlayerController();
 KFGRI = KFGameReplicationInfo(PC.GameReplicationInfo);
 if(KFGRI.WaveNumber > 0)
 {
 foreach AllActors(class'ZoneInfo',zi)
 {
 zi.AmbientSaturation=0;
 zi.KFOverlayColor.B=0;
 zi.KFOverlayColor.G=0;
 zi.KFOverlayColor.R=0;
 zi.DistanceFogColor.B=0;
 zi.DistanceFogColor.G=0;
 zi.DistanceFogColor.R=0;
 }
 SetTimer(0,false);
 }
 }
}
defaultproperties
{
 GroupName="KF-ZoneInfo"
 FriendlyName="ZoneInfoMut"
 Description="ZoneInfoMut"
 bAddToServerPackages=True
 bAlwaysRelevant=True
 RemoteRole=ROLE_SimulatedProxy
}

Подключается как ZoneInfoMut.ZoneInfoMut. Nevma предлагал дополнительно обнулить zi.AmbientHue и zi.AmbientBrightness, а у источников света (Sunlight extends Light) — LightBrightness, LightHue и LightSaturation.

Полупрозрачное перекрестие на HUD (форум killingfloor.ru, тема «KF Crosshair v0.1 alpha»). У автора мутатора-прицела не работала прозрачность. Flame: вместо Canvas.Style=3 (STY_Translucent) поставить Canvas.Style=5 (STY_Alpha), а чтобы не стала прозрачной и консоль, после отрисовки вернуть ColorModulate.W в 1.0:

function PostRender( canvas Canvas )
{
 Canvas.SetPos(Canvas.SizeX*xPos, Canvas.SizeY*yPos);
 Canvas.Style=5;
 Canvas.ColorModulate.W = 0.5;
 Canvas.SetDrawColor(0,255,0);
 Canvas.DrawColor.A = 100;
 Canvas.DrawLine(3, 5.000000);
 Canvas.DrawLine(1, 5.000000);
 Canvas.DrawLine(2, 3.000000);
 Canvas.DrawLine(0, 3.000000);
 Canvas.ColorModulate.W = 1.0;
}

Там же Flame советовал: если в PreBeginPlay есть AddToPackageMap("KFCrossH"), bAlwaysRelevant=True можно не писать; после AddInteraction отключать тик через Disable('Tick').

Временный эффект на игроке через Inventory (Flame, форум killingfloor.ru, тема «[IDEA] POWERUPS in Killing Floor»). Так у его Патриарха ракета замедляет попавшего игрока вдвое на 12 секунд:

class shZombieBossSlowItem extends Inventory;
var Pawn LuckyHuman;
function GiveTo(Pawn Other, optional Pickup Pickup)
{
 if(Other == None)
 {
 Destroy();
 return;
 }
 LuckyHuman = Other;
 SetTimer(12.0,false);
 Super.GiveTo(Other);
}
function Tick(float delta)
{
 if(LuckyHuman == None)
 {
 Destroy();
 }
 else
 {
 LuckyHuman.GroundSpeed = LuckyHuman.default.GroundSpeed * 0.5;
 }
}
function Timer()
{
 Destroyed();
 Destroy();
}
defaultproperties
{
 bOnlyRelevantToOwner=False
 bAlwaysRelevant=True
 bReplicateInstigator=True
}

Внутри кода ракеты:

Item=spawn(class'shZombieBossSlowItem');
Item.GiveTo(Pawn(Victims));

Для бонусов на здоровье и скорость Flame и dosche4kin советовали также посмотреть код игрового автомата (рулетки) и парашюта edo_Chute.

Своя информация игрока на сервере через LinkedReplicationInfo (Flame, форум killingfloor.ru, тема «Настройка слота и приоритета оружия игроком»). Чтобы передать данные из ini-файла клиента на сервер (игроков много, сервер один), не обязательно править SRPlayerReplicationInfo из ServerPerks или цеплять к игроку посторонние объекты. LinkedReplicationInfo, как и PlayerReplicationInfo, унаследован от ReplicationInfo, существует и на сервере, и на клиенте и явно принадлежит игроку. Механизм прикрепления — переменная CustomReplicationInfo в PlayerReplicationInfo и переменная NextReplicationInfo в LinkedReplicationInfo, так что получается цепочка:

PlayerReplicationInfo->PlayerReplicationInfo.CustomReplicationInfo->PlayerReplicationInfo.CustomReplicationInfo.NextReplicationInfo->...

У всех, кто использует ServerPerks, такая цепочка уже есть: ClientPerkRepLink — тоже LinkedReplicationInfo. Готовых функций прикрепления и поиска нет; свой объект добавляют в конец существующей цепочки, механизм можно взять из ClientPerkRepLink. Чтобы поменять у оружия приоритет и группу (слот) до того, как оно попадёт в инвентарь (SRHumanPawn.AddInventory; после этого менять поздно), создание оружия перехватывают в CheckReplacement мутатора — он выполняется раньше AddInventory. Приоритет и группа прописаны в defaultproperties оружия. Мутатор Flame на этом принципе в итоге перестал зависеть от ServerPerks.

Эта статья является переводом статьи от Benjamin, которому следует выразить огромное спасибо.

Автор: Kiyo