Learning to code with Pikle

WELCOME

Not made a game in Pikle yet? Try this simple space-survival game – with Evil Space FruitTM!

The gameplay

The player will fly their ship around the space fruit asteroids – collisions will deplete the player’s energy level. The fruity asteroids will keep spawning – last as long as you can!

PREREQUISITES

TUTORIAL

Step 1. Gather some art assets from the Asset Browser

Pikle contains loads of built-in art and sound assets. You can browse these assets using the Asset Browser in Pikle Player (accessed from the Hub menu). To use the assets you find, simply note the bank number (at the bottom of the Asset Browser) and the asset number (under each individual asset in the preview window).

Asset locations for sprite images and a game over sound effect

-- Store our asset locations for later
BackgroundAsset = { 25, 7 }
PlayerShipAsset = { 36, 2 }
SpaceFruitAssets = { 28, 9, 16 }
GameOverSoundAsset = { 0, 16 }

Step 2. Define some properties to track the player and the asteroids

We define constants and variables the same way, we just started the names of constants with an uppercase letter, and variable names with a lowercase letter. This isn’t required – feel free to choose your own preferred naming convention.

Player sprite indices and physics properties

PlayerSprite = 1
PlayerEnergySprite = 2
PlayerRotationSpeed = 180
PlayerThrust = 200
PlayerMaxSpeed = 100
DegToRadians = math.pi / 180

Player variables for position, velocity, rotation, energy, etc.

playerX = 0
playerY = 0
playerVelocityX = 0
playerVelocityY = 0
playerRotation = 0
playerEnergy = 1.0
playerAlive = true
playerTime = 0
playerFlash = true

Some constants that define properties for the asteroids (space fruit!)

-- Asteroid info - max number, spawn rate etc.
AsteroidSpawnRate = 0.5
AsteroidMax = 30
AsteroidSpriteBase = 3
AsteroidSpawnRadius = 200
AsteroidCollisionRadiusSq = 24*24
AsteroidDamageRate = 0.15

Some variables to track the asteroids

asteroidSpawnCounter = 0
asteroidCount = 0
asteroids = {}

Step 3. Create a space background

When we have chosen a sprite asset, we can setup a sprite to display it. The asset chosen for the background happens to be the same size as the Pikle screen (256×256). To display it, all we have to do is set the sprite’s shape (image) and enable it.

Setup a sprite to show the space background

function setupBackground()
	Engine.SpriteEnable(0, true)
	Engine.SpriteShape(0, BackgroundAsset[1], BackgroundAsset[2])
end

Did you know about Pikle sprites?

Pikle can display up to 256 sprites. We access them through their index numbers 0 to 255. We’re going to use sprite 0 for the background. Sprites have the following properties and default values:

  • Enable (true or false) defaults to false.
  • Position defaults to (0,0,0). The Z value is used to sort sprites within a single sprite layer.
  • Color defaults to white or RGBA (1,1,1,1)
  • Scale defaults to normal size, or (1,1,1)
  • Shape is an asset location and defaults to bank 0, asset 0. This can be viewed in the Asset Browser. it’s a square 🙂

Step 4. Setup player ship and implement flight controls

The player controls a space ship. The player can steer with the joystick or d-pad and apply forward thrust by pressing the A button.

The player sprite

First we need to setup the sprite for the player. The player space ship starts in the centre of the screen. We also create an energy bar – this is a white bar at the bottom of the screen. It will start the full width of the screen and then shrink to zero as the player gets damaged by asteroids.

Setup the player ship and energy bar

function setupPlayerShip()
	Engine.SpriteEnable(PlayerSprite, true)
	Engine.SpriteShape(PlayerSprite, PlayerShipAsset[1], PlayerShipAsset[2])
	Engine.SpriteEnable(PlayerEnergySprite, true)
	Engine.SpriteShape(PlayerEnergySprite, 0, 0)
	Engine.SpritePosition(PlayerEnergySprite, 0, -128, 0)
end

To update the player and simple flight physics, we need a function that will be called every game frame – roughly 30fps depending on the device. Refer to the function called updatePlayerShip() – we’ll just go through the highlights here.

Player input

