Creating an interface for customizing character appearance

ui tutorials

7/16/2024

Kaloyan Geshev

If you’ve already reviewed the tutorial on displaying 3d objects in the UI you’re familiar with the Live Views feature. This feature allows rendering dynamic textures from the engine directly into the UI.

Leveraging this powerful feature, this tutorial will guide you through creating a dedicated screen for changing a character’s appearance directly from the UI in the game.

Prerequisites

For this tutorial we’ll be extending a sample that we have, called Parallax UI which utilizes React 18 with React Router 6. Apart from that we’ll be using our component scrollable container and the Interaction Manager library to help with the keyboard interactions. For the 3D models we’ll use the free Stylized Character Kit assets.

Getting started

For this project we are going to use Unreal Engine 5.3 and with blueprints to update the game based on the UI interactions.

We’ll be using an existing project with Gameface integrated, but you can also do it for a blank project, which can be set up by following the documentation .

Creating the character

First, we’ll create a Blueprint Class called CharacterBP to contain our 3D character model. Inside we’ll add a SceneCaptureComponent2D for the camera that renders onto our UI, along with an Arrow component that will hold the character’s mesh.

The arrow will be used later for implementing logic to rotate the character.

Adding character meshes

After completing the previous step, it’s time to add the character meshes to our blueprint:

  • After adding the Stylized Character Kit to your project, access its models by navigating to All/Content/SCK_Casual01/Blueprints in the Content Browser and opening the ThirdPersonCharacter blueprint. Copy the Mesh (CharacterMesh0) from there.

  • Open the CharacterBP created earlier and paste the mesh inside the Arrow.

  • Configure the SkeletalMesh:
    • Change the Skeletal Mesh Asset in the Details menu to one of the kit meshes, e.g MESH_PC_03.
    • Set the Animation Mode in the Details menu to Use Animation Blueprint.
    • Set the Anim Class in the Details menu to ThirdPerson_AnimBP_C to apply animations to the mesh.
    • Set the visibility of the SkeletalMesh to false.

This sets the default mesh for the character and enables animations via the ThirdPerson_AnimBP_C class.

  • Create new SkeletalMesh for each character parts that you want to enable for customization inside the SkeletalMesh created in the previous steps. For example create the Chest, Head, Legs, Hands and Face.

  • Setup the new meshes. Change the Skeletal Mesh Asset, Animation mode and Animation Class for each part as done previously for the SkeletalMesh. For example, for the chest:
    • Change the Skeletal Mesh Asset to one of the chest meshes, e.g., MESH_T_01.
    • Set the Animation Mode to Use Animation Blueprint.
    • Set the Anim Class to ThirdPerson_AnimBP_C.

Setting up the live view

In our case it’s pretty straightforward, we’ll follow the guide in our documentation . Since we’ll be showing the character in our UI, we’ll need to follow the second part of the tutorial in the documentation for Transparent Live Views .

Ensure you place the CharacterBP onto the level map. Without this, nothing will render in our RenderTarget!

Defining the changeable character body parts

In the CharacterBP blueprint, set arrays with options for changing the character’s body parts. We’ll demonstrate this for the chest options, but the process is the same for other parts.

  • Create a new variable ChestOptions of type Array of Skeletal Mesh Object References.

  • Set the default value of Chest Options to include all customizable chests. In our example, the Stylized Character Kit provides 3 chests, so add 3 items and set their values to the corresponding chest meshes.

  • Repeat these steps for the face and legs to enable customization for these parts.

Create methods for changing body parts

We will create a method to change the current mesh for the chest. The steps are similar for other body parts.

To do that create a new function inside the CharacterBP called ChangeChest with following definition:

The method takes the new index for the chest from the previously defined array and updates it directly in the Chest mesh.

Implementing Body Part Changes

To change a character’s body part, we will subscribe to a custom event from the frontend, which will call the methods we created for changing a body part. We will follow the UI Scripting with C++ guide to define the custom event and make it usable in our blueprints. This event will have two arguments:

  • customizationType - String. Specifies the type of customization requested, either Chest, Legs or Face.
  • index - Number. The index of the item to use for the character’s appearance, defined in the ChestOptions, FaceOptions, or LegsOptions arrays from the previous steps.

To enable this, we will create a new C++ class extending the CohtmlGameHUD class defined by the Gameface Plugin.

MyCohtmlGameHUD.h
1
#pragma once
2
3
#include "CoreMinimal.h"
4
#include "CohtmlGameHUD.h"
5
#include "MyCohtmlGameHUD.generated.h"
6
7
UCLASS()
8
class AMyCohtmlGameHUD : public ACohtmlGameHUD
9
{
10
GENERATED_BODY()
11
12
public:
13
virtual void BeginPlay() override;
14
UFUNCTION(BlueprintImplementableEvent, BlueprintCallable, Category = Gameplay)
15
void CustomizeCharacterEvent(const FString& customizationType, const int index);
16
private:
17
UFUNCTION()
18
void BindUI();
19
};

Here we define the handler that will be executed when the JavaScript event CustomizeCharacterEvent is triggered.

The implementation is as follows:

MyCohtmlGameHUD.cpp
1
#include "MyCohtmlGameHUD.h"
2
3
void AMyCohtmlGameHUD::BeginPlay()
4
{
5
Super::BeginPlay();
6
CohtmlHUD->ReadyForBindings.AddDynamic(this, &AMyCohtmlGameHUD::BindUI);
7
}
8
void AMyCohtmlGameHUD::BindUI()
9
{
10
CohtmlHUD->GetView()->RegisterForEvent("customizeCharacter", cohtml::MakeHandler(this, &AMyCohtmlGameHUD::CustomizeCharacterEvent));
11
}

In the BindUI method, we register the customizeCharacter event, which will be triggered from the frontend.

Once the custom event is set up in C++, it can be accessed in the coherentBP blueprint. This allows us to use the event to change the character’s body part by setting up the following blueprint:

By using the Customization Type argument, we can determine which character mesh should be replaced with a new one selected from the array by the given Index.

Preserving the active index of the selected body part

To allow the Frontend to know which item is selected for the Chest, Face, and Legs, we need to create a binding model that will be accessible in the UI. This will enable us to retrieve this information.

To achieve this, we first need to define a structure called CharacterSelectedParts that will hold the active index for each customizable character part.

For instance, if the chest with index 1 is currently selected, the ChestIndex should be set to 1.

Once the structure is created, we need to create a binding model from it.

To do this, we must wait for the ReadyForBindings event and then create the model.

The blueprint below illustrates this process.

Update the active body parts index

After defining the struct for the active body part indexes, we need to update the model with the relevant active indexes whenever a character’s appearance changes. This involves enhancing the blueprint we’ve defined for changing the character’s appearance.

Here, the Character variable refers to the CharacterSelectedParts struct.

Creating the Frontend

In our Parallax UI project, we’ll create a new page called Customize Character and set it up to match the rest of the UI (we won’t delve into the styling details here). This page will have two main elements: one for the character customization menu with items for Face, Chest, and Pants, and another for displaying the character preview.

Building the Character Customization Menu and Preview

To create the customization menu, we’ll define the menu items directly in the React component. Note that using our binding system is more efficient, but for simplicity, we’ll define the items directly here. Normally, you’d define the items model on the backend in Unreal and access it via data binding attributes.

EditCharacter.jsx
1
const [selectedMenu, setSelectedMenu] = useState(-1);
2
const menus = [
3
{
4
title: 'Face',
5
items: [
6
'./images/edit-character/face_0.png',
7
...
8
],
9
},
10
{
11
title: 'Chest',
12
items: [
13
'./images/edit-character/chest_0.png',
14
...
15
],
16
},
17
{
18
title: 'Legs',
19
items: [
20
'./images/edit-character/legs_0.png',
21
...
22
],
23
},
24
];

