Four Techniques for Faster Unity 3D Development
Unity3D has been my favorite tool for game development for quite some time. I have been using it for over 8 years now, be it for professional products, personal endeavors, and as a programming and game designer teacher. Moreover, I have been using Unity for almost all Game Jams I have been to, and it has helped me quickly build up the cornerstones of my jam-games in just a few hours.
As you might know, a Game Jam is a game development contest in which participants try to make a game from scratch over a short period of time. Game Jams usually range from 24 to 72 hours, but others spam over a more extended period, such as the GitHub Game Off that spans over the entire month of November.
After various Game Jams experiences, which even included one with my group’s self-made engine in C++(only available in Brazilian Portuguese, unfortunately), I have developed a shortlist of fast prototyping rules that quickly turned into my tenant for software development: build more with less code.
The main idea of using less code (or to put it another way: to keep a smaller code base) is twofold:
- The smaller the code, the fewer the opportunities for bugs to sneak in;
- Every code change requires updates and tests, which takes time.
For Unity, specifically, there is a third reason: every change in the code will trigger Unity into refreshing, which repeatedly takes a reasonable amount of time.
In this article, I go over a few methods to easily follow this tenant using Unity 3D, speeding up your prototyping methods and overall progress.
Disclaimer: I am not endorsed by Unity 3D Technologies (yet).
1. Serialize Classes and Structs
Serialization is the process of automatically transforming data structures or object states into another format. In Unity’s case, this process facilitates the storage and reconstruction of data.
You can mark a Class or Struct as serializable by marking it [Serialized]
on top of its name. As seen in the example below from Unity's documentation:
[Serializable]
public struct PlayerStats
{
public int movementSpeed;
public int hitPoints;
public bool hasHealthPotion;
}
The main advantage of using this approach is that it allows for direct access to these properties in the Inspector, especially when using Lists and Arrays.
List of Player Stats in Unity’s Inspector. Source: Yvens Serpa.
Your project will likely hold repeated structures, such as Quests, Items, and even Dialogue. In this case, you can make each one of them a serialized Class or Struct and easily change their list of values in the Inspector.
This method also works for enums, great for type safety, and more complex structures, such as Sprites. Both cases are illustrated in the image and code below:
List of Player Stats with Sprites and Enums in Unity’s Inspector. Source: Yvens Serpa.
public enum PlayerType
{
ARCHER, KNIGHT
}[Serializable]
public struct PlayerStats
{
public int movementSpeed;
public int hitPoints;
public bool hasHealthPotion;
public Sprite face;
public PlayerType type;
}
As an extra, using enums in the serialized structure makes the Inspector display the enum cases in a nice and helpful Combobox. No more concerns about memorizing strings.
2. Use Require Component Whenever Possible
It is ubiquitous to have Scripts with component dependencies. For example, a player controller Script will likely depend on the player’s rigidbody and colliders. The safest option to ensure that all dependencies will belong to the game object during the Script execution is to mark it with the Require Component attribute.
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-r1.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-r2.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-r3.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-r4.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-r5.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-r6.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-t01.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-t02.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-t03.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-t04.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-b1.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-b2.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-b3.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-b4.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-b5.html
https://www.un.org/sites/www.iamladp.org/files/webform/c-v-b6.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-b1.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-b2.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-b3.html
https://www.un.org/sites/www.iamladp.org/files/webform/n-v-b4.html
https://www.un.org/sites/www.iamladp.org/files/webform/o-v-t1.html
https://www.un.org/sites/www.iamladp.org/files/webform/o-v-t2.html
https://www.un.org/sites/www.iamladp.org/files/webform/o-v-t3.html
https://www.un.org/sites/www.iamladp.org/files/webform/o-v-t4.html
https://www.un.org/sites/www.iamladp.org/files/webform/o-v-t5.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-e1.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-e2.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-e3.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-e4.html
https://santaritasaude.azurewebsites.net/az1/s-v-n33.html
https://santaritasaude.azurewebsites.net/az1/t-v-c10.html
https://santaritasaude.azurewebsites.net/az1/t-v-c11.html
https://santaritasaude.azurewebsites.net/az1/t-v-c12.html
https://santaritasaude.azurewebsites.net/az1/t-v-c13.html
https://www.un.org/sites/www.iamladp.org/files/webform/r-v-n-zx1.html
https://www.un.org/sites/www.iamladp.org/files/webform/r-v-n-zx2.html
https://www.un.org/sites/www.iamladp.org/files/webform/r-v-n-zx3.html
https://www.un.org/sites/www.iamladp.org/files/webform/r-v-n-zx4.html
https://www.un.org/sites/www.iamladp.org/files/webform/a-v-m06.html
https://www.un.org/sites/www.iamladp.org/files/webform/a-v-m07.html
https://www.un.org/sites/www.iamladp.org/files/webform/a-v-m08.html
https://www.un.org/sites/www.iamladp.org/files/webform/a-v-m09.html
https://www.un.org/sites/www.iamladp.org/files/webform/f-v-r5.html
https://www.un.org/sites/www.iamladp.org/files/webform/f-v-r6.html
https://www.un.org/sites/www.iamladp.org/files/webform/f-v-r7.html
https://www.un.org/sites/www.iamladp.org/files/webform/f-v-r8.html
https://www.un.org/sites/www.iamladp.org/files/webform/f-v-r9.html
https://www.un.org/sites/www.iamladp.org/files/webform/m-v-f01.html
https://www.un.org/sites/www.iamladp.org/files/webform/m-v-f02.html
https://www.un.org/sites/www.iamladp.org/files/webform/m-v-f03.html
https://www.un.org/sites/www.iamladp.org/files/webform/m-v-f04.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-v-ex14.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-v-ex15.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-v-ex16.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-v-ex17.html
https://www.un.org/sites/www.iamladp.org/files/webform/s-v-n4_0.html
https://www.un.org/sites/www.iamladp.org/files/webform/s-v-n5_0.html
https://www.un.org/sites/www.iamladp.org/files/webform/s-v-n6_0.html
https://www.un.org/sites/www.iamladp.org/files/webform/tit-v-col-h5.html
https://www.un.org/sites/www.iamladp.org/files/webform/tit-v-col-h4.html
https://www.un.org/sites/www.iamladp.org/files/webform/tit-v-col-h3.html
https://www.un.org/sites/www.iamladp.org/files/webform/tit-v-col-h2.html
https://www.un.org/sites/www.iamladp.org/files/webform/tit-v-col-h1.html
https://www.un.org/sites/www.iamladp.org/files/webform/pan-v-vik-dx05.html
https://www.un.org/sites/www.iamladp.org/files/webform/pan-v-vik-dx04.html
https://www.un.org/sites/www.iamladp.org/files/webform/pan-v-vik-dx03.html
https://www.un.org/sites/www.iamladp.org/files/webform/pan-v-vik-dx02.html
https://www.un.org/sites/www.iamladp.org/files/webform/pan-v-vik-dx01.html
https://www.un.org/sites/www.iamladp.org/files/webform/dol-v-jets-g5.html
https://www.un.org/sites/www.iamladp.org/files/webform/dol-v-jets-g4.html
https://www.un.org/sites/www.iamladp.org/files/webform/dol-v-jets-g3.html
https://www.un.org/sites/www.iamladp.org/files/webform/dol-v-jets-g2.html
https://www.un.org/sites/www.iamladp.org/files/webform/dol-v-jets-g1.html
https://www.un.org/sites/www.iamladp.org/files/webform/cle-v-jac-ip5.html
https://www.un.org/sites/www.iamladp.org/files/webform/cle-v-jac-ip4.html
https://www.un.org/sites/www.iamladp.org/files/webform/cle-v-jac-ip3.html
https://www.un.org/sites/www.iamladp.org/files/webform/cle-v-jac-ip2.html
https://www.un.org/sites/www.iamladp.org/files/webform/cle-v-jac-ip1.html
https://www.un.org/sites/www.iamladp.org/files/webform/car-v-pat-st5.html
https://www.un.org/sites/www.iamladp.org/files/webform/car-v-pat-st4.html
https://www.un.org/sites/www.iamladp.org/files/webform/car-v-pat-st3.html
https://www.un.org/sites/www.iamladp.org/files/webform/car-v-pat-st2.html
https://www.un.org/sites/www.iamladp.org/files/webform/car-v-pat-st1.html
https://www.un.org/sites/www.iamladp.org/files/webform/buff-v-bill-en-nf01.html
https://www.un.org/sites/www.iamladp.org/files/webform/buff-v-bill-en-nf2.html
https://www.un.org/sites/www.iamladp.org/files/webform/b-v-bi1.html
https://www.un.org/sites/www.iamladp.org/files/webform/b-v-bi2.html
https://www.un.org/sites/www.iamladp.org/files/webform/b-v-bi03.html
https://www.un.org/sites/www.iamladp.org/files/webform/b-v-bi04.html
https://www.un.org/sites/www.iamladp.org/files/webform/b-v-bi4.html
https://www.un.org/sites/www.iamladp.org/files/webform/p-v-c2.html
The Require Component attribute has three main functionalities:
- It forces that the Game Object has the required components.
- It blocks the required components from being removed.
- It automatically adds the required components to the Game Object when the Script is attached to it.
An example code of the Require Component usage is displayed below:
[RequireComponent(typeof(Rigidbody))]
public class PlayerScript : MonoBehaviour
{
Rigidbody rigidbody; void Awake()
{
rigidbody = GetComponent<Rigidbody>();
}
}
These functionalities highly improve the code safety and reduce the chances of unexpected null pointer exceptions by trying to access components that are either invalid or non-existent. You can also either attach them directly on the Editor Window or use the Awake (or Start) methods to get them (as seen in the example code above).
From a prototyping perspective, this approach also makes it faster to set up objects. Furthermore, Classes and Structs can have multiple Require Component attributes at once. Given a Script that does so, just adding it to a game object will attach all of them at once.
Moreover, given that these components will never be invalid under regular circumstances, the Script does not need to check for null. Therefore, less code is written.
3. Use UI Buttons for Multiple Events
This is probably the easiest and more efficient technique of this list: use the same Unity UI Button for multiple events at once. I see many new Unity developers using buttons to either perform a single task or, worse, call a specific method in a Script that handles the button logic.
Whereas having a single task is not necessarily a problem, nor have a specific method for the button logic, both tend to highly depend on changes in the codebase, and changes are very likely to happen.
A better approach is to list all of the effects of a button directly on its OnClick list of events. It can hold up for as many events as necessary, and they are easier to access and change.
You can use this approach, for example, to make a single Button’s onClick to display a Unity Panel, play a sound, and trigger an animation. For these particular tasks, no additional code is needed: the Panel can appear (setActive(true)
); a sound can be played (play()
); and, the Animator can be triggered ( setTrigger()
). All these method calls are exemplified below.
OnClick list of events example. [Source: Yvens Serpa]
A fairly common usage of this approach is to program a menu’s navigation. Each menu will activate the next one and deactivate itself. When returning or closing menus, each one will again deactivate itself and activate the former. The best part of it is that not one line of code is necessary. Again: less code, fewer bugs, more progress.
4. Abuse Unity Events
Unity has a special class named UnityEvent that behaves similarly to using the OnClick method of Unity UI Buttons. An exposed UnityEvent variable grants the Script with the same interface as the OnClick method:
One exposed UnityEvent variable named EventsToBeCalled. Source: Yvens Serpa.
The variable’s usage is essentially the same, except that the UnityEvent needs to be invoked via Script to execute its event list. The code excerpt below shows how to add one UnityEvent variable to a Script and how a simple Invoke function is written:
using UnityEngine;
using UnityEngine.Events;public class CallEventsScript : MonoBehaviour
{
public UnityEvent eventsToBeCalled; public void CallEvents()
{
eventsToBeCalled.Invoke();
}
}
The CallEvents method invokes the event list for the UnityEvent variable. Since it is a public method, other Scripts can be accessed, including Timeline’s Signals and Animation’s Events. For these two cases, no code has to be written to access the method. It is just as simple as dragging and dropping.
Animation timeline with an added event that calls for the CallEvents method. Source: Yvens Serpa.
UnityEvents can also be used to create highly flexible Scripts, for example, invoking the event list in methods such as the Awake, Start, OnEnable, OnDisable, and so on. For example, you could write a Script that always executes the list of events on its Start method, to quickly set up features without the need for coding.
A more practical example is what I call a Trigger Box. A trigger box is a game object with a collider that performs one or more actions when it collides with other game objects. This can be easily written using UnityEvents as such:
[RequireComponent(typeof(Collider))]
public class TriggerBoxScript : MonoBehaviour
{
public UnityEvent eventsToBeCalledOnCollision;
public List<string> objectsTagToActivate; private void OnCollisionEnter(Collision other)
{
if (OtherHasWantedTag(other.gameObject))
{
InvokeEvents();
}
} private void OnTriggerEnter(Collider other)
{
if (OtherHasWantedTag(other.gameObject))
{
InvokeEvents();
}
} private bool OtherHasWantedTag(GameObject other)
{
var found = objectsTagToActivate.Find(other.CompareTag);
return found != null;
} private void InvokeEvents()
{
eventsToBeCalledOnCollision.Invoke();
}
}
The example above works for both triggers and non-trigger colliders (both OnTriggerEnter and OnCollisionEnter methods call the method). It also enforces that it can only be used by game objects that have a collider.
Example of the necessary components for a Trigger Box. [Source: Yvens Serpa]
The example above shows a trigger box game object using this approach. In this particular example, whenever a game object tagged “Player” collides with the trigger box object, it will play a sound and deactivate itself. The possibilities of this sort of structure are near endless: activating enemies, changing the background music, spawn points, save points, etc.
Conclusion
In short, the two main takeaways from these approaches are the reduced amount of code necessary for them, and how they empower the Unity’s Inspector. More can be done at once and with greater flexibility.
Especially for the Events approaches (both OnClick and UnityEvents), the developer does not need to bother about setting up object dependency (objects’ methods can be invoked directly from the lists) nor with checking for their validity (only valid existing objects will be linkable in the list).
Surely there are situations in which these approaches are not the best and can be unproductive. For example, if the same series of tasks need to be done for various different elements, it might be wise to transfer it to a specific method and call it instead of listing all tasks for each element. This applies directly to Unity’s UI Buttons that might benefit from having their specific methods in these situations.
In any case, the flexibility of these approaches will certainly outperform their disadvantages, and they are also quite easy to remove or ignore. Moreover, they allow for fast prototyping and testing ideas before committing to writing a more stable and efficient final code.
Thanks for reading :)