There are 3 main functions for getting the state of the on-screen controls. GetJoyState, GetDPadState and GetButtonState.

  • A, B, START – these are the basic digital buttons. They have the following states:
    • isPressed – true if the button has only just been pressed
    • isDown – true while the button is held
    • isUp – true if the button is NOT being pressed
  • JOY0 is the analog stick – it has x and y properties ranging from -1 to 1
  • DPAD0 is the d-pad – it has button states called up, down, left and right – these buttons also have isPressed, isDown and isUp

Reading input from the on-screen Pikle controls

local jx = Engine.GetJoyState("JOY0").x
local dpad = Engine.GetDPadState("DPAD0")
local thrustButton = Engine.GetButtonState("A")

Simple flight physics

For the “fight model” we’re simply tracking the player’s velocity and position. Each frame we adjust the velocity using the thrust button input and apply the velocity to the position. We dampen the velocity so that the player doesn’t accelerate to infinite speed. The player position is wrapped so if they go off-screen, they are warped instantly to the other opposite screen edge.

Apply thrust, velocity and position

-- apply thrust to velocity of ship
playerVelocityX = playerVelocityX + thrustX * dt
playerVelocityY = playerVelocityY + thrustY * dt

-- apply velocity to move the player position
playerX = playerX + playerVelocityX * dt
playerY = playerY + playerVelocityY * dt
	
-- dampen the velocity so we slow down when not thrusting
playerVelocityX = damp(playerVelocityX, 0.9, dt)
playerVelocityY = damp(playerVelocityY, 0.9, dt)

Update the player sprite

Once we have calculated where the player should be and what orientation they are, we apply the changes to the player sprite.

Update the player sprite position and rotation

Engine.SpriteRotation(PlayerSprite, 0, 0, playerRotation)
Engine.SpritePosition(PlayerSprite, playerX, playerY, 0)

Step 5. Spawn and animate the asteroids

The asteroids are prebuilt – we setup the sprites with a random fruit image, position them around the edge of the play area in a circle and give them an initial velocity.

Creating the asteroid objects in a circle around the player

local newAsteroid = {
    active = false,
    x = cos * AsteroidSpawnRadius,
    y = sin * AsteroidSpawnRadius,
    velocityX = -cos * speed,
    velocityY = -sin * speed,
    spinRate = math.random(-90, 90),
    rotation = 0,
    sprite = spr
}

Spawning them becomes a simple process of activating the relevant sprite.

Spawn an asteroid by activating the next one in the list

function spawnAsteroid()
	asteroidCount = asteroidCount + 1
	asteroids[asteroidCount].active = true
	Engine.SpriteEnable(asteroids[asteroidCount].sprite, true)
end

Step 6. Check for collisions and deplete player energy

Once an asteroid sprite is active, we check for collision with the player using a simple distance check (for simplicity we assume all the asteroids are the same size). If an asteroid collides with the player ship, the player energy level is reduced.

Collision check using the distance between the player ship and the asteroid position (x,y)

function checkPlayerCollision(x, y, dt)
	local dx = playerX - x
	local dy = playerY - y
	local rSq = dx*dx + dy*dy
	
	if rSq < AsteroidCollisionRadiusSq then
		local damage = AsteroidDamageRate * dt
		playerEnergy = math.max(0, playerEnergy - damage)
		if playerEnergy <= 0 then
			onGameOver()
		end
	end
end

Every frame we update an energy bar sprite – we scale it across the screen – it gets smaller as the player energy level drops.

Display energy level by stretching a white sprite across the screen

function updatePlayerEnergyBar()
	Engine.SpriteScale(PlayerEnergySprite, playerEnergy*9, 0.5, 1)
end

Asteroid movement

The asteroids are continuously moving in one direction. They don’t change direction during their lifetime. And the asteroids are never removed. We simply apply the asteroid’s velocity to its position – same as with the player, except we never apply any acceleration. The velocity is multiplied by dt – the time since the last frame – this keeps the movement independent of the frame rate.

We also apply a spin to each asteroid and wrap their positions when they go beyond the screen edges.

Simple velocity calculation with wrapping position at screen edges

x = x + vx * dt
y = y + vy * dt
x = wrapCoord(x)
y = wrapCoord(y)
rot = rot + spinRate * dt

