Learning game architecture with Unity

So I’m an enterprise software developer dabbling in game development, as many of you probably are. I’ve been playing around with Unity on and off over the past couple of years. I’ve participated in two game jams, completed a couple of tutorials, read some docs, etc. But something I’ve continuously struggled with in Unity is “putting it all together.” I don’t know if I’m missing some critical knowledge, or if it’s just practice, but it feels like transitioning from “making minigames and tutorials” to “making an actual game” in Unity feels unintuitive and cumbersome, and I can’t tell if it’s the engine or just a gap in my knowledge.

One of my major issues is that, from what I can tell, there is no “entry point” into the system (which is generally where I would build a DI container and register dependencies in an enterprise environment.) Unreal Engine has the concept of a “game mode”, which is your single source of truth and controls the overall game flow. I feel like the lack of this concept in Unity is making it very difficult for me to feel like I can orchestrate anything more complex than a minigame (or at least, without feeling like I’m bypassing the engine completely.) Are people storing everything that is “global game state” in ScriptableObjects? If you’re using a “master scene” to control the overall game flow and loading in others additively, how are you communicating between scenes? How are you testing additive-only scenes that rely on the overall game state? How are you handling cross-scene dependencies?

Is the only solution here to use anti-patterns like Singletons? Unity recommends not using “DontDestroyOnLoad.,” but we could use ScriptableObject singletons, presumably - that way they avoid the whole “static instance/state” situation.

Is there a decent place to learn game architecture (in a way that is at least applicable to Unity) that is a bit more in-depth than just micro games, and that actually uses best practices and doesn’t feel like 100% throwaway code?

Have you had a read through this before: https://gameprogrammingpatterns.com/contents.html ?

A lot of the patterns described there really help alleviate some of the issues you are talking about, and certainly will be a good starting point if you have not read it before :slight_smile:

1 Like

I have, actually, and it’s a great resource. It’s great to get an understanding of several programming paradigms. It would be even more useful if I were building my own engine. It also doesn’t really do a lot to answer my questions, unfortunately - ideally what I’d like to see is how a larger, more complex project was put together in Unity.

Proper construction injection and readonly fields for dependencies can probably not happen since how unity is built. But there is third party DI frameorks for unity.

1 Like

If your goal is to avoid static, which is a good idea due to reinitializing everything manually, then you can create a single object and DontDestroyOnLoad as I don’t see a reason not to. Then create your ‘cross-scene’ classes inside that object’s attached monobehaviour and pass reference to those that need to talk to them.

1 Like

What concerns me about this is reading Unity’s best practices, they say to not use DontDestroyOnLoad:
https://unity.com/how-to/how-architect-code-your-project-scales

Did you read the explanation? They’re just saying that additive scenes are more flexible. DontDestroyOnLoad is just a hack additive scene, basically. They’re really just the same thing. If you create your own then you have more control over it, though.

1 Like

I don’t see another way to have classes that are not bound by scenes unless using static or DontDestroyOnLoad.

2 Likes

I don’t understand their reasoning, or don’t agree with it

I’d agree that additive scenes are in many ways better, but you can call Destroy on a DontDestroyOnLoad object any time you want, but that somehow means you have lost control over the object’s lifetime? Ehhh I must be missing something here.

1 Like

Maybe so. I’ve definitely seen cases where Unity’s “best practices” will say something like “we recommend B rather than A because B is slightly newer and more consistent with our new vision” and then a few days later people will insist that “Unity has declared A dead!” “A is a performance killer!” “We won’t even hire people who mention A!” “We had to start our project from scratch because it had traces of A in it.”

I just think it’s nice and organized to have a scene for all of my persistent stuff, like my main character, HUD, input handler stuff that I can set-up in the editor. I can go to the editor, just load any arbitrary scene in the game and then also load my player-stuff scene and it will automatically work when I press play.

Just curious, how does your DontDestroyOnLoad object come in to being at runtime?

2 Likes

The lack of a centralised entry point used to bother me, too. These days it really doesn’t, though. Partly because the way I design stuff now it rarely matters, and partly because it’s trivially easy to make one yourself.

For multi-scene games I generally have some kind of common scene which loads and unloads other additive scenes. That common scene gets loaded first, has whatever “manager” stuff I need in it, and allows me to use Unity’s normal entry points - Awake, Start, and so on.

Cross-scene referencing is really something that Unity should solve at an engine level. They did provide an example reference solution a while ago, but I think it’s a bit silly that it wasn’t treated as a core requirement when they added multi-scene editing support. Anyway…

Communication between scenes is handled by a cross-scene referencing system and events.

Dependencies are taken care of by having planned ahead of time what scene can be dependent on what, and writing some simple editor tools to help out with that. For instance, if I load a scene to edit and it has a dependent scene, that also gets loaded automatically.

For some parts of the game that would, by default, require loading so much stuff that editing and testing would be prohibitively difficult. For those cases I have a “test framework” scene which fulfils the dependencies but is designed for development and testing rather than for normal playing. So a small part of the game gets loaded in a void with just stuff that we’d use while testing it.

Another consideration here is designing your game for testability in the first place. Making stuff that’s too dependent on a highly flexible game state also means that you’ve got a massive surface area that your testing needs to cover. This is an especially huge issue for the parts of testing that you need humans involved in.

No, they’re not the only solution, but they’re a solution and when used appropriately they work pretty well. Avoiding something because it’s been labelled an “anti-pattern” is ignoring the specifics of your actual situation, and probably isn’t helping. If you’re an experienced software developer then have faith in your own ability to weigh up the pros and cons of a given solution in your specific situation and make a call based on that.

But, a couple of things to think about.

As for other solutions… the first is to keep in mind that Unity’s scene structure gives us a whole bunch of tools to help deal with this stuff, so one approach is to embrace and heavily lean on that. There are a variety of ways to search or query parts of a scene to find stuff, which is how I handle most of my dependencies. If I am an X I know I should have a Y in my parents / children. If I do then register / hook up / whatever, if I don’t then raise an error because something is busted.

Second, you can go the complete opposite extreme and just consider MonoBehaviour to be an adapter between your own game model and a Unity scene used for presentation and physics. You have one script somewhere which kicks off your own model (which you can make sure is run first) and then you can handle architecture for that however you want. I’ve done it before and I’ve seen others do it before and it can work quite well for certain types of projects. For other projects it just makes a lot of work building alternatives to stuff that works perfectly well out of the box. But, again, if you’re an experienced dev weigh up the pros and cons - maybe this woudl be a fit for you and/or your project.

4 Likes

Yeah I wasn’t disputing that additive scenes are arguably superior, just that the stated reasoning why was nonsense. Saying additive scenes are more organized than using DontDestroyOnLoad though is an actual valid reason I’d agree with.

I actually prefer the additive scene way if I’m planning on more than a trivial number of these objects. But I’ve also used DontDestroyOnLoad in a splash screen scene, or instantiate a DontDestroyOnLoad object from prefab when first needed. I prefer the splash screen scene way as they are still fairly organized in one place. Though from prefab can make it easier to test scenes which wouldn’t normally instantiate them (you can either just have some code in all your scenes for testing purposes which just instantiates them, or if you are lazy you can just drag them from your Assets folder into the scene, run your test, then delete them again).

My current project has more of them than I’d like because I originally used the Unet HLAPI which did not handle loading a NetworkManager in an additive scene. Even though I’ve replaced Unet, I have just stuck with the DontDestroyOnLoad objects because I always have something more important to work on. Maybe I’ll come back to this when I start getting irritated with it :stuck_out_tongue:

1 Like