Talking about Stairs game, 3D, Game development, Godot Engine and HTML5.
Here we go with the 5th part of the tutorial series about the creation of a Stairs game using Godot. The prototype is getting more and more playable step after step, so let’s see a small recap:
In first step, we built an endless staircase.
In second step we added spikes and assigned materials to the meshes.
In third step we added a bouncing ball only using trigonometry.
In fourth step we added mouse control to the ball and added some more spikes.
Now it’s time to improve ball control introducing some kind of virtual trackpad, and check if the ball falls off the stairs or hits a spike.
We will only act on the code, so there is no need to modify the scene.
First, let’s adds a virtual trackpad. We have to edit Ball.gd file, first of all removing ballRange variable since we don’t use it anymore.
Then, the idea is: the ball does not move left or right until player clicks on the canvas. Then, the x coordinate where the player clicked becomes the origin, and as long as the player drags, we move the ball to the left or to the right until the player releases the button.
Look at the script, with new lines highlighted:
extends MeshInstance
# ball starting step. 0: first step, 1: second step, and so on
var ballStartingStep = 1
# jump height, should be higher than step height
var jumpHeight = 5
# here we'll store the jump time, that is the time required for a step to take the place of another
var jumpTime
# here we'll store the amount of time the ball is in play
var ballTime
# we need to store ball starting y position to determine its y position when it's jumping
var ballY
# virtual pax X position
var virtualPadX
# variable to save ball x position when we start acting on the trackpad
var ballPositionX
# ration between movement on the screen (pixels) and movement in game (units)
var virtualPadRatio = 0.1
# Called when the node enters the scene tree for the first time.
func _ready() :
# when the virtual pad is not active, we set it to -1
virtualPadX = -1
# get Step reference
var step = get_node('/root/Spatial/Step')
# jump time, in seconds, is step height divided by step speed
jumpTime = step.mesh.size.y / step.get('speed') * 1000
# ballTime starts at zero
ballTime = 0
# determine ball y position according to step size and starting step
ballY = step.mesh.size.y * ballStartingStep + step.mesh.size.y / 2 + mesh.radius
# move the ball to ballY position
translation.y = ballY
# determine ball z position according to step size and starting step
translation.z = -step.mesh.size.z * ballStartingStep
# called at every inout evenet
func _input(event) :
# is the event a mouse button?
if event is InputEventMouseButton :
# did we press the mouse button?
if (event.pressed) :
# set virtual pad to x mouse position
virtualPadX = event.position.x
# save current ball x position
ballPositionX = translation.x
# in this case, we released the mouse button
else :
# set virtual pad to -1 again
virtualPadX = -1
# called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta) :
# is virtual pad different than -1
# that means: are we using the virtual pad?
if virtualPadX != -1 :
# get the viewport
var viewport = get_viewport()
# get mouse x position
var mouseX = viewport.get_mouse_position().x
# set ball x position according to virtual pad, current mouse position and virtual pad ratio
translation.x = ballPositionX + (mouseX - virtualPadX) * virtualPadRatio
# increase ballTime assing delta to it
ballTime += delta * 1000
# if ballTime is greater or equal than jump time...
if (ballTime >= jumpTime) :
# subtract jumpTime to ballTime
ballTime -= jumpTime
# ratio ranges from 0 (ball at the beginning of jump time) to 1 (ball at the end of jump time)
var ratio = ballTime / jumpTime
# move the ball to y position equal to sin of ratio * PI multiplied by jump height
translation.y = ballY + sin(ratio * PI) * jumpHeight
And now look at the result:
Click and drag the mouse to move the ball. You’ll probably notice the ball now can bounce outside the stair, and this is what we are going to fix now.
We must check, once the ball completes its sine movement, if the x coordinate is inside the step. If not, player won’t be able to control the ball anymore, and we let it fall down.
We still need to edit a bit Ball.gd, look at the new lines:
extends MeshInstance
# ball starting step. 0: first step, 1: second step, and so on
var ballStartingStep = 1
# jump height, should be higher than step height
var jumpHeight = 5
# here we'll store the jump time, that is the time required for a step to take the place of another
var jumpTime
# here we'll store the amount of time the ball is in play
var ballTime
# we need to store ball starting y position to determine its y position when it's jumping
var ballY
# virtual pax X position
var virtualPadX
# variable to save ball x position when we start acting on the trackpad
var ballPositionX
# ration between movement on the screen (pixels) and movement in game (units)
var virtualPadRatio = 0.1
# ball movement limit, beyond this limit the ball falls down
var ballLimit
# is the ball falling off down a step?
var ballFallingDown
# Called when the node enters the scene tree for the first time.
func _ready() :
# ball is not falling down at the moment
ballFallingDown = false
# when the virtual pad is not active, we set it to -1
virtualPadX = -1
# get Step reference
var step = get_node('/root/Spatial/Step')
# set ball limit to half mesh size
ballLimit = step.mesh.size.x / 2
# jump time, in seconds, is step height divided by step speed
jumpTime = step.mesh.size.y / step.get('speed') * 1000
# ballTime starts at zero
ballTime = 0
# determine ball y position according to step size and starting step
ballY = step.mesh.size.y * ballStartingStep + step.mesh.size.y / 2 + mesh.radius
# move the ball to ballY position
translation.y = ballY
# determine ball z position according to step size and starting step
translation.z = -step.mesh.size.z * ballStartingStep
# called at every inout evenet
func _input(event) :
# is the event a mouse button?
if event is InputEventMouseButton :
# did we press the mouse button?
if (event.pressed) :
# set virtual pad to x mouse position
virtualPadX = event.position.x
# save current ball x position
ballPositionX = translation.x
# in this case, we released the mouse button
else :
# set virtual pad to -1 again
virtualPadX = -1
# called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta) :
# is virtual pad different than -1
# that means: are we using the virtual pad?
# also, is the ball not falling down?
if virtualPadX != -1 and !ballFallingDown :
# get the viewport
var viewport = get_viewport()
# get mouse x position
var mouseX = viewport.get_mouse_position().x
# set ball x position according to virtual pad, current mouse position and virtual pad ratio
translation.x = ballPositionX + (mouseX - virtualPadX) * virtualPadRatio
# increase ballTime adding delta to it
ballTime += delta * 1000
# if ballTime is greater or equal than jump time...
if (ballTime >= jumpTime) :
# subtract jumpTime to ballTime
ballTime -= jumpTime
# if ball x position goes beyond limit...
if abs(translation.x) > ballLimit :
# the ball falls down
ballFallingDown = true
# ratio ranges from 0 (ball at the beginning of jump time) to 1 (ball at the end of jump time)
var ratio = ballTime / jumpTime
# if the ball is in play (not falling down) ...
if !ballFallingDown :
# move the ball to y position equal to sin of ratio * PI multiplied by jump height
translation.y = ballY + sin(ratio * PI) * jumpHeight
# if the ball is falling down...
else :
# move the ball down at high speed
translation.y -= delta * 30
# if the ball falls down too much...
if translation.y < -20 :
# restart the game
get_tree().reload_current_scene()
Now let’s see the result:
Now, if you land outside the step, the ball will fall down, then the game restarts.
Checking for collisions will be a bit harder, because we won’t be using any physics engine, but we can handle it.
First, we need to store all steps into an array, to give them an easy access from inside the code.
Change Spatial.gd adding these highlighted lines:
extends Spatial
# amount of steps we want in the stair
var steps = 15
# array to store all steps
var stepsArray = []
# Called when the node enters the scene tree for the first time.
func _ready() :
# get Step node
var stepNode = get_node('Step')
# insert the step in steps array
stepsArray.append(stepNode)
# this is the height of the step, determined according to mesh size
var deltaY = stepNode.mesh.size.y
# this is the depth of the step, determined according to mesh size
var deltaZ = -stepNode.mesh.size.z
# a for loop going from 1 to steps - 1
for i in range (1, steps) :
# duplicate newStep node
var newStep = stepNode.duplicate()
#insert the step in steps array
stepsArray.append(newStep)
# move the duplicated step along y axis
newStep.translation.y = deltaY * i
# move the duplicated step along z azis
newStep.translation.z = deltaZ * i
# add the step to the scene
add_child(newStep)
Now all steps are inside stepsArray array.
Now, this is the plan:
1 – Identify the step the ball is about to land on.
2 – For each spike on the step, check if it’s visible.
3 – If a spike is visible, determine the 3D coordinate of spike’s tip. It’s located half spike height above spike’s origin.
4 – Calculate the distance between spike’s tip and ball center.
5 – If the distance is less then ball radius, then we can say the spike is inside the ball.
It’s not the most accurate collision detection, but it’s exactly what we need for a hyper casual game.
Time to edit Ball.gd one more time, look at highlighted lines as usual:
extends MeshInstance
# ball starting step. 0: first step, 1: second step, and so on
var ballStartingStep = 1
# jump height, should be higher than step height
var jumpHeight = 5
# here we'll store the jump time, that is the time required for a step to take the place of another
var jumpTime
# here we'll store the amount of time the ball is in play
var ballTime
# we need to store ball starting y position to determine its y position when it's jumping
var ballY
# virtual pax X position
var virtualPadX
# variable to save ball x position when we start acting on the trackpad
var ballPositionX
# ration between movement on the screen (pixels) and movement in game (units)
var virtualPadRatio = 0.1
# ball movement limit, beyond this limit the ball falls down
var ballLimit
# is the ball falling off down a step?
var ballFallingDown
# step the ball is about to land on
var nextStep
# Called when the node enters the scene tree for the first time.
func _ready() :
# ball is not falling down at the moment
ballFallingDown = false
# when the virtual pad is not active, we set it to -1
virtualPadX = -1
# get Step reference
var step = get_node('/root/Spatial/Step')
# set ball limit to half mesh size
ballLimit = step.mesh.size.x / 2
# jump time, in seconds, is step height divided by step speed
jumpTime = step.mesh.size.y / step.get('speed') * 1000
# ballTime starts at zero
ballTime = 0
# determine ball y position according to step size and starting step
ballY = step.mesh.size.y * ballStartingStep + step.mesh.size.y / 2 + mesh.radius
# move the ball to ballY position
translation.y = ballY
# determine ball z position according to step size and starting step
translation.z = -step.mesh.size.z * ballStartingStep
# called at every inout evenet
func _input(event) :
# is the event a mouse button?
if event is InputEventMouseButton :
# did we press the mouse button?
if (event.pressed) :
# set virtual pad to x mouse position
virtualPadX = event.position.x
# save current ball x position
ballPositionX = translation.x
# in this case, we released the mouse button
else :
# set virtual pad to -1 again
virtualPadX = -1
# called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta) :
# if next step is not defined yet...
if nextStep == null :
# next step is... actually next step!
nextStep = get_node('/root/Spatial').get('stepsArray')[ballStartingStep + 1]
# is virtual pad different than -1
# that means: are we using the virtual pad?
# also, is the ball not falling down?
if virtualPadX != -1 and !ballFallingDown :
# get the viewport
var viewport = get_viewport()
# get mouse x position
var mouseX = viewport.get_mouse_position().x
# set ball x position according to virtual pad, current mouse position and virtual pad ratio
translation.x = ballPositionX + (mouseX - virtualPadX) * virtualPadRatio
# increase ballTime adding delta to it
ballTime += delta * 1000
# if ballTime is greater or equal than jump time...
if (ballTime >= jumpTime) :
# get main node
var mainNode = get_node('/root/Spatial')
# increase ball starting step
ballStartingStep = ballStartingStep + 1
# get next step, where
nextStep = mainNode.get('stepsArray')[(ballStartingStep + 1) % mainNode.get('steps')]
# subtract jumpTime to ballTime
ballTime -= jumpTime
# if ball x position goes beyond limit...
if abs(translation.x) > ballLimit :
# the ball falls down
ballFallingDown = true
# ratio ranges from 0 (ball at the beginning of jump time) to 1 (ball at the end of jump time)
var ratio = ballTime / jumpTime
# if the ball is in play (not falling down) ...
if !ballFallingDown :
# move the ball to y position equal to sin of ratio * PI multiplied by jump height
translation.y = ballY + sin(ratio * PI) * jumpHeight
# loop through all spikes in current step
for spike in nextStep.get_children() :
# is the spike visible?
if spike.visible :
# get spike global position
var spikeGlobalPosition = spike.global_transform.origin
# get ball global position
var ballGlobalPosition = global_transform.origin
# get spike tip position, which is located 1/2 height above spike global position
var spikeTip = Vector3(spikeGlobalPosition.x, spikeGlobalPosition.y + spike.mesh.height / 2, spikeGlobalPosition.z)
# get the distance between ball center and spike tip
var distance = spikeTip.distance_to(ballGlobalPosition)
# if the distance is lower than ball radius...
if distance < mesh.radius :
# make the ball fall down
ballFallingDown = true
# if the ball is falling down...
else :
# move the ball down at high speed
translation.y -= delta * 30
# if the ball falls down too much...
if translation.y < -20 :
# restart the game
get_tree().reload_current_scene()
And this is the result:
Now we are able to check when the ball hits a spike. This time I just make the ball fall down like it landed outside the step, but at least it works. Next time we’ll see how to properly manage deaths, score and level progression, meanwhile download the source code of the entire project.
Never miss an update! Subscribe, and I will bother you by email only when a new game or full source code comes out.