Tutorial (Godot Engine v3 - GDScript) - Smooth movement!

in #utopian-io6 years ago (edited)

Godot Engine Logo v3 (Competent).png Tutorial

...learn how to smooth the Invader movement!

What Will I Learn?

This tutorial builds on the last, which explained how to form Sprite formations.

In this article, you will learn how to smooth the Invader movement. I wanted to include this in the last article but ran out of time.

I defined a list of enhancements in the last tutorial, therefore I will now show you how to deliver the missing items:

  • 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

By the end of this tutorial, you will understand how to control the waves of Invaders with nice smoothing effects that add visual appeal:


Invaders with Wave.gif

Note: the recording method destroys the smoothness that is experienced when executing in a game window!

Assumptions

You will

  • Apply Acceleration and De-acceleration
  • Move the border goal posts
  • Smooth out the Invader drop
  • Apply smooth wave patterns to Invader columns

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.


Acceleration and De-acceleration

Watching those Invaders richet of the left and right borders gave me a headache! I just didn't like the effect, therefore, I wanted to show them change direction with a little more grace.

I made the design decision to apply a little physics to the game, albeit, tailoured to the need of the game, rather than implement a FULL physics engine!

By mentioning the word 'physics', I fear this article will go straight to the top of search engine results, and hordes of excited people will arrive (I wish). I can only imagine how disappointed they will be when they find out that I'm using the concepts, rather than planning to implement world dynamics!

Currently, the formation lurches immediately right, then left when it touches the border, and so on. Instead, I will introduce three new concepts:

  • Velocity: the current speed. Imagine a car; it can be stationary or be travelling up to but not exceeding its maximum speed.
  • Acceleration: the time it takes to increase the velocity of an object, i.e. you place your foot on the accelerator and the car will steadily speed up; unless you drive like me
  • De-acceleration: the time it takes to reduce velocity to zero, i.e. you take your foot off the accelerator or/and you can increase it by pressing the brakes!

The existing formation uses a static directional value, so that has to change! We need to introduce two new variables and then apply them to the logic. The formation will then accelerate from a standing start:


image.png

It will then reach maximum velocity before reaching the border:


image.png

It is important to note that although maximum velocity is reached, acceleration is continually applied to maintain it! If not, de-acceleration shall apply.
In the real-world, especially in this case of the vacuum of space, there is no friction; therefore this would not be so! However, this is our game and our rules!

At the intersection of the border, we reverse the Acceleration. The formation will continue on its path, with an ever decreasing velocity until it reaches zero velocity:


image.png

It will then appear to reverse in direction, as it begins to accelerate to maximum velocity in the opposite direction:


image.png

This produces a smooth change of direction that looks pleasing to the eye. The procedure repeats from side to side, leaving an elegant visual movement.

Let's change the code and implement it!

Invaders script alterations

The changes required are all applicable to the formation, which is the Invaders script.

Changes to the declarations

I introduced new constants and variables:

extends Node2D

const INVADER = preload("res://Invader/Invader.tscn")

const MAX_VELOCITY = Vector2(400, 40)
const ACCELERATION = Vector2(12, 8)

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

var velocity = Vector2(0, 0)
var direction = Vector2(1, 1)

as seen in the editor:

image.png

Note: I discovered the variable spelling of ACCELERATION was ACCLERATION. The example and GitHub code are fixed, but I've not recaptured screenshots because of the cost of Steemit power.

The following has been added or changed

  • The new MAX_VELOCITY constant has been added. The name states what it is for, but to confirm, this is the maximum velocity that the formation may reach in the X and Y axis.
    Note: I've set the Y to 40 Pixels Per Second (PPS) because this will be our the new drop speed when the border is intersected.
  • The new ACCELERATION constant is the other fixed value and as per the diagram and explanation above, is the game world speed that the formation will accelerate by; i.e. 12 PPS horizontally and 8 PPS vertically
  • The new velocity variable has been added to store the current velocity of the formation
  • The direction variable has been hijacked, tasked with containing: -1, 0 or +1 in the specified axis

Changes to the moveFormation function

Two changes were required in the moveFormation function:

func moveFormation(delta):
    applyHorizontalAcceleration()
    position += velocity * delta
    if direction.y > 0.0:
        position.y += direction.y
        direction.y -= 1.0

or as seen in the editor:

image.png

The changes:

  • A new applyHorizontalAcceleration function call has been added (see below)
  • The position calculation now uses the velocity variable rather than the direction

New applyHorizontalAcceleration function

A new function was added to apply the acceleration:

func applyHorizontalAcceleration():
    if direction.x > 0.0:
        if velocity.x < MAX_VELOCITY.x:
            velocity.x = min(velocity.x + ACCELERATION.x, MAX_VELOCITY.x)
    elif direction.x < 0.0:
        if velocity.x > -MAX_VELOCITY.x:
            velocity.x = max(velocity.x - ACCELERATION.x, -MAX_VELOCITY.x)

