Автор: Андрей Коротков «DRON»

Опубликовано: 18.07.2011

Изменено: 18.07.2011

Постоянная ссылка

Комментарии [3]

Быстрый старт на движке DGLE2


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


Игровой движок DGLE2

Введение

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

Фундаментальные принципы движка

Вообще, сперва я рекомендую прочитать статью "Философия движка DGLE2".

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

1) Первый принцип – дискретизация и независимость. Движок является отдельной библиотекой, каждый плагин также является отдельной библиотекой. Пользователю движка не допускается вмешиваться в работу внутренних подсистем движка, он может работать только с API движка. Это призвано обеспечить высокий уровень стабильности и быстродействия системы в целом. Исключения составляют плагины, но о них поговорим отдельно ниже.

Еще замечу, что движок позволяет внедряться в свои процессы непосредственно, например, используя вызовы OpenGL команд на прямую из приложения или плагина. Я не рекомендую этого делать из приложения, а допускаю только из плагинов. При использовании OpenGL напрямую в приложении стоит помнить о двух вещах.

Первое это то, что в движок может инициализировать как OpenGL 1.x, 2.x так и OpenGL 3.x, 4.x в зависимости от пользовательской системы, на которой запущено приложение. В первом случае будут работать все стандартные OpenGL команды такие как glColor3f(...), glBegin(...), glTranslatef(...), glPopMatrix(...) и т.д., во втором случае модель работы OpenGL поменяется полностью (см. спецификацию OpenGL 3.0 и выше). Что бы работа с движком в этом случае была безопасной необходимо либо запретить движку инициализировать OpenGL 3.0 и выше, либо проверять какая версия OpenGL была инициализирована и использовать только допустимые команды.

Второе это – внутренний менеджер состояний OpenGL в движке. Он является одним из центральных компонентов подсистемы рендеринга в движке и его основная задача быть «мостиком» между OpenGL 1.x, 2.x и OpenGL 3.x, 4.x и фильтровать ненужные вызовы команд и смен состояний OpenGL, тем самым значительно повышая быстродействие. По этому, когда вы используете в приложении на прямую OpenGL менеджер состояний ничего не будет знать про те изменения, которые вы сделали, например, установили другую текстуру или сменили мультитекстурный слой. В самом худшем случае сбой в работе менеджера состояний приводит к неверной отрисовке изображения. В принципе, менеджер состояний можно просто выключить, но я этого делать очень не рекомендую. Намного проще просто следить за теми измененми которые вы вносите в OpenGL, например, оборачивать свои блоки команд в glPushAttrib(...)glPopAttrib(...) или просто возвращать все как было.

В качестве примера можно посмотреть сэмпл из SDK движка под названием "Simple_Engine_and_OpenGL".

2) Второй принцип – 4 процедуры и никаких таймеров. В основе этого принципа лежит мой опыт разработки 3D приложений реального времени и, на мой взгляд, он единственно верный. Суть его заключается в том, что пользователь движка пишет весь свой код всего в четырех процедурах, которые регистрируются в движке и в нужный момент времени вызываются движком. Это процедуры – Init, Free, Process и Render. После старта движка методом StartEngine() интерфейса IEngineCore (об этом подробнее напишу ниже) движок попадает в бесконечный цикл и дальше взаимодействие с ним осуществляется только по средствам этих процедур.

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

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

Рассмотрим несколько примеров поведения движка – на многоядерном компьютере скорее всего Process и Render будут назначены разным ядрами и будут выполняться в отдельных потоках. На слабом компьютере, если приложение тормозит и возникает ситуация когда кол-во кадров в секунду (FPS) меньше чем интервал заданный uiProcessInterval(UPS) движок попытается сгладить «просадку» FPS и может намеренно пропускать некоторые кадры, при этом вызывая Process несколько раз подряд что бы скорость приложения не снизилась и не считая исключительных случаев, пользователь даже не заметит тормозов. В движке может использоваться пакетный или отложенный рендер, когда во время процедуры Render на самом деле ничего на экран не выводится, а все данный накапливаются в специальные буферы и потом будут выведены самым оптимальным образом в подходящий момент времени.

Из предыдущего абзаца надеюсь стало понятно почему весь ваш код должен располагаться только в рамках этих 4-х процедур, впрочем сделать как-то иначе движок и не позволит.

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

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

3) Третий принцип - это структурированная объектная модель построенная на основе виртуальных классов(или интерфейсов). Это, пожалуй, самый основной принцип, без понимания которого вы точно не сможете использовать движок.