We will have a state variable for the selected menu, used to toggle between customization menus, and a menus variable holding data for each menu.

To map the state and data, we render the following template:

EditCharacter.jsx
1
<div className="edit-character">
2
<div className="edit-character-column edit-character-column-left">
3
<div className="title edit-character-info">
4
Customize character
5
</div>
6
<Divider />
7
<div className="edit-character-item-container">
8
<gameface-scrollable-container
9
class="scrollable-container-component"
10
automatic
11
>
12
<component-slot data-name="scrollable-content">
13
{menus.map((menu, index) => {
14
return (<div key={index} onClick={(event) => { selectMenu(event, index); }} className={`edit-character-item`}>
15
<div className="edit-character-item-title">{menu.title}</div>
16
<div className={`edit-character-item-items ${selectedMenu === index ? '' : 'edit-character-item-items-hidden'}`}>
17
{menu.items.map((itemImage, itemIndex) => {
18
return (
19
<div
20
key={index + '_' + itemIndex}
21
onClick={() => { engine.trigger(`customizeCharacter`, menu.title, itemIndex) }}
22
style={{ backgroundImage: `url(${itemImage})` }}
23
className={`edit-character-item-image`}
24
data-bind-class-toggle={`edit-character-item-image-selected:{{CharacterSelectedParts.${menu.title}Index}} === ${itemIndex}`}
25
>
26
</div>
27
)
28
})}</div>
29
</div>)
30
})}
31
</component-slot>
32
</gameface-scrollable-container>
33
</div>
34
</div>
35
<img
36
src="/Script/Engine.TextureRenderTarget2D'/Game/CharacterLiveViewRT.CharacterLiveViewRT'"
37
className="edit-character-item-preview"
38
/>
39
</div>

Key Points to Note:

  1. Toggling Menus: We toggle the menus by changing the selected menu index.
1
<div key={index} onClick={(event) => { selectMenu(event, index); }} className={`edit-character-item`}>
1
const selectMenu = (event, menuIndex) => {
2
if (event.target.classList.contains('edit-character-item-items') ||
3
event.target.classList.contains('edit-character-item-image')) return;
4
5
if (selectedMenu === menuIndex) {
6
setSelectedMenu(-1);
7
return;
8
}
9
10
setSelectedMenu(menuIndex);
11
}

The selectMenu method changes the selected menu index, closes the current menu if it’s selected and you click on it, and prevents toggling if you click on any child element.

  1. Active Class Toggle: We use the data-bind-class-toggle attribute to set the active class to the item currently selected on the character. This utilizes the CharacterSelectedParts model created earlier.
  2. Triggering Customization Events: When an item is clicked, we send the customizeCharacter event to the engine.
1
onClick={() => { engine.trigger(`customizeCharacter`, menu.title, itemIndex) }}
  1. Character Preview: We display the character preview using the live view we created.
1
<img
2
src="/Script/Engine.TextureRenderTarget2D'/Game/CharacterLiveViewRT.CharacterLiveViewRT'"
3
className="edit-character-item-preview"
4
/>

Bonus - Rotating the character and focusing focusing on specific parts for customization

Adjusting the camera position for character customization

To adjust the camera position, we need to register a binding event in JavaScript that will trigger the camera repositioning. For example, we can register an event to focus on the character’s chest.

First, cast the SceneCaptureComponent2D of the CharacterBP blueprint to CohtmlGameHUD and inform the HUD that it is ready for bindings.

Next, register a binding event that is triggered by JavaScript. Let’s register an event called focusChest.

Now, we need to change the camera’s position. Use the Set Relative Location method for the SceneCaptureComponent2D to do this.

This will set the camera position to (0,0,0). However, a smooth transition from the current camera position to the new one is preferable.

