Sunday, April 7, 2013

Огнестрельное оружие в UDK - Основы

Автор: Mougli
Перевод: Vadakuma
Оригинал: Оружие - Основы

Вступление

   Сегодня мы будем говорить о базовых знаниях и настройках касающиеся создания оружия.
UDN Weapon System Technical Guide (которую я называю WSTG) дает нам некоторые понятия: как работает механизм переключения оружия, как работает блок отвечающий за режим огня (даже по сети), в чем разница между функциями в классе Weapon и его дочерних классов.




   Страничка Setting Up Weapons рассказывает нам как смоделировать и импортировать оружие, и какой код надо написать... для UTWeapons.

И все же, после прочтения всего этого, мы до сих пор не знаем некоторые вещи:

• Как добавить оружие в стандартный инвентарь.
• Установить различные виды выстрелов для оружия.
• Какие функции необходимо переписать чтобы управлять некоторыми характеристиками оружия.
• Как работает система урона.

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


Исходная точка: InventoryManager

   Как мы уже говорили, этот класс, находящийся в папке Engine ( а там уже нет GameInventoryManager или UDKInventoryManager начиная с Октября 2010 (возможно от них не было толку)) следит за всем тем, что наш игрок имеет в "сумке".

Примечание: InventoryManager завязан на Pawn, а не на PlayerController.

   Еще одна важная вещь которую мы должны знать о классе InventoryManager, это то что он просто отслеживает статус текущего оружия: значения PendingFire, о котором мы читали в WSTG. Хотя этот массив инициализируется не в InventoryManager. Другими словами, база InventoryManager не позволяет стрелять с помощью него (наделаете забавных ошибок, если попытаетесь так сделать).По умолчанию для класса UDKPawn инвентарем заправляет как раз InventoryManager, поэтому нам надо создать подкласс от последнего, для того чтобы научиться стрелять.

class SandboxInventoryManager extends InventoryManager;

DefaultProperties
{
    PendingFire(0)=0
    PendingFire(1)=0
}

   А вот и он. Крутатенюшка. Сначала вы добавляете PendingFire столько, сколько хотите режимов выстрела в своей игре. Если вы не хотите альтернативного вида выстрела, вы можете забыть о второй ячейки массива. И если же вы хотите больше чем два альтернативных выстрела, отлично, можно сделать столько сколько нужно.

   Пр. переводчика: PendingFire - массив, содержащий количество и статус альтернативных режимов выстрела одного и того же оружия. К примеру, PendingFire(0)=0-означает что оружие не стреляет, PendingFire(0)=1 - оружие в режиме огня.По умолчанию, Режим выстрела(Fire mode) 0 активируется по ЛКМ, а Режим выстрела(Fire mode) 1,по ПКМ.

   Как я отметил ранее, InventoryManager связан с Pawn, поэтому нам необходимо сказать нашему pawn,чтобы он использовал наш собственный InventoryManager:

class SandboxPawn extends UDKPawn;

DefaultProperties
{
    InventoryManagerClass=class'Sandbox.SandboxInventoryManager'
}


Лепим оружие (скрипты)

   Мы можем создать собственное оружие двумя путями: во-первых мы можем сначала написать поведение оружия( эффекты выстрела, снаряды) и лишь потом сделать для него внешний вид. Пример physics gun как раз демонстрирует, что оружие не особо нуждается в модели, для того чтобы полноценно функционировать. Для примера создадим paintball gun.


Шаг 0: несколько подсказок о настройке оружия

   Статья WSTG дает нам целый набор настроек для Weapon и UDKWeapon классов. Некоторые более важные, некоторые менее. Среди них есть массивы , значения которых определяют настройки для каждого режимы огня:

    •FiringStatesArray (array): задается через имя. Которое говорит о статусе оружия в руках и о режиме огня. По умолчанию не используется. Вы можете назначить любое значение, которое пожелаете, если не хотите заново изобретать велосипед. (Пр. Переводчика: этого параметра уже нет в скриптах)

    •WeaponFireType (array): задается через EWeaponFireType. Говорит, к какому типу принадлежит оружие. Мы доберемся до него чуть позже.

    •WeaponProjectiles (array): задается через подкласс снарядов. Если соответствующий режим огня устанавливается на выстрелы определенными снарядами, вы должны знать какой класс снарядов должен вызываться для данного оружия.

    •FireInterval (array): задается через float. Указывает сколько времени занимает выстрел в данном режиме огня. Другими словами – темп стрельбы.

    •Spread (array): задает погрешность в направлении выстрела (как бы "разброс"). Иначе, все пули(снаряды) будут лететь всегда точно в цель. Что не правдоподобно.

    •WeaponRange: задается через a float. Определяет максимальную дальность выстрела, урон, время жизни снаряда. Обратите внимание, что это значение является общим для всех режимов огня.


