Game Dev Artisan Logo
In this post we’ll be covering the basics of GDScript and how to use it as our primary scripting language within Godot.
Tutorials

Understanding The Basics of GDScript: Godot Fundamentals

In Series: Godot Fundamentals

Author Avatar
2023-06-14
14 min read

Table of Contents

Introduction 

In this post we’ll be covering the basics of GDScript and how to use it as our primary scripting language within Godot.

Video 

You can watch the video covering this topic, or go at your own pace following this post.

GDScript Overview 

GDScript is a scripting language built specifically for Godot and is the recommended scripting language to use when getting started with Godot. It has been built to work directly with Godot’s features and APIs. GDScript is designed to be easy to learn and use, making it accessible to beginners while still offering flexibility and power for more experienced developers.

GDScript is a dynamically-typed language, meaning you don't have to declare variable types explicitly. It is similar in syntax to Python, which makes it relatively easy to read and write. GDScript supports object-oriented programming (OOP) principles, allowing you to define classes, create objects (instances), and work with inheritance and polymorphism.

One of the advantages of GDScript is its tight integration with the Godot engine. It provides direct access to the engine's features, allowing you to easily manipulate game objects, handle user input, manage scenes, perform animations, and more. GDScript also has built-in support for signals, which facilitate communication between different objects and components within your game.

In addition to GDScript, Godot also supports other programming languages such as C# and C++, giving developers the flexibility to choose the language they are most comfortable with.

Overall, GDScript is a versatile and user-friendly scripting language that empowers game developers to create interactive and engaging games using the Godot game engine. Its simplicity, integration with the engine, and supportive community make it an excellent choice for both beginners and experienced developers alike.

Quick Fix for Pixel Textures 

Before we get into GDscript, let's quickly address the blurry textures from our previous posts. If you goto Project > Project Settings and Navigate to the Rendering > Textures section you can select the Default Texture Filter and select Nearest. This will give us the crisp pixel art look we want.

Tank Script 

If we open our tank.tscn we can right click our root node and attach a new script we'll call tank.gd and save it in our res://scenes/ directory.

For our script, we'll inherit from CharacterBody2D by default, and Godot will automatically include some boilerplate character functionality to our script. We can remove it so we are just left with the extends clause.

extends CharacterBody2D

For our tank, we want to add behavior to move forward, backward, turn left, turn right, and rotate our weapon left and right. We'll also want to shoot. To begin tracking our facing direction let's discuss our next concept; variables!

Variables 

We'll create a variable reference for our tank's direction using the var keyword and giving it the name direction. We can also add a Type to our variables using the : syntax and defining it's type afterwards. For the direction we'll use Vector2, we can also set it's default value to be a blank Vector2().

Let's also cover how we can follow the best practice of not using "Magic Numbers" in our code by setting our movement and turn speeds to be Constant variables using the const keyword. We'll create Constant variables for SPEED, TURN_SPEED, and ROTATE_SPEED as follows:

const SPEED = 60.0
const TURN_SPEED = 2
const ROTATE_SPEED = 20

Note that our SPEED variable has a value of 60.0. The decimal point indicates that it is of type float. There are various data types and it's best to refer to the docs for a better understanding of possible values.

Annotations 

In some cases, you may want to hold a reference to a resource or node within the scene tree. Let's go over a few examples of how to accomplish this.

Using the @export keyword, you can define that a variable must be assigned a value from the inspector, or via code, during it's implementation. This allows you to bind a sometimes unknown value to a node and use it from within itself using a variable reference.

@export var weapon: Node2D

You can also use the @onready keyword to define that a variable's value must be set after the node has become ready. This allows other nodes in the scene tree to be fully prepared before potentially being reference in code. As a shortcut you may click and drag a node from the scene tree into the script while holding down Control and dropping it on te line you would like to quickly define. This creates an internal variable reference to a node, without needing to use get_node() or the $ or % shorthands for a named node.

