PlayState = Class{__includes = BaseState}
 
 
delorean = Delorean()
 
local WIN_SPEED = 88
-- PUNCHY LAUNCH, LONG GRIND TO 88
-- BUILD RATE SLIDES FROM LAUNCH_RATE AT 0MPH TO CRUISE_RATE AT 88MPH
local LAUNCH_RATE = 12
local CRUISE_RATE = 1.2
local DRAG = 4
local HIT_PENALTY = 15
local SPAWN_GATE = 5
-- WORLD MOTION FOLLOWS THE SPEEDOMETER ON A CURVE
-- ZERO STILL MEANS PARKED
local WORLD_MAX = 560
local WORLD_CURVE = 0.55
 
function PlayState:init()
end
 
function PlayState:enter()
	-- FRESH RUN EVERY TIME PLAY STARTS
	gameSpeed = 0
	obstacles = {}
	spawnTimer = 1.5
	BACKGROUND_SCROLL_SPEED = 0
	MIDDLE_SCROLL_SPEED = 0
	FRONT_SCROLL_SPEED = 0
	delorean = Delorean()
	love.keyboard.keysPressed = {}
end
 
local function collides(playerX, playerY, playerWidth, playerHeight, obstacleX, obstacleY, obstacleWidth, obstacleHeight)
	return playerX < obstacleX + obstacleWidth
		and obstacleX < playerX + playerWidth
		and playerY < obstacleY + obstacleHeight
		and obstacleY < playerY + playerHeight
end
 
local function accelerationRate(currentSpeed)
	return CRUISE_RATE + (LAUNCH_RATE - CRUISE_RATE) * (1 - currentSpeed / WIN_SPEED)
end
 
local function calculateWorldSpeed(currentSpeed)
	local speedFraction = math.min(1, currentSpeed / WIN_SPEED)
	return WORLD_MAX * (speedFraction ^ WORLD_CURVE)
end
 
local function spawnObstacle()
	-- BALLS SPAWN NEAR THE CAR SO EVERY RUN STAYS DANGEROUS
	local spawnHeight = delorean.y + (math.random() * 120 - 60)
	spawnHeight = math.max(40, math.min(VIRTUAL_HEIGHT - 40, spawnHeight))
	local radius = 8 + math.random() * 6
	table.insert(obstacles, {
		x = VIRTUAL_WIDTH + 20,
		y = spawnHeight,
		r = radius
	})
end
 
function PlayState:update(dt)
	sounds['playMusic']:setLooping(true)
	sounds['playMusic']:play()
 
	if love.keyboard.wasPressed('t') then
		gStateMachine:change('title')
		return
	end
 
	-- ONE SPEED DRIVES EVERYTHING: HOLD RIGHT TO BUILD IT, LET GO TO COAST DOWN
	if love.keyboard.isDown('right', 'd') then
		gameSpeed = math.min(100, gameSpeed + accelerationRate(gameSpeed) * dt)
	else
		gameSpeed = math.max(0, gameSpeed - DRAG * dt)
	end
 
	-- PARALLAX SCROLL FOLLOWS THE SPEEDOMETER
	local worldSpeed = calculateWorldSpeed(gameSpeed)
	BACKGROUND_SCROLL_SPEED = worldSpeed * 0.03
	MIDDLE_SCROLL_SPEED = worldSpeed * 0.13
	FRONT_SCROLL_SPEED = worldSpeed
 
	-- MOVE EACH BACKGROUND LAYER AT ITS OWN SPEED
	backgroundScroll = (backgroundScroll + BACKGROUND_SCROLL_SPEED * dt)
		% LOOPING_POINT
 
	middleScroll = (middleScroll + MIDDLE_SCROLL_SPEED * dt)
		% LOOPING_POINT
 
	frontScroll = (frontScroll + FRONT_SCROLL_SPEED * dt)
		% LOOPING_POINT
 
	-- SPARSE OBSTACLES ONLY WHILE DRIVING
	-- A PARKED CAR SPAWNS NOTHING
	if gameSpeed > SPAWN_GATE then
		spawnTimer = spawnTimer - dt
		if spawnTimer <= 0 then
			spawnObstacle()
			spawnTimer = math.max(1.5, 3.0 - gameSpeed / 80)
		end
	end
 
	-- OBSTACLES APPROACH AT WORLD SPEED
	-- PARKED CAR MEANS A FROZEN ROAD
	for i = #obstacles, 1, -1 do
		local obstacle = obstacles[i]
		obstacle.x = obstacle.x - worldSpeed * dt
		if obstacle.x < -30 then
			table.remove(obstacles, i)
		elseif collides(delorean.x, delorean.y, delorean.width, delorean.height,
			obstacle.x - obstacle.r, obstacle.y - obstacle.r, obstacle.r * 2, obstacle.r * 2) then
			gameSpeed = math.max(0, gameSpeed - HIT_PENALTY)
			sounds['crash']:play()
			table.remove(obstacles, i)
		end
	end
 
	-- 88MPH: TIME TRAVEL ACHIEVED
	if gameSpeed >= WIN_SPEED then
		sounds['playMusic']:stop()
		gStateMachine:change('win')
	end
end
 
function PlayState:render()
	love.graphics.draw(background, -backgroundScroll, 0)
	love.graphics.draw(middle, -middleScroll, 0)
	love.graphics.draw(front, -frontScroll, 0)
	delorean:render()
 
	-- OBSTACLES
	love.graphics.setColor(1, 0.25, 0.25, 1)
	for key, obstacle in ipairs(obstacles) do
		love.graphics.circle('fill', obstacle.x, obstacle.y, obstacle.r)
	end
	love.graphics.setColor(1, 1, 1, 1)
 
	-- SPEEDOMETER
	love.graphics.setFont(normalFont)
	love.graphics.printf(math.floor(gameSpeed) .. ' MPH', 0, 10, VIRTUAL_WIDTH, 'center')
	love.graphics.setFont(smallFont)
	love.graphics.printf('hold right to accelerate - spacebar to fly', 0, 45, VIRTUAL_WIDTH, 'center')
end

Part of the BTTF project — pattern explained in Game States, template in Starter Template.