3D Map with UI markers - Adding interactions (part 2)

ui tutorials

2/13/2025

Martin Bozhilov

Overview

This is the second and final part of the “3D Map with UI markers” series. In the previous part, we laid out the project structure and implemented markers display.

We are going to implement a tooltip that appears when a point of interest is hovered over by the mouse. The tooltip will display the name of the POI and its description. Additionally, we will enhance the visuals of our UI by adjusting the size of the UI elements depending on the zoom level by the player.

Showing tooltip on hover

The approach we took for implementing this feature was to use Collision boxes that will detect when the cursor is over them.

Create collision box for every POI actor

We are going to create a Set POI Collision method which will spawn collision boxes when the level initializes to avoid doing it manually for every POI we have.

In this method, we are going to:

  • Loop through all POI actors and add box collisions to them
  • Calculate the relative scale of the boxes based on their distance from the player camera
  • Enable the input and set overlap events to true for each collision box
  • Add each collision box to the POI Collision Box array variable for later use
  • After the loop is finished, enable mouse over events from the controller class

The helper method Set Collision Box Scale will be used to calculate the scale of the collision box relative to the object’s distance to the camera.

We will need the Z position of each POI as an input and achieve our desired result by the following formula:

  • Camera Z location + current spring arm length = Camera location.
  • Distance to Actor = Camera Location - Actor Z location.
  • After we have the distance to the actor, we will scale it down by multiplying it by 0.001.

Additionally, you can fine-tune this by clamping the final result or lowering the distance to actor value - in our case, we have done both. One tip here is to use the Set Hidden in Game node in the Set POI Collision method to visualize the bounds of the collision box, like so:

Adding hover events

After setting up the collision boxes, we need to make them listen for mouse input. We will achieve this by binding our POI actors to the On Begin Cursor Over and On End Cursor Over events.

Before doing that, we need to go back to the content browser and create a POI_Blueprint actor class and replace all instances of our points of interest with it. This allows us to add an index class variable to help provide the correct information to the tooltip.

Then in the blueprint editor of POI_Blueprint create an index integer variable.

We can now proceed with the main part of this section - binding the events. We are going to bind the events on Event Begin Play. The bound events will be responsible for toggling the visibility of the tooltip, while the up-to-date info will be handled on every tick - only if the tooltip is visible to optimize performance.

Before adding the blueprint nodes for the logic, we have to create the struct for the tooltip, a variable to represent the structure, and another variable to hold the hovered actor so its information can be extracted and passed to the tooltip.

Create a structure called tooltipState with the following fields:

And also create the variables:

With that out of the way, we now have to define 2 methods - one for toggling the visibility of the tooltip and another to update its state (name, description, location).

Let’s begin by creating and implementing Update Tooltip Visibility first:

Now, let’s set up the logic on Event Begin Play in the GameState BP:

  1. We will create the tooltip data-bind model.
  2. Then loop through all the actors and call their blueprint class where we will set the correct index for each element.
  3. For every actor, we will call On Begin Cursor Over and in the event body, we will store the active actor POI and call Update Tooltip Visibility with the visibility boolean set to true.
  4. On End Cursor Over we will only call Update Tooltip Visibility with the visibility boolean set to false.

With that out of the way, we now need to implement the Update Tooltip State method. The logic for this method will be the same, with the only difference being that we will update the other fields except for visibility.

In order to pass the correct information only when needed, on Event Tick we are going to check if the tooltip is visible and if it is, we will get the correct POI’s information by providing the POIs array with the index of the Active POI. Then, we will pass the correct information to the Update Tooltip State method we just defined.

The only thing left to make this all work is to connect it to the Frontend. Let’s add the tooltip element and connect it to Unreal with data-binding

index.html
1
<div
2
class="poi-tooltip"
3
data-bind-class-toggle="visible:{{TooltipState.visible}}"
4
data-bind-style-left="{{TooltipState.coordinates.X}}"
5
data-bind-style-top="{{TooltipState.coordinates.Y}}">
6
<div class="poi-tooltip-name" data-bind-value="{{TooltipState.name}}"></div>
7
<div class="poi-tooltip-description" data-bind-value="{{TooltipState.description}}"></div>
8
</div>

What is left is to add some styling and the utility class visible and our tooltip is ready!

styles.css
1
.poi-tooltip {
2
position: absolute;
3
transform: translate(-50%, 50%);
4
color: white;
5
text-transform: capitalize;
6
text-align: center;
7
z-index: 99;
8
opacity: 0;
9
flex-direction: column;
10
justify-content: center;
11
align-items: center;
12
background-image: url(./pois/tooltipBG.png);
13
background-position: center;
14
background-size: 100% 100%;
15
padding: 1.5vmax 0 0.5vmax;
16
width: 25vmax;
17
transition: opacity 0.1 linear;
18
}
19
20
.poi-tooltip-name {
21
font-size: 1.5vmax;
22
padding-bottom: 0.5vmax;
23
margin-bottom: 0.5vmax;
24
border-bottom: 2px solid #FFD700;
25
}
26
27
.poi-tooltip-description {
28
font-size: 0.75vmax;
29
}
30
31
.visible {
32
opacity: .75;
33
}

