Implementation of generic abilities system

Hi guys,
I’m working on a turn-based strategy game, in which main gameplay loop is about ship battles. I would like to add some special abilities for each ship. So let’s say HeavyShip would be able to shoot from two sides of the ship at once, while FastShip would shoot with a special type of cannonballs to damage opponents sails and slow it down. But I don’t know how to do it technically.

So, right now I have Ship mono behaviour containing the logic of the ship, and ShipAttributes scriptable object containing attributes of the ship, like HP, max speed, number of cannons etc. So my idea was to add some information about special abilities to ShipAttributes. I know I can add some enum with whole special abilities, but having some one type containing information about all special abilities in the game doesn’t seems right to me. How would you tackle this issue?

Thanks for all the help.

1 Like

It’s a good habit in programming to never attempt to resolve problems you don’t yet have. This is why I would advise to consider component & interface-based composition where every component hosts it’s own data. This is simple and works fine - you can always refactor it later once you know the game requirement space better and it’s decent default solution.

This is modularity where ships would be defined in prefabs. Here is an example set:

NavalUnit.cs

using System.Collections.Generic;
using UnityEngine;

public class NavalUnit : MonoBehaviour
{
    public INavalHealth health { get; private set; }
    public INavalNavigation navigation { get; private set; }
    public List<INavalGun> guns { get; private set; } = new ();// public, so UI/etc systems can inspect them

    void OnEnable ()
    {
        health = GetComponentInChildren<INavalHealth>( includeInactive:false );
        navigation = GetComponentInChildren<INavalNavigation>( includeInactive:false );
        GetComponentsInChildren( includeInactive:false , guns );
    }

    /// <summary> Requests to move along a path </summary>
    /// <remarks> (called by the game loop controller) </remarks>
    public void ActMoveActionStart ( List<Vector3> path , System.Action onMoveCompleted )
    {
        navigation.OnMove( path , onMoveCompleted );
    }

    /// <summary> Requests to fire at target </summary>
    /// <remarks> (called by the game loop controller) </remarks>
    public void ActFireMissionStart ( NavalUnit target , System.Action onFireCompleted )
    {
        foreach( var gun in guns )
            gun.OnFire( target , onFireCompleted );
    }

    /// <summary> Requests _ </summary>
    /// <remarks> (called by the game loop controller) </remarks>
    public void ActTakeDamage ( System.Action onDamageCompleted )
    {
        health.OnDamagedByShells( onDamageCompleted );
    }
    
}

public interface INavalHealth
{
    /// <remarks> Only the game loop controller is supposed to change health value </remarks>
    public int Health { get; set; }

    /// <summary> Play animation, audio, fx of taking damage </summary>
    void OnDamagedByShells ( System.Action onDamageCompleted );
}

public interface INavalNavigation
{
    /// <summary> Animate unit position, play audio & fx </summary>
    void OnMove ( List<Vector3> path , System.Action onMoveCompleted );
}

public interface INavalNavigation
{
    /// <summary> Implement this property so it will slow down movements </summary>
    public int SpeedHandicap { get; set; }

    /// <summary> Animate unit position, play audio & fx </summary>
    void OnMove ( List<Vector3> path , System.Action onMoveCompleted );
}

NavalHealthDefault.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NavalHealthDefault : MonoBehaviour, INavalHealth
{
    [SerializeField] int _healthPoints = 100;
    int INavalHealth.Health
    {
        get => _healthPoints;
        set => _healthPoints = value;
    }

    [SerializeField] GameObject _shellPrefab;
    [SerializeField] Transform _shellSpawnPoint;
     
     NavalUnit _root;

    void OnEnable ()
    {
        _root = GetComponentInParent<NavalUnit>();
    }

    void INavalHealth.OnDamagedByShells ( System.Action onDamageCompleted )
    {
        StartCoroutine( DamageRoutine(onDamageCompleted) );
    }

    IEnumerator DamageRoutine ( System.Action onDamageCompleted )
    {
        Debug.Log( "<sad naval hull noises>" , this );
        /* code to play audio, animation, particles etc. goes here */
        yield return new WaitForSeconds( 2.0f );
        onDamageCompleted();
    }

}

NavalNavigatorDefault.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NavalNavigatorDefault : MonoBehaviour, INavalNavigation
{
    NavalUnit _root;

    int _speedHandicap = 0;
    public int SpeedHandicap
    {
        get => _speedHandicap;
        set => _speedHandicap = value;
    }

    void OnEnable ()
    {
        _root = GetComponentInParent<NavalUnit>();
    }

    void INavalNavigation.OnMove ( List<Vector3> path , System.Action onMoveCompleted )
    {
        StartCoroutine( MoveRoutine(path,onMoveCompleted) );
    }

    IEnumerator MoveRoutine ( List<Vector3> path , System.Action onMoveCompleted  )
    {
        var step = new WaitForSeconds( 1.0f );
        foreach( var point in path )
        {
            Debug.Log( "<happy naval engine noises>" , this );
            _root.transform.position = point;
            yield return step;
        }

        onMoveCompleted();
    }

}

NavalGunDefault.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NavalGunDefault : MonoBehaviour, INavalGun
{
    [SerializeField] GameObject _shellPrefab;
    [SerializeField] Transform _shellSpawnPoint;
     
    NavalUnit _root;

    void OnEnable ()
    {
        _root = GetComponentInParent<NavalUnit>();
    }

    void INavalGun.OnFire ( NavalUnit target , System.Action onFireCompleted )
    {
        StartCoroutine( FireRoutine(target,onFireCompleted) );
    }

    IEnumerator FireRoutine ( NavalUnit target , System.Action onFireCompleted )
    {
        if( true )// is target in distance or angle range check
        {
            Debug.Log( "<happy naval gun noises>" , this );
            /* code to aim the gun etc. the gun goes here */
            yield return new WaitForSeconds( 3.0f );
            Instantiate( _shellPrefab , _shellSpawnPoint );
        }
        else
        {
            Debug.Log( "<sad naval gun noises>" , this );
        }
        onFireCompleted();
    }

}

In this scenario your HeavyShip prefab would simply have:

  • NavalUnit
    |- NavalHealth
    |- NavalNavigation
    |- NavalGunDefault
    |- NavalGunDefault

and FastShip use

  • NavalUnit
    |- NavalHealth
    |- NavalNavigation
    |- NavalGunThatSlowsEnemiesDown

so NavalGunThatSlowsEnemiesDown instead of NavalGunDefault and this NavalGunThatSlowsEnemiesDown would be the place that hosts all the tweakable values and logic for game loop controller to use in it’s calculations.

It’s my assumption that your game loop controller calculates everything of consequence and prefabs only host data & visualise the results with anims, audio, fx etc. when called to do so

So everything depend from project scale, believe at some point you could find this solution limiting:

  1. Would create excel spreadsheet with all of ships special attributes, description of them, and try to group them for some affecting factors… like AttackBonus, DefenceBonus, MovementBoost etc…
  2. Then would expand your scriptable ship attribute with some public int DamageBonus, public Vector3[] CannonsLocationsOffsets, etc
  3. Then in ship controler would had list/array of this scriptable attributes.
  4. Finally in ship controler inside void Update() when using Attack… checking foreach inside scriptables… do we had soem attack bonus… or when shooting… how many cannons we had and where are they locations offsets from ship center, or when moving do we had some % boost… OFC this values could be cached and updated after changes… no need for callig foreach during each Update