Шаг 0.5 : добавляем наше оружие в инвентарь

   Мы сделаем это так, что бы легко было проследить за сутью реализации кода.
Когда player перезапускается (т.е. начало игры, рестарт игры, воскрешение игрока), класс Pawn создается из GameInfo и прикрепляется к PlayerController, и GameInfo вызывает функцию AddDefaultInventory, принимая  новый Pawn как параметр.
   По умолчанию не делается ничего кроме вызова функции AddDefaultInventory в классе Pawn. И это хорошо по многим причинам. Тем не менее, имеется базовый класс игры. И скорее всего вы будете иметь еще подкласс от класса Pawn (для персонажа). AddDefaultInventory из класса GameInfo можно переписать, если у вас есть какие то общие параметры, настройки для всех классов(например, для ножа).

Итак, в нашем собственном Pawn классе, переписываем функцию AddDefaultInventory вот так :

function AddDefaultInventory() 
{
    //InvManager is the pawn's InventoryManager 
    InvManager.CreateInventory(class'Sandbox.SandboxPaintballGun');
}


   Все просто. Функция имеет и второй необязательный параметр, если его значение TRUE, то будет предотвращаться немедленная экипировка оружия. Теперь, если мы запустим игру, мы ничего не увидим, ввиду того что у оружия нету собственного меша. И все же, если написать в консоли волшебную команду "showdebug weapon", мы увидим следующее в верхнем левом углу экрана:

   Зеленая часть текста говорит нам о оружии которое сейчас держит игрок, и подтверждает что наша пушка готова к использованию!


Шаг 1: Параметры оружия

   Статья WSTG рассказывает нам как обрабатываются выстрелы( в том числе и в сетевом режиме). Скорее всего мы так и не коснемся упомянутых функций, за исключением , пожалуй BeginFire, к которой мы обратимся чуть позднее. Давайте начнем разбираться что же у нас происходит в блоке WeaponFiring. ???

   Рассмотрим блок WeaponFiring, в функции BeginState, мы вызываем FireAmmunition, затем еще TimeWeaponFiring, которые будут вызывать задержку стрельбы соответствующего режима огня, определяющуюся в массиве FireInterval. Если по какой-либо причине, нужна задержкаыстрелов при стрельбе, переписать эту часть функции будет правильным решением.

Когда отведенное время закончится, сработает RefireCheckTimer. Он проверяет две вещи:

1. Была ли команда перестать стрелять оружие. Если да, то происходит выполнение команды.
2. Если команды перестать оружия не было, происходит запрос функции ShouldRefire. Если возвращается TRUE, сразу вызывается FireAmmunition без выхода из блока.


   Если же ничего из перечисленного не случилось, означает что мы перестали стрелять, поэтому вызывается HandleFinishedFiring, который по умолчанию переводит управление оружием в блок Active.


   Внимание: Как вы могли уже заметить, первый выстрел( или вызов FireAmmunition) работает несколько другим способом, нежели последующие выстрелы. Так, если вам нужно прописать какое то действие, срабатывающее перед выстрелом, правильнее всего сделать это в внутри функции FireAmmunition, а не в BeginState.


   Функция ShouldRefire проверяет есть ли боеприпасы у оружия (функция HasAmmo по умолчанию всегда возвращает TRUE), если вам еще хочется пострелять. (функция StillFiring по умолчанию возвращает значение функции PendingFire).
   WSTG показывает нам тело функции FireAmmunition (от старой версии UTWeapon, хотя все основные идеи сохранились и там). Сперва используются боеприпасы, (по умолчание ничего не происходит), а затем определяется режим огня, после чего, в соответствии с режимом, вызываются различные функции.
   Другими словами, режим огня установленный на оружии, более или менее, говорит с какими функциями нам следует поработать:

