?

Log in

No account? Create an account
18 авг, 2007 @ 15:01 Модульная архитектура игрового движка
При работе над архитектурой игрового движка (на C++) появилось следующее соображение. Если игра состоит из нескольких глав или режимов, каждый из которых загружает свои ресурсы и отличается своей игровой логикой, то каждый режим или главу удобно оформить в виде отдельного класса со своими Init/Release Resources, Process и Draw. Ниже описана архитектура такого приложения, и я буду благодарен за любые конструктивные замечания и советы. Возможно, есть более простые и идиоматические подходы, которые вам известны.

В класс-Game я вынес 1. ссылки на синглтоны Майерса draw, input, audio 2. методы инициализации игры и освобождения памяти из-под неё. Game сам является синглтоном. Обратите внимание на идиому, которую я при этом использую: Game - абстрактный класс, все его члены статические. Невозможно создать экземпляр этого класса, но его статические методы можно использовать. В отличие от традиционной идиомы определения синглтона (Банда Четырех, Александреску, Майерс), здесь нет нужды 1. определять метод Instance() 2. описывать конструкторы, оператор присваивания и деструктор как приватные. При этом мы выделяем синглтон на синтаксическом уровне: все обращения к его членам из функции main() осуществляются через "Game :: method()".

Каждый класс-GameMode должен иметь прямой доступ к draw, input, audio, и запись Game::draw нас не устраивает. По этой причине я решил сделать классы-GameMode наследниками класса-Game.

Game::Register() позволяет зарегистрировать новый режим игры. При запуске Game::Start() указывается стартовый режим и включается главный цикл приложения (обработка сообщений Windows). Игра крутится до тех пор, пока не произойдет выход из какого-нибудь режима и метод GameMode::Process() не вернет id несуществующего режима. Это значит, что игра завершается, а не переходит в новый режим.


 
 
 
const int MAX_MODES = 5;
const int UNDEFINED_ID = -1;
 
class Game
{
static Game* modes[MAX_MODES];
static int mode_count;
protected:
static Direct3d& draw; // Singleton
//static DirectInput& input;
//static DirectAudio& audio; // DMusic + DSound
 
public:


static int Init()
{
cout << "Game: Init() " << endl;
draw = Direct3d :: Instance();
//input = DirectInput :: Instance();
//audio = DirectAudio :: Instance();
return 1;
}
 
static void Release()
{
cout << "Game: Release() " << endl;
}
 
static void Register( Game* mode )
{
if (mode_count == MAX_MODES)
return;
modes[ mode_count ] = mode;
mode_count++;
}

static int Start( int mode_id )
{
cout << "Game: Start() " << endl;
// Main Windows Loop
 
while (mode_id != UNDEFINED_ID)
{
// Process returns id of
mode_id = modes[mode_id]->Process();
}
return 1;
}
virtual int Process() = 0;
};
 
Game* Game :: modes[MAX_MODES];
int Game :: mode_count = 0;
 
 
class GameMode1 : public Game
{
public:
int Process()
{
cout << "GameMode1: Process() " << endl;
// InitResources();
// Logic(); Show();
// ReleaseResources();
return 1;
}
};
class GameMode2 : public Game
{
public:
int Process()
{
cout << "GameMode2: Process() " << endl;
// InitResources();
// Logic(); Show();
// ReleaseResources();
return 2;
}
};
class GameMode3 : public Game
{
public:
int Process()
{
cout << "GameMode3: Process() " << endl;
// InitResources();
// Logic(); Show();
// ReleaseResources();
return -1;
}
};
 
 
void main()
{
if ( Game::Init() )
{
GameMode1 gm1;
GameMode2 gm2;
GameMode3 gm3;
Game::Register(&gm1);
Game::Register(&gm2);
Game::Register(&gm3);
Game::Start(0);
}
 
Game::Release();
}

Результат работы приложения:

Game: Init()
Game: Start()
GameMode1: Process()
GameMode2: Process()
GameMode3: Process()
Game: Release()
Press any key to continue . . .
topright:
[User Picture Icon]
From:die_tinte
Date:Август, 18, 2007 13:34 (UTC)
(Ссылка)
Использовал нечто подобное при программировании игрушек для КПК.
Только применял указатели на функции (Init, Release, Dispatch, Draw) вместо наследования, удобно было делать различые перехватчики, да и вообе побаивался юзать виртуальные функции на слабой платформе.
У Вас получилось нечто весьма похожее шаблон State, если не ошибаюсь.
(Ответить) (Ветвь дискуссии)
[User Picture Icon]
From:topright
Date:Август, 18, 2007 18:04 (UTC)
(Ссылка)
Я тоже стараюсь избегать наследование, т.к. оно "нарушает инкапсуляцию". Спасибо, покурю паттерн State!
(Ответить) (Уровень выше) (Ветвь дискуссии)
From:insooo
Date:Август, 22, 2007 18:23 (UTC)
(Ссылка)
Можно на "ты?" Спасибо)
В наших проектах мы используем почти такую же архитектуру. Только класс Game у нас называется GameState. Да, посмотри паттерн State, у тебя получилось действительно нечто похожее.
Второе, если я не ошибаюсь (а скорее всего я ошибаюсь) описанный синглтон известен так же под именем Monostate.

> Я тоже стараюсь избегать наследование, т.к. оно "нарушает инкапсуляцию".
Да ну? Каким образом?
(Ответить) (Уровень выше) (Ветвь дискуссии)
[User Picture Icon]
From:topright
Date:Август, 22, 2007 18:48 (UTC)
(Ссылка)
Если интересно, приглашаю познакомиться со второй версией этой архитектуры. Думаю, мне удалось избавиться от некоторых недостатков, обнаруженных в первом варианте.

Каким образом "наследование нарушает инкапсуляцию"?

Процитирую Гради Буча, "Объектно-ориентированный анализ и проектирование", гл.3: "Есть серьезные противоречия между потребностями наследования и инкапсуляции. В значительной мере наследование открывает наследующему классу некоторые секреты. На практике, чтобы понять, как работает какой-то класс, часто надо изучить все его суперклассы в их внутренних деталях".

Есть еще один аргумент в связи с наследованием, который как раз сегодня пришел мне на ум (забавное совпадение, правда? Словно специально для тебя:). Завтра изложу его в эпистолярной форме, добро пожаловать в мой журнал. :)
(Ответить) (Уровень выше) (Ветвь дискуссии)
From:insooo
Date:Август, 22, 2007 19:36 (UTC)
(Ссылка)
Процитирую Гради Буча, "Объектно-ориентированный анализ и проектирование", гл.3: "Есть серьезные противоречия между потребностями наследования и инкапсуляции. В значительной мере наследование открывает наследующему классу некоторые секреты. На практике, чтобы понять, как работает какой-то класс, часто надо изучить все его суперклассы в их внутренних деталях".

Хе, Бутч конечно авторитет, но (всегда есть но;). Если рассматривать это следующим образом. У нас есть некоторая сущность, которая представляется единым целым. Но на языке программирования ее уместнее описать именно ввиде иерархии наследования. Становится она от этого уже не единым целым? имхо нет. Более того, при таком подходе инкапсуляция как раз усиливается, потому что эта самая сущность все так же содержит изменяемые детали реализации в себе. При ином подходе, возможно, пришлось бы раскрывать гораздо больше. Тоже самое, кстати, можно сказать и про "друзей".
(Ответить) (Уровень выше) (Ветвь дискуссии)
[User Picture Icon]
From:topright
Date:Август, 22, 2007 19:46 (UTC)
(Ссылка)
Размазывать сущность по иерархии классов не вполне правильно. Обычно один класс соответствует одной некоторой entity.
(Ответить) (Уровень выше) (Ветвь дискуссии)
From:insooo
Date:Август, 22, 2007 20:38 (UTC)
(Ссылка)
Да ну? (прошу не воспринимать мое "да ну" как грубость:) Мы размазываем не сущность а детали ее реализации. Из живого примера, который щас в редакторе.

