How to Make Your First 3D Game in Unity: Build a Pickup Arena

Build a room with a floor, a player and three pickups. The player should move with the keyboard, collect each pickup once and see a clear message when all three are gone. That is a complete first 3D game in Unity: it has controls, a goal, feedback and an ending. A small arena also gives you a dependable place to learn what the Inspector, the physics system and your scripts are each responsible for.

The example below targets desktop keyboard play in Unity 6 with the Input System package. It uses 3D colliders throughout. The prototype can be made entirely from built-in shapes. Save the scene before pressing Play, then test one change at a time. A plain room that works after a fresh launch is more useful than a furnished room whose player falls through the floor.

Set up a scene you can measure

Create a 3D project and save a scene named PickupArena in an Assets/Scenes folder. Create a cube called Floor, position it at (0, -0.5, 0) and scale it to (14, 1, 10). The top of this cube is now at world height zero. Leave its Box Collider enabled and leave Is Trigger off. Add four thin cubes as walls, leaving enough space between them to travel diagonally across the arena.

Give the floor and walls visibly different materials. At the beginning that distinction matters more than detailed textures: you need to see where movement should stop. Name the wall objects by position so you can identify them from a collision report. Make walls tall enough that a slight physics bounce cannot carry the player out. Keep their scale and transform values easy to read while the room is still a test environment.

Save the scene again. After pressing Play, the editor normally treats changes to scene instances as temporary. If you tune a speed in Play Mode and want to keep it, record the number and apply it to the saved scene or prefab after stopping. This habit prevents a common beginner mystery: yesterday’s successful test disappears because the value existed only on the running instance.

Build the player from a capsule and a Rigidbody

Create a capsule named Player at (0, 1.1, 0). Its Capsule Collider should be solid. Add a Rigidbody, enable gravity and constrain rotation so contact with the walls cannot tip it onto its side. Leave translation unconstrained. At rest the capsule should settle onto the floor. Before writing a movement script, run the scene and confirm it lands. If it falls forever, check the Floor collider, the player’s collider, and their relative positions.

Install or enable the Input System through the Package Manager if your project does not already use it. The example reads a desktop keyboard directly so there are no action bindings to configure for the first run. Configure Active Input Handling to support the Input System if Unity requests it and restart the editor if prompted. A project using only the older input backend will need that configuration change or a different movement script.

Create PlayerDrive.cs and attach it to the player. The component reads keyboard state in Update, stores the intended direction and changes horizontal velocity during FixedUpdate. The existing vertical velocity is preserved so gravity still works. Clamping the input vector prevents diagonal movement from being faster simply because the player is holding two keys.

using UnityEngine;
using UnityEngine.InputSystem;

[RequireComponent(typeof(Rigidbody))]
public class PlayerDrive : MonoBehaviour
{
    [SerializeField] float speed = 5f;
    Rigidbody body;
    Vector2 direction;

    void Awake() { body = GetComponent<Rigidbody>(); }

    void Update()
    {
        var keyboard = Keyboard.current;
        if (keyboard == null) { direction = Vector2.zero; return; }
        float x = (keyboard.dKey.isPressed ? 1f : 0f)
                - (keyboard.aKey.isPressed ? 1f : 0f);
        float z = (keyboard.wKey.isPressed ? 1f : 0f)
                - (keyboard.sKey.isPressed ? 1f : 0f);
        direction = Vector2.ClampMagnitude(new Vector2(x, z), 1f);
    }

    void FixedUpdate()
    {
        body.linearVelocity = new Vector3(
            direction.x * speed, body.linearVelocity.y, direction.y * speed);
    }
}

Walk straight into a wall and release the key. The player should stop at the boundary rather than passing through it. Then hold two keys and approach the same wall diagonally. This is intentionally direct arcade movement: releasing the keys removes horizontal speed immediately. If you later add acceleration or knockback, revisit the velocity assignment because it deliberately owns the horizontal axes every physics step.

Give the camera a position that explains the controls

For this room a fixed camera works well. Place the Main Camera above and behind the arena, for example around (0, 12, -12), and rotate it to look toward the centre of the floor. Frame both the near and far walls in the Game view. W moves the player toward positive Z. From this camera position that should look like movement away from the viewer rather than movement toward the bottom edge.

Start with one camera and one active Audio Listener. An accidental second camera can conceal the view you were adjusting. Change the Game view aspect ratio and verify that the pickups remain visible near the edges. If one disappears offscreen at a narrower aspect ratio, move it or change framing before adding a follow script. The player should learn the goal from the scene itself rather than from a dot at the edge of the display.

Put the first pickup directly ahead of the spawn, the second near a side wall and the third in the opposite corner. That placement tests forward movement, turning and wall contact within a few seconds. A row of identical objects down the middle can make a prototype appear finished while hiding the fact that left and right movement are reversed.

Make a reusable pickup with an explicit owner

Create a small sphere called Pickup. Position its centre around half a unit above the floor and enable Is Trigger on its Sphere Collider. A trigger detects overlap without blocking the player. The player’s Rigidbody supplies the physics body for this pair. A 2D collider or a Rigidbody2D belongs to a separate physics system and will not take part in this 3D trigger callback. Unity documents the callback in its OnTriggerEnter reference.

Give the pickup a bright material that contrasts with the floor. Drag it from the Hierarchy into an Assets/Prefabs folder to make a prefab. Add the script below to the prefab, then place three instances in the scene. Keep each instance at the same height initially. If you edit only one scene instance, inspect the prefab overrides before expecting the change to appear on the other two.

using UnityEngine;

public class ArenaPickup : MonoBehaviour
{
    bool collected;

