Программирование звуковых игр при помощи BGT: Краткий справочник языка скриптов

Дата публикации:05.01.2015
Дата обновления:25.02.2015
Поделиться в Twitter Поделиться в F******k Поделиться в VKontakte Поделиться в Telegram Поделиться в Mastodon

Содержание

  1. Предварительные замечания
  2. Пространства имён
  3. Перечисления
  4. Определение класса
  5. Конструкторы и деструктор
  6. Исключения
  7. Ключевое слово this
  8. Наследование
  9. Ключевое слово override
  10. Абстрактные классы
  11. Ключевое слово final
  12. Константные методы
  13. Ключевое слово super
  14. Переопределение методов
  15. Контроль доступа к свойствам
  16. Ссылки
  17. Дескрипторы объектов
  18. Дескрипторы функций
  19. Приведение типов
  20. Перегрузка операторов

Предварительные замечания

Платформа для разработки звуковых игр BGT включает в себя скриптовый язык AngelScript и среду для исполнения таких скриптов. В статье Основы программирования звуковых игр на языке скриптов BGT были изложены базовые сведения, необходимые для начального освоения BGT. В данной публикации будет кратко рассказано о других возможностях языка скриптов. Материал может оказаться полезным и для тех, кто осваивает AngelScript не только для разработки звуковых игр.

Предполагается, что читатель знаком с такими понятиями, как пространство имён, классы и объекты, наследование, полиморфизм, перегрузка и переопределение методов, интерфейсы, приведение типов и т.д. В противном случае рекомендуется обратиться к соответствующей литературе или интернет-ресурсам.

Приводимые далее примеры кода носят иллюстративный характер и не являются полностью работоспособным кодом или его фрагментами. Задача примеров состоит лишь в том, чтобы продемонстрировать использование тех или иных конструкций языка скриптов и не более того.

Пространства имён

Для определения пространства имён используется ключевое слово namespace,за которым должен следовать идентификатор (имя) определяемого пространства (анонимность не допускается), а затем в фигурных скобках должны располагаться константы, переменные, классы и прочие сущности, входящие в это пространство. Пример:

 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. Полное имя теперь будет включать все идентификаторы пространств, например, configuration::ini_files::reader или configuration::xml_files::reader.

Перечисления

Перечисление (тип в виде набора именованных значений) можно определить при помощи ключевого слова enum, например:

enum ship 
{
	corvette,
	frigate,
	battleship,
	cruiser, 
	aircraft_carrier
}

Определение класса

Определение класса начинается с ключевого слова class, за которым следует имя класса. Затем в фигурных скобках располагается тело класса -- его свойства и методы:

class plane
{
	double cruising speed;
	int crew;
	// Другие свойства класса...

	void fly ()
	{
		// ...
	}

	void land ()
	{
		// ...
	}

	// Другие методы класса...
}

Все свойства и методы класса по умолчанию являются открытыми. Изменить это можно при помощи спецификатора доступа private. Свойства и методы, отмеченные как private, доступны только в самом классе и в его подклассах (о подклассах см. Наследование).

Конструкторы и деструктор

В классе можно определить два специальных метода: конструктор и деструктор (точнее, конструкторов может быть несколько, а деструктор только один). Конструктор имеет то же самое имя, что и класс; может принимать параметры, но ничего не возвращает. В имени деструктора к имени класса добавляется префикс в виде символа ~ ("тильда"). Деструктор не принимает параметры и не возвращает результат.

class plane
{
	// Конструктор.
	plane ()
	{
	// ...
	}

	// Деструктор.
	~plane ()
	{
	// ...
	}
}

Конструктор можно использовать для инициализации свойств класса (то есть для присваивания свойствам значений по умолчанию), например:

class commando
{
	string callsign;
	vector position;
	int health;
	int armour;

	// Конструктор без параметров.
	commando ()
	{
		callsign = "Delta";
		health = 100;
		armour = 100;
		}
		
		// Другие методы класса...
}

Результат вызова конструктора (даже внутри другого конструктора того же класса) -- это новый объект класса.

