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.