Творец. День шестой. Паства

Всем доброго утра (или как там у вас по Гринвичу?). Как всегда, ваш покорный слуга готов поделиться секретами gamedev’а. Сегодня мы добавим в нашу игру противников, дабы хоть как-то разнообразить геймплей.

Сегодня мы затронем тему ООП (объектно-ориентированного программирования). Поэтому – небольшой экскурс в теорию.
Среда Delphi основана на ООП. GLScene основана на ООП. Чем же занимается этот раздел программирования? Классами объектов и их упорядоченностью, а также
иерархией всех классов, которые используются в проекте. Примеры классов у Delphi – это: TForm, TButton, TTimer, TSocketConnection и т.д. Главная особенность всех их – они наследуют свойства своих родителей.
Возьмём на рассмотрение движок GLScene. Родителем всех классов является сам TGLScene, класс всех классов, от которого берут начало все остальные объекты. Дальше происходит разделение на видимые и невидимые. У них только одно свойство – Visible. Например, видимые – это: TGLSphere, TGLActor, LightSource. К невидимым относятся некоторые объекты (GLDummyCube – объект-пустышка, а также GLCamera) и почти все компоненты: GLCadencer, GLCollisionManager, GLDCEManager и т.д.
Каждый объект принадлежит определённому классу. Если мы пытаемся «свести» двух потомков, которые в том или ином колене имеют общего родителя, то всё пройдёт успешно, связь между ними установится, иначе появится ошибка о несовместимости классов. Это стоит учитывать, когда вы будете приводить один класс к другому (об этом ниже).

Open TEnemy
Чтобы уверенно управлять всеми противниками в игре, мы создадим новый класс TEnemy.

TEnemy = Class
 // процедура смерти монстра
 procedure WhatADeath(DeathBot: TEnemy);
 // процедура атаки.
 // У неё нет параметра,
 // т.к. атакуем только игрока
 procedure Attack();
public
 Health: integer;
 Model: TGLActor;
 Name: string;
end;

В новом классе указаны две процедуры и три свойства. Обратите внимание на то, что эти процедуры относятся только к классу TEnemy и больше ни у кого использоваться не могут.
Теперь мы можем оперировать этим классом где угодно – к примеру, это может быть массив из TEnemy. Добавим в раздел var проекта:

AllBots: array[0..100] of TEnemy;
countbot: integer;

Также нам понадобится DummyCube под именем Bots.

Выращиваем овец
При загрузке карты (процедура LoadMap) мы должны проверять наличие символа «e». В локальные переменные процедуры добавим Bot: TEnemy;.

Bots.DeleteChildren;
...
case line[k] of
...
’e’:
 begin
  // создаём нового противника
  Bot:=TEnemy.Create;
 Bot.Model:=TGLActor(Bots.AddNewChild(TGLActor));
  Bot.Name:=’Bot’+IntToStr(countbot);
 // Монстру и его модели присваиваем
 // одинаковое имя.
  Bot.Model.Name:=’Bot’+
  IntToStr(countbot);
  Bot.Model.Position.SetPoint(k,i,0);
  Bot.Model.LoadFromFile(GetCurrentDir+’\player\waste.md2’);
 // здесь все предварительные операции
 // с актёром: загрузка текстуры,
 // файла анимации,
 //  установка анимации и т.д.
 // (см. ”МБ” №40’2007)
 Bot.Health:=100;
 //добавим в массив нового монстра
 AllBots[countbot]:=Bot;
 // добавляем монстра в список проверки
 // столкновений
 with GetOrCreateCollision(Bot.Model) do
 begin
  BoundingMode:=cbmSphere;
  Manager:=CollisionManager1;
 end;
// добавляем физическую оболочку.
// (см.  ”МБ” №39’2007)
with GetOrCreateDCEDynamic(Bot.Model) do
 begin
  ...
 end;
 inc(countbot);
end;

20080203_2.jpg

Ведём овец на пастбище
Прописываем в процедуре CollisionManager1Collision следующий код:

for botc:=0 to countbot-1 do
 if Obj.Name = AllBots[botc].Name then AllBots[botc].WhatADeath(AllBots[botc]);

Не пугайтесь, мы ещё никого не убиваем, просто мы указываем, что какому-то монстру из нашего массива нанесли урон.
Теперь обращаемся к нашей процедуре WhatADeath.

// сила, которая применится к монстру
// после смерти
var
 Force: TAffineVector;
begin
 Force := NullVector;
 // уменьшаем здоровье монстра
 DeathBot.Health:=DeathBot.Health-15;
 // если здоровье равно нулю
 if DeathBot.Health< =0 then
  begin
   Force[0] := -50000;
    if Form1.Player.Position.X<DeathBot.Model.Position.X
  then
   // понятно, что толчок должен быть
   // направлен в сторону, куда
   // стреляет игрок.
   // Поэтому поворачиваем противника
   // в зависимости от положения игрока
  DeathBot.Model.RollAngle:=180
 else
  DeathBot.Model.RollAngle:=0;
 // применяем силу к монстру
 GetOrCreateDCEDynamic(DeathBot.Model).ApplyAccel(Force);
 if DeathBot.Model.CurrentAni-mation<>’death1’
 then
  // присваиваем анимацию смерти
 DeathBot.Model.SwitchToAni-mation(’death1’);
 end;
