Tutorial (Godot Engine v3 - GDScript) - Colour use!

in #utopian-io7 years ago (edited)

Godot Engine Logo v3 (Beginner).png Tutorial

...learn to add colour to the game!

What Will I Learn?

I ended the last Tutorial stating that the next step is to add colour to the game!

If you've tried the "Space Invaders" clone on itch.io, you will already be aware of what I mean by this; i.e. I've implemented a touch of colour!

The invaders begin with random colours and the player may select a ship colour, by pressing up/down. If an Invader is struck by a bullet of a matching colour, it is destroyed, otherwise, it raises its shield; thereby protecting it.

I'm going to show you how to add these features!


shields a working.gif

Note: the recording method does so at 30fps

therefore it ruins the quality throughout this article!


Assumptions

  • You have installed Godot Engine v3.0
  • You are familiar with GDScipt
  • You are familiar with the previous tutorials

You will

  • Add Screen Wrap Node to the player ship
  • Change the screen to HD resolution
  • Add multi-coloured Invaders
  • Add colour to the Player's Ship
  • Add colour to the lasers
  • Add shields to the Invaders

Requirements

You must have installed Godot Engine v3.0.

All the code from this tutorial is provided in a GitHub repository. I'll explain more about this, towards the end of the tutorial. You may want to download this


Add Screen Wrap Node to the player ship

I've written a competent level article explaining how to build and apply an Asteroid style screen wrap to any Node2D class.

We shall take the code from its project and apply it to the Player Scene.

You will need to access the GitHub source files

image.png

You require a copy of the ScreenWrap folder, along with its content; two files.

With this copied into place, open up the Player Scene and add a ScreenWrap instance:

image.png

The following structure is formed for the Player Ship (I've moved the ScreenWrap instance up, but there is no necessity to do so; as long as it remains a child of the Sprite)
image.png

Try running the game now, does it work?

... the answer is no. Do you know why?

When we previously added the Player script, we deliberately restricted the player to the edges, because the ship was sailing off the edges.

Given we now have a wrap solution, we should remove the clamps.

Open the Player Scene and edit its script:
image.png

Remove the four highlighted rows 24 to 27, pinning the Ship to the borders.

Try rerunning!

wrapping.gif

This demonstrates how easy it is to add the Wrap node into any project.

Let's tidy up the code a little more before we move on.

The Move function can be compressed to this:

func move(adjustment):
    position.x = position.x + adjustment.x

Given we aren't clamping to the borders, there is no need to maintain the additional variable

In the ready function, there is no need to calculate the borders, therefore we can delete the following highlighted rows

image.png

Rows 14, 15 & 16

You can also delete the two Class variables above the function; rows 8 & 9.

These small little improvements shrink and cleanse the script, making it much easier to understand.

Change the screen to HD resolution

Although the following steps weren't absolutely necessary for this tutorial. I felt an urge to remove my concern and issue with the game resolution.

I dislike the default 1024 x 600 resolution

Further to this, it doesn't lend itself to resizing for different devices; thus, fixing this now, will save pain down the road.

Please open up the Project Settings and set the screen to 1920 x 1080, the standard resolution for HD (High Definition).
image.png

If you run the game again, you'll notice the screen becomes extremely wide and everything is misplaced.

Resolutions will be different on devices, i.e. an iPhone will be different to an Android, an iPad will be different to another Tablet, even a monitor will be sold with different capabilities.

What we need to do is build this 'Space Invaders' clone, with the capability of adapting to any device.

...but how are we going to do that?

The decision for this game is to build it to operate at a High Definition resolution and then scale up/down as required.

Godot Engine supports this for us, through its Project Settings.

Correct the stars

Since we changed to HD resolution, the star background is smaller than the screen size, we need to stretch it again:
image.png

We will replace this fixed image for dynamic particles soon, but for now, open the Stars Scene. Click on the Sprite and change the Node Inspector property Transform/Scale to 2 x 2:
image.png

This will double the size of the image. When you save and return back to the Game Scene, it shall be filled with stars again!
image.png

If you don't use 2 x 2, but instead use say 2 x 1, the Stars will fit, but will be squashed in aspect, i.e. you double its width but would retain its height (like one of those magic mirrors at the funfair)

When you run the game now, a massive window will open again, with the Stars present. However, it is all too large (unless you have a 4k monitor; lucky whats-it!).

Let's use one of the really neat features of Godot-Engine. It has the capability to correct the size by shrinking or stretching as required!

Scroll to the bottom of the Project Settings of the Window
image.png

  • Change the Stretch/Mode property to 2D
  • Change the Aspect property to Keep (which is keep the aspect ratio based on both axes)

Scroll back up to the Size properties and enter 960 for Test Width and 540 for Test Height.
image.png

If you have the luxury (I don't) of a monitor with greater than HD resolution, I recommend you don't set this setting! You can develop in full HD mode; oh, how I lust for a 4K monitor.

Assuming you don't have a tiny monitor when you rerun, normality will return!


image.png

A sensibly sized window will display! In effect, we've increased our resolution to HD and then instructed Godot Engine to scale it to half HD size; this will be easily displayable by the monitor, although we've LOST half the quality of the graphics. For devices that do have the resolution, they benefit from greater clarity!

If you play with the test sizes, you'll see how Godot Engine deals with different resolutions. It will automatically scale and add black bars where it needs to, in order to preserve the screen aspect.

Aspect is measured and discussed as a ratio. I.E. take 1920 and divide by 1080, you get an aspect ration of 1.77 (recurring).
To fill the screen, the pixels cannot be square, or you would have a TV with an aspect ratio of 1.
Instead, the pixels will be wider than they are high!
I.E. for every vertical pixel it will be wider than a single pixel, in fact, 1.77-pixel size for HD Aspect! Therefore, if you draw a square of 10 pixels high, it will measure ~17.777 pixels wide!

When a game is loaded by a non-HD device, we want it to maintain these pixels. I.E. we've configured Godot Engine to keep to Both Aspects, but there are other options, i.e. maintain only horizontal or vertical, or ignore aspect etc! Have a play with the different settings.

Once the game is ready, we will enable FULL SCREEN.

The full resolution of the device would then influence how the game initialises. With our configuration, it would respect our Aspect Ratio, ensuring the design of our game remains good on any device, Albiet with potential black bars to pad it out!

Fix Player ship

With the new resolution, you will notice our Player now starts in the wrong place, let's move it by implementing a 'start position' in its script.

Here are the new lines of the script (top half of entire script):
image.png

var screenWidth = ProjectSettings.get_setting("display/window/size/width")
var screenHeight = ProjectSettings.get_setting("display/window/size/height")

I've added the fetch for the screen height (line 7)

func _ready():
    moveToStartPosition()
    $Area2D.connect("area_entered", self, "hit")

I've added the call to the moveToStartPosition function to the ready function (line 12)

func moveToStartPosition():
    var halfSpriteSize = (texture.get_size() / 2) * scale
    position = Vector2(screenWidth/2, screenHeight - halfSpriteSize.y)

The new function calculates the sprite size, halves it and then scales the resultv(line 16)
The ship is then moved to the centre on the horizontal and a half a sprite height up from the bottom on the vertical axis (line 17)

However, if you run this, something very peculiar occurs!


wrapping bug.gif

The Ship is restricted to the left and middle of the screen wrapping!

I've inadvertently discovered a BUG in the generic ScreenWrap Node!!!!!!

Given we are now using the Screen Stretching Aspect of Godot Engine, the generic code uses the get_viewport function to obtain the screen size. However, the viewport is the actual size of our Test Window setting or Device, NOT our intended Screen Size setting of the project Setting!

A fix is required in the generic Screen Wrap Node! Open the ScreenWrap script and change the initWrapArea function to this:

# Initialise the wrap area to screen size if not set
func initWrapArea():
    if wrapArea == null:
        var screenWidth = ProjectSettings.get_setting("display/window/size/width")
        var screenHeight = ProjectSettings.get_setting("display/window/size/height")
        wrapArea = Rect2(Vector2(), Vector2(screenWidth, screenHeight))

I've removed the get_viewport().size in the wrapArea assignment and replaced it with a new Vector2 create using the screen Width and Height, obtained from the ProjectSettings

If you run the game again, all is well in the world!


fixed wrap.gif

The automatic aspect is applied for us, but our Ship moves to the original dimensions (of HD)! This neatly illustrates my point! We can design the game at HD quality and Godot will automatically scale it to different resolutions, allowing us to forget about individual devices. This technique doesn't work for all games, but it does for the majority. If you need absoutely EVERY pixel of a specific game at a specific Aspect resolution, you would build each time individually.

IF I'd not thought of dealing with the device sizes today, I would have discovered the problem of my generic wrap node a long way down the development path. For which it might have been a LOT harder to rectify. So this is actually a good catch! Although, I hate making obvious errors like this.

Resize the Ship

Let's resize the ship to something that will look reasonable onscreen, safe in the knowledge it will look right when scaled to all devices.

I like the Ship scale set to (0.8, 0.8):
image.png

Feel free to experiment and set what you like!

Resizing works because we've included the scale value in our calculations. It is important to remember that Scale is applied to all textures. When I first learnt Godot, this and rotation, kept tripping me up!

Resize the Invaders

As with the Ship, let's turn our attention to the Invaders!

The current Invader image is relatively small, therefore I like the (1, 1) scale; but we'll revisit this when we add other types of Invader.
image.png

On running the game, I'm happier because it looks much better. A small tweak to the scale of the laser bullet (0.8, 0.8) completes the job!
resized.gif

Looking at the result, I will increase the velocity of the bullets shortly; see if you can figure that out for yourself?!?!

Add multi-coloured Invaders

Finally, we join the mission at hand. Let's introduce coloured Invaders!

Making them different colours is a trivial task of amending the Invader Scene script.

First, we add in a constant variable that will preload each individual Invader image:

const INVADER_TYPES = [
    preload("res://Invader/images/spaceship-white-03.png"),
    preload("res://Invader/images/spaceship-red-03.png"),
    preload("res://Invader/images/spaceship-yellow-03.png"),
    preload("res://Invader/images/spaceship-blue-03.png"),
    preload("res://Invader/images/spaceship-grey-03.png"),
    preload("res://Invader/images/spaceship-black-03.png")
]

The preload does exactly that. It instructs Godot to load the image into its cache, ready for use by the script.

Given we have an array, we can pick an image at random, by referencing the array[position]! Thus, we can assign one of these to the Sprite's texture property and the correct Sprite with Colour will show.

Next, we'll add a property to Invader so that the colourType can be set.

export (int, "WHITE", "RED", "YELLOW", "BLUE", "GREY", "BLACK") var colourType setget setColourType

This variable is exposed as a property for the Node Inspector, because individual Invaders may be added via the 2D view. ... but I also wanted to show the possibilities that export provides! As you can see, I've listed each colour as a String, but when selected in the Inspector, an integer is returned, representing the position; i.e. 0 is White,1 is Red, and so forth.

func setColourType(newColourType):
    if newColourType != null:
        colourType = newColourType
        texture = INVADER_TYPES[colourType]

A setColourType function is required. This is called by either the setget condition, when the colourType is set by the tool or by other external classes.
It first determines if a new colour type (integer) has been provided, ignoring it if not; this is important because on an initialise, a null value can be received.
Given a value is present, set the local variable (because the setget doesn't do this automatically) and then set the Sprite's texture to the colour image found in the constant array.

func setSize():
    size = texture.get_size() * get_scale()

The size was originally calculated in the init function, but I've lifted it into its own separate function for clarity and reuse

func initType():
    if colourType == null:
        var pick = randi() % INVADER_TYPES.size()
        setColourType(pick)

A new initType function is added and called by the init function (below)
If the initialise stage is triggered and the colourType remains null, we will randomly pick a colour instead. Therefore the random integer function is called with the number of images in the array. This value is then sent to the setColourType function, which sets the texture!

func _init():
    initType()
    setSize()

The new initialise function ensures a type has been set before the size function establishes the Sprites size; if it isn't, there is no loaded texture and the initialise can't happen (i.e. there is nothing loaded to gauge a size from)

Running the game now results in what we expected:


coloured invaders.gif

See if you can add colour to the Player ship; hint, it is very similar, but you need to think about the user control!

Add colour to Player ship

Adding colour to the ship is a similar process to the Invader. Add the constants and colourType, ensuring you set the correct image paths for the Player ship!

const PLAYER_TYPES = [
    preload("res://Player/images/spaceship-white-07.png"),
    preload("res://Player/images/spaceship-red-07.png"),
    preload("res://Player/images/spaceship-yellow-07.png"),
    preload("res://Player/images/spaceship-blue-07.png"),
    preload("res://Player/images/spaceship-grey-07.png"),
    preload("res://Player/images/spaceship-black-07.png")
]

export (int, "WHITE", "RED", "YELLOW", "BLUE", "GREY", "BLACK") var colourType setget setColourType

As stated, the preload is for the Player image, rather than Invader, but it looks similar
We also keep the colourType variable

func setColourType(newColourType):
    if newColourType != null:
        colourType = newColourType
        texture = PLAYER_TYPES[colourType]

func _init():
    setColourType(0)

We reuse the same setColourType function and ensure it is called by the initialise function; hardwiring it to zero (White)

If you run the game, you'll note the Player now starts in white!

...but how does the player change colour? We want them to use the up/down cursor key, so we need to adjust the Game script and add a function to the Player script to modify the colour.

Let's start by adding a function to the Player script:

func adjustColour(adjust):
    var newColourType = colourType + adjust
    if newColourType < 0:
        newColourType += PLAYER_TYPES.size()
    elif newColourType >= PLAYER_TYPES.size():
        newColourType -= PLAYER_TYPES.size()
    setColourType(newColourType)

This function accepts an adjustment value in colour, which is added to the existing value

It then wraps the colour round if it reaches the end

In the Game script, we add the following to the process function:

    if Input.is_key_pressed(KEY_UP):
        $Player.adjustColour(-1)
    if Input.is_key_pressed(KEY_DOWN):
        $Player.adjustColour(1)

This instructs the Player to change colour when either UP or DOWN cursor key is pressed

Try running it!

... it works, but!?!


colour change quick.gif

... Yes, we need to add a delay to the colour change. The Player script should only allow the change IF it is ready. So we need to introduce a counting variable , a decrement in the process function and a condition in the adjust function:

const CHANGE_COLOUR_TIME = 0.2
var changingColour = 0.0

Declared is the constant time between allowable colour changes and a counter variable of how long there is left before a colour change can occur

func _process(delta):
    reloading -= delta
    changingColour -= delta

In the process function, we now decrement delta from the changingColour counter

func adjustColour(adjust):
    if changingColour < 0.0:
        var newColourType = colourType + adjust
        if newColourType < 0:
            newColourType += PLAYER_TYPES.size()
        elif newColourType >= PLAYER_TYPES.size():
            newColourType -= PLAYER_TYPES.size()
        setColourType(newColourType)

The condition is added to ensure the colour change can only occur if the counter has run out

func setColourType(newColourType):
    if newColourType != null:
        colourType = newColourType
        texture = PLAYER_TYPES[colourType]
        changingColour = CHANGE_COLOUR_TIME

Finally, the colour change function resets the count down, to prevent another change in the period defined

Running it is now reflects expected behaviour:


delayed colour change.gif

Add colour to lasers

The laser colour will depend on the Ship's colour, hence we either pass the colour of the ship to the laser or get the laser to check the parent. Given the ship 'could' change colour whilst a bullet is issued, it is best in this scenario to pass the colour when firing the bullet.

The changes to the laser script resemble those applied to the Ship.

const VELOCITY = Vector2(0, -600)

const LASER_TYPES = [
    preload("res://Bullets/Laser/images/laser-white-02.png"),
    preload("res://Bullets/Laser/images/laser-red-02.png"),
    preload("res://Bullets/Laser/images/laser-yellow-02.png"),
    preload("res://Bullets/Laser/images/laser-blue-02.png"),
    preload("res://Bullets/Laser/images/laser-grey-02.png"),
    preload("res://Bullets/Laser/images/laser-black-02.png")
]

export (int, "WHITE", "RED", "YELLOW", "BLUE", "GREY", "BLACK") var colourType setget setColourType

func setColourType(newColourType):
    if newColourType != null:
        colourType = newColourType
        texture = LASER_TYPES[colourType]

I changed the velocity whilst I was in this script, doubling its speed

The constants for the laser colour, along with the colourType and set function were added

We then modify the Player Scene script, because we shall set the Laser colour from it:

func fire():
    if reloading <= 0.0:
        var bullet = BULLET_LASER.instance()
        _bullet.colourType = colourType_
        bullet.global_position = global_position
        get_parent().add_child(bullet)
        reloading = RELOAD_TIME

In the fire method, we set the Bullet colourType to the colourType value of the Ship

Try running it!


coloured lasers.gif

... all is good!

Can you guess how we implement the shield of the Invaders? Hint: think about the colourType variables, they will help you!

Add shields to the Invaders

This, for me, is the exciting bit!

The aim of game development is to make it unique and stand out. I don't want to build a clone of 'Space Invaders', I want to build my own twist on it. Something that people will pick-up and want to play!

Using colour is one differentiator, but I have other thoughts to explore too!

I asked in the last section if you could think of how to implement the shields of the Invaders. This is really quite simple!

When a bullet is detected by the Invader, the Area2D sends a signal to the Invader hit function. The call to the hit function includes the object that struck it, i.e. the Laser!

All we need to do is check the colourType against the Invader, if it matches, the Invader is destroyed. Alternatively, the shield shown and an automatic reduction of the shield alpha colour channel can be performed at each frame.

In the Invader script, the hit function is modified:

func hit(object):
    if object.name == 'LaserArea':
        if object.get_parent().colourType == colourType:
            queue_free()

The Invader is only removed if the object striking it was a LaserArea and that its parent (Laser Scene Script) was the same colour

Try running the game!


coloured invaders killed.gif

... half of the design is now implemented, but what about the other?

The Invaders can now only be killed by the correct bullet colour.

However, they need shields! The shield is purely a cosmetic special effect, to reflect back to the player the invicibility of the Alien!

A new Shield Scene was created, utilising new images with elipse shapes that have a slight opacity.

image.png

The Shield script was coded much like the other Nodes:

extends Sprite

const SHIELD_TYPE = [
    preload("res://Shield/images/shieldWhite.png"),
    preload("res://Shield/images/shieldRed.png"),
    preload("res://Shield/images/shieldYellow.png"),
    preload("res://Shield/images/shieldBlue.png"),
    preload("res://Shield/images/shieldGrey.png"),
    preload("res://Shield/images/shieldBlack.png")
]

var colourType setget setColourType

func setColourType(newColourType):
    colourType = newColourType

func _ready():
    if colourType != null:
        texture = SHIELD_TYPE[colourType ]

func _process(delta):
    modulate.a -= 1.0 * delta

The script displays the correct shield image and colour

It automatically reduces the opacity levels until it vanishes

The Invader script is modified to add a shield when it is ready:

const SHIELD = preload("res://Shield/Shield.tscn")

A constant variable is initialised for the Shield Scene

func addShield():
    var shield = SHIELD.instance()
    shield.colourType = colourType
    shield.modulate.a = 0.0
    add_child(shield)

A new addShield function has been added to instance a new Shield Node, setting it to the colour of the Invader and make the shield invisible. Finally it is added as a child to the Invader parent

The final amendment is extending the Invader hit function:

func hit(object):
    if object.name == 'LaserArea':
    if object.get_parent().colourType == colourType:
            queue_free()
        else:
            $Shield.modulate.a = 0.9

The additional else condition has been set for when the Bullet colour does not match the Invader.
In that situation, the shield's alpha channel is increased, thus revealing it onscreen, ready for its own process function to drain it again; therefore on screen, we see a flash of the shield!

Try running it again:


shields a working.gif

Success! The game delivers as expected (for now).

Finally

This tutorial took so much more time to write than the others! It was a simple solution for me to develop, but has been far more involved in describing. Maybe I've dropped into too low level, but my intention was to show how straight forward the implementation is! It's not complex in any shape or form, yet, the result looks good (well, in my opinion)!

My next intention is divided! I'm torn between showing 'formations' of Invaders swooping in, OR, getting the game life cycle running, i.e. a Splash Screen, Game and End. There is a need at some stage to create a 'game' and have the context between states. I'll ponder over this for now.

Don't forget to check out the game on itch.io. I'm publishing the game in advance of tutorials, so it is worth you checking it out periodically! You'll be able to guess what is coming up in my posts!

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 with 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 8)" folder into Godot Engine.

Other Tutorials

Beginners

Competent



Posted on Utopian.io - Rewarding Open Source Contributors

Sort:  

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!

Hey @sp33dy I am @utopian-io. I have just upvoted you!

Achievements

  • You have less than 500 followers. Just gave you a gift to help you succeed!
  • Seems like you contribute quite often. AMAZING!

Community-Driven Witness!

I am the first and only Steem Community-Driven Witness. Participate on Discord. Lets GROW TOGETHER!

mooncryption-utopian-witness-gif

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