Tuesday, March 15, 2016

Unity Project Layout, Part Three: Logical

Apart from having discreet scenes, Unity pretty much has no 'opinion' on the logical organization of your program.

There isn't a lot written on this that I can find (ok, online, maybe there are print books). Most of the advice is general, but sound: Separating interface from game logic (I'd go further and say you should separate game-state from game logic as well). Name things clearly. Etc.

There's a lot on folder structure, but not on game structure. I'm not necessarily talking about the gameplay code (platformer vs real-time-strategy vs RPG, etc). What I'm going to ruminate about is the structure around that; handling startup, state, game data, save data, etc.

And, as always, everyone has there own way that (probably) works for them. This is simply how the manner I've deal with these issues has evolved.

Game Entry

Most games, most programs, need a certain amount of initialization. Since everything in Unity has to be in a scene, that predictably leads to a startup scene. The name's irrelevant, 'startup', 'initialization', whatever. This scene does, however have to be set up as the number zero in the scenes list under the Build Settings menu item, as seen to the right. No matter what it is, the scene at the top of the list, number zero, is loaded when your game starts.

Once that scene is loaded you can them programmatically control the flow of your game.

By convention I call that first scene 'Startup', which may seem less than surprising.

In my startup scene I like to keep things very simple. There are only two objects in the scene, a SceneControl object (StartupSceneControl.cs) and the MasterControl object (MasterControl.cs).

StartupSceneControl.cs does only one thing, wait for the end of frame and then load the scene 'LoadData'.

The MasterControl Class

The MasterControl script instantiated here sets itself as 'Don't Destroy On Load', and so will persist through scene changes unless explicitly destroyed. This is the key class in our game that will be the central hub.

In its role as the central object in our game, MasterControl holds the current game data, CurrentGame. This is of course the current state data for the game; we serialize and deserialize this to load and save.

The handy thing about this is that in any script, anywhere, the current game state is simply:

MasterControl.Instance.CurrentGame

This gives us access to the one, and only one, copy of the current game state. MasterControl also has LoadGame and SaveGame functions which receive a path, and will serialize or deserialize a GameData object appropriately. Really it just calls CurrentGame.SerializeGameData or the opposite.

The MasterControl object is also the container for all our 'static' data. In the case of JumpShip, there's a bunch of constant data that is, well, constant, so I didn't bother putting it in a config file:

MasterControl will also hold all our data loaded from config files, that'll come later in the LoadData scene.

The MasterControl script is how you access turn processing. As you may have noted above in the MasterControl code there's a TurnProcessor class. As I've reused basically the same MasterControl class in several different projects, the actual turn processing can be set up via dependency injection. At this point I don't do that; there's only one so it just creates a TurnProcessor (derived from BaseTurnProcessor) in Start.

MasterControl provides a wrapper around processing turns (as JumpShip is basically a turn based game at heart, albeit with time-variable turns). You can advance one turn (i.e. clicked 'Next Turn'), or they can automatically recur until stopped, or some event trigger brings them to a stop.

Here, the LoadData scene controller triggers MasterControl.LoadStartupFiles, which simply activates a coroutine to start loading our data. That coroutine simply processes one data file (there are dozens, and I'm adding more all the time), then sends a message to the sceneControl script so that it can update the progress on-screen (I could add a progress bar, etc). For example:

This sends a message to the sceneControl script, which displays that it is loading the StarTypes data file. The file is then processed.

I load all this data in a coroutine, waiting a frame between each, so that things don't completely freeze up and there is some progress indicator on screen. The loading time of each separate file is negligible, though the whole lot of them might take a second or two. I could do this in a separate thread (and might make it so if any one data file load was too long), but it isn't currently worth the effort.