I’m back, an am working on a refactoring of my enemies. I only have two right now, but plan for many more, and I want whatever system I put in place to be scalable and using industry standards (as much as reasonable). I have a few goals of the refactoring:
- Break enemies down into components, e.g. health management, attack pattern, etc.
- Reduce inheritance. Breaking the enemies down will effectively do that.
- Use
ScriptableObjectsfor static enemy data. - Spawn projectiles from enemies from a child game object, rather than the enemy game object. This will allow multiple, customizable firing points on a future enemy.
However, as with any refactoring, I’m a bit paralyzed in the best approach when considering future enemies that I want to add.
ScriptableObject Paralysis
My first area of contention is regarding the ScriptableObjects. Currently I have a base EnemyData class with some very common fields for an enemy:
namespace Enemies
{
public class EnemyData : ScriptableObject
{
[SerializeField] private string displayName;
[SerializeField] private int health;
[SerializeField] private float moveSpeed;
public string GetDisplayName => displayName;
public int GetHealth => health;
public float GetMoveSpeed => moveSpeed;
}
}
From there, I have ChopperData which can apply to both my green and blue Chopper enemies:
namespace Enemies
{
[CreateAssetMenu(fileName = "ChopperData", menuName = "Enemies/Chopper")]
public class ChopperData : EnemyData
{
[SerializeField] private Projectile projectile;
[SerializeField] private float fireRateLowerBound;
[SerializeField] private float fireRateUpperBound;
protected float GetTimeUntilNextShot()
{
return Random.Range(fireRateLowerBound, fireRateUpperBound);
}
protected Projectile CreateProjectile(Transform transform, Quaternion rotation = default)
{
return Instantiate(projectile, transform.position, rotation, transform);
}
}
}
However, I have a couple issues with this:
ChopperDataseems generic enough for any enemy that files a single projectile within an interval.- Blue choppers, specifically, fire a cluster of shots in succession. Ideally, that’s configurable in the
ScriptableObject, but I’m unsure whether to…- … extend
ChopperData(or whatever I rename it to) to add the additional configurable? Starting to get tied to inheritance again. - … create a new extension of
EnemyDatathat has the same fields asChopperData, with additional fields? Duplicate fields. - … add the additional fields to
ChopperData? For example,shotClusterSize = 1for green Choppers, and 3 for blue. (leaning towards this option)
- … extend
- Additional configuration for enemies that do spread shots, or don’t fire on intervals, instead triggering on something else (e.g. clamshell enemies only shoot when open).
MonoBehaviour Paralysis
When breaking down the enemies into component MonoBehaviours, I am also a bit paralyzed.
- Should the enemy health management component handle what happens when an enemy gets hit or dies? Or should I have a separate animation component?
- When an enemy dies, I need it to stop attacking… so there has to be communication between components. I guess the attack component needs to subscribe to an event invoked from the health component?
- Same for the animation component if I separate it?
- The attack component will probably be on the child game object representing the weapon (projectile spawn point). Will those pose an issue trying to subscribe to events on components of the parent? Will this become an ugly mess?
I think I’m arriving at a reasonable solution here, and some of the questions above are a bit rhetorical, but just to give an idea of what’s going through my head. This also meshes with the ScriptableObject paralysis above and creating just a general state of paralysis to it all.
I should be able to break the paralysis by next weekend. For now, not much to add.
Leave a comment