Getting Started with LOVE 2D: Installation and First Game
Getting started with LOVE 2D is one of the easiest ways to build real games. LOVE (also written as LÖVE or LOVE2D) is a free, open-source framework for building 2D games using Lua, and it is a fantastic starting point if you want to create games without dealing with complex engine setups. Think of it like a toy box full of building blocks specifically designed for making games.
Prerequisites
You need a computer running Windows, macOS, or Linux. No prior game development experience is required, but you should be comfortable editing plain text files and using a terminal or command prompt. Knowing the basics of Lua syntax (variables, functions, tables) will help you follow the code examples, though the tutorial explains each snippet as it appears.
What is LOVE 2D?
LOVE is a game engine. An engine handles the boring but necessary stuff: opening a window, drawing graphics, detecting key presses, playing sounds. Instead of building these from scratch, you tell LOVE what you want and it handles the rest.
Here is why people like LOVE:
- Lightweight: the entire engine is just a few megabytes
- Cross-platform: your games run on Windows, macOS, and Linux
- Lua-based: you write games in Lua, which is one of the easiest programming languages to learn
- Free and open: no cost, no license worries, and you can see how it works under the hood
Installing LOVE 2D
The installation process depends on your operating system.
Windows
- Go to love2d.org and download the latest installer
- Run the installer (choose 32-bit or 64-bit; if you are unsure, 64-bit is probably right for modern computers)
- Optional: Add LOVE to your system PATH so you can run it from any command prompt
To check if it worked, open Command Prompt and type:
love
If you see a window pop up with the LOVE launcher, you are all set. The launcher is a simple screen with the LOVE logo that confirms the engine loaded correctly. If nothing appears or you get a “command not found” error, verify that LOVE is in your PATH or restart your terminal.
macOS
You have two options:
Option 1: DMG file
- Download the
.dmgfile from love2d.org - Drag the LOVE app into your Applications folder
Option 2: Homebrew If you use Homebrew, run:
brew install love
To verify, open Terminal and type love. You should see the launcher window. The Homebrew approach is usually faster because it also handles updates when you run brew upgrade.
Linux
The exact command depends on your distribution:
Debian or Ubuntu:
sudo apt install love
Different Linux distributions use different package managers, but the love package name is consistent across all of them. Debian and Ubuntu use APT, which is the most widely used Linux package manager. If the apt command reports that the love package was not found, run sudo apt update first to refresh the package index. After installation completes, the love executable is available system-wide.
Fedora:
sudo dnf install love
Fedora uses the DNF package manager, which handles dependency resolution differently from APT. The love package in Fedora’s repositories is typically the same LOVE 11.x series, but the package name and dependencies may differ from Debian-based distributions. After installation, the love binary lands in /usr/bin and is available from any terminal.
Arch Linux:
sudo pacman -S love
Arch users get the latest LOVE package from the community repository, which tends to track upstream releases more closely than Debian or Fedora. Pacman is a simpler package manager than APT or DNF and does not automatically refresh its package database unless you run sudo pacman -Sy first. If the install command fails with a “package not found” error, run sudo pacman -Sy to update the package list and try again.
If you are on a different distribution, check your package manager for the love package. Some distributions only package older versions, so you might need to download the AppImage from love2d.org instead. The AppImage is a self-contained executable that does not require installation: download it, make it executable with chmod +x, and run it directly.
Run love in your terminal to verify the installation worked.
Your first LOVE 2D game
Now let us write some game code. Every LOVE project starts the same way: a folder with a single Lua file inside. The structure is deliberately minimal so you can go from an empty directory to a running game in under a minute.
File structure
LOVE expects your main game file to be called main.lua. That is it. No complex folder structure, no configuration files needed. Just one file to start.
Create a new folder for your project, then create a file inside called main.lua:
mygame/
└── main.lua
This directory layout is the only hard requirement LOVE imposes. Everything else (subdirectories for assets, additional Lua modules, configuration) is optional and can be added as your project grows.
The game loop
Every LOVE game has three main functions that form what we call the “game loop”: the heartbeat of your game that runs continuously while the game is running. Understanding the relationship between these three functions is the single most important concept in LOVE programming.
Think of these functions like a play in a theater:
love.load(): the preparation before the play starts (runs once)love.update(): what happens on stage during each moment of the play (every frame)love.draw(): taking a photograph of the stage (every frame)
Here is what each one does in practice:
-- main.lua
-- love.load runs once when your game starts
-- Use it to set up your initial game state
function love.load()
-- x and y store the position of our square
x = 400
y = 300
end
-- love.update runs every frame (typically 60 times per second)
-- This is where you update game logic like movement
-- dt stands for "delta time" - the seconds since the last frame
function love.update(dt)
-- Move the square 100 pixels to the right every second
-- Multiplying by dt ensures consistent speed regardless of frame rate
x = x + 100 * dt
end
-- love.draw runs every frame after update
-- This is where you put all your rendering code
function love.draw()
-- Draw a red filled rectangle
-- Arguments: mode, x, y, width, height
-- Note: x and y are the TOP-LEFT corner of the rectangle
-- We're offsetting by 25 to center the 50x50 square on our position
love.graphics.setColor(1, 0, 0) -- RGB values from 0 to 1
love.graphics.rectangle("fill", x - 25, y - 25, 50, 50)
end
The dt parameter is the key to frame-rate independence. Imagine two computers: one is slow and runs your game at 30 frames per second, another is fast and runs it at 60 frames per second. Without dt, the fast computer would move objects twice as fast. By multiplying movement by dt, both computers move objects at the same speed. The slow one just takes more frames to get there. A common mistake beginners make is hardcoding movement values like x = x + 1, which creates games that feel broken on different hardware.
The coordinate system
Before we go further, it is important to understand LOVE’s coordinate system. The origin point (0, 0) is at the top-left corner of the window. Positive X extends to the right, and positive Y extends downward. This is different from some other engines where Y goes upward, so if you are coming from a math background or a platform like Unity, the inverted Y axis can take a moment to adjust to.
When you draw shapes, the x and y coordinates you provide determine where they are positioned based on this system. For example, to place a sprite in the centre of an 800x600 window, use x = 400, y = 300.
Creating a window
By default, LOVE creates a window that is 800 pixels wide and 600 pixels tall. You might want something different, so let us see how to customize it. LOVE gives you two ways to set window properties, and each serves a different purpose.
The recommended way is to create a separate file called conf.lua:
-- conf.lua
function love.conf(t)
-- t is a table that holds all configuration options
t.window.title = "My First Game" -- Window title
t.window.width = 1024 -- Width in pixels
t.window.height = 768 -- Height in pixels
t.window.resizable = true -- Let user resize the window
t.window.fullscreen = false -- Start in windowed mode
t.window.vsync = true -- Sync with monitor refresh rate
end
The conf.lua file runs before anything else, including love.load, so it is the right place for settings that must be in place before the game window appears. LOVE looks for this file automatically and calls love.conf if it exists.
You can also set the window size in code using love.window.setMode, which gives you the ability to change resolution at runtime:
function love.load()
-- Arguments: width, height, and optional settings table
love.window.setMode(1024, 768, {
fullscreen = false,
resizable = true,
vsync = true
})
end
Use conf.lua when you want configuration that is set before the game starts. Use love.window.setMode() when you need to change settings dynamically during gameplay, such as toggling fullscreen in response to a key press or adapting the window size after the player resizes it.
Drawing shapes
Let us draw some shapes. LOVE provides several functions in the love.graphics module, and you can combine them to create everything from simple debug visualisations to complex scene compositions. The colour system uses values from 0 to 1 instead of the more common 0-255: 0 means “none of this colour” and 1 means “maximum”. So (1, 0, 0) is pure red, (0, 1, 0) is pure green, (0, 0, 1) is pure blue, and (1, 1, 1) is white.
function love.draw()
-- Draw a filled rectangle
-- "fill" means solid, "line" would be just the outline
love.graphics.rectangle("fill", 100, 100, 200, 150)
-- Draw an outlined rectangle
love.graphics.rectangle("line", 100, 100, 200, 150)
-- Draw a filled circle
-- Arguments: x, y, radius, segments (higher = smoother circle)
love.graphics.circle("fill", 400, 300, 50, 32)
-- Draw an outlined circle
love.graphics.circle("line", 400, 300, 50, 32)
-- Draw a line between two points
love.graphics.line(0, 0, 800, 600)
-- Draw a triangle (polygon with 3 sides)
-- Pass a table of x,y coordinates for each vertex
love.graphics.polygon("fill", {
500, 200, -- vertex 1 (x, y)
600, 350, -- vertex 2 (x, y)
500, 350 -- vertex 3 (x, y)
})
-- Set color before drawing (RGB + optional Alpha)
-- All values range from 0 to 1
love.graphics.setColor(0.2, 0.5, 0.8, 1) -- light blue
end
The setColor call affects every draw operation that follows it until you call setColor again or the current frame ends. This means you must set the colour explicitly before each group of shapes if you want different colours, or everything ends up sharing the last colour applied. A useful pattern is to wrap colour changes around related drawing calls so you never forget to reset.
Handling keyboard input
Now let us make your game interactive. LOVE provides several ways to detect key presses, and choosing the right one depends on whether you want continuous movement or discrete actions.
The simplest is love.keyboard.isDown(), which checks if a key is currently held down. This is perfect for movement where you want the player to keep walking as long as the arrow key is pressed:
function love.update(dt)
local speed = 200 * dt
-- Check if arrow keys are held down
if love.keyboard.isDown("left") then
x = x - speed
end
if love.keyboard.isDown("right") then
x = x + speed
end
if love.keyboard.isDown("up") then
y = y - speed
end
if love.keyboard.isDown("down") then
y = y + speed
end
end
For detecting individual key presses (like firing a weapon once when a key goes down, or quitting on Escape), use the love.keypressed callback instead. This callback fires exactly once per key press, no matter how long you physically hold the key down:
function love.keypressed(key)
if key == "escape" then
-- Quit the game when escape is pressed
love.event.quit()
end
if key == "space" then
-- Do something when space is pressed
print("Space pressed!")
end
end
Combining both approaches gives you full control: use isDown for smooth movement and keypressed for one-shot actions like jumping, menu navigation, or firing a projectile. Once you have movement working, the next logical step is detecting when your player hits something, which is covered in the Collision Detection in LOVE 2D tutorial.
Loading images
Games need graphics beyond just shapes. Here is how you load and display images from files placed alongside your main.lua:
function love.load()
-- Load an image from a file
-- Make sure you have a "player.png" in your game folder
playerImage = love.graphics.newImage("player.png")
x = 400
y = 300
end
function love.draw()
-- Draw the image at position x, y
-- The third argument is rotation in radians
love.graphics.draw(playerImage, x, y, 0)
-- You can also specify the origin point (center of the image)
-- love.graphics.draw(playerImage, x, y, 0, 1, 1, playerImage:getWidth() / 2, playerImage:getHeight() / 2)
end
Place your image files in the same folder as your main.lua, and LOVE will automatically find them. For larger projects, a common convention is to create an assets/ or images/ subdirectory and adjust the path accordingly: love.graphics.newImage("images/player.png"). LOVE supports PNG, JPG, GIF, BMP, TGA, and HDR formats, with PNG being the recommended choice for sprites because it supports transparency through the alpha channel.
Running your game
Run your game from the terminal by pointing LOVE at your project folder:
love /path/to/your/game/folder
For example, if your game lives in a folder called “mygame” on your Windows desktop, you would run the following command. The path format differs between operating systems, so adjust the slashes and drive letters accordingly for your setup:
love C:\Users\YourName\Desktop\mygame
You can also run LOVE on a .zip file containing your game files, and LOVE will automatically find and run the main.lua inside. This is how you distribute finished games: zip up your project folder, rename it from .zip to .love, and share the resulting file.
Common setup problems
Let us cover some issues that often trip up beginners when they first start working with LOVE. These are the four most frequent problems seen in LOVE community forums.
Wrong filename
Problem: Nothing happens when you run LOVE.
Solution: Check that your entry point is exactly main.lua (lowercase, with the .lua extension). LOVE will not find files named Main.lua, main.LUA, or anything else. The engine does a case-sensitive search for main.lua in the project root, and if the filename differs by even one character, LOVE shows the default “no game” screen instead.
Working directory issues
Problem: You get errors about missing files, or assets do not load.
Solution: Always run LOVE from your game folder, not from somewhere else. The simplest way is to navigate to your game folder first, then run love . (the dot means “current folder”). If you run love /path/to/folder from a different directory, the working directory is where you typed the command, not the project folder. This matters because asset paths in love.graphics.newImage are relative to the working directory.
Frame-rate dependent movement
Problem: Your game runs faster on faster computers.
Solution: Always multiply movement by dt in your love.update function. The difference between hardcoded and dt-based movement is invisible on your own machine but immediately obvious to anyone with a different refresh rate:
-- This runs at different speeds on different computers
x = x + 5
-- This runs at the same speed everywhere
x = x + 200 * dt
The physical interpretation is simple: if you want to move at 200 pixels per second, multiply 200 by dt (the fraction of a second since the last frame). On a 60 Hz monitor, dt averages about 0.0167 seconds, so you get roughly 3.3 pixels per frame.
Drawing outside love.draw
Problem: Graphics do not appear, or they flicker wildly.
Solution: LOVE only draws what you put inside the love.draw() function. If you try to draw elsewhere, it either will not show up or will behave unpredictably. Keep all rendering code in love.draw(). If you need to prepare data for drawing (calculating positions, loading assets), do that in love.load or love.update, but call the actual love.graphics functions only inside love.draw.
Version information
The current stable version of LOVE is 11.x (nicknamed “Mysterious Mysteries”). It supports Lua versions 5.1, 5.2, 5.3, and 5.4, depending on which version of LOVE you use.
You can check what version you are running in code:
function love.load()
local v = love.getVersion()
print(string.format("LOVE version: %d.%d.%d", v.major, v.minor, v.revision))
-- Prints something like "LOVE version: 11.5.0"
end
Next steps
Now that you have LOVE running and your first shapes on screen, the natural next step is to start building a real game. The Collision Detection in LOVE 2D tutorial picks up right where this one leaves off, showing you how to make objects interact. If you want to add images and animations instead, jump to Drawing Animation and Sprites in LOVE 2D to learn about sprite sheets, quads, and frame-by-frame animation.
See also
- Collision Detection in LOVE 2D: making objects interact with AABB, circle, and tile-based detection
- Drawing Animation and Sprites in LOVE 2D: loading images and animating sprites with quads
- love2d.org: official website and downloads
- LOVE Wiki: complete function reference