(v0.1)

Table of Contents

Introduction Link to heading

This is the first in a series of tutorials building up to a basic Pong clone implemented using the Godot engine. We will try to accomplish as much as we can in GDScript, which is the native scripting language of the Godot engine, relying less on Godot’s SceneTree editor and Property Inspector. These tools, while useful for experimentation, can make refactoring code, debugging, and writing unit tests more difficult; grepping over files in your project folder is much faster than having to manually inspect properties of the Nodes in the SceneTree, for example.

OOP (Object-Oriented Programming) is the driving paradigm in the design of GDScript and the Godot engine. You don’t need to become an expert in Object Orientated programming to use the Godot engine or GDScript, but you should take the time to become somewhat proficient. This tutorial introduces some principles of OOP but won’t go into much detail as this isn’t a general programming tutorial.

We start by implementing something seemingly straightforward, displaying the frame rate (fps – frames-per-second). Why start with the frame rate instead of jumping in and creating the Pong clone? See Software Development Estimation as to why.

Pong FPS Tutorial 1

We begin with a high level description of how the Godot engine displays graphics to the screen.

The Godot Graphics Pipeline Link to heading

An object (Node) must be added to the Godot graphics pipeline to be rendered to the display. Nodes are visible in the Godot editor in the SceneTree hierarchy.

An example of a SceneTree, from the Godot documentation on Nodes and Scenes

Pipeline components are described at a high level below;

  • The RenderingServer projects game objects (Nodes) onto a Viewport.

  • A Viewport owns the surface that the RenderingServer draws to. The Viewport is itself a Node in the SceneTree. The Viewport surface is what is displayed on your screen unless otherwise modified by a Camera Node in the SceneTree.

  • The SceneTree is a Scene Graph containing a hierarchy of Nodes, one of which is the Viewport. The SceneTree also manages the MainLoop (the game loop). As we will learn later, the MainLoop defines the virtual methods _initialize(), _finalize(), and _process() that we override with our game logic.

  • A Node is the basic building block of the Godot engine. Only nodes that are added to the SceneTree either in the Godot Editor or using the "add_child" method can be displayed. A Node can have up to one parent Node and zero or more child Nodes. The Node at the head of the SceneTree is the “Root Node”. Note that “parent” and “child” here refer only to the Node hierarchy in the SceneGraph and not class inheritance.

  • A Viewport is associated with a DisplayServer which is responsible for window management.

  • A Camera Node can be 2D or 3D and provides customization beyond what the Viewport provides. For example, a Camera3D Node is required to render a scene in 3D. A Camera also allows for only a subset of the Viewport to be rendered to the screen. Most Godot projects use a Camera Node. We do not as this tutorial does not require one.

  • Input and Engine are Singletons providing access to the underlying capabilities of the Godot engine. Input provides access to keyboard, mouse, and game pad events. The Engine provides access to engine internals such as version, the current frame count, the frame rate (that we need), etc.

  • The RenderingServer renders Nodes in the SceneTree that appear in the hierarchy below the Viewport Node to that Viewport surface using transform parameters in the Camera Node if one is present. A SceneTree may contain multiple Viewport Nodes but we will only make use of the default “Root Viewport” Node at the head of the SceneTree.

Install the Godot Engine Link to heading

Download and install the Godot engine if you have not already.

Design Link to heading

The tasks below outline the overall design. We go into detail in the following sections as to why.

Tasks

  1. Create the Project,
  2. Initialization:
    1. Create a Viewport,
    2. Add the Viewport to the SceneTree,
    3. Create a Text Label,
    4. Add the Label as a child Node of Viewport,
    5. Assign the Label a position in x/y coordinates,
  3. Process each frame;
    1. Retrieve the current frame rate,
    2. Print the frame rate to the Label Node.
    3. Process keyboard events,
    4. Exit when Esc is depressed.
  4. Game Exit

Create the Pong Project Link to heading

Create a default project with SceneTree, Root Node and the Viewport.

  • Start Godot and create a project named “Pong” in a new folder,

Godot Editor - Create a new project

Note: The “new Folder” icon is all the way to the right.

Godot Editor - Create a new folder

  • Then choose “Other Node”,

Godot Editor - Create “Other Node”

  • And create the base “Node” (Class “Node”, Base class for all scene objects).

Godot Editor - Create a basic Node

  • Rename the root Node to “Pong”,

Godot Editor - Rename the Root Node

Godot Editor - Rename the Root Node

  • Create a folder res://src where we will place our project source files so as to declutter the project root directory.

Godot Editor - Create a src subdirectory

  • Attach a new script to the filename root “Pong” node.

Godot Editor -

  • And rename to res://src/Pong.gd

Godot Editor -

Godot Editor -

  • Open the Pong.gd file. Godot should have generated the following boilerplate code.
extends Node


# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	pass # Replace with function body.


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta: float) -> void:
	pass

OK, that’s about all the work we need to do in the Scene editor in this tutorial.

Viewport Link to heading

The RenderingServer renders the child Nodes of the Viewport to the Viewport surface. The Viewport surface is displayed to the screen.

We don’t need to create a RenderingServer or DisplayServer as these are managed by the Godot Engine. We also don’t need to create a Viewport as the “Root Node” by default is also the “Root Viewport”. We can also do without creating a Camera Node as we will be rendering the entire Viewport surface to our display without any customization.

See Classes and Class Inheritance for the implementation.

Classes and Class Inheritance Link to heading

Think of a class as the blueprints for a house and an object as the house once created (instantiated).

Open the script file Pong.gd. This file defines the Pong class and will contain the game logic we defined in the Design section. Change the first line as follows;

class_name Pong extends Node

The extends keyword means we are extending Node with a class Pong. Pong will subclass and inherit the methods and attributes from Node (inheritance being one of the principles of OOP). Almost all Godot components inherit directly or indirectly from Node. As we need to hook into the SceneTree, Pong must inherit from Node as well. Pong.gd will also contain the code we write in this tutorial.

We don’t explicitly create an instance of Pong using new() because we added it as a Node to the SceneTree. The SceneTree handles the creation and deletion of Nodes in its hierarchy. See the following section Object Instantiation and Variables.

Text Label Link to heading

We cannot render text directly to a Viewport surface as it does not support this capability, so we cannot print “Hello World” as we would to a terminal. Instead we need to add a Label Node to the SceneTree and write the current frame rate to this control using its set_text() method.

See Object Instantiation, and Dynamic vs Static Typing for the implementation.

Object Instantiation and Variables Link to heading

We need to create an instance of Label to display the frame rate – in effect we are instantiating a new Label object. We do this by calling the constructor method new() on the Label class which returns a new object of type Label.

var fps: Label = Label.new() # Create a new Label object

The = assigns the object returned by Label.new() to a new variable created by var having the namefps and type Label.

We are now able to call the set_text() method on the instance of Label stored in fps, passing in the current frames per second as a String argument.

var fps: Label = Label.new() # Create a new Label object
fps.set_text("frames per second") 

Dynamic vs Static Typing Link to heading

GDScript is a dynamically typed language that supports explicit type definitions.

In the example below, we assign a Label object to the untyped variable fps_untyped and a Label object to the typed variable fps_typed. The typed variable fps_typed can only hold objects of type Label or objects of classes that inherit from Label. An attempt to assign any other type will cause a compile time error. The untyped variable fps_untyped can hold objects of any type.

var fps_untyped = Label.new() # Untyped

var fps_typed: Label = Label.new() # Typed 

It is good practice to specify types as much as possible, even though doing so makes code more verbose, as this eliminates an entire class of errors that can be caught by a developer at compile time versus having players experience these bugs at runtime. Languages like Python are dynamically typed while a language like Go is statically typed.

Frame Rate Link to heading

Now we need to calculate or retrieve the current frame rate. Handily, Godot provides the get_frame_per_second() method for this purpose.

See Singletons for the implementation.

Singletons Link to heading

Godot provides the Singleton object Engine that implements the method get_frames_per_second() that returns the current frames-per-second. In general terms, a Singleton can be thought of as a global variable containing an instance of Engine. As the term implies, there can only be one instance of Engine and it is managed by Godot. As such, we don’t need to create an instance of Engine using new().

var fps: Label = Label.new() # Create a new Label object
fps.set_text(str(Engine.get_frames_per_second())) # Set fps.text to the current fps

The str() function converts the float returned by Engine.get_frames_per_second() to a String value required by set_text. Not performing this conversion will result in a runtime error.

Game Loop Link to heading

Still not done;

  1. Create the Project,
  2. Initialization:
    1. Create a Viewport,
    2. Add the Viewport to the SceneTree,
    3. Create a Text Label,
    4. Add the Label as a child Node of Viewport,
    5. Assign the Label a position in x/y coordinates,
  3. Process each frame;
    1. Retrieve the current frame rate,
    2. Print the frame rate to the Label Node.
    3. Process keyboard events,
    4. Exit when Esc is depressed.
  4. Game Exit

Steps (1.1) - (1.5) should be performed once, while steps (2.1) - (2.4) are performed each frame. We can accomplish this by overriding the _initialize() and _process() methods defined in the MainLoop.

See The SceneTree, Variable Scope, Static and Instance Methods, Conditionals, and Constants for the implementation.

The SceneTree Link to heading

The SceneTree is what is called a scene graph, a hierarchy of Nodes in an inverted tree structure.

The Label must be added to a Node in the SceneTree before the frame rate can be displayed. One of our first steps was to add our Pong Node to the SceneTree by subclassing the Root Node. We are now able to add Label as a child Node to the Pong Node using the add_child method. Note that Label is also a subclass of Node and therefore inherits all of Node’s attributes and methods.

var fps: Label = Label.new() # Create a new Label object
self.call_deferred("add_child", fps) # Explicitly add the label to the scene
fps.set_position(Vector2(0,0)) # Set position
fps.set_text(str(Engine.get_frames_per_second())) # Set fps.text to the current fps

Calls to add_child() (and free()) are generally passed into a call_deferred() method that postpones execution of "add_child" until idle time.

In the code, we call self.call_deferred, where self refers to “the current Node”, in this case the Pong Node. GDScript does allow self to be omitted but I prefer to use self as it makes explicit that a method is being invoked on the current Node.

The SceneTree manages the MainLoop which defines the virtual methods _initialize(), _finalize(), and _process().

To recap;

  1. Initialization: 0. Create the project,
    1. Create a Viewport,
    2. Add the Viewport to the SceneTree,
    3. Create a Label Node,
    4. Add the Label as a child Node of Viewport,
    5. Assign the Label a position in x/y coordinates,
  2. Process each frame;
    1. Retrieve the current frame rate,
    2. Print the frame rate to the Label Node.
    3. Process keyboard events,
    4. Exit when Esc is depressed.

To implement “Initialization” and “Process each frame”, we override the virtual methods, _ready()_ and _process(). Unlike Instance methods, Virtual methods must be implemented by the subclass.

  • _ready() is called after a node is added to the scene graph and is ready for use, and
  • _process() is called each frame.
func _ready():
    var fps: Label = Label.new() # Create a new Label object
    self.call_deferred("add_child", fps) # Explicitly add the label to the scene
    fps.set_position(Vector2(0,0)) # Set position

func _process(_delta: float):
    fps.set_text(str(Engine.get_frames_per_second())) # Set fps.text to the current fps

But how does Godot know to call our methods? When the game starts, Godot will instantiate the root node (Pong). Pong instantiates the Label Node and adds this as a child node to itself (using "add_child"). The SceneTree calls _ready() after Pong the Pong Node is added to the SceneTree, and calls _process() once per frame. The SceneTree will call _ready() and _process() on the Label object if we had overridden those methods, which we do not.

Variable Scope Link to heading

The previous code exhibits a bug in that fps is defined as a local variable meaning it is visible only within the scope of _ready(). But fps needs to be visible to both _ready() and _process(). We must therefore define fps as an instance variable, placing fps in the class scope and visible to all methods of the class. We create an instance variable by moving fps: to the top level.

var fps: Label # Crete a variable of type Label

func _ready():
    fps = Label.new() # Create a new Label object
	  self.call_deferred("add_child", fps) # Explicitly add the label to the scene

func _process(_delta: float):
    fps.set_text(str(Engine.get_frames_per_second())) # Set fps.text to the current fps

instance and local variables differ to static variables in that the value of a static variable is shared across all instances of a class, whereas instance and local variables are specific to an instance.

Static, Instance, and Virtual Methods Link to heading

GDScript only supports the creation of static and virtual methods. Methods not identified as static using the static keyword are virtual and can be overridden (GDScript has no virtual keyword).

A static method can be invoked even if no instances of the class exist yet. A virtual method can only be invoked on an instance of the class.

A developer cannot create non-virtual methods in GDScript. Internal methods exported by the Godot engine through GDScript however can be static, virtual (overridable) and non-virtual (non-overridable).

Conditionals Link to heading

We don’t need to update the frame rate on each frame (e.g. 60 times a second), so we make use of an if conditional to update the frame rate every tenth frame.

func _process(_delta: float) -> void:
	if Engine.get_process_frames() % 10 == 0:
		fps.set_text(FPS_TEXT + str (Engine.get_frames_per_second()))

The % is the modulo or remainder operator. It returns the remainder when dividing the number of frames since game start (Engine.get_process_frames()) by 10. If the remainder is 0 then the conditional is true and we update the frame rate.

Constants Link to heading

We define several constants with values for maximum frame rate, fps update interval, and the FPS text. Constants are used instead of variables when we don’t want the value assigned to a variable to change during program execution. Some other advantages of constants include improving code readability, and changing the value of a constant will be reflected everywhere that constant is used.

class_name Pong extends Node2D

const MAX_FPS = 0 # Zero means use the maximum frame rate
const UPDATE_INTERVAL = 10 # Update the frames per second every ten frames.
const FPS_TEXT = "FPS: " # Constant prefix displayed with the current fps, e.g. "FPS: 60"
const ACTION_UI_CANCEL = "ui_cancel"

