๐Ÿ”ท My First Unity Game ยท Episode 6 of 8 ยท See All Episodes
โš”๏ธ Episode 6 ยท Improver+ ยท Big Systems

Adventure Calls!
Unity Action RPG

A Zelda-like slice: 8-way movement with animation states, sword swings with hitboxes, enemy AI, hearts, pickups and scene-to-scene world flow. The biggest Unity build in the series.

๐Ÿ‘ถ Ages 12+ โฑ๏ธ ~2.5 Hours ๐Ÿ”ท Unity & C# โœ“ Free
๐ŸŽฎ 8-way movement ๐ŸŽฌ Animator states โš”๏ธ Hitbox combat ๐Ÿง  Enemy AI โค๏ธ Hearts & i-frames ๐Ÿšช Scene flow
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
1
๐ŸŽฎ
Top-Down Movement Done Right
Rigidbody movement with normalised input
Active
๐ŸŽฏ
Goal for this step

Build smooth 8-way movement through the physics system.

  • 1New 2D project. Player: sprite + Rigidbody2D (Gravity 0, Freeze Rotation Z!) + CapsuleCollider2D.
  • 2Read input in Update, MOVE in FixedUpdate, input wants every frame, physics wants the physics tick. This split is Unity best practice.
  • 3Set rb.linearVelocity from a normalised input vector ร— speed, normalisation kills the diagonal speed bug (an old friend by now).
  • 4Walls: sprites with colliders. The physics engine handles all sliding-along-walls for free.
PlayerMove.cs
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    public float speed = 5f;
    Rigidbody2D rb;
    Vector2 input;

    void Awake() { rb = GetComponent<Rigidbody2D>(); }

    void Update()
    {
        input.x = Input.GetAxisRaw("Horizontal");
        input.y = Input.GetAxisRaw("Vertical");
        input = input.normalized;
    }

    void FixedUpdate()
    {
        rb.linearVelocity = input * speed;
    }
}
โœ๏ธ
Fill in the Blanks
+15 XP
Input is read in Update but velocity is applied in , the physics tick. Diagonal speed is fixed by calling on the input vector.
๐Ÿง 
Knowledge Check
+15 XP
Why freeze the Rigidbodyโ€™s Z rotation?
AZ does not exist in 2D
BCollisions would physically SPIN your character like a bumped shopping trolley
CIt saves memory
2
๐ŸŽฌ
The Animator
Idle/walk animations driven by parameters
Locked
๐ŸŽฏ
Goal for this step

Hook movement into Unityโ€™s animation state machine.

  • 1Grab free character sprites (kenney.nl, free game assets, no strings) and slice the sheet in the Sprite Editor.
  • 2Select frames โ†’ drag onto the player โ†’ Unity creates an Animation + Animator. Make clips: Idle, WalkDown, WalkUp, WalkSide.
  • 3The Animator window is a state machine, states and transition arrows (your RPG slime brain, visualised!).
  • 4Add parameters Speed (float), DirX/DirY (float). Transitions test them: Speed > 0.1 โ†’ walk; direction picks which walk.
  • 5Feed parameters from code: anim.SetFloat("Speed", input.magnitude). Flip side-walk with localScale.x = -1 instead of duplicate animations.
PlayerMove.cs (additions)
Animator anim;

void Update()
{
    // ... input reading ...
    anim.SetFloat("Speed", input.magnitude);
    if (input.x != 0 || input.y != 0)
    {
        anim.SetFloat("DirX", input.x);
        anim.SetFloat("DirY", input.y);
    }
}
โœ๏ธ
Fill in the Blanks
+15 XP
The window is a visual state machine. Code drives it by setting like Speed, which transitions test against conditions.
๐Ÿง 
Knowledge Check
+15 XP
The Animator IS a state machine. Where have you hand-built that exact pattern?
ANever
BThe GML slime (idle/chase/attack) and Pongโ€™s MENU/PLAY/OVER, engines just draw the diagram for you
CIn CSS
3
โš”๏ธ
Sword Swings & Hitboxes
A short-lived trigger hitbox in facing direction
Locked
๐ŸŽฏ
Goal for this step

Implement melee attacks with proper hitboxes.

  • 1The sword hit = a child GameObject with a trigger BoxCollider2D, disabled by default, positioned by facing direction.
  • 2On Space: enable it for 0.15 s via a coroutine, Unityโ€™s "do this over time" tool (start, wait, finish, no manual timers!).
  • 3yield return new WaitForSeconds(0.15f) pauses the coroutine, not the game.
  • 4The hitboxโ€™s OnTriggerEnter2D damages any Enemy component it touches (the GML obj_swing pattern, engine-grade).
SwordAttack.cs
using UnityEngine;
using System.Collections;

public class SwordAttack : MonoBehaviour
{
    public GameObject hitbox;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
            StartCoroutine(Swing());
    }

    IEnumerator Swing()
    {
        hitbox.SetActive(true);
        yield return new WaitForSeconds(0.15f);
        hitbox.SetActive(false);
    }
}
โŒจ๏ธ
Code Challenge
+20 XP
Write the timed swing coroutine:
SwordAttack.cs
IEnumerator Swing()
{
    hitbox.SetActive();
    yield return new (0.15f);
    hitbox.SetActive();
}
๐Ÿ’ก Hint: Turn the hitbox on, wait 0.15 seconds without blocking the game, turn it off.
๐Ÿง 
Knowledge Check
+15 XP
Coroutines replace which manual pattern from your earlier games?
AThe draw call
BFrame-countdown timers (i-frames, serve delays, alarms), "wait then do" as readable code
CCollision checks
4
๐Ÿง 
Enemy AI & Knockback
Chase-state slimes with physics knockback
Locked
๐ŸŽฏ
Goal for this step

Build enemies that chase, hurt, and get knocked flying.

  • 1Enemy prefab: sprite, Rigidbody2D (gravity 0), collider, Enemy.cs with hp and the idle/chase distances you know by heart.
  • 2Chase: rb.linearVelocity = (player.position โˆ’ transform.position).normalized ร— speed (in range), else zero.
  • 3TakeDamage: hp down, plus knockback, rb.AddForce(direction * 8f, ForceMode2D.Impulse): real physics punch!
  • 4Death at 0 hp: burst particles (Episode 3 prefab!), Destroy. Contact damage to the player comes next step.
  • 5Prefab it, scatter five around the map.
Enemy.cs
using UnityEngine;

public class Enemy : MonoBehaviour
{
    public int hp = 3;
    public float speed = 2.2f, aggroRange = 4f;
    Transform player;
    Rigidbody2D rb;

    void FixedUpdate()
    {
        float dist = Vector2.Distance(transform.position, player.position);
        if (dist < aggroRange)
            rb.linearVelocity = (player.position - transform.position)
                                .normalized * speed;
        else
            rb.linearVelocity = Vector2.zero;
    }

    public void TakeDamage(int amount, Vector2 fromDirection)
    {
        hp -= amount;
        rb.AddForce(fromDirection * 8f, ForceMode2D.Impulse);
        if (hp <= 0) Destroy(gameObject);
    }
}
โœ๏ธ
Fill in the Blanks
+15 XP
A physics shove uses rb. with ForceMode2D.. The chase begins inside the distance.
๐Ÿง 
Knowledge Check
+15 XP
Impulse knockback (vs teleporting the enemy back) feels better becauseโ€ฆ
AIt is one line
BPhysics integrates it: the enemy skids, recovers, collides believably, weight instead of teleport
CEnemies prefer it
5
โค๏ธ
Hearts, Damage & i-Frames
Player health with UI hearts and invincibility
Locked
๐ŸŽฏ
Goal for this step

Complete the combat loop with player health done properly.

  • 1PlayerHealth.cs: 6 half-hearts, an i-frames window after hits (a coroutine + sprite flashing, you know exactly why from GML).
  • 2Enemy contact: OnCollisionStay2D (not Enter, standing IN an enemy must keep hurtingโ€ฆ once per i-frame window).
  • 3Hearts UI: Canvas with heart Images; update full/half/empty sprites from current hp (a loop over the array).
  • 4Death: hp 0 โ†’ game-over panel + restart button (Episode 3โ€™s scene reload).
  • 5Pickups: heart drops (20% on enemy death, Instantiate a pickup prefab; its trigger heals and Destroys itself).
PlayerHealth.cs
public IEnumerator Hurt(int dmg, Vector2 knockDir)
{
    if (invincible) yield break;
    hp -= dmg;
    UpdateHeartsUI();
    if (hp <= 0) { GameOver(); yield break; }

    invincible = true;
    for (int i = 0; i < 6; i++)     // flash 6 times
    {
        sprite.enabled = !sprite.enabled;
        yield return new WaitForSeconds(0.12f);
    }
    sprite.enabled = true;
    invincible = false;
}
โœ๏ธ
Fill in the Blanks
+15 XP
Continuous contact damage uses OnCollision2D rather than Enter. The flash-and-immunity window is handled by one guarded by an flag.
๐Ÿง 
Knowledge Check
+15 XP
yield break inside a coroutine does what?
ACrashes it
BExits the coroutine early, like return, for the hurt-while-invincible and death cases
CPauses forever
6
๐Ÿšช
World Flow & The Slice
Doors between scenes, spawn points, persistent health
Locked
๐ŸŽฏ
Goal for this step

Connect scenes into a world and ship the vertical slice.

  • 1Build 2 more scenes (forest, cave, File โ†’ New Scene, add to Build Profiles!).
  • 2Door prefab: trigger + target scene name + spawn point id. Enter โ†’ LoadScene; a SpawnManager positions the player at the matching id on load.
  • 3Persist health BETWEEN scenes: DontDestroyOnLoad on the player (and camera), Unityโ€™s "persistent" flag.
  • 4The slice checklist: move โœ“ animate โœ“ fight โœ“ get hurt โœ“ heal โœ“ travel โœ“. That set of verbs IS an action RPG.
  • 5Episode 7 wraps it in real menus and UI. Save the project!
Door.cs
using UnityEngine;
using UnityEngine.SceneManagement;

public class Door : MonoBehaviour
{
    public string targetScene;
    public string spawnId;

    void OnTriggerEnter2D(Collider2D other)
    {
        if (other.CompareTag("Player"))
        {
            SpawnManager.nextSpawnId = spawnId;
            SceneManager.LoadScene(targetScene);
        }
    }
}
โŒจ๏ธ
Code Challenge
+20 XP
Send the player through a door:
Door.cs
if (other.CompareTag())
{
    SpawnManager.nextSpawnId = ;
    SceneManager.(targetScene);
}
๐Ÿ’ก Hint: Filter to the player tag, stash where they should appear, then load the destination scene.
๐Ÿง 
Knowledge Check
+15 XP
DontDestroyOnLoad on the player solves which problem?
ASlow loading
BScene loads normally destroy everything, health/inventory would reset at every door without it
CEnemy respawns
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Combat, AI, health systems and world flow, a real action-RPG core in Unity. Episode 7 gives it menus and UI polish!
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Episode 7: UI & Menus โ†’
โญ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi