gamedev-skills/awesome-gamedev-agent-skills검사 통과
SKILL DETAIL
love2d-core
gamedev-skills/awesome-gamedev-agent-skills/love2d-core
Structure and debug a LÖVE (Love2D) game in Lua: the love.load/update/draw loop, delta-time movement, input, and screen states. Use when building a LÖVE 11.x game (main.lua, conf.lua, .love).
설치 수 · 237출처 보기
Installation
npx skills add https://github.com/gamedev-skills/awesome-gamedev-agent-skills --skill love2d-core
스킬 파일
SKILL.md
최근 동기화 · 2026. 10. 25.
references/state-stack.md›
# Screen state stack (LÖVE 11.5)
A small, complete screen manager: menu → game → pause, with the active screen on a stack so
overlays (pause on top of game) are trivial. Each *screen* is a plain Lua table with any of
the optional methods `enter`, `leave`, `update(dt)`, `draw`, `keypressed(key)`.
This is the full version of the brief snippet in `SKILL.md`. Drop `state_stack.lua` in your
project root and the screens under `screens/`.
## `state_stack.lua`
```lua
-- state_stack.lua — minimal screen/state manager for LÖVE 11.x.
-- A screen is a table that may define: enter, leave, update(dt), draw, keypressed(key).
local Stack = { screens = {} }
-- Add a screen on top (e.g. open a pause overlay above the game).
function Stack.push(screen)
table.insert(Stack.screens, screen)
if screen.enter then screen:enter() end
end
-- Remove and return the top screen (e.g. close the pause overlay).
function Stack.pop()
local screen = table.remove(Stack.screens)
if screen and screen.leave then screen:leave() end
return screen
end
-- Replace the top screen entirely (e.g. menu -> game).
function Stack.switch(screen)
if #Stack.screens > 0 then Stack.pop() end
Stack.push(screen)
end
function Stack.current()
return Stack.screens[#Stack.screens]
end
return Stack
```
## Wiring it in `main.lua`
Delegate each LÖVE callback to the top screen. The helper tolerates screens that omit a
method, so a screen only implements what it needs.
```lua
local Stack = require("state_stack")
-- Call method `name` on the top screen if it exists.
local function forward(name, ...)
local screen = Stack.current()
if screen and screen[name] then screen[name](screen, ...) end
end
function love.load() Stack.push(require("screens.menu")) end
function love.update(dt) forward("update", dt) end
function love.draw() forward("draw") end
function love.keypressed(k) forward("keypressed", k) end
```
### Drawing overlays (pause on top of the paused game)
To keep the game visible behind a pause screen, draw every screen bottom-to-top but only
update the top one:
```lua
function love.update(dt)
forward("update", dt) -- only the top screen updates
end
function love.draw()
for _, screen in ipairs(Stack.screens) do -- draw all, bottom to top
if screen.draw then screen:draw() end
end
end
```
## Example screens
```lua
-- screens/menu.lua
local menu = {}
function menu:draw()
love.graphics.setColor(1, 1, 1)
love.graphics.print("MENU — press Enter to play, Esc to quit", 20, 20)
end
function menu:keypressed(key)
if key == "return" then
require("state_stack").switch(require("screens.game"))
elseif key == "escape" then
love.event.quit()
end
end
return menu
```
```lua
-- screens/game.lua
local Stack = require("state_stack")
local game = {}
function game:enter()
self.player = { x = 100, y = 100, speed = 220 }
end
function game:update(dt)
if love.keyboard.isDown("right") then
self.player.x = self.player.x + self.player.speed * dt -- dt-scaled motion
end
end
function game:draw()
love.graphics.setColor(0.2, 0.8, 1.0)
love.graphics.rectangle("fill", self.player.x, self.player.y, 40, 40)
love.graphics.setColor(1, 1, 1)
love.graphics.print("P = pause, hold Right to move", 20, 20)
end
function game:keypressed(key)
if key == "p" then Stack.push(require("screens.pause")) end -- overlay, game stays beneath
end
return game
```
```lua
-- screens/pause.lua
local Stack = require("state_stack")
local pause = {}
function pause:draw()
-- Dim the game beneath, then label the overlay.
love.graphics.setColor(0, 0, 0, 0.5)
love.graphics.rectangle("fill", 0, 0, love.graphics.getWidth(), love.graphics.getHeight())
love.graphics.setColor(1, 1, 1)
love.graphics.print("PAUSED — press P to resume", 20, 60)
end
function pause:keypressed(key)
if key == "p" then Stack.pop() end -- return to the game underneath
end
return pause
```
## Notes
- `require("screens.menu")` returns the **same table** each call (Lua caches modules), so a
screen here is effectively a singleton. If you need fresh per-instance state, make the
module a factory: `return function() return setmetatable({}, Screen) end` and call it.
- Keep screen logic in `update`/`draw`; do not block in `enter`. Long loads belong in a
loading screen that yields across frames.
- This manager has no transitions/animation by design. Add a `fade` screen pushed on top that
draws a shrinking alpha rectangle if you want crossfades — the stack already supports it.
SKILL.md›
---
name: love2d-core
description: >
Structure and debug a LÖVE (Love2D) game in Lua: the love.load/update/draw loop,
delta-time movement, input, and screen states. Use when building a LÖVE 11.x game
(main.lua, conf.lua, .love).
---
# LÖVE (Love2D) Core
Set up and debug the foundation of a LÖVE game in Lua: the callback loop, frame-rate-
independent movement, input, and screen states. Targets **LÖVE 11.5**.
## When to use
- Use when starting a LÖVE game, wiring up `main.lua`/`conf.lua`, or fixing the core loop,
movement that runs at the wrong speed, input handling, or screen switching.
- Use when the workspace has `main.lua` calling `love.*`, a `conf.lua`, or a `.love` file.
**When *not* to use:** Lua *language* questions unrelated to LÖVE; physics bodies/joints
(LÖVE uses Box2D via `love.physics` — a separate concern); shader code (`love.graphics`
GLSL is its own topic). For cross-engine save/load patterns, use `save-systems`.
## Core workflow
1. **Confirm the entry points.** A LÖVE game runs `main.lua`; it should define
`love.load()` (one-time setup), `love.update(dt)` (state), and `love.draw()` (rendering).
Window/version setup goes in `conf.lua` (run *before* modules load).
2. **Pin the version.** Set `t.version = "11.5"` in `conf.lua` so LÖVE warns on mismatch.
3. **Drive all motion by `dt`** (delta time, in seconds) so speed is frame-rate independent.
4. **Handle input** two ways: polled (`love.keyboard.isDown` in `update`, for held keys) and
event (`love.keypressed` callback, for discrete presses).
5. **Manage screens** (menu, game, pause) with a small state stack rather than a pile of
`if` flags — see Patterns and `references/state-stack.md`.
6. **Run and observe.** Launch with `love .` from the project folder; verify the window,
motion speed, and input on screen before assuming it works.
## Patterns
### 1. `main.lua` skeleton (the callback loop + input)
```lua
-- main.lua — LÖVE calls these callbacks for you. Colors are 0–1 in LÖVE 11.x.
function love.load()
-- One-time setup. speed is in PIXELS PER SECOND, not per frame.
player = { x = 100, y = 100, size = 40, speed = 220 }
love.graphics.setBackgroundColor(0.1, 0.1, 0.12)
end
function love.update(dt)
-- Polled input: good for continuous movement while a key is held.
if love.keyboard.isDown("right") then player.x = player.x + player.speed * dt end
if love.keyboard.isDown("left") then player.x = player.x - player.speed * dt end
if love.keyboard.isDown("down") then player.y = player.y + player.speed * dt end
if love.keyboard.isDown("up") then player.y = player.y - player.speed * dt end
end
function love.draw()
love.graphics.setColor(0.2, 0.8, 1.0) -- tint ON
love.graphics.rectangle("fill", player.x, player.y, player.size, player.size)
love.graphics.setColor(1, 1, 1) -- reset tint before text/images
love.graphics.print("Arrow keys to move, Esc to quit", 10, 10)
end
function love.keypressed(key)
-- Event input: fires once per physical press. Use for menus, jumps, toggles.
if key == "escape" then love.event.quit() end
end
```
### 2. Frame-rate independence (the single most common bug)
```lua
-- RIGHT: scaled by dt → same real-world speed at 30 or 240 FPS.
player.x = player.x + player.speed * dt
-- WRONG: "pixels per frame" → moves twice as fast at double the frame rate.
player.x = player.x + player.speed
```
### 3. `conf.lua` (window + version; runs before `main.lua`)
```lua
-- conf.lua — must be its own file; love.conf will NOT run from main.lua.
function love.conf(t)
t.version = "11.5" -- the LÖVE version this game targets (string "X.Y")
t.window.title = "My LÖVE Game"
t.window.width = 800
t.window.height = 600
t.window.vsync = 1 -- number since 11.0: 1 = on, 0 = off, -1 = adaptive
t.window.resizable = false
t.modules.physics = false -- disable modules you don't use to trim startup/memory
end
```
### 4. Color is 0–1 in LÖVE 11.x (not 0–255)
```lua
-- LÖVE 11.x uses normalized floats. (Pre-11.0 code used 0–255 and will look wrong.)
love.graphics.setColor(1, 0, 0) -- opaque red
love.graphics.setColor(0.2, 0.8, 1.0, 0.5) -- translucent cyan (alpha 0.5)
-- Need to convert old byte values? Use the helper instead of dividing by hand:
love.graphics.setColor(love.math.colorFromBytes(128, 234, 255))
```
### 5. Screen states (brief — full manager in references)
```lua
-- A screen is a table with optional :update(dt), :draw(), :keypressed(key).
-- Keep the active screen on a stack so pause/menu overlays are trivial to pop.
local Stack = require("state_stack") -- see references/state-stack.md for the module
-- Call a method on the top screen only if it defines it (a menu may have no update).
local function forward(name, ...)
local screen = Stack.current()
if screen and screen[name] then screen[name](screen, ...) end
end
function love.load() Stack.push(require("screens.menu")) end
function love.update(dt) forward("update", dt) end
function love.draw() forward("draw") end
function love.keypressed(key) forward("keypressed", key) end
```
## Pitfalls
- **Speed varies with FPS** → you forgot `* dt`. Every per-frame change to position, timers,
or animation must be scaled by `dt`.
- **`love.conf` placed in `main.lua`** → it silently does nothing. It must live in `conf.lua`,
which LÖVE runs *before* loading modules.
- **Colors washed out or invisible** → you used 0–255 values. 11.x does not rescale them;
every channel above 1 draws at full intensity, so `setColor(128, 234, 255)` comes out
plain white and an alpha of `128` is fully opaque. Use 0–1 floats or
`love.math.colorFromBytes` (11.3+).
- **Everything tinted after one `setColor`** → color is global and persists across draws.
Reset with `love.graphics.setColor(1, 1, 1)` before drawing text/images you want untinted.
- **Holding a key fires `love.keypressed` only once** → key repeat is off by default.
Call `love.keyboard.setKeyRepeat(true)` (e.g. for text fields or menu scrolling); the
repeats then arrive with the third argument `isrepeat` set, so a jump handler can
ignore them. Releases go to `love.keyreleased(key)`.
## References
- For a complete push/pop screen-state manager (menu → game → pause, with delegated
callbacks), read `references/state-stack.md`.
## Related skills
- `save-systems` — saving/loading game state (engine-agnostic).
- `input-systems` — rebindable, multi-device input architecture.
- `pygame-core` / `phaser-core` — the same loop concepts in other lightweight engines.