@onready var body_sprite := $BodySprite
@onready var animation_player = $AnimationPlayer
@onready var collider = $CollisionShape2D

Functions 

To start using functions, you use the func keyword followed by the name of your function. Godot has many built in functions for each class, but most nodes will have the _ready, _process, and _physics_process as some examples. In our case we'll use the _physics_process built in function.

func _physics_process(delta):
    ...

The physics process function is provided with a delta property that contains the value of the current delta between the last physics frame and the current one. We can use that to smooth out movement over a constant time.

For our tank's process, let's start by grabbing our current input as a Vector2 using the Input class' get_vector() method.

var input_direction := Input.get_vector("turn_left", "turn_right", "move_backward", "move_forward")

In order for this function to work, we'll need to setup our InputMap

Input Map 

If we goto our Project > Project Settings > Input Map tab, we can add custom actions names and their key binds. Inside the Add New Action input field, let's enter turn_left, turn_right, move_forward, move_backward, weapon_rotate_left, weapon_rotate_right, and weapon_fire.

For each of these new actions listed, let's click the + button and add bindings for each key.

Input Handling 

Back to our tank's script, we can better leverage our new InputMap and review the new input_direction which should be a Vector2 containing our inputs being pressed.

We can use a control flow if statement to check the input_direction's x value to see if we are turning left or right. If so we'll update our direction and set the corresponding rotation to match.

if input_direction.x != 0:
    # Rotate direction based on our input vector and apply turn speed
    direction = direction.rotated(input_direction.x * (PI / 2) * TURN_SPEED * delta)
    rotation = direction.angle()

We will also use an if statement to check our forward/backward vector y axis and set our movement velocity and play our proper movement animation.

if input_direction.y != 0:
    # Move in a forward/backward motion and play animation
    animation_player.play("move")
    velocity = lerp(velocity, (direction.normalized() * input_direction.y) * SPEED, SPEED * delta)
else:
    # Bring to a stop
    velocity = Vector2.ZERO
    animation_player.play('idle')

Once we've set our velocity to what movement we expect, we can use the move_and_slide() function to apply our velocity to our tank.

# Apply our movement velocity
move_and_slide()

Also note the use of the # to define a comment block to better describe our code.

We can also track the rotation of our weapon node by getting a single input axis and applying the rotation:

# Apply Weapon Rotation
var weapon_rotate_direction := Input.get_axis("rotate_weapon_left", "rotate_weapon_right")
weapon.rotation_degrees += (weapon_rotate_direction * ROTATE_SPEED * delta * PI)

Weapon Script 

Let's open our Weapon scene and add a new script called weapon.gd also saved in the res://scenes/ folder. This script should extend from the Node2D and we'll add a class_name keyword set to Weapon.

extends Node2D
class_name Weapon

This will enable class level integration with Godot's engine and gives us additional features that we can depend on.

To help us track the Weapon's mode we'll cover a nice feature called Enumerators using the enum keyword. Let's define a list of states that our Weapon may be in, such as READY, FIRING, and RELOADING.

enum STATES { READY, FIRING, RELOADING }

We'll also define a variable named state and set it's type to STATES to limit it's values to only those defined in our enum. We can also give it a default value of STATES.READY

Custom Functions 

To track state changes, let's create a custom function called change_state as a convenience to setting the current state of our weapon. We can also use this function later to do things like emit signals, or validate state changes.

func change_state(new_state: STATES):
    state = new_state

Let's also create a custom function named fire that will be called by our tank whenever it receives the input action to trigger it. We'll also make calls to our new change_state function to inform our weapon of when it is triggered and when it is reloading, and also when it is ready again.

Our fire function should first verify that the state is not currently firing or reloading and if so, it should just return with no behavior. Otherwise, we can assume we want to create an instance of a bullet, and reload our weapon.

