Creating nameplates and other dynamic onscreen elements

ui tutorials

3/8/2022

Mihail Todorov

Markers, character information and other elements that frequently move on the screen based on character and camera positions are very common in game UI. The biggest struggle developers face while creating these elements in Gameface/Prysm is how to create a more complex element while maintaining maximum performance. In this tutorial we’ll demonstrate how to create player and enemy nameplates that are positioned on screen depending on where their corresponding character is. We’ll also show how to add more complexity to these nameplates with health and mana bars, experience gaining and more. And finally, we’ll add these UI elements to a simple game.

This tutorial assumes some familiarity with Gameface or Prysm. If you are not that familiar with the product yet, you can check out our starter guide (link to starter guide).

To get started we’ll first create the frontend part of this sample and we can do this by making a html page called ‘index.html’ where the user and enemy nameplates will be defined. The reason why we are doing this in a single page is because it will be more performant than having it split into multiple pages (and views respectively) for each nameplate.

Once we have our html page created, inside we’ll add the following code.

1
<head>
2
<link rel="stylesheet" href="./style.css" />
3
</head>
4
<body>
5
<div class="user-nameplate-container"></div>
6
7
<div class="enemy-nameplate-container"></div>
8
9
<script src="./cohtml.js"></script>
10
<script src="./index.js"></script>
11
</body>

Here we are adding the style.css(link to file) that contains all of the styles that will be applied, cohtml.js(link to file) that will allow us to communicate with the game, and index.js that we’ll create for any custom logic that we might want to add.

In the user-nameplate-container we’ll add the following html code:

1
<div class="user-avatar-container">
2
<div class="user-avatar user-avatar-image"></div>
3
<div class="user-power-move-bar">
4
<div class="user-power-move-overlay"></div>
5
<div class="user-power-move">
6
<div class="user-power-move-icon"></div>
7
</div>
8
</div>
9
<div class="user-level">100</div>
10
</div>
11
<div class="user-stats-container">
12
<div class="user-power-moves-counter">
13
<div class="user-power-move-container">
14
<div class="user-power-move-fill"></div>
15
</div>
16
</div>
17
18
<svg viewBox="0 0 590 108" class="user-bars-container">
19
<defs>
20
<clipPath id="bar">
21
<path d="M1,2H556s1.823,25.039,32,19c3.131-.626-35.578,74.836-60,84-5.009,1.88-526.256,0-526.256,0Z"/>
22
</clipPath>
23
<linearGradient id="healthGradient">
24
<stop offset="5%" stop-color="\#9b2312" />
25
<stop offset="95%" stop-color="\#c54230" />
26
</linearGradient>
27
<linearGradient id="manaGradient">
28
<stop offset="5%" stop-color="\#1a392e" />
29
<stop offset="95%" stop-color="\#40879c" />
30
</linearGradient>
31
<linearGradient id="expGradient">
32
<stop offset="5%" stop-color="\#41461b" />
33
<stop offset="95%" stop-color="\#2f6113" />
34
</linearGradient>
35
</defs>
36
<path
37
fill="\#2a1e23"
38
stroke="\#947662"
39
stroke-width="10"
40
fill-rule="evenodd"
41
d="M1,2H556s1.823,25.039,32,19c3.131-.626-35.578,74.836-60,84-5.009,1.88-526.256,0-526.256,0Z"
42
/>
43
<line x1="0" y1="65" x2="565" y2="65" stroke="\#947662" stroke-width="5" />
44
<path
45
fill="\#2a1e23"
46
stroke="\#947662"
47
stroke-width="7"
48
fill-rule="evenodd"
49
d="M510,108l-20,20l-590,0v-20Z"
50
/>
51
<g id="user-bars" clip-path="url(\#bar)">
52
<rect
53
y="6"
54
width="590"
55
height="59"
56
fill="url(\#healthGradient)"
57
class="user-health-bar"/>
58
<rect
59
y="70"
60
width="565"
61
height="45"
62
fill="url(\#manaGradient)"
63
class="user-mana-bar"
64
/>
65
</g>
66
<rect
67
transform="skewX(-40deg)"
68
y="113"
69
x="165"
70
width="430"
71
height="15"
72
fill="url(\#expGradient)"
73
class="user-experience-bar"
74
/>
75
</svg>
76
<div class="user-health-percentage">100</div>
77
<div class="user-mana-percentage">100</div>

Once we run this code in the Player, we can see the following:

Where each of the numbers refers to:

  1. User level
  2. Experience bar
  3. Mana bar
  4. Health bar
  5. User power moves counter - these will be triggered when a power move is executed and is depleted
  6. Power move fill - which will be empty after each use and will start to fill. Once full, you can use the power move

