Build a 3D HTML5 game like “Stairs” using Godot – Step 4: controlling the ball with the mouse and adding more spikes

Read all posts about "" game

Welcome to the 4th step in the making of a HTML5 game like Stairs using Godot.

Let’s see a small recap: in first step, we saw how to build an endless staircase, in second step we added spikes and assigned materials to the meshes, then in third step we added a bouncing ball only using trigonometry. Now it’s time to control the ball and add more spikes.

There is quite a lot to do in this step, so let’s jump straight to the point: we need more spikes. We could generate them at runtime just like we did with the steps as seen in step 1, but this time let’s build them directly from the editor.

First, let’s make the step a bit bigger, by selecting it then from Inspector panel changing its size to x = 18.

You may not need to change the size of your step if you followed this tutorial with your own sizes, anyway that’s what I am doing.

So let’s select the step from Scene panel:

Then from Inspector panel set Size x = 18.

Now select the spike and from Inspector panel set Transform -> Translation -> x = -7.5.

Now your step should look like this:

The spike should be on the leftmost side of the step. Now let’s duplicate it.

From Scene panel right click on Spike and select Duplicate.

Your spike has been duplicated, but you won’t see it because it’s in the same place of the original spike, but you should see Spike2 in Scene panel.

Just like we did with the first spike, let’s select Spike2 in Scene panel and from Inspector panel set Transform -> Translation -> x = 5.

Now the step should look this way:

The second spike is next to the first one. Now we should repeat this process five more times, creating five more spikes whose Translation x values are -2.5, 0, 2.5, 5 and 7.5.

At the end of this process, the step should look like this one:

And your Scene panel should have the new spikes in it:

At this time we want to stop randomly moving the first spike as seen in step 2 and just show some random spikes, which will never move.

We have to change Step.gd script this way. A lot of line have been changed, so it’s a good idea to completely rewrite it rather than just editing it.

extends MeshInstance

# stair speed
var speed = 3

# variable to store y/z ratio
var yzRatio

# variable to store the amount of steps
var steps

# initialize the random number generator
var rnd = RandomNumberGenerator.new()

# maximum amount of spikes on each step
var maxSpikes = 4

# Called when the node enters the scene tree for the first time.
func _ready() :
	
	# set a time-based seed for random number generator
	rnd.randomize()
	
	# determine y/z ratio
	yzRatio = mesh.size.y / mesh.size.z
	
	# get steps value from Spatial node
	steps = get_node('/root/Spatial').get('steps')
	
	# custom function to set the spikes
	setSpikes()
	
# This function position enables some random spikes
func setSpikes() :
	
	# loop from 0 to 6
	for i in 7 :
		
		# set i-th child of the step (the i-th spike) to invisible
		get_child(i).visible = false
	
	# loop from 0 to maxSpikes - 1
	for i in maxSpikes :
		
		# toss a number from 0 to 6		
		var randomSpike = rnd.randi_range(0, 6)

		# set the randomSpike-th child of the step to visible
		get_child(randomSpike).visible = true

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta) :
	
	# move the step along y axis according to speed and elapsed time
	translation.y -= delta * speed
	
	# move the step along z axis according to speed, elapsed time and y/z ratio
	# y/z ratio is necessary because y and z speed cannot be the same as steps
	# do not have the same size so the stair does not have a 45 degrees angle
	translation.z += delta * speed / yzRatio
	
	# is z position greater than mesh z size, making the step to be outside camera eye?
	if (translation.z > mesh.size.z) :
		
		# move the step along y axis as if it were the highest step
		translation.y += steps * mesh.size.y
		
		# move the step along z axis as if it were the furthest step
		translation.z += steps * mesh.size.z * - 1
		
		# set random spikes
		setSpikes()

Since now we are using the random number generator here, there’s no need to keep using it in Spatial.gd script, which now becomes:

extends Spatial

# amount of steps we want in the stair
var steps = 15

# Called when the node enters the scene tree for the first time.
func _ready() :
	
	# get Step node
	var stepNode = get_node('Step')
	
	# 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()
		
		# 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)

I just removed the two lines var rnd = RandomNumberGenerator.new() and rnd.randomize() which we don’t need anymore.

And this is what we have now:

Now that we have an endless staircase with multiple spikes, we want to move the ball with the mouse.

The idea is to detect mouse position relative to game viewport and translate it to the movement on a step. Basically this means that when the mouse is on the very left side of the canvas, the ball will be on the very left side of the step, when the mouse is on the center of the canvas the ball will be on the center of the step, when the mouse is on the very right side of the canvas, the ball will be on the very right side of the step, and so on.

We need to add just a couple of lines to Ball.gd

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

# here we store ball movement range
var ballRange

# Called when the node enters the scene tree for the first time.
func _ready() :
	
	# get Step reference
	var step = get_node('/root/Spatial/Step')
	
	# ball movement range is equal to mesh horizontal size minus ball diameter
	ballRange = step.mesh.size.x - mesh.radius * 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 every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta) :
	
	# get the viewport
	var viewport = get_viewport()
	
	# get mouse X position relative to viewport width
	# 0 = left, 1 = right
	var mouseX = viewport.get_mouse_position().x / viewport.get_visible_rect().size.x
	
	# set ball x position according to mouseX value
	translation.x = -ballRange / 2 + ballRange * mouseX
	
	# 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

