Вступление в US

(Материалы по UDK)

Модератор: Buxyr

Аватара пользователя
KrisGames

Универсал Коммандо Лидер
Командор
Сообщения: 1171
Рег. Ср апр 28, 2010 10:20 am
Награды: 3
Репутация: 428
Вклад в проект: 75
Откуда: Иваново
Благодарил (а): 129 раз
Поблагодарили: 19 раз

Вступление в US

Сообщение KrisGames » Пт май 18, 2012 11:26 am

Класс

В ОО логике класс представляет собой описание того, как будет "выглядеть" вещь, если бы она была создана. Класс является общим шаблоном. Он определяет абстрактные свойства (например, деталь машины имеет свой цвет), но, обычно, не определяет реальное проявление этих свойств в природе (например, какого цвета деталь). Класс также определяет поведение объекта. Набор специальных функций однозначно указывает, как будет "вести" себя объект в Вашей программе. На языке программистов, такие абстрактные свойства называются "переменными экземпляра" (instance variables), а функции, описывающие поведение объекта - "методами" (method). Процесс создания объекта на основе класса называется "реализацией" (instantiation).

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

Механика класса

В Unreal Script мы определяем классы посредством "объявления класса" (class declaration):

class MyClass expands MyParentClass;

Ключевое слово class говорит Unreal Script, что Вы объявляете новый класс. MyClass, в данном случае, является именем нового класса (имя может быть любое, но желательно, со смыслом, например, CarPart).

После того, как Вы объявили новый класс в Unreal Script, самое время определить переменные экземпляра. Все что Вам нужно для этого сделать - это указать список необходимых свойств:

var int color; // Номер цвета детали
var byte manufacturer; // ID (ссылка на) производителя

Довольно изящно будет, если Вы отделите переменные от основной части кода комментариями:

///////////////////////////////////////////////////
// Переменные экземпляра для MyClass

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

Определение методов в новом классе очень похоже на объявление переменных: просто составьте список необходимых функций:

function doAThing() {

// Выполнение некоторого кода UnrealScript
}

function doAnotherThing() {

// Другая функция, которая что-то делает
}

И снова, Вы можете отделить методы от остального кода при помощи комментариев. Также, рекомендуется давать методам "внятные" имена, отражающие его суть. Например, метод, моющий Ваши носки может быть назван cleanSocks()1.

В Unreal Script, создание объектов называется "spawning" (порождение). Вот как можно использовать функцию Spawn() для создания нового объекта из класса-шаблона:

var actor MyObject; // Переменная, содержащая (ссылающаяся на) объект
MyObject = Spawn(MyClass); // Порождение объекта

Краткое обсуждение методов

Объект является независимым контейнером данных, которые "висят" в памяти. Ключевым словом является именно независимость. Каждый объект выполняет свою работу. Если Вы описали класс MyBot и создали два объекта (экземпляра) из этого класса с именами BotA и BotB эти два объекта ничего друг о друге не знают и даже не подозревают о существовании друг друга. Это приводит нас к следующей концепции объектно-ориентированного программирования: объекты изменяют сами себя.

Когда создается объект он вставляется в хэш таблицу в памяти и ему присваивается индивидуальный ключ поиска, затем о нем забывается. Система находит объект по ключу в случае, если Вы хотите что-то с ним сделать, для всех остальных целей объект является "как бы" недоступным. Недоступность проявляется в том, что Вы не можете просто взять и поменять объект как обычную переменную. Вы не можете, например, открыть объект и изменить содержащиеся в нем переменные (это одно из основных отличий объектов от структур в языке С++). Вместо этого, Вы говорите объекту изменить самого себя. Это производится посредством методов.

Методы класса определяют, каким образом будет действовать объект после создания. Если Вы хотите изменить переменную экземпляра (объекта), Вам необходимо описать соответствующий метод, который позволяет это сделать. Наш класс CarPart может иметь, например, такой метод:

function setColor(int newColor)
{
color = newColor;
}

В данном примере, когда вызывается метод setColor(), объект принимает число в качестве аргумента и устанавливает значение цвета равное этому числу. То есть объект изменяет сам себя. Синтаксис вызова метода в Unreal Script выглядит следующим образом:

MyObject.setColor(15); // Говорим объекту, чтобы он вызвал метод setColor()


