Introduction:

The Svelto.ECS methods designed to implement ECS based GUIs are simple and powerful, following the fundamental principles around the whole Svelto implementation. Their design doesn’t involve use of callbacks, as it relies on the power of Svelto.Tasks (but you can achieve similar results with other patterns like async/await) and it uses a naive, but efficient way to bind GUI elements to specific entities. In this article we will see how to achieve this with a simple, but feature complete scenario. Source code is available on github as usual: https://github.com/sebas77/Svelto.MiniExamples/tree/master/Example3-GUI

The Scene

I strongly recommend to use nested prefabs to build GUIs. This because the code is designed to handle every functional piece of the GUI as a separate widget that can be composed through nested prefabs, making simple to create GUIs that would be potentially work out of the box.

Labels and Buttons are the simplest widgets. A Gridbox can be made of GridBoxCell and Buttons widgets, but the Gridbox itself can be a widget on its own, reusable by other GUIs. All the widgets logic is fully encapsulated, therefore their reuse should be as simple as reusing their entities and engines already developed.

Our project includes 3 widgets: a localized label, an input localized label (field) and a button with localized text:

The main GUI is an interface to input a display name and looks like:

and this is the hierarchy of the GUI:

When developing GUIs with Svelto, the prefab hierarchy must be seen just a way like another to serialize the GUI structure. It must not been seen any differently than using a XAML based GUI for instance.

In this example the DisplayName GUI is directly placed on the scene, but I’d rather load the GUI at run-time using addressable assets as otherwise the scene will rapidly become a container of many guis.

The Composition Root

let’s focus first on how the Svelto Entities are built from the scene.

The SveltoGUIHelper will help for this purpose. For this case we (sort of) serialize the entities in the prefab hierarchy through the use of the EntityDescriptorHolder (as seen in the Survival example).

Each gameobject that generates a Svelto Entity has an EntityDescriptorHolder like:

and so on, they not coincidentally appear on the root of each nested prefab in the hierarchy.

let’s study the following code

The first Create method builds the Svelto Entity found on the DisplayNameDescriptorHolder linked to the root GameObject. The following CreateAll build all the entities related to the nested widgets (prefabs) found in the GUI hierarchy.

Of course these entities must be managed by the relative engines. For this example these are:

GUI related svelto entities heavily rely on classic GameObjects and on the Implementor based Object Oriented abstraction to abstract them, therefore Entity Descriptors are full of EntityViewStructs. Let’s see how they work in some few cases (the following explanation assumes that you know how EntityViewStructs and Implementor work):

LocalizedTextDescriptor is an EntityDescriptor that generates an entity with a LocalizedLabelEntityViewStruct which holds an ILabelText label. The ILabelText is implemented by the implementor (Monobehaviour) LabelTextProImplementor to abstract TextMeshProUGUI objects. Of course the implementor is found on the game object where the TextMeshProUGUI lies.

ILabelText textKey receives an ID to identify the string to print. The key itself is a string just because theoretically it could be used as key in a json file or similar text files.

the LocalizedLabelEntityViewStruct is managed by the LocalizingTextEngine. This engine reacts on add and when a LocalizedLabelEntityViewStruct is added, it switches its text from the selected key to a translated text. In this way, when the application starts, all the entities built with a label will automatically convert their keys in to localized strings.

A naive LocalizationService is used to make the string localization ready. This static class doesn’t use the Service Layer, but shows how static classes could be used to hide away dependencies that are ECS-shy (I will discuss about this more in depth in another occasion).

ValidateDisplayGUIInputEngine is the main driver of the display name GUI logic. It has two responsibilities (hence it could have been split in two engines!): checking that we are using a valid name and deciding what to do when the submit button is clicked.

Let’s start from the validation as it shows some Svelto ECS features dedicated to the GUI related logic. First one is QueryUniqueEntity. This method, that can be useful in other contexts too, is a contract that guarantees that in the group chosen there must be only one entity. This shows the intention that we are using a group to identify a specific entity.

In this case, since we know that we have just one InputLabel, we fetch its entity through this code:

ExclusiveGroups.DisplayName is of course the group that identifies the entities that belong to the DisplayName GUI.

In this method we also find an implementation of an official Svelto service layer. The Svelto Service is a much better abstraction to what can be provided by a simple static class like the LocalizationService.

The IServiceFactory maps service interfaces to their implementation. The implementation can change according the context/composition root. For example in this case we are using a mock request, but the composition root could have created a real request, connecting to a real web server or whatever. Changing the service implementation won’t affect the code of the Engine that uses it.

This is how a ServiceFactory is implemented:

Service requests do not use callbacks, but instead are fully integrated with Svelto.Tasks. In this case we are using a service request that accepts an external dependency (the name to validate). The integration works in this way:

