← Back to Projects

BackyardTD

BackyardTD is a 2D Tower Defense game where the goal is to attack the evil plants that are trying to attack your base. Your army consists of different animals that you can buy with your starting capital. As you kill the plants, you get more money to spend so you can buy multiple agents to get a bigger army making you undefeatable.

C#GamesUnityWindowsTower Defense

Project Info

Engine:
Unity Engine
Language:
C#
Time Spent:
10 days
Reason:
School Project
# of Levels:
2 Levels

My Contributions

  • First time creating a game on my own.
  • Made the UI.
  • Implemented features.
  • Designed the levels.
  • Tilemap assets from free pack.

Design

I’ve used a Tilemap to draw out the map and set a path up for the enemies to walk over. There are waypoints for the enemies and borders to prevent the “towers” to be placed. The UI is made using Canvases and Buttons with a very transparent background.

Asset Credits: Tilemap: Kittens and Elves at Work Frog Ninja & Pink Man: Pixel Frog Piranha Plants: Ansimuz

Towers: Idling/Attacking

The towers will play the idle animation until enemies walk into their collider. Whenever that happens, the tower locks onto that target, plays the attack animation and starts attacking the enemy.

Frog Ninja and Pink Guy Attacking

TowerAttack.cs (csharp)

TowerAttack.cs (csharp)
public class TowerAttack : MonoBehaviour
{
    private GameObject target;
    [SerializeField] private bool canAttack = true;
    [SerializeField] private float damage = 5f;
    [SerializeField] private float attackCooldown = 0.8f;
    private Animator animator;
    private CircleCollider2D circleCollider;

    void Start()
    {
        animator = GetComponent<Animator>();
        circleCollider = GetComponent<CircleCollider2D>();
        circleCollider.enabled = true;
    }


    void OnTriggerStay2D(Collider2D other)
    {
        if (other.CompareTag("Enemy") == true && canAttack == true)
        {
            target = other.gameObject;
            Attack();
        }
    }

    void Attack()
    {
        if (canAttack == true)
        {
            canAttack = false;
            animator.SetTrigger("Attack");
            target.GetComponent<EnemyHealth>().TakeDmg(damage);
            StartCoroutine(Cooldown());
        }
    }

    IEnumerator Cooldown()
    {
        yield return new WaitForSeconds(attackCooldown);
        canAttack = true;
    }
}

Towers: Spawning + Shop + Balance

Whenever you buy a tower in the shop, it’ll follows your mouse position until you place it. Whenever you touch a border, you cannot place it and the tower will turn red. When you place the tower, the money will be deducted from your balance and it will start it’s idle animation.

Tower Spawning

TowerSpawn.cs (csharp)

TowerSpawn.cs (csharp)
public class TowerSpawn : MonoBehaviour
{
    // Note: Some variables like sprite, playerCurrency, mousePositionManager, red, normal, radiusRenderer, collisionCheck are assumed to be defined elsewhere or handled by Unity Inspector
    private SpriteRenderer sprite;
    private PlayerCurrency playerCurrency;
    private MousePositionManager mousePositionManager;
    private Color red;
    private Color normal;
    private bool isPlaced = false;
    private bool collisionCheck = false; // Assuming this is set by collision events
    private SpriteRenderer radiusRenderer; // Assuming this is assigned

    private void Start()
    {
        sprite = GetComponent<SpriteRenderer>();
        playerCurrency = GameObject.Find("Player").GetComponent<PlayerCurrency>();
        mousePositionManager = GameObject.Find("Player").GetComponent<MousePositionManager>();
        red = new Color(1f, 0.3f, 0.3f, 1f);
        normal = new Color(1f, 1f, 1f, 1f);
        transform.position = Vector3.zero;
        // Assuming radiusRenderer is assigned here or in Inspector
        radiusRenderer = GetComponentInChildren<SpriteRenderer>(); // Example assignment
        if (radiusRenderer) radiusRenderer.enabled = true;
    }
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (collisionCheck == false)
            {
                isPlaced = true;
            }
        }

        if (isPlaced == false)
        {
            transform.position = mousePositionManager.newPos;
            // Simplified color change logic - needs collision detection implementation
            // sprite.color = collisionCheck ? red : normal;
        }

        if (isPlaced == true && collisionCheck == false)
        {
            if (radiusRenderer) radiusRenderer.enabled = false;
            gameObject.GetComponent<TowerAttack>().enabled = true;
            gameObject.GetComponent<Animator>().enabled = true;
            DeductBalance();
            Destroy(this); // Destroy the spawn script component
        }
    }

    void DeductBalance()
    {
        // Simplified balance check - assumes playerCurrency exists and works
        if (gameObject.name.Contains("Frog Tower") && playerCurrency.Balance >= 50)
        {
            playerCurrency.NFT_NoStonks(); // Assumes this method exists
        }
        else if (gameObject.name.Contains("Pink Man Tower") && playerCurrency.Balance >= 125)
        {
            playerCurrency.PMT_NoStonks(); // Assumes this method exists
        }
        else if ((gameObject.name.Contains("Frog Tower") && playerCurrency.Balance < 50) || (gameObject.name.Contains("Pink Man Tower") && playerCurrency.Balance < 125))
        {
            Destroy(gameObject); // Destroy the tower object if not enough balance
        }
    }

    // Need collision detection methods (OnTriggerEnter2D, OnTriggerExit2D)
    // to set collisionCheck and change sprite color based on borders
}

