#Architecture planning for a Roguelike Farming game.

1 messages · Page 1 of 1 (latest)

sweet kindle
#

I'm designing a system for a Unity game where each plant type is defined using a ScriptableObject. The ScriptableObject stores the plant’s base parameters such as growth speed, harvest value, and an associated attack type.

The attack-type itself is also defined as a ScriptableObject, since different plants can use different attack behaviours (for example: projectile, bomb, electric shock, etc.). Each attack-type has its own unique set of parameters. For instance, a projectile attack might have parameters like projectile speed and damage, while a bomb attack might have explosion radius and fuse time.

During gameplay, the player can purchase items or upgrades that modify these parameters at runtime (for example increasing damage, reducing cooldown, or changing projectile speed). Since these values change during the run, it would not be appropriate to modify the ScriptableObject assets directly, as they represent the base definitions. Instead, these parameters would need to exist as mutable runtime copies that can be safely modified without affecting the original assets.

If this were only for the plant’s own parameters, creating a mutable runtime copy of the plant stats would be straightforward. However, since the plant references an attack-type ScriptableObject hierarchy with multiple inherited attack types, each with different parameter sets, this would likely require creating separate mutable runtime copies for each attack-type as well?

The question is how to structure this cleanly. Is creating runtime copies for each attack type the correct approach in this situation, or is there a better way to organize this system while still keeping ScriptableObjects as immutable base definitions? Or is there a better architecture than this for implementing this.

Plus I plan to have other SO based paramaters similar to attack-type like Special-Powers etc.

Code structure I have in mind:
(Same as above discussed)
https://paste.ofcode.org/cweheC9sHkDx4usURGDgGj

long lichen
#

I am not sure, why you would need copies of the attack-type at all. If you are going to change those, yeh, than you might need a copy. But if they are just basic types, you should be good to go with the direct asset, or am I missing something here?

leaden minnow
#

I use SOs as templates basically. At runtime I typically convert them into a runtime type which is serializable and mutable. So it can be changed and serialized/saved to my save game file.

#

In some projects if they grow I use roslyn code generators to simplify the maintenance and development of these parallel types.

long lichen
#

Adding to Praetors answer, I had a similar situation where I would create node based quests, which in the end were based on SOs and in and out nodes, so every quest could trigger any other and rely on any quest (or in extend item in inventory). When in runtime, I then saved this entire quest state to the player, so next time the game runs, it loads this save file and overwrites it back to the player state.

dire pumice
#

One alternative architecture/approach to think about with this (especially if you are using ScriptableObjects as data), is to think whether you need them at all. ScriptableObjects are great for configuration and they allow for both data and behaviour. However, since you are focused on using SO's as data, you can remove the need to ScriptableObjects entirely and use JSON for your data (one source of truth), this way you don't need to create, maintain or instantiate runtimes. You could create "catalogues" for the data (PlantCatalogue, AttackCatalogue) that parses the data from JSON into memory and use that approach. This is how I would go about architecting and designing a system like this. You also have a few libraries for JSON (Newtonsoft JSON) or the more recently built-in .NET JSON parser (System.Text.Json)

long lichen
#

But this eliminates the convenience of the editors file and inspector relation. So you move your object based setup to an entirely data driven setup. Id say, both ways are valid, but it really comes down to what you want as a workflow on how to set it up and how well you set it up in both cases.

dire pumice
#

Very true, it is a question of workflow, personally I prefer external tools and a more developer/data-driven style workflow but that doesn't mean you can't create Unity-specific editors/tools for the JSON, so you can still maintain that level of tooling entirely within Unity if you wanted to but then you can other options as well and it means that you can both game data and a save/load system with one file-type. Since ScriptableObject should not be used for saving/loading systems, It's also easier to use/maintain (once the system is in place because you don't need to set up references in the inspector). I do agree with it being a different type of workflow though and while the JSON approach for game data over ScriptableObject may be slightly more involved to set up/use, there is a payoff

long lichen
#

I can see multiple payoffs with both systems and working around those making the project harder to maintain. But I get why you are on the path of developer-driven and text based approach. Many ways to rome I guess 😄

sweet kindle
# dire pumice One alternative architecture/approach to think about with this (especially if yo...

I still have to instantiate 'run time types' as I would need to loop through every plant and every instance of that plant would probably have different values in the middle of the game. I'm not sure if the parsing speed would be appropriate while modifying it runtime. But we could always gather the data from JSON and create a run time template and access the run time template through the wave.

But I just want to stick to SO's as I know the workflow. Thanks for the idea btw!

sweet kindle
leaden minnow
#

Instead I tend to build a composable data structure

#

for example a plant may have a list of Actions. And those Actions may each have a list of Effects

#

and an Effect may have a list of string/float pairs or enum/float pairs which hold the data they need

#

there may be inheritance in a runtime-only class that actually parses/implements the attack or effect from this data

#

but the data model itself avoids inherticance to simplify the type hierarchy that belongs to the "game data" that needs to be serialized etc

#

So for example an effect may have [("damage", 10), ("range", 50), ("attackSpeed", .5)]. And another effect may have [("range", 10), ("healing", 15)]... and so on

sweet kindle
#

But in that case, the data model would have parameters which will never be used? For example the water based attack, does not need projectiles parameter. They would just have to be null in that case?

leaden minnow
sweet kindle
#

And the projectile parameter would never need "liquid type". All the attack types will have all possible parameters, whether they are being used or not?

leaden minnow
#

The list of attributes only has the used parameters

#

That's why it's a list of string/value pairs

#

Not a big class with all possible fields

sweet kindle
#

The issue I see with storing them as string/value pairs is if I have 30-40 different kinds of plants, it would be hard to experiment with different kinds of attack types etc?

leaden minnow
#

Not sure how that would be related.

dire pumice
#

For this you could use an interface "IAttack" and implement that in the ScriptableObject, then you could maintain a List<T> of your ScriptableObject data, pass it in at runtime and still call (plant.Attack()), you are using a contract in this case but you can not limited to the inheritance structure. Once you have your ScriptableObject data then you can use that at runtime and change it however you like. You can even maintain a list of attacks "List<IAttack> attacks", then interate through and call "Attack" on all objects that implement that contract. Going for a compositional approach over inheritance will make your game a lot more flexible. another approach is just restrict your ScriptableObject to data only (no inheritance, no interface), load that data in and then in your systems, you can maintain a List<T> of those interfaces and perform your different attacks

sweet kindle
#

With SO's I just have to quick swap

leaden minnow
#

it's not mutually exclusive

sweet kindle
dire pumice
# sweet kindle With SO's I just have to quick swap

You don't necessarily have to swap ScriptableObjects. ScriptableObjects for runtime data isn't ideal (one of the reasons they make for poor save/load systems). You keep the data for authoring and then you can parse/load that data into memory and use that as your runtime data. You load the ScriptableObjects into memory at start time and then you are free to modify that however you like. Personally, I would keep the SO as pure data and then let my systems modify that data and runtime once It's loaded

sweet kindle
#

I wont be swapping SO's during runtime. What I mean is — under this Plant SO, there will be an attack type SO. I can switch this attack type SO from projectile to bomb or fire to experiment with.

#

Yes you're right. I will only be keeping the SO as pure data.

leaden minnow
#

For the record Painkiller and I are not arguing for the same model here (i don't think)

sweet kindle
#

yes

dire pumice
#

Just have two types of ScriptableObjects, instead of "PlantSO : AttackSO" have "AttackSO, PlantSO", no inheritance or contract, just two or more ScriptableObjects with your data

sweet kindle
#

The base data is Plant SO

#

But now, I seem to have better ideas with the Interface and all. I'll work on it.
Thanks!

#

I'll brb.

dire pumice
#

I see but you could design them without the inheritance model and have something like this:

public class PlantData : ScriptableObject
{
    public float growthSpeed;
    public int harvestMoney;
    // More fields here
}

public class AttackData : ScriptableObject
{
    public float cooldown;
    public float projectileSpeed;
    public int damage;
    public float explosionRadius;
}

This way you keep your ScriptableObjects and data and your systems/implementation can use the data, you don't necesily need the inheritance structure because you aren't adding behaviour. Instead you just create a few ScriptableObjects for the certain data you need. AttackData will fulfill any requirement for any projectile you would need and then you just use whatever the system needs