To achieve this, define a method in CharacterBP called getCameraPosition that retrieves the current camera position, returning the x, y, and z coordinates.

Then, create a smooth transition between the old and new camera positions. Use a timeline to interpolate each camera axis value from the old to the new one.

In this example, we interpolate the X and Z values of the current camera position to 130 for the X and 25 for the Z. You can adjust these values to suit your needs and achieve the best visual result.

Additionally, stop any timelines currently running for the camera. This prevents animation clashes when focusing on different parts of the character, such as the chest and legs simultaneously. Create a method to stop timelines for changing the camera position.

Here, Timeline Face, Timeline Legs, Timeline Chest, and Timeline Initial are references to the timelines that need to be stopped.

Summarizing all these steps, the blueprint for CharacterBP will look like this:

The blueprints for focusing on the face, legs, and initial camera position are:

After completing the engine setup, trigger these binding events from the frontend.

First, modify the menu’s data structure by adding a focusCallback property:

EditCharacter.jsx
1
const menus = [
2
{
3
title: 'Face',
4
items: [...],
5
focusCallback: () => engine.trigger("focusFace"),
6
},
7
...
8
];

After making these changes, call the focusCallback of the menu when it is selected:

EditCharacter.jsx
1
const selectMenu = (event, menuIndex) => {
2
...
3
if (selectedMenu === menuIndex) {
4
setSelectedMenu(-1);
5
engine.trigger("initialFocus");
6
return;
7
}
8
9
setSelectedMenu(menuIndex);
10
menus[menuIndex].focusCallback();
11
}

Following these steps will result in this:

Rotating the character

In the tutorial about the 3D objects in the UI, we’ve set up a method for rotating our character.

We will enhance this rotation by adding functionality to rotate the character using both the mouse and the keyboard.

First, we need to listen for rotate left/right events in our backend. This is configured in the CharacterBP as follows:

Once configured, you can trigger these events through keyboard actions or when the mouse is dragged left or right.

Rotate character with the keyboard

For keyboard rotation, refer to the setup in this tutorial.

Simply replace the backend-triggered events—such as engine.trigger("rotateCameraLeft"); with engine.trigger("characterRotateLeft"); and do the same for right rotation.

Rotate character with the mouse

To enable character rotation with the mouse, follow these steps:

  1. Add a mouse down event to the live view element.
  2. Add a mousemove event to the document that triggers characterRotateLeft and characterRotateRight events to the backend.
  3. Finally, clear the events.

The handlers definitions for dragging starts, stops, and continues are the following:

1
let isDragging = false;
2
let initialMouseX = 0;
3
4
const startDragging = (event) => {
5
if (isDragging) return;
6
isDragging = true;
7
initialMouseX = event.clientX;
8
document.addEventListener('mousemove', dragModel);
9
document.addEventListener('mouseup', stopDragging);
10
}
11
12
const dragModel = (event) => {
13
if (event.clientX < initialMouseX) {
14
engine.trigger("characterRotateLeft");
15
initialMouseX = event.clientX;
16
return;
17
}
18
19
if (event.clientX > initialMouseX) {
20
engine.trigger("characterRotateRight");
21
initialMouseX = event.clientX;
22
}
23
24
}
25
26
const stopDragging = (event) => {
27
document.removeEventListener('mousemove', dragModel);
28
document.removeEventListener('mouseup', stopDragging);
29
isDragging = false;
30
}

To determine if the mouse is being dragged left or right, use the event.clientX property and compare it to the initial position. Trigger the appropriate rotate event to the backend and update the initial position with the current one: initialMouseX = event.clientX;.

And finally to enable dragging on the live view element, add the event in the useEffect method:

1
const model = useRef(null);
2
...
3
useEffect(() => {
4
...
5
model.current.addEventListener('mousedown', startDragging);
6
return () => {
7
model.current && model.current.removeEventListener('mousedown', startDragging);
8
...
9
};
10
}, []);

On this page