As with the player the asteroids are updated every frame.

Step 7. Game-over sound

The game is over when the player energy has been depleted to zero. When this happens we trigger an audio effect. Note how we pass the bank and asset numbers (second and third parameters) – similar to how we use sprite assets.

Trigger one-off sound on game over

Engine.AudioPlayOneShot(0, GameOverSoundAsset[1], GameOverSoundAsset[2])

Step 8. Measure and display player’s survival time

We use the simple Pikle Text layer to display the player’s current time – the idea is to last for as long as possible. When the player has lost all energy, the timer is stopped and the game is over. To play again, re-upload the script.

Drawing order

By default, the text layer is drawn behind the sprite layer. To get the text on top of the sprites, we need to set an order number higher than the sprites – for this we use Engine.TextOrder(..) function.

Thank you for reading

More tutorials are coming soon!

SPACE FRUIT GAME – FULL CODE LISTING

-- Store our asset locations for later
BackgroundAsset = { 25, 7 }
PlayerShipAsset = { 36, 2 }
SpaceFruitAssets = { 28, 9, 16 }
GameOverSoundAsset = { 0, 16 }

-- Some info about the player that won't change (better than
-- having raw numbers spread about the code)
PlayerSprite = 1
PlayerEnergySprite = 2
PlayerRotationSpeed = 180
PlayerThrust = 200
PlayerMaxSpeed = 100
DegToRadians = math.pi / 180

playerX = 0
playerY = 0
playerVelocityX = 0
playerVelocityY = 0
playerRotation = 0
playerEnergy = 1.0
playerAlive = true
playerTime = 0
playerFlash = true

-- Asteroid info - max number, spawn rate etc.
AsteroidSpawnRate = 0.5
AsteroidMax = 30
AsteroidSpriteBase = 3
AsteroidSpawnRadius = 200
AsteroidCollisionRadiusSq = 24*24
AsteroidDamageRate = 0.15

-- Variable asteroid data
asteroidSpawnCounter = 0
asteroidCount = 0
asteroids = {}


function setupBackground()
	Engine.SpriteEnable(0, true)
	Engine.SpriteShape(0, BackgroundAsset[1], BackgroundAsset[2])
end

function setupPlayerShip()
	Engine.SpriteEnable(PlayerSprite, true)
	Engine.SpriteShape(PlayerSprite, PlayerShipAsset[1], PlayerShipAsset[2])
	Engine.SpriteEnable(PlayerEnergySprite, true)
	Engine.SpriteShape(PlayerEnergySprite, 0, 0)
	Engine.SpritePosition(PlayerEnergySprite, 0, -128, 0)
end

-- Basic exponential decay to zero (No overshoot)
function damp(source, lambda, dt)
    return source * math.exp(-lambda * dt)
end

function wrapCoord(x)
	if x < -128 then 
	    x = 128 
	elseif x > 128 then 
	    x = -128 
	end
	return x
end

function updatePlayerShip(dt)
	local thrustX = 0
	local thrustY = 0

	if playerAlive then
		playerTime = playerTime + dt
		Engine.Text(1, 1, string.format("%.2f", playerTime))

		local jx = Engine.GetJoyState("JOY0").x
		local dpad = Engine.GetDPadState("DPAD0")
		local thrustButton = Engine.GetButtonState("A")

		-- allow dpad directions to control steering too
		if dpad.left.isDown then
			jx = -1
		elseif dpad.right.isDown then
			jx = 1
		end

		-- rotate the player ship based on the joystick X (horizontal 	position)
		playerRotation = (playerRotation - jx * dt * PlayerRotationSpeed) % 360
	
		-- calculate thrust based on angle of ship
		if thrustButton.isDown then
			thrustX = -math.sin(playerRotation * DegToRadians) * 	PlayerThrust
			thrustY = math.cos(playerRotation * DegToRadians) * PlayerThrust
		end
	else
		Engine.SpriteEnable(PlayerSprite, playerFlash)
		playerFlash = not playerFlash
	end

	-- apply thrust to velocity of ship
	playerVelocityX = playerVelocityX + thrustX * dt
	playerVelocityY = playerVelocityY + thrustY * dt

	-- apply velocity to move the player position
	playerX = playerX + playerVelocityX * dt
	playerY = playerY + playerVelocityY * dt
	
	-- dampen the velocity so we slow down when not thrusting
	playerVelocityX = damp(playerVelocityX, 0.9, dt)
	playerVelocityY = damp(playerVelocityY, 0.9, dt)

	-- if the payer goes off the screen, wrap to the other side
	playerX = wrapCoord(playerX)
	playerY = wrapCoord(playerY)

	-- apply rotation and position to the sprite
	Engine.SpriteRotation(PlayerSprite, 0, 0, playerRotation)
	Engine.SpritePosition(PlayerSprite, playerX, playerY, 0)
