Your First Love2D Game
Chapter 3: Your First Love2D Game
Understanding the Game Loop
Every Love2D game is built around a simple game loop with three main phases:
- Update: Process input, update game logic, handle physics
- Draw: Render graphics to the screen
- Events: Handle special events like key presses or window resizing
Love2D handles this loop automatically and calls specific functions in your code at the right times.
Basic Love2D Callbacks
Love2D uses callback functions that you define to handle different parts of the game loop:
-- Called once when the game starts
function love.load()
-- Initialize variables, load assets
end
-- Called every frame to update game logic
function love.update(dt)
-- dt = delta time (seconds since last frame)
-- Update positions, handle input, etc.
end
-- Called every frame to draw graphics
function love.draw()
-- Draw sprites, shapes, text, etc.
end
-- Called when a key is pressed
function love.keypressed(key)
-- Handle single key press events
end
-- Called when a key is released
function love.keyreleased(key)
-- Handle key release events
end
Your First Interactive Game
Let’s create a simple game where you control a circle with the arrow keys:
-- Game variables
local player = {
x = 400,
y = 300,
speed = 200,
size = 20
}
-- Called once at startup
function love.load()
love.window.setTitle("My First Love2D Game")
love.graphics.setBackgroundColor(0.1, 0.1, 0.2) -- Dark blue background
end
-- Called every frame (dt = delta time)
function love.update(dt)
-- Check for continuous key presses
if love.keyboard.isDown("left") or love.keyboard.isDown("a") then
player.x = player.x - player.speed * dt
end
if love.keyboard.isDown("right") or love.keyboard.isDown("d") then
player.x = player.x + player.speed * dt
end
if love.keyboard.isDown("up") or love.keyboard.isDown("w") then
player.y = player.y - player.speed * dt
end
if love.keyboard.isDown("down") or love.keyboard.isDown("s") then
player.y = player.y + player.speed * dt
end
-- Keep player on screen
if player.x < player.size then
player.x = player.size
elseif player.x > love.graphics.getWidth() - player.size then
player.x = love.graphics.getWidth() - player.size
end
if player.y < player.size then
player.y = player.size
elseif player.y > love.graphics.getHeight() - player.size then
player.y = love.graphics.getHeight() - player.size
end
end
-- Called every frame to draw
function love.draw()
-- Draw player as a white circle
love.graphics.setColor(1, 1, 1) -- White color (RGB values 0-1)
love.graphics.circle("fill", player.x, player.y, player.size)
-- Draw instructions
love.graphics.setColor(0.8, 0.8, 0.8) -- Light gray
love.graphics.print("Use WASD or arrow keys to move", 10, 10)
love.graphics.print("Press ESC to quit", 10, 30)
-- Display player position
love.graphics.print("Position: " .. math.floor(player.x) .. ", " .. math.floor(player.y), 10, 60)
end
-- Handle single key press events
function love.keypressed(key)
if key == "escape" then
love.event.quit()
end
-- Reset player position with spacebar
if key == "space" then
player.x = 400
player.y = 300
end
end
Output: A white circle that moves smoothly with WASD or arrow keys, stays within screen bounds, and shows position information.
Adding Color and Visual Feedback
Let’s enhance our game with colors and visual effects:
local player = {
x = 400,
y = 300,
speed = 200,
size = 20,
color = {r = 1, g = 1, b = 1}, -- White
isMoving = false
}
local trail = {} -- Store previous positions for a trail effect
function love.load()
love.window.setTitle("Enhanced Movement Game")
love.graphics.setBackgroundColor(0.05, 0.05, 0.1)
end
function love.update(dt)
local oldX, oldY = player.x, player.y
player.isMoving = false
-- Movement with different colors
if love.keyboard.isDown("left", "a") then
player.x = player.x - player.speed * dt
player.color = {r = 1, g = 0.3, b = 0.3} -- Red when moving left
player.isMoving = true
end
if love.keyboard.isDown("right", "d") then
player.x = player.x + player.speed * dt
player.color = {r = 0.3, g = 1, b = 0.3} -- Green when moving right
player.isMoving = true
end
if love.keyboard.isDown("up", "w") then
player.y = player.y - player.speed * dt
player.color = {r = 0.3, g = 0.3, b = 1} -- Blue when moving up
player.isMoving = true
end
if love.keyboard.isDown("down", "s") then
player.y = player.y + player.speed * dt
player.color = {r = 1, g = 1, b = 0.3} -- Yellow when moving down
player.isMoving = true
end
-- Return to white when not moving
if not player.isMoving then
player.color = {r = 1, g = 1, b = 1}
end
-- Keep player on screen
player.x = math.max(player.size, math.min(love.graphics.getWidth() - player.size, player.x))
player.y = math.max(player.size, math.min(love.graphics.getHeight() - player.size, player.y))
-- Add to trail if position changed
if player.x ~= oldX or player.y ~= oldY then
table.insert(trail, {
x = oldX,
y = oldY,
life = 1.0,
color = {r = player.color.r, g = player.color.g, b = player.color.b}
})
end
-- Update trail
for i = #trail, 1, -1 do
trail[i].life = trail[i].life - dt * 2
if trail[i].life <= 0 then
table.remove(trail, i)
end
end
end
function love.draw()
-- Draw trail
for _, point in ipairs(trail) do
love.graphics.setColor(point.color.r, point.color.g, point.color.b, point.life * 0.5)
love.graphics.circle("fill", point.x, point.y, player.size * point.life)
end
-- Draw player
love.graphics.setColor(player.color.r, player.color.g, player.color.b)
love.graphics.circle("fill", player.x, player.y, player.size)
-- Draw a border around the player
love.graphics.setColor(1, 1, 1)
love.graphics.circle("line", player.x, player.y, player.size)
-- UI Text
love.graphics.setColor(0.9, 0.9, 0.9)
love.graphics.print("Use WASD or arrow keys to move", 10, 10)
love.graphics.print("Player changes color based on direction!", 10, 30)
love.graphics.print("Space: Reset position | ESC: Quit", 10, 50)
local speedText = player.isMoving and "Moving" or "Stopped"
love.graphics.print("Status: " .. speedText, 10, 80)
end
function love.keypressed(key)
if key == "escape" then
love.event.quit()
elseif key == "space" then
player.x = love.graphics.getWidth() / 2
player.y = love.graphics.getHeight() / 2
trail = {} -- Clear trail
end
end
Output: A circle that changes color based on movement direction, leaves a fading trail, and has enhanced visual feedback.
Adding Sound Effects
Let’s add audio feedback to make the game more engaging:
local player = {
x = 400,
y = 300,
speed = 200,
size = 20
}
local sounds = {}
function love.load()
love.window.setTitle("Movement Game with Sound")
-- Create simple sound effects programmatically
-- Note: In a real game, you'd load sound files
-- sounds.move = love.audio.newSource("assets/sounds/move.wav", "static")
-- sounds.reset = love.audio.newSource("assets/sounds/reset.wav", "static")
-- For this example, we'll use placeholder sounds
-- You can add actual sound files to your assets folder
end
function love.update(dt)
local wasMoving = player.isMoving or false
player.isMoving = false
if love.keyboard.isDown("left", "a", "right", "d", "up", "w", "down", "s") then
if love.keyboard.isDown("left", "a") then
player.x = player.x - player.speed * dt
end
if love.keyboard.isDown("right", "d") then
player.x = player.x + player.speed * dt
end
if love.keyboard.isDown("up", "w") then
player.y = player.y - player.speed * dt
end
if love.keyboard.isDown("down", "s") then
player.y = player.y + player.speed * dt
end
player.isMoving = true
-- Play movement sound when starting to move
if not wasMoving and sounds.move then
sounds.move:play()
end
end
-- Keep player on screen
player.x = math.max(player.size, math.min(love.graphics.getWidth() - player.size, player.x))
player.y = math.max(player.size, math.min(love.graphics.getHeight() - player.size, player.y))
end
function love.draw()
-- Draw player
love.graphics.setColor(0.3, 0.7, 1) -- Light blue
love.graphics.circle("fill", player.x, player.y, player.size)
-- Draw border
love.graphics.setColor(1, 1, 1)
love.graphics.circle("line", player.x, player.y, player.size)
-- Instructions
love.graphics.print("WASD/Arrow keys: Move", 10, 10)
love.graphics.print("Space: Reset | ESC: Quit", 10, 30)
love.graphics.print("Position: " .. math.floor(player.x) .. ", " .. math.floor(player.y), 10, 60)
end
function love.keypressed(key)
if key == "escape" then
love.event.quit()
elseif key == "space" then
-- Play reset sound
if sounds.reset then
sounds.reset:play()
end
player.x = love.graphics.getWidth() / 2
player.y = love.graphics.getHeight() / 2
end
end
Game Structure Best Practices
Even for simple games, good organization helps:
-- Game state
local game = {
state = "playing", -- "playing", "paused", "menu"
score = 0,
time = 0
}
local player = {
x = 400,
y = 300,
speed = 200,
size = 20
}
-- Separate update functions for clarity
function updatePlayer(dt)
if love.keyboard.isDown("left", "a") then
player.x = player.x - player.speed * dt
end
if love.keyboard.isDown("right", "d") then
player.x = player.x + player.speed * dt
end
if love.keyboard.isDown("up", "w") then
player.y = player.y - player.speed * dt
end
if love.keyboard.isDown("down", "s") then
player.y = player.y + player.speed * dt
end
-- Boundary checking
player.x = math.max(player.size, math.min(love.graphics.getWidth() - player.size, player.x))
player.y = math.max(player.size, math.min(love.graphics.getHeight() - player.size, player.y))
end
function updateGame(dt)
game.time = game.time + dt
game.score = math.floor(game.time * 10) -- Score based on time
end
-- Separate draw functions
function drawPlayer()
love.graphics.setColor(0.2, 0.8, 0.2) -- Green
love.graphics.circle("fill", player.x, player.y, player.size)
love.graphics.setColor(1, 1, 1)
love.graphics.circle("line", player.x, player.y, player.size)
end
function drawUI()
love.graphics.setColor(1, 1, 1)
love.graphics.print("Score: " .. game.score, 10, 10)
love.graphics.print("Time: " .. math.floor(game.time), 10, 30)
love.graphics.print("WASD: Move | P: Pause | ESC: Quit", 10, 60)
if game.state == "paused" then
love.graphics.setColor(1, 1, 0)
love.graphics.print("PAUSED - Press P to resume", 300, 200)
end
end
-- Main Love2D callbacks
function love.load()
love.window.setTitle("Structured Game Example")
love.graphics.setBackgroundColor(0.1, 0.1, 0.1)
end
function love.update(dt)
if game.state == "playing" then
updatePlayer(dt)
updateGame(dt)
end
end
function love.draw()
if game.state == "playing" or game.state == "paused" then
drawPlayer()
drawUI()
end
end
function love.keypressed(key)
if key == "escape" then
love.event.quit()
elseif key == "p" then
if game.state == "playing" then
game.state = "paused"
elseif game.state == "paused" then
game.state = "playing"
end
elseif key == "space" then
-- Reset game
player.x = love.graphics.getWidth() / 2
player.y = love.graphics.getHeight() / 2
game.score = 0
game.time = 0
end
end
Output: A well-structured game with pause functionality, score system, and organized code.
Common Beginner Mistakes
- Forgetting Delta Time: Always multiply movement by
dt
for frame-rate independent movement - Color Range Confusion: Love2D uses 0-1 for colors, not 0-255
- Missing love.graphics.setColor(): Remember to set colors before drawing
- Coordinate System: (0,0) is top-left, Y increases downward
- File Path Issues: Use forward slashes and check case sensitivity
Next Steps
You’ve created your first interactive Love2D game! In the next chapter, we’ll dive deeper into the game structure and explore the complete set of Love2D callbacks and their uses.
Chapter Summary:
- Love2D games use callback functions for different phases
love.update(dt)
handles game logic and inputlove.draw()
renders graphics to screen- Delta time (
dt
) ensures smooth, frame-rate independent movement - Good code organization improves maintainability
- Color values in Love2D range from 0 to 1
- Always test your game frequently during development