Tutorial (Godot Engine v3 - GDScript) - Sprite formations!
Tutorial
...learn how to create formations of sprites!
What Will I Learn?
This tutorial builds on the last, which explained how to create lots of Sprites!.
In this article, you will learn how to group the individual Invaders in a better way.
..." but they were moving in formation, were they not?"
... I hope to hear you cry!
Yes, but no! Yes, they did move in a coordinated manner, but the implementation of the script was deliberately poor! In fact, somebody (they know who they are) raised this concern to me; which is extremely pleasing and flattering because it confirmed I'm walking you at the pace that I had intended to in this tutorial!
As previously stated, there is NEVER a concrete right or wrong way to develop code. There are good designs, fast implementations or spagehti mess, but never PERFECT code because it can always be tweaked, transformed or rewritten; much like a novelist writing an epic story. Beauty can be found in well-constructed code, but often, it'll only deliver a specific need. Programmers can simply go mad in the quest of reaching perfection with a small routine, simply, just let it go!
I've already demonstrated the power of Godot Engine:
- by using a single instance, to demonstrate how Invaders can be quickly created
- by using a grouping of Nodes, to show cross communication between them
However, to do so, I deliberately had to choose not to optimise the example! My experience nagged and knawed at me, crying out how I was creating sub-optimal code. However, I held-off the cleansing and refactoring process, because otherwise, how will you learn?
In this session, I'm going to explain my thinking behind some code restructuring, so that you can learn. To force 'change', I've decided the following enhancements are desirable:
- Smooth the Invader movement
- Accelerate and de-accelerate at the borders (to protect the little Invaders from smashing their heads on the inside of their ships)
- Break the monotony of the static grid, by adding wave patterns to their movement
- Design an implementation that allows for future pattern changes
- Optimise the implementation
To achieve this, I was forced to make drastic simplifications. The net result was to improve the quality of the game, whilst ensuring actor responsibilities were delegated correctly.
Actors are as you would expect in a play or a film; the actors of your game; the Godot Scene's you design and develop and then instantiate
By the end of this tutorial, you will understand the improved script for forming waves of Invaders. They'll move with a smooth vigour whilst benefitting from a more efficient solution.

Note: I will not implement all of the new enhancements in this session.
Assumptions
- You have installed Godot Engine v3.0
- You've completed the previous tutorials
- You should be familiar with Scenes, Nodes, creating instances and a basic grasp of GDScript syntax and documentation
You will
- Change the Formation implementation
- Amend the alignment of the Invaders in the grid
- Detect the Formation reaching its goal and stopping any more movement
Requirements
You must have installed Godot Engine v3.0.
All the code from this tutorial will be provided in a GitHub repository. I'll explain more about this, towards the end of the tutorial.
Going forward - a small note
In my previous posts, I've stated opinion, advice and thoughts on different topics; such as scripting languages versus interpreted ones, how Trees are used in Computer Science and so forth.
I'd already considered splitting these out into separate articles and that mirrors feedback I have received from others.
These tutorials will, therefore, focus solely on the 'coding' and 'design' side of this specific development. I will try to minimise other subjects and will branch them off to their own posts where required.
Last time
I left you pondering over a question, to recap:
The Invaders are falling off the bottom of the screen. Why????
More of a proverbial question because I hope you were thinking ahead. Quite simply:
I'd not enforced a screen height check with the Invaders.
As soon as one detects the bottom (much like the side borders), it should shout the fact and the entire fleet would stop and triumph in the battle against humanity!
Well done if you managed to consider this and secondly, big A* to anyone who had the confidence to code it.
Implementation Design
A 'fag-packet' design has been shown below, to represent the existing Scene implementation:
Fag-packet: Cigarette boxes are, therefore it is said that any design that can be drawn on one, must be good; often drawn by geeks down the pub whilst supping a nice beverage

