This tutorial will cover how to create a menu system. The goal of this tutorial is to have a home screen with a Start Game option that will load up a level and a Quit to Desktop option that will be used to quit the game.
The starting point for this tutorial is the MainMenu
class. This will be the root object for the menu. All other menus will be a child of the Main Menu.
using CryEngine;
using CryEngine.UI;
class MainMenu
{
private Canvas _canvas;
private MainMenuController _controller;
private Menu _currentMenu;
public bool Visible { get { return _canvas.Active; } set { _canvas.Active = value; } }
public MainMenu(Canvas canvas)
{
_canvas = canvas;
// Create the Controller that controls this menu.
_controller = new MainMenuController(this);
// Instantiate the menu.
_currentMenu = new HomeMenu(canvas, _controller);
Mouse.ShowCursor();
}
}
The MainMenu
will have a MainMenuController
. This class will handle all the logic related to starting a game, switching menus, etc.
using CryEngine;
class MainMenuController
{
private MainMenu _mainMenu;
public MainMenuController(MainMenu mainMenu)
{
_mainMenu = mainMenu;
}
public void StartGame()
{
}
public void QuitToDesktop()
{
}
}
Next is the Menu
class and the HomeMenu
class. The HomeMenu
will eventually create a UI and handle the user's interaction with it.
using CryEngine;
using CryEngine.UI;
abstract class Menu
{
protected UIElement Root { get; private set; }
protected UIElement View { get; private set; }
protected MainMenuController Controller { get; private set; }
protected Menu(UIElement root, MainMenuController controller)
{
Root = root;
Controller = controller;
}
}
class HomeMenu : Menu
{
public HomeMenu(UIElement root, MainMenuController controller) : base(root, controller)
{
}
}
And finally inside the Game
class an instance of the MainMenu
and a Canvas
will be created.
public class Game : IDisposable
{
private static Game _instance;
private Canvas _canvas;
private MainMenu _mainMenu;
private Game()
{
// The server doesn't support rendering UI and receiving input, so initializing those system is not required.
if(Engine.IsDedicatedServer)
{
return;
}
_canvas = SceneObject.Instantiate<Canvas>(null);
_mainMenu = new MainMenu(_canvas);
}
public static void Initialize()
{
if(_instance == null)
{
_instance = new Game();
}
}
public static void Shutdown()
{
_instance?.Dispose();
_instance = null;
}
public void Dispose()
{
if(Engine.IsDedicatedServer)
{
return;
}
_canvas.Destroy();
}
}
The first menu will be a Home menu that will feature a Start Game button and a Quit to Desktop button.
For this part the CRYENGINE logo is required to be in the assets directory in /assets/libs/ui. The logo can be downloaded .
The home menu will be managed by the UIHomeMenu
class which inherits from the UIElement
class. The class is responsible for creating a basic container where the buttons will sit.
using System;
using CryEngine;
using CryEngine.Rendering;
using CryEngine.Resources;
using CryEngine.UI;
class UIHomeMenu : UIElement
{
public override void OnAwake()
{
// Create a vertical panel strip.
var panel = Instantiate<Panel>(this);
panel.Background.Color = Color.Black.WithAlpha(0.5f);
panel.RectTransform.Size = new Point(400f, Renderer.ScreenHeight);
panel.RectTransform.Alignment = Alignment.Left;
panel.RectTransform.Pivot = new Point(1f, 0.5f);
panel.RectTransform.Padding = new Padding(50f, 0f);
// Display the logo.
var logo = Instantiate<Panel>(panel);
// Load the logo from the 'libs/ui' folder.
var path = System.IO.Path.Combine(DataDirectory, "logo.png");
logo.Background.Source = ResourceManager.ImageFromFile(path, true);
logo.RectTransform.Alignment = Alignment.Top;
logo.RectTransform.Size = new Point(300f, 300f);
logo.RectTransform.Padding = new Padding(0f, 150f); // Padding from top of the panel
}
}
Make sure to include the CryEngine.Resources and CryEngine.Rendering namespace.
Back in the HomeMenu
class the menu needs to be instantiated and attached to the root UIElement
. This is done with a new method in the Menu
class that will create and attach the UI properly to the root.
protected T CreateView<T>() where T : UIElement
{
var view = SceneObject.Instantiate<T>(Root);
view.RectTransform.Alignment = Alignment.Center;
view.RectTransform.Size = new Point(Root.RectTransform.Width, Root.RectTransform.Height);
// Store a reference to the View. This is needed later on.
View = view;
return view;
}
The method will be called from inside the HomeMenu
class to create an instance of UIHomeMenu
.
private UIHomeMenu _view;
public HomeMenu(UIElement root, MainMenuController controller): base(root, controller)
{
_view = CreateView<UIHomeMenu>();
}
After compiling and running the game will look like this:
Now that the basic UI is in place, the Start Game and Quit to Desktop buttons can be added.
The buttons will be displayed in a column. The UIButtonList
is going to act as the container for the buttons in the menu. To make it easier to position the buttons the UIButtonList
will inherit from VerticalLayoutGroup
which is specifically made to display elements in a column.
The AddButton
method will create an instance of a Button
and will add it to the vertical layout group. This way buttons can be easily added to the menu.
using CryEngine;
using CryEngine.UI;
class UIButtonList : VerticalLayoutGroup
{
public Button AddButton(string label)
{
// Make the button.
var button = SceneObject.Instantiate<Button>(this);
button.RectTransform.Size = new Point(RectTransform.Width, 40f);
button.RectTransform.Alignment = Alignment.Top;
button.RectTransform.Pivot = new Point(0.5f, 1f);
button.Ctrl.Text.Content = label;
// Add the button to the vertical layout group.
Add(button);
return button;
}
}
Now that buttons can be added the UIButtonList
is ready to be added to the UIHomeMenu
. This will be done in the OnAwake
method of the UIHomeMenu
. After creating an instance of UIButtonList
the Start Game and Quit to Desktop buttons will be added to it.
To handle the button pressed two new events need to be added to the UIHomeMenu
as well. One StartPressed
for when the start button is pressed, and one QuitToDesktopPressed
for when the quit button is pressed. And finaly a private field of type UIButtonList
named _mainMenuButtons
is required to keep a reference to the container.
public event Action StartPressed;
public event Action QuitToDesktopPressed;
private UIButtonList _mainMenuButtons;
At the end of the OnAwake
method of UIHomeMenu
an instance of UIButtonList
will be created, and the buttons and their related callbacks will be added.
// Create the UIButtonList.
_mainMenuButtons = SceneObject.Instantiate<UIButtonList>(panel);
_mainMenuButtons.RectTransform.Size = new Point(panel.RectTransform.Width - 50f, 400f);
_mainMenuButtons.RectTransform.Alignment = Alignment.Center;
// Assign callbacks to each button for when they are pressed.
_mainMenuButtons.AddButton("Start Game").Ctrl.OnPressed += () => StartPressed?.Invoke();
_mainMenuButtons.AddButton("Quit to Desktop").Ctrl.OnPressed += () => QuitToDesktopPressed?.Invoke();
After compiling the project and running the game the scene looks like this.
Now that the UI is ready and the necessary callbacks are available, the HomeMenu
can be modified to perform an action when the buttons are pressed.
First the the related method of the MainMenuController
are added to the OnPressed events of the UIHomeMenu
.
class HomeMenu
{
private UIHomeMenu _view;
public HomeMenu(UIElement root, MainMenuController controller)
{
_view = CreateView<UIHomeMenu>();
// Assign a callback to the button press event that will call the controller.
_view.StartPressed += controller.StartGame;
_view.QuitToDesktopPressed += controller.QuitToDesktop;
}
}
Finally, the methods in MainMenuController
are implented so that StartGame and QuitToDesktop perform an action. In the MainMenuController
class the following methods need to be updated.
public void StartGame()
{
Engine.Console.ExecuteString("map FirstMap", false, true);
// Hide the menu.
_mainMenu.Visible = false;
}
public void QuitToDesktop()
{
Engine.Shutdown();
}
After compiling and running the game the menu will now be functional. When Start Game is pressed it will load the map called FirstMap (if that map exists) and hide the UI. If Quit to Desktop is pressed a shutdown message is send to the engine which will quit the game.
For a finishing touch, the buttons can be made more visually appealing.
If the mouse hover over the button the following actions need to happen:
First a new class called the UIButton
that derives from Button
is required. The OnAwake
method will be overridden so it will change the style of the text and disable the background image.
using CryEngine;
using CryEngine.Resources;
using CryEngine.UI;
class UIButton : Button
{
public override void OnAwake()
{
base.OnAwake();
Background.Active = false; // Disable the background image.
ShowPressedFrame = false; // Disable the frame the appears when pressing the button.
Ctrl.Text.Alignment = Alignment.Left;
Ctrl.Text.Height = 24;
Ctrl.Text.DropsShadow = false;
}
}
Then inside the UIButtonList
class, a UIButton
will be instantiated instead of a Button
by replacing the following line.
var button = SceneObject.Instantiate<Button>(this);
...with...
var button = SceneObject.Instantiate<UIButton>(this);
After compiling the menu will now look as follows.
Next the text needs to move to the right during a certain amount of time if the mouse is hovering over it, and return to the original position once the mouse has left.
Inside the UIButton
class the following fields need to be added.
private bool _mouseOver;
private float _moveSpeed = 4.0f;
private float _moveProgress = 1.0f;
private float _targetOffset = 15.0f;
After that the SetOver
, SetNormal
and OnUpdate
need to be overridden. Inside those methods the colors will be overridden, and the value of _mouseOver
will be changed. OnUpdate
will be used to move the text smoothly.
public override void SetOver()
{
// Change the text color on mouse over.
Ctrl.Text.Color = new Color(0.8f, 0.8f, 0.8f);
if(!_mouseOver)
{
_mouseOver = true;
_moveProgress = 1.0f - _moveProgress;
}
}
public override void SetNormal()
{
// Reset the text color when the mouse leaves the button.
Ctrl.Text.Color = Color.White;
if(_mouseOver)
{
_mouseOver = false;
_moveProgress = 1.0f - _moveProgress;
}
}
public override void OnUpdate()
{
if(_moveProgress < 1.0)
{
_moveProgress = MathHelpers.Max(_moveProgress + _moveSpeed * FrameTime.Delta, 1.0f);
var progress = _mouseOver ? _moveProgress : 1.0f - _moveProgress;
var offset = MathHelpers.Lerp(0.0f, _targetOffset, progress);
Ctrl.Text.Offset = new Point(offset, 0);
}
}
After compile and running the game mousing over the buttons will cause them to gradually move to the right:
Finally an arrow needs to appear next to the button when mousing over the button.
The following fields are required for this at the top of the class.
private string _hoverArrowUrl = System.IO.Path.Combine(DataDirectory, "arrow_rt.png");
private Panel _hoverArrow;
Then inside the OnAwake
method a new instance of Panel
will be created which show the arrow image.
_hoverArrow = SceneObject.Instantiate<Panel>(this);
_hoverArrow.RectTransform.Size = new Point(16f, 16f);
_hoverArrow.RectTransform.Alignment = Alignment.Left;
_hoverArrow.RectTransform.Pivot = new Point(1f, 0.5f);
_hoverArrow.Background.Source = ResourceManager.ImageFromFile(_hoverArrowUrl);
_hoverArrow.Background.Color = Color.White;
_hoverArrow.Active = false;
Finally, the arrow will be enabled and disabled in the SetOver
and SetNormal
methods.
public override void SetOver()
{
// Change the text color on mouse over.
Ctrl.Text.Color = new Color(0.8f, 0.8f, 0.8f);
if(!_mouseOver)
{
_mouseOver = true;
_moveProgress = 1.0f - _moveProgress;
_hoverArrow.Active = true;
}
}
public override void SetNormal()
{
// Reset the text color when the mouse leaves the button.
Ctrl.Text.Color = Color.White;
if(_mouseOver)
{
_mouseOver = false;
_moveProgress = 1.0f - _moveProgress;
_hoverArrow.Active = false;
}
}
After compiling and running the game the arrow will show up when the mouse hovers over the buttons in the menu.