How: Created as an open-source 2D game framework in Lua. First released in 2008 by Henk Boom and Anders Ruud.
Who: Developed and maintained by the LÖVE Development Team (Anders Ruud, Bart van Strien, and community).
Why: To offer a lightweight, high-performance, and minimal-overhead 2D framework for rapid prototyping and full game development in Lua without the bloat of visual editors.
Introduction
LÖVE (commonly known as Love2D) is an open-source framework for making 2D games in the Lua programming language. It is highly portable, lightweight, and leverages powerful libraries like SDL2, OpenGL/OpenGL ES, OpenAL, and Box2D underneath to handle windowing, graphics, audio, and physics.
Advantages
Blazing Fast: Extremely fast startup times and low memory/CPU footprint.
Lua Integration: Scripted in Lua, enabling rapid prototyping, dynamic coding, and minimal boilerplate.
Full Control: No forced IDE or editor — use any text editor and build your engine architecture how you see fit.
Cross-Platform: Compile once and run on Windows, macOS, Linux, Android, iOS, and the Web (via tools like LÖVe.js).
Powerful APIs: Tightly integrated Box2D physics, OpenAL sound, SDL2 window management, and GLSL shading out of the box.
Disadvantages
No GUI Editor: Completely code-driven. You must write or import code for UI, animation timelines, and level design.
Barebones Architecture: Does not provide high-level abstractions like “entities” or “scenes”. You must design your own game loop architecture or import libraries.
Lua Limitations: Dynamic typing and lack of default OOP (requires metatables or libraries like classic or hump).
Project Setup & Running
Basic Directory Structure
Every LÖVE project requires at least a main.lua file.
my-game/
├── main.lua -- Core entry point, contains callbacks
├── conf.lua -- Optional config for window, modules, etc.
├── assets/ -- Textures, audio, fonts
└── src/ -- Source code modules
Config File (conf.lua)
Used to configure the game window, modules, and package parameters before the engine loads.
function love.conf(t) t.identity = "my_game_save_dir" -- Folder name for save data in user directory t.version = "11.5" -- Compatible LÖVE version t.console = true -- Enable terminal console for print debugging (Windows) t.window.title = "LÖVE 2D Game" -- Window title t.window.icon = "assets/icon.png" -- Path to window icon t.window.width = 800 -- Screen width t.window.height = 600 -- Screen height t.window.resizable = true -- Let user resize window t.window.vsync = 1 -- 1 = vertical sync enabled, 0 = disabled t.window.msaa = 4 -- Multisample anti-aliasing (0 to 16) t.modules.physics = true -- Enable Box2D physics module t.modules.joystick = true -- Enable gamepad/joystick supportend
Running the Game
Command Line: Run the folder directly.
love .
Distribution (.love): Zip all files in the root folder (do not zip the folder itself, zip the contents) and rename the extension from .zip to .love.
Core Callbacks (Lifecycle)
Lifecycle Methods
LÖVE is fully event-driven and calls specific functions in main.lua at critical lifecycle phases.
-- Called ONCE when the game starts. Use for loading assets, instantiating objects, and initialization.function love.load() player_img = love.graphics.newImage("assets/player.png") player_x = 100 player_y = 100 player_speed = 300end-- Called every frame. Used to update game state (positions, physics, logic).-- @param dt Delta Time (seconds since the last frame)function love.update(dt) -- Example keyboard polling (FPS independent movement) if love.keyboard.isDown("d") then player_x = player_x + player_speed * dt elseif love.keyboard.isDown("a") then player_x = player_x - player_speed * dt endend-- Called every frame. Used exclusively to render graphics to the screen.function love.draw() love.graphics.draw(player_img, player_x, player_y)end
Input Event Callbacks
Event callbacks are triggered instantly when physical inputs are triggered by the operating system, unlike polling inside love.update.
-- Triggered when a key is pressed down-- @param key The character representation (e.g., "space", "escape", "w")-- @param scancode The layout-independent keyboard code-- @param isrepeat Boolean indicating if the key is repeating due to being held downfunction love.keypressed(key, scancode, isrepeat) if key == "escape" then love.event.quit() -- Quit game end if key == "space" and not isrepeat then player:jump() endend-- Triggered when a key is releasedfunction love.keyreleased(key, scancode) print("Key released: " .. key)end-- Triggered when a mouse button is pressed-- @param button Mouse button index (1 = Left, 2 = Right, 3 = Middle)-- @param presses Number of consecutive clicks (e.g. 2 for double click)function love.mousepressed(x, y, button, istouch, presses) if button == 1 then player:shoot(x, y) endend-- Triggered when the mouse wheel is rotated-- @param y Fractional scroll amount (-1 to 1) along the vertical axisfunction love.wheelmoved(x, y) if y > 0 then camera:zoomIn() elseif y < 0 then camera:zoomOut() endend
System Event Callbacks
-- Called when the window gains or loses focusfunction love.focus(f) if not f then print("Game Paused - Lost Focus") -- Pause music, display pause screen endend-- Called when the game window is resized (if resizable = true)function love.resize(w, h) print(("Window resized to %dx%d"):format(w, h))end-- Called just before the game closes-- @return If true is returned, the shutdown process is aborted!function love.quit() print("Saving player progress...") save_game_data() return false -- Return false to allow closing, true to prevent itend
Graphics & Rendering
Basic Colors & Math
Color Format: Colors in LÖVE use normalized RGBA float format (0.0 to 1.0) instead of 0-255.
function love.draw() -- Set active color to green (Red=0, Green=1, Blue=0, Alpha=1) love.graphics.setColor(0, 1, 0, 1) love.graphics.rectangle("fill", 50, 50, 200, 100) -- fill rectangle -- Draw red outline circle love.graphics.setColor(1, 0, 0, 0.5) -- semi-transparent red love.graphics.circle("line", 400, 300, 50) -- Reset color to default (White) so subsequent drawings aren't tinted love.graphics.setColor(1, 1, 1, 1)end
Drawings Shapes API
-- Draw modes: "fill" (solid shape) or "line" (outline wireframe)love.graphics.rectangle(mode, x, y, width, height, rx, ry, segments) -- Draw rect (rx, ry for rounded corners)love.graphics.circle(mode, x, y, radius, segments) -- Draw circlelove.graphics.line(x1, y1, x2, y2, x3, y3, ...) -- Connect dots with lineslove.graphics.polygon(mode, vertices) -- Draw polygon (vertices = flat table of coords)love.graphics.ellipse(mode, x, y, radiusx, radiusy, segments) -- Draw ellipselove.graphics.arc(mode, arctype, x, y, radius, angle1, angle2) -- Draw pie/slice shape
Images & Quads (Spritesheet Cutting)
Loading large assets is computationally expensive. Load them inside love.load once, and render them in love.draw.
local player_spritelocal sheet_imagelocal sprite_quadfunction love.load() -- Load full image player_sprite = love.graphics.newImage("assets/player.png") -- Load spritesheet sheet_image = love.graphics.newImage("assets/spritesheet.png") -- Slice Spritesheet: newQuad(x, y, quad_width, quad_height, sheet_width, sheet_height) sprite_quad = love.graphics.newQuad(32, 0, 32, 32, sheet_image:getDimensions())endfunction love.draw() -- Drawing a normal Image: draw(image, x, y, rotation, scale_x, scale_y, offset_x, offset_y) love.graphics.draw(player_sprite, 100, 100, 0, 2, 2) -- Scaled 2x -- Drawing a Quad: draw(sheet, quad, x, y, rotation, scale_x, scale_y, offset_x, offset_y) -- Offset parameters act as origin points (anchor) for scaling/rotation love.graphics.draw(sheet_image, sprite_quad, 200, 200, 0, 1, 1, 16, 16) -- Centered rotation originend
Fonts & Text Rendering
local default_fontlocal large_fontfunction love.load() -- Default System Font default_font = love.graphics.newFont(12) -- Custom TrueType Font (.ttf or .otf) large_font = love.graphics.newFont("assets/Outfit-Bold.ttf", 32)endfunction love.draw() -- Set active font love.graphics.setFont(large_font) love.graphics.setColor(1, 1, 0, 1) -- Yellow -- Basic Print: print(text, x, y, rotation, scale_x, scale_y, offset_x, offset_y) love.graphics.print("GAME OVER", 300, 100) -- Advanced Wrapped Text: printf(text, x, y, limit_width, align, rotation, scale_x, scale_y) love.graphics.setFont(default_font) love.graphics.setColor(1, 1, 1, 1) love.graphics.printf("This is a long description text that will wrap automatically when reaching the limit width.", 200, 200, 400, "center")end
Coordinate Transformations (Camera / Matrices)
LÖVE provides push/pop functions to manipulate the active drawing coordinate matrix, enabling custom viewport manipulation, translations, rotations, and cameras.
function love.draw() love.graphics.push() -- Save current transformation state -- Transform coordinate space to simulate camera love.graphics.translate(-camera_x, -camera_y) -- Pan love.graphics.scale(camera_zoom) -- Zoom love.graphics.rotate(camera_rotation) -- Rotate camera view -- Draw world objects in transformed coordinate space love.graphics.rectangle("fill", player_x, player_y, 32, 32) love.graphics.pop() -- Restore base transformation state -- Draw static HUD / UI elements (not affected by camera translation) love.graphics.print("Score: " .. score, 10, 10)end
Canvases (Render Targets)
A Canvas (or Framebuffer) is an offscreen texture that can be rendered to instead of rendering directly to the screen. Useful for screen scaling, post-processing, and shader pipelines.
local canvasfunction love.load() -- Create canvas matching native virtual game resolution (320x180 retro) canvas = love.graphics.newCanvas(320, 180) canvas:setFilter("nearest", "nearest") -- Pixel art scaling filterendfunction love.draw() -- Redirect draw calls to canvas love.graphics.setCanvas(canvas) love.graphics.clear(0, 0, 0, 1) -- Clear canvas black -- Draw pixel art game elements love.graphics.setColor(1, 1, 1, 1) love.graphics.rectangle("fill", 20, 20, 50, 50) -- Reset canvas redirection back to main monitor frame buffer love.graphics.setCanvas() -- Render canvas to screen, stretching it to fit the full window size local window_w, window_h = love.graphics.getDimensions() local scale_x = window_w / 320 local scale_y = window_h / 180 love.graphics.draw(canvas, 0, 0, 0, scale_x, scale_y)end
Input Management
Keyboard Polling
function love.update(dt) -- Check if specific key is currently held down if love.keyboard.isDown("right", "d") then player_x = player_x + speed * dt end if love.keyboard.isDown("left", "a") then player_x = player_x - speed * dt endend
Mouse Polling & Cursor
function love.update(dt) -- Get current mouse coordinates local mx, my = love.mouse.getPosition() -- Poll button states (1=Left, 2=Right, 3=Middle) if love.mouse.isDown(1) then spray_particles(mx, my) endendfunction love.load() -- Hide OS mouse cursor love.mouse.setVisible(false) -- Load custom hardware system cursor custom_cursor = love.mouse.newCursor("assets/crosshair.png", 8, 8) love.mouse.setCursor(custom_cursor)end
Gamepad / Joystick Controller Support
Tightly integrated with the SDL Game Controller API for standardized mappings across Xbox, PlayStation, and Nintendo switch pads.
local joysticks = {}local active_controller = nilfunction love.load() -- Scan for connected joysticks/gamepads joysticks = love.joystick.getJoysticks() if #joysticks > 0 then active_controller = joysticks[1] endend-- Gamepad event callbacksfunction love.gamepadpressed(joystick, button) if button == "a" then player:jump() endendfunction love.update(dt) if active_controller and active_controller:isGamepad() then -- Read analog sticks (range -1.0 to 1.0) local left_x = active_controller:getGamepadAxis("leftx") local left_y = active_controller:getGamepadAxis("lefty") -- Deadzone handling if math.abs(left_x) > 0.15 then player_x = player_x + left_x * speed * dt end endend
Audio & Sound Effects
Source Types: Static vs Stream
Static: Audio data is loaded fully into RAM. Perfect for short, recurring sound effects (SFX) like jumps, laser shots, and impacts to prevent disk read overhead.
Stream: Audio data is loaded in small chunks from disk dynamically. Essential for long music tracks or background ambiance (BGM) to save system memory.
source:play() -- Play sourcesource:pause() -- Pause sourcesource:stop() -- Stop playback (resets to beginning)source:setVolume(val) -- Set volume (0.0 to 1.0)source:setPitch(val) -- Set pitch scaling (0.5 to 2.0)source:setLooping(b) -- Set looping state (true / false)source:isPlaying() -- Return true if currently playingsource:tell() -- Get current playback position (seconds)source:seek(sec) -- Skip to position in seconds
Physics System (Box2D Integration)
Rigidbodies, Shapes, and Fixtures
LÖVE wraps the high-performance Box2D C++ physics engine internally.
local worldlocal player_bodylocal player_shapelocal player_fixturelocal ground_bodylocal ground_shapelocal ground_fixturefunction love.load() -- 1. Create a physics world with Gravity (x, y) and sleep option -- Gravity points downward along y-axis at 9.81 m/s^2 (multiplied by pixel scaling) love.physics.setMeter(64) -- scale factor: 64 pixels equals 1 meter world = love.physics.newWorld(0, 9.81 * 64, true) -- 2. Create the Static Ground ground_body = love.physics.newBody(world, 400, 500, "static") -- Body types: "static", "dynamic", "kinematic" ground_shape = love.physics.newRectangleShape(800, 50) -- dimensions: width, height ground_fixture = love.physics.newFixture(ground_body, ground_shape) -- 3. Create the Dynamic Player player_body = love.physics.newBody(world, 400, 100, "dynamic") player_body:setMass(1) player_shape = love.physics.newCircleShape(20) -- circle with radius 20 pixels player_fixture = love.physics.newFixture(player_body, player_shape, 1.0) -- body, shape, density player_fixture:setRestitution(0.4) -- Bounce factor (0 = none, 1 = maximum bounce) player_fixture:setFriction(0.2) -- Slide friction coefficientendfunction love.update(dt) -- Step the physics world (update positions based on forces) world:update(dt)endfunction love.draw() -- Draw Ground love.graphics.setColor(0.5, 0.5, 0.5, 1) love.graphics.polygon("fill", ground_body:getWorldPoints(ground_shape:getPoints())) -- Draw Player love.graphics.setColor(0, 1, 0.5, 1) -- Circle positions are updated by the Box2D simulation engine automatically love.graphics.circle("fill", player_body:getX(), player_body:getY(), player_shape:getRadius())end
Triggered automatically by the physics solver when fixtures begin and end overlapping.
local function beginContact(fixture_a, fixture_b, contact) local body_a = fixture_a:getBody() local body_b = fixture_b:getBody() print("Collision detected between body: " .. tostring(body_a) .. " and " .. tostring(body_b))endlocal function endContact(fixture_a, fixture_b, contact) -- Cleanup overlap statesendfunction love.load() -- Set event handlers inside the world solver world:setCallbacks(beginContact, endContact, nil, nil)end
Filesystem & Persistence
Safe Save Directory
For security, LÖVE sandboxes the filesystem. You cannot write files randomly to system paths.
Read access is permitted inside the project folder (res://).
Write access is restricted purely inside a dedicated system app data folder (user://).
Folder path: C:\Users\Username\AppData\Roaming\LOVE\identity_name (Windows) or ~/Library/Application Support/LOVE/identity_name (macOS).
Reading & Writing Files
-- Write file to user save directoryfunction save_game_score(score) local data = tostring(score) local success, message = love.filesystem.write("score.txt", data) if success then print("Game saved successfully") else print("Write failed: " .. message) endend-- Read filefunction load_game_score() if love.filesystem.getInfo("score.txt") then local contents, size = love.filesystem.read("score.txt") return tonumber(contents) end return 0 -- Default scoreend
Shaders (GLSL)
Vertex and Pixel Shaders in LÖVE
LÖVE uses custom dialect matching OpenGL ES 2.0 / 3.0 shading languages (GLSL).