Changing UI scale based on zoom level

To enhance the immersion of our UI, we will make it scale based on the current zoom level.

  • On maximum zoom, the area markers will fade out, and the POI markers will scale a bit.
  • On medium zoom, the area markers will show, and the POI markers will scale down.
  • On minimum zoom, the area markers will scale up.

Adjusting the size of area markers with custom events

We are going to put the logic for this functionality in our TopDownCamera pawn actor, where the camera that shows the UI is located and where the zoom logic is handled.

We must first get a reference to our GameState to manipulate its variables.

Then let’s create a method called Adjust UI Scale and connect it to the Event Tick event after our zoom method

Now let’s implement the Adjust UI Scale method. The logic for it will be relatively simple. In our zoom logic, we have set the SpringArm’s max zoom-out value to be 20000. So in this method, we will check how much the camera is zoomed and send appropriate events to the UI.

  • If the zoom level is below 5000, we will set the Zoomed field in our MapModel structure to true, which will tell the UI to scale the POIs, and we’ll also send the zoomedInMax event to the UI.
  • If the zoom level is between 5000 and 10000, we will set the Zoomed field to false, scaling down the POI marks, and we’ll send the zoomedIn custom event to the UI.
  • If the zoom level is above 10000, we will only send a zoomedOut event to the UI.

Note: Before we initialize any logic, we must first check if the binding are ready, otherwise when trying to update the model, we’ll get an error.

Now let’s head back to our UI and implement the custom events.

Implementing the custom events in the UI

Create a javascript file - index.js and link it to your HTML

index.html
1
<script src="./cohtml.js"></script>
2
<script src="./index.js"></script>
3
</body>

Then in the index.js we will get all of our markers and we will listen for our custom events with the engine.on() method.

The whole logic will be controlled by toggling different classes based on the event sent.

index.js
1
const areaMarkers = document.querySelectorAll('.area-marker')
2
3
engine.on('zoomedOut', () => {
4
areaMarkers.forEach((areaMarker) => {
5
areaMarker.classList.remove('mark-hidden')
6
areaMarker.classList.add('mark-small')
7
})
8
})
9
10
engine.on('zoomedIn', () => {
11
areaMarkers.forEach((areaMarker) => {
12
areaMarker.classList.remove('mark-hidden')
13
areaMarker.classList.remove('mark-small')
14
})
15
})
16
17
engine.on('zoomedInMax', () => {
18
areaMarkers.forEach((areaMarker) => {
19
areaMarker.classList.add('mark-hidden')
20
})
21
})

Now let’s also add the appropriate utility CSS classes to make it all happen.

  • mark-small will shrink the area marks text.
  • mark-hidden will set the opacity of the area mark to 0 - essentially fading out the text.
styles.css
1
.mark-small {
2
font-size: 1.5vmax;
3
}
4
5
.mark-hidden {
6
opacity: 0;
7
}

Let’s also add a utility class for our points of interest that we will toggle based on the zoomed field in the model.

styles.css
1
.poi-small {
2
width: 2.25vmax;
3
height: 2.25vmax;
4
}

And another one for the content of the POIs which we will use to straighten the POI when the player zooms in on it.

styles.css
1
.poi-content {
2
// Existing styles
3
transition: transform 0.1s linear;
4
}
5
6
.poi-content-zoomed {
7
transform: rotateX(0) rotateZ(-45deg);
8
}

Finally connect it to the elements with the data-bind-class-toggle attribute

index.html
1
<div class="poi-container" data-bind-for="poi:{{MapModel.Pois}}">
2
<div class="poi"
3
data-bind-class-toggle="poi-small:{{MapModel.zoomed}} === false"
4
data-bind-style-left="{{poi.coordinates.X}}"
5
data-bind-style-top="{{poi.coordinates.Y}}">
6
<div class="poi-content" data-bind-class-toggle="poi-content-zoomed:{{MapModel.zoomed}}">
7
<div class="poi-image" data-bind-style-background-image-url="'./pois/' + {{poi.name}} + '.png'"></div>
8
</div>
9
</div>
10
</div>

And with that our frontend is complete! Now if you save everything and play your level your UI should react to the zoom level of your camera!

Adjusting collision boxes with zoom level

One final thing we can implement is to dynamically change the size of the collision boxes on our POIs that control the tooltip. Adding this will be very simple since we are going to reuse our Set Collision Box Scale method that we used when creating the boxes.

In your player pawn blueprint on Event Tick after the Adjust UI Scale method, connect a new method called Adjust Collision Box Scale where we will implement this logic.

Let’s proceed by setting up the Adjust Collision Box Scale method. We will loop through all the collision boxes, call the Set Collision Box Scale method, and set the new scale with the Set Relative Scale 3D method.

And with that, we conclude our UI! Feel free to expand on it and create a stunning 3D map for your game!

On this page