В качестве API движок предоставляет набор интерфейсов, т.е. классов все методы которых виртуальные а реализации самих классов скрыты от пользователя движка в библиотеке. Каждый интерфейс отвечает только за свой круг узких задач, что позволяет создать четкую архитектуру и структуру самого движка. Интерфейсы наследуются друг от друга и позволяют получать через себя доступ к другим интерфейсам. Из библиотеки движка экспортируется всего две функции это CreateEngine(...) и FreeEngine(...), вызов первой функции приводит к тому что в библиотеке создается основной класс ядра Core и функция возвращает указатель на интерфейс IEngineCore который реализует этот класс. Доступ ко всем остальным интерфейсам подсистем движка осуществляется именно через этот интерфейс. Функция FreeEngine(...) принимает указатель на интерфейс IEngineCore и удаляет его реализующий класс. Одна библиотека может создать сколь угодно много инстанций движка, т.к. каждый вызов CreateEngine(...) будет создавать новый экземпляр.

Стоит отметить, что различные экземпляры движка имеют разные менеджеры ресурсов, окна и т.д., но можно их связывать в рамках одного приложения предоставляя доступ к общим ресурсам и тем самым организовывая полноценные мультирендер системы. Для упрощения работы с созданием движка в заголовочные файлы включены функции GetEngine(...) и FreeEngine(...), которые скрывают в себе вызовы аналогичных функций из библиотеки. Функции в заголовочном файле делают по сути тоже самое, но еще и подключают и отключают саму библиотеку.

Теперь еще одно важное замечание по интерфейсам, заголовочные файлы движка, кроссплатформенные, а сам движок предполагает возможность своего использования еще и в различных языках программирования. Это наложило определенные ограничения на сами интерфейсы.

Во-первых, в методах интерфейсов используются только стандартные типы данных такие как int или char или же типы которые объявлены в заголовочных файлах самого движка и реализация которых может меняться в зависимости от языка или платформы. Во-вторых, движок использует принципы Microsoft COM, но при этом лишь их эмулирует. На этом остановлюсь подробнее. Что бы интерфейсы корректно работали в MS Windows в таких языках как Delphi, C# и некоторых других, необходимо что бы они были наследованы от системного интерфейса IUnknown, в котором есть три метода QueryInterface, AddRef и Release. Спецификация COM предписывает вызывать Release для каждого интерфейса, что бы его удалить. Про остальные методы и про сам COM можете прочитать в Интернете.

Я сейчас говорю это к тому, что в движке у всех интерфейсов эти методы являются «пустышками» и у них нет совершенно никакой реализации. Они сделаны лишь для того что бы, например, компилятор Delphi в который «вшита» реализация COM мог к ним обращаться и это бы не приводило к ошибкам. Сам движок может быть собран под Windows и без поддержки COM (для всех платформ кроме Windows так и есть), но в этом случае его можно будет корректно использовать только из C++. Для удаления объекта реализующего интерфейс в движке используется метод Free(), интерфейсы у которых этот метод отсутствует – не нуждаются в удалении.

Еще важно знать, что любой метод интерфейса всегда возвращает тип HRESULT, и проверив значение можно анализировать ошибки в приложении. Результат может быть одним из следующих:

  • S_OK - значит, что все прошло успешно.
  • S_FALSE - означает что все прошло успешно, но возникли какие то проблемы не сказывающиеся на работу системы и не приводящие к краху. Например, метод Load(...) интерфейса IResourceManager при попытке загрузить текстуру не нашел искомого файла и был вынужден вернуть вместо требуемой текстуры текстуру по умолчанию (оранжево-черные шашечки).
  • E_INVALIDARG - означает, что пользователь движка передал неверные параметры и метод не знает что с ними делать. Скорее всего, это ошибка программиста пишущего приложение.
  • E_FAIL - означает, что в процессе выполнения метода возникла серьезная ошибка, которая мешает завершить его выполнение. Например, не удалось инициализировать OpenGL или произошла ошибка выделения памяти или еще что-либо неожиданное.
  • E_ABORT - возвращается в том случае, если внутри метода какой либо другой метод вернул E_FAIL и данный метод не может быть корректно завершен.

Последнее, что хочется сказать про интерфейсы это – принцип наследования. В движке есть всего один коренной интерфейс, это IDGLE2_Base и все остальные интерфейсы наследованы от него. Также, есть несколько базовых интерфейсов, например все ресурсы такие, как текстуры, звуки, меши, материалы и т.д. наследованы от интерфейса IEngBaseObj, а подсистемы движка такие как, менеджер ресурсов, подсистема ввода, файловая подсистема и т.д. наследованы от интерфейса IEngineSubSystem. Например, наследование для всех подсистем движка выглядит следующим образом:

Игровой движок DGLE2

Любой базовый интерфейс обладает методом который позволяет узнать, что на самом деле он реализует, что бы на основании этого можно было определить к какому интерфейсу его можно корректно привести. Движок внутри себя, как правило, оперирует базовыми интерфейсами, например метод Load(...) интерфейса IresourceManager может загружать любые типы данных поддерживаемые движком (а их может быть сколь угодно много т.к. поддержка может расширяться за счет сторонних плагинов), но при этом возвращает всегда указатель на базовый интерфейса для любого ресурса IEngBaseObj.

