Home - JakeTurner616/pygame-lua-bindings GitHub Wiki
Pygame in Lua
Philosophy
The idea is to simplify development of simple 2d games by using a custom Lua framework with an event registration system. This reduces boilerplate code, making event handling more intuitive and allows simple games to be written somewhat easier.
Introduction
Welcome to the Pygame Lua Game Development Tutorial! This guide will walk you through the process of creating games using Lua and Pygame. We'll start with a simple "Hello World" program and gradually build up to more complex games. By the end of this tutorial, you'll have a solid understanding of what this framework is all about.
Setting Up
To get started, you'll need to set up your environment to run Lua scripts with Pygame, Luckily this part is really easy:
- Install the framework.
- Edit the test script in the
test_script
directory. - Run
main.py
to execute the Lua script.
Understanding the mainloop and event system
The main loop and event handling are fundamental to game development, and pygame is no different. Ensuring the game remains interactive and responsive is a crucial job of the mainloop and event handlers and are difficult to maintain for quick prototyping pygame code as this task is left to the programmer. Using Pygame with Lua can potentially offer a simplified and streamlined way to handle these processes compared to traditional Python implementations. This wiki highlights these differences and demonstrates the ease of use with Lua.
In a standard Pygame application written in Python, the main loop handles event processing, updating the game state, and rendering:
import pygame
pygame.init()
# Setup
screen = pygame.display.set_mode((800, 600))
running = True
# Main loop
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
# Update game state
# Draw to the screen
screen.fill((0, 0, 0))
pygame.display.flip()
pygame.quit()
Using Pygame with Lua, the main loop and event handling are simplified and abstracted even more through an event registration system. This system could make it quite easy to manage events and game state updates:
-- Initialize the game state and variables
local running = true
-- Define event handler for keydown
local function on_keydown(event)
if event.key == K_ESCAPE then
running = false
elseif event.key == K_RIGHT then
-- Move right
elseif event.key == K_LEFT then
-- Move left
end
end
-- Register event handlers
register_event_handler('on_keydown', on_keydown)
-- Function to handle events
local function handle_events()
for _, event in ipairs(get_events()) do
if event.type == "QUIT" then
running = false
elseif event.type == "KEYDOWN" then
on_keydown(event)
end
end
end
-- Function to update the game state
local function update_game()
-- Game logic goes here
end
-- Function to render the game
local function render_game()
clear_canvas()
-- Drawing code goes here
flip_display()
end
-- Main loop
while running do
handle_events()
update_game()
render_game()
end
Explanation:
The register_event_handler function is used to bind specific functions to handle events like on_keydown. This makes it easy to manage and extend event handling. The handle_events function processes events using the registered handlers, simplifying the event loop. The main loop continuously calls handle_events, update_game, and render_game to keep the game responsive.
Examples (easy to intermediate)
Example 1: Hello World
Let's begin with the classic "Hello World" program. This will introduce you to the basic structure of a Lua script using Pygame.
-- Clear the screen and draw some shapes
clear_canvas()
-- Draw Blue text
draw_text(50, 50, "Hello, Pygame!", "Arial", 30, 0x0000FF)
-- Update display
flip_display()
In this script, we:
Clear the canvas to ensure there is nothing left over from previous drawings. Draw the text "Hello, Pygame!" in blue at the specified coordinates (50, 50). Update the display to show the drawn text.
Example 2: Moving a Shape
Next, we'll create a program that moves a shape around the screen using keyboard input.
-- Initialize the position of the shape
local shape_x, shape_y = 100, 100
-- Function to handle key events
local function handle_keys(event)
if event.key == K_RIGHT then
shape_x = shape_x + 10
elseif event.key == K_LEFT then
shape_x = shape_x - 10
elseif event.key == K_UP then
shape_y = shape_y - 10
elseif event.key == K_DOWN then
shape_y = shape_y + 10
end
end
-- Function to draw the shape
local function draw_shape()
clear_canvas()
draw_rectangle(shape_x, shape_y, 50, 50, 0x00FF00) -- Draw a green rectangle
flip_display()
end
-- Register event handlers
register_event_handler('on_keydown', handle_keys)
register_function("draw", draw_shape)
-- Start the main loop
start_main_loop()
In this script, we:
Initialize the position of the shape at (100, 100). Define a function handle_keys to handle keyboard input and update the position of the shape accordingly. Define a function draw_shape to clear the canvas and draw the shape at its current position as a green rectangle. Register the event handler for keydown events to call handle_keys. Register the draw_shape function to be called for drawing each frame. Start the main loop to continuously update and draw the shape.
Example 3: Bouncing Ball
Now, let's create an animated bouncing ball that reacts to the edges of the window.
-- Clear the screen and draw initial shapes
clear_canvas()
-- Initial positions
local circle_x, circle_y = 300, 300
local angle = math.rad(45) -- Initial angle in radians (45 degrees)
local speed = 7 -- Speed of movement
-- Function to update position (animation example)
local function update_position()
-- Move circle
circle_x = circle_x + math.cos(angle) * speed
circle_y = circle_y + math.sin(angle) * speed
-- Bounce off the edges
local bounce_randomness = math.rad(math.random(-5, 5)) -- Smaller range for slight angle variation
if circle_x <= 0 then
angle = math.pi - angle -- Reflect horizontally on the left edge
circle_x = 0 -- Reset position to within boundary
angle = angle + bounce_randomness
elseif circle_x >= 800 then
angle = math.pi - angle -- Reflect horizontally on the right edge
circle_x = 800 -- Reset position to within boundary
angle = angle + bounce_randomness
end
if circle_y <= 0 then
angle = -angle -- Reflect vertically on the top edge
circle_y = 0 -- Reset position to within boundary
angle = angle + bounce_randomness
elseif circle_y >= 600 then
angle = -angle -- Reflect vertically on the bottom edge
circle_y = 600 -- Reset position to within boundary
angle = angle + bounce_randomness
end
-- Ensure angle stays within 0 to 2*pi range
if angle < 0 then
angle = angle + 2 * math.pi
elseif angle >= 2 * math.pi then
angle = angle - 2 * math.pi
end
end
-- Function to draw updated shapes
local function draw()
clear_canvas()
draw_circle(0x00FF00, { circle_x, circle_y }, 50) -- Draw a green circle
flip_display()
end
-- Register Lua functions to be called from Python
register_function("update_position", update_position)
register_function("draw", draw)
-- Start the main loop
start_main_loop()
In this script, we:
Initialize the position, angle, and speed of the ball. Define a function update_position to update the ball's position and handle bouncing off the edges.
The ball's position is updated based on its current angle and speed. When the ball hits an edge, its angle is reflected, and a slight randomness is added to create a more natural bounce. The angle is wrapped around to stay within the range of 0 to 2π radians.
Define a function draw to clear the canvas and draw the ball at its current position as a green circle. Register the update_position and draw functions to be called from Python. Start the main loop to continuously update and draw the ball.
Example 4: Snake Game
We'll now create a classic Snake game. This example will introduce you to handling more complex game logic and using multiple objects.
-- Game constants
local SCREEN_WIDTH, SCREEN_HEIGHT, TILE_SIZE = 800, 600, 20
-- Initialize game state
local snake = {{x = 10, y = 10}}
local food = {x = 15, y = 15}
local direction = {x = 1, y = 0}
local score = 0
local input_locked = false -- Lock for input processing
-- Draw function
local function draw_game()
clear_canvas()
-- Draw snake
for _, s in ipairs(snake) do
draw_rectangle(s.x * TILE_SIZE, s.y * TILE_SIZE, TILE_SIZE, TILE_SIZE, "#00FF00")
end
-- Draw food
draw_rectangle(food.x * TILE_SIZE, food.y * TILE_SIZE, TILE_SIZE, TILE_SIZE, "#FF0000")
-- Draw score
draw_text(10, SCREEN_HEIGHT - 30, "Score: " .. score, "Arial", 20, "#FFFFFF")
flip_display()
end
-- Event handling
register_event_handler('on_keydown', function(event)
if input_locked then return end -- Ignore inputs if locked
local key_map = {
[K_RIGHT] = {1, 0}, -- Right arrow
[K_LEFT] = {-1, 0}, -- Left arrow
[K_UP] = {0, -1}, -- Up arrow
[K_DOWN] = {0, 1} -- Down arrow
}
local dir = key_map[event.key]
if dir and (dir[1] ~= -direction.x and dir[2] ~= -direction.y) then
direction = {x = dir[1], y = dir[2]}
input_locked = true -- Lock input until next update
end
end)
-- Game logic
function process_events()
for _, e in ipairs(get_events()) do
if e.type == "QUIT" then
stop_main_loop()
end
end
end
function update_position()
-- Move snake
local head = {x = snake[1].x + direction.x, y = snake[1].y + direction.y}
-- Check collision with walls or self
if head.x < 0 or head.x >= SCREEN_WIDTH / TILE_SIZE or
head.y < 0 or head.y >= SCREEN_HEIGHT / TILE_SIZE or
(#snake >= 4 and (function()
for _, s in ipairs(snake) do
if s.x == head.x and s.y == head.y then
return true
end
end
return false
end)()) then
stop_main_loop()
end
-- Move snake
table.insert(snake, 1, head)
-- Check for food collision
if head.x == food.x and head.y == food.y then
score = score + 1
food = {x = math.random(0, SCREEN_WIDTH / TILE_SIZE - 1), y = math.random(0, SCREEN_HEIGHT / TILE_SIZE - 1)}
else
table.remove(snake)
end
input_locked = false -- Unlock input after updating
end
-- Register functions and start the loop
register_function("process_events", process_events)
register_function("update_position", update_position)
register_function("draw", draw_game)
start_main_loop()
In this script, we:
Trigger Event Registration: The register_event_handler function binds the provided function to the 'on_keydown' event. This means the function will be called whenever a key is pressed.
Create Input Locking: The input_locked variable ensures that input is only processed once per update cycle, preventing multiple direction changes in a single frame.
Define Key Mapping: The key_map table maps key codes to direction vectors. For example, pressing the right arrow key (K_RIGHT) sets the direction to {1, 0}.
Define Direction Changes: The function checks if the new direction is not directly opposite to the current direction to prevent the snake from reversing. If valid, it updates the direction and locks the input until the next update.