[Love2D Game Maker] Learn to create the classic game of Pong in 10mins

1. Create New Project

A love2D project can be several styles: 1. A single Lua script file, 2. A folder with a Lua script file named main.lua, 3. A package file with .love extension, this format is mostly used when you want to distribute your games to others.

In this tutorial, we will use the second style. So we create a folder in the Documents folder.

1.1 Create project folder

Select ‘Docs’ Tab, then select ‘Documents’ row in Sandbox section

Documents

Click ‘Select’ button on right of the navigation bar

Enter ‘Selection mode’ of the File Explorer

We can create, select, copy, delete, rename file/folder items.

Here we click the ‘NewFolder’ button in the toolbar

Enter the folder name

Here I use DemoPong as the new project name

1.2 Copy from other project

You can also copy other project as a template, then modify it as you like.

There are many demo projects in the Bundle of the App. You can copy them to Documents, so you can modify them then try to run.

Select Code Editor tab,

Click the topleft button in the navigation bar, a menu will pop up

click the ‘Open Files in Bundle’ menu

Here is the File Selection Panel

There are 3 folders, Games is the folder where all the love2d games live in.

Click on it.

Here is the contents of Games folder

We can see there are 6 items in it. Some are folder type and one is the special ‘.love’ package type.

Enter selection mode

Then we can select the folders we want

Then click the ‘…’ button in the bottom toolbar

Then click the ‘Copy’ menu

now we have copied all the selected items

Let’s quit the File Selection Panel now

Here is the Documents folder in the ‘Docs’ tab

You should know how to navigate here

If you forget, you can go back to 1.1 section

Enter Selection mode

Click ‘…’ button in bottom toolbar

Click ‘Paste’ menu

Then all the items will be pasted here

Go to ‘Love’ tab

pull down to refresh

Then you can see all the games you copied last step will show in the Sandbox section.

1.3 Download some open source love2d game projects from the Internet

You can also download love2d projects source code to your iPhone/iPad, save them to Files app. Then copy the projects to folder Love2D Game Maker in Files app.

Love2DGameMaker folder in Files app

You can add/remove files in it.

It’s very convenient for you to import games from the Internet or your computer.

2 config the game options

2.1 create conf.lua file

Go to DemoPong folder

Enter Selection mode

Click ‘NewFile’ button in bottom toolbar

Enter ‘conf.lua’, the config file name can only be ‘conf.lua’

Click Done

2.2 config options

Here is the Love2D documentation of Config Files – LOVE (love2d.org)

We can copy the demo file contents into out conf.lua, then do some modifications according to our requirements.

Here is a demo config file that I used in the pong-master game in Bundle

-- Configuration
function love.conf(t)
    t.title = "Pong"
    t.version = "11.1"
    t.gameversion = "0.1"
    t.accelerometerjoystick = true
    t.window.x = 110
    t.window.y = 100
    t.window.width = 200               -- The window width (number)
    t.window.height = 200              -- The window height (number)
    t.window.resizable = true
    t.window.fullscreen = true         -- whether fullscreen
    -- t.window.fullscreentype = exclusive
    t.window.borderless = true
    t.window.vsync = true              -- Enable vertical sync (boolean)
    t.window.fsaa = 0                  -- The number of samples to use with multi-sampled antialiasing (number)
    t.window.display = 1               -- Index of the monitor to show the window in (number)
    t.window.highdpi = true           -- Enable high-dpi mode for the window on a Retina display (boolean). Added in 0.9.1
    t.window.srgb = false              -- Enable sRGB gamma correction when drawing to the screen (boolean). Added in 0.9.1

    t.modules.audio = true             -- Enable the audio module (boolean)
    t.modules.event = true             -- Enable the event module (boolean)
    t.modules.graphics = true          -- Enable the graphics module (boolean)
    t.modules.image = true             -- Enable the image module (boolean)
    t.modules.joystick = true          -- Enable the joystick module (boolean)
    t.modules.keyboard = true          -- Enable the keyboard module (boolean)
    t.modules.math = true              -- Enable the math module (boolean)
    t.modules.mouse = false             -- Enable the mouse module (boolean)
    t.modules.physics = true           -- we don't need phys on our game.
    t.modules.sound = true             -- Enable the sound module (boolean)
    t.modules.system = true            -- Enable the system module (boolean)
    t.modules.timer = true             -- Enable the timer module (boolean)
    t.modules.window = true            -- Enable the window module (boolean)
end

This file tells the love2d framework to create a fullscreen window, then the game content will show in that window. If you don’t want a fullscreen window, you can set the `t.window.fullscreen = true` to false. Then the love2d framework will create a window with size 200×200, and origin position (topleft) at point (110,100).

If you want to learn more about the config options, you can read the documents then try to modify the options then run the game to see the effects.

This is why I develop the app, it can help you learn to coding. I have also developed other apps, LuaLu REPL, an app that can run pure lua codes, Solar2D Studio an app that similar to Love2D Game Maker, but based on Solar2D game engine (also known as CoronaSDK)

3 implement the game logic

3.1 Create main.lua file

Create ‘main.lua’ file in DemoPong folder

Below is content of main.lua

debug = false
screenWidth = love.graphics.getWidth()
screenHeight = love.graphics.getHeight()
drawParticles = true

local safeX, safeY, safeW, safeH = love.window.getSafeArea()

updatetime = 0
paddleSize = {w = 50, h = 100}
puckSize = 50
wallThickness = 10
centerLineThickness = 10
goalSize = 300
paddleOffset = 100
gameSettings = {time = 3 * 60, started = false, timeLeft = 3 * 60}

world = nil

objects = {}

score = {left = 0, right = 0}

function love.load(arg)
    setScale()
    
    --love.window.setPosition(130, 80, 1)
    
    world = love.physics.newWorld(0, 0)

    createPaddles()
    createPuck()
    createWalls()
    
    nuor = ""

end

-- Function to remove a specific object from the table
function removeFromTable(tbl, obj)
    for i, value in ipairs(tbl) do
        if value == obj then
            table.remove(tbl, i) -- Remove the object from the table
            break -- Exit the loop once the object is removed
        end
    end
end

function removeAllPhysicsObjects(world)
    -- Destroy all bodies
    local bodies = world:getBodies()
    for _, body in ipairs(bodies) do
        body:destroy()
    end

    -- Destroy all joints
    local joints = world:getJoints()
    for _, joint in ipairs(joints) do
        joint:destroy()
    end
end


function love.resize(w, h)
  print(("Window resized to width: %d and height: %d."):format(w, h))
    local oldW = screenWidth
    local oldH = screenHeight
    screenWidth = love.graphics.getWidth()
    screenHeight = love.graphics.getHeight()
    if (oldW ~= screenWidth or oldH ~= screenHeight) then
        love.window.setMode(screenWidth, screenHeight)
    end
    
    removeFromTable(objects, leftPaddle)
    removeFromTable(objects, rightPaddle)
    removeFromTable(objects, puck)
    removeFromTable(objects, wallT)
    removeFromTable(objects, wallB)
    
    removeAllPhysicsObjects(world)

    createPaddles()
    createPuck()
    createWalls()

end

function love.displayrotated(i, o)
    nuor = love.window.getDisplayOrientation(i)
    print("rotate")
    
    if (nuor == "landscape") then
        --love.event.quit()
    end
end

function love.update(dt)
    updatetime = dt

    world:update(dt)
    puck.pSystem:setPosition(puck.body:getX(), puck.body:getY())
    puck.pSystem:update(dt)

    if (puck.body:getX() < 0) then
        resetPuck()
        score.right = score.right + 1
    elseif (puck.body:getX() > screenWidth) then
        resetPuck()
        score.left = score.left + 1
    end

    if gameSettings.started then
        gameSettings.timeLeft = gameSettings.timeLeft - dt

        if (gameSettings.timeLeft < 0) then
            if score.left > score.right then
                gameSettings.winner = "Blue Wins!"
            elseif score.right > score.left then
                gameSettings.winner = "Red Wins!"
            elseif score.right == score.left then
                gameSettings.winner = "Tie Game"
            end

            gameSettings.started = false
            gameSettings.timeLeft = gameSettings.time
            resetPuck()
            score.left = 0
            score.right = 0
        end
    end
end

function love.keypressed(key, scancode, isrepeat)
    if key == 'd' and not isrepeat then
        debug = not debug
    end

    if love.keyboard.isDown('escape') then
        love.event.push('quit')
    elseif love.keyboard.isDown('r') then
        resetPuck()
        score.left = 0
        score.right = 0
    elseif love.keyboard.isDown('p') then
        drawParticles = not drawParticles
    end
end

function love.draw()
    
    love.graphics.print(nuor, 100, 100)
    
    -- center line
    love.graphics.setColor(1, 1, 1)
    love.graphics.rectangle("fill", love.graphics.getWidth() / 2 -
                                (centerLineThickness / 2), 0,
                            centerLineThickness, love.graphics.getHeight())

    love.graphics.setColor(1, 1, 1)

    -- puck and paddles
    for i, o in ipairs(objects) do
        love.graphics.draw(o.img, o.body:getX(), o.body:getY(), 0, 1, 1,
                           o.img:getWidth() / 2, o.img:getHeight() / 2)
    end

    -- draw particle systems
    if (drawParticles) then
        love.graphics.draw(puck.pSystem, 0, 0)
    end

    -- scores and timer
    love.graphics.setColor(0, 0, 1)
    love.graphics.printf(tostring(score.left), 10, 50 * sy,
                         (screenWidth - 20) / (10 * sx), "left", 0, 10 * sx,
                         10 * sy)
    love.graphics.setColor(1, 0, 0)
    love.graphics.printf(tostring(score.right), 10, 50 * sy,
                         (screenWidth - 20) / (10 * sx), "right", 0, 10 * sx,
                         10 * sy)
    love.graphics.setColor(1, 1, 0)
    love.graphics.printf(SecondsToClock(gameSettings.timeLeft), 10, 50 * sy,
                         (screenWidth - 20) / (10 * sx), "center", 0, 10 * sx,
                         10 * sy)

    -- winner message
    if (not gameSettings.started and
        not (gameSettings.winner == nil or gameSettings.winner == '')) then
        love.graphics.setColor(0, 1, 0)
        love.graphics.printf(gameSettings.winner, 10, screenHeight / 2,
                             (screenWidth - 20) / (10 * sx), "center", 0,
                             10 * sx, 10 * sy)
        love.graphics.setColor(1, 1, 1)
    end

    if debug then
        drawDebug()
    end
end

function love.touchpressed(id, x, y, dx, dy, pressure)
    if not gameSettings.started then
        gameSettings.started = true
        gameSettings.winner = nil
    end

    -- prevent puck from being stuck in x direction, and start motion after resetting puck
    if math.abs(puck.body:getLinearVelocity()) < 10 then
        local v = 500
        if math.random(0, 1) < 0.5 then
            v = v * -1
        end
        puck.body:setLinearVelocity(v, 0)
    end

    local isLeft = true
    if (x > (screenWidth / 2)) then
        isLeft = false
    end

    if (isLeft and leftPaddle.touchid == nil) then
        leftPaddle.touchid = id
        leftPaddle.joint:setTarget(paddleOffset, y)
    elseif (not isLeft and rightPaddle.touchid == nil) then
        rightPaddle.touchid = id
        rightPaddle.joint:setTarget(screenWidth - paddleOffset, y)
    end
end

function love.touchmoved(id, x, y, dx, dy, pressure)
    if (leftPaddle.touchid == id and x <= screenWidth / 2) then
        leftPaddle.joint:setTarget(paddleOffset, y)
    elseif (rightPaddle.touchid == id and x > screenWidth / 2) then
        rightPaddle.joint:setTarget(screenWidth - paddleOffset, y)
    end
end

function love.touchreleased(id, x, y, dx, dy, pressure)
    if (leftPaddle.touchid == id) then
        leftPaddle.touchid = nil
    elseif (rightPaddle.touchid == id) then
        rightPaddle.touchid = nil
    end
end

function getCirclePaddle(size, color)
    local paddle = love.graphics.newCanvas(size, size)
    love.graphics.setCanvas(paddle)
    love.graphics.setColor(color.r, color.g, color.b, 1)

    love.graphics.circle("fill", size / 2, size / 2, size / 2)

    love.graphics.setCanvas()
    return paddle
end

function getPaddle(size, color)
    local paddle = love.graphics.newCanvas(size.w, size.h)
    love.graphics.setCanvas(paddle)
    love.graphics.setColor(color.r, color.g, color.b, 1)
    love.graphics.rectangle("fill", 0, 0, size.w, size.h, 0, 1, 1, size.w / 2,
                            size.h / 2)
    love.graphics.setCanvas()
    return paddle
end

function getRectangle(width, height, color)
    local rect = love.graphics.newCanvas(width, height)
    love.graphics.setCanvas(rect)
    love.graphics.setColor(color.r, color.g, color.b)
    love.graphics.rectangle("fill", 0, 0, width, height)
    love.graphics.setCanvas()
    return rect
end

function createWalls()
    wallT = {
        img = getRectangle(screenWidth, wallThickness, {r = 1, g = 1, b = 0})
    }

    wallT.body = love.physics.newBody(world, screenWidth / 2, wallThickness / 2,
                                      "static")
    wallT.shape = love.physics.newRectangleShape(screenWidth, wallThickness)
    wallT.fixture = love.physics.newFixture(wallT.body, wallT.shape)
    wallT.fixture:setFriction(0)
    table.insert(objects, wallT)

    wallB = {
        img = getRectangle(screenWidth, wallThickness, {r = 1, g = 1, b = 0})
    }
    wallB.body = love.physics.newBody(world, screenWidth / 2,
                                      screenHeight - (wallThickness / 2),
                                      "static")
    wallB.shape = love.physics.newRectangleShape(screenWidth, wallThickness)
    wallB.fixture = love.physics.newFixture(wallB.body, wallB.shape)
    table.insert(objects, wallB)
end

function setScale()
    local width, height = love.graphics.getDimensions()
    sx = width / 1920
    sy = height / 1080
    paddleSize.w = paddleSize.w * sx
    paddleSize.h = paddleSize.h * sy
    wallThickness = wallThickness * sx
    puckSize = puckSize * sx
    centerLineThickness = centerLineThickness * sx
    goalSize = goalSize * sy
    paddleOffset = paddleOffset * sx
end

function createPaddles()
    leftPaddle = {
        img = getPaddle(paddleSize, {r = 0, g = 0, b = 1}),
        touchid = nil
    }
    leftPaddle.body = love.physics.newBody(world, paddleOffset,
                                           screenHeight / 2, "dynamic")
    leftPaddle.body:setFixedRotation(true)
    leftPaddle.shape = love.physics
                           .newRectangleShape(paddleSize.w, paddleSize.h)
    leftPaddle.fixture = love.physics.newFixture(leftPaddle.body,
                                                 leftPaddle.shape)
    leftPaddle.joint = love.physics.newMouseJoint(leftPaddle.body, paddleOffset,
                                                  screenHeight / 2)
    rightPaddle = {
        img = getPaddle(paddleSize, {r = 1, g = 0, b = 0}),
        touchid = nil
    }
    rightPaddle.body = love.physics.newBody(world, screenWidth - paddleOffset,
                                            screenHeight / 2, "dynamic")
    rightPaddle.body:setFixedRotation(true)
    rightPaddle.shape = love.physics.newRectangleShape(paddleSize.w,
                                                       paddleSize.h)
    rightPaddle.fixture = love.physics.newFixture(rightPaddle.body,
                                                  rightPaddle.shape)
    rightPaddle.joint = love.physics.newMouseJoint(rightPaddle.body,
                                                   screenWidth - paddleOffset,
                                                   screenHeight / 2)

    table.insert(objects, leftPaddle)
    table.insert(objects, rightPaddle)
