Программирование игр, создание игрового движка, OpenGL, DirectX, физика, форум
GameDev.ru / Программирование / Статьи / Объекты в играх: организация игрового цикла.

Объекты в играх: организация игрового цикла.

Автор:

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

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

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

А связанный список (linked list) мы выбираем, потому что это - один из наиболее удобных способов хранения последовательности данных произвольной длины. Для реализации списка можно, например, воспользоваться соответствующими шаблонами из STL (Standard Template Library). Использовать шаблон списка из STL можно так:

#include <list> 
typedef std::list <MyDataType> MyList; 
// Наш связанный список
typedef std::list<MyDataType>::iterator MyListIterator; 
// Итератор, используется для последовательной обработки списка

Здесь MyDataType - это наш тип данных, которые мы хотим хранить в списке. Цикл обработки данных в списке может выглядеть так:

MyList List; // Собственно список
MyDataType* pData = NULL; // Наши данные

MyListIterator iterator = List.begin(); // Настраиваемся на первый элемент списка
// И производим итерации до тех пор, пока не достигнут его конец
while(iterator != List.end ())
{
  pData=*iterator; // Получаем указатель на текущий элемент списка
  ...
  // Что-то делаем с данными:
  iterator++; // Перемещаемся к следующему элементу
}

Чтобы поместить элемент в начало списка, можно вызвать List.push_front(pData), а в конец списка - List.push_back(pData); для удаления элемента можно использовать List.erase(iterator), эта функция возвращает новое значение итератора. List.clear() очищает список, и наконец, List.empty () возвращает ненулевое значение, если список не пустой. Более подробное описание не привожу - я хотел лишь вкратце пояснить использование этих шаблонов.

Ближе к делу - наши объекты

Итак, собственно объекты. Чтобы максимально эффективно спроектировать базовый класс, сначала нужно определить, что общего будет у всех наших объектов. Пусть поборники чистоты C++ смотрят на меня с неодобрением, но все методы и члены данных здесь я объявляю как public - хотя вы, если хотите, можете написать функции типа Get()/Set().

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

class CObject 
{
public:
  RECT   rcRect;   // Окружающий объект прямоугольник
  POINT  ptPosn;   // Экранные координаты объекта
  BOOL   bAnim;    // Есть ли анимация
  BOOL   bDie;     // Флаг "смерти" - объект нужно уничтожить?

  BYTE   Frame;    // Текущий кадр
  BYTE   Counter;  // Счетчик для анимации

  CObject() {}
  virtual ~CObject() {}

  void Kill() { bDie = TRUE; }
  BOOL Collision(CObject* pObject);

  virtual void Blit() = 0;
  virtual BYTE GetType() = 0;
  virtual void Process(CObject* pObject) = 0;
  virtual BOOL Move() = 0;
};

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

Метод  Назначение
void Kill();  Метод вызывается для уничтожения объекта
BOOL Collision(Cobject* pObject);  Возвращает TRUE в случае столкновения с другим объектом
void Blit();  Рисует объект на экране
BYTE GetType();  Возвращает тип объекта (пуля, взрыв, монстр, и т.д.)
void Process(CObject* pObject);  Обработка взаимодействия объекта с другими объектами,
может вызываться несколько раз на протяжении
одной итерации игрового цикла
BOOL Move();  Обработка внутренних данных объекта - изменение счетчиков,
координат, и т.д. Вызывается один раз за
итерацию игрового цикла и возвращает FALSE
в случае, если объект нужно уничтожить.

Обработка списка объектов

Обработку объектов можно разделить на две части: изменение экранных координат и прочих внутренних данных (метод Move()) и обработка их взаимодействия - вызов метода Process() каждого элемента списка с аргументом, равным поочередно всем остальным элементам.

typedef  CObject *LPOBJECT;
typedef std::list <LPOBJECT > MyList; 

typedef std::list<LPOBJECT >::iterator MyListIterator; 

// Наш список объектов
MyList List;