As seen in the editor:

image.png

This method's purpose is to continually apply acceleration to the velocity in the direction set. It will ensure the MAX_VELOCITY is never exceeded:

func applyHorizontalAcceleration():

The function name with no parameters

    if direction.x > 0.0:

Check whether the horizontal direction is right

        if velocity.x < MAX_VELOCITY.x:

If so, check whether we are already at maximum velocity

            velocity.x = min(velocity.x + ACCELERATION.x, MAX_VELOCITY.x)

Increase the horizontal velocity until we reach maximum. The min function is used here to constrain it; i.e. when the acceleration is added to the velocity, if it is more than the maximum, we take the minimum value, which will be the constant.

    elif direction.x < 0.0:

If the direction is not right, let's check for left. If the direction is zero, NO acceleration is applied and the formation shall rest!

        if velocity.x > -MAX_VELOCITY.x:

Given the direction is left, check whether we are already at maximum velocity in the opposite direction

            velocity.x = max(velocity.x - ACCELERATION.x, -MAX_VELOCITY.x)

Given maximum velocity has not been reached add acceleration in the opposite direction.

The left direction is on the negative scale, therefore when acceleration is subtracted from velocity it will become less than the maximum velocity, hence the max function is used to constrain the maximum velocity.

Let's run the code!


horizontal acceleration (bumpy).gif

... Ah ... Great, it works, BUT... there are concerns!

When the border is hit, the formation continues to move with momentum, thus almost two (four due to recording issues) invaders disappear offscreen.

That's not good! Can you think of a way to solve this issue?

Note: a problem also exists when the formation reaches the bottom! Let it run and see what happens. Can you diagnose the issue?

Moving the border goal posts!

As seen, the formation has a little problem in which Invaders disappear over the edge of the screen, as de-acceleration is applied. This can be easily remedied, by moving the border detection points!

Currently, the horizontal border points are at zero (left) and the screen width (right).

Further to this, the Invader sprites are positioned centrally, therefore the intersection will always be at their midpoint.

What we want to do is move the border position (goal posts; for those that know about football) inward by at least two Invader widths. This would help immensely, although it would not be perfect! It would be an approximation because acceleration and maximum velocity should also be used to calculate the required positions.

For this tutorial, that math is beyond its scope. I will address it in a future article. I expect the game intensity to rise, which means these two parameters will increase! Have a think, can you solve it?

Let's make the changes!

hitLeftBorder function changes

We first need to make a simple change to the hitLeftBorder function:

func hitLeftBorder(invader):
    if direction.x < 0.0:
        if invader.global_position.x < invader.size.x * 2 :
            return true
    return false

The invader global position check has been increased from zero to two Invader widths.

hitRightBorder function changes

We also apply a simple alteration to the hitRightBorder function:

func hitRightBorder(invader):
    if direction.x > 0.0:
        if invader.global_position.x > screenWidth - (invader.size.x * 2) :
            return true
    return false

The Invader global position is now checked against the screen width minus two Invader widths.

Note: These two changes demonstrate why I separated these checks into individual functions.

By doing so, there was no need to change the checkBorderReached function.

Originally, I placed all of the code from the three functions into it, but it 'smelt' bad.

The readability was MORE important than the additional few lines added to the script, whilst creating the two extra functions. There is also an inherent benefit of reusability of the two hit functions!

Let's run it now!


horizontal acceleration (better).gif

Note: Unfortunately, due to recording issues, it's not as smooth as in Godot! Recording and conversion drop's frames to keep the file size small, hence quality is lost.

... Much better, although we need to deal with the drop! That is now painful to watch.

Smooth out the Invader drop

To smooth the Invader drop, we'll apply acceleration and de-acceleration in the Y-axis.

First, we'll add a new applyVerticalAcceleration function:

func applyVerticalAcceleration():
    if direction.y > 0.0:
        if abs(velocity.x) != abs(MAX_VELOCITY.x):
            velocity.y = min(velocity.y + ACCELERATION.y, MAX_VELOCITY.y)
        else:
            velocity.y = 0.0
            direction.y = 0.0

This function checks whether the direction down has been set, i.e. it will be either 1 or 0.

If so, using the abs function (absolute; i.e. ignore sign), make sure the Invader is not moving horizontally at maximum velocity; if it is, then it is unlikely it has reversed direction

Accelerate in the downward direction, constraining it by the maximum downward velocity; taking the minimum of the two calculations to ensure it is capped

Otherwise reset the downward direction and velocity to zero, because the drop is finished with

Next, we need to add this to the moveFormation function:

func moveFormation(delta):
    applyHorizontalAcceleration()
    applyVerticalAcceleration()
    position += velocity * delta
#   if direction.y > 0.0:
#       position.y += direction.y
#       direction.y -= 1.0

