0
\$\begingroup\$

Sorry for the title, I did not know how else to describe the question. I am writing a turn-based strategy game and am looking for a way to enable design-time additions of game mechanics where the mechanics each require some different math.

What I'd like to do is to be able to create a ScriptableObject and then modify it with the formula and various descriptions (all through the inspector), then add it to the game.

As an example, my game has cities where each city has several attributes such as population and various technology levels. I'd like to be able to add various "resource generators" at design-time via the inspector with the formula for how the resource is generated per turn.

Some examples of resource generators:

  • A "gold" resource generator with the "population * 0.025" formula to indicate that, each turn, the city generates 2.5% of it's population as gold.
  • A "research" resource generator with "assigned population * 0.5" as the formula to indicate that, each turn, the population assigned to research generates 0.5 pts of research per person.
  • A "production" resource generator with the "assigned population * production tech level * 0.75" with a max value of "1000 * city level" where excess production gets dumped to gold.

And so on. These are examples, but the formulas should allow for arbitrary interactions with other values in the game.

Some of the simpler formulas seems pretty easy, i.e. create a scriptable object attached to a primary value (say population) and then a simple modifier. But I'm stuck on how to do the more complex ones.

Anyone have an idea about how to accomplish something like this?

\$\endgroup\$
2
  • \$\begingroup\$ whats the reason for not doing the maths in code? are all your formulars linear equations? can you give an example for a complex formula? \$\endgroup\$ Nov 24, 2020 at 12:29
  • \$\begingroup\$ Just to be able to iterate on the game with various resource generators and resource spenders. I was trying to think of a way to architect the game such that it didn't care how many generators/spenders there were and could process 1 or many in the same manner. \$\endgroup\$
    – jpreed00
    Nov 24, 2020 at 15:01

2 Answers 2

1
\$\begingroup\$

I would really recommend to implement mechanics like this in code. There is a limit to how much you can "soft-code" until your soft-coded logic becomes just as complex as hard-coded logic.

But if you really want to go that route, then I have two alternatives for you.


The first you might want to look into is a C# feature called reflection. This allows you (among other things) to acquire the value of a variable when you have the name of that variable in a string at runtime.

Example:

// these would come from the ScriptableObject
string inputResource = "population"
float factor = 0.025f;
string outputResource = "gold"

// obtaining the value of a variable with the name only known at runtime
FieldInfo inputFieldInfo = city.GetType().GetField(inputResource);
int inputValue = inputFieldInfo.GetValue(city) as Integer;
FieldInfo outputFieldInfo = city.GetType().GetField(outputResource);
int oldOutputValue = outputFieldInfo.GetValue(city) as Integer;

// using the value in your program and writing it back also using reflection   
outputFieldInfo.SetValue(city, oldOutputValue + inputValue * factor);

One problem you see here is that you have no design-time check that the variable of the class city was entered in the inspector of the ScriptableObject actually exists. So when someone misspelled populaiton, then it would only fail at runtime when Type.GetField returns null. Another is that reflection can be rather expensive performance-wise, so when you need to do this very often, it might become an FPS hog.


But there is also another way which gives you more type-safety and should be faster, but has more wide-ranging consequences for your overall software architecture. Instead of writing your class City like that:

public class City : MonoBehaviour {
     public int population;
     public int gold;
     public int tech_level;   

     void Start() {
         population = 10;
     }      
}

Put all the properties into an array, and define an enum which can be used as an index to that array:

public class City : MonoBehaviour {
     public enum ResourceType {
         POPULATION,
         GOLD,
         TECH_LEVEL
     }

     public int[] resources = new int[3];        

     void Start() {
         resources[ResourceType.POPULATION] = 10;
     }   
}

Now you can refer to individual resource types by values of an enum, and you don't need reflection to do that at runtime.

// these would come from the ScriptableObject
City.ResourceType inputResource = City.ResourceType.POPULATION;
float factor = 0.025f;
City.ResourceType outputResource = City.ResourceType.GOLD;

// obtaining the value of a variable with the enum known at runtime
city.resources[outputResource] += city.resources[inputResource] * factor;

Yes, this piece of code got a lot simpler. But the cost is that any other code using city resources which does not need that level of flexibility will get more complicated.

\$\endgroup\$
2
  • \$\begingroup\$ I also thought about building up a series of objects -- like Formula as the top-level, then FormulaAdd/FormulaSubtract/FormulaMultiply/FormulaDivide and so on, so you could nest them and build up arbitrary complexity based on the mathematical primitives. The problem there, like you state, would simply be attaching the primitives to the appropriate value within the city. \$\endgroup\$
    – jpreed00
    Nov 24, 2020 at 17:50
  • \$\begingroup\$ @jpreed00 Well, I guess you could create a whole bunch of ScriptableObject instances and cascade them together in that system to create a relatively simple binomial formula. Or you could just write one line of C# and achieve the same thing. Or if you really want to avoid code and stay in a designer-friendly point&click interface, why not use Bolt? \$\endgroup\$
    – Philipp
    Nov 24, 2020 at 21:32
1
\$\begingroup\$

Just to be able to iterate on the game with various resource generators and resource spenders. I was trying to think of a way to architect the game such that it didn't care how many generators/spenders there were

You can do something like this:

public enum Resource {
    Gold, Research, Production
}

public struct ResourceValue {
    private readonly Resource resource;
    private readonly float value;

    public ResourceValue(Resource resource, float value) {
        this.resource = resource;
        this.value = value;
    }

    public Resource Resource => resource;
    public float Value => value;
}

public abstract class ResourceGenerator : ScriptableObject {
    //Generates and returns a resource for the given city based on
    //that city's current properties
    abstract public ResourceValue Generate(City city);
}

[CreateAssetMenu(menuName = "Gold Generator")]
public class GoldGenerator : ResourceGenerator {
    [SerializeField] private float populationFactor = .025f;

    override public ResourceValue Generate(City city) {
        return new ResourceValue(Resource.Gold, city.Population * populationFactor);
    }
}

[CreateAssetMenu(menuName = "Research Generator")]
public class ResearchGenerator : ResourceGenerator {
    [SerializeField] private float assignedFactor = .5f;

    override public ResourceValue Generate(City city) {
        return new ResourceValue(Resource.Research, city.AssignedResearchers * assignedFactor);
    }
}

public class City : MonoBehaviour {
    [SerializeField] private List<ResourceGenerator> resourceGenerators;
    [SerializeField] private int population;
    [SerializeField] private int assignedResearchers;

    public int Population => population;
    public int AssignedResearchers => assignedResearchers;

    //call this function each turn to generate resources according to the resource generators we've assigned tot his city
    public void GenerateResources(Player player) {
        //iterate through each generator, run its formula, and apply the generated
        //resources to the player
        foreach (var generator in resourceGenerators) {
            ResourceValue generated = generator.Generate(this);
            player.AddResource(generated);
        }
    }
}

Here we create several ScriptableObject classes representing resource generators. Their formulas are coded, but key values that the formulas use are serialized properties that can be edited in the Inspector. We can assign resource generators to a city, and the city will loop through all of its resource generators when generating resources. This way the city doesn't need to know or care the type or quantity of resource generators it has.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .