Search Unity

Discussion Composition based and Data Driven Approach to Game Design Data

Discussion in 'Scripting' started by Limnage, Apr 15, 2023.

  1. Limnage

    Limnage

    Joined:
    Sep 12, 2013
    Posts:
    41
    In games you almost always need to define static design data that defines how various things like abilities, stats, etc. work in your game.

    Unreal has the Gameplay Ability System (GAS) framework that handles this, but Unity does not so it is up to the developer to create their own solution to this problem (henceforth referred to as a GAS solution, but the system is not limited to abilities per se). This is significantly harder than it seems at first, and to this day I haven't seen an example solution in Unity that solves the complexity of this problem.

    Some examples of previous discussion:

    https://forum.unity.com/threads/gameplay-ability-system-in-a-data-oriented-approach.1229973/
    https://forum.unity.com/threads/composition-in-scriptableobjects.808002/
    https://forum.unity.com/threads/designing-scattered-stats-attributes.1253907/
    https://www.reddit.com/r/gamedev/comments/8ltodz/composition_based_design_for_a_spell_system/
    https://www.reddit.com/r/roguelikedev/comments/ctw6vt/unity_composition/

    I believe there are some simple tenets one can make in regards to GAS solution:
    • Favor composition over inheritance. A purely inheritance-based solution will quickly break down with increasing gameplay complexity
    • Avoid code duplication and boilerplate
    • Not superfluous unused fields. For example a base Ability god class that has range, cast time, and every possible ability field even though some abilities may not have a range or cast time.
    • Make defining new gameplay data as easy as possible
    I think it's helpful to also define test example cases to check whether a GAS solution can handle them:

    RPG Stats and Abilities
    MP = 2*intelligence + 3*wisdom

    staff: 20% increased intelligence, "primary spell range" +4

    Defining Monster Types
    You have various monsters, and you want to define a list of abilities and stats for each (and also ensure there are no duplicate stats). For example

    Berserker
    {
    Stats: HP, Rage
    Abilities: Whirlwind, Frenzy
    }

    Mage
    {
    Stats: HP, MP
    Abilities: Fireball
    }

    Bloodmage
    {
    Same as Mage, but additionally:
    Stats: Blood
    Abilities: Blood Spike
    }

    Mind Control Chains

    Targeting:
    • 1) target unit
      • targeting conditions: range (primary spell range) 10 from caster, enemy unit, target MP < caster MP,
    • 2) target circle area
      • targeting conditions: center range (this is not a primary spell range) 5 from target unit 1), radius 5
    Effects:
    • Set all tiles in target area 2) on fire
    • caster player now controls target 1)
    • all enemy units in area 2) are now chained to target 1) with chains of length 15. If all chains are broken by distance, then the spell ends
    • consumes mana per second based on the targets level and how many enemy units were in the target area. when mana runs out, spell ends
    • a % of the damage applied to the controlled unit is transferred to the caster as well, and that % grows over time
    • once the mind control link breaks, both units are stunned for a duration that depends on how long the mind control was active
    All data used to create this ability should be reusable and generic. For example, the area targeting in 2) should be the same component as targeting a ranged AoE fireball, except with the origin of the targeting range being target unit 1) instead of the caster.

    Networked Overrides
    Usually an ability is cast by a local player or AI, but in a networked game you need to cast the same abilities based off of player input information that you receive from the server instead.

    Clicker Game

    A series of purchasable buildings (building_1 through building_10) that cost gold to purchase and produce gold. The formulas for the cost and production of building_n are some complicated formula like
    cost: log(n) * 10^n * max(n, 100)
    production: 5^n * n^0.5

    Every quantity in the game can be modified, including modifier values themselves. For example, and upgrade with purchasable levels where
    n = upgrade level
    g = current gold amount
    Golden Power = ( logn(g / 10 + 1) * 5 + 1) + 1.4 ^ (n ^ 0.7)
    Then Golden Power is a multiplier on gold gain per second, so
    gold / s *= Golden Power

    and other modifiers may modify Golden Power itself, so there may be another upgrade that does
    s = current stone amount
    Golden Power *= 5*log10(s + 1)

    So you'd need a GAS solution to define all the currencies, building series, upgrades, etc.

    Again, the solutions should be generic. So just like the gold currency has gold per second stat that can be modified, it should be possible for buildings or upgrades to have a gain per second stat that can be modified, e.g. City buildings producing Factory buildings that produce gold. This gain per second quantity component should be implemented the same way in each.

    These examples are purposely complicated, but they're not far off from game design data you will come across in actual games.

    Purpose

    The purpose of this thread is to attempt to find a GAS approach with example code that fulfills all the test cases. You of course wouldn't use the exact same code base in the RPG and the Clicker game, but the approach should be applicable to both.

    Has anyone been able to find a concrete GAS solution approach that fulfills the above requirements?
     
    Last edited: Apr 18, 2023
  2. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    SerializeReference exists and has existed since 2019 that allows you to do polymorphic serialisation, effectively solving your entire problem here.
     
  3. Limnage

    Limnage

    Joined:
    Sep 12, 2013
    Posts:
    41
    SerializeReference still has many issues. For example it's not like you can put [SerializeReference] on an interface field and it will automatically show you a dropdown of every object that implements that interface and store it nicely. I think you know this given this thread: https://forum.unity.com/threads/serialized-interface-fields.1238785/

    In any case, I deleted the portion about ScriptableObject since the editing solution is not the crux of the issue here. If there's a way to create a game design data system using ScriptableObjects that fulfills all the above requirements, I'd love to see it. None of the code examples I've seen in similar threads have been able to solve the problem.
     
  4. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    Well it lets you serialise plain C# classes, including with a base interface type. Yes it doesn't have the built in inspector support, but a property drawer fixes this, or tools like Odin Inspector support this out of the box.

    And then you just take a 'component' like approach like you would with Unity's components. Look for the component you want, get the data you care about from it. This is the approach I've taken with items and similar systems and it's worked wonderful for me.

    Hell I've been trying to take it further with a prototype pattern, where you can reuse data from other objects and further reduce repeated information:
    upload_2023-4-15_14-49-25.png

    The greyed out components belong to different assets, but are displayed inline to show you all the data that the object you are looking is accumulating.

    Odin's doing the drawing (though there is a bit of my own custom drawing here), but all the serialisation is Unity, with the Prototype components just being:
    Code (CSharp):
    1. [SerializeReference]
    2. private List<IPrototypeComponent> _prototypeComponents = new();
    It's the kind of thing where the Unity Editor isn't going to do all this out of the box, you're going to have to make the tools to get it to do what you want.

    So what you want is possible, you just need to make the tools for it.
     
  5. Limnage

    Limnage

    Joined:
    Sep 12, 2013
    Posts:
    41
    Do you mean that you implemented your own ECS/system to act on these components? Or did you use DOTS or something?
     
  6. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,859
    No, these objects just have methods similar to
    GetComponent<T>()
    /
    TryGetComponent<T>(out T)
    and my systems just find the component I care about and go from there.
     
  7. Limnage

    Limnage

    Joined:
    Sep 12, 2013
    Posts:
    41
    It seems unfortunate that developers have to implement their own component system to get a nice composition based approach, but that seems like the cleanest way to do it.

    For anyone reading this thread, another bit of information that might be helpful is that C# 8.0 has default methods of interfaces, which can help make pseudo-mixins.