The diagram depicts the Game Scene, which has an attached script that creates the Invader Scene instances and places them in formation.
The Invader instances assume independent thought and movement. I.E. each moves, checks whether they encroach a border and then calls out to the others to change direction. That means every Invader must 'listen' to events thrown at them. Further to this, there was a need to record the fact that a 'shout' occurred and not to respond to duplicates.
This emits a 'design smell'; something that doesn't feel right, but what?
The only reason the Invaders need to talk to each other is to preserve the formation when changing direction. Instead, what if we delegated this to a controller? Someone with a big megaphone? Their job is to ensure all Invaders stay in position and follow its command.
For this tutorial, I'm going to call that controller Invaders, implement it as a Scene and add many Invader instances to it, as children:

The design doesn't change much, but this single change will ripple through space and time, causing simplification to the implementation. The yellow box has NOW evolved into a Scene.
When you stumble on a change that simplifies the solution, that often is the RIGHT choice. Remember, simplicity is OFTEN best for performance and readability (although there are always exceptions!)
It will be assigned the following responsibilities:
- Create the formation of Invader instances
- Determine how to move the Invader instances as one unit
- Check each Invader, in turn, looking for when to reverse direction
- Drop the formation down when a reverse occurs
- Check for conquering the pesky humans; i.e. reached the bottom
From this list, you'll observe that most of these (if not all) responsibilities already exist in the Invader implementation. This is a good indicator that you had a poor design. By promoting the responsibilities upwards, into a controller, the entire design simplifies.
Let's implement this change!
Create new Invaders Scene
Please:
- Create a new Scene
- Add a Node2D as the root, because it has the capability to be placed on the screen
- Save the scene (I saved it in a folder called Invaders. Then remember to save the scene)
- ... finally, attach a new script
The following code should be pasted in:
extends Node2D
const INVADER = preload("res://Invader/Invader.tscn")
var screenWidth = ProjectSettings.get_setting("display/window/size/width")
var screenHeight = ProjectSettings.get_setting("display/window/size/height")
var direction = Vector2(400, 0)
func addAsGrid(size):
for y in range (size.y):
var newPos = Vector2(0, y)
for x in range (size.x):
newPos.x = x
createInvader(newPos)
func createInvader(pos):
var invader = INVADER.instance()
invader.position = pos * invader.size
add_child(invader)
func moveFormation(delta):
position += direction * delta
if direction.y > 0.0:
position.y += direction.y
direction.y -= 1.0
func checkBorderReached():
for invader in get_children():
if hitLeftBorder(invader) or hitRightBorder(invader):
direction.x = -direction.x
direction.y = 8.0
break
func hitLeftBorder(invader):
if direction.x < 0.0:
if invader.global_position.x < 0:
return true
return false
func hitRightBorder(invader):
if direction.x > 0.0:
if invader.global_position.x > screenWidth:
return true
return false
func _process(delta):
moveFormation(delta)
checkBorderReached()
The code looks like this in my editor (remember to sort the tabulation):

Let's work through this code:
extends Node2D
const INVADER = preload("res://Invader/Invader.tscn")
var screenWidth = ProjectSettings.get_setting("display/window/size/width")
var screenHeight = ProjectSettings.get_setting("display/window/size/height")
var direction = Vector2(400, 0)
- We extend the Node2D class
- Set-up a constant to our Invader Scene (which we originally had in our Game Scene script)
- Capture the screen dimensions in local variables
- ...and finally set-up a Vector2 with our initial direction for the Invader movement
NOTE: the scale has changed, because we are going to be moving the single Node2D, which has all the Invader instances attached as children. By moving this node, the children move too. The 400 in the X-axis means move 400 pixels in 1 second. The screen is set to 1024 pixels wide, thus it will take a little over 3 seconds from border to border.
func addAsGrid(size):
for y in range (size.y):
var newPos = Vector2(0, y)
for x in range (size.x):
newPos.x = x
createInvader(newPos)
This replaces the original init function found in the Game Scene script.
I've simplified the creation by delegating the creation of the invader into a secondary function:
func createInvader(pos):
var invader = INVADER.instance()
invader.position = pos * invader.size
add_child(invader)
This function is a slave to the previous. It has been spilt this way because, for me, it makes it much more easier to understand. Secondly, each function has a specific task to perform, i.e. the first constructs the layout, whilst this second creates the Invader. There is also the added benefit that the second function may be reused (..that's my intention!)
One very important observation to make is that the invader position uses the 'size' of the invader itself. This is provided by the Invader instance (see the next section). This 'size' variable is very important because it allows each Invader to be placed precisely side by side without any overlap.
func moveFormation(delta):
position += direction * delta
if direction.y > 0.0:
position.y += direction.y
direction.y -= 1.0
This, when called with delta time, moves the current formation on screen. If direction down has been set, it decreases it per call, thereby ensuring the Invaders drop a little before resting.
I've omitted a discussion on delta time. This is a special unit of time that all game engines use to smooth movement and communication. When the process method in any Godot Engine node is called, the delta time is provided. Delta time is a simple topic, but one that is hard to grasp, therefore I will write a separate article shortly to explain its purpose and use.
For now, delta time is to used to ensure the formation travels 400 pixels every second, no matter the Frames Per Second that the game may execute at. Given the screen is 1024 pixels, you can, therefore, assume a movement from one side to the other takes a little over 3 seconds.
func checkBorderReached():
for invader in get_children():
if hitLeftBorder(invader) or hitRightBorder(invader):
direction.x = -direction.x
direction.y = 8.0
break
This performs the function of determining whether any invader has touched a border and then reverses the formation's direction. It negates the need for the Node Group communication found in the previous article.
- A loop for every child of the Invaders scene is used to gain a reference to each Invader
- For the given Invader a check is called, via two utility functions; checking each of the horizontal borders
- If either is true, the horizontal direction is reversed and the Invader is set to drop down
- ... when true, break, a special script term which forces the current loop to stop, is called. In this case, we want to stop looking for any more Invaders crossing the border, because we already have one! We only need to check until the first case is found, thus this is more efficient than the Node Group communication method. Every Invader in that solution needed to listen, shout or react!
func hitLeftBorder(invader):
if direction.x < 0.0:
if invader.global_position.x < 0:
return true
return false
The first of the two border utility functions. This determines whether the Invader has crossed the left hand border.
- It first checks the current movement is left (it would be -8.0)
- If the Invader is moving left, then check whether it has crossed the left border
- Given both states are true, then return true
- Otherwise default to returning false
It is important to test that the Invader is moving left as well as having crossed the border, to avoid a specific bug.
It is possible for the Invader to cross the border in such a way that although the direction is reversed, the Invader still remains over the border edge; which results in a double reversal via another check. This results in a strange wobble effect and a random conclusion.
func hitRightBorder(invader):
if direction.x > 0.0:
if invader.global_position.x > screenWidth:
return true
return false
The second utility class is very similar to the first, but checks the Invader is moving right and has crossed the right border
func _process(delta):
moveFormation(delta)
checkBorderReached()
... finally, the process method is called every frame by the Godot Engine, along with the delta time. This relays the information to the moveFormation function and then checks for any border hit.
Note: although this is all you need to add for the Invaders scene, the game will fail if you try to run it. We need to alter the Invader and Game Scenes; given we have moved responsiblities and the functionality that goes with it.
Invader script alterations
The Invader is drastically simplified because it has now delegated the formation movement logic to the Invaders instance! This means you can delete all of the original lines and replace them with these:
extends Sprite
var size
func _init():
size = texture.get_size() * get_scale()
In the editor:
As you can see, this is significantly less script. Let me explain it:
extends Sprite
Extend the Sprite class, as before
var size
Allocate a class variable called size, this can and IS accessed by the Invaders createInvader function. It is populated by the following init function.
NOTE: I've just noticed a mistake here! See if you can figure it out. Hint, think about the function that calls the createInvader; specifically, what its responsibility is and the problem I will have with reusing this function.
func _init():
size = texture.get_size() * get_scale()
For every object created, the _init_ialise method will be called first; therefore, the calculation of the size of the Sprite is made here. The Size of the Texture is obtained and it is scaled. Unfortunately, there isn't a built-in Sprite property for this, but I personally think it would have been a useful exposure by Godot.
That's it! The Invader is very dumb now.

Although it would be sensible if you were to select the Invader root node, select Node > Groups in the Inspector and delete the Invaders label, because this is not now needed for the communication:
Game script alterations
The Game script becomes much more compact now, too:
extends Node
const INVADERS = preload("res://Invaders/Invaders.tscn")
func _ready():
var invaders = INVADERS.instance()
invaders.addAsGrid(Vector2(8, 4))
add_child(invaders)
Or as seen in the editor:
Let's analyse the code:
extends Node
The same vanilla Node is extended
const INVADERS = preload("res://Invaders/Invaders.tscn")
A new constant is loaded with our new Invaders class (make sure you get the folder structure correct)
func _ready():
var invaders = INVADERS.instance()
invaders.addAsGrid(Vector2(8, 4))
add_child(invaders)
A new ready function is added, which is automatically called once by Godot Engine after all other children nodes have been added to the Scene.
- An Invaders instance is created
- The instance is asked to create an 8 by 4 grid of Invaders
- Finally, the Invaders instance is added as a child, and thus, shows on screen
NOW try running the game! You should experience a new, improved and rather smooth formation of Invaders:

Notice how the Invaders are all now butting up against each other; we'll fix that in a section below.
Amend the alignment of the invaders in the grid
Previously, I mentioned that I realised there was a bug in my Invaders createInvader function:
func createInvader(pos):
var invader = INVADER.instance()
invader.position = pos * invader.size
add_child(invader)
The assumption I had when I created this was that it would be reused by other formation functions when added.
Currently, the addAsGrid function is the only type of formation and is the sole consumer of the createInvader function.
When I created the createInvader function, I knew that I wanted to space the Invader by its size, to ensure they are evenly spaced, to avoid touch, but be as close as possible. However, if I were to reuse this function, applying the size to the position may not be desired; in fact, highly unlikely.
Instead, what I needed to do was apply the size in the addAsGrid function and pass the absolute and final position to the createInvader function; thereby, allowing the create function to be used by other formation functions.
...but a catch-22 arose, because the Invader instance is not available in the addAsGrid function!
Therefore, it's not possible to calculate the position until the Invader is created, because the size is required to calculate the position, which is passed! DOH.
A compromise is required. This allows for the reusability of the createInvader function, by passing the reference back, thereby enabling the addAsGrid to set it's postion, like so:
func addAsGrid(size):
for y in range (size.y):
var newPos = Vector2(0, y)
for x in range (size.x):
newPos.x = x
var invader = createInvader()
invader.position = newPos * invader.size
func createInvader():
var invader = INVADER.instance()
add_child(invader)
return invader
or as seen in the editor:
- Line 15: Now calls the createInvader function with no parameter. In return, it expects to receive the new Invader instance
- Line 16: Sets the position of the new instance, using the grid position * Invader size
- Line 18: The pos parameter has been removed
- Line 21: Returns the new instance of Invader
This now works well, but doesn't fix our shoulder to shoulder Invaders issue.
Let's give them a 5 pixel padding, by changing Line 16:
invader.position = (newPos * invader.size) + Vector2(x*5, y*5)
For each (grid position * Invader size), we have to add 5 pixels to both coordinates per grid position
The script in the editor looks like this:
... and the result on the formation is great!

Detect the end of Humanity!
We need to address the Invaders wiping us out! Their goal is to reach the bottom of the screen or to KILL Humanities only hope, with a Fricking Lazer Beam (or a simple bullet).
The screen height can be used, much like the left and right border checks, therefore we need to add a couple of new methods and amend the proc in the Invaders script:
func checkForWin():
for invader in get_children():
if hitBottomBorder(invader):
direction.x = 0
break
func hitBottomBorder(invader):
if invader.global_position.y > screenHeight:
return true
return false
func _process(delta):
moveFormation(delta)
checkBorderReached()
checkForWin()
as seen in the editor:
Let's examine the code:
func checkForWin():
for invader in get_children():
if hitBottomBorder(invader):
direction.x = 0
break
The new method checks whether the Invaders have reached the bottom of the screen
- Loop through all the invaders
- If the current invader has hit the bottom
- ... then set the horizontal movement to zero; freezing the formation (we'll need to change this in the future to change the game state to "end")
- ... finally break out of the loop as the game has been ended
func hitBottomBorder(invader):
if invader.global_position.y > screenHeight:
return true
return false
This method is a simplified version of the left and right border checks. The Invader global height position is checked against the height of the screen. If it has passed the threshold, it returns true, otherwise it returns false.
func _process(delta):
moveFormation(delta)
checkBorderReached()
checkForWin()
The checkForWin function has been added to the list of calls to make for each process frame.
Finally
That concludes this issue. I will follow these up shortly with several more:
- How to add smooth transitions (acceleration and de-acceleration) to the formation
- Adding graphics to the backdrop and Invaders
- Adding the player ship and firing a weapon
Please do comment and ask questions! I'm more than happy to interact with you.
Sample Project
I hope you've read through this Tutorial, as it will provide you the hands-on skills that you simply can't learn from downloading the sample set of code.
However, for those wanting the code, please download from GitHub.
You should then Import the "Space Invaders (part 2)" folder into Godot Engine.
Other Tutorials
Beginners
Competent
Posted on Utopian.io - Rewarding Open Source Contributors
Thank you for the contribution. It has been approved.
You can contact us on Discord.
[utopian-moderator]
Hey @roj, I just gave you a tip for your hard work on moderation. Upvote this comment to support the utopian moderators and increase your future rewards!
@sp33dy, Upvote is the only thing I can support you.
Any support is great support; thanks
Great tutorials, how is your java? We could definitely need some good devs in our project =) (Minecolonies)
Java, I'm an expert :) Seriously, I'm an I/T Architect with a solid ~18 years Java background as a programmer. Unfortunately, I've got enough going on right now.
What a pity, we could need some additional manpower, we got a great team assembled already but we have a lot planned as well. We had around a million download in the last half year so if you're interested and with spare time one day hit me up =)
I definitely think we should talk at some stage. I even considered writing games in Java myself, but jumped on Godot, as it has easier support for different platforms. I assume you aren't hitting out at tablets/mobiles? My expertise is really eCommerce and back-office integrations, especially Java performance tuning.
Our application runs on the client and server of Minecraft.
It's quite a performance critical on the server as we have a lot of entities and at the same time, a lot of players and all of that currently runs on one core (The Minecraft world ticks entities only on one core).
Performance tuning could be definitely something we could need.
We also wanted to set up a nice system which logs us the performance of the different systems we have, so we discover our weak points more easily, but as I wrote before, we're missing manpower for those tasks.
Hey @sp33dy I am @utopian-io. I have just upvoted you!
Achievements
Suggestions
Get Noticed!
Community-Driven Witness!
I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!
Up-vote this comment to grow my power and help Open Source contributions like this one. Want to chat? Join me on Discord https://discord.gg/Pc8HG9x
You've got upvoted by
Utopian-1UP
!You can give up to ten 1UP's to Utopian posts every day after they are accepted by a Utopian moderator and before they are upvoted by the official @utopian-io account. Install the @steem-plus browser extension to use 1UP. By following the 1UP-trail using SteemAuto you support great Utopian authors and earn high curation rewards at the same time.
1UP is neither organized nor endorsed by Utopian.io!