Replicating the shadow menu from LA Noire

ui tutorials

11/21/2024

Mihail Todorov

The main menu of L.A. Noire is iconic, blending minimalist design with atmospheric visuals. Its subtle interplay of shadows and lighting sets the tone for the game’s noir aesthetic. This article will guide you through recreating this captivating shadow menu effect, bringing cinematic flair to your game’s user interface.

Getting Started

To get started we’ll first create our UI separately from the backend. For that we’ll only need one html page and link the cohtml.js file to it.

Styling our Text

The first thing we need to do is to style our text to look like a shadow. To do that we’ll create a h1 tag with our text and style it in the style tag.

1
body {
2
width: 100vw;
3
height: 100vh;
4
margin: 0;
5
display: flex;
6
align-items: center;
7
justify-content: center;
8
}
9
10
h1 {
11
font-size: 45vh;
12
color: black;
13
filter: blur(8px);
14
opacity: 0.9;
15
transform: scaleY(2.2);
16
font-weight: 900;
17
coh-font-fit-max-size: 33vh;
18
coh-font-fit-mode: fit;
19
text-transform: uppercase;
20
position: absolute;
21
}

And we get the following:

You may notice that we are using a custom css property in our code coh-font-fit. This will allow us to fit any word in the screen regardless of it’s length.

Adding the texts

To add all of our texts, we’ll simply just add them as h1 tags again

1
<h1>RESUME</h1>
2
<h1>NEW GAME</h1>
3
<h1>OPTIONS</h1>
4
<h1>QUIT</h1>

Switching between the texts

In order to switch between the texts we need to hide them first. To do that we’ll simply change the opacity from 0.9 to 0

1
h1 {
2
font-size: 45vh;
3
color: black;
4
filter: blur(8px);
5
opacity: 0.9;
6
opacity: 0;
7
transform: scaleY(2.2);
8
font-weight: 900;
9
coh-font-fit-max-size: 33vh;
10
coh-font-fit-mode: fit;
11
text-transform: uppercase;
12
position: absolute;
13
}

Then we simply need to get all of our texts in our JavaScript and set the initial element index.

1
let index = 0;
2
const texts = document.querySelectorAll('h1');

This will allow us to send events from the game to change the index based on the key we’ve pressed and change this index.

1
engine.on('change', (dir) => {
2
3
const prevIndex = index;
4
5
index += dir;
6
if (index < 0) index = texts.length - 1;
7
if (index === texts.length) index = 0;
8
9
texts[prevIndex].style.opacity = 0;
10
texts[index].style.opacity = 0.9;
11
});

Now if we mock the event in our frontend.

1
engine.whenReady.then(() => {
2
engine.trigger('change', 0); // This allows us to set the initial element to be opaque so we can see it.
3
4
});
5
6
document.addEventListener('keydown', (event) => {
7
if (event.keyCode === 40) engine.trigger('change', 1); // Pressing the arrow key down
8
if (event.keyCode === 38) engine.trigger('change', -1); //Pressing the arrow key up
9
})

We can see how the texts change.

Making everything prettier

Although we got a good result it’s not that visually appealing. This is why we’ll add a morphing effect, that will make it look like the different text blend into each other. As if the object that causes the shadow morphs.

To do that we’ll need to create two animations with two classes.

1
.morph-in {
2
animation: morph-in 0.2s forwards;
3
}
4
5
.morph-out {
6
animation: morph-out 1s forwards; /*The morph out animation is longer so that the blending effect is better*/
7
}
8
9
@keyframes morph-in {
10
from {
11
filter: blur(50px);
12
opacity: 0;
13
}
14
15
to {
16
filter: blur(8px);
17
opacity: 0.9;
18
}
19
}
20
21
@keyframes morph-out {
22
from {
23
filter: blur(8px);
24
opacity: 0.9;
25
}
26
27
to {
28
filter: blur(50px);
29
opacity: 0;
30
}
31
}

With that we simply change our logic that changes the opacity so that it now adds and removes the animations.

1
engine.on('change', (dir) => {
2
3
const prevIndex = index;
4
5
index += dir;
6
if (index < 0) index = texts.length - 1;
7
if (index === texts.length) index = 0;
8
9
texts[prevIndex].style.opacity = 0;
10
texts[index].style.opacity = 0.9;
11
texts[prevIndex].classList.add('morph-out');
12
texts[prevIndex].classList.remove('morph-in');
13
texts[index].classList.add('morph-in');
14
texts[index].classList.remove('morph-out');
15
});

Adding this to a game

In the game the menu is shown as a shadow on wall lit by car lights. To emulate a similar effect we can use our In World UI Feature in Unreal Engine.

Setting up the project

Before we start adding it, we need to set up our project in Unreal Engine. We’ll use the getting started guide from our documentation to install the Gameface plugin and integrate it into an existing project or use the sample project created by the installer.

Once that’s complete, create a new blank map in Unreal Engine to start configuring your setup.

Adding our UI to the world

To do that we’ll use our newly created Gameface tab in Unreal and select Add In-World UI.

Which will spawn a CohtmlPlane actor in the world.

This actor allows us to render an HTML page inside the 3D world. To do that we’ll add the page we made to the ${PROJECT_BASE}/Content/uiresources folder and then in the Actor we’ll select Cohtml and set the URL to it which in our case would be coui://${NAME_OF_FILE}.

Finally for a more atmospheric look we’ll add the CohtmlPlane next to a brick wall and light it up using a SpotLight.

If we run the page we’ll get the following

Changing the text from the game

First in our Level Blueprint we’ll get the CohtmlPlane and the SpotLight and set them as variables.

Once we have those, we are able to use the Gameface Component from the plane to trigger the change event we set up on our page. For this example and for simplicity we’ll trigger the event using the Y and H keys on the keyboard, but feel free to use any key combination.

As you can see both are very similar with the only exception that we pass a negative number for the direction for the Y key and a positive for the H key.

Finally we can do so that whenever we press the ENTER key the light will dim.

In conclusion

Recreating the L.A. Noire shadow main menu effect adds a layer of visual storytelling to your game. By combining creativity with technical precision, you can craft a menu that doesn’t just function but leaves a lasting impression on your players.

On this page