Bonus features
Tutorial
·
Beginner
·
+10XP
·
30 mins
·
(11)
Unity Technologies

In this tutorial, you’ll explore extra features you can add to take your game to the next level.
Languages available:
1. Overview
In this tutorial, you’ll find a collection of optional bonus features you can add to your project. This is not only an opportunity to improve and personalize your project, but it’s also a chance to challenge yourself and further develop your Unity skills.
Below is the list of features in their approximate order of difficulty from least technically difficult to the most technically difficult. Each of these ideas is explained in more detail throughout the rest of the tutorial. Feel free to pick and choose which ones interest you!
- Create your own level
- Create your own Post-processing Profile
- Use NavMesh AI to create enemies’ paths
- Add footsteps audio for the player character
- Add a timer
- Create a Start menu
- Add obstacles (keys and locked doors)
You can find examples of these bonus features in the demo version of Haunted House on Unity Play.
2. Create your own level
You’ve been working with pre-made levels to speed up development, but now it’s time to make something that’s truly yours. Creating your own level allows you to personalize the experience and express your creativity as a designer.
To help you get started, we’ve provided a collection of modular rooms, similar to building in games like The Sims. These rooms are designed to snap together and be customized to fit your vision.
You’ll find pre-decorated modular rooms with themed props for each type room, such as the following:
- Kitchen: fridge, oven, pots, kitchen table
- Bathroom: toilet, bathtub, mirror
- Living room: dining table, clock, fireplace
- Bedroom: bed frame, closet, side table
We also included special prefabs for the Start Room and Finish Room, which are used in our provided levels. You can use these as anchors for your custom layout.
If you prefer to build from scratch, you’ll find a variety of empty rooms in different shapes and sizes. These are undecorated and give you full creative control over layout and design.
Each room is fully enclosed by walls, so to connect rooms, you’ll need to manually delete the walls between them. We recommend removing two walls from each room. You can delete more walls (for wider spaces), depending on your design goals.
To polish your room connections, we’ve also included corner-finishing pieces in the form of Wall_Door and Wall_DoorDoubleGap prefabs. These are perfect for adding visual continuity and structure at the open corners where walls were removed, which is especially useful when working with blank rooms or creating new connections between rooms.
Pre-decorated rooms come with predefined connection points: areas intentionally left open to simplify snapping and path flow. In most cases, you’ll only need to delete walls from the adjoining room, and you won’t need to add extra finishing poles.
While it might be tempting to place rooms randomly, this is a great opportunity to apply level design principles. Think about how the player will explore the space: where they start, how they progress, what paths they’ll follow, and whether there are optional rooms or dead ends to explore. If you’re curious to learn more about level design, we recommend you check out Unity's Introduction to game level design (e-book).
1. Start fresh with a new scene:
- Don’t modify the existing scene. Instead, create a new blank scene for your level.
- Add an Empty GameObject and name it “[YourChoice]Level”. This will act as the parent GameObject for all level elements.
- Reset its position to X = 0, Y = 0, and Z = 0.
2. Browse the Room prefabs:
- Navigate to _3DStealthGame > Prefabs > Environment > Rooms.
- Explore the complete room prefabs (like Bathroom, Bedroom, etc.) and the Start/Finish Room prefabs.
- Also, If you want more flexibility, look inside the Rooms Blanks folder for empty rooms of different shapes and sizes.
3. Build your layout using vertex snapping:
- Drag the desired room prefabs into your scene.
- To connect rooms, manually delete the walls where you want the path to be.
Tip: Delete two walls from each room (four total) to create a path. However, if you want, you can delete more to create wider connections.
- Use vertex snapping to align rooms precisely:
- Select the GameObject you want to move, then select the Move tool.
- Hold V to enable vertex snapping.
- Hover over the vertex you want to snap from, then drag it toward another mesh.
4. Add finishing elements to room connections:
- For custom paths (like between empty rooms), add visual polish to the corners using the Wall_Door and Wall_DoorDoubleGap prefabs located in _3DStealthGame > Prefabs > Environment > Walls.
- Place these prefabs at the corners of newly created paths using vertex snapping to avoid abrupt wall endings and give your layout a clean, intentional look.
5. Decorate your rooms:
- Add props and furniture to personalize rooms, even the pre-decorated ones:
- Browse _3DStealthGame > Prefabs > Environment > Decorations for props sorted by room type or as general-use items.
- You can mix and match freely; use what fits your vision!
- Keep your Hierarchy window clean:
- Add all elements as child GameObjects of the Level[YourChoice] GameObject.
- Create empty GameObjects for each room if you want to group elements by space.
6. Add style with materials:
- Change the look of your rooms by applying materials:
- Walls: _3DStealthGame > Art > Materials > Walls
- Floors: _3DStealthGame > Art > Materials > Floors
- Drag and drop the materials directly onto surfaces in your scene.
7. Convert your level into a prefab:
- Once you're happy with your layout, click and drag the [YourChoice]Level GameObject from the Hierarchy window into the Project window inside the _3DStealthGame > Prefabs > Levels folder. This creates a reusable prefab of your entire level.
8. Add core gameplay elements:
- Finally, bring in all the gameplay systems from earlier tutorials:
- UI Document, Player character, Enemies, Audio Sources, EndGame trigger.
Make sure everything is placed and configured properly in your new level.
Once you're done, you’ll have a fully functional haunted level that’s completely your own and designed with care, creativity, and intention.
3. Create your own Post-processing profile
In the previous tutorials you added a pre-configured post-processing profile to enhance the visual mood of your level. In this step, you'll create your own profile or customize a duplicate of the provided one to better match your artistic direction. Post-processing effects are a powerful way to control the visual tone of your game, whether you want it to feel eerie, cinematic, stylized, or surreal.
You have access to a variety of visual effects including the following:
- Ambient Occlusion
- Anti-aliasing
- Auto Exposure
- Bloom
- Chromatic Aberration
- Color Grading
- Deferred Fog
- Depth of Field
- Grain
- Lens Distortion
- Motion Blur
- Screen Space Reflections
- Vignette
Experimenting with different combinations can help establish the atmosphere and emotional tone of your environment. You can also refer to the documentation for Post-processing effects for deeper insight.

