Pull to refresh

Дуэт OGRE 3D и SDL

Website development *
В последнее время тут стали появляться статьи, связанные с движком OGRE, в т.ч. и пост по созданию простейшей программы, поэтому я решил не откладывать дело в долгий ящик и написать эту статью. Когда-то не так давно я уже писал небольшой поверхностный обзор библиотеки SDL, и здесь, в качестве продолжения, приведу конкретный пример использования этой библиотеки, а именно — как заставить её работать вместе с движком.

Интро


Для начала отвечу на вопрос — зачем это? OGRE и сам работает на большинстве популярных платформ, умеет создавать окошко для визуализации, а для обработки ввода пользователя многие уроки на оф. сайте движка рекомендуют использовать библиотеку OIS. Это действительно так, и раньше я использовал движок именно в связке с этой библиотекой, пока не пришлось писать под Linux. Тогда и возникли неприятные моменты. Окошко OGRE по непонятным мне причинам нередко отказывалось реагировать на перетаскивание мышью, часто изображение в нём мерцало (было подозрение на конфликт с Compiz или Emerald); ввод с помощью OIS не мог похвастаться приличной скоростью, и после орудования мышкой в течение нескольких секунд программу хотелось поскорее закрыть. Тогда я решил попробовать SDL в качестве альтернативы OIS. Библиотека справилась весьма успешно с возложенными на неё обязанностями — созданием окна и обработкой ввода, при этом показав приличную скорость работы.

Каркас программы


Итак, нам понадобятся OGRE SDK и SDL SDK. Я не буду описывать процесс их установки, потому что в разных системах он разный, да и несложных уроков по этой теме немало. Перейдём сразу к делу. Первая задача — написать код программы, в которой созданием окна и обработкой ввода будет заниматься SDL, а OGRE будет лишь производить рендеринг в чужое для него окно. Постараемся сделать наш код переносимым хотя бы между Windows и Linux.

Начнём с того, что инициализируем SDL и создадим с её помощью окно:
  1. #include <Ogre.h>
  2. #include <SDL.h>
  3. #ifdef _WINDOWS
  4. #include <SDL_syswm.h>
  5. #endif
  6.  
  7. ...
  8.  
  9. // Инициализация видео подсистемы SDL
  10. SDL_Init(SDL_INIT_VIDEO);
  11. // Центрировать создаваемое окно на экране
  12. SDL_putenv((char*)"SDL_VIDEO_CENTERED=true");
  13. // Установка видеорежима и одновременное создание окошка 800x600@16 с получением контекста OpenGL
  14. SDL_SetVideoMode(800, 600, 16, SDL_OPENGL);
  15. // Не оставляем окно безымянным
  16. SDL_WM_SetCaption("Demo window", NULL);
* This source code was highlighted with Source Code Highlighter.

Теперь надо настроить OGRE, установить для него визуализатор. Как закодить эту часть программы — сугубо индивидуальное предпочтение каждого. Здесь я оставил за собой право вручную загрузить лишь самые необходимые плагины и установить нужные параметры. Главное — не создавать окно средствами OGRE:
  1. // Начинаем инициализацию OGRE с создания объекта Root
  2. Ogre::Root *root = new Ogre::Root();
  3. // Загружаем плагин-визуализатор OpenGL из текущей папки (или из другого места)
  4. #ifdef _WINDOWS
  5.  root->loadPlugin("./RenderSystem_GL.dll");
  6. #else
  7.  root->loadPlugin("./RenderSystem_GL.so");
  8. #endif
  9. // Создаём сам визуализатор
  10. Ogre::RenderSystem *rs = root->getRenderSystemByName("OpenGL Rendering Subsystem");
  11. // ... делаем его текущим
  12. root->setRenderSystem(rs);
  13. // ... и настраиваем видео-режим
  14. rs->setConfigOption("Full Screen", "no");
  15. rs->setConfigOption("Video Mode", "800 x 600 16-bit colour");
  16. // Инициализируем OGRE, но без автоматического создания окна
  17. root->initialise(false);
* This source code was highlighted with Source Code Highlighter.

Самое интересное — говорим OGRE, что контекст OpenGL уже получен, и в него надо производить визуализацию:
  1. // Устанавливаем параметры создания окна OGRE.
  2. // Фактически никакого окна не создаётся. Мы просто указываем OGRE, куда
  3. // ему производить визуализацию
  4. Ogre::NameValuePairList params;
  5. #ifdef _WINDOWS
  6.  SDL_SysWMinfo info;
  7.  SDL_VERSION(&info.version);
  8.  SDL_GetWMInfo(&info);
  9.  params["externalWindowHandle"] = Ogre::StringConverter::toString(reinterpret_cast<size_t>(info.window));
  10.  params["externalGLContent"] = Ogre::StringConverter::toString(reinterpret_cast<size_t>(info.hglrc));
  11.  params["externalGLControl"] = Ogre::String("True");
  12. #else
  13.  // Под Linux чуть проще
  14.  params["currentGLContext"] = Ogre::String("True");
  15. #endif
  16.  
  17. // Создание "виртуального" окна с выбранными параметрами
  18. Ogre::RenderWindow *wnd = root->createRenderWindow("Main window", 800, 600, false, &params);
  19. wnd->setVisible(true);
* This source code was highlighted with Source Code Highlighter.

Пора заканчивать с инициализацией и переходить к рабочей части:
  1. // Сюда будем складывать очередное событие для обработки
  2. SDL_Event evt;
  3. // Да, цикл должен продолжаться
  4. bool running = true;
  5. // Основной цикл визуализации
  6. while (running)
  7. {
  8.  // Читаем поступившие со времени предыдущего кадра системные события и события ввода
  9.  while (SDL_PollEvent(&evt))
  10.  {
  11.   // Если юзер закрыл окно, прекращаем цикл
  12.   if (evt.type == SDL_QUIT)
  13.    running = false;
  14.  }
  15.  // Отрисовываем один кадр
  16.  root->renderOneFrame();
  17.  
  18.  // Почему-то необходимость вызывать SDL_GL_SwapBuffers(); есть только в Windows.
  19.  // В Linux прекрасно работает без этого и глючит, если это использовать.
  20.  #ifdef _WINDOWS
  21.   SDL_GL_SwapBuffers();
  22.  #endif
  23. }
* This source code was highlighted with Source Code Highlighter.

При завершении программы не забываем убрать за собой:
  1. // Удаляем главный объект OGRE, освобождая тем самым все используемые движком ресурсы
  2. delete root;
  3. // Закрываем SDL
  4. SDL_Quit();
* This source code was highlighted with Source Code Highlighter.

Код получился достаточно коротким, половину места занимают комментарии. После компиляции и запуска программы появляется окошко с непонятным содержимым:
image
Это ожидаемый результат, потому что на данном этапе не создано ни одного viewport'а и ни одной камеры, из которой OGRE мог бы производить визуализацию. Поэтому всё, что отображается в окне — произвольные данные, находящиеся в видеопамяти.

Менеджер ввода


Теперь было бы неплохо сделать небольшую удобную систему отлова событий и отсылки их слушателям. Сначала опишем интерфейсы слушателей:
  1. // Input.h
  2.  
  3. #include <list>
  4. #include <SDL.h>
  5.  
  6. /// Интерфейс слушателей событий клавиатуры
  7. class KeyboardListener
  8. {
  9. public:
  10.  virtual ~KeyboardListener() {}
  11.  virtual void onKeyboardEvent(const SDL_KeyboardEvent &evt) = 0;
  12. };
  13.  
  14. /// Интерфейс слушателей событий мыши
  15. class MouseListener
  16. {
  17. public:
  18.  virtual ~MouseListener() {}
  19.  virtual void onMouseMotion(const SDL_MouseMotionEvent &evt) = 0;
  20.  virtual void onMouseBtn(const SDL_MouseButtonEvent &evt) = 0;
  21. };
  22.  
  23. /// Интерфейс слушателей всех остальных типов событий
  24. class EventListener
  25. {
  26. public:
  27.  virtual ~EventListener() {}
  28.  virtual void onEvent(const SDL_Event &evt) = 0;
  29. };
* This source code was highlighted with Source Code Highlighter.

Определив интерфейсы слушателей, можно написать и сам класс менеджера ввода:
  1. class InputManager
  2. {
  3. private:
  4.  typedef std::list<KeyboardListener*> KeyboardListenersList;
  5.  typedef std::list<MouseListener*> MouseListenersList;
  6.  typedef std::list<EventListener*> OtherEventsListenersList;
  7.  
  8. private:
  9.  KeyboardListenersList kb_listeners;
  10.  MouseListenersList mouse_listeners;
  11.  OtherEventsListenersList other_listeners;
  12.  
  13. public:
  14.  InputManager() {}
  15.  ~InputManager() {}
  16.  
  17.  /// Захватывает и обрабатывает события
  18.  void capture();
  19.  
  20.  /// Регистрирует слушателя событий клавиатуры
  21.  void regKeyboardListener(KeyboardListener *l);
  22.  /// Снимает регистрацию слушателя событий клавиатуры
  23.  void unregKeyboardListener(KeyboardListener *l);
  24.  
  25.  /// Регистрирует слушателя событий мыши
  26.  void regMouseListener(MouseListener *l);
  27.  /// Снимает регистрацию слушателя событий мыши
  28.  void unregMouseListener(MouseListener *l);
  29.  
  30.  /// Регистрирует слушателя событий общего типа
  31.  void regEventListener(EventListener *l);
  32.  /// Снимает регистрацию слушателя событий произвольного типа
  33.  void unregEventListener(EventListener *l);
  34. };