end

function setupAsteroids()
	local spawnAngle = 0
	for i=1,AsteroidMax do
		-- setup all asteroid sprites with a random image
		local shape = math.random(SpaceFruitAssets[2], SpaceFruitAssets[3])
		local spr = AsteroidSpriteBase+i-1
		Engine.SpriteShape(spr, SpaceFruitAssets[1], shape)
		-- setup spawn position, velocity and rotation
		local cos = math.cos(spawnAngle)
		local sin = math.sin(spawnAngle)
		local speed = 50 + math.random(0, 100)
		local newAsteroid = {
		    active = false,
		    x = cos * AsteroidSpawnRadius,
		    y = sin * AsteroidSpawnRadius,
		    velocityX = -cos * speed,
		    velocityY = -sin * speed,
		    spinRate = math.random(-90, 90),
		    rotation = 0,
		    sprite = spr
		}
		asteroids[i] = newAsteroid
		spawnAngle = spawnAngle + 20*DegToRadians
	end
end

function spawnAsteroid()
	asteroidCount = asteroidCount + 1
	asteroids[asteroidCount].active = true
	Engine.SpriteEnable(asteroids[asteroidCount].sprite, true)
end

function onGameOver()
	playerAlive = false
	Engine.AudioPlayOneShot(0, GameOverSoundAsset[1], GameOverSoundAsset[2])
end

function checkPlayerCollision(x, y, dt)
	local dx = playerX - x
	local dy = playerY - y
	local rSq = dx*dx + dy*dy
	
	if rSq < AsteroidCollisionRadiusSq then
		local damage = AsteroidDamageRate * dt
		playerEnergy = math.max(0, playerEnergy - damage)
		if playerEnergy <= 0 then
			onGameOver()
		end
	end
end

function moveAsteroid(asteroid, dt)
	local x = asteroid.x
	local y = asteroid.y
	local vx = asteroid.velocityX
	local vy = asteroid.velocityY
	local spinRate = asteroid.spinRate
	local rot = asteroid.rotation
	local spr = asteroid.sprite

	x = x + vx * dt
	y = y + vy * dt
	x = wrapCoord(x)
	y = wrapCoord(y)
	rot = rot + spinRate * dt
	
	asteroid.x = x
	asteroid.y = y
	asteroid.rotation = rot
	
	Engine.SpritePosition(spr, x, y, 0)
	Engine.SpriteRotation(spr, 0, 0, rot)

	if playerAlive then
		checkPlayerCollision(x, y, dt)
	end
end


function updateAsteroids(dt)
	-- spawn one or more asteroids (accumulate counter over time to track
	-- how many we should spawn in this update)
	if asteroidCount < AsteroidMax then
		asteroidSpawnCounter = asteroidSpawnCounter + dt
		while asteroidSpawnCounter >= 1 do
			asteroidSpawnCounter = asteroidSpawnCounter - 1
			spawnAsteroid()
		end
	end

	for i=1,#asteroids do
		if asteroids[i].active then
			moveAsteroid(asteroids[i], dt)
		end
	end
end

function updatePlayerEnergyBar()
	Engine.SpriteScale(PlayerEnergySprite, playerEnergy*9, 0.5, 1)
end

function init()
	Engine.TextOrder(256)
	setupBackground()
	setupPlayerShip()
	setupAsteroids()
end

function update()
	local dt = Engine.TimeStep()
	updatePlayerShip(dt)
	updateAsteroids(dt)
	updatePlayerEnergyBar()
end