how store and calculate characteristic in ScriptableObject

I have flexible statistics system. each stat is SO that has key and value. The value should be calculated according to the formula. for example
key = “movement” value = stat[movement[base]] + stat[agility[value]]. how can I add a formula to the SO, according to which the value will be calculated?
p.s. each stat consists of two classes: SO and date. The date stores the value of the stats of a specific entity. for example: for calculate damage, I send data to SO and they return calculated value.

You need to create your own formula parser from a string. Or an easier and less error prone way is to create a base SO, then a child SO for each formula you want:

public abstract class BaseFormulaSO : ScriptableObject
{
    public abstract float GetValue(Data data);
}

[CreateAssetMenu()]
public class Formula1 : BaseFormulaSO
{
    public override float GetValue(Data data)
    {
        // do you calculations...
    }
}

[CreateAssetMenu()]
public class Formula2 : BaseFormulaSO
{
    public override float GetValue(Data data)
    {
        // do you calculations...
    }
}

Then where you want to choose a formula:

// You can assign any formula from the Editor...
[SerializeField]
private FormulaSO formula;

public void CalculateFormula()
{
    var value = formula.GetValue(this);
    // Do something with value...
}

now I have 273 stats and I would not like to create a separate child for each stat, because comunity are engaged in balance and I don’t want them to get into the code.

273 stats?!

I mean are you sure you want to have a game with 273 stats? you understand that you have to incorporate ALL of them into your gameplay (write code for each stat on how it will affect your game) AND balance all of them so they can work together? I don’t want to say it’s impossible… but it’s impossible.

Anyway, in your case you need to write a string (formula) parser, using Regex for example:

using System;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using UnityEngine;

// Your "Stats" enum
public enum Stat
{
    Movement,
    Agility,
    Stamina
}

public class FormulaParser
{
    public enum ValueType
    {
        Base,
        Value
    }

    public enum Operation
    {
        None,
        Add,
        Subtract
    }

    public class StatOperation
    {
        public StatOperation(Operation operation, Stat stat, ValueType valueType)
        {
            Operation = operation;
            Stat = stat;
            ValueType = valueType;
        }

        public Operation Operation { get; }
        public Stat Stat { get; }
        public ValueType ValueType { get; }
    }

    private static readonly Regex Pattern = new Regex(@"(\+|\-)?\s*stat\[(\w+)\[(base|value)\]\]", RegexOptions.Compiled);

    public static List<StatOperation> Parse(string formula)
    {
        var results = new List<StatOperation>();

        var isFirst = true;

        foreach (Match match in Pattern.Matches(formula))
        {
            if (match.Groups.Count != 4)
                continue;

            var operationStr = match.Groups[1].Value;
            var operation = Operation.None;

            if (!isFirst)
            {
                operation = operationStr switch
                {
                    "+" => Operation.Add,
                    "-" => Operation.Subtract,
                    _ => throw new InvalidOperationException($"Invalid or missing operation before stat {match.Groups[2].Value}")
                };
            }

            isFirst = false;

            var statTypeStr = match.Groups[2].Value;

            if (!Enum.TryParse(statTypeStr, true, out Stat statType))
                throw new InvalidOperationException($"Unknown stat type: {statTypeStr}");

            var valueTypeStr = match.Groups[3].Value;

            if (!Enum.TryParse(valueTypeStr, ignoreCase: true, out ValueType valueType))
                throw new InvalidOperationException($"Invalid value type: {valueTypeStr}");

            results.Add(new StatOperation(operation, statType, valueType));
        }

        return results;
    }
}

Usage:

public static void TestParseFormula()
{
    var formula = "stat[movement[base]] + stat[agility[value]] - stat[stamina[base]]";

    var parsedResults = FormulaParser.Parse(formula);
 
    foreach (var result in parsedResults)
    {
        Debug.Log($"Operation: {result.Operation}, Stat: {result.Stat}, Value Type: {result.ValueType}");
    }
}

Also you don’t want to parse your formula each time you need it for performance reasons, but parse it once and store (cache) the result (List) inside your SO for example.

If you want to test create/test Regex patterns, you can use this excellent website regex101: build, test, and debug regex and of course Microsoft documentation Regular Expression Language - Quick Reference - .NET | Microsoft Learn

Unity binaries aren’t that hard to reverse engineer. You would have to keep the logic on a server. Adjusting the layout of your objects and code won’t make it harder on them just you.

I think he meant he wants people to easily extend/modify the system (modding), without the need to write code. But you may be right I don’t know, I stopped thinking at “273 different stats”.

Then don’t give them the code. That’s the only way.

If you’re concerned about the user ‘hacking your save files,’ or ‘cheating in your game,’ which is playing on their computer, just don’t be.

There’s nothing you can do about it. Nothing is secure, it is not your computer, it is the user’s computer.

If it must be secure, store it on your own server and have the user connect to download it.

Anything else is a waste of your time and the only person you’re going to inconvenience is yourself when you’re debugging the game and you have savegame errors. Work on your game instead.

Remember, it only takes one 12-year-old in Finland to write a script to read/write your game files and everybody else can now use that script. Read about Cheat Engine to see more ways you cannot possibly control this.

This message is not about cheaters. this post is about comunity. so that to copy new balance from google table with one button.

for example i have class and table from comunity and have calculate value from formula upload from table to StatSO

public class StatData
{
    [SerializeField] private string Key;
    [SerializeField] private Dictionary<StatSource, float> _statsTypeContainer;
    private StatSO _soLink;

    public float Get(StatSource source, CharacterStat character)
    {
        if (source == StatSource.All)
            return _statsTypeContainer.Values.Sum();
        if (source == StatSource.Value)
            return _soLink.execute(character);
        if (_statsTypeContainer.ContainsKey(source))
            return _statsTypeContainer[source];
        Debug.LogError($"Source: {source} in {Key} not presented");
        return 0;
    }
}

public enum StatSource
{
    All,
    Value,
    Base,
    Equpment,
    Buff,
    Constelation,
    Gear,
    Food
}

9693284--1383098--изображение_2024-03-11_132143385.png

I found a solution with string parsing and a parsing library for с# but this solution a slow.
the idea was born to parse the formula, for example, into a queue. or List… or the chain… before the build and use it to calculate value. example: formula min[runSpeed[all]+10*movementSpeed[value],3000] decompose to list:
0.getValue(movementSpeed,value,character);
1.multiplay(10,List[0]);
2.getValue(runSpeed,all,character);
3.summary(List[1],List[2]);
4.minimum(List[3],3000);
but I have no idea how to do it. and what will happen to performance in this case

I literally gave you the code for it, and how to deal with performance. Just look up my previous answer. Or as an alternative, you could include and use a scripting runtime into your project (LUA, JavaScript… just google “LUA Unity” or “JavaScript Unity”.)

Also I don’t want to appear rude, but if you don’t know how to write a string parser, I don’t think you have yet the necessary skills to complete your (monster) project. I strongly suggest you reduce your project scope by a lot, finish a basic working version with 3 to 5 stats max, no dynamic/formula/community thing, THEN once you feel comfortable with your newly acquired skills, you can start a Phase 2 of your project by adding more features. Go step by step. Also there’s no such thing as “273 stats” in a game. It’s just impossible, at least not before 5 or 6 decades.

now its work on parsed string and cashing value in dictionary. but in this solve i see problem… I would like to try to make something like a node system, but I don’t know how to approach it…

8 stat base(string,agility,etc). 5 stat moving(move speed, dash distance, etc). 120 stat for battle(magic scool, ranged, meele,sumon,and for everyone: dmg,block,crt,evade,piercing). 110 for profession(fising,mining,choping,shamanism) and lastet for utility(weight,vision range,etc)… this system good work in java with serealization to json… in unity i have do them with SO. and based on the work of SO, I would like to optimize those places that I don’t like

I found a solution to my problem. I decided put answer, in case someone else needs it.
i make parser from string to expression tree(linq.Expression). and it works perfectly.
take formula: 12 + (stat(MiningEfficiency(Base)) * stat(MiningEfficiency(Power)) * (stat(Strength(Value))^0,35) * (stat(MiningLevel(Value))/2) * (stat(MiningEfficiency(Talent)) + stat(MiningEfficiency(Equipment))))
and calculate this 1kk iteration. I got the following results:
1 and 2 result is Expression with different call function
3 result - function in code

9771852--1400403--upload_2024-4-15_15-30-9.png