end

function createPuck()
    puck = {img = getCirclePaddle(puckSize, {r = 1, g = 1, b = 0})}
    puck.body = love.physics.newBody(world, screenWidth / 2, screenHeight / 2,
                                     "dynamic")
    puck.body:setLinearDamping(-.20)
    puck.body:setBullet(true)
    puck.shape = love.physics.newCircleShape(puckSize / 2)
    puck.fixture = love.physics.newFixture(puck.body, puck.shape)
    puck.fixture:setRestitution(.9)

    local pSystem = love.graphics.newParticleSystem(puck.img, puckSize)
    pSystem:setParticleLifetime(0.2, 0.5)
    pSystem:setLinearAcceleration(-100, -100, 100, 100)
    pSystem:setColors(1, 1, 0, 255, 1, 1, 1, 255)
    pSystem:setSizes(1.0, 0.01)
    pSystem:setEmissionRate(60)
    puck.pSystem = pSystem

    table.insert(objects, puck)
end

function resetPuck()
    puck.body:setLinearVelocity(0, 0)
    puck.body:setX(screenWidth / 2)
    puck.body:setY(screenHeight / 2)
end

function SecondsToClock(seconds)
    local seconds = tonumber(seconds)

    if seconds <= 0 then
        return "00:00"
    else
        mins = string.format("%02.f", math.floor(seconds / 60))
        secs = string.format("%02.f", math.floor(seconds - mins * 60))
        return mins .. ":" .. secs
    end
end

function drawDebug()
    love.graphics.setColor(1, 1, 1)
    love.graphics.print("DT: " .. tostring(updatetime), 10, 10)
    love.graphics.print("FPS: " .. tostring(1.0 / updatetime), 10, 20)
    love.graphics.print(
        "Screen " .. tostring(love.graphics.getWidth()) .. "x" ..
            tostring(love.graphics.getHeight()) .. " scale " .. tostring(sx) ..
            "x" .. tostring(sy), 10, 30)
    love.graphics.print("Puck " .. tostring(puck.body:getX()) .. ", " ..
                            tostring(puck.body:getY()) .. "   v: " ..
                            puck.body:getLinearVelocity(), 10, 40)
    love.graphics.print("Score " .. tostring(score.left) .. ":" ..
                            tostring(score.right), 10, 50)
    love.graphics.print("Game " .. tostring(gameSettings.timeLeft), 10, 60)
    
    love.graphics.print("SafeArea " .. tostring(safeX) .. "," .. tostring(safeY) .."," .. tostring(safeW) .."," .. tostring(safeH), 10, 70)

    for _, body in pairs(world:getBodies()) do
        for _, fixture in pairs(body:getFixtures()) do
            local shape = fixture:getShape()

            if shape:typeOf("CircleShape") then
                local cx, cy = body:getWorldPoints(shape:getPoint())
                love.graphics.circle("fill", cx, cy, shape:getRadius())
            elseif shape:typeOf("PolygonShape") then
                love.graphics.polygon("fill",
                                      body:getWorldPoints(shape:getPoints()))
            else
                love.graphics.line(body:getWorldPoints(shape:getPoints()))
            end
        end
    end
end