1. Create a New Volume Profile:
- In the Project window, navigate to _3DStealthGame > Art > PostProcessing.
- Right-click and select Create > Rendering > Volume Profile to make a new profile from scratch.
- Add the new Volume Profile to the Volume Profile property box of the Global post-processing GameObject in the scene.
- Select the new profile and select Add Override > Post-processing to add effects. Adjust each effect's settings to suit your desired look.
2. Duplicate and customize the existing profile:
If you'd prefer to use the existing profile as a starting point:
- In the Project window, inside _3DStealthGame > Art > PostProcessing select the Scary Volume Profile asset and duplicate it with Ctrl+D (macOS: Cmd+D). Rename the duplicate so it’s easy to identify.
- Add the duplicate to the Volume Profile property box of the Global post-processing GameObject in the scene.
- Select the cloned profile in the Project window to begin customizing it. You can fine-tune existing effects or add new ones.
Use your artistic judgment to strike the right balance; subtle changes can often have a powerful impact on mood and immersion.
4. Use NavMesh AI to create enemies paths
In tutorial 6, you learned how to create enemies that follow a fixed path using waypoints. While this is useful, you might want your enemies to feel less robotic and more dynamic.
In this step, you’ll use Unity’s AI Navigation system to let a ghost enemy wander randomly within a specific room. The NavMesh Surface is an invisible plane baked over walkable areas, which allows GameObjects with a NavMesh Agent component to move intelligently across it.
You’ll write a simple script that makes your ghost randomly pick a point within a certain radius, move there, wait for a short time, and then repeat the cycle. This creates a more complex and unpredictable movement pattern.
1. Bake a NavMesh for your chosen room:
- In the Hierarchy window, use the foldout (triangle) to expand the Level GameObject and select the room where you want the ghost to patrol.
- Add a NavMesh Surface component to the parent room GameObject.
- Open the Agent Type dropdown and select the Open Agent Settings…
- Set the Radius property to 0.5 and the Step Height property to 0.1.
Note: Lowering the Step Height property prevents the NavMesh from being baked over certain GamObjects. However, if your game requires the enemy to move over uneven or bumpy surfaces (like stairs or ramps), setting a low step height might restrict that behavior. In those cases, a better approach is to instead exclude specific GameObjects by adding the NavMesh Obstacle component to them, which gives you direct control over which GameObjects should block navigation.
2. Prepare your ghost for AI navigation:
- Duplicate an existing Ghost GameObject in your scene.
- Rename the duplicate “GhostAI”.
Note: If the ghost has a WayPointPatrol script, remove or disable it.
- Add a NavMesh Agent component to the GhostAI GameObject.
3. Create the AIPatrol script:
- Add a new script to the Ghost GameObject and name it “AIPatrol”.
Open the AIPatrol script and replace the content with the following lines of code:
using UnityEngine;
using UnityEngine.AI;
public class AIPatrol: MonoBehaviour
{
// select the radius in which the enemy can find a new random destination
public float wanderRadius = 3f;
// set the time the enemy can stay there
public float wanderTimer = 1.5f;
private NavMeshAgent agent;
private float timer;
void Start()
{
agent = GetComponent<NavMeshAgent>();
timer = wanderTimer;
}
void Update()
{
timer += Time.deltaTime;
//after the enemy has stayed there for the wanderTime you have defined,
//it will calculate a new destination and go there
if (timer >= wanderTimer)
{
Vector3 newPos = RandomNavPosition(transform.position, wanderRadius, NavMesh.AllAreas);
agent.SetDestination(newPos);
timer = 0;
}
}
// a method where we calculate the new destination point. First, we define a random direction vector
// and set its max magnitude to the wanderRandius
Vector3 RandomNavPosition(Vector3 origin, float dist, int layermask)
{
Vector3 randDirection = Random.insideUnitSphere * dist;
randDirection += origin;
// we check if there is an avaliable point to move inside the navmesh surface
// within the magnitude and direction of the randDirection vector
if (NavMesh.SamplePosition(randDirection, out NavMeshHit navHit, dist, layermask))
{
//If there is an available point under those conditions, it returns that point position
return navHit.position;
}
else
// if there is no point avaliable, stay in the same point
return origin;
}
}
4. Test and fine-tune the behavior:
- In the Inspector window, adjust the Speed and Angular Speed values of the NavMesh Agent component to control movement smoothness.
- Modify the Wander Radius and Wander Timer values in the AIPatrol component to control how far and how often the ghost changes direction.
5. (Optional) Save your ghost as a prefab:
- Drag the GhostAI GameObject into the Prefabs folder in the Project window to create a reusable prefab.
5. Add footsteps audio for the player character
You’ve already implemented diegetic and non-diegetic sounds. Now let’s take it a step further by adding a looping footstep sound for your character while it is walking to enhance the gameplay feel.
1. Add a new Audio Source component to the Player GameObject:
- In the Hierarchy window, select the Player GameObject. Add an Audio Source component and assign the SFXFootstepsLooping to the Audio Resource property.
- Disable the Play On Awake property.
- Enable the Loop property.
Note: Although this is technically a diegetic sound, set the Spatial Blend property to fully 2D (the default setting) to ensure the footstep volume remains consistent for the Audio Listener component.
2. Update the PlayerMovement script:
At the top of the class, declare a new variable:
AudioSource m_AudioSource;- Inside the Start() method, add the following line of code:
m_AudioSource = GetComponent<AudioSource>();- In the FixedUpdate() method, add the following line of code at the end:
if (isWalking)
{
if (!m_AudioSource.isPlaying)
{
m_AudioSource.Play();
}
}
else
{
m_AudioSource.Stop();
}3. Test the new functionality:
- Enter Play mode and test.
- The footsteps should now play when the player character walks and stop when the player character stops.
6. Add a timer
Your game already includes compelling mechanics and obstacles, but players currently have unlimited time to complete the level. Adding a timer introduces a sense of urgency and pressure, enhancing the overall challenge.
In this step, you'll implement a simple timer that shows the player how long they’ve been playing the game.
This method is just one approach; we encourage you to explore other techniques if you want a different style of time-based gameplay.
1. Create the Timer Label in the UI Builder window:
- Open your MainUI document in the UI Builder window.
- From the Library pane, under the Controls section, click and drag a Label element into the Hierarchy pane.
- Rename the new element “TimerLabel”.
- Use the foldout (triangle) to expand the Position section and set the Position Mode property to Absolute.
- Use the foldout (triangle) to expand the Align section and set the Align Self property to Center.
- Customize the label’s font, color, and size to match your desired visual style.
2. Update the GameEnding script:
- At the top of the class, declare three new member variables:
private float m_Demo_GameTimer = 0f;
private bool m_Demo_GameTimerIsTicking = false;
private Label m_Demo_GameTimerLabel;Note: In C#, member variables are a naming convention used to refer to variables that are declared directly within a class but outside of any methods. They belong to the GameObject and exist as long as the GameObject exists. In contrast, local variables are declared inside methods and only exist while the method is running.
- In the Start() method, add the following lines of code:
m_Demo_GameTimerLabel = uiDocument.rootVisualElement.Q<Label>("TimerLabel");
m_Demo_GameTimer = 0.0f;
m_Demo_GameTimerIsTicking = true;
Demo_UpdateTimerLabel();- In the Update() method, add the following lines of code at the beginning:
if (m_Demo_GameTimerIsTicking)
{
m_Demo_GameTimer += Time.deltaTime;
Demo_UpdateTimerLabel();
}- Create the new Demo_UpdateTimerLabel() method:
void Demo_UpdateTimerLabel()
{
m_Demo_GameTimerLabel.text = m_Demo_GameTimer.ToString("0.00");
}This basic setup will display the elapsed time on screen. You can expand it later to create a countdown to trigger the game ending when a time limit is reached.
7. Create a main menu
Another fundamental part of almost any complete game is the main menu, where players can enter the game, access credits, or view the controls. In this project, we’ve provided a simple main menu using a second scene that contains a UI Document with the new menu UI.
If you're curious to explore how it was built, we encourage you to inspect the UI Document and its elements. We also recommend checking out the Getting Started with UI Toolkit course for a deeper explanation of creating a main menu with UI Toolkit.
1. Review the MainMenu scene:
- Navigate to _3DStealthGame > Tutorial_Demo > Demo_Scenes and open the MainMenu scene.
You’ll see that, apart from the Main Camera and Directional Light GameObjects, there's also an EventSystem GameObject. This GameObject is responsible for registering UI input, which works differently from in-game controls.
- The MenuUI GameObject includes two buttons, Start and Quit, and has a script attached called MainMenu.cs that controls the UI buttons behavior.
Note: The Quit button will only work in a local build of your game, not in the Editor.
The script attached to the MenuUI GameObject checks whether the Start button is clicked. If it is, it loads the next scene in the list. In Unity, you usually have different scenes representing levels or areas of your game (for example, a menu scene, a gameplay scene, or a battle scene). These scenes are organized in a specific order to create a natural flow.
2. Fix Aspect ratio in Game view:
If the menu looks off in the Game view, it might be due to the aspect ratio. Up until now, you've been using Free Aspect, but the menu is designed for a specific resolution. To fix this:
- Go to the top of the Game view, click on Free Aspect to open the dropdown, and select QHD (2560 x 1440). You’ll see the menu fits nicely now.
3. Update the scene list and add your own scene:
- Open the Main scene you created during this course.
- Select File > Build Profiles from the main menu.
- Under Scene List, select the Add Open Scenes button.
Make sure your new scene appears after the MainMenu scene. If it doesn’t, drag it into the correct position.
- Now, when you return to the MainMenu scene, select the Play button, then select the Start button; you’ll be taken to your own Main scene!
This same flow will apply when you build your game and run it outside the Editor.
8. Add obstacles (keys and locked doors)
Adding obstacles and puzzles is a great way to increase the depth and engagement of your game. A classic and simple technique is to include collectibles that interact with the environment, such as keys and locked doors.
In this step, we’ve implemented a basic key and door system to illustrate how GameObjects in your game can be connected through logic and player actions. Keep in mind, this is just one possible implementation; you’re encouraged to explore and create your own variations.
Here’s how this system works:
- Each Key GameObject has a string that serves as its unique name. When the player character collides with a key, the key’s name is added to a list of owned keys on the player character’s script.
- Each Door GameObject is configured with a required key name. When the player character collides with a door, the door checks if the player character owns the corresponding key. If so, the door destroys itself to allow the player character passage.
This simple mechanic creates an opportunity for players to explore, collect, and unlock, laying the foundation for more advanced interactions in your game world.
1. Update the PlayerMovement script:
- At the top of the class, declare a new list variable to store owned keys:
private List<string> m_OwnedKeys = new List<string>();- Create a new method AddKey() to add a key to the list:
public void AddKey(string keyName)
{
m_OwnedKeys.Add(keyName);
}- Create a new method OwnKey() to check if a key is owned:
public bool OwnKey(string keyName)
{
return m_OwnedKeys.Contains(keyName);
}2. Set Up the Key prefab:
Now it’s time to set up the Key prefab and its functionality. We’ve provided a key model for you to use, but feel free to use one you find online, or even create your own if you have 3D modeling experience!
Just make sure your Key prefab includes the following:
- A collider component set as Trigger.
- A script Key.cs with the following code:
public class Key : MonoBehaviour
{
public string KeyName;
private void OnTriggerEnter(Collider other)
{
PlayerMovement player = other.gameObject.GetComponent<PlayerMovement>();
if (player == null) return;
player.AddKey(KeyName);
Destroy(GameObject);
}
}This setup allows the key to be collected by the player character, registered, and removed from the scene.
3. Set Up the Door prefab:
You also need to properly configure a Door prefab. We’ve provided a door model for you to use, but feel free to use your own. Just make sure the Door GameObject includes the following:
- A collider component that is not set as Trigger, to block player character movement.
- A script called Door.cs with the following code:
public class Door : MonoBehaviour
{
public string KeyName;
private void OnCollisionEnter(Collision collision)
{
PlayerMovement player = collision.gameObject.GetComponent<PlayerMovement>();
if (player == null) return;
if (player.OwnKey(KeyName))
{
Destroy(GameObject);
}
}
}This ensures the door only opens when the player has collected the required key. Now, when you add a Door and a Key prefabs into the scene, you can set their Key Name in the Inspector window. Just make sure that the Key and the Door you want to connect have matching Key Name values.
If you want to take this a step further, you could even add UI elements to show which keys are in the player’s inventory, and use color codes to indicate which key is linked to which door.
9. Next steps
If you followed all the steps, you’ve now created a unique and expanded version of your game. Congratulations!
In the next tutorial, you’ll learn how to create a web build of your game so you can easily share it with your friends and family.