Методы, переменные, и защита объектов

Помните, я Вам говорил, что объекты изменяют сами себя? На самом деле это не совсем так. Я так говорил, т.к. хотел, чтобы Вы думали об объектах, как о независимых "существах" в Вашей программе. Говоря на чистоту, переменные экземпляра можно менять напрямую:

MyObject.color = 15;

Обратите внимание на отличие. В случае с методом, мы говорим объекту изменить себя, а в последнем - меняем природу объекта напрямую. Это иллюстрирует очередной момент "Как думают программисты". Вы, возможно, спрашиваете себя: "Если я могу изменять объект напрямую, то зачем мне вообще нужны эти методы, и какая от них польза?". Отлично. Подумаем об этом.

Допустим, у нас наложены некоторые ограничения на значение переменной color. Например, Вы хотите, чтобы переменная color могла принимать значения только от 1 до 10. Значения же не входящие в этот диапазон могут вызвать сбой программы или привести к нежелательным результатам. В этом случае будет не очень хорошо, если пользователю разрешено менять параметры напрямую. Верно? Даже, если Вы будете знать, что значения должны быть из промежутка от 1 до 10, другой человек, использующий Ваш код, может об этом и не подозревать.

Выход:
Cделайте Вашу переменную private ("частной"), т.е. такой, что только самому классу будет дозволено ее изменять:

var private int color; // Объявляем private переменную

Ключевое слово private называют "спецификатором доступа". Private указывает на то, что данная переменная доступна только изнутри объекта, а снаружи ее "не видно". Объект может изменить значение такой переменной посредством метода, но если попытаться обратиться к этой переменной с помощью прямого доступа:

MyObject.color = 15;

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

Семейство классов

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

Очень удобно проиллюстрировать объектные отношения на основе аналогии с машиной. Класс CarPart не дает нам полную картину, что же представляет собой деталь машины на самом деле. Учитывая то, что мы теперь знаем об объектах, мы, возможно, даже не будем использовать CarPart... вместо этого, мы опишем класс, например, SteeringWheel (руль). Это звучит более точнее и такой класс удобней использовать.

Фактически, лед тронулся. Мы уже мысленно построили отношения между CarPart и SteeringWheel. Действительно, SteeringWheel (руль) является разновидностью CarPart (детали). Верно? Так что, если класс CarPart опишет методы и свойства присущие всем возможным разновидностям деталей, а другие классы, такие как SteeringWheel, "расширят" его функциональность?

На языке программистов такие отношения называются "родительско-дочерние отношения". CarPart, в данном случае, играет роль "родителя" (или super класса) класса SteeringWheel. В Unreal Script дочерний класс объявляется следующим образом:

class SteeringWheel expands CarPart package(MyPackage);

Видите ключевое слово expands? Оно означает что класс, который мы объявляем (SteeringWheel) является "дочерним" классом CarPart. Как только Unreal встретит expands, он сразу сформирует отношения между соответствующими классами.

Принцип I: Наследование

Что же нам дают эти родительско-дочерние отношения, нам, пытающимся решить поставленную задачу? Да это существенно упрощает нам жизнь! При установлении такого рода отношений между двумя классами, дочерний класс мгновенно "наследует" свойства и методы родителя. И все это без написания и строчки кода: SteeringWheel обладает всей функциональностью CarPart. Если класс CarPart имеет метод setColor(), то SteeringWheel также имеет его. Наследование распространяется на переменные, на методы и на состояния (states).

Такой подход позволяет нам построить то, что программисты называют "Иерархия объектов" (или "Семейство классов"). Это выглядит как генеалогическое дерево:

Object
| expanded by
Actor
| expanded by
CarPart
| expanded by
SteeringWheel


Object и Actor являются специальными классами в Unreal (подробнее см. "UnrealScript Language Reference"). Полное дерево классов в Unreal представляет собой огромную "паутину" взаимоотношений, которую, Вы только можете себе представить. В нашем примере, у нас простое отношение типа "is-a":

A SteeringWheel is a CarPart (Руль есть Деталь)
A CarPart is an Actor (Деталь есть Актер)
An Actor is an Object (Актер есть Объект)

Каждый следующий "слой" в дереве наследования расширяет функциональность предыдущего. Это позволяет нам без труда описывать сложные объекты в рамках "входящих" в него объектов. Очень важно понимать, что отношения не коммутативны (взаимнообратны). SteeringWheel (руль) всегда является CarPart (деталью), но CarPart (деталь) не всегда является SteeringWheel (рулем). Двигаясь вверх по иерархии, мы двигаемся к простому, вниз - к более специфичному. Надеюсь, понятно? Отлично!

Э-э-э! Секундочку... если мы строим машину (car) и машина сделана из деталей, где же сам класс Car? Вот мы и нарвались на различия во взаимоотношениях: "is-a" ("есть") против "has-a" ("имеет"). Ясно, что CarPart (деталь) не является Car (машиной). С другой стороны отношение "CarPart expands Car" будут также неверно. Поэтому, лучше всего, построить иерархию таким образом:

Object
|
Actor
/ \
Car CarPart
|
SteeringWheel

Класс Car "происходит" от класса Actor, и, при этом, не имеет прямого отношения с CarPart (Вы можете называть их братьями). Вместо этого, класс Car может содержать в себе переменные типа CarPart. В этом случае, мы имеем отношения "has-a" ("имеет"). Car (машина) имеет SteeringWheel (руль), но SteeringWheel (руль) не является Car (машиной). Если Вы создаете иерархию классов и запутались, то очень удобно представить отношения фразами "имеет" и "есть".

Как видно из вышесказанного, иерархия отношений позволяет нам делать интересные вещи. Если нам вдруг захотелось, например, расширить понятие о машине, мы можем добавить Vehicle (транспорт):

Object
|
Actor
/ \
Part Vehicle
/ | | \
CarPart AirPart Car Airplane

Правда круто! Не только потому, что это является удобным способом организовать и визуально представить объекты, но и потому, что в силу наследования, отпадает необходимость копирования и переписывания кода!

Принцип II: Полиморфизм

Поли что? Очередное словечко из лексикона этих сумасшедших программистов. (Если Вы до этих пор понимали все, что я пытался втолковать, значит, Вы являетесь больше программистом, чем сами об этом думаете) Полиморфизм - одна из фундаментальных основ объектно-ориентированного программирования. При наследовании, дочерний класс "перенимает" переменные, методы и состояния (states) родительского класса... но что если мы хотим изменить наследованные элементы? В примере с машиной, Вы, возможно, захотите написать класс Pedal (педаль), в котором определите метод PushPedal() (нажатие педали). Когда вызывается PushPedal(), этот метод производит какое-то действие (например, активирует тормоза). Если, затем Вы расширите класс Pedal новым классом, например, AcceleratorPedal (педаль газа), метод PushPedal(), мягко говоря, становится некорректным. При нажатии педали газа, явно не должны включаться тормоза! (Иначе у Вас будут большие проблемы с законом при релизе Вашей программы, уж поверьте мне).

В этом случае, Вы должны заменить унаследованное поведение педали другим. Это можно сделать посредством "полиморфизма" или "перегрузкой функции". Вы будете часто использовать этот прием, программируя на Unreal Script. Пояснение что же такое полиморфизм я позаимствовал у Tim Sweeney:

[Перегрузка функции] подразумевает под собой написание новой версии функции в подклассе (дочернем классе). Например, Вы пишете код для нового вида монстра - Demon. Класс Demon, который Вы создали, расширяет класс Pawn. Что происходит, когда Pawn видит игрока - вызывается функция [SeePlayer()] и Pawn начинает его атаковать. Неплохо, но что, если, скажем, Вы хотите по-своему определить поведение Demon в Вашем классе.

Для этого, просто переопределите эту функцию в дочернем классе. Когда создается объект этого класса, он будет обладать новым поведением, а не заимствованным у родителя. Если Вы хотите запретить переопределение функции подклассам, то воспользуйтесь ключевым словом final при объявлении метода:

function final SeePlayer()

Таким образом, Вы застрахуетесь от перегрузки функции в "производных" классах. Очень разумно использовать это для поддержки целостности поведения в Вашем коде. Tim Sweeney, также, указывает на то, что использование final повышает производительность в Unreal.


Автор
© 1998-2002 Brandon Reinhart
Перевод сделан 32_Pistoleta.


ICQ : 470-451-451
Mail: KrisGames@yandex.ru

Вернуться в «Конструктор UDK»

 

 

cron