То не сказка, а присказка
Сегодня мы только начнем делать полноценный менеджер меню. Все же это дело не из простых, да и для понимания обычно проще, когда весь код разложен по полочкам, а не свален в одну кучу. Поэтому будем делать итоговую демку в несколько "присестов", сегодня первый из них - напишем основу, разберемся в терминах и набросаем простой пример с использованием GlScene.
Предыстория
Начну с того, как я раньше программировал меню. На самом деле почти все диалоговые окна дублировали код друг друга, бессвязно болтаясь по различным классам. То есть добавление одного подменю "Опции" к остальным пунктам "Помощь", "Выход", "Профили" и другим вызывало массу головной боли, приходилось выстраивать целые блоки показа/скрытия вроде вот таких:
if(OptionsClicked)then begin Options.Show; MainMenu.Hide; end; .................................. if(GotoMainMenuClicked)then begin Options.Hide; Help.Hide; Levels.Hide; Profiles.Hide; Exit.Hide; MainMenu.Show; end;
И так для каждого подменю. А когда встал вопрос о добавлении нового вложенного диалога с вопросом "А вы действительно хотите удалить этот профиль?" и кнопками "да/нет", у меня от страха затряслись губы... :)
Формирование задачи
В общем, напрашивается одно замечательное решение - нужно унифицировать процесс создания игрового меню. То есть нам нужен механизм, который бы сам управлял вложенными страницами меню, показывал и скрывал определенные подменю, сделав нашу жизнь более удобной и простой!
Только что я употребил термин "страница меню", под этим я понимаю отдельное целостное подменю, которое при показе блокирует все остальные страницы. То есть нажали кнопку "Опции" и у нас вылезло окошко с настройками игры, при этом предыдущая страница меню скрывается или блокируется. Нажмем "Ок" и окно опций скроется, активизируется предыдущая страница меню, и мы выйдем на уровень выше, в главное меню.
Таким образом у нас будет выстраиваться цепочка из страниц меню:
Главное меню -> Профили -> Окно удаления профиля
Из этой цепочки видно, что при закрытии последнего окна, мы выйдем обратно в Профили, при зарытии которого выйдем еще на уровень выше. То есть страницы как бы добавляются в некий список, а затем выходят из него. Кто зашел последним, выйдет первым... Подобная система называется стековой и придумана не мною... :)
Для организации такого стека нам нужен только список, в который будем как бусины нанизывать новые и новые страницы меню. Не знаю как у вас, а у меня уже чешутся руки, готовые начать программировать! Приступим!
Страница меню, BasePage
Для начала организуем простую систему базовой страницы меню, от которой мы будем наследовать все остальные страницы, которые встретятся в игре.
TBasePage = class protected fUnitTime: Single; // from 0 to 1 fShowSpeed: Single; fHideSpeed: Single; fCurrentState: TPageState; fFinishState: TPageState; protected Procedure HideStep(aDeltaTime: Single); virtual; Procedure ShowStep(aDeltaTime: Single); virtual; Procedure HideComplete(); virtual; Procedure UpdateUnitTime(); virtual; public Function IsVisible: Boolean; Procedure Show; virtual; Procedure ForcedShow; virtual; Procedure Hide; virtual; Procedure Update(aDeltaTime: Single); virtual; Constructor Create(); end;
Все ясно из названий: показать, скрыть и форсированно показать (то есть показать мгновенно, без проявления по альфе, без анимаций и т.д., необходимо при первом появлении меню). Хочу подчеркнуть, что TBasePage - это основа, простой класс пустышка с большим количеством виртуальных методов. А вот для программистов GlScene мы отнаследуем класс TGLBasePage, о котором поговорим чуть ниже. Вот в нем как раз и реализуем проявление по альфе, добавление объектов на экран, привязку библиотеки материалов и так далее.
Список страниц, ListPage
Пришло время организовать последовательность страниц, то есть наш стек, в который будут добавляться страницы, одна за другой.
TPageList = class(TList) public Function GetPageByIndex(aPageIndex: Integer): TBasePage; Function GetPageByClass(aPageClass: TBasePageClass): TBasePage; Function AddPageByClass(aPageClass: TBasePageClass): TBasePage; Function GetIndexByClass(aPageClass: TBasePageClass): Integer; Function DeletePageByClass(aPageClass: TBasePageClass; aShouldFree: Boolean = false): Boolean; Function DeletePage(var aPage: TBasePage; aShouldFree: Boolean = false): Boolean; end;
Здесь немного сложнее, но смысл также ясен из названий самих методов: получить страницу, добавить страницу, удалить страницу. Поговорим немного подробнее о приставке -ByClass у некоторых методов.
Одна страница - один класс, все чисто и прозрачно
Чтобы управлять страницами нам нужно их как-то идентифицировать в нашем списке. Как это сделать? Для простоты я решил добавить поиск в списке по классу страницы. Таким образом можно быстро получить доступ до необходимого подменю:
MyList.GetPageByClass(TOptionsPage)
и вуаля, нам вернется экземпляр страницы опций, который был добавлен в список MyList! Чудеса, не правда ли? Не нужны ни константы, ни дополнительные переменные, ни строковые значения, ничего более... Нужно знать только сам класс!
Графичекая страница TGLBasePage
Мы уже разобрались с базовыми терминами, и теперь можно рассмотреть более практичный класс, который можно использовать для визуализации наших страниц меню. Я назвал его TGLBasePage, что отражает предназначение этого класса - использование в связке с GlScene. Наследуемся, конечно, от TBasePage и добавляем новый функционал, так или иначе связанный с графикой.
Описание самого класса довольно специфическое:
TGLBasePage = class(TBasePage) protected fParent: TGLBaseSceneObject; fPageDummy: TGlDummyCube; Procedure HideComplete(); override; protected Procedure AddToScene; virtual; Procedure RemoveFromScene; virtual; Procedure UpdateUnitTime(); override; Procedure InitCustomObjects(fMaterialLibrary: TGlMaterialLibrary); virtual; public Procedure Show; override; Procedure InitGLPage(aGlParent: TGLBaseSceneObject; fMaterialLibrary: TGlMaterialLibrary); end;
Здесь уже и библиотека материалов, и базовый объект GlScene, и методы AddToScene()/RemoveFromScene() для корректного появления и пропадания нашей визуальной страницы.
Менеджер
Давайте попробуем объединить все это в одном менеджере, который бы магическим образом управлял бы всеми страницами сразу! По сути, мы добрались до центрального лица нашей системы-меню, генерала страниц, директора визуального театра!
Описание может показаться довольно мудреным, но, при ближайшем просмотре становится ясно, что на самом деле все прозрачно:
TPageManager = class protected fParent: TGLBaseSceneObject; fMaterialLibrary: TGlMaterialLibrary; fRegistered: TPageList; fStack: TPageList; protected Procedure OnPageShowComplete(aPage: TBasePage); Procedure OnPageHideComplete(aPage: TBasePage); public Function RegisterPage(aPageClass: TGLBasePageClass): TGlBasePage; Function Push(aPageClass: TBasePageClass): TBasePage; Function ForcedPush(aPageClass: TBasePageClass): TBasePage; Function Pop: TBasePage; Function PopPage(aPageClass: TBasePageClass): TBasePage; Function GetPageByClass(aPageClass: TGLBasePageClass): TGlBasePage; Procedure Update(aDeltaTime: Single); Constructor Create(aGlParent: TGLBaseSceneObject; aMaterialLibrary: TGlMaterialLibrary); end;
Разберу всего несколько самых основных методов:
- RegisterPage(aPageClass: TGLBasePageClass) - регистрируем страницу, при этом создается экземпляр класса aPageClass и заносится во внутренний список fRegistered
- Push(aPageClass: TBasePageClass) - добавить в стек новую страницу, которая берется из списка зарегистрированных страниц fRegistered
- ForcedPush(aPageClass: TBasePageClass) - форсированно добавить страницу, при этом она моментально появляется на экране (зачастую нужно для бэков, которые сразу должны отобразиться)
- Pop() - выбросить последнюю страницу из стека
- PopPage(aPageClass: TBasePageClass) - выбросить страницу класса aPageClass из стека
Идея заключается в следующем... Сначала регистрируются все классы наших страниц. При этом менеджер создаст их экземпляры и поместит в свой внутренний список. Таким образом никаких дополнительных манипуляций с памятью нам не потребуются. Страницы будут существовать всегда, а вот показаны только те из них, которые находятся в стеке. Как раз для манипуляции страницами в стеке и предназначены методы Push() и Pop() с "говорящими" названиями... Первый - добавляет страницу в стек, при этом страница начнет показываться на экране. Второй метод выбрасывает последнюю страницу из стека.
Как видно из описания нашего класса, там все время фигурируют TGLBasePageClass, то есть я решил пропитать наш менеджер gl-страницами. Таким образом данный TPageManager работает только с графическими страницами, созданными на базе GlScene. Но на самом деле, немного переписав данный менеджер, его можно приспособить к работе с любым графическим движком (да хоть с VCL)!
Вообще, если данными наработками будет кто-то пользоваться, то конечно же я рассмотрю более подробно те или иные части всех вышеперечисленных классов. Но на данный момент я считаю важнее разобрать простой пример глубокой вложенности нескольких страниц друг в друга.
Небольшой пример использования
Чтобы не быть голословным, я решл набросать небольшую демонстрацию работы. Пока интерактив минимален - нажимаем "пробел" и у нас добавляется еще одна страница на экран, увеличивая вложенность страниц друг в друга. Хоть демонстрация и примитивна, но, уверен, уже этот пример сочными красками нарисует в воображении будущее меню, которое порадует даже изысканного игрока.
Выглядит наша демка так:
И сразу регистрация страниц:
fPages[0] := TBaseMainMenu; fPages[1] := TOptionsPage; fPages[2] := TCreditsPage; fPages[3] := THelpPage; fPages[4] := TProfilePage; fPageManager := TPageManager.Create(fMainDummy, fMatLib); for i := low(fPages) to high(fPages) do fPageManager.RegisterPage(fPages[i]);
Как видно, я создал дополнительных пять классов (TBaseMainMenu, TOptionsPage, TCreditsPage, THelpPage, TProfilePage), отнаследованных от TGlBasePage. Каждый класс отвечает за свое подменю. Все они идентичны, поэтому рассмотрим только TBaseMainMenu:
TBaseMainMenu = class(TGLBasePage) protected fHudSprite: TGlHudSprite; Procedure InitCustomObjects(fMaterialLibrary: TGlMaterialLibrary); override; Procedure UpdateUnitTime(); override; end;
Перекрыли два защищенных метода... В InitCustomObjects() создаем худ-спрайт, а в UpdateUnitTime() управляем его прозрачностью. В принципе, ничего сложного, обычные для GlScene дела.
Для красивости я решил не только добавлять страницы по нажатии на пробел, но также и убирать их, чтобы показать работу Pop()-метода. Пришлось завести переменную fReverse, показывающую в какую сторону движемся.
В итоге код обработки клавиши пробел разросся до неприлично большого состояния:
if not SpacePressed and IsKeyDown(vk_Space) then begin if fPageReverse then begin fCurPop := fPageManager.Pop(); if(fLastPop <> fCurPop) then begin fLastPop := fCurPop; fPageIndex := fPageIndex - 1; if fPageIndex = low(fPages) then fPageReverse := false; end; end else begin fPageManager.Push(fPages[fPageIndex]); fPageIndex := fPageIndex + 1; if fPageIndex > high(fPages) then fPageReverse := true; end; end;Где все также красуются два простых метода Pop() и Push().
Таким образом, можно выделить простую схему работы с TPageManager'ом:
- создаем все необходимые классы подменю, наследуя их от TGLBasePage
- регистрируем все страницы с помощью RegisterPage()
- при необходимости добавляем или убираем наши страницы с помощью Pop/Push
- обновляем наш PageManager с помощью метода Update()
Видео работы нашей демки:
Так что качаем первую (и далеко не последнюю) демонстрацию работы TPageManager'а или апдейтимся до 20ой ревизии в svn'е.
Неплохо :)
ОтветитьУдалитьЯ в SpaceSim придумал немного другую идею, которую кратко обрисовал в этом посте:
http://perfect-daemon.blogspot.com/2011/06/gamescreen.html
(второй или третий абзац). К сожалению, придумывал такую систему в рамках конкурса (как следствие - она оказалась несовершенна), поэтому походу дела ее много пилил.
Если кратко - то суть почти та же, разе что саму игру я тоже считаю элементом "меню".
Жду новых статей :)
супер!
Удалитьидея стопроцентно одинаковая! я тоже подсмотрел мысль на гд.ру, только совершенно в другом разделе и касаемо других вещей))..
да, у меня страница - также призвана выполнять функционал, идентичный твоим GameScreen'ам! да, игра тоже одна из страниц, в моей терминологии это GamePage))..
посмотрел твой код - высший пилотаж, очень все здорово! в рамках конкурса это вдвойне круто!
у меня самый большой сдвиг мысли именно в сторону вложенности страниц друг в друга... немного не понял, как это выполняется у тебя, поэтому очень хотелось бы статью по использованию твоих GameScreen'ов!
если у тебя будет время написать о том, как использовать и какие плюсы/минусы у всей системы, буду безумно благодарен!
"у меня самый большой сдвиг мысли именно в сторону вложенности страниц друг в друга..."
УдалитьВот тут прокол. У меня нет стека, есть активная страница и есть остальные, причем остальные могут быть как скрыты, так и просто в режиме паузы (не апдейтятся, но отображаются). Возможно, постараюсь переделать под стек, либо придумать иную систему, например что-то VCL-подобное (примером может являться движок Source с его окнами, поведение которых очень похоже на Windows-окна).
С недавних пор я сторонник той идеи, что лучше сначала плохо сделать и переделать, чем сто лет проектировать идеальный вариант. Это касается, конечно, только кодинга.
P.S. Заранее извиняюсь за ошибки, непонятки или пунктуацию — во мне много пива :)
Насчет отсутствия стека - какой скрин тогда активизируется при деактивации текущего? Кто из них "последний"?
УдалитьПо поводу "плохо сделать и переделать" - поддерживаю! Make games, not engines :)