EWFT_InstantHit: InstantFire() Скоротечный огонь
EWFT_Projectile: ProjectileFire() Огонь снарядами
EWFT_Custom: CustomFire() Что-то самодельное

Прежде чем двигаться дальше, давайте установим в нашем оружии конкретный режим огня.

class SandboxPaintBallGun extends UDKWeapon;
DefaultProperties
{
    FiringStatesArray(0)=WeaponFiring //We don't need to define a new state
    WeaponFireTypes(0)=EWFT_InstantHit FireInterval(0)=0.1 Spread(0)=0
}

Скоротечный огонь.(Instant Fire)

   Если взглянуть на функцию InstantFire, то можно увидеть что в ее теле вызывается некая Trace, для того что бы найти, куда же мы направили наше оружие. Не очень хочется связываться с этим. В конце функции есть вызов ProcessInstantHit. Вот его то мы и перепишем.
   В нашем случае, полностью меняем содержание этой функции, потому как мы просто хотим переместить decal в помеченную точку. Что получилось:


simulated function ProcessInstantHit(byte FiringMode, ImpactInfo Impact, optional int NumHits)
{
    WorldInfo.MyDecalManager.SpawnDecal (
        DecalMaterial'HU_Deck.Decals.M_Decal_GooLeak',// UMaterialInstance used for this decal. 
        Impact.HitLocation, // Decal spawned at the hit location. 
        rotator(-Impact.HitNormal), // Orient decal into the surface. 
        128,
        128, // Decal size in tangent/binormal directions.
        256, // Decal size in normal direction.
        false, // If TRUE, use "NoClip" codepath.
        FRand() * 360, // random rotation
        Impact.HitInfo.HitComponent // If non-NULL, consider this component only.    
    )
}


Все это нам дает следующий результат:


   На видео показано как я использовал Decal Material. Кстати говоря, учтите что такая реализация специально выглядит не очень красиво, да и с точки зрения производительности нецелесообразно, а является лишь доказательством работоспособности данного подхода к написанию собственного вида оружия.

   Кроме того, если вы заметили, по умолчанию получившееся оружие находится в режиме "full auto"(автоматическое). А что если необходим режим одиночного выстрела(тоесть нужно нажимать на кнопку каждыйраз когда надо выстрелить)?
Хорошо, пусть это будет вашим домашним заданием! Пара подсказок:

- PendingFire обнуляется лишь когда происходит вызов StopFire
- По умолчанию StillFiring предполагает что у PendingFire есть значение TRUE, поэтому оружие должно продолжить стрелять.


Шаг 2: Отображение оружия

   У нас есть работающее оружие, пришло время придать ему форму. Для этого мы должны сделать две вещи: добавить меш оружия, и разместить его в правильном месте. Как и следовало ожидать есть разница между видом оружия от третьего лица и от первого. Мы все сделаем как надо для обоих видов. Но сперва, пропишем то, что необходимо для каждой из ситуаций: прикрепляем к оружию ее модель:


DefaultPropeties
{
     Begin Object class=SkeletalMeshComponent Name=GunMesh
         SkeletalMesh=SkeletalMesh'WP_LinkGun.Mesh.SK_WP_Linkgun_3P'
         HiddenGame=FALSE
         HiddenEditor=FALSE
     End Object
     Mesh=GunMesh
     Components.Add(GunMesh)
}


   Я использую меш Link Gun для примера. Вообще используются различные виды модели одного и того же оружия в зависимости от вида камеры в игре. Если вы посмотрите в AnimSet Editor на оружие, то заметите, что оно смещено в сторону. Вероятнее всего это предварительное смещение, для адекватного позиционирования оружия.


   Так же как и меш от pawn, меш оружия должен быть смещен по оси Х, иначе оружие будет иметь неправильную позицию ( И все же, в контенте UDK, модели Shock Rifle от 1-ого лица и link gun от 3-его выглядят неправильно повернутыми( хотя это неважно для меша который используется для вида от 3-его лица)). В итоге, вы имеете два решения : повернуть модель с помощью кода, или сказать своему 3д-артисту, что он плохой человек и ему следует повернуть модель в 3д редакторе. :)
   С этим покончили, теперь оружие прорисовывается. Вам придется верить мне на слово, потому как на данный момент его нельзя увидеть, т.к. оно расположено где-то за "горизонтом" (Возможно в центре локации, но я не проверял).
   Обратите внимание, что все настройки меша находятся в MeshComponent, это означает что ваша модель может быть как и static, так и skeletal. По умолчанию предполагается что оружие это skeletal меш( и настроен скелет для использования на оружии различной анимации), поэтому skeletal мешем пользуются в 99% случаев.
   Единственный вариант, который я вижу, где уместно использовать static, это оружие ближнего боя при игре от 3-его лица. А physics gun вообще демонстрирует, что в некоторых случаях оружию просто не требуется меш.


Вид оружия с камерой от первого лица. (First Person perspective)

   Класс UDKWeapon имеет в себе функцию SetPosition, она вызывается при каждой последующей прорисовке кадра. По умолчанию она ничего не делает, но ее можно переписать так чтобы она поместила модель оружия в правильное место, по образу и подобию как действует та же функция, только в UTPawn. Несколько грубоватое решение, но со своей задачей справляется.


simulated event SetPosition(UDKPawn Holder)
{ 
    local vector FinalLocation;
    local vector X,Y,Z;

    Holder.GetAxes(Holder.Controller.Rotation,X,Y,Z);
    FinalLocation= Holder.GetPawnViewLocation(); //this is in world space. 
    FinalLocation= FinalLocation- Y * 12 - Z * 32; // Rough position adjustment 
    SetHidden(False); 
    SetLocation(FinalLocation); 
    SetBase(Holder); 
    SetRotation(Holder.Controller.Rotation);
}
   Если вы читали мои туторы про камеры, то возможно поняли, что именно я тут сделал.По некоторым причинам, вызовы SetHidden(false) и SetBase необходимы. Вот что получается в результате.


   Вид оружия с камерой от третьего лица.(Third Person Perspective)

А вот это намного проще. И не требует знаний пространственной геометрии. :) Все что нам надо, так это прицепить модель оружия к соответствующему сокету.
   Что бы сделать это, нужно использовать функцию AttacWeaponTo из класса UDKWeapon, которая присоединит SkeletalMeshComponent к соответствующему сокету. Но дело в том, что по умолчанию, эта функция не вызывается( наверно потому, что не знает куда присоединять оружие). Быстрый осмотр класса UTWeapon показал нам, что все это должно делаться в TimeWeaponEquipping, что в общем, имеет смысл. Перепишем эти функции:



simulated function TimeWeaponEquipping()
{ 
    AttachWeaponTo( Instigator.Mesh,'WeaponPoint' );
    super.TimeWeaponEquipping();
} 

simulated function AttachWeaponTo( SkeletalMeshComponent MeshCpnt, optional Name SocketName ) 
{
    MeshCpnt.AttachComponentToSocket(Mesh,SocketName); 
}

Не правда ли все просто!? А вот результат наших стараний:



   Однако, мы только присоединили оружие к игроку, мы не трогали никаких настроек самого оружия,(такие как месторасположение оружия), что может привести к некоторым проблемам, например, при выстреле снаряда. Это означает, что нам надо сделать кое-что в функции SetPosition() :

simulated event SetPosition(UDKPawn Holder) 
{ 
    local SkeletalMeshComponent compo; 
    local SkeletalMeshSocket socket; 
    local Vector FinalLocation; 
    compo = Holder.Mesh; 
    if (compo != none) 
    { 
        socket = compo.GetSocketByName('WeaponPoint');
        if (socket != none) 
        { 
            FinalLocation = compo.GetBoneLocation(socket.BoneName);
        }
    } 
    //And we probably should do something similar for the rotation :) 
    SetLocation(FinalLocation);
}

No comments:

Post a Comment