class Agent {
//...
public:
virtual Desition *Decide();
//...
};

class SteeringAgent : public Agent {
//...
};

class IntersectionAgent : public Agent {
//...
};

class ParkingAgent : public Agent {
//...
};

и т.д.

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

class LogicCar {
//...
private:
Agent *aglet;
//...
};

LogicCar попадает в блок, ему выдается агент блока и он просто вызывает aglet->Decide();

P.S. Предлагаю перенести дальнейшее обсуждение в более приемлимое место.
(Ответить) (Уровень выше) (Ветвь дискуссии)
From:insooo
Date:Август, 22, 2007 20:56 (UTC)
(Ссылка)
P.S. Всем этим я как бы старался показать, как детали реализации одной сущности можно скрыть за иерархией наследования и интерфейсом базового класса, таким образом, усиливая инкапсуляцию.
(Ответить) (Уровень выше) (Ветвь дискуссии)
[User Picture Icon]
From:topright
Date:Август, 23, 2007 00:22 (UTC)
(Ссылка)
В начале ты написал: "сущность, которая представляется единым целым. Но на языке программирования ее уместнее описать именно ввиде иерархии наследования". Теперь ты уточнил мысль: "Мы размазываем не сущность а детали ее реализации". С новой формулировкой я, конечно, совершенно согласен. А из неё непосредственно следует, что наследование, кроме того, что помогает нам бороться со сложностью, порождает новую проблему. Так как детали реализации не инкапсулированы в классе, а размазаны по иерархии, то понять поведение класса можно только в сложном контексте его наследственности. А если реализация предков закрыта для нас, находится черт-те где в недоступном или в уже скомпилированном модуле? В итоге ни обетованной инкапсуляции, ни утопической модульности...
(Ответить) (Уровень выше) (Ветвь дискуссии)
[User Picture Icon]
From:sim0nsays
Date:Август, 18, 2007 17:44 (UTC)
(Ссылка)
Ммм, и где здесь "модульная структура игрового движка"?
(Ответить) (Ветвь дискуссии)
[User Picture Icon]
From:topright
Date:Август, 18, 2007 18:08 (UTC)
(Ссылка)
Класс GameMode1 - это модуль. Инкапсулирует игровую логику, прорисовку, загрузку используемых ресурсов. Таких модулей может быть n штук. Кандидаты в модули: отдельная глава игры; меню; один из жанровых режимов игры со своим особым интерфейсом и т.д.
(Ответить) (Уровень выше) (Ветвь дискуссии)
[User Picture Icon]
From:ddima
Date:Август, 19, 2007 06:08 (UTC)
(Ссылка)
Довольно стандартная схема, я говорил про нее в лекции на #gamedev_lecture.
Беда в том, что чтобы продолжать сохранять полную модульность, список виртуальных вызовов Game::Process() быстро разростется до сотни функций. При этом многие модули практически не будут пересекаться по вызовам.
И все равно тебе не удастся полностью развязать отдельные модули - кросс вызовы все равно будут, потому что попытка их разделения через Game приведет к дальнейшему (уже неоправданному ) расширению проекта.
Можно посмотреть например, игру XPandRally или какую-нибудь другую от Техленда (там исходный java код игры включен в дистрибутив). Там такой же класс называется GameObject.
(Ответить) (Ветвь дискуссии)
[User Picture Icon]
From:boher
Date:Август, 19, 2007 18:29 (UTC)
(Ссылка)
Чем-то похоже на стейт игры. Основная идея такая, что игра имеет некий стейт, а он уже занимается основной обработкой.
(Ответить) (Ветвь дискуссии)
[User Picture Icon]
From:topright
Date:Август, 19, 2007 19:29 (UTC)
(Ссылка)
Совершенно верно.
(Ответить) (Уровень выше) (Ветвь дискуссии)