Enemies: Spawning + Waves System + Damage

When you click the [Start First Wave] button, the Enemy script (which is actually the Waves System/Game Manager) randomly assigns the amount of waves and then starts the first wave. While the countdown happens for the wave to start, the NewWave() function randomly assigns the amount of enemies that will spawn with random health, speed and damage that it does to the player if you fail to kill it.

Enemy Spawning

Enemy.cs (Wave/Game Manager) (csharp)

Enemy.cs (Wave/Game Manager) (csharp)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro; // Assuming TextMeshPro is used

public class Enemy : MonoBehaviour // Renaming suggestion: WaveManager or GameManager
{
    public static List<GameObject> enemies; // Static list to track enemies
    private bool endWave = false;
    private bool startGame = false;
    private int wavesLeft;
    private int enemyCount;
    [SerializeField] private GameObject enemyPrefab; // Assign enemy prefab in Inspector
    [SerializeField] private Transform spawnPoint; // Assign spawn point in Inspector
    [SerializeField] private Transform[] waypoints; // Assign waypoints in Inspector
    [SerializeField] private float delay = 0.5f; // Delay between spawns
    [SerializeField] private GameObject startFirstWaveButton; // Assign button in Inspector
    [SerializeField] private GameObject wcGameObject; // Assign Wave Countdown UI object
    [SerializeField] private TextMeshProUGUI wcText; // Assign Wave Countdown Text
    // Assuming a Texts struct/class exists and is assigned
    // [SerializeField] private Texts texts;

    void Awake()
    {
        enemies = new List<GameObject>();
        endWave = false;
        if (wcGameObject) wcGameObject.SetActive(false); // Hide countdown initially
        // Assuming texts are assigned elsewhere or via Inspector
        // if (texts?.wavesText) texts.wavesText.enabled = false;
        // if (texts?.enemyText) texts.enemyText.enabled = false;
    }

    void Update()
    {
        if (startGame == true)
        {
            if (enemies.Count == 0 && endWave == false)
            {
                endWave = true;
                if (wavesLeft <= 0) // Check if wavesLeft is 0 or less
                {
                    Debug.Log("All waves cleared!");
                    SceneManager.LoadScene("Wave Cleared"); // Ensure this scene exists
                }
                else
                {
                    // wavesLeft is already decremented before calling NewWave
                    NewWave();
                }
            }
        }
    }

    public void StartFirstWave()
    {
        if (startFirstWaveButton) startFirstWaveButton.SetActive(false);
        wavesLeft = Random.Range(2, 5);
        // if (texts?.wavesText) texts.wavesText.enabled = true;
        // if (texts?.enemyText) texts.enemyText.enabled = true;
        Debug.Log($"Starting Game with {wavesLeft + 1} waves."); // +1 because wavesLeft is decremented before first wave starts
        NewWave(); // Start the first wave process
    }

    void NewWave()
    {
        enemyCount = Random.Range(2, 7);
        Debug.Log($"Starting Wave {wavesLeft}. Spawning {enemyCount} enemies.");
        startGame = true; // Ensure game logic runs
        StartCoroutine(CountdownNextWave());
    }

    IEnumerator Spawn()
    {
        if (wcGameObject) wcGameObject.SetActive(false);
        Transform parentTransform = spawnPoint ? spawnPoint : transform; // Use spawnPoint or this object's transform

        for (int i = 0; i < enemyCount; i++)
        {
            if (enemyPrefab)
            {
                GameObject instenemy = Instantiate(enemyPrefab, parentTransform.position, Quaternion.identity);
                EnemyMove moveScript = instenemy.GetComponent<EnemyMove>();
                if (moveScript)
                {
                    moveScript.Waypoints(waypoints);
                    // Assign random stats here if needed, e.g.:
                    // instenemy.GetComponent<EnemyHealth>().InitializeHealth(Random.Range(10f, 25f));
                }
                enemies.Add(instenemy);
            }
            yield return new WaitForSeconds(delay);
        }
        wavesLeft--; // Decrement wavesLeft *after* spawning for the current wave
        endWave = false; // Ready for next wave check
    }

    IEnumerator CountdownNextWave()
    {
        if (wcGameObject && wcText)
        {
            wcGameObject.SetActive(true);
            for (int i = 5; i > 0; i--)
            {
                wcText.SetText($"Wave starts in: 
{i}
 Seconds.");
                yield return new WaitForSeconds(1);
            }
        } else {
             yield return new WaitForSeconds(1); // Default wait if no UI
        }
        StartCoroutine(Spawn());
    }
}

Enemies: Movement

The Enemies follow waypoints (white dots which are referenced in the EnemyMove() script) to navigate through the map. When the enemy reaches the end, the player takes damage.

Enemy Waypoints

EnemyMove.cs (csharp)

EnemyMove.cs (csharp)
using UnityEngine;

public class EnemyMove : MonoBehaviour
{
    private Transform[] waypoints;
    private int waypointIndex = 0;
    private float moveSpeed; // Set externally or randomized in Start
    private PlayerHealth player; // Assign player health script reference
    private float dmg = 1; // Damage dealt by this enemy

    void Start()
    {
        player = GameObject.Find("Player")?.GetComponent<PlayerHealth>(); // Find player health script
        // Initialize speed, potentially randomized
        moveSpeed = Random.Range(1f, 3f);
        // Ensure waypoints are assigned before trying to access them
        if (waypoints != null && waypoints.Length > 0)
        {
             transform.position = waypoints[waypointIndex].position;
        } else {
             Debug.LogError("Waypoints not assigned to EnemyMove script on " + gameObject.name);
             // Optionally destroy the enemy if waypoints are missing
             // Destroy(gameObject);
        }
    }

    void Update()
    {
        if (waypoints != null && waypoints.Length > 0) // Check if waypoints exist
        {
            Move();
        }
    }

    // Call this from the spawner to assign waypoints
    public void Waypoints(Transform[] wps)
    {
        waypoints = wps;
        // Set initial position after waypoints are assigned
        if (waypoints != null && waypoints.Length > 0)
        {
             transform.position = waypoints[waypointIndex].position;
        }
    }

    void Move()
    {
        if (waypointIndex >= waypoints.Length) return; // Already reached the end

        // Move towards the current waypoint
        transform.position = Vector3.MoveTowards(transform.position, waypoints[waypointIndex].position, moveSpeed * Time.deltaTime);

        // Check if the waypoint is reached
        if (transform.position == waypoints[waypointIndex].position)
        {
            waypointIndex++;
        }

        // Check if the end is reached
        if (waypointIndex == waypoints.Length)
        {
            Enemy.enemies?.Remove(gameObject); // Safely remove from static list
            player?.TakeDmg(dmg); // Damage player if script found
            Destroy(gameObject);
        }
    }
}

Enemies: Health

Whenever the enemy gets hit by a tower, the enemy loses health. When the enemy is out of health, it dies.. that’s essentially it.

Enemy Health/Waypoints

EnemyHealth.cs (csharp)

EnemyHealth.cs (csharp)
using UnityEngine;

public class EnemyHealth : MonoBehaviour
{
    [SerializeField] private float maxHealth = 10f; // Use maxHealth for clarity
    private float currentHealth;
    [SerializeField] private Transform healthbar; // Assign health bar transform in Inspector
    private PlayerCurrency playerCurrency;
    private float initialHealthbarScaleX; // Store initial scale

    void Start()
    {
        currentHealth = maxHealth; // Start with full health
        playerCurrency = GameObject.Find("Player")?.GetComponent<PlayerCurrency>(); // Safely find currency script

        // Store initial health bar scale if assigned
        if (healthbar != null)
        {
            initialHealthbarScaleX = healthbar.localScale.x;
        } else {
            Debug.LogWarning("Healthbar transform not assigned to EnemyHealth on " + gameObject.name);
        }
        UpdateHealthBar(); // Set initial health bar size
    }

    public void InitializeHealth(float healthValue) {
        maxHealth = healthValue;
        currentHealth = maxHealth;
        UpdateHealthBar();
    }

    public void TakeDmg(float damage)
    {
        currentHealth -= damage;
        UpdateHealthBar();

        if (currentHealth <= 0)
        {
            Die();
        }
    }

    void UpdateHealthBar()
    {
        if (healthbar != null)
        {
            float healthPercentage = currentHealth / maxHealth;
            Vector3 scale = healthbar.localScale;
            scale.x = initialHealthbarScaleX * healthPercentage;
            healthbar.localScale = scale;
        }
    }

    void Die()
    {
        Enemy.enemies?.Remove(gameObject); // Safely remove from static list
        playerCurrency?.Stonks(); // Grant currency if script found
        Destroy(gameObject);
    }
}