func fire():
    if state == STATES.FIRING || state == STATES.RELOADING:
        return
            
        change_state(STATES.FIRING)
        ...
        # Set our state to reload and start our timer
        change_state(STATES.RELOADING)
        reload_timer.start()

Timers and Signals 

Now that we have a state for our Weapon for reloading, let's add a child node of type Timer to our Weapon Scene and name it ReloadTimer and we can add a variable reference using the @onready keyword.

@onready var reload_timer = $ReloadTimer

We can see that we start our reload_timer when we successfully fire our weapon, but we will want to set our state back to READY once we have reached the timeout period. We can do this by going to the Nodes Tab on the Inspector, and selecting Signals.

We'll see there is a signal called timeout under the Timer category, and this is triggered when the timer's time has ran out. We can select that and connect it with a new function called _on_reload_timer_timeout, which is the default naming convention when connecting to a signal. When this is called, we want to change_state(STATES.READY).

func _on_reload_timer_timeout():
    change_state(STATES.READY)

Bullet 

Next let's create our Bullet scene called bullet.tscn and save it in the res://scenes/ folder. We'll extend the Area2D and add a CollisionShape2D and Sprite2D. For our Sprite2D, we can set the texture property to a PlaceholderTexture2D and set the size to 3px. For our CollisionShape2D we can also add a RectangleShape2D as our shape property and set it's size to follow the same 3px sizing.

We'll also create a script for bullet.gd and setup some basic movement using a direction Vector2 variable and a Constant called SPEED of 500. This will simply take the direction and add it the current position multiplied by SPEED and delta every physics frame.

extends Area2D

const SPEED = 500

var direction: Vector2 = Vector2()

func _physics_process(delta):
    position += direction.normalized() * SPEED * delta

Instances 

With our bullet scene created, we can now attach it as a PackedScene for our tank's weapon to be able create new instances of those bullets whenever it is fired.

Back in our Weapon script, we'll add an export variable to allow us to set the desired PackedScene as our bullet.

@export var BULLET_SCENE: PackedScene

Once this is defined and set within the root Node's inspector property, we can begin instancing bullets from within the fire function. After the change_state(STATES.FIRING) is called in our fire function, let's create a local bullet instance and reference it within the code. Start by instancing the PackedScene and store it in our bullet scoped variable. We will also set the direction and position based on the global rotation of our tank/weapon. Lastly we'll add the instance to our scene tree, but we'll want to confirm we add the child to our world space, so that translating isn't messed up by local space.

# Create a bullet at our position and set it's direction
var bullet = BULLET_SCENE.instantiate()
bullet.direction = Vector2.from_angle(global_rotation)
bullet.global_position = global_position
# Add the bullet to the root scene so translation is in world space
get_tree().root.add_child(bullet)

Fire Our Weapon 

Now that we can create bullet instances, let's wire up our fire function to our input action weapon_fire and get testing. Head back to the tank script and add a new function called _input which receives an event property containing possible input events. We'll be checking that our input event's action matches the weapon_fire string and then call our weapon's fire function.

func _input(event):
    if event.is_action_pressed("weapon_fire"):
        weapon.fire()

You may have also noticed that our weapon variable reference did not give us auto-complete hinting, because we didn't define that our weapon variable was in fact of type Weapon based on the class we defined previously. If we update it, we should start seeing auto complete when starting to type.

@export var weapon: Weapon

Test 

Now that we've covered everything needed, let's go ahead and save and run our project and see how far well we've done. Our tank should be able to move forward, backward, turn left and right, as well as rotate our weapon left and right, and lastly fire a bullet.

Recap 

This is a great start to your understanding of Godot's GDscript, be sure to spend more time working on your own projects that leverage GDScript and all it has to offer. If you have any questions or would like to see things specifically covered, please let us know and feel free to join the community on Discord for more in-depth conversations.

I hope you enjoyed!

Happy Coding.

Want to support our work?Support us via Ko-fi