Skip to end of metadata
Go to start of metadata

You are viewing an old version of this page. View the current version.

Compare with Current View Page History

« Previous Version 3 Next »

This tutorial will show how to create a popup system that can be used to show messages and notifications for the user.

Initial Setup

This tutorial requires a level that will make the popups stand out against the background. The assets can be downloaded here, and need to be extracted in the root folder of the project. These assets contain an example level which is called Example which has everything that's required for the tutorial. Alternatively, a new blank level can be created in the Sandbox.

To begin, a new PopupFactory class needs to be created. The PopupFactory will allow popups to be easily created by calling one of its methods.

PopupFactory.cs
using System;
using System.Collections.Generic;
using System.Linq;
using CryEngine.Common;
using CryEngine.UI;

namespace CryEngine.Game
{
    public class PopupFactory
    {
        private Canvas _canvas;
 
        public PopupFactory(Canvas canvas)
        {
            _canvas = canvas;
        }
 
        public void MakeTestPopup(string content)
        {
 
        }
    }
}

 

Then inside the Game class a Canvas and a PopupFactory will be created.

Game.cs
public class Game
{
	private static Game _instance;
 
	private Canvas _canvas;
	private PopupFactory _popupFactory;
 
	private Game()
	{
		CreateUI ();
	}
 
	private void CreateUI()
	{
		_canvas = SceneObject.Instantiate<Canvas>(null);
		_canvas.RectTransform.Size = new Point(Screen.Width, Screen.Height);
		_popupFactory = new PopupFactory(_canvas);
	}
 
	public static void Initialize()
	{
		if(_instance == null)
		{
			_instance = new Game();
		}
	}
 
	public static void Shutdown()
	{
		_instance._canvas.Destroy();
		_instance = null;
	}
}

Creating the Popup UI

Next, the UIPopup class will be created, which inherits from the Panel class. This will expose a Header and a Content property.

Creating the Panel

First a basic panel will be created where the contents of the popup will sit.

UIPopup.cs
using System;
using System.Collections.Generic;
using CryEngine.Common;
using CryEngine.UI.Components;
using CryEngine.UI;

namespace CryEngine.Game
{
    public class UIPopup : Panel
    {
        private readonly Color DarkBlue = new Color(0.106f, 0.157f, 0.204f);
		private readonly Color MediumBlue = new Color(0.603f, 0.708f, 0.822f);
        private readonly Color LightBlue = new Color(0.573f, 0.678f, 0.792f);

        public override void OnAwake()
        {
            base.OnAwake();

            // Set the background color of the panel.
            Background.Color = DarkBlue;
 
			// Add a frame around the outside.
			var frame = AddComponent<Image>();
			var path = System.IO.Path.Combine(DataPath, "frame.png");
			frame.Source = ResourceManager.ImageFromFile(path);
			frame.SliceType = SliceType.Nine;
			frame.Color = LightBlue;

            // Set the size of the popup.
            RectTransform.Size = new Point(425f, 120f);
            RectTransform.Alignment = Alignment.Center;
        }
    }
}

Images in the C# UI have a SliceType property. When an image is sliced, some parts of the image will be stretched as the image size is changed. This is particularly useful for textures with borders or patterns that tile along the center.

An image slice type has three possible values: ThreeHorizontal, ThreeVertical, and Nine. The diagram below shows how each slice type splits the image and how these slices get stretched.

 

Back inside the PopupFactory, a method is added that will create an instance of a UIPopup.

PopupFactory.cs
private UIPopup MakePopup(string header, string content)
{
	var popup = SceneObject.Instantiate<UIPopup>(_canvas);

	return popup;
}

 

The MakeTestPopup method is changed so it creates a new popup when it's called.

PopupFactory.cs
public void MakeTestPopup(string content)
{
	MakePopup("Test Popup", content);
}

 

And inside the Game class the following line is added to the CreateUI method so MakeTestPopup is called.

Game.cs
_popupFactory.MakeTestPopup("Hello world!");

 

After compiling and running the game the popup will appear in the screen.

