Recreating the separating texts in Forspoken

ui tutorials

11/7/2024

Mihail Todorov

In Forspoken, dealing damage to enemies triggers a unique separating text effect that adds flair to combat feedback. This stylistic touch enhances the game’s dynamic feel. In this article, you’ll learn how to replicate this visually striking effect for your own game, bringing a touch of magic to your combat UI.

Overview

In this tutorial we’ll be showing you how to recreate the separating effect that happens on the damage texts in the game Forspoken .

To see the sample live you can find the whole project in the ${Gameface package}/Samples/uiresources/UITutorials/SeparatingTexts directory.

Getting started

To get started we’ll create two files: index.html and index.js and link them together.

To make this example look more like a game, we’ll also include an image for the background and use the DCC ASH Font for the texts.

Styling our sample

First of we’ll begin by styling our components. We’ll start by adding the font and the image to the background in a style tag in our index.html:

1
@font-face {
2
font-family: "DCC-Ash";
3
src: url(./DCC-Ash.otf);
4
}
5
6
body {
7
width: 100vw;
8
height: 100vh;
9
margin: 0;
10
padding: 0;
11
font-size: 5vh;
12
background-image: url(./battle-background.jpg);
13
background-repeat: no-repeat;
14
background-position: center;
15
background-size: cover;
16
font-family: "DCC-Ash";
17
}

Then we’ll style our words. Since we want each letter to move in a random direction, we’ll need to create a separate element for each one. This is why we’ll style both the word and the letters.

1
.separating-word {
2
display: flex;
3
position: absolute;
4
transform: translate(-50%, -50%);
5
text-transform: uppercase;
6
}
7
.moving-letter {
8
transform: translate(0px, 0px);
9
opacity: 1;
10
color: white;
11
transition: opacity 4s, transform 4s, color 2s;
12
}

Each .separating-word we’ll be positioned absolutely on the screen and it’s text will always be in uppercase.

As for each .moving-letter here is where the magic will happen. We’ll use CSS Transforms to create the animations and to do that each letter will need to have a basic styling. In our case that would be to have it’s initial position set, initial opacity and color.

Making the words appear and separate

Adding the words to our page

To add the words to our page we’ll go to our index.js file and make a createSeparatingWord function.

1
function createSeparatingWord(word, color, x, y) {}

To this function we need to pass which is the word, what color it would be and where it should appear on the screen.

The first step would be to create our word element. For this we’ll create a new element, add the separating-word class to it and set it’s position according to the x and y coordinates.

1
function createSeparatingWord(word, color, x, y) {
2
const separatingWord = document.createElement("div");
3
separatingWord.classList.add("separating-word");
4
separatingWord.style.topPX = y;
5
separatingWord.style.leftPX = x;
6
}

Then for each letter we’ll create a new element and set it inside our word element. As we want the letters on the left side to have a text shadow on their left and those on their right, we’ll get the middle of the word and the shadow offset as well. The shadow offset will ensure that the further away the letter is from the middle one, the bigger the shadow is.

1
function createSeparatingWord(word, color, x, y) {
8 collapsed lines
2
const separatingWord = document.createElement("div");
3
separatingWord.classList.add("separating-word");
4
separatingWord.style.topPX = y;
5
separatingWord.style.leftPX = x;
6
7
const wordMiddle = Math.floor(word.length / 2);
8
const shadowOffset = 2 / wordMiddle;
9
10
for (let i = 0; i < word.length; i++) {
11
const letter = word[i];
12
const letterElement = document.createElement("div");
13
letterElement.classList.add("moving-letter");
14
letterElement.textContent = letter;
15
letterElement.style.textShadow = `${
16
shadowOffset * (i - wordMiddle) * -1
17
}px 2px ${color}`;
18
19
separatingWord.appendChild(letterElement);
20
}
21
22
document.body.appendChild(separatingWord);
23
}

Finally we append the word element to the body.

Animating the letters

Next we need to animate the letters and make them move and disappear.

To do that we can set the transform of each letter so it offsets slightly. Here again we’ll be using the middle of the word to make sure that letters to the right and left go in their corresponding direction.

And since we already have a function for creating the word, we can add the necessary styles in there.

