In this tutorial, we’ll guide you through the process of creating a simple scrolling shooter game using LÖVE, a Lua-based game development framework. The game involves controlling a plane, shooting bullets, spawning enemies, and handling collisions. Take a look at the final source code.


1. Setup the Project and Environment

  • Install LÖVE: Download and install LÖVE from the official website.
  • Project Structure:
    • Create a new folder for your game project.
    • Inside this folder, create two files: conf.lua and main.lua.
    • Create an assets folder to store images and sounds.

2. Configure the Game Window (conf.lua)

Set up the game window dimensions and title.

function love.conf(t)
    t.title = "Scrolling Shooter"  -- Window title
    t.version = "11.4"             -- LOVE2D version
    t.window.width = 480           -- Window width
    t.window.height = 800          -- Window height
end
  • Explanation: A vertical window is ideal for a scrolling shooter.

3. Load Assets and Initialize Variables (main.lua)

3.1 Initialize Game Variables

At the top of main.lua, declare global variables for timers, player properties, images, sounds, and game state.

-- Timers
canShoot = true
canShootTimerMax = 0.2
canShootTimer = canShootTimerMax
createEnemyTimerMax = 0.4
createEnemyTimer = createEnemyTimerMax
 
-- Player Object
player = { x = 200, y = 710, speed = 150, img = nil }
isAlive = true
score = 0
 
-- Image Storage
bulletImg = nil
enemyImg = nil
 
-- Sound Storage
gunSound = nil
 
-- Entity Storage
bullets = {}  -- Bullets in play
enemies = {}  -- Enemies in play

3.2 Load Assets

In the love.load function, load images and sounds.

function love.load()
    player.img = love.graphics.newImage('assets/plane.png')
    bulletImg = love.graphics.newImage('assets/bullet.png')
    enemyImg = love.graphics.newImage('assets/enemy.png')
    gunSound = love.audio.newSource('assets/gun-sound.wav', 'static')
end
  • Explanation: Ensure the assets exist in the assets folder.

4. Handle Player Input and Movement

4.1 Movement Controls

In the love.update function, handle player movement based on key presses.

-- Horizontal movement
if love.keyboard.isDown('left', 'a') then
    if player.x > 0 then
        player.x = player.x - (player.speed * dt)
    end
elseif love.keyboard.isDown('right', 'd') then
    if player.x < (love.graphics.getWidth() - player.img:getWidth()) then
        player.x = player.x + (player.speed * dt)
    end
end
 
-- Vertical movement
if love.keyboard.isDown('up', 'w') then
    if player.y > (love.graphics.getHeight() / 2) then
        player.y = player.y - (player.speed * dt)
    end
elseif love.keyboard.isDown('down', 's') then
    if player.y < (love.graphics.getHeight() - 55) then
        player.y = player.y + (player.speed * dt)
    end
end
  • Explanation: The player cannot move off-screen.

4.2 Shooting Controls

Allow the player to shoot bullets.

if love.keyboard.isDown('space', 'rctrl', 'lctrl') and canShoot then
    -- Create a new bullet
    newBullet = {
        x = player.x + (player.img:getWidth() / 2),
        y = player.y,
        img = bulletImg
    }
    table.insert(bullets, newBullet)
    gunSound:play()
    canShoot = false
    canShootTimer = canShootTimerMax
end
  • Explanation: Shooting is limited by a timer to prevent spamming bullets.

5. Implement Shooting Mechanics

5.1 Update Shooting Timer

In love.update, decrement the shooting timer.

-- Update shooting timer
canShootTimer = canShootTimer - (1 * dt)
if canShootTimer < 0 then
    canShoot = true
end

5.2 Move Bullets

Update bullet positions and remove them if they go off-screen.

for i, bullet in ipairs(bullets) do
    bullet.y = bullet.y - (250 * dt)
    if bullet.y < 0 then
        table.remove(bullets, i)
    end
end

6. Spawn Enemies and Manage Movement

6.1 Enemy Spawn Timer

