File-system based scene routing for LÖVE 2D games inspired by Next.js
- 📁 File-system based routing - Create scenes by simply adding files to your scenes directory
- 🔀 Dynamic routes - Support for parameterized routes like
[id].lua - 📱 Layout system - Wrap scenes with reusable layouts
- 🎯 Simple API - Easy to integrate into existing LÖVE 2D projects
- 🔧 Configurable - Customize scenes directory and behavior
luarocks install love-scenesInstall directly from GitHub:
luarocks install --server=https://luarocks.org/manifests/zhuravkovigor love-scenesOr clone and install manually:
git clone https://github.com/zhuravkovigor/love-scenes.git
cd love-scenes
luarocks make love-scenes-1.0-1.rockspec- Download the latest release from GitHub Releases
- Extract the files to your project directory
- Require the library in your
main.lua
Clone the repository and install locally:
git clone https://github.com/zhuravkovigor/love-scenes.git
cd love-scenes
make install-- main.lua
local LoveScenes = require('love-scenes')
function love.load()
-- Initialize with default settings
LoveScenes.init()
-- Navigate to the main scene
LoveScenes.navigate('/')
end
function love.update(dt)
LoveScenes.update(dt)
end
function love.draw()
LoveScenes.draw()
end
-- Forward input events
function love.keypressed(key, scancode, isrepeat)
LoveScenes.keypressed(key, scancode, isrepeat)
end-- scenes/index.lua (Main menu at route "/")
local scene = {}
function scene:load(params)
self.title = "My Awesome Game"
end
function scene:update(dt)
-- Scene update logic
end
function scene:draw()
love.graphics.printf(self.title, 0, 100, love.graphics.getWidth(), "center")
end
function scene:keypressed(key)
if key == "space" then
require('love-scenes').navigate('/game')
end
end
return scene-- scenes/game/index.lua (Game scene at route "/game")
local scene = {}
function scene:load(params)
self.player = {x = 100, y = 100}
end
function scene:update(dt)
if love.keyboard.isDown("left") then
self.player.x = self.player.x - 100 * dt
end
if love.keyboard.isDown("right") then
self.player.x = self.player.x + 100 * dt
end
end
function scene:draw()
love.graphics.circle("fill", self.player.x, self.player.y, 20)
end
return sceneLove Scenes uses a file-system based routing approach similar to Next.js:
scenes/
├── index.lua # Route: /
├── layout.lua # Layout for all scenes
├── game/
│ ├── index.lua # Route: /game
│ └── layout.lua # Layout for /game/* routes
├── level/
│ └── [level].lua # Route: /level/1-1, /level/forest, etc.
├── profile/
│ └── [id].lua # Route: /profile/123, /profile/abc, etc.
├── shop/
│ └── [category].lua # Route: /shop/weapons, /shop/armor, etc.
└── settings/
├── index.lua # Route: /settings
└── audio/
└── index.lua # Route: /settings/audio
scenes/index.lua→/(root)scenes/about/index.lua→/aboutscenes/game/level/index.lua→/game/level
scenes/user/[id].lua→/user/123,/user/abcscenes/level/[level].lua→/level/1-1,/level/forestscenes/shop/[category].lua→/shop/weapons,/shop/armorscenes/post/[slug]/[id].lua→/post/hello-world/123
-- scenes/level/[level].lua
local scene = {}
function scene:load(params)
self.levelId = params.level -- Access the dynamic parameter
print("Loading level:", self.levelId)
-- Different logic based on level
if self.levelId == "boss-1" then
self:loadBossLevel()
else
self:loadNormalLevel()
end
end
return sceneLayouts wrap scenes and provide common UI elements:
-- scenes/layout.lua (Root layout for all scenes)
local layout = {}
function layout:draw(drawScene)
-- Draw header
love.graphics.setColor(0.2, 0.2, 0.2)
love.graphics.rectangle("fill", 0, 0, love.graphics.getWidth(), 60)
-- Draw scene content
love.graphics.push()
love.graphics.translate(0, 60)
drawScene() -- This renders the current scene
love.graphics.pop()
-- Draw footer
love.graphics.setColor(0.2, 0.2, 0.2)
love.graphics.rectangle("fill", 0, love.graphics.getHeight() - 40,
love.graphics.getWidth(), 40)
end
return layoutNavigate between scenes using the navigate function:
local LoveScenes = require('love-scenes')
-- Navigate to different routes
LoveScenes.navigate('/')
LoveScenes.navigate('/game')
LoveScenes.navigate('/profile/123')
LoveScenes.navigate('/settings/audio')
-- Navigate with additional parameters
LoveScenes.navigate('/user/123', {tab = "settings"})All configuration parameters are optional. Love Scenes works out of the box with sensible defaults:
-- Minimal setup - all parameters are optional
LoveScenes.init()
-- With custom configuration
LoveScenes.init({
scenesPath = "scenes", -- Directory containing scenes (default: "scenes")
autoLoad = true, -- Automatically load scenes on init (default: true)
enableLayouts = true, -- Enable layout system (default: true)
debugMode = false -- Enable debug logging (default: false)
})| Parameter | Type | Default | Description |
|---|---|---|---|
scenesPath |
string | "scenes" |
Directory containing your scene files |
autoLoad |
boolean | true |
Automatically scan and load scenes on initialization |
enableLayouts |
boolean | true |
Enable the layout system for wrapping scenes |
debugMode |
boolean | false |
Enable debug logging to console |
Scenes have several lifecycle methods:
local scene = {}
function scene:load(params)
-- Called when scene is created and loaded
-- Access route parameters via params
-- Initialize scene data, load assets, set up state
end
function scene:onEnter(next)
-- Called when navigating to this scene
-- next is an optional callback for controlling transition timing
if next then
-- Perform any animations or async setup
-- Call next() when ready for the scene to become active
next()
end
end
function scene:onLeave(next)
-- Called when leaving this scene
-- next is an optional callback for controlling transition timing
if next then
-- Perform cleanup animations
-- Call next() when ready to complete the transition
next()
end
end
function scene:update(dt)
-- Called every frame
end
function scene:draw()
-- Called every frame for rendering
end
-- LÖVE 2D callbacks are automatically forwarded
function scene:keypressed(key, scancode, isrepeat)
-- Handle input
end
function scene:mousepressed(x, y, button, isTouch, presses)
-- Handle mouse input
end
return scenelocal scene = {}
local fade_alpha = 1
function scene:onEnter(next)
if next then
-- Start fade-in animation
fade_alpha = 0
-- Don't block transition, let it proceed immediately
next()
else
fade_alpha = 1
end
end
function scene:update(dt)
-- Simple fade-in animation
if fade_alpha < 1 then
fade_alpha = math.min(1, fade_alpha + dt * 2)
end
end
function scene:draw()
love.graphics.push()
love.graphics.setColor(1, 1, 1, fade_alpha)
-- Draw scene content with fade effect
love.graphics.printf("Scene Content", 0, 100, love.graphics.getWidth(), "center")
love.graphics.pop()
end
return sceneLayouts also have lifecycle methods:
local layout = {}
function layout:load()
-- Called when layout is created
end
function layout:onEnter(scene, next)
-- Called when a scene using this layout is entered
-- scene: the scene that will be rendered
-- next: optional callback for controlling transition timing
if next then
next()
end
end
function layout:onLeave(next)
-- Called when leaving this layout
-- next: optional callback for controlling transition timing
if next then
next()
end
end
-- Called when leaving this layout
end
function layout:update(dt)
-- Called every frame before scene update
end
function layout:draw(drawScene)
-- Called every frame for rendering
-- drawScene() renders the current scene
end
return layoutInitialize the library with optional configuration. All parameters are optional - the library works with sensible defaults.
Parameters:
config(table, optional): Configuration options. If omitted, uses default values.scenesPath(string, optional): Directory containing scenes (default: "scenes")autoLoad(boolean, optional): Automatically load scenes on init (default: true)enableLayouts(boolean, optional): Enable layout system (default: true)debugMode(boolean, optional): Enable debug logging (default: false)
Examples:
-- Minimal setup
LoveScenes.init()
-- With custom scenes directory
LoveScenes.init({ scenesPath = "game-scenes" })
-- With debug mode
LoveScenes.init({ debugMode = true })Navigate to a scene.
Parameters:
path(string): Route path (e.g., "/", "/game", "/user/123")params(table, optional): Additional parameters to pass to the scene
Get the current active scene instance.
Get the current active layout instance.
Here's a complete example of a simple game with multiple scenes:
-- main.lua
local LoveScenes = require('love-scenes')
function love.load()
LoveScenes.init({
debugMode = true -- Enable debug logging
})
-- Start at main menu
LoveScenes.navigate('/')
end
function love.update(dt)
LoveScenes.update(dt)
end
function love.draw()
LoveScenes.draw()
end
-- Forward all LÖVE callbacks
function love.keypressed(key, scancode, isrepeat)
LoveScenes.keypressed(key, scancode, isrepeat)
end
function love.mousepressed(x, y, button, isTouch, presses)
LoveScenes.mousepressed(x, y, button, isTouch, presses)
end-- scenes/index.lua (Main Menu)
local LoveScenes = require('love-scenes')
local scene = {}
function scene:load()
self.title = "My Awesome Game"
self.menuItems = {"Start Game", "Settings", "Quit"}
self.selectedIndex = 1
end
function scene:update(dt)
-- Menu logic here
end
function scene:draw()
-- Draw title
love.graphics.setFont(love.graphics.newFont(32))
love.graphics.printf(self.title, 0, 100, love.graphics.getWidth(), "center")
-- Draw menu items
love.graphics.setFont(love.graphics.newFont(16))
for i, item in ipairs(self.menuItems) do
local y = 200 + (i - 1) * 40
local color = i == self.selectedIndex and {1, 1, 0} or {1, 1, 1}
love.graphics.setColor(color)
love.graphics.printf(item, 0, y, love.graphics.getWidth(), "center")
end
love.graphics.setColor(1, 1, 1) -- Reset color
end
function scene:keypressed(key)
if key == "up" then
self.selectedIndex = math.max(1, self.selectedIndex - 1)
elseif key == "down" then
self.selectedIndex = math.min(#self.menuItems, self.selectedIndex + 1)
elseif key == "return" then
if self.selectedIndex == 1 then
LoveScenes.navigate('/game')
elseif self.selectedIndex == 2 then
LoveScenes.navigate('/settings')
elseif self.selectedIndex == 3 then
love.event.quit()
end
end
end
return scene-- scenes/game/index.lua (Game Scene)
local LoveScenes = require('love-scenes')
local scene = {}
function scene:load()
self.player = {
x = 400,
y = 300,
speed = 200
}
self.enemies = {}
self.score = 0
end
function scene:update(dt)
-- Player movement
if love.keyboard.isDown("left", "a") then
self.player.x = self.player.x - self.player.speed * dt
end
if love.keyboard.isDown("right", "d") then
self.player.x = self.player.x + self.player.speed * dt
end
if love.keyboard.isDown("up", "w") then
self.player.y = self.player.y - self.player.speed * dt
end
if love.keyboard.isDown("down", "s") then
self.player.y = self.player.y + self.player.speed * dt
end
-- Keep player on screen
self.player.x = math.max(20, math.min(love.graphics.getWidth() - 20, self.player.x))
self.player.y = math.max(20, math.min(love.graphics.getHeight() - 20, self.player.y))
end
function scene:draw()
-- Draw player
love.graphics.setColor(0, 1, 0) -- Green
love.graphics.circle("fill", self.player.x, self.player.y, 20)
-- Draw UI
love.graphics.setColor(1, 1, 1) -- White
love.graphics.print("Score: " .. self.score, 10, 10)
love.graphics.print("Press ESC to return to menu", 10, 30)
end
function scene:keypressed(key)
if key == "escape" then
LoveScenes.navigate('/')
end
end
return scene-- scenes/profile/[id].lua (Dynamic User Profile)
local scene = {}
function scene:load(params)
self.userId = params.id
self.userInfo = self:loadUserInfo(self.userId)
end
function scene:loadUserInfo(id)
-- Simulate loading user data
return {
name = "User " .. id,
level = math.random(1, 100),
score = math.random(1000, 99999)
}
end
function scene:draw()
love.graphics.printf("User Profile", 0, 50, love.graphics.getWidth(), "center")
love.graphics.printf("ID: " .. self.userId, 0, 100, love.graphics.getWidth(), "center")
love.graphics.printf("Name: " .. self.userInfo.name, 0, 130, love.graphics.getWidth(), "center")
love.graphics.printf("Level: " .. self.userInfo.level, 0, 160, love.graphics.getWidth(), "center")
love.graphics.printf("Score: " .. self.userInfo.score, 0, 190, love.graphics.getWidth(), "center")
love.graphics.printf("Press ESC to go back", 0, 250, love.graphics.getWidth(), "center")
end
function scene:keypressed(key)
if key == "escape" then
require('love-scenes').navigate('/')
end
end
return sceneNow you can navigate to different user profiles:
LoveScenes.navigate('/profile/123')LoveScenes.navigate('/profile/player1')LoveScenes.navigate('/profile/admin')
- Keep related scenes in subdirectories (e.g.,
scenes/game/,scenes/menu/) - Use descriptive names for dynamic routes:
[playerId].lua,[levelName].lua
- Initialize all scene data in the
load()method - Use
onEnter()andonLeave()for cleanup and transitions - Store global state outside of individual scenes if needed
- Preload assets in
load()method - Use
onLeave()to clean up resources - Consider using layouts for shared UI elements
- Use absolute paths for navigation:
/game,/settings/audio - Pass additional data via the params parameter
- Handle navigation errors gracefully
Check out the scenes/ directory in this repository for complete examples including:
- Main menu with navigation
- Game scene with player movement
- Settings scene with configurable options
- Dynamic profile scenes with URL parameters
- Layout system with header and footer
Contributions are welcome! Please feel free to submit a Pull Request.
MIT License - see LICENSE file for details.