The applyVerticalAcceleration function call has been added to the second line
You may now delete the original three lines that performed the Invader drop (commented out above)

Let's rerun!


smooth drop.gif

Note: the drop looks far more drastic in the capture.

... Much much better! They are now zig-zagging smoothly and with ease!

Apply smooth wave patterns to Invader columns

I still dislike the static appearance of the grid of Invaders, therefore, let's add some wave patterns!

We'll use the good old Sine Wave to help our formation a little.

Before I start, there is an important decision to make. Who will have the responsibility for the position in the formation? I.E. Is the Invaders script going to tell the Invader instances where to position themselves OR should we allow for the Invader to make its own decision?

For now, I'm going to make it the responsibility of the Invader instance to adjust itself in a Sine Wave pattern; however, I'm likely to change this in a future session when we introduce new formations.

I will deliberately delay this change because I do not wish to think ahead of myself and start to build another formation type right now!

This is the type of thinking you will need to make as a developer. Prioritise what is needed, develop things that are likely to be changed in the future and make sure you WRITE decisions DOWN! Each decision should be noted in some way so that you don't forget a valuable thought! I use spreadsheets.

Changes to the Invader

Given the Invader script is now very simple, I need to provide you with the entire new script that I'd like you to implement:

extends Sprite

const WOBBLE_AMPLITUDE = 8
const WOBBLE_FREQUENCY = 2

var size
var time = 0.0

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

func _process(delta):
    time += delta
    wobble()

func wobble():
    offset.y = WOBBLE_AMPLITUDE * sin(time * WOBBLE_FREQUENCY) 

Or as seen in the editor:

image.png

Let's walk through the code:

extends Sprite

We still extend the Sprite

const WOBBLE_AMPLITUDE = 8
const WOBBLE_FREQUENCY = 2

We introduce two constants:

  1. WOBBLE_AMPLITUDE is how 'peaky' the wave will be; i.e. the height of it (in simplistic terms)
  2. WOBBLE_FREQUENCY is how often or quickly the wave is applied, i.e. the higher the number, the fast the wave shakes
    Have a play with these settings!
var size

The size variable already existed

var time = 0.0

Introduce a time variable, to hold the total elapsed time, used by the Sine wave calculation

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

Existing function to calculate the Invader size

func _process(delta):
    time += delta
    wobble()

The process function will total the elapsed time and then call the wobble function below

func wobble():
    offset.y = WOBBLE_AMPLITUDE * sin(time * WOBBLE_FREQUENCY) 

The wobble function simply calculates a new Invader offset value in the Y-Axis. Sprites may be positioned, which can be local to a parent and global to the screen. It also may have a second offset to the parent. In this way, we isolate the Wave movement from the Invaders formation position, i.e. this is applied on top of the other.

The calculation takes the Amplitude (i.e. height) and multiply it by the Sine calculation of the elapsed time multiplied by the Frequency (how often)

If you run this, you'll be VERY disappointed! Nothing appears to really happen, but if you observe closely, they ALL move up and down in a wave pattern; as well as horizontally.

What we actually need to do is modify the addAsGrid method in the Invaders script. Please amend it to:

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) + Vector2(x*5, y*5)
            invader.time = x

The very last line is all that is needed!
We will offset the time of each Invader, by the column they are in. This slight adjustment means the Sine wave is slightly different in each column

Let's run it!


Invaders with Wave.gif

Again, the recording doesn't represent the smoothness found in the game.

... Even better! I'm now very happy

A few fixes

If you let the Invaders win (as mentioned above), you'll observe a poor bug!

Upon reaching the bottom, they continue to scroll off, downwards and to the left. Let's fix this:

In the Invaders script, you will need to edit the checkForWin function:

func checkForWin():
    for invader in get_children():
        if hitBottomBorder(invader):
            direction = Vector2(0, 0)
            velocity = Vector2(0, 0)
            break

The change is simple. When the bottom is detected, the direction and velocity values should be set to zero.

If you run it again, you will notice the formation stops; however, the Invaders continue in their wave pattern! We'll address this in a future tutorial.

Finally

That concludes this issue. My next article will address one of these topics:

  1. Adding graphics to the backdrop and Invaders
  2. 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 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 3)" 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.

Can't wait for the tutorial where you will add the ship and make it so you can fire its weapon!

You can contact us on Discord.
[utopian-moderator]

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!

Suggestions

  • Contribute more often to get higher and higher rewards. I wish to see you often!
  • Work on your followers to increase the votes/rewards. I follow what humans do and my vote is mainly based on that. Good luck!

Get Noticed!

  • Did you know project owners can manually vote with their own voting power or by voting power delegated to their projects? Ask the project owner to review your contributions!

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

@OriginalWorks let try this; based on what I read

Coin Marketplace

STEEM 0.36
TRX 0.12
JST 0.039
BTC 69735.97
ETH 3533.64
USDT 1.00
SBD 4.72