* This source code was highlighted with Source Code Highlighter.

Теперь перейдём к реализации класса. Основной интерес представляет метод capture():
  1. SDL_Event evt;
  2. while (SDL_PollEvent(&evt))
  3. {
  4.  switch (evt.type)
  5.  {
  6.  // Событие клавиатуры
  7.  case SDL_KEYUP: case SDL_KEYDOWN:
  8.  {
  9.   KeyboardListenersList::const_iterator kb_it = kb_listeners.begin(), kb_end = kb_listeners.end();
  10.   for (; kb_it != kb_end; ++kb_it)
  11.    (*kb_it)->onKeyboardEvent(evt.key);
  12.   break;
  13.  }
  14.  // Событие мыши
  15.  case SDL_MOUSEBUTTONDOWN: case SDL_MOUSEBUTTONUP: case SDL_MOUSEMOTION:
  16.  {
  17.   MouseListenersList::const_iterator m_it = mouse_listeners.begin(), m_end = mouse_listeners.end();
  18.   for (; m_it != m_end; ++m_it)
  19.   {
  20.    if (evt.type == SDL_MOUSEBUTTONDOWN || evt.type == SDL_MOUSEBUTTONUP)
  21.     (*m_it)->onMouseBtn(evt.button);
  22.    if (evt.type == SDL_MOUSEMOTION)
  23.     (*m_it)->onMouseMotion(evt.motion);
  24.   }
  25.   break;
  26.  }
  27.  // Все остальные события
  28.  default:
  29.   OtherEventsListenersList::const_iterator other_it = other_listeners.begin(), other_end = other_listeners.end();
  30.   for (; other_it != other_end; ++other_it)
  31.    (*other_it)->onEvent(evt);
  32.  }
  33. }
* This source code was highlighted with Source Code Highlighter.

Этот метод извлекает события из очереди и рассылает их нужным слушателям. Код легко адаптировать для прослушивания разных конкретных типов событий. На самом деле, узкоспециализированных слушателей можно создать довольно много, в данном случае я привёл лишь примеры слушателей мыши и клавиатуры.
Осталось совсем немного — реализовать методы регистрации слушателей. Тут всё просто, поэтому я приведу код реализации только для методов регистрации слушателей клавиатуры, остальные реализуются по аналогии:
  1. void InputManager::regKeyboardListener(KeyboardListener *l)
  2. {
  3.  if (l && std::find(kb_listeners.begin(), kb_listeners.end(), l) == kb_listeners.end())
  4.   kb_listeners.push_back(l);
  5. }
  6.  
  7. void InputManager::unregKeyboardListener(KeyboardListener *l)
  8. {
  9.  if (l)
  10.   kb_listeners.remove(l);
  11. }
* This source code was highlighted with Source Code Highlighter.

Теперь у нас есть приличный менеджер ввода, не идеальный конечно, но дающий простор для фантазии в плане дальнейшего усовершенствования. С использованием менеджера ввода основной цикл программы можно переписать:
  1. InputManager *imgr = new InputManager();
  2. while (!stop)
  3.  {
  4.   imgr->capture();
  5.   root->renderOneFrame();
  6.   ...
  7. }
  8. delete imgr;
* This source code was highlighted with Source Code Highlighter.

Для обработки событий нужно будет создать классы, реализующие соответствующие интерфейсы. Как минимум стоит отлавливать событие SDL_QUIT, чтобы корректно завершать программу :)

Напоследок


Надо сказать, что использование SDL я вовсе не считаю необходимым, но библиотека, тем не менее, является весьма приятной для использования. API, хотя и полностью написанный на Си, не уступает по удобству объектно-ориентированному интерфейсу OIS. Привязка к OGRE также не вносит каких-то особых неудобств. OGRE, в свою очередь, показал ещё раз свою работоспособность в условиях экстремальной эксплуатации :)
Tags:
Hubs:
Total votes 33: ↑31 and ↓2 +29
Views 7.6K
Comments 15
Comments Comments 15

Posts