    void OnTriggerEnter(Collider other)
    {
        if (collected) return;
        var score = other.GetComponentInParent<ArenaScore>();
        if (score == null) return;
        collected = true;
        score.Collect();
        gameObject.SetActive(false);
    }
}

This script asks the entering collider for an ArenaScore component on itself or a parent. Put ArenaScore on the player root. That lets a child collider belong to the same player without relying on an exact object name. The collected flag is set before the score changes. Even if the player has multiple colliders, the same pickup cannot award two points during the same encounter.

Show progress and give the final pickup a consequence

Create ArenaScore.cs with the code below and attach it to the player. For this intentionally small prototype, OnGUI displays a simple label without a Canvas or a font asset. The target is three because the room contains exactly three pickups. If you change that count, update the target or derive it from the level configuration. An unreachable win condition is an easy mistake when adding a fourth pickup halfway through development.

using UnityEngine;

public class ArenaScore : MonoBehaviour
{
    [SerializeField] int target = 3;
    int collected;
    bool won;

    public void Collect()
    {
        if (won) return;
        collected++;
        won = collected >= target;
    }

    void OnGUI()
    {
        string label = won ? "Arena clear!" : $"Pickups: {collected}/{target}";
        GUI.Label(new Rect(16, 16, 240, 32), label);
    }
}

Play the room twice, collecting the objects in different orders. The label should move from zero to one on the first pickup, from one to two on the next, and to the win message on the third. Walk back through the first position. Nothing should change. If a sphere disappears but the count you see does not change, look for a second player or another score display before modifying the trigger logic.

For a deliberate ending, decide whether movement should remain available after winning. In this version it does: the player can walk around the cleared room. That is useful while testing boundaries. A later version could disable input and display a restart button. Add that only after the complete room works. Pausing all time on victory too early can make a replay button or an animation appear broken for reasons unrelated to scoring.

Debug by observing the first missing event

When a pickup fails, establish which event is missing. Did the player reach the pickup’s actual collision volume? Open the Scene view during play and inspect both collider shapes. Is the pickup’s collider enabled and marked as a trigger? Is there an active Rigidbody on the player? Are the two objects on layers that can interact? A visible mesh can touch the player while its collider sits somewhere else after a child transform was moved.

If a temporary log at the start of OnTriggerEnter appears, collision detection is already working. Log the incoming collider’s name and inspect its parent hierarchy. A projectile, scenery object or unconfigured player can enter the trigger without carrying ArenaScore. The null check deliberately ignores it. If the log never appears, adjusting the text label or the win target cannot solve the problem; the score method has not been reached yet.

Test what happens when two pickups overlap. Both may be collected on a single step, which is valid if they represent separate rewards. If that looks confusing, spread them farther apart so the player sees two distinct interactions. Also try starting inside a pickup and against a wall. These edge cases reveal incorrect spawn positions without requiring an elaborate debugger or a large level.

Build the game and repeat the route outside the editor

Save the scene and ensure it is included in the active desktop build profile’s scene list. Set the arena as the intended starting scene for this prototype. Make a build in a separate output directory. Keep the executable alongside the supporting files Unity creates. Launch the build directly and confirm that it opens into the same arena, accepts keyboard input, displays the score and reaches the win state.

Try a fresh launch after closing the build. The score should start at zero and all pickups should return because they are scene instances and the example does not persist progress. If the build opens a different scene, inspect the scene list and order. If it opens the right room but ignores input, check focus, the active input backend and the player object’s components. Make one change per test so you can identify the real fix.

Make a short acceptance run before sharing the build

Write down a route that a second player can follow without knowing your project. Ask them to start the game, move toward the blue wall, collect the nearest sphere, circle the centre of the room and take the remaining two. Ask them to describe what the first pickup did. If they say it vanished but cannot tell that their score increased, move the counter into a more visible part of the screen or increase its contrast. The physics can be working perfectly while the player receives too little information to understand the goal.

Try the same route at a small window size. Watch whether the score covers a pickup, whether the far wall cuts off the player’s feet, and whether the victory message appears while the player is still visible. These are inexpensive fixes at the grey-box stage. A camera adjustment of a few degrees may solve a visibility problem that would otherwise tempt you to rebuild the room.

Finally, keep the build that passed this test. When you add a timer, an enemy or a second room, run the same route in both versions. If movement suddenly feels heavier, compare the Rigidbody constraints and speed before blaming the new art. If score changes twice, inspect the new colliders before rewriting the score system. A known-good build gives you a reference even after the scene has changed beyond recognition.

Common questions

Do I need to learn modelling before making this game?

No. Cubes, a capsule and spheres have enough visual information to teach movement, collisions and goals. Replace one pickup with custom art only after the primitive version works. Keep its collider separate from the visible model so a prettier shape does not quietly change gameplay.

Why does my player fall when movement starts?

Check whether the floor is solid and whether another script overwrites vertical velocity or moves the Transform through geometry. Re-run the untouched landing test with PlayerDrive disabled. If the capsule already falls through the floor, repair the physics setup before diagnosing keyboard input.

How do I support a controller later?

Move keyboard-specific reading into an Input Action that returns a two-dimensional movement vector and bind both WASD and a gamepad stick. The physics code already consumes a direction, so you can keep the movement and scoring systems while replacing only the source of input.

What should I add to the second version?

Add a second room with a different route, a clear restart control and one obstacle that changes how the player approaches a pickup. Give each addition a test: the second room loads from a build, restart restores all three rewards, and the obstacle leaves at least one readable path to victory. That turns a working exercise into a game you can expand with confidence.