Dash & Slash: An Isometric Rogue-like
————————————————————————————————————-
Module : Development Project CTEC3451
Engine : Unity C#
[Procedural Gen "Cellular Autamata, FSM "Finite State Machine"]
Note: In GitHub Navigate To master Branch to Find the Files
Please Scroll Down
Overview
This project was my love letter to isometric rogue-likes, built from the ground up in Unity. The core idea was to create a game that felt fresh and unpredictable every time you hit play. How'd I do it? By diving deep into procedural level generation using Cellular Automata (CA). We're talking randomly generated 3D maps, smart enemy AI, and a combat system that feels meaty. I wanted a clean UI with real-time score tracking, too. The whole thing was a blast, and it taught me a ton about balancing replayability with technical complexity.
This project was my love letter to isometric rogue-likes, built from the ground up in Unity. The core idea was to create a game that felt fresh and unpredictable every time you hit play. How'd I do it? By diving deep into procedural level generation using Cellular Automata (CA). We're talking randomly generated 3D maps, smart enemy AI, and a combat system that feels meaty. I wanted a clean UI with real-time score tracking, too. The whole thing was a blast, and it taught me a ton about balancing replayability with technical complexity.
This project was my love letter to isometric rogue-likes, built from the ground up in Unity. The core idea was to create a game that felt fresh and unpredictable every time you hit play. How'd I do it? By diving deep into procedural level generation using Cellular Automata (CA). We're talking randomly generated 3D maps, smart enemy AI, and a combat system that feels meaty. I wanted a clean UI with real-time score tracking, too. The whole thing was a blast, and it taught me a ton about balancing replayability with technical complexity.
The Grind (a.k.a. Challenges)
Procedural Generation Balance: Getting the CA algorithm just right was a real puzzle. Too random, and the levels were just chaotic blobs. Too structured, and they felt boring. It was a constant back-and-forth of tweaking the initial fill probability and rule iterations to find that sweet spot.
Procedural Generation Balance: Getting the CA algorithm just right was a real puzzle. Too random, and the levels were just chaotic blobs. Too structured, and they felt boring. It was a constant back-and-forth of tweaking the initial fill probability and rule iterations to find that sweet spot.
Procedural Generation Balance: Getting the CA algorithm just right was a real puzzle. Too random, and the levels were just chaotic blobs. Too structured, and they felt boring. It was a constant back-and-forth of tweaking the initial fill probability and rule iterations to find that sweet spot.
Mesh Generation: This was a beast. I had to create a custom MeshGenerator class to turn my 2D grid data into 3D geometry. The first attempts were... rough. Inverted walls, weird gaps, and messed-up normals made everything look like a funhouse mirror. Fixing this meant fine-tuning the vertex and triangle generation algorithms to get a clean, renderable mesh.
Mesh Generation: This was a beast. I had to create a custom MeshGenerator class to turn my 2D grid data into 3D geometry. The first attempts were... rough. Inverted walls, weird gaps, and messed-up normals made everything look like a funhouse mirror. Fixing this meant fine-tuning the vertex and triangle generation algorithms to get a clean, renderable mesh.
Mesh Generation: This was a beast. I had to create a custom MeshGenerator class to turn my 2D grid data into 3D geometry. The first attempts were... rough. Inverted walls, weird gaps, and messed-up normals made everything look like a funhouse mirror. Fixing this meant fine-tuning the vertex and triangle generation algorithms to get a clean, renderable mesh.
AI Pathfinding: The levels were dynamic, so the AI couldn't rely on a pre-baked navigation mesh. The solution? Dynamically generating the NavMesh at runtime. It took some serious work to make sure the NavMeshAgent could navigate a brand new map without a hitch.
AI Pathfinding: The levels were dynamic, so the AI couldn't rely on a pre-baked navigation mesh. The solution? Dynamically generating the NavMesh at runtime. It took some serious work to make sure the NavMeshAgent could navigate a brand new map without a hitch.
AI Pathfinding: The levels were dynamic, so the AI couldn't rely on a pre-baked navigation mesh. The solution? Dynamically generating the NavMesh at runtime. It took some serious work to make sure the NavMeshAgent could navigate a brand new map without a hitch.
Scoring System: My ScoreManager was a bit of a diva. During intense combat, it would sometimes get confused and refuse to update the score correctly. Debugging real-time data flow in chaotic moments was a huge learning curve.
Scoring System: My ScoreManager was a bit of a diva. During intense combat, it would sometimes get confused and refuse to update the score correctly. Debugging real-time data flow in chaotic moments was a huge learning curve.
Scoring System: My ScoreManager was a bit of a diva. During intense combat, it would sometimes get confused and refuse to update the score correctly. Debugging real-time data flow in chaotic moments was a huge learning curve.
Approach
Procedural Generation From 2D Grid to 3D World
For the level design, I built a two-part procedural system. First, theMapGeneratorclass used Cellular Automata (CA) to create a maze-like 2D grid. TheMeshGeneratorthen took that raw data and transformed it into a fully navigable 3D environment. This modular approach allowed me to handle the logical and visual parts of the level creation separately.
Procedural Generation From 2D Grid to 3D World
For the level design, I built a two-part procedural system. First, theMapGeneratorclass used Cellular Automata (CA) to create a maze-like 2D grid. TheMeshGeneratorthen took that raw data and transformed it into a fully navigable 3D environment. This modular approach allowed me to handle the logical and visual parts of the level creation separately.
Procedural Generation From 2D Grid to 3D World
For the level design, I built a two-part procedural system. First, theMapGeneratorclass used Cellular Automata (CA) to create a maze-like 2D grid. TheMeshGeneratorthen took that raw data and transformed it into a fully navigable 3D environment. This modular approach allowed me to handle the logical and visual parts of the level creation separately.
Part A: MapGenerator.cs - "The Brains of the Operation"
This class was the mastermind behind the dungeon layouts. It started with a simple, randomly filled 2D grid and then ran a Cellular Automata algorithm to "smooth" it out, creating distinct walls and open areas. It also handled crucial post-processing steps like identifying individual rooms and connecting them with passages to ensure every level was fully traversable.
Part A: MapGenerator.cs - "The Brains of the Operation"
This class was the mastermind behind the dungeon layouts. It started with a simple, randomly filled 2D grid and then ran a Cellular Automata algorithm to "smooth" it out, creating distinct walls and open areas. It also handled crucial post-processing steps like identifying individual rooms and connecting them with passages to ensure every level was fully traversable.
Part A: MapGenerator.cs - "The Brains of the Operation"
This class was the mastermind behind the dungeon layouts. It started with a simple, randomly filled 2D grid and then ran a Cellular Automata algorithm to "smooth" it out, creating distinct walls and open areas. It also handled crucial post-processing steps like identifying individual rooms and connecting them with passages to ensure every level was fully traversable.
AI Development
For enemy AI, I used Unity’s NavMeshAgent for pathfinding and a Finite State Machine (FSM) to control behaviors. I created different enemy types, like the melee-focused TurtleEnemy and the ranged MageEnemy, each with their own attack patterns.
AI Development
For enemy AI, I used Unity’s NavMeshAgent for pathfinding and a Finite State Machine (FSM) to control behaviors. I created different enemy types, like the melee-focused TurtleEnemy and the ranged MageEnemy, each with their own attack patterns.
AI Development
For enemy AI, I used Unity’s NavMeshAgent for pathfinding and a Finite State Machine (FSM) to control behaviors. I created different enemy types, like the melee-focused TurtleEnemy and the ranged MageEnemy, each with their own attack patterns.
Combat System
I built a projectile system that included homing projectiles using the ProjectileMoverBase class. This made combat more dynamic, with projectiles tracking enemies and dealing damage on impact.
Combat System
I built a projectile system that included homing projectiles using the ProjectileMoverBase class. This made combat more dynamic, with projectiles tracking enemies and dealing damage on impact.
Combat System
I built a projectile system that included homing projectiles using the ProjectileMoverBase class. This made combat more dynamic, with projectiles tracking enemies and dealing damage on impact.
Key Code Snippet: The Smoothing Algorithm
This is the core of the Cellular Automata logic. It iterates through the map and decides whether a tile should be a wall or a floor based on its neighbors.
Key Code Snippet: The Smoothing Algorithm
This is the core of the Cellular Automata logic. It iterates through the map and decides whether a tile should be a wall or a floor based on its neighbors.
Key Code Snippet: The Smoothing Algorithm
This is the core of the Cellular Automata logic. It iterates through the map and decides whether a tile should be a wall or a floor based on its neighbors.
void SmoothMap()
{
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
int neighbourWallTiles = GetSurroundingWallCount(x, y);
if (neighbourWallTiles > 4)
map[x, y] = 1; // Wall
else if (neighbourWallTiles < 4)
map[x, y] = 0; // Open space
}
}
}Part B: MeshGenerator.cs - The Architect
This class was responsible for turning the 2D grid data from "MapGenerator" into the 3D geometry players see in-game. It reads the map data and intelligently creates the mesh for the floor and walls, ensuring there were no gaps and that the lighting and physics worked correctly. This was a challenging but rewarding process that taught me a lot about low-level mesh creation in Unity.
Key Code Snippet: Generating Walls
This snippet shows how I iterated through the mesh's outlines to create the 3D walls. The code calculates the height and UVs, then generates the vertices and triangles for both the front and back faces of each wall segment.
Part B: MeshGenerator.cs - The Architect
This class was responsible for turning the 2D grid data from "MapGenerator" into the 3D geometry players see in-game. It reads the map data and intelligently creates the mesh for the floor and walls, ensuring there were no gaps and that the lighting and physics worked correctly. This was a challenging but rewarding process that taught me a lot about low-level mesh creation in Unity.
Key Code Snippet: Generating Walls
This snippet shows how I iterated through the mesh's outlines to create the 3D walls. The code calculates the height and UVs, then generates the vertices and triangles for both the front and back faces of each wall segment.
Part B: MeshGenerator.cs - The Architect
This class was responsible for turning the 2D grid data from "MapGenerator" into the 3D geometry players see in-game. It reads the map data and intelligently creates the mesh for the floor and walls, ensuring there were no gaps and that the lighting and physics worked correctly. This was a challenging but rewarding process that taught me a lot about low-level mesh creation in Unity.
Key Code Snippet: Generating Walls
This snippet shows how I iterated through the mesh's outlines to create the 3D walls. The code calculates the height and UVs, then generates the vertices and triangles for both the front and back faces of each wall segment.
void CreateWallMesh(float squareSize)
{
CalculateMeshOutlines();
List<Vector3> wallVertices = new List<Vector3>();
List<int> wallTriangles = new List<int>();
Mesh wallMesh = new Mesh();
float wallHeight = 10;
foreach (List<int> outline in outlines)
{
for (int i = 0; i < outline.Count - 1; i++)
{
Vector3 start = vertices[outline[i]];
Vector3 end = vertices[outline[i + 1]];
Vector3 bottomLeft = start;
Vector3 bottomRight = end;
Vector3 topLeft = bottomLeft + Vector3.up * wallHeight;
Vector3 topRight = bottomRight + Vector3.up * wallHeight;
// Front face
wallVertices.Add(bottomLeft);
wallVertices.Add(bottomRight);
wallVertices.Add(topRight);
wallVertices.Add(topLeft);
// Back face
wallVertices.Add(bottomRight);
wallVertices.Add(bottomLeft);
wallVertices.Add(topLeft);
wallVertices.Add(topRight);
// ... (rest of the code to add triangles and UVs)
}
}
walls.mesh = wallMesh;
MeshCollider wallCollider = gameObject.AddComponent<MeshCollider>();
wallCollider.sharedMesh = wallMesh;
}Worflow Diagram
Worflow Diagram
Worflow Diagram
+------------------------------------------------+
| **MapGenerator.cs** (The Logic) |
+------------------------------------------------+
|
[GenerateMap() Called]
|
v
+------------------------------------------------+
| **Phase 1: Grid Initialization** |
| • RandomFillMap() |
| • SmoothMap() (Cellular Automata) |
+------------------------------------------------+
|
v
+------------------------------------------------+
| **Phase 2: Level Refinement** |
| • ProcessMap() |
| - Removes small rooms/walls |
| - Connects closest rooms |
| • Adds a border |
+------------------------------------------------+
|
[Passes final map to...]
|
v
+------------------------------------------------+
| **MeshGenerator.cs** (The Mesh) |
+------------------------------------------------+
|
[GenerateMesh() Called]
|
v
+------------------------------------------------+
| **Phase 3: Mesh Creation** |
| • TriangulateSquare() |
| - Generates cave mesh |
| • CreateFloorMesh() |
| - Generates floor mesh and collider |
| • CreateWallMesh() |
| - Generates 3D walls and collider |
+------------------------------------------------+
|
v
+------------------------------------------------+
| **Procedurally Generated Level
Core Scripts
PlayerController.cs " You're in Control! "
This script is the brain and brawn behind your player character in Dash & Slash, I built it as a Singleton, a robust design pattern that ensures there's only one of these crucial controllers in your game, making it a reliable hub for all things player-related. This comprehensive script handles everything: from responsive movement (both forward and backward with distinct speeds) and precise mouse-based aiming, to the firing of projectiles and the nuanced management of health and damage feedback. It's also tightly integrated with the animation system, ensuring your player's on-screen actions always match their current state. Basically, it's the full package for bringing your hero to life!
"Player Controller Code Snippet"
PlayerController.cs " You're in Control! "
This script is the brain and brawn behind your player character in Dash & Slash, I built it as a Singleton, a robust design pattern that ensures there's only one of these crucial controllers in your game, making it a reliable hub for all things player-related. This comprehensive script handles everything: from responsive movement (both forward and backward with distinct speeds) and precise mouse-based aiming, to the firing of projectiles and the nuanced management of health and damage feedback. It's also tightly integrated with the animation system, ensuring your player's on-screen actions always match their current state. Basically, it's the full package for bringing your hero to life!
"Player Controller Code Snippet"
PlayerController.cs " You're in Control! "
This script is the brain and brawn behind your player character in Dash & Slash, I built it as a Singleton, a robust design pattern that ensures there's only one of these crucial controllers in your game, making it a reliable hub for all things player-related. This comprehensive script handles everything: from responsive movement (both forward and backward with distinct speeds) and precise mouse-based aiming, to the firing of projectiles and the nuanced management of health and damage feedback. It's also tightly integrated with the animation system, ensuring your player's on-screen actions always match their current state. Basically, it's the full package for bringing your hero to life!
"Player Controller Code Snippet"
/// <summary>
/// Handles player shooting: fire rate, projectile spawning, animations.
/// </summary>
void Shoot()
{
/// <summary>
/// Checks if cooldown has passed.
/// </summary>
if (Time.time > nextFireTime)
{
/// <summary>
/// Sets next shot time.
/// </summary>
nextFireTime = Time.time + fireRate;
/// <summary>
/// Player is now shooting.
/// </summary>
isShooting = true;
/// <summary>
/// Spawns a projectile at the gun's barrel.
/// </summary>
Instantiate(projectilePrefab, projectileSpawnPoint.position
/// <summary>
/// Triggers shooting animation.
/// </summary>
animator.SetBool("IsShooting", true);
/// <summary>
/// Schedules 'ResetShooting' after cooldown.
/// </summary>
Invoke("ResetShooting", fireRate);
}
}
/// <summary>
/// Resets shooting state and animation.
/// </summary>
void ResetShooting()
{
/// <summary>
/// Player is no longer shooting.
/// </summary>
isShooting = false;
/// <summary>
/// Turns off shooting animation.
/// </summary>
if (animator != null)
{
animator.SetBool("IsShooting", false);
}
}
LaserController.cs: Know Your Aim!
You ever feel like you're shooting into the void? Not anymore! This little gem provides instant visual feedback, drawing a laser from the player's gun to precisely where the mouse cursor is aiming on the ground. It's a simpleLineRendererpaired with aRaycast, but it makes a huge difference in aiming precision and overall player experience.
LaserController.cs: Know Your Aim!
You ever feel like you're shooting into the void? Not anymore! This little gem provides instant visual feedback, drawing a laser from the player's gun to precisely where the mouse cursor is aiming on the ground. It's a simpleLineRendererpaired with aRaycast, but it makes a huge difference in aiming precision and overall player experience.
LaserController.cs: Know Your Aim!
You ever feel like you're shooting into the void? Not anymore! This little gem provides instant visual feedback, drawing a laser from the player's gun to precisely where the mouse cursor is aiming on the ground. It's a simpleLineRendererpaired with aRaycast, but it makes a huge difference in aiming precision and overall player experience.
using UnityEngine;
public class LaserController : MonoBehaviour
{
private LineRenderer lineRenderer; // Component to draw the laser line.
public Transform laserOrigin; // Point where the laser starts (e.g., gun barrel).
public float laserLength = 50f; // Max distance the laser can reach.
/// <summary>
/// Gets the LineRenderer component.
/// </summary>
void Start()
{
lineRenderer = GetComponent<LineRenderer>();
}
/// <summary>
/// Updates the laser position each frame.
/// </summary>
void Update()
{
// Set laser start point.
lineRenderer.SetPosition(0, laserOrigin.position);
// Get mouse screen position.
Vector3 mousePosition = Input.mousePosition;
// Adjust Z-coordinate for world conversion.
mousePosition.z = Camera.main.nearClipPlane;
// Convert screen to world coordinates.
Vector3 worldPosition = Camera.main.ScreenToWorldPoint(mousePosition);
RaycastHit hit; // Stores raycast hit info.
// Cast a ray from origin towards mouse aim.
// If it hits something within range.
if (Physics.Raycast(laserOrigin.position, (worldPosition - laserOrigin.position).normalized, out hit, laserLength))
{
// Set laser end to hit point.
lineRenderer.SetPosition(1, hit.point);
}
else
{
// If no hit, extend laser to max length.
lineRenderer.SetPosition(1, laserOrigin.position + (worldPosition - laserOrigin.position).normalized * laserLength);
}
}
}WallTransparency.cs: Never Lose Sight!
This script is all about enhancing the player's experience by making sure they're never hidden behind a wall. When the camera's view of the player is obstructed by an object tagged "Wall," that wall dynamically becomes transparent. It's a small detail that drastically improves visibility and prevents frustration.
WallTransparency.cs: Never Lose Sight!
This script is all about enhancing the player's experience by making sure they're never hidden behind a wall. When the camera's view of the player is obstructed by an object tagged "Wall," that wall dynamically becomes transparent. It's a small detail that drastically improves visibility and prevents frustration.
WallTransparency.cs: Never Lose Sight!
This script is all about enhancing the player's experience by making sure they're never hidden behind a wall. When the camera's view of the player is obstructed by an object tagged "Wall," that wall dynamically becomes transparent. It's a small detail that drastically improves visibility and prevents frustration.
using System.Collections;
using UnityEngine;
public class WallTransparency : MonoBehaviour
{
public Material transparentMaterial; // The material to apply when the wall is transparent
public Material originalMaterial; // The wall's original opaque material
private Renderer wallRenderer; // The Renderer component of the wall
private Transform player; // Reference to the player's transform
private bool isTransparent = false; // Flag to track the current state of the wall
/// <summary>
/// Start is called before the first frame update.
/// Initializes references to the wall's renderer and the player.
/// </summary>
void Start()
{
wallRenderer = GetComponent<Renderer>(); // Get the renderer component
originalMaterial = wallRenderer.material; // Store the original material
player = GameObject.FindGameObjectWithTag("Player").transform; // Find the player by tag
}
/// <summary>
/// Update is called once per frame.
/// Checks if the player is behind the wall and toggles transparency accordingly.
/// </summary>
void Update()
{
Vector3 playerPosition = player.position; // Get the player's current position
Vector3 wallPosition = transform.position; // Get the wall's current position
// Calculate the direction vector from the wall to the player, normalized
Vector3 directionToPlayer = (playerPosition - wallPosition).normalized;
RaycastHit hit; // Stores information about what the raycast hits
// Perform a raycast from the wall towards the player
if (Physics.Raycast(wallPosition, directionToPlayer, out hit))
{
// If the ray hits the player AND the wall isn't already transparent
if (hit.transform.CompareTag("Player") && !isTransparent)
{
SetTransparent(); // Make the wall transparent
}
// If the ray does NOT hit the player AND the wall IS transparent
else if (!hit.transform.CompareTag("Player") && isTransparent)
{
SetOpaque(); // Make the wall opaque again
}
}
}
/// <summary>
/// Sets the wall's material to the transparent material.
/// </summary>
void SetTransparent()
{
wallRenderer.material = transparentMaterial;
isTransparent = true;
}
/// <summary>
/// Sets the wall's material back to its original opaque material.
/// </summary>
void SetOpaque()
{
wallRenderer.material = originalMaterial;
isTransparent = false;
}
}EnemyBase, TurtleEnemy.cs, & MageEnemy.cs: AI with Personality!
All enemies in "Dash & Slash" inherit from anEnemyBaseclass , which handles common properties like health, movement speed, and attack rate. This allowed for code reusability and easy extension. On top of this, I created two distinct enemy types: a close-quarters brawler and a cunning ranged attacker.
This is the blueprint for all your baddies. It handles all the stuff every enemy needs: finding the player, managing health, moving with the NavMeshAgent, and setting up the basic attack loop. The cool part here is thevirtualkeyword on methods likeAttack()andUpdate(). This tells Unity, "Hey, any class inheriting from me can totally replace or extend what I do here!"
EnemyBase, TurtleEnemy.cs, & MageEnemy.cs: AI with Personality!
All enemies in "Dash & Slash" inherit from anEnemyBaseclass , which handles common properties like health, movement speed, and attack rate. This allowed for code reusability and easy extension. On top of this, I created two distinct enemy types: a close-quarters brawler and a cunning ranged attacker.
This is the blueprint for all your baddies. It handles all the stuff every enemy needs: finding the player, managing health, moving with the NavMeshAgent, and setting up the basic attack loop. The cool part here is thevirtualkeyword on methods likeAttack()andUpdate(). This tells Unity, "Hey, any class inheriting from me can totally replace or extend what I do here!"
EnemyBase, TurtleEnemy.cs, & MageEnemy.cs: AI with Personality!
All enemies in "Dash & Slash" inherit from anEnemyBaseclass , which handles common properties like health, movement speed, and attack rate. This allowed for code reusability and easy extension. On top of this, I created two distinct enemy types: a close-quarters brawler and a cunning ranged attacker.
This is the blueprint for all your baddies. It handles all the stuff every enemy needs: finding the player, managing health, moving with the NavMeshAgent, and setting up the basic attack loop. The cool part here is thevirtualkeyword on methods likeAttack()andUpdate(). This tells Unity, "Hey, any class inheriting from me can totally replace or extend what I do here!"
public class EnemyBase : MonoBehaviour
{
// Common enemy properties shared by all derived classes
public Transform player; // Target (usually the player).
public float speed = 2f; // Movement speed.
public float attackDistance = 1.5f; // Range to initiate attack.
public float lookRadius = 10f; // How far enemy detects player.
public float attackRate = 1f; // Cooldown between attacks.
protected float nextAttackTime = 0f;// When next attack is allowed.
// Core Unity components.
protected Animator animator; // For animations.
protected NavMeshAgent agent; // For pathfinding.
public float maxHealth = 100f; // Max HP.
protected float currentHealth; // Current HP.
/// <summary>
/// Initializes common enemy components and finds the player.
/// Marked 'virtual' so derived classes can extend it.
/// </summary>
protected virtual void Start()
{
// Finds player if not set.
if (player == null) GameObject.FindGameObjectWithTag("Player").transform;
// Gets components.
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
currentHealth = maxHealth;
}
/// <summary>
/// Handles enemy behavior logic each frame.
/// Marked 'virtual' for specific enemy overrides.
/// </summary>
protected virtual void Update()
{
// Check for player within lookRadius.
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
if (distanceToPlayer <= lookRadius)
{
// Move towards player via NavMesh.
if (agent.isOnNavMesh) agent.SetDestination(player.position);
// Check if ready to attack.
if (distanceToPlayer <= attackDistance && Time.time >= nextAttackTime)
{
Attack(); // Calls the specific attack.
nextAttackTime = Time.time + attackRate;
}
UpdateAnimator(); // Updates animation states.
}
}
/// <summary>
/// Placeholder for enemy-specific attack logic.
/// Must be overridden by derived classes.
/// </summary>
protected virtual void Attack() { }
/// <summary>
/// Handles enemy taking damage. Triggers death if health runs out.
/// </summary>
public void TakeDamage(float damage)
{
currentHealth -= damage;
if (currentHealth <= 0) Die();
else if (animator != null) animator.SetTrigger("IsGettingHit");
}
/// <summary>
/// Handles enemy death sequence.
/// Marked 'virtual' for specific enemy death behaviors.
/// </summary>
protected virtual void Die()
{
if (animator != null) animator.SetBool("IsDead", true);
enabled = false; // Disable script.
// Call ScoreManager.Instance.AddKill(GetType().Name); (in actual code)
StartCoroutine(RemoveAfterAnimation(animator.GetCurrentAnimatorStateInfo(0).length));
}
// Coroutine for removing GameObject after animation.
public IEnumerator RemoveAfterAnimation(float waitTime)
{
yield return new WaitForSeconds(waitTime);
Destroy(gameObject);
}
}TurtleEnemy.cs: The Melee Powerhouse
TheTurtleEnemyis your in-your-face brawler. It inherits directly fromEnemyBaseand overrides theUpdate()andAttack()methods to define its close-quarters combat style. It just wants to get close and smash!
TurtleEnemy.cs: The Melee Powerhouse
TheTurtleEnemyis your in-your-face brawler. It inherits directly fromEnemyBaseand overrides theUpdate()andAttack()methods to define its close-quarters combat style. It just wants to get close and smash!
TurtleEnemy.cs: The Melee Powerhouse
TheTurtleEnemyis your in-your-face brawler. It inherits directly fromEnemyBaseand overrides theUpdate()andAttack()methods to define its close-quarters combat style. It just wants to get close and smash!
public class EnemyBase : MonoBehaviour
{
// Common enemy properties shared by all derived classes
public Transform player; // Target (usually the player).
public float speed = 2f; // Movement speed.
public float attackDistance = 1.5f; // Range to initiate attack.
public float lookRadius = 10f; // How far enemy detects player.
public float attackRate = 1f; // Cooldown between attacks.
protected float nextAttackTime = 0f;// When next attack is allowed.
// Core Unity components.
protected Animator animator; // For animations.
protected NavMeshAgent agent; // For pathfinding.
public float maxHealth = 100f; // Max HP.
protected float currentHealth; // Current HP.
/// <summary>
/// Initializes common enemy components and finds the player.
/// Marked 'virtual' so derived classes can extend it.
/// </summary>
protected virtual void Start()
{
// Finds player if not set.
if (player == null) GameObject.FindGameObjectWithTag("Player").transform;
// Gets components.
agent = GetComponent<NavMeshAgent>();
animator = GetComponent<Animator>();
currentHealth = maxHealth;
}
/// <summary>
/// Handles enemy behavior logic each frame.
/// Marked 'virtual' for specific enemy overrides.
/// </summary>
protected virtual void Update()
{
// Check for player within lookRadius.
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
if (distanceToPlayer <= lookRadius)
{
// Move towards player via NavMesh.
if (agent.isOnNavMesh) agent.SetDestination(player.position);
// Check if ready to attack.
if (distanceToPlayer <= attackDistance && Time.time >= nextAttackTime)
{
Attack(); // Calls the specific attack.
nextAttackTime = Time.time + attackRate;
}
UpdateAnimator(); // Updates animation states.
}
}
/// <summary>
/// Placeholder for enemy-specific attack logic.
/// Must be overridden by derived classes.
/// </summary>
protected virtual void Attack() { }
/// <summary>
/// Handles enemy taking damage. Triggers death if health runs out.
/// </summary>
public void TakeDamage(float damage)
{
currentHealth -= damage;
if (currentHealth <= 0) Die();
else if (animator != null) animator.SetTrigger("IsGettingHit");
}
/// <summary>
/// Handles enemy death sequence.
/// Marked 'virtual' for specific enemy death behaviors.
/// </summary>
protected virtual void Die()
{
if (animator != null) animator.SetBool("IsDead", true);
enabled = false; // Disable script.
// Call ScoreManager.Instance.AddKill(GetType().Name); (in actual code)
StartCoroutine(RemoveAfterAnimation(animator.GetCurrentAnimatorStateInfo(0).length));
}
// Coroutine for removing GameObject after animation.
public IEnumerator RemoveAfterAnimation(float waitTime)
{
yield return new WaitForSeconds(waitTime);
Destroy(gameObject);
}
}MageEnemy.cs The Ranged Tactician
This cunning foe takes a different approach [cite: uploaded:MageEnemy.cs]. TheMageEnemyalso inherits fromEnemyBase, but it overridesUpdate()andAttack()to focus on ranged combat. It'll keep its distance, fire projectiles, and strategically stop moving when it's ready to attack.
using System.Collections; // For Coroutines.
public class MageEnemy : EnemyBase
{
public float attackRange = 5f; // Optimal distance for ranged attack.
public GameObject projectilePrefab; // Projectile to spawn.
public Transform projectileSpawnPoint; // Where projectiles emerge.
public float attackDamage = 15f; // Damage per projectile.
private bool isAttacking = false; // True when in attack animation.
/// <summary>
/// Overrides base Update for ranged attack logic.
/// </summary>
protected override void Update()
{
if (currentHealth <= 0) { Die(); return; } // Handles death early.
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// If in range and not attacking, initiate attack.
if (distanceToPlayer <= attackRange && !isAttacking)
{
if (Time.time >= nextAttackTime)
{
Attack(); // Calls the overridden Attack method.
nextAttackTime = Time.time + attackRate;
}
StopMoving(); // Stops movement while attacking.
}
else if (!isAttacking)
{
base.Update(); // Continues base movement (chasing).
}
}
/// <summary>
/// Implements specific ranged attack logic.
/// </summary>
protected override void Attack()
{
Debug.Log("Mage attacks!");
// Spawns projectile, setting its direction and damage.
GameObject projectile = Instantiate(projectilePrefab, projectileSpawnPoint.position, Quaternion.LookRotation(player.position - transform.position));
projectile.GetComponent<ProjectileMoverEnemy>().damage = attackDamage;
// Triggers attack animation and manages cooldown via Coroutine.
if (animator != null)
{
animator.SetBool("IsAttacking", true);
isAttacking = true;
StartCoroutine(ResetAttackAnimation(animator.GetCurrentAnimatorStateInfo(0).length));
}
}
/// <summary>
/// Coroutine to reset attack animation state.
/// </summary>
private IEnumerator ResetAttackAnimation(float waitTime)
{
yield return new WaitForSeconds(waitTime); // Waits for animation to complete.
if (animator != null) animator.SetBool("IsAttacking", false);
isAttacking = false; // Resets attacking flag.
// Resume movement if player is out of range.
if (Vector3.Distance(transform.position, player.position) > attackRange) animator.SetBool("IsRunning", true);
}
/// <summary>
/// Stops mage movement and sets idle animation.
/// </summary>
private void StopMoving()
{
agent.isStopped = true;
animator.SetBool("IsRunning", false);
animator.SetBool("IsIdle", true);
}
/// <summary>
/// Overrides base Die for mage-specific death.
/// </summary>
protected override void Die()
{
if (animator != null) animator.SetBool("IsDead", true);
enabled = false; agent.isStopped = true; // Disable components.
foreach (Renderer renderer in GetComponentsInChildren<Renderer>()) renderer.enabled = false;
StartCoroutine(RemoveAfterAnimation(animator.GetCurrentAnimatorStateInfo(0).length));
}
}The Final Word (a.k.a. Outcome)
The project turned out awesome. I ended up with a fully functional rogue-like that had all the core features I wanted. The Cellular Automata approach was a huge success, making every run feel unique. The AI, powered by a NavMeshAgent and FSM, kept the enemies challenging, and the homing projectiles added a fun layer of depth.
This project was a masterclass for me in procedural generation, AI, and the absolute importance of testing and iteration. I'm hyped to keep building on it!
The project turned out awesome. I ended up with a fully functional rogue-like that had all the core features I wanted. The Cellular Automata approach was a huge success, making every run feel unique. The AI, powered by a NavMeshAgent and FSM, kept the enemies challenging, and the homing projectiles added a fun layer of depth.
This project was a masterclass for me in procedural generation, AI, and the absolute importance of testing and iteration. I'm hyped to keep building on it!
The project turned out awesome. I ended up with a fully functional rogue-like that had all the core features I wanted. The Cellular Automata approach was a huge success, making every run feel unique. The AI, powered by a NavMeshAgent and FSM, kept the enemies challenging, and the homing projectiles added a fun layer of depth.
This project was a masterclass for me in procedural generation, AI, and the absolute importance of testing and iteration. I'm hyped to keep building on it!
