Een retro arcade-ervaring, opnieuw opgebouwd met moderne Unity technieken.
Een afgerond project gebaseerd op Galaga, waarin ik een Scriptable Object en het event-systeem heb geïmplementeerd. Dit project heb ik aan het begin van mijn schoolcarrière gemaakt als een manier om de basis van Unity en C# onder de knie te krijgen.
Met de ervaring die ik inmiddels heb opgedaan, zie ik hoe de code anders en efficiënter had kunnen worden geschreven, maar het blijft een waardevol leerproject.
In deze vroege versie van de remake heeft de speler nog een 'God class' structuur. Het beheert beweging, schieten en schade in één script.
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;
[Space][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);
}
}
}
}
Door gebruik te maken van ScriptableObjects kon ik variabelen zoals snelheid eenvoudig aanpassen buiten de scripts om.
using UnityEngine;
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/PlayerManager", order = 1)]
public class PlayerManager : ScriptableObject
{
public float moveSpeed = 2;
public float bulletSpeed = 20;
}
De vijanden maken ook gebruik van ScriptableObjects voor hun data en communiceren via Unity Events voor score-updates.
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);
}
}
public void ScoreDead()
{
UiManager.Instance.EnemyDeathScoreRecieved(UiManager.Instance.enemyDeathScore);
}
}
}
using UnityEngine;
[CreateAssetMenu(fileName = "Data", menuName = "ScriptableObjects/EnemyManager", order = 1)]
public class EnemyManager : ScriptableObject
{
public float maxHealth = 10;
public float moveSpeed = 2;
}
Een eenvoudige spawner die vijanden op willekeurige posities genereert met behulp van een Coroutine.
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);
}
}
}
}
De GameManager is een Singleton die de levens van de speler, scores en respawning beheert.
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();
}
}
}