#Stat system implementation issues

1 messages · Page 1 of 1 (latest)

potent sinew
#

StatSystem.cs

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

using UnityEngine;

public class StatSystem
{
    //   Variables
    // Events
    public event EventHandler<StatChangeArgs> OnValueChange = delegate { };
    public event EventHandler<StatChangeArgs> OnMaxChange = delegate { };
    
    // Stat type
    public StatType statType { get; }

    // Stat vars
    public float value { get; private set; }
    public float maxValue { get; private set; }
    


    //   Instantiation tools
    // Instance creation
    public StatSystem(StatType type, float max) {
        this.statType = type;

        this.maxValue = max;
    }

    // Value init
    public void Max() {
        this.value = maxValue;
    }
    


    //   Stat management
    // Type check
    public bool IsSameType(StatType checkType) {
        return this.statType.Equals(checkType);
    }
    
    // Value
    public void ModifyValue(object sender, StatChangeArgs changeArgs) { 
        if(IsSameType(changeArgs.statType)) { 
            ModifyStatValue(changeArgs.value); 
        } 
    }
    public void ModifyStatValue(float changeAmount) {
        this.value = Mathf.Min(value + changeAmount, maxValue);
        OnValueChange(this, new StatChangeArgs(statType, changeAmount));
    }
    

    public void ModifyMax(object sender, StatChangeArgs changeArgs) { 
        if(IsSameType(changeArgs.statType)) { 
            ModifyStatMax(changeArgs.value); 
        } 
    }
    public void ModifyStatMax(float changeAmount) {
        this.maxValue = changeAmount;
        OnMaxChange(this, new StatChangeArgs(statType, changeAmount));
    }
}
#

StatType.cs

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

using UnityEngine;

public class StatType
{
    //   Vars
    public string typeName { get; }
    
    //   Instantiation
    public StatType(string typeName) {
        this.typeName = typeName;
    }



    //   Functions
    // Equality
    public bool Equals(StatType other) {
        return string.Equals(this.typeName, other.typeName);
    }

    public float SetMaxFunction() {
        return 0;
    }
}
#

@languid vector so I'd rescript StatSystem to be StatSystem<T>, where value and maxValue are of type T?

placid ledge
#

What's your problem exactly? Are you just asking how you should structure everything?

potent sinew
#

Essentially, yeah - I got hung up on how to change how ModifyStatValue and ModifyStatMax work for different StatTypes

languid vector
#

Then yeah, you're always going to know the stat's value type when you're changing it, so you can probably just hold them in a Stat<T> generic where T would be the value (float, boolean, integer, etc), instead of making a whole inheritance structure for different stats

placid ledge
#

Well, for my stats system I just derive them all from the general utility and functions that all stats would use instead of making a whole separate class.

#

Rather, keep the stats in a hierarchy which each stat derives the general methods from.

languid vector
#

There are many ways to skin a cat, in general

potent sinew
#

I may be misunderstanding something, but in that case how'd I refer to each Stat as a different component (or would I need to at all)? At least right now, each of the 'more advanced' StatSystems (HealthSystem, TimeSystem, etc.) subscribes a method in their StatSystems to an event called by my pickup collection behavior* - also, my UI implementation depends on referring to them as different components

#

i apologise for my naming conventions btw

languid vector
#
player.StatManager.GetStat<int>("Health").value
player.StatManager.GetStat<int>("Health").modifiers
player.StatManager.GetStat<int>("Health").GetModifiedValue()
``` is kind of how I'd access these things personally
potent sinew
tepid whale
#

Tried scimming over everything since I can't dedicate the time needed for help, but it seems you should create a class for each type of system that modifies a Stat and use the flyweight pattern. That way, when you grab a time pickup, your time stat class (whatever you name it) will call its own ModifyStatValue method and change it. Your health stat class will have its own implementation of the method as well. This means you only need to pass in the changeAmount parameter and the stat in question will update accordingly . . .

languid vector
placid ledge
#

If you wanted to use coroutines and buffs, I would probably suggest having a container and storing modifications to stats as objects.

languid vector
#

Random's suggestion is probably ideal by the way, having each stat able to define it's own behaviour is powerful

#

It can be a bit clutter-ey having a script for all your stats though

potent sinew
#

At least as I understand it that's what I've been trying to do already, I'm probably just not implementing it properly? idk

#

when a pickup is collected, CollectBehavior triggers an event (which each of the ____Systems subscribe their StatSystems to) and passes some data through

#

what I'm hearing is I need to 'move' some of the important bits of StatSystem (ModifyStatValue, etc) to the more specific classes so I can change the way they behave there

languid vector
#
abstract class Stat<T>
{
  T value
  List<Modifier<T>> modifiers
  abstract T GetModified()
}

class HealthStat : Stat<int>
{
  // implementation
}

placid ledge
#

Yeah, with generics or not. You should derive the base class you got there.

#

Most of those methods can be static too

potent sinew
#

so, at least what I'm thinking

abstract class Stat<T>
{
    // The value and it's max (the max is for UI stuff mostly)
    T value
    T maxValue

    // All the modifiers/effects acting on the stat
    List<StatModifier<T>> modifiers

    // Methods that change the value or the max
    abstract void changeValue()
    abstract void changeMax()
}

// some examples
class HealthStat : Stat<int> { /* implementation not shown */ }
class TimeStat : Stat<float> { /* implementation not shown */ }
class AmmoStat : Stat<int> { /* implementation not shown */ }
placid ledge
#

Yeah, that looks good.

languid vector
#

Yeah, give or take

potent sinew
#

I'm assuming that this is also applicable to, say, damage and damage types?

languid vector
#

You may also want to make value and maxValue properties with a getter/setter instead of explicitly defining changeValue and changeMax

potent sinew
#

so ```cs
T value { get; set; }
T maxValue { get; set; }

languid vector
#

But that's all code style more than anything

#

ye

#

when they're properties I believe they can be made abstract, so you will need to implement them in the derived classes

potent sinew
#

as far as handling pickups goes, how would I specify the Stat the pickup intends to modify?

placid ledge
#

The modifier container idea requires some checking when you add and remove from it, but overall I use something similar as it helps keeping track of what's being added or reduced.

languid vector
#

Yeah the pickups would have a similar architecture with their own OnPickup(Entity grabber) or something like that

potent sinew
#

would they contain an instance of Stat<T> themselves?

placid ledge
#

Well, you would have all the stats on a playermanager, right?

#
    void PickUp()
    {
        PlayerManager obj = interactingEntity.GetComponent<PlayerManager>();
        bool wasPickedUp = obj.PickUpItem(item.GetCopy());

        if(wasPickedUp)
        {
            Destroy(gameObject);
        }

    }
languid vector
#

Not unless they have some stat. If you think about it, right? when a player collides with a pickup, the pickup should be able to find a reference to some stat for modification. That means that entities that are able to pick up that powerup should always share a field of some sort

placid ledge
#

You'd access the gameObject with a collidier or some interaction script, and apply it to your playerstats

potent sinew
#

Not exactly sure how I'd handle that, though. Thinking about ammo, a pickup may be for an ammo type (or for a specific weapon!) that the player doesn't have

languid vector
#

Just give players the stat, set max amount to 0?

potent sinew
placid ledge
#

So, your item would contain some data inside this gameobject lying around, the normal way to handle this is to have a collider event for when your player touches this item. You'll have eventdata from the object and could be able to reference to back to the player.

languid vector
#

You can also maybe have the stats as a mono and use GetComponentInChildren<[AmmoType]>() on the player?

potent sinew
#

hmm...

#

what about UUIDs, since I've been thinking of using that to identify weapons to begin with?

placid ledge
#

If you're making weapons with similar base types but with different stats, sure.

potent sinew
placid ledge
#