Например, давайте напишем функцию, которая бы загружала из файла текстуру и возвращала её интерфейс, а в случае неудачи возвращала бы пустой указатель. Предположим что движок уже инициализирован и есть глобальная переменная интерфейса IEngineCore с именем pEngineCore. Напишем это безопасным до паранойи способом, и будет это выглядеть следующим образом:

// C++
using namespace DGLE2;

ITexture* LoadTexture(char *pFileName)
{
   IResourceManager *p_res_man;

   if (!pEngineCore || pEngineCore->GetSubSystem(ESS_RESOURCE_MANAGER, (IEngineSubSystem *&)p_res_man) != S_OK)
       return NULL;

   IEngBaseObj *p_obj;

    if (p_res_man->Load(pFileName, p_obj) == S_OK)
    {
       E_ENG_OBJ_TYPE obj_type;
		
       p_obj->GetType(obj_type);
       
       if (obj_type == EOT_TEXTURE)
          return (ITexture*)p_obj;
       else
          return NULL;
    }
    else
       return NULL;
}
 
// Delphi
function LoadTexture(pFileName : PAnsiChar) : ITexture;
var
p_res_man : IResourceManager;
p_obj : IEngBaseObj;
obj_type : Integer;
begin
 if (pEngineCore <> nil) and (pEngineCore.GetSubSystem(ESS_RESOURCE_MANAGER, IEngineSubSystem(p_res_man)) = S_OK) then
  begin
   Result := nil;
   Exit;
  end;

 if p_res_man.Load(pFileName, p_obj) = S_OK then
 begin
  p_obj.GetType(obj_type);

  if obj_type = EOT_TEXTURE then
     Result := ITexture(p_obj)
  else
     Result := nil;
  end else
     Result := nil;
end;
 
// C#
static ITexture LoadTexture(string pFileName)
{
  IEngineSubSystem p_sub_sys = null;
  IResourceManager p_res_man = null;

  if (pEngineCore == null)
     return null;

  pEngineCore.GetSubSystem(E_ENGINE_SUB_SYSTEM.ESS_RESOURCE_MANAGER, out p_sub_sys);

  if (p_sub_sys == null)
     return null;

  p_res_man = (IResourceManager)p_sub_sys;

  IEngBaseObj p_obj;

  p_res_man.Load(pFileName, out p_obj, (uint)E_TEXTURE_LOAD_FLAGS.RES_LOAD_DEFAULT);

  if (p_obj != null)
  {
      E_ENG_OBJ_TYPE obj_type;
        		
      p_obj.GetType(out obj_type);
      
     if (obj_type == E_ENG_OBJ_TYPE.EOT_TEXTURE)
         return (ITexture)p_obj;
     else
         return null;
  }
  else
      return null;
}

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

Если вы посмотрите на примеры из SDK, то увидите, что я конечно не делаю такое количество проверок, как в примере выше, многое можно доверить движку, а шанс например получить пустой указатель на IResourceManager нулевой, но нужно понимать что во многих случаях лучше перестраховаться.

4) Четвертый принцип - плагинная архитектура и расширяемость. Тут особенно говорить не о чем, кроме разве что того, что если вы хотите что бы код вашего приложения был кроссплатформенным и работал стабильно, то нужно стараться ничего не использовать кроме интерфейсов движка и стандартных средств языка.

Что это значит? Допустим вам нужно прочитать файл с жесткого диска, если вы это сделаете средствами файловой подсистемы движка IMainFileSystem, то ваш код будет гарантированно работать на любой платформе, т.к. реализация этой подсистемы зависит от платформы и за нее отвечает движок. Так же стоит пользоваться типами ресурсов движка и если надо сделать поддержку своего формата, например, мешей или текстур, то нужно создать свой интерфейс и понаследовать его от интерфейса IMesh или ITexture а дальше реализовать его и зарегистрировать в менеджере ресурсов движка. Тогда вы сможете через стандартные интерфейсы и методы работать с вашими собственными специфичными данными.

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

Так, а что дальше?

Что бы не переусложнять статью, на этом я думаю, стоит поставить точку. В статье я постарался раскрыть все основные принципы на которых строится движок DGLE2. Я думаю что понимания этих принципов и беглое изучения сэмплов из SDK движка вполне достаточно что бы взять и начать делать свою первую программу на движке, а со всем остальным уже разбираться по ходу.

Как я писал в самом начале, я буду писать цикл уроков по движку и разбирать там все подробно и поэтапно, что бы досконально все понять, стоит обратиться к нему.

Если остались вопросы или вы считаете что я забыл что-либо прояснить в этой статье, то можете оставлять комментарии или писать на форум, а я уже в свою очередь буду корректировать статью или писать новые, если ваши вопросы будут на столько масштабны.

 


 

Другие статьи про игровой движок DGLE2 читайте здесь:
http://dron.deeprosoft.com/dgle2_articles