Мутаторы и UnrealScript

Изменение характеристик оружия

Карточка 9-мм пистолета у Торговца Стартовый 9-мм пистолет в меню Торговца.

class AlteredWeaponMut extends Mutator;
function PostBeginPlay()
{
class'Single'.default.ItemName = "Handgun";
}
defaultproperties
{
GroupName="KFAlteredWeaponMut"
FriendlyName="Altered Weapon Mutator"
Description="Пример мутатора, который демонстрирует, как изменить существующее оружие."
}

В этом руководстве охватывается большая часть существенных атрибутов, которые могут быть изменены.

Семантика классов

Weapon – это класс оружия в инвентаре, который содержит общую информацию. Например, когда предмет должен появиться в инвентаре, его название и вес. Содержит соответствующие ссылки к классам WeaponPickup, WeaponAttachment и FireMode. Также этот класс отвечает за то, как оружие будет отображаться от 1-го лица. Примером данного класса является 'Single'.

WeaponPickup – Отображаемая модель оружия, когда оно (оружие) лежит где-то в игровом мире, вне инвентаря игрока (это касается и торговца). Содержит информацию о названии оружия, сколько боеприпасов (и их количество) расходуется при стрельбе и т.д. Примером этого класса является 'SinglePickup'.

WeaponAttachment – Отвечает за то, как оружие отображается от 3-го лица (и как будут видеть его другие игроки со стороны). Примером данного класса является ‘SingleAttachment’.

FireMode – Класс отвечает за то, какой режим огня имеет оружие, какой боезапас использует, какой урон наносит и тому подобные, связанные с этим вещи. Оружие может иметь два режима огня и необязательно один из них может быть наступательным (например, альт. огонь beretta 9mm (по-умолчанию это ПКМ) включает фонарик). Примером данного класса является 'SingleFire'.

Ammo - Класс отвечает за то, сколько боеприпасов оружие может иметь, сколько изначально содержит, и сколько боезапаса вы получаете, когда подбираете ящик с боеприпасами. Примером данного класса является 'SingleAmmo'.

AmmoPickup – Используется для того, чтобы указать, сколько боезапаса пополняет тот или иной вид подбираемого ящика с боеприпасами (например, 9mm патроны, лежащие на земле). Примером данного класса является 'SingleAmmoPickup'.

Projectile – Используется для указания типа используемых оружием боеприпасов (дробь, арбалетный болт, ракета и т.п.), а также описывает скорость полета снаряда, наносимые им повреждения и другую информацию. Примером данного класса является 'CrossbowArrow'.

Изменение названия оружия

class'Single'.default.ItemName = "9mm Tactical";

9mm Tactical будет являться именем оружия, которое отображается в HUD.

class'SinglePickup'.default.ItemName = "9mm Pistol";

9mm Pistol будет являться именем оружия, которое будет отображаться в магазине.

class'SinglePickup'.default.ItemShortName = "9mm Pistol";

9mm Pistol будет являться коротким именем оружия, которое будет отображаться в инвентаре игрока (в том числе и в магазине).

Режимы стрельбы

class'SingleFire'.default.bWaitForRelease = true;

Если установлено значение true, то для стрельбы надо будет нажимать на кнопку, иначе (значение flase) - достаточно зажать кнопку для стрельбы.

Урон

class'SingleFire'.default.DamageMax = 32;

32 – максимальный показатель урона (без учёта множителей).

  1. Если тип заряда - снаряд(например, болт арбалета или ракета L.A.W)
class'CrossbowArrow'.default.Damage = 300;

300 – неизменный показатель урона (т.е. без учета множителей).

class'BoomStickBullet'.default.HeadShotDamageMult = 1.5;

1.5 – множитель урона от попадания дроби в голову.

class'CrossbowArrow'.default.HeadShotDamageMult = 6.0;

6.0 – множитель урона от попадания болта в голову.

class'DamTypeM14EBR'.default.HeadShotDamageMult = 2.0;

2.0 – множитель повреждений от попадания в голову, заданный классом DamTypeM14EBR.

Скорость стрельбы

class'SingleFire'.default.FireRate = 0.5;
class'SingleFire'.default.FireAnimRate = 1.5;

1.5 указывает на то, что скорость выше обычной, но и при этом не является удвоенной.

Отдача

class'SingleFire'.default.RecoilRate = 0.07;
class'SingleFire'.default.MaxVerticalRecoilAngle = 300;

Чем больше значение, тем выше отдача может увести прицел вверх.

class'SingleFire'.default.MaxHorizontalRecoilAngle = 50;

Чем больше значение, тем сильнее прицел будет уводить в стороны при стрельбе.

Точность

class'SingleFire'.default.Spread = 0.015;

0.015 - значение разброса первой пули.

class'SingleFire'.default.MaxSpread = 0.12;

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

Прицеливание

class 'single'.default.bHasAimingMode = true;

Если значение true, то возможность регулирования кратности прицела есть. Иначе (т.е. значение false) - такой возможности не будет.

class 'single'.StandardDisplayFOV = 70.0;
class 'single'.ZoomedDisplayFOV = 65.0;
class 'single'.PlayerIronSightFOV = 75.0;

Боезапас

class 'SingleAmmo'.default.MaxAmmo = 240;
class 'SingleAmmo'.default.InitialAmount = 120;
class 'SingleAmmo'.default.AmmoPickupAmount = 30;
class 'SingleAmmo'.default.AmmoAmount = 20;

Сообщение о подборе

class 'SingleAmmo'.default.PickupMessage = You got another 9mm Handgun.

Подбирая оружие, внизу по середине экрана будет появляться следующий текст - You got another 9mm Handgun.

Торговец

class'SinglePickup'.default.Cost = 1000;

1000 - стоимость определенного оружия.

class'SinglePickup'.default.AmmoCost = 10;
class'SinglePickup'.default.BuyClipSize = 30;

Кроме того, есть три переменные, показывающие статистику оружия в магазине (максимальное значение - 100). Важно помнить, что статистика оружия может не быть точной и не соответствовать действительности:

class'SinglePickup'.default.PowerValue = 20;
class'SinglePickup'.default.SpeedValue = 50;
class'SinglePickup'.default.RangeValue = 35;

Информация об инвентаре

class'Single'.default.InventoryGroup = 2;

1 - Оружие ближнего боя;

2 - Пистолеты;

3 - Основное оружие;

4 - Особое (зачастую, довольно мощное и тяжёлое);

5 - Подручные инструменты (то, что не всегда используется как оружие для уничтожения, но находит своё применение для поддержки команды);

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

class'Single'.default.Priority = 3;

Priority задаёт, какое оружие группы достаётся первым: чем выше значение, тем раньше (см. уточнение из обсуждения на форуме ниже).

class'Single'.default.GroupOffset = 1;

1 - Общий приоритет заданного предмета.

class'Single'.default.Weight = 0;

0 указывает на то, что вес предмета равен нулю.

class'Single'.default.bKFNeverThrow = true;

Если значение = true, значит предмет нельзя выкинуть или продать. Иначе (т.е. = false) - его можно продать или выкинуть.

Дополнения

По материалам темы «[Руководство] Изменение оружия» на форуме killingfloor.ru.

Priority. Приоритет определяет, какое оружие достаётся первым при нажатии кнопки его группы: чем выше значение, тем раньше оно выхватывается. Пример из темы: у Deagle приоритет 120, у револьвера 70, поэтому в группе пистолетов (2) Deagle достаётся раньше револьвера. Значение имеет тип byte, от 0 до 255.

Цепочка инвентаря. Всё оружие игрока образует цепочку: в Pawn есть переменная Inventory, а у каждого предмета инвентаря есть своя переменная Inventory со ссылкой на следующий предмет:

Pawn.Inventory->Pawn.Inventory.Inventory->Pawn.Inventory.Inventory.Inventory->...->None

Оружие добавляется в цепочку функцией AddInventory и удаляется функцией DeleteInventory.

Tick и WeaponTick. Tick работает всё время, пока оружие существует, а WeaponTick — только когда это оружие выбрано и находится в руках игрока.

Полезные переменные Pickup-класса:

  • AmmoCost и BuyClipSize задают цену патронов: AmmoCost — цена количества патронов, указанного в BuyClipSize (BuyClipSize часто равен MagCapacity, размеру магазина).
  • CorrespondingPerkIndex — номер перка, к которому относится оружие. Узнать перк оружия W можно так:
W.PickupClass.default.CorrespondingPerkIndex
  • AmmoAmount и MagAmmoRemaining хранят патроны выброшенного оружия. Если выбросить AA12, в котором было 20 патронов, из них 5 в текущем магазине, то в AmmoAmount[] будет 20, а в MagAmmoRemaining — 5.
  • Weight есть и в Weapon, и в WeaponPickup. Weight в WeaponPickup проверяется при попытке поднять или купить оружие, а Weight в Weapon — фактический вес оружия в руках. Значения должны совпадать; если переменная не прописана в defaultproperties, она по умолчанию равна 10.
  • bDropped показывает, как Pickup появился на карте: true — оружие выбросил игрок (или монстр), иначе оно заспавнилось само. Это важно, потому что в KFGameType есть механизм удаления оружия, лежащего на земле.

Список оружия пакета хранится в его ucl-файле, например в KFMod.ucl.

CheckCanCarry в Pickup-классе позволяет ограничить, кто может поднять оружие. С таким кодом поднять его сможет только саппорт выше 10 уровня:

function bool CheckCanCarry(KFHumanPawn P)
{
 local KFPlayerReplicationInfo KFPRI;
 KFPRI = KFPlayerReplicationInfo(P.PlayerReplicationInfo);
 if (KFPRI != none)
 {
 if (
 KFPRI.ClientVeteranSkill.Name=='SRVetSupportSpec'
 && KFPRI.ClientVeteranSkillLevel > 10
 )
 return Super.CheckCanCarry(P);
 }
 return false;
}

Изменения не применились. Если после правки характеристик в игре ничего не поменялось, удалите файлы .u и .ucl перед компиляцией: уже собранные пакеты заново не компилируются. Файл .ucl можно не удалять, если вы правите существующее оружие, но при добавлении нового оружия удаляйте оба файла, так как в .ucl должна появиться строка нового оружия.

Мутатор для смены цены стандартного оружия

Из темы «Изменение цены оружия.» на форуме killingfloor.ru. Чтобы поменять цену стандартного ствола без правки самого оружия, Flame предложил маленький мутатор (пример: MAC10 за 200 вместо 500):

class ModifyWeaponCost extends Mutator;
simulated function PostBeginPlay()
{
 class'KFMod.MAC10Pickup'.default.cost = 200;
}
defaultproperties
{
 GroupName="KF-ModifyWeaponCost"
 FriendlyName="ModifyWeaponCost"
 Description="ModifyWeaponCost"
 bAddToServerPackages=True
 bAlwaysRelevant=True
 RemoteRole=ROLE_SimulatedProxy
}

Мутатор подключается как ModifyWeaponCost.ModifyWeaponCost. Для других стволов добавьте в PostBeginPlay такие же строки (class'KFMod.AA12Pickup'.default.cost = ... и т. д.). Как собрать пакет, см. компиляция и декомпиляция мутаторов.