So yeah, with the StatModifier objects, each weapon contains one for each stat which you would then send to each stat container on the player.

#

and you'd update your stats on equip and de-equip. It's how I did a system previously, but changed it quite a bit later on.

languid vector
#

The weapon itself can also contain the stat

placid ledge
#

There's quite a few ways, but ultimately when I started to introducing items that say increase a stat by a percent, it became harder to keep track of original values and such.

#

Which I resolve in a update function between stat changes.

potent sinew
#

I presume I'd add a UUID field to the Stat class, then?

placid ledge
#

You wouldn't need a unique ID on stats; stick them on equipment, but not necessarily consumables.

potent sinew
#

I see, so only things like AmmoStat would implement the UUID field

placid ledge
#

Eh, I dont think you need unique ammo unless you're adding stats to those too, but it wouldn't hurt.

#

And by stats, I mean if one type of ammo has different stats, then yeah.

potent sinew
placid ledge
#

It makes sense on the weapons because you can say have a plasma gun with +5 recoil and another with +10 firing speed, but if ammo is just a component to use the weapon, they don't necessarily have to be unique.

#

You'd have different types of ammo, sure, but plasma ammo can be used in all plasma weapons, ect.

potent sinew
#

yeah, my thoughts exactly

#

so if I call an event in some Stat<T>, how'd I pass that event's info up to its listeners? for example, allowing the UI to listen in on the player's HealthStat? it seems to me like this would get kind of complicated if I have to create a new List for each T

placid ledge
#

Honestly, skip the generics. Lmao

#

I'm more used to Java, and I had a small problem recently using some generics that would be otherwise easily solvable with Java, but instead I had to implement some interface and do some callback shenanigans with it just so I could avoid some cast comparisons.

#

Reflection I think it was called?

potent sinew
placid ledge
#

Up to you, floats probably better if you want to introduce ceiling or floor functionality in your equations.

potent sinew
#

I mean, if I have x% modifiers, yeah probably

placid ledge
#

Generics is nice if you want to have like rpg stats like strength being a whole number, but having an attack speed stat that's a float.

potent sinew
#

Yeah, not going to implement RPG stats

#

I guess one remaining question I have is if StatModifiers are on timers (via coroutines) how do I manage removing them from the list once they expire?

placid ledge
#

You'd have to reference the stats with a buffmanager of sorts, and when they expire do like a callback.

#

I think that's what I did at the start, but I started to introduce a bunch of stats and changed it up a bit.

#
public class AttributeEffect : ActiveEffectObject
{
    //Need to prevent duplicate values eventually

    [SerializeField] private List<StatModifier> statModifiers;
    [SerializeField] private float duration;
    private List<StatModifier> modsToRemove = new List<StatModifier>();

    private void OnValidate()
    {

    }

    public override void ExecuteEffect(EntityManager entity)
    {
        foreach(StatModifier mod in statModifiers)
        {
            entity.stats[mod.statType].AddModifier(mod);
            modsToRemove.Add(mod);
        }

        entity.StartCoroutine(RemoveBuff(entity, modsToRemove, duration));
    }

    private static IEnumerator RemoveBuff(EntityManager entity, List<StatModifier> modsToRemove, float duration)
    {
        yield return new WaitForSeconds(duration);

        foreach (StatModifier mod in modsToRemove)
        {
            entity.stats[mod.statType].RemoveModifier(mod);
        }
    }
}```
#

My attribute duration potion class basically.

potent sinew
#

I see

placid ledge
#

I have two lists for specific calculation reasons, but you can just remove by object source. I think I did that originally but I ran into errors for some reason I forget.

#

But the class itself should remove the stat when the coroutine ends

#

Just need a reference to the player/stats

#

and I do like a update function everytime I remove/add stuff instead of just deducting so stuff like % items will recalculate

potent sinew
#

I see

#

or at least, I think I do

#

anyways, huge thanks to you all, this was immensely helpful!

placid ledge
#

yeah, no problem. This stuff is fun ^^