After that we can add the enemy nameplate with the following code:

1
<div class="enemy-nameplate-container">
2
<div>
3
<svg class="enemy-health-bar" viewBox="0 0 600 600">
4
<defs>
5
<linearGradient id="healthGradient">
6
<stop offset="5%" stop-color="\#9b2312" />
7
<stop offset="95%" stop-color="\#c54230" />
8
</linearGradient>
9
</defs>
10
<path
11
class="enemy-health-bar-fill"
12
d="M 50, 300a 250,250 0 1,0 500,0a 250,250 0 1,0 -500,0"
13
fill="transparent"
14
stroke-width="70"
15
stroke="\#c54230"
16
/>
17
</svg>
18
<div class="user-avatar-container">
19
<div class="user-avatar enemy-avatar"></div>
20
<div class="user-power-move-bar enemy-level">100</div>
21
</div>
22
</div>
23
</div>

Once we run it in the player, we’ll get the following, where 1 is the enemy health bar and 2 is the enemy level:

Now that we have our nameplates created, we’ll need to go over each element and data-bind it in order to have it working with our game. We’ll start with the user nameplate since it will be static on our screen.

The first thing that we’ll need to data-bind are the power moves. The idea here is simple: the power moves will be an array of objects which will have a filled property that can be either true or false. This will allow us to control how many power moves our character has. For each object in the array we’ll create a power move element and then we’ll add a data-bind-if to create the fill.

1
<div class="user-power-moves-counter">
2
<div class="user-power-move-container" data-bind-for="powerMove:{{PlayerModel.playerPowerMoves}}">
3
<div class="user-power-move-fill" data-bind-if="{{powerMove.filled}}"></div>
4
</div>
5
</div>

Now if we use a power move, we can set the filled property to false and it will appear as empty on the screen.

Next we can set the level and the experience bar.

For the level we can just data-bind the value:

1
<div class="user-level" data-bind-value="{{PlayerModel.playerLevel}}">100</div>

And for the experience bar we’ll data-bind the width:

1
<rect
2
transform="skewX(-40deg)"
3
y="113"
4
x="165"
5
width="430"
6
height="15"
7
fill="url(\#expGradient)"
8
data-bind-style-width="{{PlayerModel.playerExperience}}"
9
class="user-experience-bar"
10
/>

We can also do the same for the health and mana bars:

1
<rect
2
y="6"
3
width="590"
4
height="59"
5
fill="url(\#healthGradient)"
6
class="user-health-bar"
7
data-bind-style-width="{{PlayerModel.playerHealth}}"
8
/>
9
10
<rect
11
y="70"
12
width="565"
13
height="45"
14
fill="url(\#manaGradient)"
15
class="user-mana-bar"
16
data-bind-style-width="{{PlayerModel.playerMana}}"
17
/>

Since the health, mana and experience bars are svg elements that are scaled, if we use percentage the width will be set according to the parent svg element width and then scaled. To avoid that and any unnecessary calculations, we can set the max values to be the same as the width. That being said, sometimes you would want to display things in a different way on the screen and in such cases it is recommended to have the logic that does this in the backend rather than the frontend to avoid any performance issues.

For simplicity we’ll set all of these values as they are without any changes - for example we’ll set the max player health to be 590 as is the width of the bar.

The last things that have to be shown are the texts of the health and mana, for this we’ll be using data-bind-value again:

1
<div class="user-health-percentage" data-bind-value="{{PlayerModel.playerHealth}}">100</div>
2
<div class="user-mana-percentage" data-bind-value="{{PlayerModel.playerMana}}">100</div>

Next we can add data-binding to the enemy nameplates. Since we expect to have more than one enemy, we’ll start by adding a data-bind-for to our “enemy-nameplate-container” class element:

1
<div
2
class="enemy-nameplate-container"
3
data-bind-for="index, enemy:{{EnemyModel.enemies}}"
4
>

With this we can now set the individual values of the nameplate. We’ll first start with the positioning. Since we are going to apply transformations to it such as translating and scaling it, we can directly use ‘data-bind-style-transform2d’ which allows us to provide a transform matrix from our model.

1
<div
2
class="enemy-nameplate-container"
3
data-bind-for="index, enemy:{{EnemyModel.enemies}}"
4
data-bind-style-transform2d="{{enemy.transform}}"
5
>

Then the only two things left to data-bind are:

The level, which we can do the same way as the user nameplate

1
<div class="user-power-move-bar enemy-level" data-bind-value="{{enemy.level}}">100</div>

And the health bar

1
<path
2
class="enemy-health-bar-fill"
3
d="M 50, 300a 250,250 0 1,0 500,0a 250,250 0 1,0 -500,0"
4
fill="transparent"
5
stroke-width="70"
6
stroke="\#c54230"
7
data-bind-style-stroke-dashoffset="2600 - {{enemy.health}}"
8
/>

As you can see, we are using stroke-dashoffset to create the effect of a radial bar and we are doing a calculation of 2600 (the stroke-dasharray) - the health, which will go from 1500 to 0 to create the desired effect.

Now we have to hook up everything to our game to see it in action. For the game we’ll be using the one made in this tutorial: https://www.youtube.com/watch?v=ROMWCMmNBHE by Unreal Engine Tutorials without the UI from the tutorial and with some minor modifications to fit our use case.

For this tutorial we are using Unreal Engine 4.27 with Gameface 1.30

The first thing we’ll add to our game is our UI. To start we’ll create a folder called ‘multiple-nameplates’ in GAME_ROOT_FOLDER\Content\uiresources and copy our html, css and javascript files to there. Then we’ll open our HUD blueprint and change the coui: path to match our newly created folder

Now if we load our game we’ll see the Player nameplate on the screen.

Next we have to create the structs that will be our models. We’ll be using two models, one for the Player and one for all of the enemies - PlayerModel and EnemyModel respectively.

To create them, we’ll just make the following structs in our Content Browser:

  1. PowerMove struct. This struct will allow us to create a bar with segments that will show the number of power moves that our character can use. The reason that we are creating a struct is so that we are able to use it as an array in our PlayerModel

    Here the default value of the ‘filled’ property will be true.

  2. Next we need to make the Player struct:

  3. Then the Enemy struct which will be for a single enemy:

  4. And finally the EnemiesArray which will be an array of Enemy structs that we’ll be using as our EnemyModel:

Now that we have all of our structs ready, we can start adding them to our HUD blueprint. To start we’ll just create three variables for the EnemiesArray (the array of Enemy structs), Enemy (which will be the EnemiesArray struct) and Player which we will use to create our models. Next we need to add the Ready for Bindings event so that we are able to register the models. To do that in the blueprint we right click on an empty space and find ‘Get Gameface HUD

From there we pull the pin and find ‘Bind Event to Ready for Bindings’

And then we can pull the red event pin from there and create our event. In our event listener we can now create our PlayerModel which will be responsible for the Player and our EnemyModel which will hold all of our Enemy nameplates.

After we create our models we need to synchronize them (like in the screenshot above), so that their values get updated in the frontend.

Next we need to start updating the values of the Player struct so that our character can have dynamic health and mana, gain experience, and use power moves. We will start with creating our variables in the ThirdPersonCharacter blueprint. We’ll need the following items:

With the following default values:

Where PlayerModel is our struct that we’ll pass to our HUD, Model Player Power Moves is the array of power moves, powerMoveCounter will tell us how many power moves we have left and NextLevelExperience will tell us how much experience points we need to get to the next level.

The first thing we are going to set in our model is health. We’ll add it to our struct in the Event AnyDamage listener, right after setting it

This however won’t affect the model so we can add an event dispatcher to dispatch a PlayerUpdate event that will update it when we need it to. And we’ll pass our struct to it so that we can use it to update the values in the frontend.

Next we can make the left key deplete our Mana. Before that however we need to do a quick check if the Mana is above 45 to be able to fire. That way we won’t actually have it drop below 0.

Then we can deplete the Mana by 45 after firing the projectile. To make things more interesting, we can also set the damage the projectile deals to correspond to our Player level, which will make it so that the higher the level, the more damage that we do:

And finally we update our struct again and call the PlayerUpdate event:

After depleting our Mana we have to refill it. We can do that by adding a check for each Tick to see if the Mana is below 565 (and update it by 1 if it is). Since however this will be too fast, we can set the Tick Interval in our Class Defaults to be 0.1

Now each time we fire and deplete the Mana it will start to refill on its own.

The last thing we can do for now is to update the power moves and power move timeout. We can do this the same way we did it with the Mana, we will check if the timeout is 100 and if there are any power moves left

Then we’ll set the counter to be less by one, set the timeout to 0, and set the corresponding filled property to be false.

And just like with the Mana we can also set the power move timeout to update in the Tick

Now we can get to our Enemies. But first we need to add an empty actor to our map, which we’ll call EventBus. This will allow us to dispatch events to our HUD from our Enemies without having to bind an event listener to each individual enemy. Inside we’ll add the EnemyDeath event dispatcher.

In the BP_EnemyBase we’ll again add variables and their default values:



The first thing we can do is to add the EventBus and the PlayerCharacter as variables so we don’t have to get the empty actor or the Player each time we need to dispatch an event or update a value.

