3
\$\begingroup\$

Since I'm making an RPG in Unity, I need a place to store all the possible moves/attacks in the game. These have the following properties:

  • there will be a very large number of them, in the hundreds perhaps;
  • apart from the natural tweaking process during development, they will remain unchanged forever: they're read-only data;
  • they must be easily accessible and sortable whenever and wherever I want.

Initially I thought I would solve this problem very easily by creating a simple Move class with the relevant attributes (damage dealt, range, etc.), and then I would create a second file, possibly a ScriptableObject, which declares a global empty list of Move elements. I would then move onto the Inspector, which has a very nice UI, to quickly fill in the list by repeatedly clicking on ‘+’ and filling in the various attribute fields by hand.

However, I soon ran into an unexpected problem: the Inspector usually updates the list whenever the Move class is changed (e.g. if I add an attribute, it will be automatically added to all elements with a default value); but there are some circumstances, like if the script is temporarily unable to be compiled due to a mistype or another similar error, where the changes aren't backward compatible anymore, and so, after fixing the issue, the list in the Inspector will become empty. Since the elements were created from there and are nowhere to be found in the code, they are effectively lost forever; and there will be hundreds of these moves, so I can't afford losing them so easily.

So I thought that I should initialize the list directly in the code, but naturally C# won't allow me to: the script should only define the class and not initialize an instance of it, so if I try to give the list some elements, I get an error; and if I put the initializing part within Start() or Awake(), that part of the code just won't run and the list in the Inspector will still be empty unless I fill it in from the Inspector itself.

At this point I'm thinking of rather extreme solutions like writing a JSON file to hold all the information and have a script somehow read them from there when needed, but surely there is a better way to deal with this type of data in Unity, right?

\$\endgroup\$

2 Answers 2

6
\$\begingroup\$

My preferred workflow here is to author the move data in a spreadsheet, then write an importer script in Unity that reads the spreadsheet and creates or updates a ScriptableObject instance for each move in the list.

That way I can make bulk edits and analyses of the balance of the set of moves as a whole in the spreadsheet tool, and then update the in-game versions with one click.

This does lead to a lot of asset files, but if you just dump them into their own subfolder then they don't clutter your workflows. Most common edits can be made through the spreadsheet, so you usually only have to touch the individual files for data that's less convenient to author in spreadsheet form (like references to image/effect assets in your project).

To organize them into lists/categories, I'll have another ScriptableObject that contains a list of references to the individual assets, also automatically updated by the import script. Then my game code can reference those index objects to always have an up-to-date collection of all the current moves/etc.

Having each entry as a separate asset makes it easier for version control and working in parallel as a large team, as well as avoiding/isolating data loss in cases like you describe. It also lets you make multiple versions of a move that all exist simultaneously in your data, and switch between them as you try out which variant you like, without having to re-create the data each time you want to try another variant.

I do this as an exercise with my students, having them design battling monsters and their moves, or ingredients and recipes for a crafting system, and working through the steps of writing the importer.

It's a bit involved to fit in a StackExchange answer in its entirety, but I usually use the EPPlus library (an older, more permissibly licensed free version) which can read and even write to Excel .xlsx files. This and the source spreadsheet can go in an Editor folder so it doesn't bloat your game build. You could do something equivalent with CSV, though you lose the convenience of things like named ranges, conditional formatting, formulas, and charts. I'd bet you could make it work with a Google Sheets or LibreOffice Calc document if you prefer free/non-Microsoft alternatives.

\$\endgroup\$
4
\$\begingroup\$

You might be able to solve this by turning Move into a scriptable object as well. Those are much more stable in case of changes than POCOs in a serialized array of a MonoBehaviour or ScriptableObject. They certainly won't just get deleted by the engine on a whim, because each move is an asset file in the filesystem.

One possible way how you could lose data about your existing moves is if you would rename fields in the Move class. If you find yourself in a situation where you have to do that, then take a look at the [FormerlySerializedAs] attribute. It can help with migrating existing SOs to a new format. Retaining the data through more complex refactorings might require a bit of editor scripting to transform and copy the values from old fields to new ones, but that's usually a solvable problem.

If the purpose of the referencing behavior or SO is just to keep the references to the moves somewhere so they end up in the game build in a way that the game code can find them, then you might want to consider using the optional Addressables package instead. Those allow you to give each Move a unique string identifier that can be used to load it at runtime. So you no longer need a "data-holder" object for them. You can also load your complete catalog of moves at once by giving each move asset the same Label and then load them all with Addressables.LoadAssetsAsync.

\$\endgroup\$

You must log in to answer this question.

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