Introduction

In this post series I'll write about how to program a simple game in Unity, along with some explanations about game development and programming in Unity in general. If you want to learn the basics for making your first game or if you already work in other areas of game development (e.g. concept art, 3D modelling, animation) and want to know more about game programming, I hope this series will be useful for you.

Three main topics will be explored:

  • Understanding the concept of game loop and how it applies to Unity;
  • An overview of the most used script type in Unity, the MonoBehaviour;
  • Implementation of a simple but funcional version of the game Breakout.

For this series, you'll need:

  • A basic understanding of programming: variables, loops, functions and a bit about classes
  • A recent version of Unity, any 2019 version should suffice;
  • A code editor; I'll be using Visual Studio Community 2019, but if you want a lighter approach I'd recommend Visual Studio Code with the C# plugin.

This is part 1; let's talk about the game loop design pattern and MonoBehaviours.


Game loop

Almost all games use a design pattern called game loop, which implementation is responsible for three essential tasks:

  1. Reading user input;
  2. Updating the game internal state;
  3. Render the screen.

As the name implies, these three tasks run in a loop, and the number of times this loop runs in a second determines the frames per second, or FPS, of the game. Pseudo code for this implementation is shown below:

while(gameIsRunnning) {
    readInput(); // 1
    update(); // 2
    render(); // 3
}

If you want to render your game at 30 FPS, each loop should take at most 33.3ms (1s / 30), and if your target is 60 FPS it should be 16.6ms (1s / 60). This means that all three tasks should be blazing fast, which fortunately is not hard to attain with today computers and consoles.

The game loop is also responsible to keep a smooth timing of your frames; if your entire loop runs faster than the desired frame duration, it adds a small delay for compensating this and the frame will still have the correct duration. However, if the loop is too slow, the game will slow down, and if it's not possible or feasible to further improve performance you'll probably need what is called a variable time step: the game loop will advance the game state based on how much actual time has passed, instead of using a fixed amount. We don't need to worry about this calculation because Unity will take care of it for us, but bear in mind that if any of the three steps takes too long the frame rate will suffer.

When implementing gameplay code in Unity, you'll need to implement at least the equivalent to update(), for advancing the internal state of the object, and if the object receives input from the player, readInput() implementation is needed. The render() step is done via shaders, and even if you don't implement one Unity will use its defaults.

If you want to know more details about the game loop and its tricky parts, I suggest reading the Game Loop section on the great Game Programming Patterns website.


The MonoBehaviour class

According to the Unity manual, MonoBehaviour is "the base class from which every Unity script derives", and you'll probably spend most of your programming time in Unity scripts because that's how you create components that can be added to GameObjects. For the purposes of this post Unity script and component will mean the same thing. Also, GameObject will mean the same as object.

You can create scripts clicking with the right button on the Project tab, or clicking on the Create button more to the left, right under the tab title

When your first create a Unity script, let's say NewUnityScript, this is what you'll see:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class NewUnityScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

We can see that a new class called NewUnityScript was created deriving from MonoBehaviour, and that two functions were created by default: Start() and Update().

Start()

This function is called once when the component is activated on the scene for the first time, just before Update() starts being called (we'll talk about it in a bit).

Start() is used for initialization purposes, and can be used to delay this initialization to when it's more appropriate. This is because, as said above, it is executed when the script is activated for the first time, which means if the script is deactivated when the game starts Start() will not run. It all depends on your specific needs. An example:

void Start()
{
    maxHp = 10;
    transform.position = GetInitialPosition();
}

Update()

This functions is called at each frame when the component is activated, and it's most used for updating the internal state of the component on the current frame. Input reading functions are commonly placed on Update() on user-controller objects (via buttons, keys, axes, etc). The readInput() and the update() steps of the game loop shown earlier will be effectively implemented here.

Update() is called 30 or 60 times per second, depending on how you configure your game, and the time between calls can be slightly higher depending on various factors such as the number of objects on your scene, the CPU and GPU loads, how much processing Update() is doing at each frame, and so on. An example of updating a object position at each frame according to its velocity is shown below:

void Update()
{
    // Update position at each frame
    transform.position += velocity * Time.deltaTime;
}

Other useful functions are listed below.

Awake()

This function is called once on the creation of the component, regardless of the component being activated or not, and it's always called before the Start() of any of the objects of the scene. This means that if your Start() functions relies on some processing being done beforehand, place this processing in a Awake() function.

If the object itself is not activated when the scene loads, however, Awake() will run as soon as the object is activated. It is commonly used to obtain references to other components on the same object or elsewhere in the hierarchy. Example below:

void Awake()
{
    // Obtain a reference to a Rigidbody on this object
    rigidbody = GetComponent<Rigidbody>();
    // Obtain a reference to a Animator on the parent object(s)
    animator = GetComponentInParent<Animator>();
}

FixedUpdate()

Similar to Update(), but runs decoupled from the frame rate at a fixed rate, which by default is 0.02s or 20ms. Mostly used for Physics calculations.

void FixedUpdate()
{
    if(forceAmount > 0)
    {
        // Apply a force on the rigidbody 
        rigidbody.AddForce(Vector3.right * forceAmount);
    }
}

OnEnable() and OnDisable()

These pair of functions are called on the component when the object is enabled and disabled, respectively. Can be used to further separate initialization code and also for executing something when the object is deactivated.

// assume the game has a minimap showing the enemies location
void OnEnable()
{
    AddToMinimap();
}
void OnDisable()
{
    RemoveFromMinimap();
}

OnDestroy()

As the name implies, this functions is called when the object is destroyed, either when the scene is closed or via an explicit call to Destroy(). Less commonly used, but useful when some cleanup is necessary when the object is destroyed.

// assume this is part of some enemy object
void OnDestroy()
{
    RemoveFromEnemyList();
}

That's it for this post. Part 2 coming soon!