Unity Good Code Architecture

Context, I’m primarily a C# API dev and I’m busy working on a 2D Unity game. My knowledge of Unity is mostly cobbled together from various tutorials and random googling over the years.

The game I’m working on is coming along pretty well and I’ve ended up veering away from Monobehaviours for most my logic, partially due to familiarity, not knowing how to automate testing on Monobehaviours, a penchant for a spot of generic polymorphism and a distrust for Unity’s object life cycle.

For the most part I’ve gotten things to a point where it makes me warm and fuzzy.

There are some parts though where I’m stuck on “what’s a nice way of doing this?” I can think of numerous ways of making it work, but none of them sparks joy.

Specifically I’m stuck on assets and very specifically on access to “global” assets, like icon sprites. I dislike using the UI for creating references as I’ve found it to be flakey and difficult to maintain at a large scale. I’m currently using a scriptable object to cart them around, but it also feels iffy. What’s the best way to handle this?

More generally, can someone perhaps recommend some practical examples of well designed, complex Unity games? I’ve found many tutorials and examples but they all seem to be either very simple or written in a way that will become awful the moment it scales up.

(Also, I’ve ended up writing my entire game in a UI Canvas (as it’s 2D and a very… UI game), is there a massive detrimental consequence to this that I’m missing?)

How I would do this is to make some kind of game controller object and game controller script that basically acts as the master control for everything. This works really well for games where there needs to be a lot of things happening and interacting at once, because it can reduce lag by making all interactions controlled by only one object.

As for making it all on a Iu canvas, I don’t think there’s anything wrong with that. I think it might be harder, but you can do it if you want.

Use Monobehavior is easy to write samll function,but when project is big,i think it is not easy to control you project。
There is something bad i think.

  1. The code logic is depend on GameObject and it has gameobject this excess information。
  2. As you say,life cycle, if you Instantiate one GameObject, all scripts on GameObject will call Awake, Start, it is uncontrollable。
  3. Serialization, if you put too msessage in MonoBehavior Inspector for easy use, if you want to change a class strctue, it is too difficult, and if this Monobehavior Used too many place, it will be a disaster。

Unity Official Example always use Monobehavior to write, i think it is not good。
In My Poject,i use C# class(Not Inherited from Monobehavior) to controll GameObject (or Monobehavior),the logic running without gameobject,and game only has one entry。

Pseudo code:

public class GameRunning :Monobehavior
{
     void Awake(){ SystemMnager.Init(); }
     void FixedUpdate(){ SystemMnager.FixedUpdate(); }
     void Update(){ SystemMnager.Update(); }
     void LateUpdate(){ SystemMnager.LateUpdate(); }
     void OnDestroy(){ SystemMnager.Destroy(); }
} 

SystemManager controlls all C# class manager(Not Inherited from Monobehavior) UIManager, AnimatorManager, LevelManager
All you login should begin fom this entry,you can control you life cycle from this.

About c# class controll GameObject:

Pseudo code:

public class Entity
{
     public GameObject Go{ get;private set;}
}

I created a c# Object Entity to running logic and controll gamobject, insatead of Instantiate a Monobehavior and ues unity’s life cycle to running my logic, also if logic is no need to controll the gameobject,it should not has the Go field.
you r writing a 2D Game,i do not know the game of the playing method,but i do not think use ui is a good way,my team use SpriteRender Way to achieve the game logic,UI better only use for Panel Show,UI rebuild it‘s not a good thing and i think controll ui on Game Main Logic is too fussy, I am not srue this is useful fo you。
My English is bad, I hope I can help you。

Regarding the assets, the runtime data needs to have a serialized reference to an asset - you can’t access assets by path or filename in runtime, only in editor - either by assigning references in the inspector, or by creating the reference by scripts, using UnityEditor.AssetDatabase class.
The reason for this is that all asset references are known during the build, therefore Unity can create a complete dependency graph, discard unused assets, bundle the included assets according to their usage, etc.

If you are trying to somehow automate or code-drive the asset reference acquisition, use AssetDatabase.

Of course, there are other options, like Resources folder, addressables… but that’s not the default workflow and requires different approach.

From my experience, I can tell you that I did (more than) once the same mistake as you, “veering away from Monobehaviours” too much, and in the end I could have as well just written my own engine. Find some compromise, do things the Unity way, and build on top of that… or not, that’s really up to you.

Unity has a testing framework that also supports loading scenes and running tests in them.

Maybe try to elaborate more on what exactly you are doing, why are you separating your logic outside of Monobehaviours (a move I completely understand) or what exactly are the uncertainties you are feeling. This was always an interesting topic for me.

Depending on what you’re making, there’s essentially two approaches that I think will fit your workflow:

  • Addressables/Resources and manual loading/unloading of the assets you need
  • A singleton ScriptableObject that holds a reference to every asset you need

The singleton is a lot easier, but it has two big implications:

  • You load all the assets all at once at some point during your startup.
  • You keep those assets in memory permanently.

If the game is small enough, those two things could be good. They’ll prevent any additional load times, and your code can be simpler since you never have to unload anything.

It’s also easier to switch from the singleton approach to an Addressable approach rather than switching from a default approach (prefabs with asset references) to an addressable approach.