Unity 2D ScriptableObjects Unity Events šŸŽ® Play on itch.io

Galaga Remake

A classic 1981 arcade space shooter experience, engineered in Unity using ScriptableObject data containers and event-driven architecture.

šŸ‘¾

Play Galaga Remake

Experience the classic space shooter rebuild directly in your browser on itch.io.

šŸš€ Launch Game on itch.io

About the Project

Unity 2D C# ScriptableObjects Singleton Pattern

This project is an early-curriculum Galaga arcade remake created to master Unity 2D physics, UnityEvent communications, and ScriptableObject data decoupling.

While reflective of early architectural patterns, it established essential game development fundamentals including Coroutine wave spawners, interface damage handling, and persistent score systems.

Player Behaviour & Controls

The PlayerBehaviour script manages player 2D horizontal input movement, bullet instantiation via RigidBody2D impulses, and damage handling through the IDamagable interface.

PlayerBehaviour.cs
C#
namespace com.GLU.GD.P5.ScriptableObjects
{
    public class PlayerBehaviour : MonoBehaviour, IDamagable
    {
        private float health = 1;
        public float Health => health;
        public PlayerManager playerManager;

        [SerializeField] private UnityEvent PlayerDied;

        public AudioSource DeathAudio;
        public AudioSource shootingAudio;
        
        [Space]
        [SerializeField] private GameObject bullet;
        [SerializeField] private Transform bulletSpawnOrigin;
        [SerializeField] private GameObject explosionEffect;

        private void Start()
        { 
            PlayerDied.AddListener(GameManager.Instance.PlayerIsDead);
        }

        void Update()
        {
            float horizontalInput = Input.GetAxis("Horizontal");
            transform.position += Vector3.right * (horizontalInput * playerManager.moveSpeed * Time.deltaTime);

            if (Input.GetButtonDown("Fire1"))
            {
                shootingAudio.Play();
                GameObject instanciatedBullet = Instantiate(bullet, bulletSpawnOrigin.position, Quaternion.identity);
                Rigidbody2D bulletRigidbody = instanciatedBullet.GetComponent();
                bulletRigidbody.AddForce(Vector2.up * playerManager.bulletSpeed, ForceMode2D.Impulse);
            }
        }

        public void DoDamage(float damage, IDamagable other)
        {
            health -= damage;
            if (health == 0)
            {
                PlayerDied.Invoke();
                DeathAudio.Play();
                Destroy(this.gameObject);
                other.DoDamage(float.PositiveInfinity, this);
                Instantiate(explosionEffect, transform.position, Quaternion.identity);
            }
        }
    }
}

Player Data (ScriptableObject)

Utilizing ScriptableObjects decouples tuning parameters (movement speed, bullet velocity) from code execution, enabling designer-friendly tweaking in the Unity Inspector.

PlayerManager.cs
C#
using UnityEngine;

[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/PlayerManager", order = 1)]
public class PlayerManager : ScriptableObject
{
    public float moveSpeed = 2;
    public float bulletSpeed = 20;
}

Enemy Behaviour & Events

Enemy entities utilize Coroutines for descent movements and communicate score increases to the UI via UnityEvent callbacks.

EnemyBehaviour.cs
C#
namespace com.GLU.GD.P5.ScriptableObjects
{
    public class EnemyBehaviour : MonoBehaviour, IDamagable
    {
        private float health;
        public float Health => health;

        public EnemyManager enemyManager;
        [SerializeField] private UnityEvent EnemyDied;
        [SerializeField] private UnityEvent ScoreUpdate;
        [SerializeField] private GameObject explosionEffect;
        
        void Start()
        {
            health = enemyManager.maxHealth;
        }

        void Update()
        {
            StartCoroutine(EnemyMoverCoroutine());
        }

        public IEnumerator EnemyMoverCoroutine()
        {
            yield return new WaitForSeconds(1.5f);
            transform.position += Vector3.down * (enemyManager.moveSpeed * Time.deltaTime);
        }

        public void DoDamage(float damage, IDamagable other)
        {
            health -= damage;
            if (health <= 0)
            {
                ScoreUpdate.Invoke();
                Destroy(this.gameObject);
                Instantiate(explosionEffect, transform.position, Quaternion.identity);
            }
        }
    }
}

Enemy Data (ScriptableObject)

EnemyManager.cs
C#
using UnityEngine;

[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/EnemyManager", order = 1)]
public class EnemyManager : ScriptableObject
{
    public float maxHealth = 10;
    public float moveSpeed = 2;
}

Wave Enemy Spawner

A Coroutine-driven wave spawner that continuously instantiates enemy prefabs at randomized spawn anchor points.

EnemySpawner.cs
C#
namespace com.GLU.GD.P5.ScriptableObjects
{
    public class EnemySpawner : MonoBehaviour
    {
        private bool spawningTimer = true;

        [SerializeField] private GameObject enemy;
        [SerializeField] private Transform[] spawningPointsEnemy;

        private void Start()
        {
            StartCoroutine(EnemySpawnerCoroutine());
        }

        public IEnumerator EnemySpawnerCoroutine()
        {
            while (spawningTimer)
            {
                Instantiate(enemy, spawningPointsEnemy[Random.Range(0,spawningPointsEnemy.Length)].position, Quaternion.identity);
                yield return new WaitForSeconds(3.5f);
            }
        }
    }
}

Game Manager Singleton

The persistent GameManager Singleton tracks player lives, high scores across scenes, and triggers player respawning workflows.

GameManager.cs
C#
namespace com.GLU.GD.P5.ScriptableObjects
{
    public class GameManager : MonoBehaviour
    {
        public static GameManager Instance;

        public int PlayerLives = 3;
        public float RespawnTimer = 1f;
        public int HighScoreBeforeDeath;
        public int SceneToLoadAfterNoLive;

        private void Awake()
        {
            if (Instance != this && Instance != null)
            {
                Object.Destroy(this.gameObject);
            }
            else
            {
                DontDestroyOnLoad(this.gameObject);
                Instance = this;
            }
        }

        private void Update()
        {
            if (UiManager.Instance.GameScore > UiManager.Instance.HighScore)
            {
                UiManager.Instance.HighScore = UiManager.Instance.GameScore;
            }
        }

        public void PlayerIsDead()
        {
            PlayerLives--;
            HighScoreBeforeDeath += UiManager.Instance.HighScore;

            if (PlayerLives > 0)
            {
                Invoke("RespawnPlayer", RespawnTimer);
            }
            if (PlayerLives <= 0)
            {
                SceneManager.LoadScene(SceneToLoadAfterNoLive);
            }
        }

        public void RespawnPlayer()
        {
            SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
            UiManager.Instance.HighScore += HighScoreBeforeDeath;
            UiManager.Instance.UpdatingUi();
        }
    }
}