Next we can add a new static mesh to our CapsuleComponent that will indicate the position of the nameplate (since if we use the character position it will place it in the model feet). Once it’s added we can move it to the top of the capsule and then remove its Static Mesh.

Then in the Event Graph we can get its position and convert it to screen coordinates. We’ll do it on every tick so that the nameplates can have an up-to-date position at any time.

Here we do a couple of things:

  • Get the location of the Nameplate Origin static mesh that we created and convert it to ScreenLocation;
  • Get the viewport size and check if the x and y coordinates that we got are within the screen. If they are, we set the isOnScreen boolean to true and to false if they are not. This will let us hide the nameplate if it’s off screen so that we can have only the visible elements in the frontend
  • After that we’ll use the PlayerCharacter variable that we saved to get the distance between the Player and Enemy - that way we can scale the nameplate to be smaller the further away the Enemy gets.
    We also check if the distance is greater than 4000 so we can hide the nameplates of enemies that are very far away.
  • Finally we save all of our data to our Enemy struct

Something that can be noticed here is the absence of an Enemy Update event. Unlike in the ThirdPersonCharacter blueprint, we will update the EnemyModel on each tick in order to have the nameplates positioned correctly at all times.

We can now set the Enemy Health in our struct, but before that, we can enrich it by adding d a damage marker to our enemy nameplates with the following code in the index.js file

1
const damageTemplate = document.createElement('div');
2
damageTemplate.classList.add('damage-marker');
3
4
engine.whenReady.then(() => {
5
engine.on('show-damage', (damage, index) => {
6
const newDamage = damageTemplate.cloneNode();
7
newDamage.textContent = damage;
8
document.querySelector('.enemy-nameplate-${index}').appendChild(newDamage);
9
});
10
});
11
12
function animationEnd(event) {
13
event.target.parentNode.removeChild(event.target);
14
}

In the html we can add an onanimationend event and data-bind-class to the enemy-nameplate-container so we are able to select the correct nameplate to attach the marker to:

1
<div
2
class="enemy-nameplate-container"
3
data-bind-for="index, enemy:{{EnemyModel.enemies}}"
4
data-bind-class="'enemy-nameplate-' \+ {{index}}"
5
data-bind-style-transform2d="{{enemy.transform}}"
6
onanimationend="animationEnd(event)"
7
>

Now whenever the damage marker animation is completed, the animationEnd function will be called and will remove it from the DOM.

Then in Unreal we can create a JS event and trigger it, once an enemy receives damage

And we’ll pass the damage it receives and the index of the enemy so that we know which nameplate has to show the marker.

The last thing that needs to be in our game is to set the level of the Player and gain experience whenever an enemy dies. In the future if we decide to have different enemies, each should be worth a different amount of experience points. This is why we need to create a function called UpdateExperienceAnd Level in our ThirdPersonCharacter blueprint and call it from the BP_EnemyBase. That way we can pass the experience of each individual enemy.

Here we need to check if the Player’s current experience (with the added gained experience) is less than the necessary to pass to the next level. If it is, we just set it as a percent and update it to our struct.If it’s not, we subtract the necessary experience for the next level from the total experience, set the result as the current experience, and update the level by 1.

Since we want to increase the necessary experience for the next level each time we reach a certain level we’ll create a macro that will update it based on the Player level and we can call it every time the Player reaches a new level.


Now in our BP_EnemyBase blueprint we can set Update the Player experience when an enemy dies

Then we can dispatch the EnemyDeath event from the EventBus and pass the index

With our Player and Enemies ready we can now set up our HUD to update the model whenever the values change using the events we created.

The first event that we’ll listen for is the PlayerReady that dispatches when the initial model animation finishes. When that happens we can spawn our enemies in the map and add their structs to the EnemiesModel.

Here we get the position of the ThirdPersonCharacter and spawn the enemies in a random point in a radius around it. Then we get the Enemy struct from the newly spawned actor and add it to the array. We also add the actors to an array so we don’t have to get them each tick with Get Actors of Class

Next we have to add the PlayerUpdate event listener

Here we simply set the Player struct and update the model.

The last thing we have to listen for is the Enemy death:


We remove the enemy from the EnemyModel once it’s dead and update the indexes of the remaining enemies.

And finally we need to set the EnemyModel to update on each tick with the correct values:

The only thing that we do differently here than the PlayerUpdate is that we add the isReadyForBindings node so that we don’t try to update the model before it has been created.

Voilà! You’ve now seen how to add dynamic elements to your UI. There are plenty of additional features you can add to the nameplates and experiment with. Do let us know if you indeed do so.