Скидки перков на сервере с ServerPerks задаёт функция GetCostScaling в классе перка: например, if ( Item==class'MAC10Pickup') return 0.4; даёт этот ствол со скидкой. Чтобы убрать все скидки, по совету из темы декомпилируйте ServerPerksP и удалите эту функцию из перков; по словам eXes1ze`, без ServerPerks так сделать нельзя, потому что все бонусы прописаны именно в нём (BAHO считал, что в принципе можно обойтись и без него, но способа не привёл). Подробнее о бонусах перков: кодинг перков ServerPerks.

Перк, урон пуль, вспышки и прицелы

По темам «Вопрос по модификации оружейных мутаторов.», «подскажите как поменять значёк класса оружия в магазине», «Требуется помощь с мутатором "Паук" из Doom3 мода», «Не идет дмг по оружию», «Помогите», «Огонь вокруг персонажа», «Изменение отображения оптического прицела.» и «Помогите с оружием» на форуме killingfloor.ru.

Значок перка. Значок перка в магазине задаёт CorrespondingPerkIndex в defaultproperties pickup-класса; CorrespondingPerkIndex=3 — командос. Если поменять только DamageType в Fire-классе (в теме SA80 перевели на урон M14), хедшоты начнут засчитываться другому перку, но значок и прицеливание не изменятся.

Прокачка перка от своего оружия. В DamageType оружия вписывают функции начисления статистики из DamageType оружия нужного перка. Для командоса:

static function AwardKill(KFSteamStatsAndAchievements KFStatsAndAchievements, KFPlayerController Killer, KFMonster Killed )
{
 if( Killed.IsA('ZombieStalker') )
 KFStatsAndAchievements.AddStalkerKill();
}
static function AwardDamage(KFSteamStatsAndAchievements KFStatsAndAchievements, int Amount)
{
 KFStatsAndAchievements.AddBullpupDamage(Amount);
}

По словам offutt, можно попробовать и проще: дать пуле тип урона оружия командоса, MyDamageType=Class'KFMod.DamTypeBullpup'. Чтобы урон качал сразу два перка (огонь и автоматы), Flame унаследовал DamageType от KFWeaponDamageType (иначе компилятор не знает bDealBurningDamage) и вызвал в AwardDamage обе функции:

static function AwardDamage(KFSteamStatsAndAchievements KFStatsAndAchievements, int Amount)
{
 KFStatsAndAchievements.AddFlameThrowerDamage(Amount);
 KFStatsAndAchievements.AddBullpupDamage(Amount);
}

Сигнатура AwardKill должна совпадать с объявлением в KFWeaponDamageType: static function AwardKill(KFSteamStatsAndAchievements KFStatsAndAchievements, KFPlayerController Killer, KFMonster Killed );. Лишний параметр (например, int Amount) даёт ошибку Redefinition of 'function AwardKill' differs from original in KFWeaponDamageType. Для собственной статистики в ServerPerks, по словам Flame, нужно править классы SRStatList, ClientPerkRepLink, ServerStStats и StatsObject.

Урон снарядного оружия. У M99 из IJCWeaponPack нет MaxDamage и MinDamage: урон наносит пуля, в M99Bullet.uc это Damage=770.000000 и HeadShotDamageMult=4.000000.

Следы попаданий и трассер. Чтобы пуля оставляла белые точки на стенах, Flame советовал унаследовать её от ShotgunBullet (вместо Projectile) и задать DrawScale=1.000000. Трассер можно сделать наследником KFTracer:

class M99Tracer extends KFTracer;
defaultproperties
{
 mLifeRange(0)=4.500000
 mLifeRange(1)=4.500000
 mColorRange(0)=(B=0,G=0,R=255)
 mColorRange(1)=(B=0,G=0,R=255)
}

и спавнить его в PostBeginPlay пули строками Trail = Spawn(class'M99Tracer',self); и Trail.Lifespan = Lifespan;. Если пули светятся (bDynamicLight=True), поставьте им LightType=LT_None, чтобы они не освещали поверхности, над которыми пролетают.

Оружие ближнего боя с версии 1046. В KFMeleeFire вместо damageConst и maxAdditionalDamage теперь используется MeleeDamage (ответ Freddy). Старые мутаторы выдают предупреждения Missing IntProperty IntProperty KFMod.KFMeleeFire.damageConst.

Скорость удара мечом (тема «Ускорение оружия берса...», форум killingfloor.ru). Если уменьшить DamagedelayMin и DamagedelayMax, меч рубит быстрее, но анимация удара отстаёт: это задержка урона. LLIePLLIeHb советовал уменьшать FireRate в fire-классе (для Claymore - ClaymoreSwordFire), так же и на остальном оружии; анимации без опыта лучше не трогать. Множители скорости анимаций (FireAnimRate, ReloadAnimRate, SelectAnimRate и др.) пишутся в defaultproperties кода оружия: больше единицы - быстрее, меньше - медленнее (3xzet). Полоски мощности, скорости и радиуса в магазине - только описание, они задаются в pickup-классе (ClaymoreSwordPickup.uc, ChainswordPickup.uc: PowerValue, SpeedValue, RangeValue) и на характеристики не влияют. Сам урон меняется в fire-классах основной и альтернативной атаки (ChainswordFire.uc и ChainswordFireB.uc, ClaymoreSwordFire.uc и ClaymoreSwordFireB.uc) строкой maxAdditionalDamage= (до версии 1046, см. выше).

Вспышка, освещающая всё вокруг стрелка. Свет при выстреле включает функция WeaponLight в KFWeaponAttachment: от первого лица она ставит LitWeapon.bDynamicLight = true, от третьего — bDynamicLight = true. Lexass переопределил её в своём Attachment-классе с false в обоих местах и LightBrightness=0.000000 в defaultproperties:

simulated function WeaponLight()
{
if ( (FlashCount > 0) && !Level.bDropDetail && (Instigator != None)
&& ((Level.TimeSeconds - LastRenderTime < 0.2) || (PlayerController(Instigator.Controller) != None)) )
{
if ( Instigator.IsFirstPerson() )
{
LitWeapon = Instigator.Weapon;
LitWeapon.bDynamicLight = false;
}
else
bDynamicLight = false;
SetTimer(0.15, false);
}
else
Timer();
}

Оптический прицел. Текстурный прицел задаётся в классе оружия, например у SVD: ZoomMat=FinalBlend'svd-c_T.SVDScopeFB' и ScriptedTextureFallback=Texture'svd-c_T.alpha_lens_64x64'. Кратность и поле зрения прицелов LLIePLLIeHb настраивал так:

 scopePortalFOVHigh=10.000000 // Зум сложного прицела
 scopePortalFOV=10.000000 // Зум упрощенного прицела
 PlayerIronSightFOV=32.000000 // Зум текстурного прицела
 ZoomedDisplayFOVHigh=55.000000 // FOV сложного прицела
 ZoomedDisplayFOV=55.000000 // FOV упрощенного прицела

Если в прицеле трассеры вылетают из точки первого выстрела, Lexass советовал прописать в Attachment-классе функцию DoFlashEmitter, которая крепит вспышку к кости Muzzle через AttachToBone(mMuzFlash3rd, 'Muzzle');, или задать в классе оружия FlashBoneName="Muzzle" (у AnRouge помогло указать кость своего глушителя).

Анимации оружия. У класса Weapon есть переменные IdleAnim, RestAnim, AimAnim, RunAnim, SelectAnim и PutDownAnim; доставание оружия задаёт SelectAnim, убирание — PutDownAnim. По умолчанию ожидаются имена вроде Idle и Select. Если анимации в вашем пакете называются иначе, пропишите их имена, иначе оружие может пропадать из рук после смены на другое и появляться только при выстреле (совет CompJ).

Свет выстрела без потери звука, мины и гранаты

По темам «Поправка света (вспышек) от оружия», «Правка HUD'а и правка кода взрывчатки», «Граната не наносит урон двери» и «PipeBomb» на форуме killingfloor.ru.

Подсветка при стрельбе: без LightBrightness. В теме «Поправка света (вспышек) от оружия» Lexass позже уточнил свой рецепт: функцию WeaponLight переопределяйте, как показано выше, а в defaultproperties пишите только две строки:

LightType=LT_Pulse
LightRadius=0.000000

LightRadius — радиус вспышки, LightBrightness — яркость. LightBrightness=0.000000 не ставьте: по наблюдению автора, у многих стандартных пушек это убирает звук выстрела. Вспышку можно убирать выборочно, например оставив её мощным стволам и дробовикам.

Мина не пропадает после последнего броска. Чтобы мина, как молотов, не пропадала из инвентаря после последнего броска, 3xzet предлагал попробовать закомментировать в таймере PipeBombFire.uc уничтожение оружия:

function Timer()
{
 Weapon.ConsumeAmmo(ThisModeNum, Load);
 DoFireEffect();
 Weapon.PlaySound(Sound'KF_AxeSnd.Axe_Fire',SLOT_Interact,TransientSoundVolume,,TransientSoundRadius,,false);
 if( Weapon.ammoAmount(0) <= 0 && Instigator != none && Instigator.Controller != none )
 {
 // Weapon.Destroy();
 Instigator.Controller.ClientSwitchToBestWeapon();
 }
}

Мигающая кнопка на лежащей мине — обычный шейдер плюс эмиттер: замените шейдер обычной текстурой, а эмиттер закомментируйте.

Мина взрывается, когда на неё наступают. Flame предложил убрать таймер обнаружения (function Timer() {}) и отслеживать касание в ProcessTouch класса мины. Для игроков:

if (
 KFHumanPawn(Other.Instigator)!=None
 || KFHumanPawn(Other)!=None
 )
{
 Explode(Location, vector(Rotation));
 Destroy();
}

Проверка Other.Instigator нужна, потому что Pawn иногда касается мины не собой, а классом-обёрткой. У Flame для зомби аналогичное условие срабатывало на мобов с Mass>400. Возможно, придётся поиграть с CollisionRadius=10.000000 и CollisionHeight=5.000000 в defaultproperties.

Граната, которая не повреждает двери. В функции TakeDamage двери урон не от монстров и не от сварки проходит, только если у двери включён bSmallArmsDamage или тип урона — именно класс DamTypeFrag (и урон не меньше DamageThreshold):

if (!instigatedBy.IsA('KFMonster') && damageType != class'DamTypeWelder' && damageType != class'DamTypeUnWeld')
{
 if(!bSmallArmsDamage && damageType != class'DamTypeFrag' || Damage < DamageThreshold )
 return;
}

Проверяется точное совпадение класса, поэтому достаточно дать гранате другой тип урона. Flame сделал это мутатором, не трогая Nade (альтернатива — своя копия Nade и правка GetNadeType в классах SRVet*):

class DoorSafeNadeMut extends Mutator;
function PostBeginPlay()
{
 class'KFMod.Nade'.default.MyDamageType=class'DamTypeDoorSafeFrag';
}

Здесь DamTypeDoorSafeFrag — ваш собственный класс типа урона.

Дополнения

Отдача. В теме «M4 Ris (Tactical)» Flame советовал уменьшить отдачу, выставив maxVerticalRecoilAngle и maxHorizontalRecoilAngle хоть в 0; Atlan считал полезнее RecoilRate=0. Lexass ответил, что у него уже стоят нули, но прицел всё равно ходит из-за самой анимации выстрела.

Название в магазине. В теме «Туррель из RPG мода» турель показывалась у торговца с кривым названием. prototype22 предположил, что название задают строки в файле ...Pickup.uc, которых у турели не было, и их нужно добавить:

ItemName="турель"
ItemShortName="турель"

При выбросе выпадает старое оружие. В теме «Заклинивающее при стрельбе оружие» (форум killingfloor.ru) новый ствол на основе АК47 при выбрасывании превращался в обычный калаш. Dr_Killjoy: в оружии остался старый PickupClass, поэтому при выкидывании создаётся пикап исходного оружия. Автор поблагодарил за подсказку и изменил PickupClass.

Предмет перед игроком, а не в нём. В теме «Немного про мины и аптечки» (форум killingfloor.ru) аптечку для медика пытались делать из прожектайла мины. Dr_Killjoy советовал вместо этого спавнить пикап (условие лечения смотреть в FirstAidKit) и не в самом медике, иначе тот сразу его подберёт. Точку перед игроком он считал так (SpawnLoc.X — расстояние вперёд):

local vector SpawnLoc;
SpawnLoc.X = 100;
SpawnLoc = SpawnLoc >> Instigator.Rotation;
SpawnLoc = SpawnLoc + Instigator.Location;

Действие на правую кнопку мыши. В теме «Взрыв турелей Портал, Дум и т.д. по нажатию ПКМ» (форум killingfloor.ru) турель из Portal хотели взрывать по ПКМ через FireModeClass(1). Flame поправил: FireModeClass(1) - это средняя кнопка мыши, а не правая. Его совет: спавн взрыва и уничтожение турели можно поместить в ToggleIronSights в коде турели.

---

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

Автор: Kiyo