Control the rate at which enemies appear.

-- Update enemy spawn timer
createEnemyTimer = createEnemyTimer - (1 * dt)
if createEnemyTimer < 0 then
    createEnemyTimer = createEnemyTimerMax
 
    -- Spawn a new enemy
    randomX = math.random(10, love.graphics.getWidth() - 10)
    newEnemy = { x = randomX, y = -10, img = enemyImg }
    table.insert(enemies, newEnemy)
end

6.2 Move Enemies

Update enemy positions and remove them if they exit the screen.

for i, enemy in ipairs(enemies) do
    enemy.y = enemy.y + (200 * dt)
    if enemy.y > 850 then
        table.remove(enemies, i)
    end
end

7. Detect Collisions and Update Game State

7.1 Collision Detection Function

Define a function to check for collisions.

function CheckCollision(x1, y1, w1, h1, x2, y2, w2, h2)
    return x1 < x2 + w2 and
           x2 < x1 + w1 and
           y1 < y2 + h2 and
           y2 < y1 + h1
end

7.2 Handle Collisions

Check for collisions between bullets and enemies, and between enemies and the player.

for i, enemy in ipairs(enemies) do
    for j, bullet in ipairs(bullets) do
        if CheckCollision(
            enemy.x, enemy.y, enemy.img:getWidth(), enemy.img:getHeight(),
            bullet.x, bullet.y, bullet.img:getWidth(), bullet.img:getHeight()
        ) then
            table.remove(bullets, j)
            table.remove(enemies, i)
            score = score + 1
        end
    end
 
    if CheckCollision(
        enemy.x, enemy.y, enemy.img:getWidth(), enemy.img:getHeight(),
        player.x, player.y, player.img:getWidth(), player.img:getHeight()
    ) and isAlive then
        table.remove(enemies, i)
        isAlive = false
    end
end
  • Explanation: When an enemy is hit by a bullet, both are removed, and the score increases. If an enemy collides with the player, the game is over.

8. Render Graphics on the Screen

8.1 Draw Bullets and Enemies

In the love.draw function, render bullets and enemies.

for i, bullet in ipairs(bullets) do
    love.graphics.draw(bullet.img, bullet.x, bullet.y)
end
 
for i, enemy in ipairs(enemies) do
    love.graphics.draw(enemy.img, enemy.x, enemy.y)
end

8.2 Draw Player and HUD

Render the player and the score.

-- Set color to white
love.graphics.setColor(255, 255, 255)
love.graphics.print("SCORE: " .. tostring(score), 400, 10)
 
if isAlive then
    love.graphics.draw(player.img, player.x, player.y)
else
    love.graphics.print(
        "Press 'R' to restart",
        love.graphics:getWidth() / 2 - 50,
        love.graphics:getHeight() / 2 - 10
    )
end
  • Explanation: If the player is dead, prompt to restart the game.

9. Restart the Game After Game Over

Allow the player to restart the game by pressing ‘R’.

if not isAlive and love.keyboard.isDown('r') then
    -- Reset game state
    bullets = {}
    enemies = {}
    canShootTimer = canShootTimerMax
    createEnemyTimer = createEnemyTimerMax
    player.x = 50
    player.y = 710
    score = 0
    isAlive = true
end

10. Additional Features and Tips

  • Debugging Mode: Add a debug mode to display FPS or other stats.
  • Boundaries: Ensure the player and enemies stay within the screen bounds.
  • Game Balance: Adjust timers and speeds to make the game challenging but fair.
  • Asset Quality: Use high-quality images and sounds to enhance the gaming experience.
  • Extensions:
    • Add power-ups.
    • Introduce different enemy types.
    • Implement levels or waves.

Conclusion

By following this tutorial, you’ve created a functional scrolling shooter game using LOVE2D. This project covers fundamental game development concepts like rendering graphics, handling user input, collision detection, and managing game states. You can build upon this foundation to create more complex and engaging games.

Go and take a look at the source code for the finished example.

Happy Coding!