Deathball: Learning Game Architecture with Love2D and Lua
In 2020, I embarked on an experimental game development project called Deathball - a platformer-shooter hybrid built with Love2D and Lua. This project served as a deep dive into modern game architecture patterns, particularly Entity Component Systems (ECS), and demonstrated how indie developers can build sophisticated games with lightweight frameworks.
The Vision
Deathball was designed as a fast-paced action game combining precision platforming with intense combat. The concept was simple but technically challenging: create a responsive character that could seamlessly transition between platforming traversal and ranged combat, all while maintaining smooth 60fps performance.
Key gameplay features included:
- Fluid movement mechanics with precise jumping and dashing
- Laser-based combat system with realistic physics
- Dynamic camera system that follows action intelligently
- Tiled map integration for efficient level design
- ECS architecture for maintainable, modular code
Technical Architecture
Love2D Framework Choice
Love2D provided the perfect balance of simplicity and power for this project:
-- main.lua - Core game loop structure
_G.GAME = require 'config.game'
_G.DIMENSIONS = require 'config.dimensions'
_G.MAPS = require 'config.maps'
function love.load()
love.window.setTitle('Melee is terrible')
love.window.setMode(_G.GAME.SCREEN_WIDTH, _G.GAME.SCREEN_HEIGHT, {
fullscreen = _G.GAME.IS_FULLSCREEN or false
})
gamestate = play
gamestate.load()
end
function love.update(dt)
local message = gamestate.update(dt)
if message and message == 'play' then
gamestate = play
gamestate.load()
elseif message and message == 'pause' then
gamestate = pause
gamestate.load()
end
Input.update()
end
Entity Component System Implementation
The heart of Deathball’s architecture was a custom ECS implementation using the Concord library:
-- Component definitions
local Transform = Component(function(e, x, y, rotation)
e.x = x or 0
e.y = y or 0
e.rotation = rotation or 0
end)
local Physics = Component(function(e, body, fixture)
e.body = body
e.fixture = fixture
e.velocity = { x = 0, y = 0 }
end)
local Drawable = Component(function(e, texture, quad)
e.texture = texture
e.quad = quad
e.visible = true
end)
local Player = Component(function(e, speed, jumpForce)
e.speed = speed or 200
e.jumpForce = jumpForce or 400
e.grounded = false
end)
System-Based Logic
Game logic was cleanly separated into focused systems:
-- systems/input.lua - Player input handling
local InputSystem = System({
Transform, Physics, Player
})
function InputSystem:update(dt)
for i = 1, self.pool.size do
local e = self.pool:get(i)
if love.keyboard.isDown('a') then
e.body:setLinearVelocity(-e.speed, e.body:getLinearVelocity())
elseif love.keyboard.isDown('d') then
e.body:setLinearVelocity(e.speed, e.body:getLinearVelocity())
end
if love.keyboard.isDown('space') and e.grounded then
e.body:applyLinearImpulse(0, -e.jumpForce)
e.grounded = false
end
end
end
Physics Integration with Windfield
One of the project’s technical highlights was integrating Windfield, a Love2D physics wrapper, for realistic movement and collision:
-- entities/player.lua
local Player = Assemblage(function(e, world, x, y)
e:give(Transform, x, y)
-- Create physics body with Windfield
local body = world:newCircleCollider(x, y, 12)
body:setType('dynamic')
body:setFriction(0.5)
body:setRestitution(0)
e:give(Physics, body, body.fixture)
e:give(Player, 200, 400)
e:give(Drawable, playerTexture)
return e
end)
Advanced Camera System
The camera system provided smooth, responsive tracking with configurable behavior:
-- systems/camera.lua
local CameraSystem = System({Transform, Player})
function CameraSystem:update(dt)
for i = 1, self.pool.size do
local e = self.pool:get(i)
-- Smooth camera following with lerp
local targetX = e.x - GAME.SCREEN_WIDTH / 2
local targetY = e.y - GAME.SCREEN_HEIGHT / 2
CAMERA.x = CAMERA.x + (targetX - CAMERA.x) * 0.1
CAMERA.y = CAMERA.y + (targetY - CAMERA.y) * 0.1
-- Constrain camera to map bounds
CAMERA.x = math.max(0, math.min(CAMERA.x, mapWidth - GAME.SCREEN_WIDTH))
CAMERA.y = math.max(0, math.min(CAMERA.y, mapHeight - GAME.SCREEN_HEIGHT))
end
end
Level Design with Tiled Integration
Deathball featured seamless integration with Tiled Map Editor through the Simple Tiled Implementation (STI) library:
-- utils/map.lua
local sti = require 'libraries.sti'
function loadMap(mapName)
local map = sti('resources/maps/' .. mapName .. '.lua')
-- Create physics bodies from collision layer
if map.layers['collision'] then
for i, obj in pairs(map.layers['collision'].objects) do
local body = world:newRectangleCollider(
obj.x, obj.y, obj.width, obj.height
)
body:setType('static')
body:setCollisionClass('Platform')
end
end
return map
end
Combat System Design
The projectile system showcased sophisticated entity management:
-- entities/laser.lua
local Laser = Assemblage(function(e, world, x, y, direction)
e:give(Transform, x, y, direction)
local body = world:newRectangleCollider(x, y, 4, 2)
body:setType('kinematic')
body:setCollisionClass('Projectile')
e:give(Physics, body)
e:give(Projectile, 500, 2.0) -- speed, lifetime
e:give(Drawable, laserTexture)
return e
end)
-- systems/projectile.lua
local ProjectileSystem = System({Transform, Physics, Projectile})
function ProjectileSystem:update(dt)
for i = self.pool.size, 1, -1 do
local e = self.pool:get(i)
-- Update lifetime
e.lifetime = e.lifetime - dt
if e.lifetime <= 0 then
e.body:destroy()
e:destroy()
else
-- Move projectile
local vx, vy = e.body:getLinearVelocity()
e.body:setLinearVelocity(e.speed * math.cos(e.rotation), vy)
end
end
end
Development Evolution
The commit history reveals the project’s iterative development approach:
Core Systems Phase (16 commits)
- ECS Foundation (
0abaa46
): Initial Entity Component System setup - Physics Integration (
557b2a2
): Box2D physics with camera system - Input Refinement (
252b6b2
): Improved input handling and collision detection - Animation System (
8aa0137
): Sprite animation framework (though noted as “awful” in commits!)
Polish and Optimization Phase
- Map Loading (
76ebb4c
): Refactored map loading system with platform fixes - Performance (
7d888bb
): Fixed FPS counter positioning and rendering optimizations - Audio Integration: Sound effects for laser firing, explosions, and atmospheric music
Technical Challenges and Solutions
Performance Optimization
- Entity Pooling: Implemented object pooling for frequently created/destroyed entities like projectiles
- Spatial Partitioning: Used Box2D’s built-in collision detection for efficient physics queries
- Render Batching: Minimized draw calls by batching similar sprites
Code Architecture
- Modular Design: Each system focused on a single responsibility
- Data-Driven Configuration: Game constants separated into configuration files
- Asset Management: Centralized resource loading and cleanup
-- config/game.lua
return {
SCREEN_WIDTH = 1024,
SCREEN_HEIGHT = 768,
IS_FULLSCREEN = false,
TARGET_FPS = 60,
PHYSICS_GRAVITY = 800,
PLAYER_SPEED = 200,
JUMP_FORCE = 400
}
Art and Audio Integration
The project featured a cohesive audiovisual design:
- Sprite Assets: Custom pixel art for player, enemies, and environmental objects
- Environmental Art: Multi-layered forest backgrounds with parallax scrolling
- Sound Design: Impact-driven audio for jumps, laser fire, and explosions
- Music: Atmospheric background track (
wiiu_final.ogg
) for immersion
Lessons Learned
ECS Architecture Benefits
- Modularity: Adding new gameplay features required minimal changes to existing code
- Performance: Component-oriented data layout improved cache efficiency
- Debugging: System isolation made tracking down bugs much easier
Love2D Framework Insights
- Rapid Prototyping: Lua’s flexibility enabled quick iteration on game mechanics
- Community Libraries: Rich ecosystem (Windfield, STI, Concord) accelerated development
- Cross-Platform: Single codebase deployed to Windows, macOS, and Linux
Physics Integration Challenges
- Tuning: Balancing realistic physics with responsive game feel required extensive tweaking
- Collision Detection: Managing collision classes and callbacks for complex interactions
- Performance: Physics simulation became bottleneck with too many dynamic bodies
Project Impact
Deathball served as a comprehensive exploration of modern game development architecture patterns. The ECS implementation became a template for future projects, and the clean separation of concerns made the codebase remarkably maintainable despite rapid feature iteration.
The project demonstrated that indie developers can build sophisticated games using open-source tools and frameworks, with careful architecture decisions being more important than engine choice.
Conclusion
While Deathball never reached commercial release, it achieved its primary goal: providing hands-on experience with professional game development patterns. The ECS architecture, physics integration, and modular design principles learned during this project directly influenced my approach to subsequent game development work.
The project showcased Love2D’s potential for serious game development while highlighting the importance of clean code architecture in managing project complexity. For developers interested in game programming, Deathball’s codebase serves as an excellent example of how to structure a non-trivial game project using modern architectural patterns.
The complete source code remains available as an open-source reference for developers exploring ECS architecture in Love2D.