Unity Game Development: Player, Camera, and Enemy Control
Player Controller
This section details the script for controlling the player’s movement.
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.InputSystem;
public class PlayerController : MonoBehaviour
{
// Rigidbody of the player.
private Rigidbody rb;
// Movement along X and Z axes.
private float movementX;
private float movementZ;
// Speed at which the player moves.
public float speed = 5f; // Set a default speed for smoother movement.
// Start is called before the first frame update.
void Start()
{
// Get and store the Rigidbody component attached to the player.
rb = GetComponent<Rigidbody>();
}
// This function is called when a move input is detected.
void OnMove(InputValue movementValue)
{
// Convert the input value into a Vector2 for movement.
Vector2 movementVector = movementValue.Get<Vector2>();
// Store the X and Z components of the movement.
movementX = movementVector.x;
movementZ = movementVector.y; // Changed to movementZ for clarity
}
// FixedUpdate is called once per fixed frame-rate frame.
private void FixedUpdate()
{
// Create a 3D movement vector using the X and Z inputs.
Vector3 movement = new Vector3(movementX, 0.0f, movementZ);
// Apply force to the Rigidbody to move the player.
rb.AddForce(movement * speed);
}
}
Camera Controller
This section explains how to create a camera that follows the player.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CameraController : MonoBehaviour
{
// Reference to the player GameObject.
public GameObject player;
// The distance between the camera and the player.
private Vector3 offset;
// Start is called before the first frame update.
void Start()
{
// Calculate the initial offset between the camera's position and the player's position.
offset = transform.position - player.transform.position;
}
// LateUpdate is called once per frame after all Update functions have been completed.
void LateUpdate()
{
// Maintain the same offset between the camera and player throughout the game.
transform.position = player.transform.position + offset;
}
}
Creating Walls
1. Create an Empty Parent GameObject for the Walls
- Create Empty Object: Right-click in the Hierarchy >
Create Empty
and rename it “Walls”. - Reset Transform: In the Inspector, reset the Transform values of the Walls object.
2. Create the West Wall
- Create Cube: Right-click in the Hierarchy >
3D Object > Cube
and rename it “West Wall”. - Reset Transform: In the Inspector, reset the West Wall’s Transform values.
- Parent to Walls: Drag the West Wall GameObject onto the Walls GameObject to make it a child.
- Set Transform:
- Scale: X = 0.5, Y = 2.0, Z = 20.5
- Position: X = -10 (to place it on the left edge).
3. Create a Material for the Walls
- Create Material: Right-click in the Project window >
Create > Material
and name it “Walls”. - Set Material Properties:
- Color: RGB (79, 79, 79) for dark gray.
- Metallic: 0, Smoothness: 0.25 (matte finish).
- Apply Material: Drag the Walls material onto the West Wall GameObject in the Scene view.
4. Duplicate and Create Other Walls
- East Wall: Duplicate the West Wall > Rename to “East Wall” > Set Position X to +10.
- North Wall: Duplicate the East Wall > Rename to “North Wall” > Rotate Y = 90, Position X = 0, Position Z = 10.
- South Wall: Duplicate the North Wall > Rename to “South Wall” > Position Z = -10.
Creating Collectibles
1. Create a PickUp GameObject
- Create Cube: Right-click in the Hierarchy >
3D Object > Cube
. Rename to “PickUp”. - Set Transform Values:
- Position Y = 0.5 (raise above ground).
- Rotation = 45 on all axes (tilt).
- Scale = 0.5 on all axes (make smaller).
2. Create a PickUp Material
- Duplicate Background Material: Right-click
Background
material in the Project window >Edit > Duplicate
. - Rename & Color: Rename to “PickUp” and set the color to RGB (255, 200, 0) for bright yellow.
- Apply Material: Drag the new material onto the PickUp GameObject.
3. Rotate the PickUp GameObject
- Create Rotator Script: Select
PickUp
>Add Component > New script
, name it “Rotator”. - Rotate in Update: In
Rotator
script, add:transform.Rotate(new Vector3(15, 30, 45) * Time.deltaTime);
4. Make PickUp a Prefab
- Create Prefab Folder: Right-click in Assets >
Create > Folder
, name it “Prefabs”. - Convert to Prefab: Drag PickUp from Hierarchy to Prefabs folder.
5. Add More Collectibles
- Create Parent GameObject: In the Hierarchy, create an empty GameObject named “PickUp Parent”. Reset its Transform.
- Move PickUp Under Parent: Drag PickUp into “PickUp Parent”.
- Duplicate PickUp: Select PickUp, duplicate (Ctrl+D), and move the new instances around the scene.
Detecting Collisions with Collectibles
1. Disable PickUps with OnTriggerEnter
- Add OnTriggerEnter: In
PlayerController
,void OnTriggerEnter(Collider other) { }
- Set Object Inactive: Inside
OnTriggerEnter
, add:other.gameObject.SetActive(false);
2. Tag the PickUp Prefab
- Create Tag: Go to
Inspector > Tag > Add Tag
, create “PickUp”. - Apply Tag: Select the
PickUp
prefab and assign the “PickUp” tag.
3. Write Conditional Statement
- Check for Tag: Inside
OnTriggerEnter
, add: if (other.gameObject.CompareTag("PickUp")) { }
- Disable PickUp: Inside the
if
, set:other.gameObject.SetActive(false);
4. Set PickUp Colliders as Triggers
- Enable Trigger: In the
PickUp
prefab’sBox Collider
, check “Is Trigger”.
5. Add Rigidbody to PickUp Prefab
- Add Rigidbody: In the
PickUp
prefab, add aRigidbody
. - Disable Physics: In the
Rigidbody
, uncheck “Use Gravity” and check “Is Kinematic”.
Displaying Score and Text
1. Storing Collected PickUp Value
- Declare Count: In
PlayerController
, declareprivate int count;
. - Initialize Count: In
Start()
, setcount = 0;
. - Increment Count: In
OnTriggerEnter()
, addcount = count + 1;
after deactivating the PickUp.
2. Creating the UI Text
- Add Text Object: Right-click in Hierarchy > UI > Text – TextMeshPro, rename to
CountText
. - Position Text: Set
Pos X = 10
,Pos Y = -10
for better placement.
3. Display Count Value
- Import TMP: Add
using TMPro;
. - Declare Text Variable:
public TextMeshProUGUI countText;
. - Create SetCountText(): Update text with
countText.text = "Count: " + count.ToString();
. - Call SetCountText: In
Start()
andOnTriggerEnter()
.
4. Create Game End Message
- Add Win Text: Create a new TextMeshPro UI, name
WinText
, set “You Win!”, and center it. - Disable at Start: In
Start()
,winTextObject.SetActive(false);
. - Display Win Text: When
count >= 12
, showwinTextObject.SetActive(true);
.
Enemies
This section covers setting up an enemy with AI navigation.
- Set up a Unity 3D environment with a player and an enemy
- Implement AI navigation using NavMesh for the enemy.
- Add obstacles (static and dynamic) to challenge the player.
- Create win and lose conditions using Unity scripting.
2. Create an Enemy
Step 1: Create an Empty Enemy GameObject
- Create an empty GameObject by right-clicking in the Hierarchy window and selecting Create Empty. Rename it to “Enemy”.
- Reset the Transform component of the Enemy GameObject so that its position is set to the origin (0, 0, 0).
Step 2: Add a Cube for the Enemy’s Body
- Right-click on the Enemy GameObject in the Hierarchy window, and select 3D Object > Cube. Rename the new cube to “EnemyBody”.
- Set the Scale of the EnemyBody GameObject to
0.5, 1, 0.5
to make it taller and more rectangular. - Set the Position of the EnemyBody to
(0, 0.5, 0)
to position it slightly above the ground.
Step 3: Create a Material for the Enemy
- In the Project window, right-click inside the Materials folder and select Create > Material. Rename the material to “Enemy”.
- Set the material’s Base Map color to any color you like for the Enemy GameObject.
- Drag and drop the Enemy material onto the EnemyBody GameObject in the Scene view or the Hierarchy window to apply the material.
Tip: You may want to move the Enemy GameObject slightly to the side to keep the scene organized. Make sure you move the Enemy parent GameObject, not just the EnemyBody child GameObject.
3. Bake a NavMesh
Step 1: Bake a NavMesh on the Ground GameObject
- Select the Ground GameObject in the Hierarchy window.
- In the Inspector window, click Add Component > NavMeshSurface to add the NavMesh component to the ground.
- Click Bake in the NavMeshSurface component to bake the NavMesh.
Step 2: Configure the NavMesh Surface
- In the Inspector window, expand the Object Collection module in the NavMeshSurface component settings by clicking the foldout triangle.
- In the Collect Objects dropdown, select Current Object Hierarchy.
- Click Bake again to re-bake the NavMesh with the new settings.
- Now, any PickUp GameObjects should be ignored by the NavMesh and won’t affect the pathfinding.
4. Make the Enemy Chase the Player
Step 1: Add a NavMesh Agent to the Enemy
- Select the Enemy GameObject in the Hierarchy window.
- In the Inspector window, click Add Component > Nav Mesh Agent to add the NavMeshAgent component to the Enemy.
- Set the Speed property to around
2.5
to control how fast the enemy moves.
Step 2: Add a New EnemyMovement Script
- With the Enemy GameObject selected, click Add Component > New Script in the Inspector window.
- Name the script “EnemyMovement”.
- Move the new script from the Assets folder into the Scripts folder in the Project window.
- Open the EnemyMovement script in your code editor to begin editing.
Step 3: Add Variables for the NavMesh Agent and Player
- At the top of your script, right after
using UnityEngine
, add the following line to include the NavMeshAgent namespace:using UnityEngine.AI;
- Declare the following variables above the Start method:
public Transform player; // Reference to the player's Transform private NavMeshAgent navMeshAgent; // Reference to the NavMeshAgent component
- In the Start method, initialize the navMeshAgent variable with:
navMeshAgent = GetComponent<NavMeshAgent>();
Step 4: Set the Enemy’s Destination as the Player’s Position
- In the Update method, add the following code to update the Enemy GameObject’s destination to the Player GameObject’s position:
if (player != null) { navMeshAgent.SetDestination(player.position); }
Step 5: Assign the Player to the Script
- Save the script and return to the Unity Editor.
- Select the Enemy GameObject in the Hierarchy window.
- In the Inspector window, drag the Player GameObject from the Hierarchy and drop it into the Player slot in the EnemyMovement script component.
Step 6: Test the Game
- Save the scene and run the game.
- The Enemy GameObject should now chase the Player GameObject around the scene.
- If the enemy is not chasing the player, check the script for errors and ensure that the Player reference is properly assigned in the EnemyMovement component.
5. Create Static Obstacles
Step 1: Create a Variety of Obstacles
- Create a new Cube GameObject by right-clicking in the Hierarchy window and selecting 3D Object > Cube.
- Use the Move, Rotate, and Scale tools to transform the cube into an interesting obstacle. You can create multiple obstacles of various shapes and sizes to make the scene more challenging.
- Make sure to turn at least one of the obstacles into a ramp to test the Enemy GameObject’s ability to climb slopes.
Step 2: Include the Obstacles in the NavMesh Surface
- In the Hierarchy window, drag each obstacle GameObject onto the Ground GameObject to make them child GameObjects.
- Select the Ground GameObject, and in the Inspector window, click Bake again in the NavMeshSurface component to regenerate the NavMesh with the newly added obstacles.
- You should now see areas around the obstacles carved out of the blue NavMesh Surface overlay in the scene.
Step 3: Customize the Agent Settings
- In the NavMesh Surface component, click the Agent Type dropdown and select Open Agent Settings. Alternatively, go to Window > AI > Navigation.
- Increase the Step Height to allow the agent to hop onto higher surfaces, and increase the Max Slope to allow the agent to climb steeper hills.
Step 4: Test Your Game
- Play the game and experiment with different agent settings.
- Test if the Enemy can navigate around the obstacles and climb ramps if set up.
6. Create Dynamic Obstacles
Step 1: Create a Light, Movable Cube
- Create a new Cube GameObject and rename it to “DynamicBox”.
- Scale and position the box in the scene as desired.
- Create a new material named “Dynamic Obstacle”, give it a color, and assign it to the DynamicBox.
- In the Inspector window, add a Rigidbody component to the DynamicBox and set the Mass to around
0.1
to make it easier to push around. - Test the scene and observe that the Enemy GameObject can pass straight through the dynamic obstacle.
Step 2: Make the Cube a NavMesh Obstacle
- Select the DynamicBox GameObject and add the NavMesh Obstacle component.
- Enable the Carve option in the NavMesh Obstacle component to ensure the obstacle is included in the NavMesh calculations and blocks the enemy’s path.
Step 3: Turn It into a Prefab
- Drag the DynamicBox GameObject from the Hierarchy window into the Prefabs folder in the Project window to create a prefab of the DynamicBox.
- Duplicate and scatter the DynamicBox prefab instances around the play area as needed.
- For organization, you might want to create an empty parent GameObject to hold all the DynamicBox instances.
Step 4: Test Your Game
- Experiment with different stacks or arrangements of dynamic obstacles to create fun and challenging play areas.
- Play the game to see how the dynamic obstacles affect the enemy’s pathfinding.
7. Set the Win and Lose Conditions
Step 1: Add a Lose Condition
- Open the PlayerController script.
- Add the following OnCollisionEnter function before the final curly bracket in the script to handle collision with the Enemy:private void OnCollisionEnter(Collision collision)Step 2: Add an “Enemy” Tag to the EnemyBody
{
if (collision.gameObject.CompareTag(“Enemy”))
{
// Destroy the player GameObject
Destroy(gameObject);
// Display “You Lose!” message
winTextObject.gameObject.SetActive(true);
winTextObject.GetComponent<TextMeshProUGUI>().text = “You Lose!”;
}
}
- Save the script and return to the Unity Editor.
- Select the EnemyBody GameObject in the Hierarchy window.
- In the Inspector window, locate the Tag dropdown menu and select Add Tag.
- Click the Add (+) button to create a new tag and name it “Enemy”. Make sure the capitalization matches what you wrote in the code.
- Save the new tag, then apply it to the EnemyBody GameObject (not the Enemy parent GameObject).
- Run the game to test — when the Enemy GameObject collides with the player, the Player GameObject will be destroyed, and the message “You Lose!” will be displayed.
Step 3: Destroy the Enemy GameObject When the Player Wins
- Open the PlayerController script again.
- In the SetCountText function, add the following line of code to destroy the Enemy GameObject when the player wins:
Destroy(GameObject.FindGameObjectWithTag("Enemy"));
Step 4: Test Your Completed Game
- Save the script and return to the Unity Editor.
- Run the game to test the win condition. When the player collects all the PickUp GameObjects, the Enemy GameObject should be destroyed.
PlayerController.cs
using UnityEngine; using UnityEngine.InputSystem; using TMPro; publicclassPlayerController : MonoBehaviour { // Rigidbody of the player.private Rigidbody rb; // Variable to keep track of collected “PickUp” objects.privateint count; // Movement along X and Y axes.privatefloat movementX; privatefloat movementY; // Speed at which the player moves.publicfloat speed = 0; // UI text component to display count of “PickUp” objects collected.public TextMeshProUGUI countText; // UI object to display winning text.public GameObject winTextObject; // Start is called before the first frame update.voidStart(){ // Get and store the Rigidbody component attached to the player. rb = GetComponent(); // Initialize count to zero. count = 0; // Update the count display. SetCountText(); // Initially set the win text to be inactive. winTextObject.SetActive(false); } // This function is called when a move input is detected.voidOnMove(InputValue movementValue){ // Convert the input value into a Vector2 for movement. Vector2 movementVector = movementValue.Get(); // Store the X and Y components of the movement. movementX = movementVector.x; movementY = movementVector.y; } // FixedUpdate is called once per fixed frame-rate frame.privatevoidFixedUpdate(){ // Create a 3D movement vector using the X and Y inputs. Vector3 movement = new Vector3 (movementX, 0.0f, movementY); // Apply force to the Rigidbody to move the player. rb.AddForce(movement * speed); } voidOnTriggerEnter(Collider other){ // Check if the object the player collided with has the “PickUp” tag.if (other.gameObject.CompareTag(“PickUp”)) { // Deactivate the collided object (making it disappear). other.gameObject.SetActive(false); // Increment the count of “PickUp” objects collected. count = count + 1; // Update the count display. SetCountText(); } } // Function to update the displayed count of “PickUp” objects collected.voidSetCountText(){ // Update the count text with the current count. countText.text = “Count: “ + count.ToString(); // Check if the count has reached or exceeded the win condition.if (count >= 12) { // Display the win text. winTextObject.SetActive(true); // Destroy the enemy GameObject. Destroy(GameObject.FindGameObjectWithTag(“Enemy”)); } } privatevoidOnCollisionEnter(Collision collision){ if (collision.gameObject.CompareTag(“Enemy”)) { // Destroy the current object Destroy(gameObject); // Update the winText to display “You Lose!” winTextObject.gameObject.SetActive(true); winTextObject.GetComponent().text = “You Lose!”; } }
EnemyMovement.cs