Исключения

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

Ключевое слово this

В любом методе для ссылки на сам объект можно использовать ключевое слово this. Кроме прочего, это бывает необходимо для разрешения неоднозначности с именами переменных, когда имена параметров метода совпадают с именами свойств, например:

class commando
{
	string Callsign;
	// Другие свойства класса...


	// Конструктор с параметром.
	commando (string Callsign)
	{
		this.Callsign = Callsign;
	}

}

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

Поддерживается механизм одиночного наследования. суперкласс (базовый класс) указывается в определении класса после его имени и отделяется от него символом : ("двоеточия"), например:

class fighter : plane
{
// ...
}

Класс fighter является производным от класса plane или, по-другому, его подклассом.

Ключевое слово override

Если метод класса помечен как override, то компилятор проверит, действительно ли этот метод переопределяет существующий метод суперкласса. Если у суперкласса такого метода нет, то появится сообщение об ошибке. Ключевое слово override следует располагать после сигнатуры метода перед открывающей фигурной скобкой. Пример:

class alpha
{
	void method1()
	{
	}
}

class beta
{
	void method1() override
	{
	}
	
	// В суперклассе нет метода method2,
	// поэтому его нельзя переопределить и компилятор выдаст 
	// сообщение об ошибке!
	void method2() override
	{
	}
}

Абстрактные классы

Класс может быть помечен как абстрактный при помощи ключевого слова abstract. Это позволяет исключить создание объектов (экземпляров) этого класса. Например:

abstract class unit
{
}

class farmer : unit
{
}
// ...
// Ошибка: нельзя создать объект абстрактного класса!
@units[0] = unit;

// Правильно: farmer -- это не абстрактный класс!
@unit[0] = farmer;
// ...

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

Внимание! В скриптах BGT использование ключевого слова abstract вызывает ошибку компиляции.

Ключевое слово final

Если в определении класса добавить ключевое слово final, то этот класс станет последним в иерархии наследования, то есть при попытке создать производный класс компилятор выдаст сообщение об ошибке. Многие из встроенных классов BGT, например, sound, определены как final. Пример:

final class Game
{
// Свойства и методы класса...
}

Использование ключевого слова final не ограничивается вышеописанным вариантом. Оно также используется для обозначения методов класса, которые нельзя переопределять в подклассах. При попытке сделать это компилятор выдаст сообщение об ошибке. Ключевое слово final нужно поместить после сигнатуры метода и перед открывающей фигурной скобкой, например:

class enemy
{
	void say(string text) final
	{
		// ...
	}
}

Константные методы

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

class gun 
{
	private int ammo;
	// Другие свойства и методы класса...
	
int get_ammo() const
	{
		// Ошибка: нельзя изменить свойства класса
		// внутри константного метода!
		ammo = 100;

		// ...
		
		// Правильно: значение свойства не изменяется.
		return ammo;
	}
}

Ключевое слово super

В конструкторе производного класса можно вызывать конструктор базового класса при помощи ключевого слова super.

Если в конструкторе подкласса отсутствует явный вызов конструктора суперкласса при помощи 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);
}

Такие ссылки используются только для передачи параметров и возвращаемых значений в методах и функциях.

У таких ссылок есть различные варианты, определяемые ключевыми словами:

  • &in -- используется для передачи по ссылке параметра, изменение которого внутри функции не скажутся на исходном значении. Такой вид ссылок рекомендуется использовать совместно с ключевым словом const в описании параметра;
  • &out -- используется для передачи ссылки на изменяемый параметр, через который будет возвращён результат работы функции или метода (если функция или метод должны вернуть более одного результата);
  • &inout -- комбинация первых двух. То же самое, что и просто &.

Дескрипторы объектов

Дескриптор объекта в BGT проще всего представлять как переменную, указывающую на некоторый объект. Через эту переменную можно получить доступ к свойствам и методам объекта, на который она указывает. При объявлении такой переменной её тип обозначается постфиксом в виде символа @ ("собака") вслед за именем класса, на объект которого она будет указывать:

sound@ shot;

Это пример дескриптора для объекта класса sound. Этот дескриптор никак не инициализирован, то есть указывает на пустоту. Для инициализации дескриптора достаточно присвоить ему новый объект соответствующего класса:

sound@ shot;
@shot =  sound;
shot.load("sounds\\shot.wav");

Когда необходимо обратиться к самому дескриптору , то его имя нужно предварять префиксом @, а когда нужно получить доступ к свойству или методу объекта, на который указывает дескриптор, этот префикс добавлять не надо.

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

sound s;
sound@ sh1;
sound@ sh2;

@sh1 = s;
@sh2 = s;
if (sh1 is sh2)
{
	// ...
}

Также при помощи is выполняется проверка пустого дескриптора:

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 объявить тип функции, дескриптор которой предполагается использовать. Любая функция будет принадлежать объявленному типу, если совпадает число и типы аргументов и тип возвращаемого значения. Пример объявления типа my_function_type:

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);
}

Приведение типов

Поддерживается динамическое приведение типов, . Синтаксически это оформляется с помощью ключевого слова cast, за которым в угловых скобках следует имя класса, а затем в круглых скобках имя дескриптора объекта, который необходимо привести к указанному типу. В случае успешного выполнения возвращается дескриптор, ссылающийся на объект указанного типа; в случае неудачи возвращается null.

// В коде инициализации уровня...
@units[j] = farmer();

// В процессе игры потребовалось
// вызвать метод, поддерживаемый только классом farmer...
farmer@ f = cast<farmer>(units[i]);
if @f!=null)
{
	farmer.chop_wood();
}

Интерфейсы

Для объявления интерфейса служит ключевое слово interface, за которым следует имя интерфейса, а затем в фигурных скобках указываются его методы, например:

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 в объявлении класса.

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;
}

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

Методы, собранные в следующую таблицу, соответствуют унарным операторам. Эти методы не принимают параметров, а воздействуют на сам объект. Для наглядности в скобках указаны операции, которые эти операторы реализуют для целых чисел.

Имя методаОператор
opNeg- (унарный минус )
opCom~ (инверсия)
opPreInc++ (префиксный инкремент)
opPreDec-- (префиксный инкремент)
opPostInc++ (постфиксный инкремент)
opPostDec-- (постфиксный декремент)

Ниже представлены методы, соответствующие бинарным операторам. Бинарные операторы требуют двух операндов, но данные методы получают лишь один параметр (это правый операнд), левым операндом является сам объект (в методах с суффиксом _r ситуация обратная -- левый операнд передаётся в качестве параметра, а правым является сам объект).

Имя методаОператор
opAdd и opAdd_r+
opSub и opSub_r-
opMul и opMul_r*
opDiv и opDiv_r/
opMod и opMod_r%
opAnd и opAnd_r&
opOr и opOr_r|
opXor и opXor_r^
opShl и opShl_r<<
opShr и opShr_r>>
opUshr и opUshr_r>>>

Ниже представлены методы, связанные с различными операторами присваивания.

Имя методаОператор
opAssign = (присвоить значение)
opAddAssign+=
opSubAssign -=
opMulAssign *=
opDivAssign /=
opModAssign %=
opAndAssign &=
opOrAssign |=
opXorAssign ^=
opShlAssign <<=
opShrAssign >>=
opUshrAssign >>>=

Индексный оператор или оператор индексирования обозначается квадратными скобками и представляет собой операцию извлечения элемента массива по указанному индексу. Чтобы перегрузить оператор индексирования, необходимо определить метод 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 в соответствии со следующими правилами:

  • Вернуть 0, если первый и второй операнды совпадают.
  • Вернуть отрицательное число, если первый операнд меньше второго.
  • Вернуть положительное число, если первый операнд больше второго.


Распространение материалов сайта означает, что распространитель принял условия лицензионного соглашения.
Идея и реализация: © Владимир Довыденков и Анатолий Камынин,  2004-2024