This is enough at the moment to have the ball moving following mouse position, try by yourself:

Move the mouse to control the ball. It’s not the best way to control the ball as we would need some kind of virtual trackpad, but it will be added later, once we’ll be able to detect collisions. Meanwhile, download the source code of the entire project.

Get the most popular Phaser 3 book

Through 202 pages, 32 source code examples and an Android Studio project you will learn how to build cross platform HTML5 games and create a complete game along the way.

Get the book

215 GAME PROTOTYPES EXPLAINED WITH SOURCE CODE
// 1+2=3
// 100 rounds
// 10000000
// 2 Cars
// 2048
// A Blocky Christmas
// A Jumping Block
// A Life of Logic
// Angry Birds
// Angry Birds Space
// Artillery
// Astro-PANIC!
// Avoider
// Back to Square One
// Ball Game
// Ball vs Ball
// Ball: Revamped
// Balloon Invasion
// BallPusher
// Ballz
// Bar Balance
// Bejeweled
// Biggification
// Block it
// Blockage
// Bloons
// Boids
// Bombuzal
// Boom Dots
// Bouncing Ball
// Bouncing Ball 2
// Bouncy Light
// BoxHead
// Breakout
// Bricks
// Bubble Chaos
// Bubbles 2
// Card Game
// Castle Ramble
// Chronotron
// Circle Chain
// Circle Path
// Circle Race
// Circular endless runner
// Cirplosion
// CLOCKS - The Game
// Color Hit
// Color Jump
// ColorFill
// Columns
// Concentration
// Crossy Road
// Crush the Castle
// Cube Jump
// CubesOut
// Dash N Blast
// Dashy Panda
// Deflection
// Diamond Digger Saga
// Don't touch the spikes
// Dots
// Down The Mountain
// Drag and Match
// Draw Game
// Drop Wizard
// DROP'd
// Dudeski
// Dungeon Raid
// Educational Game
// Elasticity
// Endless Runner
// Erase Box
// Eskiv
// Farm Heroes Saga
// Filler
// Flappy Bird
// Fling
// Flipping Legend
// Floaty Light
// Fuse Ballz
// GearTaker
// Gem Sweeper
// Globe
// Goat Rider
// Gold Miner
// Grindstone
// GuessNext
// Helicopter
// Hero Emblems
// Hero Slide
// Hexagonal Tiles
// HookPod
// Hop Hop Hop Underwater
// Horizontal Endless Runner
// Hundreds
// Hungry Hero
// Hurry it's Christmas
// InkTd
// Iromeku
// Jet Set Willy
// Jigsaw Game
// Knife Hit
// Knightfall
// Legends of Runeterra
// Lep's World
// Line Rider
// Lumines
// Magick
// MagOrMin
// Mass Attack
// Math Game
// Maze
// Meeblings
// Memdot
// Metro Siberia Underground
// Mike Dangers
// Mikey Hooks
// Nano War
// Nodes
// o:anquan
// One Button Game
// One Tap RPG
// Ononmin
// Pacco
// Perfect Square!
// Perfectionism
// Phyballs
// Pixel Purge
// PixelField
// Planet Revenge
// Plants Vs Zombies
// Platform
// Platform game
// Plus+Plus
// Pocket Snap
// Poker
// Pool
// Pop the Lock
// Pop to Save
// Poux
// Pudi
// Pumpkin Story
// Puppet Bird
// Pyramids of Ra
// qomp
// Quick Switch
// Racing
// Radical
// Rebuild Chile
// Renju
// Rise Above
// Risky Road
// Roguelike
// Roly Poly
// Run Around
// Rush Hour
// SameGame
// SamePhysics
// Save the Totem
// Security
// Serious Scramblers
// Shrink it
// Sling
// Slingy
// Snowflakes
// Sokoban
// Space Checkers
// Space is Key
// Spellfall
// Spinny Gun
// Splitter
// Spring Ninja
// Sproing
// Stabilize!
// Stack
// Stairs
// Stick Hero
// String Avoider
// Stringy
// Sudoku
// Super Mario Bros
// Surfingers
// Survival Horror
// Talesworth Adventure
// Tetris
// The Impossible Line
// The Moops - Combos of Joy
// The Next Arrow
// Threes
// Tic Tac Toe
// Timberman
// Tiny Wings
// Tipsy Tower
// Toony
// Totem Destroyer
// Tower Defense
// Trick Shot
// Tunnelball
// Turn
// Turnellio
// TwinSpin
// vvvvvv
// Warp Shift
// Way of an Idea
// Whack a Creep
// Wheel of Fortune
// Where's my Water
// Wish Upon a Star
// Word Game
// Wordle
// Worms
// Yanga
// Yeah Bunny
// Zhed
// zNumbers