end;

Чтобы анимация смерти не «зацикливалась», в процедуре CadencerProgress пишем:

for botc:=0 to countbot-1 do
 if AllBots[botc].Model.CurrentFra-me=183 then
  // как только достигнут последний кадр
  // анимации смерти, останавливаем
  // проигрывание
 AllBots[botc].Model.AnimationMode:=aamNone;

Заблудшие овцы
Отлично! Теперь по файлу уровня мы можем расставить символы «e» и запустив игру, немножко поупражняться в стрельбе по статичным мишеням. В следующей статье мы поговорим о различных алгоритмах поиска пути (а их, поверьте, достаточно: волновой, поиск в глубину, поиск в ширину, алгоритм Дейкстры, Астар и др.).
А пока немножко поиграем в игру «Почему я балдею от ООП». А вот, собственно, почему. Представьте себе, что вы решили усложнить нашу игру. Вы захотели добавить новые виды монстров: орки, тролли, ведьмы, маги и т.д. Понятно, что у них появятся новые свойства, как например количество маны, количество силы, различных финтифлюшек, штучек-дрючек. Но ООП – специалист в области, как штучек, так и дрючек. Если вы всех противников сделали потомками TEnemy, то можете прыгать до потолка. Допустим, у нашего класса TEnemy есть процедура FindWay(x,y) (поиск пути с параметром «координаты искомой точки»).
Также предположим, что у нас имеется список AllEnemy: TList. Мы можем добавлять в него монстров так: AllEnemy.Add(Monster); а так использовать их, если знаем индекс x, нужного нам противника: (AllEnemy[x] as TMonster). Можем и удалить: AllEnemy.Delete(x).
А теперь – барабанная дробь – представьте, что наш игрок забрёл в комнату, где кишмя кишат всякого рода тёмные существа. Здесь может оказаться кто угодно (TTroll, TOrk, TMage), но все они – потомки класса TEnemy, и поэтому у всех их есть процедура Attack. И вот теперь, мы берём всю эту ватагу и одной строчкой (вы только подумайте!) посылаем на игрока:

for x:=1 to AllEnemy.Count do (AllEnemy[x] as TEnemy).Attack;

Если это будет стратегия, то мы можем к процедуре Attack приделать параметр Obj, чтобы армия атаковала определённый объект. Заметьте, что если вы будете использовать вместо списка массив, то индекс у ячеек лежит в отрезке от 0 до Count-1.

О динамических овцах
В моей игре я поставил ограничение на количество противников – 100. Но в более серьёзных играх их может быть и больше, верно? Если мы изначально не уверены, сколько ячеек будет в массиве, то используются динамические массивы. Организуются они ещё проще, чем обычные:

var
 AllEnemy: array of TEnemy;

Вдруг мы захотели, чтобы длина массива равнялась 14 (ну, взбредёт в голову):

SetLength(AllEnemy, 14);

Как говорится, приведено в исполнение.
Мы можем объявить и трёхмерный динамический массив (к примеру, для карты уровня):

Map: array of array of array of integer;

И обращаться к нему соответственно:

Map[x,y,z]:=1;

О создателях овец
Как вы уже убедились, правильно построенная система классов спасёт вас от бесконечного отлавливания багов в сотнях страницах кода. Не будь родителя TEnemy у TTroll и TOrk, пришлось бы вызывать процедуру атаки для каждого класса в отдельности. Вообще, над иерархией объектов полезно подумать заблаговременно.

Вот вам Золотые Правила Иерархии, которые я когда-то вычитал в «Игромании»:
Принцип первый. Если два класса, относящиеся к разным базовым классам, имеют одинаковые по смыслу и реализации свойства или методы, значит, вы что-то не так сделали. Необходимо пересмотреть всю систему и сделать её не избыточной.
Принцип второй. Пользуйтесь замечательным правилом Лезвия Оккама: не плодите сущностей сверх необходимости. Например, не стоит объединять всех игровых персонажей с зелёной чешуёй в отдельный класс, основным отличием которого является свойство «зелёночешуйчатости». Если только это свойство не ключевое для игры.
Принцип третий. Представьте себе, что вам нужно добавить в вашу игру: гиппогрифа, чемодан, танк со сменными колёсами, всем живым существам признак наркотического отравления, изменить у всех объектов проверку столкновений с боксовой на полигональную. Если все эти вещи вы сможете сделать без изменения структуры иерархии (имеется в виду изменение связей между старыми классами; добавление новых классов и свойств, естественно, разрешается) – значит, вы всё сделали правильно.
Иногда, гейм-дизайнеры предлагают супер-идею, из-за которой движок приходится переписывать с нуля. Чтобы проблем и израсходованного аспирина было меньше, пользуйтесь правилами – и будет вам счастье.

20080203_code_1.jpg

О будущем овец

В следующей статье мы поговорим об алгоритмах поиска пути. Если у вас есть предложения интересных тем для обсуждения (к примеру, разрывание монстров на куски или анимирование приёмов кунг-фу) – милости прошу на наш форум.


Рекомендуем почитать: