Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice
  3. Dismiss Notice

Question How to get static game data for a subclass

Discussion in 'Scripting' started by bazzboyy1, May 21, 2024.

  1. bazzboyy1

    bazzboyy1

    Joined:
    Mar 16, 2024
    Posts:
    18
    Hello, I'm in the process of setting up some infrastructure for an status effects and ability system. I've done this before but I want to know if there's a cleaner way of going about this particular problem.

    Essentially I need a way for my abilities to get their static data that is stored in a scriptable object. This data contains both generic ability data, and specific ability data. I.E abiltiy name is generic, all abilities need a name. zap duration or whatever is data specific to the ZapAbility. For sake of argument, lets assume that I can't serialize my fields in monobehaviour, hence why I'm using scriptable objects.

    So typically to solve this problem I have used generics, but this basically introduces a bunch of complexity which I don't like. It usually looks like the following:


    Code (CSharp):
    1.  
    2. using System.Collections;
    3. using UnityEngine;
    4.  
    5. public class AbilityGameData : ScriptableObject {
    6.     public string AbilityName;
    7. }
    8.  
    9. public abstract class Ability : MonoBehaviour
    10. {
    11.     protected abstract AbilityGameData AbilityGameData { get; }
    12.     public abstract IEnumerator Cast(Unit caster, Unit target);
    13. }
    14.  
    15. public abstract class Ability<T> : Ability where T : AbilityGameData
    16. {
    17.     [SerializeField]
    18.     protected T GameData;
    19.     sealed override protected AbilityGameData AbilityGameData => GameData;
    20. }
    21.  
    22. public class ZapAbilityGameData : AbilityGameData
    23. {
    24.     public int ZapAmount;
    25. }
    26.  
    27. public class ZapAbility  : Ability<ZapAbilityGameData>
    28. {
    29.     public override IEnumerator Cast(Unit caster, Unit target)
    30.     {
    31.         Zap(target, GameData.ZapAmount);
    32.         yield return null;
    33.     }
    34.  
    35.     public void Zap(Unit target, int amount)
    36.     {
    37.         // whatever
    38.     }
    39. }
    40.  
    Essentially you end up having to create this kind of redundant marker base class, because you can't store collections of Generic types without type arg specification. Not only that, but I also have to expose AbilityGameData property to the subclass which it doesn't even care about.

    Maybe this is a seperate issue, but I don't have the luxury of using SerializeField dependency injection with my StatusEffects, because I apply them to units using AddComponent. Is this a mistake? Should I just spawn a StatusEffect prefab?

    Does anyone have a solution that doesn't involve generics, and also doesnt have redundant data laying around (i.e having both a generic Game Data and Concrete Game Data members). I'd also love to consolidate the pattern for retrieving game data. Having serialize fields for one and a query for the other kind of smells.

    Perhaps I could not worry about storing static game data, and instead store an AbilityId that anything can use to look up the GameData?

    EDIT: Pls excuse my scoping & naming convention - I just wrote this out as an example
     
    Last edited: May 21, 2024
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    39,357
    Your use of the term "static" is slightly confusing to me... I think you mean the base unchanging data of your ZapEffect thingy perhaps?? Either way...
    static
    has a very specific meaning in C# code and it is NOT something you would likely want here. :)

    Generics also have their own issues with the Unity editor, but you can get what you want I think.

    Let me throw you out this little tidbit of info, using ScriptableObjects for an RPG system:

    ScriptableObject usage in RPGs:

    https://forum.unity.com/threads/scr...tiple-units-in-your-team.925409/#post-6055289

    https://forum.unity.com/threads/cre...ssigned-in-the-inspector.946240/#post-6174205


    ALSO: interfaces might be very useful to combine with ScriptableObjects above:

    Using Interfaces in Unity3D:

    https://forum.unity.com/threads/how...rereceiver-error-message.920801/#post-6028457

    https://forum.unity.com/threads/manager-classes.965609/#post-6286820

    Check Youtube for other tutorials about interfaces and working in Unity3D. It's a pretty powerful combination.
     
  3. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,390
    I would just scrap the generics and compose different ability data via various interfaces, and let various ability components pattern match for the relevant interfaces.

    Or scrap the ability components, and just have all the work happen in the scriptable object with a general purpose component managing more or more ability scriptable objects. If there is mutable data relevant to a scriptable object, let it factory that and manage that within the aforementioned component.
     
  4. bazzboyy1

    bazzboyy1

    Joined:
    Mar 16, 2024
    Posts:
    18
    Ive seem Static commonly used in context of web and databases when referring to data that doesnt change, or changes very infrequently. But yeah, i just mean immutable data that wont change at run time in this circumstance.

    cheers for the resources, im quite familiar with scriptable objects, its more so just resolving this data nicely when dealing with polymorphism.

    I dont suppose you could elaborate on your interface solution? I dont quite understand what you mean by pattern matching. I usually use util class for behaviour like spawning lineaer projectile or doing aoe damage. Are u suggesting i have interfaces for those and the associated data?

    The scriptable object solution seems decent. If i understand correclty, you might have an wbility controller which contains references to ability scriptables. Instances them for mutable data like say current damage stacks or something. How would i cast an abiliity? Would the controller route to the target ability scriptables cast inplementation?
     
  5. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,390
    Pattern matching pretty much means:
    Code (CSharp):
    1. if (someObject is ISomeInterface someInterface)
    2. {
    3.     someInterface.DoStuff();
    4. }
    Side note: GetComponent<T> works with interfaces, if you didn't know that already.

    The point of my second idea is that you do not care what derived type the ability scriptable object is. The point is using inheritance is to not care what potential derived type something is, you just use it as the base implementation. If you start needing to care about particular derived types, you are falling into anti-pattern territory.

    Some rough idea of what I mean:
    Code (CSharp):
    1. public class AbilityContainer : MonoBehaviour
    2. {
    3.     private List<AbilityInstanceEntry> _activeEntries = new();
    4.  
    5.     public void AddAbility(AbilityData abilityData)
    6.     {
    7.         var entry = new AbilityInstanceEntry(abilityData);
    8.         _activeEntries.Add(entry);
    9.     }
    10.  
    11.     private void Update()
    12.     {
    13.         foreach (var entry in _activeEntries)
    14.         {
    15.             entry.InstanceData.UpdateInstanceData();
    16.         }
    17.     }
    18.  
    19.     public sealed class AbilityInstanceEntry
    20.     {
    21.         public AbilityInstanceEntry(AbilityData abilityData)
    22.         {
    23.             _abilityData = abilityData;
    24.             _instanceData = abilityData.GetInstanceData();
    25.         }
    26.      
    27.         private AbilityData _abilityData;
    28.      
    29.         private IAbilityInstanceData _instanceData;
    30.      
    31.         public AbilityData AbilityData => _abilityData;
    32.      
    33.         public IAbilityInstanceData InstanceData => _instanceData;
    34.     }
    35. }
    36.  
    37. public abstract class AbilityData : ScriptableObject
    38. {
    39.     [SerializeField]
    40.     private string _abilityName;
    41.  
    42.     public string AbilityName => _abilityName;
    43.  
    44.     public abstract IAbilityInstanceData GetInstanceData();
    45. }
    46.  
    47. public interface IAbilityInstanceData
    48. {
    49.     void UpdateInstanceData();
    50. }
    Just a basic idea about what I mean by using inheritance correctly.
     
    Kurt-Dekker likes this.
  6. bazzboyy1

    bazzboyy1

    Joined:
    Mar 16, 2024
    Posts:
    18
    I guess I just don’t understand what the necessity of composing an ability with interfaces would be in the context of the example I provided where abilities are just cast, and that’s it. If there were more complex operational requirements, like say target input type (does the ability require a target to be cast, or a point in an area, or perhaps a vector), then it would make sense to me that an interface could be useful for making this distinction to cast an ability with the arguments it needs. Am I understanding that’s what you’re describing more or less?

    My game is somewhat turn based, you can’t tell by the example I gave, but the actually code for casting ability accepts both ally and enemy teams in addition to the caster. This lets my ability code do whatever to needs to do to whomever. Consumers of ability don’t need to know anything more at this rate about the concrete internals. But if they did, I assume that’s when interfaces would come in? That’s not so much the problem I’m concerned with right now.

    my main question is regarding getting the game data in relation the the concrete ability that it needs in order to execute. Say for example aoe radius for an explosion. If I keep my logic in the same hierarchy as the ability data, then that works pretty swell I guess. The logic is in the same place as the game data and there’s no dramas. Maybe I lose the ability to run coroutines which kind of sucks. But maybe that’s solvable with an instanced gameobject that runs on behalf of the ability.

    Are you perhaps suggesting that ability data is purely data with interfaces, and a script executes the ability in accordance with the interfaces it has? I.e IDoesAoe implemented on our explosion data, and the script can now execute the explosion? If this is what you’re suggesting, I think a lot of flexibility would be lost. I don’t think this kind of solution would be sutible in a game like dota for example but where abilities are highly customised.

    soz for rambles I just want to ensure I’m on the same page
     
  7. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    8,390
    It's important to note generics and inheritance are two different things and generally don't interoperate (not without getting into covariance and contravariance). Often they can completely work against each other. Generics is about... well generic, reusable code. Inheritance is about polymorphism, and being able to treat multiple different types as the same thing. And then interfaces are mostly the same as polymorphism, but via a composed approached.

    If you want the to be able to have your ability component to know the strongly typed ability data is corresponds to, then you will need to use generics as you are doing so now. It's not an uncommon approach to have a non-generic base class, plus a generic version. I do this reasonably often, though only when I will need collection of the non-generic base type.

    Or your ability data can factory a specific component onto a game object, allowing said component to make a reasonable assumption as to the type of ability data it's working off and cast it appropriately (not a huge fan of this, though). Though this is easier with plain C# objects as you can use a constructor that takes a more concrete type.

    Otherwise you can go down the route of abstracting things out so there is no longer a need to know the concrete type. Things like targeting implementations can work of an "tell, don't ask" basis. Such as "give me the implementation for how targeting works for this ability", and the object you get does this work. You don't need to specifically know targeting works for a particular ability, it can just define that itself and you simply need to get the object and pass it on as needed.

    In the end it's just general, proper OOP stuff.
     
  8. bazzboyy1

    bazzboyy1

    Joined:
    Mar 16, 2024
    Posts:
    18
    I mean to me generics seem to be more about type safety more than anything. I.E instead of having to use ArrayList (A list of objects that people used prior to generics in c#) that can represent anything, we can now be explicit about the object type and present errors in compile time. The code is effectively the same for managing the collection under the hood for ArrayList and List. List just adds typesafety. Not necessary prevention of code duplication, atleast in this example I think. And this is the main reason I feel inclined to use it when making the abilities. Just so 1. Abilities can conveniently get their associated data, and 2. So it's explicit what that game data contains. It's just kinda hard for me weigh up the benifit of this against the complication it introduces in the class hierarchy with having to add another abstraction (non-generic base class or interface) for storing collections of these.

    I'm still having trouble understanding what you mean when you suggest abstracting things out without more concrete examples of this. If we go back to the Cast behaviour, what there might we be abstracting? The whole cast implementation? I guess that would make sense. We could define an Ability, and it would have a CastBehaviour, which would probably also need to define its TargetImplementation, since it's a dependency, and we would assign the cast behaviour to the ability, and we will have avoided inheritance. This has benefits if you reuse your cast behaviour I suppose. Is this what you meant? I guess it solves my data problem if I make my CastImplementation a scriptable object which I'm not privy to, since I like using monoBehaviours for the coroutine properties. But I can work around that I suppose.

    This is my interpretation of your suggestion in the context of a moba like game:

    Code (CSharp):
    1. public class Ability : ScriptableObject
    2. {
    3.     [SerializeReference]
    4.     ICastBehaviour castBehaviour;
    5.  
    6.     private Unit caster;
    7.  
    8.     public void Init(Unit caster)
    9.     {
    10.         this.caster = caster;
    11.     }
    12.  
    13.     public bool CanCast()
    14.     {
    15.         // Returns true if cast input conditions met.
    16.         return castBehaviour.CastInputBehaviour.ProcessCastInput();
    17.     }
    18.  
    19.     public void Cast()
    20.     {
    21.         castBehaviour.OnCast(caster);
    22.     }
    23. }
    24.  
    25. public interface ICastBehaviour
    26. {
    27.     public ICastInputBehaviour CastInputBehaviour { get; }
    28.     public void OnCast(Unit caster);
    29. }
    30.  
    31. public class LeapSlamCastBehaviour : ICastBehaviour
    32. {
    33.     [SerializeField]
    34.     private PointCastInputBehaviour pointTargetBehaviour;
    35.     [SerializeField]
    36.     private float aoeRadius;
    37.     [SerializeField]
    38.     private float damage;
    39.     [SerializeField]
    40.     private float maxLeapDistance;
    41.  
    42.     // Probably wont work because there might be more to consider such as whether or not we are not in LOS
    43.     public ICastInputBehaviour CastInputBehaviour => pointTargetBehaviour;
    44.  
    45.     public void OnCast(Unit caster)
    46.     {
    47.         // Move caster towards point
    48.         // Get units around pointTargetBehaviour.point
    49.         // Apply damage to all units
    50.     }
    51. }
    52.  
    53. public class StompCastBehaviour : ICastBehaviour
    54. {
    55.     [SerializeField]
    56.     private NoTargetBehaviour pointTargetBehaviour;
    57.     [SerializeField]
    58.     private float aoeRadius;
    59.     [SerializeField]
    60.     private float damage;
    61.  
    62.     public ICastInputBehaviour CastInputBehaviour => pointTargetBehaviour;
    63.  
    64.     public void OnCast(Unit caster)
    65.     {
    66.         // Get units in radius around caster
    67.     }
    68. }
    69.  
    70. public interface ICastInputBehaviour
    71. {
    72.     public bool ProcessCastInput();
    73. }
    74.  
    75. public class TargetCastInputBehaviour : ScriptableObject, ICastInputBehaviour
    76. {
    77.     [SerializeField]
    78.     private bool canTargetAllies;
    79.  
    80.     [SerializeField]
    81.     private bool canEnemiesAllies;
    82.  
    83.     Unit target { get; }
    84.  
    85.     public bool ProcessCastInput()
    86.     {
    87.         // Listen for mouse click on unit
    88.         // Check if ally if can target ally
    89.         // Other cast conditions
    90.         return true;
    91.     }
    92. }
    93.  
    94. public class PointCastInputBehaviour : ScriptableObject, ICastInputBehaviour
    95. {
    96.     Vector3 Point { get; }
    97.  
    98.     public bool ProcessCastInput()
    99.     {
    100.         // Listen for mouse click on point on map
    101.         return true;
    102.     }
    103. }
    104.  
    105. public class NoTargetBehaviour : ScriptableObject, ICastInputBehaviour
    106. {
    107.     // Target processes immediately
    108.     public bool ProcessCastInput()
    109.     {
    110.         return true;
    111.     }
    112. }