0% found this document useful (0 votes)
11 views8 pages

20 Advanced Coding Tips For Big Unity Projects

20 Advanced Coding Tips for Big Unity Projects

Uploaded by

dufenmiao
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
Download as docx, pdf, or txt
0% found this document useful (0 votes)
11 views8 pages

20 Advanced Coding Tips For Big Unity Projects

20 Advanced Coding Tips for Big Unity Projects

Uploaded by

dufenmiao
Copyright
© © All Rights Reserved
Available Formats
Download as DOCX, PDF, TXT or read online on Scribd
Download as docx, pdf, or txt
Download as docx, pdf, or txt
You are on page 1/ 8

20 Advanced Coding Tips For Big Unity Projects - YouTube

https://www.youtube.com/watch?v=dLCLqEkbGEQ

So, you finally decided to begin work on your dream game: a fantasy MMORPG sandbox Battle Royale
powered by a blockchain economy. What could possibly go wrong? Then, two months later, progress comes
to a grinding halt. You have scripts that are a thousand lines long. You've forgotten what your old code does.
Adding new features means you have to rewrite three old ones. Every script relies on every other one, and
overall, your project becomes an unorganized, unmanageable, confusing dumpster fire of spaghetti code.
Tragically, you are forced to scrap the project and give up on your Game Dev dreams. Sound familiar?

There are hundreds of hours of Unity tutorials online, but very few are geared towards more advanced
developers aiming to create large-scale commercial games. That's why I've compiled a list of some of the most
valuable Unity coding tips that I've learned over the years, along with examples of how I've actually used
these techniques in my own game. Hopefully, by the end of this video, you'll have the tools you need to write
scalable, well-structured, clean code that won't come back to bite you down the road.

I've ordered these roughly by difficulty, so stay till the end for the most advanced techniques.

Let's start off with the simple one: variable names. Many new programmers get way too excited when they
realize that they can name variables whatever they want. In case this is you, consider that when you come
back to a script after a few weeks, there's no way you're going to remember what e and sussie and your mom
mean. Variable names should be concise but meaningful, and function names should be verbs. If you really
want to go above and beyond, consider following some sort of strict naming convention. I decided to use the
standards outlined in the C# documentation: public fields are Pascal case, private instance variables start with
an underscore and are camel case, and constants are in all caps. You can also define your own rules and
conventions if they make sense in your project. For example, I used camel case for variables exposed to the
Unity editor, and since I'm making a multiplayer game, I've decided to start all networked variables with the
prefix "net". The key here is consistency, especially if you're working with a team.

Moving on to equally groundbreaking advice: comments. Use them frugally, but definitely use them. Use them
to describe the purpose of complicated instance variables, explain confusing blocks of code, or remind
yourself of things to implement later. Do not use them to explain things that are obvious, and do not use them
to hoard old code for weeks on end; that's what version control is for. Comments are especially useful for
explaining the purpose of functions. This is something I picked up from my university programming class; the
professor insists that every function should have a comment summarizing that method. On public methods,
consider using documentation comments, which you can use to provide pre/post conditions, a description of
parameters, and an explanation of the return value.

Continuing with code readability: encapsulate code in functions wherever possible. Instead of having methods
that are 100 lines long and if statements nested five times, break up your logic into functions, and break up
those functions into more functions. This helps to maintain a level of abstraction so that anyone can tell what
your program is doing at the highest level. Then, if you want to get into the weeds, you can look at the
implementation of one function at a time. I used to pile all my logic into the update function, and I would put
a comment above each block of code explaining what it did. If you use well-named functions, you shouldn't
need comments to explain what your code is doing.

Plan out your code before you write it. You can do this with a good old-fashioned paper and pen, in a Word
document, or even directly in your IDE with comments. I'm talking diagrams, flow charts, pseudo code,
whatever floats your boat. Trust me, the time saved from debugging and rewriting code will be worth a little
extra forethought in the end.

Sometimes, you'll need access to a variable in a separate script. You may be used to simply making this
variable public, but this means that the value can be changed by any script in your project, leaving this
variable open to external modification can cause logic errors and cause the class to behave in unexpected
ways. For example, there's a reason why C# doesn't let us modify the count variable of a list. The standard
solution is to make the variable private and then add a public function which returns the variable's value,
called a getter. C# has a shorthand way to write these getters and setters: if you want a variable to be publicly
accessible but not publicly alterable, make the setter private and the getter public.

A similar problem arises when we want to be able to set a variable from the inspector. Most tutorials will tell
you to just make this variable public, as we just discussed, making a variable public can cause other issues.
Luckily, you can use a tag called [SerializeField] instead. This will allow you to access the variable in the
inspector but not from other classes. It will also clean up your Intellisense.

Unity is built around a component architecture, so it makes sense to write our code like components as well.
This means instead of having one script called "player" which handles everything to do with the player object,
you should make a new script for each new feature/system. For example, in my game, I have six scripts on my
player object: a separate one for input, movement, health, weapon handling, grenade throwing, and UI. Each
script works almost entirely separately from the others. Ideally, you should be able to add and remove these
components freely without impacting the other features. If you absolutely need to reference other systems,
you can use the [RequireComponent] tag to ensure that that script will always exist on the object.
Additionally, it's best to design these components so that they work as intended on any game object. For
example, I wrote my health class in a way that allows me to add it to any game object that has health, such as
barrels or drones in the future. This is one of the main advantages of the component architecture.

Enums can be used to represent a closed set of choices. For example, I use them to represent the weapon
classes in my game and also to represent the current state of the match. The coolest part is you can set the
value of an enum inside the inspector with a drop-down menu. In order to use an enum, declare the possible
values in a list, then you can make variables of that type which store one of the choices you specified.

Coroutines always seemed very scary to me, but now that I know how they work, I found them to be super
useful. Basically, coroutines spread a task over multiple frames, allowing you to run code with delays without
blocking the main execution of your program. They are most useful for methods that contain a sequence of
events or a procedural animation. Please note: Coroutines still run on the main thread, so using them is not
the same as threading. I use a coroutine to play my match start sequence since it involves a lot of sequential
time delays. I also use a coroutine to repeatedly place footsteps underneath my characters. To declare a
coroutine, create a function with a return type IEnumerator, then inside the function, you can use yield
return to pause the execution and continue in the same spot later on. yield return new WaitForSeconds()
delays for a certain amount of time, and yield return new WaitUntil() delays until the specified condition is
true. This is a great way to avoid a lot of messy timer with Time.deltaTime. You can start a coroutine by calling
StartCoroutine() and passing in the function name as a string. If you want to call a function after a certain
one-time time delay, then it may be easier to use the Invoke() method. Basically, you can call this and pass in
your function name as a string parameter along with a time in seconds. This will call your method after the
specified delay. You can also use InvokeRepeating(), which will call a method repeatedly at certain time
intervals. I don't need the bots in my game to calculate a new destination every frame, so I use
InvokeRepeating() to update their path every half a second.

Structs are kind of like a light version of classes. A key difference between structs and classes is that a struct is
a value type while classes are reference types. In this way, structs behave similar to primitives: every time you
assign a struct to a variable, that struct is copied. You should use a struct to store immutable data types that
logically represent only a single value. A lot of the time, I use a struct when I want to have a list of some
custom data types show up in the inspector. For example, I have a struct defined in my player UI manager
which represents a UI group. If I mark the struct as Serializable, then I can set the fields of each UI group in
the inspector. I also use a structure to store and transmit input for the current frame, since only the input
handler class should be able to change the input.

Now we're getting to the good stuff: a Singleton is a type of class which only allows one instance of itself to
ever be created. To implement a Singleton, make a static variable of the same type as the class. Then, in the
Awake() method, store the new instance into the static variable or destroy the script if the instance has
already been assigned. If you don't know what static variables are, they are variables that belong to the whole
class instead of just one object. This means that you can access static members from anywhere with just the
class name. Hey, that's pretty convenient! The issue is non-static members cannot be used in static functions.
A Singleton gives us the best of both worlds: if you use the static instance variable to reference the actual
instance of the script in the scene, then you can access all the non-static members as if they were static.
Singletons are most suited for scripts that need to exist within the Unity scene but also need to be referenced
by many different scripts. And remember, there can only be one instance of the script in the scene at a time,
so don't try to use a Singleton for your enemy script, since there will likely be many enemies in your scene at
one time. I use Singletons for my manager scripts like my game manager and network manager. I also use it
for utilities like my object pool; the object pool needs to be in the scene, but I also want to be able to
reference it from anywhere. As part of this tip, I'm going to suggest that you create a manager class anytime
you have a bunch of different scripts that need to access a set of information. For example, the current game
state and the list of players on each team are important for many different scripts, so that's why I created a
Singleton game manager class to handle these systems and expose the information to anyone who needs it.
Just remember that these managers should still only have one job each and should not be trying to handle too
much at once.
In game development, much of our code is event-driven. This means most of the time we're just sitting
around waiting for something to happen. C# events are well-suited for this type of programming, and they can
help remove dependency between systems. In my game, the player movement script and the grenade script
need to know when the match begins in order to reset their respective action cooldowns. However, the game
manager handles the start of the match. How should I solve this problem?

Well, I could loop through every player in the match, get a reference to the player movement and throw
grenade script, and then call a public function ResetCooldown on each. This would work, but it makes my
manager script dependent on the player scripts, meaning I can't change my player scripts without changing
the manager script. Let's use C# events to solve this problem.
First, I'll make a variable of type Action inside the game manager called GameStarted. To trigger this event, I'll
call .Invoke on that action variable. I use a question mark before the dot operator to avoid null reference
exceptions. You can also add parameters like this, but I don't need parameters in this case.

Now, my player script can subscribe to this event by referencing the action variable and then using += to link
its own function to that event. Now, when the GameStarted event is invoked by the game manager, all the
scripts which I've hooked into this action will be notified. This means that the game manager does not have to
deal with my player scripts at all, and best of all, if another script needs to reference this event in the future,
then I won't have to change my game manager to include it.

In general, it's okay if your scripts depend on your manager classes, but not the other way around. I also use
C# events for my health quests. I have an event for when the health is changed and for when the object has
died. This allows me to separate the health and regen logic from stuff like UI and death logic, which is handled
in a different script. Now I can use health as a component that can be reused and added to any object. Unity
has its own event system which you may prefer. It works in almost the same way as a C# event, except you
define a Unity event variable instead of an action variable. The main advantage of Unity events is you can
assign subscribers in the inspector, just like you would for button events. In my game, I've used C# events
instead because I prefer to link things up in code.

In my game, I wanted to code a system that allowed me to animate menu screens, so I made an animation
controller which has a PlayEnter and PlayExit coroutine. Then, inside these coroutines, I looped through all
the elements in the animation, got the component UISlideInAnim, and then called Play on that script. That
works fine, but what if I want to play a different type of animation? Well, I could use one script that has the
logic for every animation, but then I'd need a bunch of if statements inside the Play function, and things could
get very messy very quickly depending on how different the animations are. It turns out that this problem can
be easily solved with an interface.

Interfaces can be used when you want two classes to behave similarly without enforcing a relationship
between the classes. The structure of an interface is similar to that of a normal class, except you don't need a
constructor, and you don't actually provide the implementation for any methods. When another class
implements an interface, they are required to provide the implementations. In this way, an interface
represents a contract that guarantees certain methods will exist in a class. So, how about I make an interface
called IUIAnimation which has the method headers PlayEnter and PlayExit, and I'll make the slide-in
animation and the fade-in animation both implement this interface. Now we have a way to interface with two
different scripts in the same way. We can simply use GetComponent<IUIAnimation>, and that will get any
script which implements the IUIAnimation interface, and then we just call its Play function.

Let's continue on the topic of object-oriented programming. If you ever have one class that is a certain
subcategory of another class, you may want to consider using inheritance. You can make your class extend
another class with a colon, the same way you would implement an interface. This means that all public and
protected members of the superclass will be accessible by the subclass. The main advantage of this is you
don't have to rewrite code shared by both classes. If, for some reason, you do want to change the
implementation of an inherited method, then you can override it. In my game, I have several types of
weapons. For now, there are assault rifles, shotguns, and submachine guns, which all behave a little
differently. Even though they are different, the majority of the code for each weapon is the same. So, I could
either copy and paste the same code into multiple different scripts (obviously not a good idea), have one
script with the code for every weapon (which gets messy real fast), or I could have multiple gun classes that
inherit from a parent class. That's the best option. In practice, this means all the guns can share the same code
for generic functionality like ammo, fire rate, and parsing input. Oh, and what if I want to add melee weapons
in the future? All weapons need to respond to input, and all weapons will have a fire rate, so I can put that
code into a superclass called Weapon. Now, all guns inherit the weapon code from the Weapon class, as well
as the generic gun code from the Gun class. Here's one more trick: if I make the Weapon class abstract, then I
don't actually have to implement every method. Similar to interfaces, subclasses will have to provide the
implementation. Of course, I can no longer instantiate a Weapon by itself because the class is incomplete, but
that makes sense because I'll never have just a Weapon.

Here's a less obvious use case for inheritance that also combines a lot of the previous tips. In my game, I have
bots and I have players. I want each character to use the exact same movement and gun groups just in one
instance. The input will be provided by the player, and in the other instance, the input will be calculated by
the AI. I started off by checking if the player was a bot in each script and then getting the input from the
corresponding source, but this got really annoying and messy as I added more mechanics. My solution was to
make an abstract class called InputHandler. InputHandler has a public method that allows scripts to subscribe
to receive input. Now I have two subclasses of InputHandler: ClientInputHandler and BotInputHandler.
These classes send the character input to their subscribers every frame using that CharacterInput struct we
talked about earlier. Although they populate the struct in completely different ways, because I want to
interact with different scripts in the same way, I'm using an interface here called IReceiveInput, which is
implemented by every class who wants to receive input. This is a common event listener design pattern.
Here's where the magic happens: inside my player scripts, all I have to do is get an InputHandler component
and subscribe to its events. Since the ClientInputHandler and BotInputHandler are both types of
InputHandler, this code will work whether the character has a bot input script or a client's input script
attached. For the most part, all the character scripts don't even need to know if the character is a bot or a real
player.

My weapons have a lot of different stats. I need to reference these stats in several different places, from the
weapon scripts to the weapon selection screen. Fortunately, Unity recently released a feature called
scriptable objects, which are perfect for bundling data. The main advantage they have over normal classes is
you can access them in the inspector without having to attach them to a prefab or object in the scene. To
create a scriptable object, just extend Unity's ScriptableObject class and then add the members you want to
be included. Then, in the project, I can create an instance of that scriptable object by going to Assets > Create
> New Weapon Stats. Now, I can rename it and set all the variables. Here's where things get cool: I can
replace all the stat variables in my weapon class with a GunStats class, and then watch this: I can just drag the
GunStats object into that field in the inspector, and now this gun script will use all the stats from that
scriptable object. But wait! Different weapons will use different stats. Lucky for me, scriptable objects can also
extend each other, which works exactly as you would expect it to. The ShotgunStats object now includes all
the variables from the GunStats object. And because of polymorphism, the gun script can use a ShotgunStats
object the same as it uses a GunStats object. I also use a scriptable object to hold the entire weapon set,
which I use in the weapon selection screen.

Custom editor tools can range from crazy UI menus to simple scripts that interact with the scene outside of
play mode. Even simple tools can massively improve your workflow. I just recently started experimenting with
these. If you saw my map creation video, you know that I had to add box colliders throughout the entire
scene. The problem is, Unity generates a navigation mesh with the mesh renderers, not the colliders, meaning
the navigation mesh didn't match the actual colliders. This caused my bots to get stuck trying to reach places
they couldn't. To fix this, I made a script that looped through all the colliders and generated a mesh to match
the bounds of the box collider. Then, when I wanted to generate the navmesh, I could enable all the meshes.
To add a button in the inspector, follow this template: make a normal MonoBehaviour that has the function
you want to call, then import UnityEditor and make a new class that extends Editor. Add the CustomEditor
tag with type set to the class you just made. Then, override the OnInspectorGUI function. Then, you can add
buttons like I did here.

I'll close with two super simple tips which also happen to be the most important ones. First, use a version
control software. I use Plastic SCM because that's what Unity provides by default. Most version control
software will allow you to push changes to the cloud, which means you'll have a backup of your project at
every stage in development. I've lost significant portions of my projects before, and because I wasn't using
version control, I had to rewrite everything. It makes no sense to spend so much time working on something
and not go through the extra effort to make physical and online backups. As a plus, most version control
software also allows you to easily transfer your project files from computer to computer and easily collaborate
with others. Finally, refactor often and write code well the first time. Many developers, including myself, write
code haphazardly without planning and without preparing for the future, usually under the impression that
they'll go back and refactor everything at a later date. Spoiler alert: you won't. The longer you push it back,
the more daunting of a task it becomes, and the less likely you are to do it. Even when you are prototyping,
unless you plan on rebuilding the project from the ground up, you should take extra care with how you
program and how your systems interact. Nobody likes to rewrite code that they've already written. As a
young, self-taught game developer, I didn't discover these tools and techniques for years. Hopefully, this
video helps you to skip the learning curve and expose you to some of the more advanced programming
devices that don't get enough attention. If anything was completely new to you, I encourage you to do some
more research on your own in order to completely grasp the concept. I know I probably went too fast to fully
explain everything. I also didn't provide a one-size-fits-all design pattern for you to use in every project
because, well, I don't think that exists. Now it's your job to implement what you learned and make the best
decisions for your own game. If you're interested in the game I'm working on, check out my devlog series.
Besides that, put something you learned in the comments and subscribe for more game dev content. See you
next time!

You might also like