Добавлен , опубликован

Основы UnrealScript: Учимся на практике

Содержание:

Понятие классов

В мире много одинаковых вещей. Яблоки на деревьях, автомобили одной модели, капли дождя... Зная, как поведет себя одна вещь, мы можем рассчитать, как поведет себя другая такая же вещь. И пускай в будущем, какая-то из них может измениться или вовсе исчезнуть, изначально они все равно одинаковы. В какой-то мере, можно сказать, что все одинаковые вещи принадлежат к одному классу. Подобный принцип работает и в UDK. Нам необходимо, чтобы все снаряды, оружия и транспортные средства вели себя одинаково, в таком случае, в аналогии с машинами, которые создаются по определенному чертежу, нашими чертежами выступят классы. Каждый класс содержит переменные, функции и прочие свойства, которые определяют поведение объекта. Также, как и машины, созданные по "чертежу" класса объекты могут меняться в последствии, но суть у них остается одна и та же.
В итоге, делаем крайне важный итог - все представители класса создаются одинаковыми
Покажем на примере. Откройте наш TestActor. Измените код, чтобы весь скрипт выглядел следующим образом.
class TestActor extends Actor
    placeable;

var() int MyInt;

function PostBeginPlay()
{
    `log(self @ MyInt);
}

defaultproperties
{
    MyInt=4

    Begin Object Class=SpriteComponent Name=Sprite
        Sprite=Texture2D'EditorResources.S_NavP'
        HiddenGame=True
    End Object
    Components.Add(Sprite)
}
Скомпилируйте, откройте редактор и сделайте так, чтобы на открытой вами карте было два экземпляра нашего актора. Сохраните карту и запустите игру, после чего загляните в лог. Там мы обнаружим следующее:
[0010.61] ScriptLog: TestActor_0 4
[0010.61] ScriptLog: TestActor_1 4
Как видим, все экземпляры одного класса называются именем самого класса и порядковым номером, начиная, по аналогии с массивами, с нуля. Поскольку ни один из экземпляров не был изменен, MyInt для обоих равен 4. Но как же определить, кто есть кто? Выделите один из акторов и посмотрите в самый низ. Там, немного левее полей с масштабированием, появится надпись в стиле Persistent Level.TestActor_1 Selected. Так и определяется. Теперь щелкните на акторе дважды, чтобы открыть свойства. Найдите наш MyInt (кто не помнит, он должен быть в разделе TestActor) и поменяйте его, к примеру, на 23. Сохраните уровень и запустите игру. Лог выдаст следующее:
[0007.76] ScriptLog: TestActor_0 4
[0007.76] ScriptLog: TestActor_1 23
Все представители класса создаются одинаковыми, после чего могут легко изменяться. Это крайне важный принцип объектно-ориентированного программирования. Он освобождает нас от создания отдельного файла для каждого экземпляра в отдельности.

Наследование

Наследование - второй важный принцип ООП.
Припустим, у вас в игре будет 4 типа оружия - пистолеты, автоматы, дробовики и снайперские винтовки. Если подумать, у них будет очень много общего. У каждого типа будут такие свойства, как урон, максимальные патроны, точность и т.д.. Вместо того, чтобы создавать эти свойства в каждом из типов, мы можем просто сделать так, чтобы они наследовали их от определенного общего класса оружия, и уже менялись так, чтобы обладать разной функциональностью.
Для пущей ясности, рассмотрим классы оружия через UnCodeX. Найдите в древе классов следующий путь Actor\Inventory\Weapon\UDKWeapon. Под следующим пунктом, UTWeapon, мы увидим разные типы оружия.
В списке можно увидеть все предоставленные в UDK виды оружия - UTBeamWeapon (Link Gun, наше стартовое оружие), TWeap_RocketLauncher и UTWeap_ShockRifleBase. Все они действуют по разному, но принцип работы в них один. Заглянув в UTWeapon, мы найдем следующий код:
/** Initial ammo count if in weapon locker */
var int LockerAmmoCount;

/** Max ammo count */
var int MaxAmmoCount;

/** Holds the amount of ammo used for a given shot */
var array<int> ShotCost;
Вещи типа MaxAmmoCount и ShotCost являются общими для всех видов оружия, так что, вместо того, чтобы заново писать их в каждом классе отдельно, мы объявили их в родительском классе, UTWeapon.
К слову, раз уж речь зашла об оружии, почему бы нам не создать новое?
Создайте в папке классов новый текстовый документ и назовите его TestGun.uc. Откройте в редакторе и поместите туда следующий код:
class TestGun extends UTWeap_RocketLauncher_Content;

defaultproperties
{
    FireInterval(0)=0.1
    ShotCost(0)=0
}
Скомпилируйте. Если у вас возникли вопросы типа "А как оно скомпилировалось? Я же не создал ни одной новой переменной!", заглянем в скрипты повыше в иерархии. ShotCost мы уже видели в UTWeapon, в строке 27
/** Holds the amount of ammo used for a given shot */
var array<int> ShotCost;
Эта переменная определяет, сколько патронов будет тратиться на один выстрел.
Заглянув еще выше, в Weapon, мы найдем FireInterval
/** Holds the amount of time a single shot takes */
var() Array<float> FireInterval;
Эта переменная определяет промежуток времени между двумя выстрелами из этого оружия.
Это интересно: Обратили внимание на нули после переменных? Дело в том, что оружия в UDK изначально рассчитаны на несколько режимов стрельбы, и все данные, характеризующие стрельбу, являются динамическими массивами. В случае UT это сделано для стрельбы через правую и левую кнопки мыши (кто не знает, в UT каждое оружие имеет два в принципе разных режима стрельбы), в обычных же играх это можно использовать для реализации стрельбы очередями, одиночными, и автоматической стрельбы, не говоря уже о также абсолютно разных режимах, типа подствольного гранатомета и т.д..
Когда мы создаем новый класс, все переменные, функции и свойства из классов в иерархии выше также автоматически создаются. Это избавляет нас от кучи дублированного текста, так как все, что будет общим в подклассах, объявляется только один раз. Чтобы понять, что у нас уже есть, очень важно время от времени читать исходники наших родительских классов, это убережет нас от изобретения велосипеда, колеса и других транспортных средств, тем самым сэкономив немало времени.
В нашем случае, использование ShotCost и FireInterval позволило нам создать, по сути, новое оружие, не переписывая заново принципы его работы.
Пора уже поместить наше оружие на уровень. Откройте любую карту, откройте Actor Classes, найдите Pickups\Weapon\UTWeaponPickupFactory (для последних версий UDK точно этот путь, для более старых может отличаться), и перетяните на уровень. Выглядеть эта штука будет так:
Кликните на нее дважды, и в первом же разделе, напротив Weapon Pickup Class, выставьте TestGun. Поскольку спавнеры оружия являются путевыми точками, размещение оного потребует перестройки путей. Сделать это можно с помощью кнопки сверху.
Сохраните карту и запустите игру. Пробежите над размещенным спавнером, чтобы подобрать оружие, и опробуйте его в действии.
Важно: если у вас при старте игры не появляется интерфейс, и при подбирании оружия ничего не меняется, откройте меню View, выберите пункт World Properties, разверните категорию Game Type и напротив строчки Default Game Type из выпадающего списка выберите UTDeathmatch.
Итак, мы имеем архичитерское оружие. Что же мы сделали? Изменение ShotCost на 0 сделало так, что мы не тратим патроны при выстреле, в итоге, у нас бесконечный боезапас, а значение 0.1 в FireInterval заставляет наше оружие делать по 10 выстрелов в секунду.
Важно помнить, что все переменные и функции новый класс наследует только от классов выше. Для демонстрации небольшой пример.
Добавьте в наш скрипт TestGun.uc переменную MyInt
class TestGun extends UTWeap_RocketLauncher_Content;

var int MyInt;

defaultproperties
{
    FireInterval(0)=0.1
    ShotCost(0)=0
}
Создайте новый класс AnotherGun.uc, используя TestGun
class AnotherGun extends TestGun;

defaultproperties
{
    MyInt=4
}
Скомпилируйте. Все прекрасно скомпилировалось, так как MyInt существует в TestGun. Теперь немного поменяйте AnotherGun, чтобы оно походило не от TestGun, а от его родительского класса UTWeap_RocketLauncher_Content
class AnotherGun extends UTWeap_RocketLauncher_Content;

defaultproperties
{
    MyInt=4
}
В этом случае мы получим ошибку
Warning, Unknown property in defaults: MyInt=4
Вот такие пироги.

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