Adding a Header

Next up, we'll add a header to our popup along with some dividers.

To begin we'll add an inner container where our content will sit. It will be 20px smaller than the popup itself to create some padding.

At the top of the UIPopup class, add a private UIElement field:

private UIElement _innerContainer;

At the end of the OnAwake method, add the following to instantiate our inner container:

// Create an inner container with 10px of padding around the outside.
// All other content will be a child of this container.
_innerContainer = SceneObject.Instantiate<UIElement>(this);
_innerContainer.RectTransform.Size = new Point(RectTransform.Width - 20f, RectTransform.Height - 20f);
_innerContainer.RectTransform.Alignment = Alignment.Center;

Now we can add a header. At the top of the class add the following:

private Text _header;
public string Header { set { _header.Content = value.ToUpper(); } }

We'll be using the Header property later on to set the value of the header when creating our popup.

Finally, we'll create the header text itself by adding the following to the OnAwake method:

// Create the header that appears at the top of the popup.
_header = _innerContainer.AddComponent<Text>();
_header.DropsShadow = false;
_header.Height = 18;
_header.FontStyle = System.Drawing.FontStyle.Bold;
_header.Alignment = Alignment.TopLeft;


Now that the UI is updated, let's head back into our PopupFactory where we'll change the MakePopup method to the following:

private UIPopup MakePopup(string header, string content)
{
	var popup = SceneObject.Instantiate<UIPopup>(_canvas);
	popup.Header = header;
	// TODO: Content
	return popup;
}

Compile then run the game and you should see a header appearing with the text 'Test Popup'.

Adding Content

Next up, we'll add some content. Similar to the Header, we'll add a private field and a public property to the top of our UIPopup class:

private Text _content;
public string Content { set { _body.Content = value; } }

Then inside our OnAwake method we'll create our body Text:

_content = _innerContainer.AddComponent<Text>();
_content.DropsShadow = false;
_content.Height = 18;
_content.FontStyle = System.Drawing.FontStyle.Bold;
_content.Alignment = Alignment.Left;
_content.Offset = new Point(0f, 0f);
_content.Color = MediumBlue;


Back inside PopupFactory, modify the MakePopup method to the following:

private UIPopup MakePopup(string header, string content)
{
	var popup = SceneObject.Instantiate<UIPopup>(_canvas);
	popup.Header = header;
	popup.Content = content;
	return view;
}

Compile and head in game. Now you should a popup with the header 'Title' and the content 'Hello World!'.

Adding buttons

Now we'll add some buttons so that interacting with the popup provides some feedback. For this we'll be adding our buttons to a horizontal layout group. We'll also automatically adjust the width of all the existing buttons to be evenly distributed when a new button is added.

Inside UIPopup add the following to the top of the class:

public event EventHandler<UIPopup> Shown;
public event EventHandler<UIPopup> Closed;
 
private HorizontalLayoutGroup _buttonGroup;
private	List<Button> _buttons;

At the end of our OnAwake method, we'll add the following to create our horizontal layout group for the buttons and to initialize our Buttons list:

_buttonGroup = SceneObject.Instantiate<HorizontalLayoutGroup>(_innerContainer);
_buttonGroup.RectTransform.Size = new Point(_innerContainer.RectTransform.Width, 20f);
_buttonGroup.RectTransform.Alignment = Alignment.Bottom;
_buttonGroup.RectTransform.Pivot = new Point(0.5f, 0f);
 
_buttons = new List<Button>();

Then we'll add the following method:

public void AddButton(string label, Action onPress)
{

}

Let's instantiate a button:

public void AddButton(string label, Action onPress)
{
	// Instantiate a button
	var button = SceneObject.Instantiate<Button>(_buttonGroup);
	button.RectTransform.Size = new Point(_buttonGroup.RectTransform.Width, 30f);
	button.RectTransform.Alignment = Alignment.Left;
	button.RectTransform.Pivot = new Point(1f, 0.5f);
	button.Ctrl.Text.Content = label;
	button.Ctrl.Text.Height = 18;
	button.Ctrl.Text.FontStyle = System.Drawing.FontStyle.Bold;
	button.Ctrl.Text.DropsShadow = false;
	button.Background.Color = MediumBlue;
	
	// Store a reference to the button.
	_buttons.Add(button);
 
	// TODO: Resize buttons here.
 
	// Add the button to the horizontal group.
	_buttonGroup.Add(button);
}

To automatically resize the buttons, we'll need to add the following line between _buttons.Add and _buttonGroup.Add of the AddButton method:

// Set width based on number of buttons.
_buttons.ForEach(x => x.RectTransform.Width = _innerContainer.RectTransform.Width / _buttons.Count);

This will set the width of each button to be equal to the width of the popup's inner container divided by the number of buttons.

The last thing we'll need is to assign the onPress callback into the button's OnPressed event. So let's add the following to the end of the AddButton method:

button.Ctrl.OnPressed += () =>
{
	Closed?.Invoke(this);
	onPress?.Invoke();
};

Back inside our PopupFactory, we'll modify our MakeTestPopup method to add some buttons:

public void MakeTestPopup(string content)
{
	var popup = MakePopup("Test Popup", content);
	popup.AddButton("Yes", null);
	popup.AddButton("No", null);
}


Compile and run the game. Now you should see two buttons appear under our popup:

Adding Dividers

For the final bit of UI, we'll add a divider below the header and above the buttons.

Add the following method into UIPopup:

private UIElement AddDivider(UIElement root)
{
	var divider = SceneObject.Instantiate<Panel>(root);
	divider.RectTransform.Size = new Point(_innerContainer.RectTransform.Width, 1f);
	divider.AddComponent<Image>().Color = LightBlue.WithAlpha(0.2f);
	return divider;
}

This will create a 1px high Panel that is the width of our popup's inner container.

Inside our OnAwake method, we'll add two new lines to add our dividers:

// Create a divider under the header.
var topDivider = AddDivider(_innerContainer);
topDivider.RectTransform.Alignment = Alignment.Top;
topDivider.RectTransform.Padding = new Padding(0f, 30f);

// Create a divider above the buttons.
var bottomDivider = AddDivider(_innerContainer);
bottomDivider.RectTransform.Alignment = Alignment.Bottom;
bottomDivider.RectTransform.Padding = new Padding(0f, -30f);

Compile and run the game. Now your popups should look like this:

Implementing an 'Info' popup

Now that we have our popup UI in place, we can start to work on creating a functional 'Info' popup.

Our popup system only allows one popup to be display at any one time. If other popups are shown at the same time, we'll need to prevent them from being shown but flag them appropriately so that when the current popup is closed, the next popup waiting to be shown will be displayed.

We'll begin by making some modifications to UIPopup.

At the top of the class we'll add the following property:

public bool FlaggedForShow { get; private set; }

Then we'll add the follow two methods which will trigger our Shown and Closed methods:

public void Show()
{
	FlaggedForShow = true;
	Shown?.Invoke(this);
}
 
public void Closed()
{
	FlaggedForShow = false;
	Closed?.Invoke(this);
}

Inside our PopupFactory, we'll need to hook into the Shown and Closed events to display then destroy the popup.

We'll also need to keep track of when a popup is added and removed, so that when a popup is closed any remaining popups that are in the system will be shown.

Let's begin at the top of the class where we'll add a list of UIPopup:

private List<UIPopup> _popups;

Then at the end of the constructor we'll initialize the list:

public PopupFactory(Canvas canvas)
{
    _canvas = canvas;
    _popups = new List<UIPopup>();
}

Now we'll modify our MakePopup method so that it registers into the Shown and Closed callbacks:

private UIPopup MakePopup(string header, string content)
{
    var popup = SceneObject.Instantiate<UIPopup>(_canvas);
    popup.Header = header;
    popup.Content = content;

    // Register popup.
    popup.Shown += ShowPopup;
    popup.Closed += OnClosePopup;
 
	_popups.Add(popup);
 
	popup.Active = false;

    return popup;
}

Then we'll add the associated handlers:

private void ShowPopup(UIPopup popup)
{
	Mouse.ShowCursor();

	// Display popup then bring it to front.
	popup.Active = true;
	popup.SetAsLastSibling();
}
 
private void OnClosePopup(UIPopup popup)
{
	Mouse.HideCursor();
 
	// Unregister popup.
	popup.Shown -= OnShowPopup;
	popup.Close -= OnClosePopup;
	_popups.Remove(popup);
 
	popup.Destroy();
 
	// Find a popup that is waiting to be shown.
	var nextPopup = _popups.FirstOrDefault(x => x.FlaggedForShow == true);
	if (nextPopup != null)
	{
  		// Show the next popup waiting to be shown.
   		ShowPopup(nextPopup);
	}
	else
	{
   		Mouse.HideCursor();
	}
}

Back in our PopupFactory, we'll add a new method for creating the popup:

public UIPopup MakeInfoPopup(string content, Action onOkay = null)
{
	var popup = MakePopup("Confirm", content);
	popup.AddButton("Okay", onOkay);
	return popup;
}

Back in SampleApp, we'll modify our 'MakeTestPopup' line and will replace it with the following:

_popupFactory
	.MakeOkayPopup("Something important just happened!", () => Log.Info("You acknowledge this and become deeply moved."))
	.Show();

Note that we now have to explicitly called Show to display the popup.


Compile and head in-game. Now the following popup will appear. Clicking the button will cause the popup to close.

If you look in the console, you'll see that it printed a message when you pressed the button:

Adding a Background Panel

Now that we have a basic system in place, we want to show a transparent black panel behind our popup when it is shown to help distinguish the popup from the underlying UI.

At the top of PopupFactory add the following field:

private Panel _panel;

Then in the constructor of our PopupFactory we'll instantiate a Panel with a black background:

public PopupFactory(Canvas canvas)
{
	_canvas = canvas;
	_popups = new List<UIPopup>();
	_panel = SceneObject.Instantiate<Panel>(_canvas);
	_panel.Background.Color = Color.Black.WithAlpha(0.8f);
	_panel.RectTransform.Size = new Point(Screen.Width, Screen.Height);
	_panel.RectTransform.Alignment = Alignment.Center;
	_panel.Active = false;
}

As the panel starts inactive, we'll need some logic that will enable it. So we'll be changing the Active state of the panel based on when a popup is shown and closed.

To do so, we'll modify our OnShowPopup method:

private void ShowPopup(UIPopup popup)
{
	Mouse.ShowCursor();

	// Enable then bring the panel to front.
	_panel.Active = true;
	_panel.SetAsLastSibling();

	// Display popup then bring it to front.
	popup.Active = true;
	popup.SetAsLastSibling();
}

Then we'll modify our OnClosePopup method:

// Find a popup that is waiting to be shown.
var nextPopup = _popups.FirstOrDefault(x => x.FlaggedForShow == true);
if (nextPopup != null)
{
 	// Show the next popup waiting to be shown.
 	ShowPopup(nextPopup);
}
else
{
 	Mouse.HideCursor();
	_panel.Active = false;
}

Compile and head in game. When the popup now shows you'll see that the background panel appears. When it is closed, the background panel will disappear.

Implementing additional popups

With the groundwork in place, we can easily implement additional popups. In the PopupFactory we'll add a new method that will create a Yes/No style popup:

public UIPopup MakeConfirmationPopup(string content, Action onYes, Action onNo = null)
{
	var popup = MakePopup("Info", content);
	popup.AddButton("Yes", onYes);
	popup.AddButton("No", onNo);
	return popup;
}

And we'll also add a Yes/No/Cancel style popup:

public UIPopup MakeConfirmationCancelPopup(string content, Action onYes, Action onNo, Action onCancel)
{
	var popup = MakePopup("Confirm", content);
	popup.AddButton("Yes", onYes);
	popup.AddButton("No", onNo);
	popup.AddButton("Cancel", onCancel);
	return popup;
}

And there you go!

  • No labels