|
Содержание
Предварительные замечанияПлатформа для разработки звуковых игр BGT включает в себя скриптовый язык AngelScript и среду для исполнения таких скриптов. В статье Основы программирования звуковых игр на языке скриптов BGT были изложены базовые сведения, необходимые для начального освоения BGT. В данной публикации будет кратко рассказано о других возможностях языка скриптов. Материал может оказаться полезным и для тех, кто осваивает AngelScript не только для разработки звуковых игр. Предполагается, что читатель знаком с такими понятиями, как пространство имён, классы и объекты, наследование, полиморфизм, перегрузка и переопределение методов, интерфейсы, приведение типов и т.д. В противном случае рекомендуется обратиться к соответствующей литературе или интернет-ресурсам. Приводимые далее примеры кода носят иллюстративный характер и не являются полностью работоспособным кодом или его фрагментами. Задача примеров состоит лишь в том, чтобы продемонстрировать использование тех или иных конструкций языка скриптов и не более того. Пространства имёнДля определения пространства имён используется ключевое слово namespace audio { const int MONO = 1; const int STEREO = 2; } Полное имя переменной или класса, входящих в пространство имён, состоит из имени пространства, за которым следуют два двоеточия (::), и имени самой переменной или класса. Полное имя нужно указывать во всех случаях, когда обращение происходит за пределами того пространства имён, в котором была определена переменная, константа или класс. Пример: // ... void main () { func (audio::MONO); // ... } void func (int mode) { switch (mode) { case audio::MONO: // ... break; case audio::STEREO: // ... break; } } Пространства имён могут быть вложенными, например: namespace configuration { namespace ini_file { class reader { // ... } } namespace xml_files { class reader { // ... } } } В примере определены пространство имён configuration и два вложенных в него пространства ini_files и xml_files. Полное имя теперь будет включать все идентификаторы пространств, например, ПеречисленияПеречисление (тип в виде набора именованных значений) можно определить при помощи ключевого слова enum ship { corvette, frigate, battleship, cruiser, aircraft_carrier } Определение классаОпределение класса начинается с ключевого слова class plane { double cruising speed; int crew; // Другие свойства класса... void fly () { // ... } void land () { // ... } // Другие методы класса... } Все свойства и методы класса по умолчанию являются открытыми. Изменить это можно при помощи спецификатора доступа Конструкторы и деструкторВ классе можно определить два специальных метода: конструктор и деструктор (точнее, конструкторов может быть несколько, а деструктор только один). Конструктор имеет то же самое имя, что и класс; может принимать параметры, но ничего не возвращает. В имени деструктора к имени класса добавляется префикс в виде символа ~ ("тильда"). Деструктор не принимает параметры и не возвращает результат. class plane { // Конструктор. plane () { // ... } // Деструктор. ~plane () { // ... } } Конструктор можно использовать для инициализации свойств класса (то есть для присваивания свойствам значений по умолчанию), например: class commando { string callsign; vector position; int health; int armour; // Конструктор без параметров. commando () { callsign = "Delta"; health = 100; armour = 100; } // Другие методы класса... } Результат вызова конструктора (даже внутри другого конструктора того же класса) -- это новый объект класса. ИсключенияОбработка исключений не поддерживается. По этой причине в конструкторах не рекомендуется выполнять такую инициализацию, которая может завершиться ошибкой (например, проверка подключения к интернет) и эту ошибку предполагается обработать программно не в теле конструктора, а в коде, создающем объект. Ключевое слово thisВ любом методе для ссылки на сам объект можно использовать ключевое слово class commando { string Callsign; // Другие свойства класса... // Конструктор с параметром. commando (string Callsign) { this.Callsign = Callsign; } } НаследованиеПоддерживается механизм одиночного наследования. суперкласс (базовый класс) указывается в определении класса после его имени и отделяется от него символом : ("двоеточия"), например: class fighter : plane { // ... } Класс fighter является производным от класса plane или, по-другому, его подклассом. Ключевое слово overrideЕсли метод класса помечен как override, то компилятор проверит, действительно ли этот метод переопределяет существующий метод суперкласса. Если у суперкласса такого метода нет, то появится сообщение об ошибке. Ключевое слово class alpha { void method1() { } } class beta { void method1() override { } // В суперклассе нет метода method2, // поэтому его нельзя переопределить и компилятор выдаст // сообщение об ошибке! void method2() override { } } Абстрактные классыКласс может быть помечен как абстрактный при помощи ключевого слова abstract class unit { } class farmer : unit { } // ... // Ошибка: нельзя создать объект абстрактного класса! @units[0] = unit; // Правильно: farmer -- это не абстрактный класс! @unit[0] = farmer; // ... Абстрактным может быть только класс целиком; абстрактные методы не поддерживаются, поэтому если в абстрактном классе есть методы, то они должны быть определены (то есть иметь сигнатуру и тело). Внимание! В скриптах BGT использование ключевого слова Ключевое слово finalЕсли в определении класса добавить ключевое слово final class Game { // Свойства и методы класса... } Использование ключевого слова class enemy { void say(string text) final { // ... } } Константные методыКлючевое слово class gun { private int ammo; // Другие свойства и методы класса... int get_ammo() const { // Ошибка: нельзя изменить свойства класса // внутри константного метода! ammo = 100; // ... // Правильно: значение свойства не изменяется. return ammo; } } Ключевое слово superВ конструкторе производного класса можно вызывать конструктор базового класса при помощи ключевого слова Если в конструкторе подкласса отсутствует явный вызов конструктора суперкласса при помощи super, то компилятор неявно подставляет вызов конструктора суперкласса без параметров. В таком случае если у суперкласса отсутствует конструктор без параметров, то ещё на этапе компиляции возникнет ошибка. При помощи ключевого слова class enemy { int health; int power; enemy (int health, int power) { this.health = health; this.power = power; } } class robot : enemy { int ai_level; robot (int health, int power, int ai_level) { // Явно вызываем нужный нам конструктор суперкласса. super(health, power); this.ai_level = ai_level; } } Переопределение методовВсе методы класса являются виртуальными, поэтому подкласс может переопределить любой метод суперкласса (исключая конструкторы и деструктор). Для переопределения метода необходимо, чтобы в подклассе был реализован метод с той же сигнатурой (то есть именем и списком параметров), что и в базовом классе. Чтобы вызвать базовую реализацию перекрываемого метода, необходимо к имени метода добавить префикс из имени базового класса и двух символов двоеточия , например: class robot : enemy { // ... void move (int direction) { // Вызвать реализацию метода из суперкласса. enemy::move(direction); // Делаем что-то ещё. } } Контроль доступа к свойствамВ скриптах BGT свойства класса доступны объектам других классов, если не помечены как private. Однако в AngelScript это всего лишь поведение по умолчанию и его можно изменить при помощи так называемых аксессоров (или сеттеров и геттеров), которые позволяют контролировать доступ к свойствам класса. Это даёт возможность сделать, например, отдельные свойства доступными только для чтения или только для записи. Общий синтаксис определения аксессоров следующий: class MyObj { // Виртуальное свойство с аксессорами. int prop { get const { return realProp; } set { // Новое значение сохраняется в скрытом свойстве. realProp = value; } // Реальное свойство, хранящее соответствующее значение. private int realProp; } } Если set-аксессор не установлен, то виртуальное свойство будет доступно только для чтения; если не установлен get-аксессор, то виртуальное свойство будет доступно только для записи. Несмотря на то что у встроенных классов BGT (например, sound) используются аксессоры, для пользовательских скриптов эта возможность заблокирована. СсылкиBGT поддерживает передачу параметров и возврат результата по значению и по ссылке. Использование ссылок позволяет избежать затрат памяти и времени, необходимых на создании локальной копии объекта при передачи его в качестве параметра. Для передачи параметра по ссылке необходимо в сигнатуре метода к типу соответствующего параметра или возвращаемого значения добавить символ & ("амперсанд"), например: class alpha { string name; alpha (string name) { this.name = name; } } void byValue (alpha a) { a.name ="Beta"; } void byReference (alpha& a) { a.name = "Beta"; } void main () { alpha a("Alpha"); // Этот вызов не изменяет a, // так как передается копия этого объекта. byValue(a); alert("Info", "Name: " + a.name); // Этот вызов меняет a, // так как передаётся ссылка на объект. byReference(a); alert("Info", "Name: " + a.name); } Такие ссылки используются только для передачи параметров и возвращаемых значений в методах и функциях. У таких ссылок есть различные варианты, определяемые ключевыми словами:
Дескрипторы объектовДескриптор объекта в BGT проще всего представлять как переменную, указывающую на некоторый объект. Через эту переменную можно получить доступ к свойствам и методам объекта, на который она указывает. При объявлении такой переменной её тип обозначается постфиксом в виде символа @ ("собака") вслед за именем класса, на объект которого она будет указывать: sound@ shot; Это пример дескриптора для объекта класса sound. Этот дескриптор никак не инициализирован, то есть указывает на пустоту. Для инициализации дескриптора достаточно присвоить ему новый объект соответствующего класса: sound@ shot; @shot = sound; shot.load("sounds\\shot.wav"); Когда необходимо обратиться к самому дескриптору , то его имя нужно предварять префиксом @, а когда нужно получить доступ к свойству или методу объекта, на который указывает дескриптор, этот префикс добавлять не надо. Ключевое слово sound s; sound@ sh1; sound@ sh2; @sh1 = s; @sh2 = s; if (sh1 is sh2) { // ... } Также при помощи if (sh1 !is null) { //... } Дескриптор суперкласса может указывать на объект подкласса, что, по сути, и есть реализация полиморфизма. Пример: class unit { void update () { alert("Info", "unit::update"); } } class farmer : unit { alert("Info", "farmer::update"); } class warrior : unit { alert("Info", "warrior::update"); } void main () { // Массив из двух дескрипторов объектов типа unit. units@[] = units(2); // В реальной игре здесь // должна быть инициализация игрового уровня, // например по карте из файла. @units[0] = farmer(); @units[1] = warrior(); // В игровом цикле перебираем все объекты // и обновляем их состояние. for (int i=0; i<units.length() && !key_pressed(KEY_SPACE); ++i) { units[i].update(); } } Дескрипторы позволяют управлять выделением и освобождением памяти более эффективно, чем это делает встроенный уборщик мусора. Например, если дескриптору, ссылающемуся на существующий объект, присвоить значение null, то это приведёт к уничтожению объекта (при условии, что нет других дескрипторов, ссылающихся на этот объект), то есть будет вызван деструктор этого объекта и память, занятая объектом, будет освобождена. Дескрипторы функцийВ скриптах поддерживается такой вид дескрипторов, как дескрипторы функций, которые можно передавать в качестве параметров других функций и методов. Из-за строгой типизации в AngelScript для этого необходимо сначала при помощи ключевого слова funcdef void my_function_type(); Объявленный таким образом тип можно использовать так же, как и любой спецификатор типа. Для того чтобы объявить дескриптор функции достаточно к спецификатору типа добавить символ @ ("собака"). Пример передачи дескриптора функции в качестве параметра другой функции: // Объявление типа функции. funcdef void my_function_type(); // Принимает дескриптор функции в качестве первого параметра. void do_n_times(my_function_type@ what, uint n) { for(uint i=0; i<n; i++) { what(); } } // Дескриптор этой функции будет передан // в качестве параметра. void print_a_message1 () { alert("Сообщение #1", ""); } // Ещё одна функция, дескриптор которой // будет передан в качестве параметра. void print_a_message1 () { alert("Сообщение #2", ""); } // Главная функция скрипта. void main() { do_n_times(print_a_message1, 3); do_n_times(print_a_message2, 5); } Приведение типовПоддерживается динамическое приведение типов, . Синтаксически это оформляется с помощью ключевого слова // В коде инициализации уровня... @units[j] = farmer(); // В процессе игры потребовалось // вызвать метод, поддерживаемый только классом farmer... farmer@ f = cast<farmer>(units[i]); if @f!=null) { farmer.chop_wood(); } ИнтерфейсыДля объявления интерфейса служит ключевое слово interface usable { bool on_use (human@ user); } В определении класса, реализующего один или несколько интерфейсов, имена интерфейсов перечисляются в списке наследования через запятую после суперкласса (если таковой имеется), например: class unit { // Свойства и методы класса. } class human ;: unit { int health; human () { health = 100; } void improve_health (int value) { health += value; if (health>100) health = 100; } // Другие методы класса... } // Класс аптечки, // который может быть использован // (поддерживает интерфейс usable). class medkit : unit, usable { int value; // Другие свойства класса... // Реализация метода, // специфичная для аптечки. bool on_use (human@ user) { if @user!=null) { if (value>0) { user.improve_health(value); value = 0; } } // Другие методы класса... } Для интерфейсов тоже предусмотрены дескрипторы и обозначаются они символом @ после имени интерфейса. Такой дескриптор может ссылаться на любой объект, реализующий данный интерфейс, например: class medkit: usable { // Свойства и методы класса... } class key : usable { // Свойства и методы класса... } class spray : usable { // Свойства и методы класса... } const int MEDKIT = 0; const int SPRAY = 1; const int KEY_RED = 2; const int KEY_YELLOW = 3; const int KEY_GREEN = 4; class superman : warrior { // Свойства и методы класса... // Инвентарь -- массив дескрипторов интерфейса usable. usable@[] inventory(5); @inventory[MEDKIT] = medkit; @inventory[SPRAY] = spray; @inventory[KEY_RED] = key(); @inventory[KEY_YELLOW] = key(); @inventory[KEY_GREEN] = key(); //... // Используем аптечку по нажатию клавиши M. if (key_pressed(KEY_M)) { inventory[MEDKIT].use(this); } // ... Mixin-классыНесмотря на то, что множественное наследование не поддерживается, в скриптах предусмотрен механизм mixin-классов, которые нельзя инстанцировать, но можно использовать в списках наследования для классов, не принадлежащих одной иерархии. Ключевое слово mixin class storage { void store(string data) { // ... } string restore () { // ... } } class farmer : unit, storage { // ... } class game: storage { // ... } Здесь класс storage может содержать универсальную реализацию сохранения и восстановления данных в виде методов store и restore. Эти методы могут быть вызваны в любом из классов, унаследованных от storage. В mixin-классе можно использовать свойства и методы, которые не определены в самом mixin-классе, но будут определены в производном классе. Например: mixin class mix_me { void method1() { // Присваиваем значение свойству, // которое в этом классе отсутствует. prop = 100; } } class beta : mix_me { int prop; beta() { method1(); } } Методы mixin-класса переопределяют методы суперкласса. (то есть класса, указанного первым в списке наследования). Свойства mixin-класса, совпадающие со свойствами суперкласса, игнорируются. Например: // Mixin-класс. mixin class mix_me { int prop = 99; // ... } // Суперкласс. class alpha { int prop = 66; // ... } // Подкласс. class beta : alpha, mix_me { beta() { // Покажет: a = 66. alert("Info", "a=" + a); } } Перегрузка операторовВ скриптах поддерживается перегрузка операторов, то есть изменение действий, связанных с такими операциями, как сложение, вычитание, умножение и так далее. Перегрузка операторов возможна только для объектов конкретного класса и делается это при помощи переопределения методов с предопределёнными именами. Например, в следующем примере для класса gun переопределяется метод opAddAssign, который соответствует оператору += (сложение с присваиванием). После этого становится возможным прибавлять к gun целое число (в игре такое действие может соответствовать перезарядке оружия): class gun { int ammo = 0; void opAddAssign (int count) { ammo+=count; } } void main () { gun shotgun; // Добавили десять патронов. shotgun += 10; } Обсуждение того, в каких случаях следует, а в каких не следует использовать механизм перегрузки операторов, не является предметом данной статьи, поэтому ограничимся лишь перечислением доступных для переопределения методов. Методы, собранные в следующую таблицу, соответствуют унарным операторам. Эти методы не принимают параметров, а воздействуют на сам объект. Для наглядности в скобках указаны операции, которые эти операторы реализуют для целых чисел.
Ниже представлены методы, соответствующие бинарным операторам. Бинарные операторы требуют двух операндов, но данные методы получают лишь один параметр (это правый операнд), левым операндом является сам объект (в методах с суффиксом _r ситуация обратная -- левый операнд передаётся в качестве параметра, а правым является сам объект).
Ниже представлены методы, связанные с различными операторами присваивания.
Индексный оператор или оператор индексирования обозначается квадратными скобками и представляет собой операцию извлечения элемента массива по указанному индексу. Чтобы перегрузить оператор индексирования, необходимо определить метод opIndex. Этот метод получает индекс элемента в качестве единственного параметра и должен вернуть значение, соответствующее этому индексу. Пример перегрузки оператора индексирования для класса vector: class vector { double x,y,z; // ... double& opIndex(int i) { if(i==0) { return x; } else if(i==1) { return y; } else if(i==2) { return z; } } } В этом примере метод возвращает не просто значение, а ссылку на соответствующую координату вектора. Это позволяет использовать оператор индексирования для присваивания нового значения соответствующей координате: vector v; v[1] = 42; Для того чтобы перегрузить операторы эквивалентности (== и !=), необходимо определить один метод с именем opEquals. Метод должен принимать один параметр, который является вторым операндом, и возвращать значение типа bool. Например: class commando { string callsign; // ... bool opEquals (commando@ other) const { return this.callsign==other.callsign; } } // Где-нибудь в коде... if (commando1 != commando2) { // Делаем что-нибудь... } Для того чтобы перегрузить операторы сравнения (<, <=, > и >=), необходимо определить один метод с именем opCmp. Этот метод должен принимать один параметр, который является вторым операндом, и возвращать значение типа int в соответствии со следующими правилами:
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Распространение материалов сайта означает, что распространитель принял условия лицензионного соглашения. Идея и реализация: © Владимир Довыденков и Анатолий Камынин, 2004-2024 |
Социальные сети