I am going to post this which is notes I made after a company I worked for hired a bunch of academically trained C# guys to work on a project made the Unity component way. They were lost and hated the most powerful tool in the Unity arsenal…The Inspector. It may help wean you away from your propensity for playing by the rule and using design patterns.
"
Unity, C#, Mono and .NET
C# engineers are initially going to be the wrong people for be programming games in general. They’re trained in practices that work actively against how Unity uses C#.
So they need to understand that game development is about making up new rules (breaking them) because the rules aren’t designed for games.
In the world of game development, it’s all about optimizations because it has to run within a specific time limit in order to be interactive and enjoyable.
This means breaking the rules.
The earliest game developers (since the birth of the very first game) up to today, had to be about breaking rules because the hardware wasn’t (and still isn’t really) designed for games. So to make games you need to play outside the established set of rules. That means using things that aren’t designed for something, and not using things that are designed for something (in a language and hardware context).
This typically means that most optimizations are breaking the “rules”.
Of course to any game dev using Unity, we’re not really aware we’re breaking rules, we’re just flexible enough to think well outside the box, because that’s a normal every day mode of thinking for the humble game developer.
At its core, Unity uses Mono. Mono is an implementation of the .NET framework. That by definition means it’s not the same framework .NET developers may be used to. Even though it’s supposed to behave the same as the framework they’re used to, not all things are implemented the same way (if even at all).
Unity took this a step further and changed a few rules about what to expect from C# via operator overloading – using bool to check vs null, and overloading equals to check against a fake null being just two examples. This also broke newer C# features such as null propagating and coalescing operators. With constructors being forbidden, readonly fields become harder to set. And so on.
Hell, Unity doesn’t even use polymorphism to call Update() in MonoBehaviour derived classes. It feels a bit alien to not include “override” when creating an Update() function. But that’s just how Unity works.
So if anyone argues “But it works this way in .NET…” it’s already irrelevant. There is a definitive distinction between "C# vs Unity C#”.Some random thoughts:
-
If they want to stick with their .NET code, then fine. Just keep it in DLLs. And create an API to let their .NET code talk to Unity and vice versa. If they want to work within the Unity namespace, they must adhere to Unity’s rules. No ifs, ands, or buts about that.
-
You use an API in the way it’s meant to be used. You don’t try to bend the rules or work around it, then complain that the API doesn’t work the way it should. Thinking you can write your own buttons for example and use physics raycasting instead of a Unity Button component hogties any system that uses the UI eventData looking for interactable bools, loses for free animation and colorBlock transitions from your toolkit. Using the OnClick event delegate is the exact same but faster as intercepting it in script. Real world testing has proven this even though it looks like they are the same. Unity does something under the hood with pooling and reassigning delegates on the fly.
-
If upgrading Unity versions break your code or objects and force you to re-engineer it, then see point #1 about adhering to Unity’s rules. Note, I am not talking about Unity bugs – these are very valid reasons on skipping certain versions while upgrading. I am talking about how if you try to access internals via reflection, then your reflected code breaks because the internals changed, well guess what, that’s on you for depending on internals to never change. (Note: This should be extremely unlikely now that Unity open sourced their C# code, but the point still stands). Setting up objects to be normalized with a uniform scale and Quaternion.Identity rotation and it’s coordinate system position at 0, 0, 0 will avoid serious headaches when Instantiating objects.
-
Similar to #1 above, if they’ve ever worked with WPF, they should be familiar with MVVM. Or MVC. Keep the logic and UI code separate. Apply this line of thinking to Unity – if you’re going to get all fancy with your .NET code, keep it out of Unity code. Changing your code should not break Unity. Changing Unity UI should not break your code.
-
As has already been said, game dev is very different from business dev. Unfortunately it’s very hard to convey this point to folks, especially if they have little technical knowledge of the underlying issues. You have people with specialized skills and you don’t try to put them on jobs they’re not suited for. Even though architects and electricians work together to build a house, you don’t expect an electrician to draw up blueprints for the house and expect the house to not collapse. Or architects to not die from electrocution. And so on.
The irony in saying that games development “breaks all the rules” is that it can be tempting for a non-technical person to say, “Well isn’t the opposite also true? Can’t you break game dev rules then?”
This isn’t even a C# issue. There are similar arguments way back in the C++ days when avoiding the STL was a way to optimize your game. Avoiding the STL was a positively alien concept to business devs. Some things never change, I guess
-
unity uses C# as a scripting language, not as C#, not as .NET
-
as a scripting language, C# has some benefits, none of which come from .NET
-
unity is a constantly evolving game engine, getting better all the time
-
unity is a game engine, not a corporate platform, like .NET
-
using the latest releases of Unity has LOTS of advantages, few negatives
-
moving up through Unity versions doesn’t entail a lot of scripting changes
-
moving up through Unity versions often bring benefits worth the effort
-
Unity is better than it ever was, for most platforms, in 2018.1 onwards
-
capable .NET programmers are perfectly equipped to move up versions
I think the really important distinction to be made is that while you’re writing code in C#, the majority of what the computer is doing isn’t in the scripting runtime. I would point out that the scripting runtime sits on top of a game engine, and understanding what’s going on in the game engine part of the system is really important. A dev might know C# really well, but that’s only part of the picture.
“to a man with a hammer everything looks like a nail”.
Whether you use IL2CPP or Mono or not, data must be marshalled to C++ since that is what the actual engine runs in. The engine will always run as C++
Another thing of interest is how to allocate variables in a class. In most business .NET software, variables get declared inside functions and cleaned up automatically as they go out of scope. With a game, there is often some performance to be gained by declaring some variables at class level and re-using them throughout functions. It goes against modern scoping concerns, but it addresses memory and performance issues that are usually more important in games. If you need a reference then store it at Start. Do not ever get a reference in an Update loop.
On Unity nulls
if(gameObject!=null)
A faster solution to a .NET dev is to use the following:
if(!System.Object.ReferenceEquals(gameObject, null ))
Keep in mind that those two lines of code do not actually do the same thing. If you destroy the GO, the top line will work as you expect, but the bottom line will not since it still references the container object. Unless you actively assign null to the variable, that won’t work in a lot of cases. Unity uses a fake null object and overloads ==, which means that using reference equals does not return the same thing as a regular null check. A GameObject can pass a reference equals check, and still throw a null reference error on the very next line.
Instantiate/AddComponent creates a C# object and the corresponding C++ object together. It registers the newly created GameObject/Component with the various systems Unity uses. (Incidentally calling new on a GameObject does the same thing too, don’t be afraid to use new GameObject(“SomeName”) when the situation calls for it).
New is used for creating regular vanilla C# objects. It goes ahead and calls the constructor.
The thing is you can’t use new to create a MonoBehaviour or to add Components to a GameObject. Unity will throw an error. Even if you do create your entire structure without GameObjects, you still need to use GameObjects and Components to interact with Unity’s rendering system (and physics ect too). Which means in most cases, you are better off just using components in the first place, rather then putting an extra layer in between you and the engine.
Under the hood Unity reserves the MonoBehaviour constructor for its own purposes, and does stuff that isn’t fully publicly documented hooking up internal engine stuff. There are other ways that probably could have been handled, but that’s what Unity picked, so constructors are just out of bounds to us. Because of that they instead give us Awake() and Start(), but they really aren’t the same thing for two notable reasons:
- You can’t give them parameters.About the closest we can get is a static CreateBlah(…) method which calls Instantiate or AddComponent and then does whatever else we would want to do.
- Awake and Start are called at different times to a constructor. I believe that Awake is immediately after construction, so that one’s pretty similar, but Start is called as a part of the next iteration of the engine loop. Understanding that engine loop and what it means for your objects and their lifecycles and when you can reliably access what things is pretty important, and fairly specific to a game engine (or similar real-time systems).
This is of course only relevant for MonoBehaviour and friends. You can make plain C# classes and use their constructors and that’s all groovy. You can use those classes from your MonoBehaviours, and you can have those classes reference MonoBehaviours to interact with the scene.
I despise going though code and seeing myVal = 7f or myText = “hello world” and then looking in the hierarchy to find what this generic thing is referencing and everything has the default name. It smells to me like copying sample code and shows no regard for other team members working with your stuff.