Killing Floor: кастомное оружие, лимит памяти и Ref-ссылки на ресурсы
По теме форума Tripwire «Sooner or later you'll run out of memory» (2013)
В январе 2013 года на форуме Tripwire шла длинная тема о проблеме, в которую рано или поздно упирался любой сильно модифицированный сервер Killing Floor: клиенты вылетают с «out of virtual memory», и чем больше на сервере кастомного оружия и мобов, тем чаще. В обсуждении участвовали Dave_Scream, poosh (автор ScrnBalance), FluX и Guardiannknight, и закончилось оно рабочим рецептом. Ниже разбор.
Почему кончается память
- Killing Floor — 32-битная игра на Unreal Engine 2.5. Без флага Large Address Aware процессу доступно только 2 ГБ адресного пространства, сколько бы ОЗУ ни стояло в ПК. У одного из участников было 16 ГБ и Windows 7, а на некоторых картах игра всё равно падала каждые несколько секунд.
- Кастомный контент в классической связке с ServerPerks грузится статически. Строка
TraderInventory=3:SVD-cMut.SVDcPickupзаставляет ServerPerks загрузить класс пикапа. Пикап ссылается на класс оружия, а оружие на меши, скины, материалы прицела, звуки, attachment для вида от третьего лица, режимы огня и снаряды. Всё это оказывается в памяти ради одной строки в меню трейдера. - На сервере со 100+ пушками большинство из них за игру никто не покупает. В игре на 6 человек за волну реально используется около 18 стволов (2-4 на игрока плюс нож, 9mm и гранаты).
Стресс-тест poosh: 33 756 пикапов на большой карте опустили FPS ниже 1, но игра не упала и в лимит памяти не упёрлась. Сами экземпляры акторов почти ничего не весят. Дорого обходится мультимедиа (меши, текстуры, звуки), которое тянут статические ссылки.
Ещё два наблюдения из темы:
- Команда
obj linkersна крошечной карте показывала в памяти пакеты других карт сLazy=0(напримерMountainPass_T.utx,HillbillyHorror_anim.ukx). Карты ссылаются на ресурсы друг друга, поэтому загрузка одной может подтянуть куски других. - Многие кастомные пушки идут с несжатыми текстурами, и это кратно увеличивает расход памяти.
Идея: Ref-строки вместо статических ссылок
Стандартное оружие KF уже так устроено. Вместо Mesh=SkeletalMesh'...' класс хранит обычную строку MeshRef="Package.Object". Строка ни на что не ссылается, поэтому ничего не грузится, пока код не вызовет DynamicLoadObject(). Когда оружие уничтожено, значения по умолчанию снова становятся none, и сборщик мусора может освободить ресурсы.
Статические ссылки (старый вариант):
class SVDc extends KFWeapon;
defaultproperties
{
HudImage=Texture'svd-c_T.SVD-cUnselected'
SelectedHudImage=Texture'svd-c_T.SVD-cSelected'
SelectSound=Sound'Svd-c_SN.svd-c_draw'
Mesh=SkeletalMesh'svd-c_A.svdmesh'
Skins(0)=Shader'svd-c_T.Svd_D_Sh'
Skins(1)=Texture'KF_Weapons2_Trip_T.hands.BritishPara_Hands_1st_P'
ZoomMat=FinalBlend'svd-c_T.SVDScopeFB'
}
То же оружие на ref-строках:
defaultproperties
{
HudImageRef="svd-c_T.SVD-cUnselected"
SelectedHudImageRef="svd-c_T.SVD-cSelected"
SelectSoundRef="Svd-c_SN.svd-c_draw"
MeshRef="svd-c_A.SVDmesh"
SkinRefs(0)="svd-c_T.Svd_D_Sh"
SkinRefs(1)="KF_Weapons2_Trip_T.hands.BritishPara_Hands_1st_P"
ZoomMatRef="svd-c_T.SVDScopeFB"
}
KFWeapon.PreloadAssets() превращает строки в настоящие объекты: вызывает DynamicLoadObject() для меша, иконок HUD, звука доставания и каждого элемента SkinRefs, затем копирует их на заспавненное оружие. Если строка неверная, объект останется none. Оружие при этом стреляет, но его не видно и не слышно.
Подвох: кто вызывает PreloadAssets
В стандартном KF KFGameType.WeaponSpawned() вызывает KFPlayerController.ClientWeaponSpawned(), а эта функция представляет собой жёстко прописанный switch по каждому стандартному оружию (AA12, AK47, Benelli, Chainsaw, Crossbow и так далее). До PreloadAssets() кастомного оружия дело не доходит, и простая замена ссылок на строки оставляет его без модели и звуков.
Исправление должно жить в PlayerController. Если каждый автор оружия будет переопределять его сам, пушки начнут конфликтовать друг с другом, поэтому в теме договорились делать это на уровне ServerPerks / ScrnBalance:
- ScrnBalance уже вызывает
PreloadAssets()/UnloadAssets()для любого оружия с пустым мешем по умолчанию. - С модифицированным ServerPerks переопределите
ClientWeaponSpawned()иClientWeaponDestroyed()вKFPCServ. - Со стандартным ServerPerks остаётся ждать поддержки или ставить ScrnBalance рядом.
Универсальный ClientWeaponSpawned() от poosh с исправлением attachment от 22.01.2013:
simulated function ClientWeaponSpawned(class<Weapon> WClass, Inventory Inv)
{
local class<KFWeapon> W;
local class<KFWeaponAttachment> Att;
W = class<KFWeapon>(WClass);
// preload only weapons that have no static assets
if ( W != none ) {
if ( W.default.Mesh == none )
W.static.PreloadAssets(Inv);
Att = class<KFWeaponAttachment>(W.default.AttachmentClass);
if ( Att != none && Att.default.Mesh == none ) {
if ( Inv != none )
Att.static.PreloadAssets(KFWeaponAttachment(Inv.ThirdPersonActor));
else
Att.static.PreloadAssets();
}
PreloadFireModeAssets(W.default.FireModeClass[0]);
PreloadFireModeAssets(W.default.FireModeClass[1]);
}
}
simulated function ClientWeaponDestroyed(class<Weapon> WClass)
{
local class<KFWeapon> W;
local class<KFWeaponAttachment> Att;
W = class<KFWeapon>(WClass);
// a weapon with a static default mesh keeps its assets
if ( W != none && W.default.MeshRef != "" && W.static.UnloadAssets() ) {
Att = class<KFWeaponAttachment>(W.default.AttachmentClass);
if ( Att != none && Att.default.Mesh == none )
Att.static.PreloadAssets();
UnloadFireModeAssets(W.default.FireModeClass[0]);
UnloadFireModeAssets(W.default.FireModeClass[1]);
}
}
PreloadFireModeAssets() / UnloadFireModeAssets() вызывают одноимённую статическую функцию у KFFire, KFMeleeFire или KFShotgunFire, а затем у класса снаряда.
Без исправления от 22.01.2013 клиент мог вылететь, если переключался на вид от третьего лица (F4) или умирал с кастомной пушкой в руках.
Перевод оружия по шагам
- Класс оружия.
HudImage,SelectedHudImage,SelectSound,MeshиSkinsпереносятся вHudImageRef,SelectedHudImageRef,SelectSoundRef,MeshRefиSkinRefs. Используйте именноRef-имена: в одном из первых примеров темы забыли переименоватьHudImage. - Дополнительные ресурсы, например прицел: заведите свои ref-переменные и расширьте статические функции.
var string ZoomMatRef;
var string ScriptedTextureFallbackRef;
static function PreloadAssets(Inventory Inv, optional bool bSkipRefCount)
{
super.PreloadAssets(Inv, bSkipRefCount);
default.ZoomMat = FinalBlend(DynamicLoadObject(default.ZoomMatRef, class'FinalBlend', true));
default.ScriptedTextureFallback = texture(DynamicLoadObject(default.ScriptedTextureFallbackRef, class'texture', true));
if ( SVDc(Inv) != none ) {
SVDc(Inv).ZoomMat = default.ZoomMat;
SVDc(Inv).ScriptedTextureFallback = default.ScriptedTextureFallback;
}
}
static function bool UnloadAssets()
{
if ( super.UnloadAssets() ) {
default.ZoomMat = none;
default.ScriptedTextureFallback = none;
}
return true;
}
- Attachment.
MeshиAmbientSoundстановятся ref-строками. - Режимы огня.
FireSound,StereoFireSound,NoAmmoSound, а у оружия ближнего боя ещёMeleeHitSounds. - Снаряды. У самого
ProjectileнетPreloadAssets(). Либо наследуйтесь от стандартного класса, где она есть (CrossbuzzsawBlade,CrossbowArrow,M99Bullet,LAWProj,M79GrenadeProjectile,MP7MHealinglProjectile,PipeBombProjectile), либо вызывайте preload снаряда из своего режима огня. УShotgunBulletэтих функций тоже нет. Из стандартных дробовых снарядов меш или звуки есть только у гвоздя, так что большинству кастомной дроби ничего делать не нужно.
class MyShotgunBullet extends ShotgunBullet;
var string StaticMeshRef;
static function PreloadAssets()
{
UpdateDefaultStaticMesh(StaticMesh(DynamicLoadObject(default.StaticMeshRef, class'StaticMesh', true)));
}
static function bool UnloadAssets()
{
UpdateDefaultStaticMesh(none);
return true;
}
Грабли, о которых писали в теме
| Симптом | Причина | Что делать |
|---|---|---|
| Оружие невидимое и беззвучное | Никто не вызывает PreloadAssets() (стандартный ServerPerks) или в строке опечатка | Добавить хук в PlayerController и проверить каждую ref-строку |
| Другие игроки не видят модель от третьего лица | На пакет с мешем attachment больше ничто не ссылается, поэтому он не отправляется клиентам | Оставить хотя бы одну статическую ссылку на каждый пакет, например не переводить Mesh у attachment. Вариант с AddToPackageMap() из пикапа предлагался, но не проверялся |
Нет меша от первого лица, если игрок появился с этим оружием, после выброса и подбора он есть; в логе Playanim: Sequence 'Idle' not found for mesh | При начальном спавне ресурсы не подгружены | Убедиться, что стартовый инвентарь проходит через тот же preload-хук |
| Вылет на F4 или при смерти с кастомной пушкой | Старый ClientWeaponSpawned() без исправления attachment | Взять исправленную версию выше |
| Пушка, собранная копированием кода стандартного оружия, стреляет и наносит урон, от третьего лица видна, но меша от первого лица нет | По словам Dr_Killjoy, MeshRef поддерживается только для стандартных акторов | Использовать старую переменную Mesh (тема «[HELP] Не отображается меш оружия в игре.») |
ServerPerks сам добавляет .u оружия в ServerPackages, и клиенты скачивают все пакеты, на которые этот .u ссылается. Когда статическая ссылка заменена строкой, связанный .ukx/.utx/.uax может перестать отправляться. Звуки и текстуры обычно остаются привязаны через пикап (PickupSound, TraderInfoTexture), поэтому чаще всего пропадает именно меш attachment.
#exec OBJ LOAD FILE=... эту проблему не решает. Директива только говорит ucc, какие файлы загрузить при компиляции, и в скомпилированный .u не попадает.
А что с мобами и скинами
Мобов сознательно не трогали. Те, что прописаны в сквадах, рано или поздно всё равно появятся, и динамическая загрузка добавила бы только подлагивания между волнами. Если какие-то кастомные мобы не нужны, просто не загружайте их мутатор. Мобы, которые существуют только для !rtd или призыва админом, всё равно занимают память. Вопрос в теме подняли, но решения не нашли.
Скины персонажей (PlayerRecordClass) тоже обсуждали без готового решения.
Результаты и ограничения
- Guardiannknight не увидел заметной экономии на сервере, но после перевода почти всех пушек клиенты стали вылетать заметно реже.
- poosh держал монстров из Doom 3, Brute, Shiver, Jason и большой список кастомного оружия без проблем с памятью после перехода на refs.
- Часть вылетов вообще не связана с памятью. Отдельные карты (часть карт Murphy, HarbourV3, Pandora Box) роняли выделенные серверы после рестарта карты. Прежде чем винить память, проверьте сервер на официальных картах без кастомного контента.
- «Больше не значит лучше»: если убрать почти одинаковые стволы (AK47 / Golden AK47 / MKb42), память освободится, а геймплей почти не изменится.
Wormbo с форумов Epic предостерегал от универсального мутатора, который обнуляет defaults у произвольных классов. В движках до UE3 сборка мусора синхронная и может длиться секунды, defaults не сбрасываются при смене карты, а серверу могут быть нужны метаданные ресурсов (длительность звука, размер текстуры, кости меша). Сошлись на том, что переводить нужно само оружие.
Вопросы и ответы
- Занимает ли оружие из списка трейдера память, если его никто не купил?
- Да, если его классы ссылаются на меши, текстуры и звуки статически. ServerPerks загружает каждый класс пикапа из списка трейдера, и каждая статическая ссылка в цепочке (пикап, оружие, attachment, режимы огня, снаряды) подтягивает свои ресурсы в память.
- Хватит ли #exec OBJ LOAD, чтобы пакет ушёл клиентам?
- Нет. Директива только говорит компилятору, какие файлы загрузить во время компиляции. В .u она не сохраняется и ничего не добавляет в ServerPackages.