// начало игрового цикла 
while(game_loop)
{
  // Часть 1 - обработка внутренних данных, и т.д.
  MyListIterator iterator = List.begin();  // Настраиваемся на первый элемент:
  CObject* pObject;

  // Для всех элементов:
  while(iterator != List.end())
  {
    pObject=*iterator;
    if(!pObject->Move())  // Объект просит себя уничтожить?
    {
      delete pObject;     // Удаляем объект 
      // Удаляем элемент списка и получаем следующий
      iterator = List.erase(iterator);     
    }
    else
    iterator++; // Следующий элемент
  } // while

  // Часть 2 - обработка взаимодействия
  iterator = List.begin();
  while(iterator != List.end())
  {
    pObject=*iterator;
    MyListIterator iterator2 = List.begin();
    CObject* pObject2;

    while(iterator2 != List.end())
    {
      pObject2 = *iterator2;

      // Проверяем:
      if(pObject && pObject2 && pObject != pObject2)
        pObject->Process(pObject2); // И обрабатываем

      iterator2++;
    } // while
    iterator++;
  } // while
} // конец игрового цикла 

Вывод на экран осуществляется просто:

for(MyListIterator iterator=List.begin(); iterator != List.end(); iterator++)
{
  pObject=*iterator;
  pObject->Blit();
}

И напоследок, после выхода из игрового цикла необходимо удалить все объекты в списке:

for(MyListIterator iterator=List.begin(); iterator !=List.end(); iterator++)
{
  pObject=*iterator;
  delete pObject;
}
// Очищаем список
List.clear();

Итак, мы умеем обрабатывать список объектов и у нас есть базовый класс, от которого эти объекты нужно наследовать.

Пример объекта

Теперь мы можем начать проектировать реальный класс, описывающий поведение конкретного объекта. Пусть это будет, например, взрыв. Для начала, определимся с его свойствами:
·  Он должен прорисовывать себя на экране, увеличивая счетчик анимации;
·  По окончании анимации он должен быть удален;
·  Игрок при контакте с этим объектом должен быть уничтожен.

Для анимации взрыва будем использовать вот такую последовательность кадров:

Изображение

// Наследуем CExplosion от CObject
class CExplosion : public CObject
{
public:
  // x, y - экранные координаты взрыва
  CExplosion(int x, int y);   
  virtual ~CExplosion();

  // Возвращает тип объекта - взрыв
  BYTE GetType()  { return OBJECTTYPE_EXPLOSION; }


  // Функции обработки
  void Process(CObject* pObject);
  BOOL Move();

  // Функция прорисовки
  void Blit();
};

Опишем методы обработки данных:

void CExplosion::Process(CObject* pObject)
{
  // на всякий случай:
  if(bDie) return;

  // Получим тип объекта, с которым взаимодействуем:
  BYTE Type = pObject->GetType();

  // Если другой объект - взрыв, дальнейшая обработка не нужна
  if(Type== OBJECTTYPE_EXPLOSION) return;

  // Если есть столкновение:
  if(Collision(pObject))
  {
    switch(pObject->GetType())
    {
      // Взрывом задело объект типа "игрок"
      case OBJECTTYPE_PLAYER:
        pObject->Kill(); // Убиваем игрока
        break;
      default:
        break;
    } // switch
  } // if
} // Process()


BOOL CExplosion::Move()
{
  // Уничтожить?
  if(bDie) return FALSE;

  // Изменяем счетчик анимации
  if(Counter < 3) Counter++; 
  else
  {
    Counter = 0;
    if(Frame < 8) // Изменяем текущий кадр
      Frame++;
    else
      return FALSE; // Уничтожить
  }
  return TRUE; // Не уничтожать
} // Move()

Теперь, когда у нас есть класс CExplosion, мы можем очень легко добавить взрыв прямо в игровом цикле, например, так:

// Добавляем взрыв в позиции (100, 100)
CExplosion* pExplosion = new CExplosion(100, 100);
List.push_front(pExplosion);

Возможности применения

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

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

Специально для демонстрации этого метода я написал простенькую игрушку Green Invaders (чем-то похожую на старую игру Space Invaders). Почему "Green"? Просто враги, нарисованные в меру моих художественных способностей, зеленого цвета, и это название - первое что мне пришло в голову.

Для компиляции вам понадобится Microsoft Visual C++ (все нормально компилировалось под шестой версией), DirectX7 SDK и NukeDX (http://www.nukesoftware.com) - это замечательная библиотека классов - позволяет очень удобно работать с DirectX7 API, загружать данные, и т.д. Зачем в сотый раз создавать первичную и вторичную поверхности, и так далее, когда все уже сделано? Библиотека проста в освоении и удобна - но это уже другая история:

Итак, дерзайте! Пусть у нас появится еще много хороших игр!

Исходники вы можете скачать здесь: 20030506.zip.

4 апреля 2003

#игровой цикл, #игровые объекты


Обновление: 18 ноября 2009

2001—2018 © GameDev.ru — Разработка игр