var fps: Label # Crete a variable of type Label

func _ready():
	Engine.set_max_fps(MAX_FPS) # Set the "do not exceed" frame rate.
  fps = Label.new() # Create a new Label object
	fps.set_position(Vector2(0,0)) # Set position
	self.call_deferred("add_child", fps) # Add the label to the scene

func _process(_delta: float) -> void:
	if Engine.get_process_frames() % UPDATE_INTERVAL == 0:
		fps.set_text(FPS_TEXT + str(Engine.get_frames_per_second()))

Handling Player Input Link to heading

We can exit the game by closing the Godot window. But as the plan is to use the keyboard to control the paddles, we may as well explore how to handle keyboard input and exit the game when the Esc key is depressed.

The methods to retrieve player input are provided by the Input Singleton. We use the is_action_pressed() method and filter events to the "ui_cancel" signal which is by default mapped to the Esc key.

See Handling Input for the implementation.

Handling Input Link to heading

Checking if the player depressed the Esc key is straightforward. Input.is_Action_Pressed("ui_cancel") returns true when an event action is received that is mapped to the Esc key.

func _process(_delta: float) -> void:
	if Input.is_action_pressed(ACTION_UI_CANCEL): exitGame()
	if Engine.get_process_frames() % UPDATE_INTERVAL == 0:
		fps.set_text(FPS_TEXT + str (Engine.get_frames_per_second()))

func exitGame() -> void:
	self.get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
	self.get_tree().quit()

We define a method exitGame() to send the “window close” request to the DisplayServer (attached to the root node) and tell the SceneTree to exit.

Game Exit Link to heading

We need to cleanup any resources we allocate during execution (the Label) as Godot only supports limited garbage collection.

See Object Constructors and Destructors, Finalizers for the implementation.

Object Constructors and Destructors, Finalizers Link to heading

The virtual methods _init(), _ready(), and _finalize() can be overridden to manage the object life cycle.

  • _init() is a constructor. This method is called when an object is instantiated in, for example, a call to new(). _init() is called before _ready().
  • _ready() is called after the object is added to the SceneTree and is ready to be used. This means that _ready() is only called on objects added to the SceneTree.
  • finalize() is a destructor, called when the object is deleted following, for example, a call to call_deferred("free").

Godot supports limited garbage collection, for example resources explicitly allocated (e.g. using new()) must be explicitly deleted using free(). Not doing so will result in memory leaks.

Our tutorial code shows this; we explicitly instantiated Label using new() requiring that we explicitly delete Label by calling fps.call_deferred("free") (call_deferred()) when we are done with it. We do this in the _finalize() method of our Pong class.

func _finalize() -> void:
	fps.call_deferred("free") # Becasue we added the label, we also need to delete it

The Pong Node was added to the SceneTree in section Create the Pong Project, so the creation and deletion of the Pong instance is managed by the SceneTree.

Conclusion Link to heading

All that to display the current frame rate. If we had jumped right into the implementation we would also have had to consider loading assets, collision detection, implementing physics for ball strikes and bounces, sound effects, and player point scores for game start and game over. Never mind some rudimentary AI if we want to support single player mode against a computer opponent.

We will need to do all this anyway, but trying to implement all of that, while learning GDScript, while learning how the Godot engine works is a lot to bite off.

See The Final Code for the implementation.

The Final Code Link to heading

res://src/Pong.gd

class_name Pong extends Node

const MAX_FPS = 0 # Zero means use the maximum frame rate
const UPDATE_INTERVAL = 10 # Update the frames per second every ten frames.
const FPS_TEXT = "FPS: " # Constant prefix displayed with the current fps, e.g. "FPS: 60"

const ACTION_UI_CANCEL = "ui_cancel"

var fps: Label = Label.new() # Create a new Label object

func _ready() -> void:
	Engine.set_max_fps(MAX_FPS)
	fps.set_position(Vector2(0,0)) # Set position
	self.call_deferred("add_child", fps) # Explicitly add the label to the scene

func _process(_delta: float) -> void:
	if Input.is_action_pressed(ACTION_UI_CANCEL): exitGame()
	if Engine.get_process_frames() % UPDATE_INTERVAL == 0:
		fps.set_text(FPS_TEXT + str (Engine.get_frames_per_second()))

func _finalize() -> void:
	fps.call_deferred("free") # Becasue we added the label, we also need to delete it

func exitGame() -> void:
	self.get_tree().root.propagate_notification(NOTIFICATION_WM_CLOSE_REQUEST)
	self.get_tree().quit()

Advanced Concepts Link to heading

Versions Link to heading

  • 0.1 – Initial.