1
function createSeparatingWord(word, color, x, y) {
18 collapsed lines
2
const separatingWord = document.createElement("div");
3
separatingWord.classList.add("separating-word");
4
separatingWord.style.topPX = y;
5
separatingWord.style.leftPX = x;
6
7
for (let i = 0; i < word.length; i++) {
8
const letter = word[i];
9
const letterElement = document.createElement("div");
10
letterElement.classList.add("moving-letter");
11
letterElement.textContent = letter;
12
letterElement.style.textShadow = `${
13
shadowOffset * (i - wordMiddle) * -1
14
}px 2px ${color}`;
15
16
separatingWord.appendChild(letterElement);
17
}
18
19
document.body.appendChild(separatingWord);
20
21
window.requestAnimationFrame(() => {
22
window.requestAnimationFrame(() => {
23
separatingWord.children.forEach((letter, i) => {
24
const randomX = Math.round(
25
Math.random() * (i - wordMiddle) * 5
26
);
27
const randomY = Math.round(Math.random() * 10) - 5;
28
letter.style.transform = `translate(${randomX}px, ${randomY}px)`;
29
letter.style.opacity = 0;
30
letter.style.color = color;
31
});
32
});
33
});
34
}

As you can see, here we are wrapping our changes in 2 requestAnimationFrame callbacks. The reason is that, if we change it immediately Gameface won’t have the time to paint the initial element and then transition to the next style, but it would rather jump to the end result immediately. And the reason that we need two callbacks is that due to the way Gameface works, it’s paint is always 1 frame behind.

Removing the letters

Finally we need to remove the letters after they have disappeared.

Since we need to actually wait for them to disappear, we need to add a transitionend event to the word element. And as we already have the element in our function, let’s also do it there.

1
function createSeparatingWord(word, color, x, y) {
32 collapsed lines
2
const separatingWord = document.createElement("div");
3
separatingWord.classList.add("separating-word");
4
separatingWord.style.topPX = y;
5
separatingWord.style.leftPX = x;
6
7
for (let i = 0; i < word.length; i++) {
8
const letter = word[i];
9
const letterElement = document.createElement("div");
10
letterElement.classList.add("moving-letter");
11
letterElement.textContent = letter;
12
letterElement.style.textShadow = `${
13
shadowOffset * (i - wordMiddle) * -1
14
}px 2px ${color}`;
15
16
separatingWord.appendChild(letterElement);
17
}
18
19
document.body.appendChild(separatingWord);
20
21
window.requestAnimationFrame(() => {
22
window.requestAnimationFrame(() => {
23
separatingWord.children.forEach((letter, i) => {
24
const randomX = Math.round(
25
Math.random() * (i - wordMiddle) * 5
26
);
27
const randomY = Math.round(Math.random() * 10) - 5;
28
letter.style.transform = `translate(${randomX}px, ${randomY}px)`;
29
letter.style.opacity = 0;
30
letter.style.color = color;
31
});
32
});
33
});
34
35
36
separatingWord.addEventListener("transitionend", () => {
37
separatingWord.parentNode.removeChild(separatingWord);
38
});
39
}

Linking it our game

To link it to our game, we simply need to do the following

1
engine.whenReady.then(() => {
2
engine.on('damageWord', createSeparatingWord)
3
})

Which will show our word when the game sends the damageWord event.

Mocking it in our sample

But as our sample doesn’t have a game behind it yet, we would need to mock this on the frontend.

To do this, we’ll simply set a timer to show a random word at a random place on the screen and we’ll also add an event listener so we can show the random word on click.

1
const texts = [
2
{
3
text: "vulnerable",
4
color: "red",
5
},
6
{
7
text: "immune",
8
color: "white",
9
},
10
{
11
text: "stunned",
12
color: "yellow",
13
},
14
{
15
text: "poisoned",
16
color: "green",
17
},
18
{
19
text: "frozen",
20
color: "blue",
21
},
22
{
23
text: "burned",
24
color: "orange",
25
},
26
];
27
28
setInterval(() => {
29
const x = Math.floor(Math.random() * window.innerWidth);
30
const y = Math.floor(Math.random() * window.innerHeight);
31
32
const randomWord = texts[Math.floor(Math.random() * texts.length)];
33
engine.trigger('damageWord', randomWord.text, randomWord.color, x, y)
34
}, 500);
35
36
document.addEventListener("click", (event) => {
37
const randomWord = texts[Math.floor(Math.random() * texts.length)];
38
engine.trigger('damageWord', randomWord.text, randomWord.color, event.clientX, event.clientY)
39
});

In Conclusion

The separating text effect from Forspoken is more than just a visual treat—it’s a way to immerse players in the flow of combat. By mastering this technique, you can add an eye-catching and impactful element to your game that players will remember.

On this page