The service request implementation can be synchronous or asynchronous, as the IEnumerator will be able to handle both the situation. The important thing to keep in mind is that the next instruction after the yield will be executed only when the service completes its execution.

ValidateDisplayGUIInputEngine assumes that the GUI can be opened and used only once, so it keeps its loop activated as long as the GUI is displayed.
The loop checks if the current string is different than the previous one checked, if so it asks to the service layer to validate it and wait a second before checking again.

now the important bit: depending if the validation of the string has been successful or not, I need to write a different string in to a specific label to give some feedback to the user. How would be possible to get hold of this specific label though?

The EntityDescriptorHolder has two optional fields: Group Name and Id. The Group Name is usually used more often together with QueryUniqueEntity. Setting a group name will build the specific entity in the specific group (and in case with a specific ID) overriding any other parameter. In this case I am building the NameError label (I should have found a better name sorry) in the group named DisplayName.FeedbackLabel which is in fact ExclusiveGroup.FeedbackLabel (see code below)

The trick is to map a string to a specific ExclusiveGroup using the new ExclusiveGroup constructor that accepts a string. In this case:

Which is used then as:

While all the DisplayName GUI unique entities can be found through the DisplayName ExclusiveGroup, the labels are not unique, therefore I use a new group to identify it. The EntityDescriptorHolder Group Name allow to glue the Group to a specific label in the gameobject hierarchy.

The CheckOKClicked task waits for the SubmitButton to be clicked. If it’s clicked, it assumes that a valid name is found (which may be too naive as approach, but OK for this example). Since the User has been validated, the UserEntity is moved to the group UserToRegister, which is irrelevant for this example, but shows how groups can be used as state too. A yield break will stop this task at this point.

Let’s have a look at how the button logic works. The common GUI frameworks provides two basic GUI functionalities that are being assumed to be useful across all the GUIs:

ButtonClickingEventEngine reacts on ButtonEntityViewStruct entity add (which means when a Button entity is built and submitted) and registers the DispatchOnSet found inside the Button implementor to notify when a button has been pushed.

DispatchOnSet and DispatchOnChange use is much less relevant nowadays, but they haven’t been removed just because of the GUI pattern. Note that since their constructor needs now a valid EGID, it won’t be possible to instantiate them inside a monobehaviour. Hence their use has been slightly modified to be certain that they are used for valid reasons.

The ButtonClickingEvent Engine registers a listener to the Implementor DispatchOnSet buttonEvent. The implementor register the listener to the UI.Button click event and dispatch a message back when the button is clicked. However the new DispatchOnSet/Change cannot be used in multiple places and their new way to be used assumes that one and only one specific engine has the responsibility to handle their message. The consequence of handling this message can be modifying an entity as the only way to communicate with other engines.

What the publisher publishes is not custom data, but a copy of a modified EntityStruct. In fact the publisher/consumer pattern relies on an entity to be modified to work. This is why the entitiesDB.PublishEntityChange accepts only an EGID:

The publisher sends a warning if no consumers are registered, but doesn’t assume the number of consumers registered. N consumers can now register to a ButtonEntityStruct change and usually the pattern is to have a consumer per Engine. The Engine is fully responsible to create the consumer and consume the data inside it. If a consumer is created, but no data consumed, publishing will throw an exception.

We can now switch back to ValidateDisplayGUIInputEngine and see how the engine consumes ButtonEntityStruct changes:

First thing to note is that we are creating a consumer that consumes only ButtonEntityStruct changes that come from the group ExclusiveGroups.DisplayName. This is important because we don’t want to listen to every button change. Note that the consumer has been created with a buffer of size 1: this because we expect to consume at the same frequency of the publishing.

The Publisher/Consumer is the only Svelto.ECS class to be multi-thread safe (using fast locking), that because the pattern is useful also to let engines communicate between different threads. It’s also useful to let different EnginesRoots communicate together.

Conclusion (what we have learned)

  • Svelto ships with a subset of functionalities that help build GUIs. Svelto ECS based GUI are strongly dependent on Gameobjects.
  • Svelto ships with a set of methods that allow to create automatically ECS entities from gameobjects
  • Monobehaviours as implementors are used to glue the GUI functionalities to the Svelto.ECS engines through the use of DispatchOnSet and DispatchOnChange
  • Implementors fully abstract the OOP functionalities from the ECS ones. The engines are never aware of OO objects.
  • The new ExclusiveGroup naming feature together with the EntityDescriptorHolder parameters allow to bind specific entities to specific groups so that those entities can be used directly from Svelto.ECS engines. Changing the entity data will implicitly change the GUI data. (it’s a sort of databinding, but I don’t want to call it in this way as no events are involved)
  • The new Service Layer interface/pattern allows to abstract services from ECS. It’s a useful design to follow for these cases.
  • The Publisher/Consumer EnginesRoot functionality is powerful and can be used to let engines communicate between each other as consequence of user events.

Leave a Reply