Building classic Pong in Love2D with Lua in one stream
- YT :: https://www.youtube.com/watch?v=CYmCwHJs-A8
- Original title :: Ping Pong in Lua
Tsoding had tried to build Pong during several earlier game engine reviews but kept getting sidetracked. This time he commits to finishing it in a single session and picks Love2D, which chat had been suggesting. He has barely used Lua or Love2D before. Over about three hours he builds a full two-player Pong: a ball bouncing off the walls, keyboard and gamepad paddles, a dashed net, a hand-made 3x5 pixel font for the scores, rectangle collision, scoring, and sound effects. He publishes it as ping-pong-love-remastered under GPL2. Along the way he comments on Lua's syntax (likes it) and semantics (dislikes them), complains about LuaJIT's missing integer division, tries to ship a game as bytecode only, and has a rant about AI answers that "hit the target but miss the point".
Love2D first impressions
- Love2D is not really an engine. It is a single executable that bundles Lua bindings to common game libraries: graphics, audio, input, and Box2D physics. You point it at a folder (or a zip) containing
main.luaand it runs it. The same zip runs on every platform, including the web. - The game is written as three callbacks:
love.load,love.update(dt)andlove.draw. Input is polled insideupdate(love.keyboard.isDown), much like raylib. It took him a while to find this in the docs. - Docs quirks he ran into:
- Draw modes are plain strings (
"fill","line"). - Colors are in the 0-1 range now, but the docs describe it mainly as "before 11.0 it was 0-255".
love.graphics.setLineWidthexists, among many other global setters.- Resizing the window is done with
love.window.setMode. - The system Lua on his distro was from 2015 and did not support hex literals, so he built Lua 5.4 from source in seconds. Love2D itself uses LuaJIT, which is Lua 5.1 plus a few 5.2 features.
Building the game
- Ball: it moves at
dx, dytimesdt. The new position is computed first and only committed if it does not hit a wall; on a hit, the matching velocity component flips. - Constants: he capitalises tunable constants and lists them at the top of the file. Sizes are expressed in "balls": paddle thickness = ball size, paddle length = 6 balls, padding = 4 balls. Paddle speed is derived from ball speed. Everything scales together when one value changes.
- Net: first drawn as a dashed line, by splitting the height into N segments and drawing every other one with a numeric
for(inclusive range, 1-based). He then makes it thicker withsetLineWidth. - Look: he copied the background colour from a screenshot of the original Pong (slightly greenish).
- Input: W/S keys for the left paddle and arrow keys for the right. Gamepads are found with
love.joystick.getJoysticks: the first gamepad goes to the left paddle, the second to the right. The D-pad is read withisGamepadDown("dpup"/"dpdown"). Local multiplayer is trivial, with no networking or state sync, which he thinks would be great for kids or for teaching. - Paddle limits: a move is only applied if the paddle stays inside the window.
- Score font: he hand-codes 3x5 bitmaps for the digits 0-9 as Lua tables, squeezes them into a compact form with an Emacs macro, and renders each "on" cell as a rectangle. His point: you don't need AI to make assets; you can bake them directly into the source.
- Multi-digit numbers:
count_digitsrepeatedly divides by 10.number_widthis digits × 3 cells plus padding. Rendering starts at the rightmost digit and walks left usingn % 10. The left score is right-aligned to the net and the right score left-aligned. - Scoring: if the ball leaves on the left, the right player scores, and vice versa. The ball is reset to the centre and heads toward the player who was scored on, as in the online original he checked.
printandassertwere enough for debugging. - Collision: the first version, which compared ranges of the ball against the paddle edges, ignored the ball's size, so the ball got stuck in paddles. He replaced it with a standard AABB check: two rectangles do not overlap if R1 < L2, R2 < L1, B1 < T2 or B2 < T1, and they overlap otherwise. The ball's tentative new position is checked against both paddles. Remaining glitches, such as the ball sliding along a paddle when pushed from the side, went away once paddle movement was also blocked when it would collide with the ball. He took that approach from his earlier game jaibreak.
- Sound:
love.audio.newSource(path, "static")for short effects,"stream"for music, pluslove.audio.play. He reused free sounds (a UI blip for hits, a blast for scores) and credited their authors. - Publishing: he found an unfinished Pong attempt in Love2D from seven years ago that he had completely forgotten about, so he named the new repo
ping-pong-love-remastered. It is licensed GPL2 "just to annoy people": he argues licenses barely matter now that code can be laundered through LLMs.
Lua opinions
- He likes the syntax. It is light on noise, and
if ... then ... endblocks stay readable as programs grow, unlike Python's indentation. He sees it as the middle ground Python was aiming for. - He dislikes the semantics: dynamically and weakly typed, a "JavaScript-grade" toy scripting language. Undeclared variables silently become globals, and typos produced nil errors several times. "This is why people invented variable declaration."
- Missing conveniences: no
+=, nocontinue(the idiom isgoto continue), no destructuring. LuaJIT has no//integer division. A Love2D issue proposing it was closed, because LuaJIT is faster than stock Lua and switching to 5.3 isn't planned. His view is that projects should own and patch their dependencies. He added "integer division in LuaJIT" to his idea list. - He wants languages to allow a leading
or/andso multi-line conditions can be aligned vertically. TLA+ allows this with its bulleted disjunction/conjunction lists, and he'd like to see it in more languages. - Naming quips:
nilsignals an academic language andnulla business one; C++ "outweirds everyone" withnullptr.
Closed-source Love2D games
A chatter pointed out that Love2D ships your source code inside the game. He tried compiling the script to bytecode with luac and loading it in Love2D. It failed with "incompatible bytecode" because Love2D uses LuaJIT, not stock Lua. He thinks a small hack to Love2D or LuaJIT could let it load precompiled bytecode, and noted it as a possible future stream.
"Hit the target but missed the point"
When he searched whether Love2D has a built-in rectangle-intersection check (raylib has one, and Love2D bundles all of Box2D), the answers explained how to write one yourself. That is technically correct but ignores the actual question. He says this is his most common frustration with ML systems: they hit the target but miss the point, and he expects the phrase to become more relevant every year. The real answer turned out to be no, so he wrote the check himself.