Простенькое GUI для XNA

от автора

Доброго времени суток. Эта статья не откроет Вам новые грани программирования, она не расскажет о классном способе решения проблемы, ничего такого. Просто ещё один старый велосипед, ржавый, но на ходу, и ехать ему ещё очень долго…

Итак

Когда я начал писал первую «серьёзную» игру на XNA стала проблема с отсутствием стандартного GUI на этом движке. Так как я учусь, опыта у меня немного, было решено писать свою систему интерфейса, вместо использования уже готовых инструментов. За основу было взято реализацию с известного в прошлом движка HGE. Ничего революционного там не было: класс Gui, класс GuiObject, от последнего наследуются разные кнопочки, списочки и т.д.

Базовый код

class Gui { 	public  GuiObject elements[];          public Gui()          {              elements = new GuiObject[6];         } }  class GuiObject { 	public Rectangle rect; //нужно для определения попадания мыши, отрисовки, и т.д. 	public bool lpressed; //флажок зажатой левой кнопки мыши 	public bool rpressed;  	public bool lclick;// флажок клика левой кнопкой мыши         public bool rclick;         public GameState drawstate; // используется для обработки событий, об этом позже          public bool darktransparency; // используется для разнообразия кнопочек, об этом позже         public bool lighttransparency;         public string text;          public bool undercursor;           public GuiObject(Rectangle rec, bool dtr, bool ltr, GameState st, UpdateFunction f,DrawFunction f2, string text = "")          {             rect = rec;             lpressed = false;             rpressed = false;             enable = true;             lclick = false;             rclick = false;             darktransparency = dtr;             lighttransparency = ltr;             drawstate = st;             this.text = text;             updateFunction = f;             drawFunction = f2;         } }  public enum GameState {         Any,         MainMenu,         Game } 

Итак, база была готова. Следующая проблема — обработка событий. Незадолго до написания этого кода, в универе нам рассказывали про делегаты. Уметь вызывать неизвестные тебе функции вполне неплохая способность. Было решено остановиться именно на них. В прочем именно делегаты и используются для создания кнопочек в Windows Forms приложениях на С#. В GuiObject добавился следующий код.

Код с делегатами

        public delegate void UpdateFunction(ref GuiObject me);         public delegate void DrawFunction(Texture2D line, Texture2D darkbackground, Texture2D lightbackground, ref GuiObject me);          public DrawFunction drawFunction;         public UpdateFunction updateFunction;          /*Указатели на себя используются для возможности изменения членов класса из других функций. Полезно, например, при создании переключателей*/         /*Текстуры line, darkbackground, lightbackground - это текстуры окантовки, и два фона*/ 

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

Обработка

//Gui.cs  public void Update(MouseState mstate,GameState state,GameTime gameTime)         {             for (int i = 0; i < elements.Length; i++)             {                 if (elements[i].drawstate == state&&elements[i].enable)                 {                     elements[i].Update(mstate);                     elements[i].updateFunction(ref elements[i]);                 }             }         }  /*Конечно можно использовать foreach вместо for, но в первом варианте нельзя делать ссылку на себя*/ /*Почему две функции обновления? Потому что первая обновляет состояние (разные click и pressed), а вторая удалённо вызывает обработчик именно для данного элемента (делегат короче).*/  // GuiObject.cs  public void Update(MouseState state)         {             lclick = false;             rclick = false;             if (rect.Contains(new Point(state.X, state.Y)))              {                 if (state.LeftButton == ButtonState.Pressed)                     if (!lpressed) { lclick = true; lpressed = true; }                 if (lpressed && state.LeftButton == ButtonState.Released)                     lpressed = false;                  if (state.RightButton == ButtonState.Pressed)                     if (!rpressed) { rclick = true; rpressed = true; }                 if (rpressed && state.RightButton == ButtonState.Released)                     rpressed = false;                 undercursor = true;             }             else undercursor = false;         } 

С обработкой разобрались, осталось лишь отрисовка. Помните, в GameState есть пункт Any? Если нужно, чтобы кнопочка была всегда,… а в прочем смотрите.

Отрисовка

//Gui.cs public void Draw(Texture2D line, Texture2D darkbackground, Texture2D lightbackground, GameState state)         {             for (int i = 0; i < elements.Length; i++)             {                 if ((elements[i].drawstate == GameState.Any || elements[i].drawstate == state)&&elements[i].enable)                     elements[i].drawFunction(line, darkbackground, lightbackground, ref elements[i]);             }         } 

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

Ужасный пример использования кода из разрабатываемой игры

// void Init()             state = GameState.MainMenu;              gui = new Gui();              gui.elements[0] = new GuiObject(new Rectangle(0, 0, width-205, height), false, false, GameState.Game, Main, MapGuiDraw);             gui.elements[1] = new GuiObject(new Rectangle(width - 205, 0, 205, height), false, false, GameState.Game, RightPanel, RightPanelDraw);             gui.elements[2] = new GuiObject(new Rectangle(width - 205, 0, 205, 39), false, false, GameState.Game, GameMenuButton, GameMenuButtonDraw);             gui.elements[3] = new GuiObject(new Rectangle((width - 150) / 2, height / 2, 150, 30), false, false, GameState.MainMenu, StartGameButton, StandartButtonDraw, "Start game");             gui.elements[4] = new GuiObject(new Rectangle((width - 150) / 2, height / 2 + 50, 150, 30), false, false, GameState.StartGameMenu, GenerateButton, StandartButtonDraw, "Generate");  //Draw Functions Example          void StandartGuiDraw( Texture2D line, Texture2D darkbackground, Texture2D lightbackground,  ref GuiObject me)         {             if (me.darktransparency) DrawTexturedRect( darkbackground, me.rect);             if (me.lighttransparency) DrawTexturedRect( lightbackground, me.rect);              DrawOutLine(line, me.rect);              if (me.text != "")             {                 Vector2 size = font.MeasureString(me.text);                 spriteBatch.DrawString(font, me.text, new Vector2((int)(me.rect.X + me.rect.Width / 2 - size.X / 2), (int)(me.rect.Y + me.rect.Height / 2 - size.Y / 2)), Color.White);             }         } 

Пример работы в картинках

Игра Townsman (в разработке)
Меню:

Меню генератора карт:

Игровой экран:

Игра Ancient Empires
Меню:

Меню редактора карт:

Редактор карт:

Особенности работы

Главный недостаток такого подхода — при добавлении нового элемента в Gui нужно лезть в класс и менять размер массива. Решается использованием списков.

В общем, это всё о чем я хотел рассказать. Спасибо за внимание.

Об играх

Об материалах: игры мои, Townsman ещё пишутся, скоро доделаю, и дам об этом знать.
Ancient Empires можно найти на ex.ua или в гугле со связкой small games.

ссылка на оригинал статьи http://habrahabr.ru/post/180613/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *