Loading...
Loading...
Spotify
Back to blog listing

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)

  1. ECS Foundation (0abaa46): Initial Entity Component System setup
  2. Physics Integration (557b2a2): Box2D physics with camera system
  3. Input Refinement (252b6b2): Improved input handling and collision detection
  4. Animation System (8aa0137): Sprite animation framework (though noted as “awful” in commits!)

Polish and Optimization Phase

  1. Map Loading (76ebb4c): Refactored map loading system with platform fixes
  2. Performance (7d888bb): Fixed FPS counter positioning and rendering optimizations
  3. 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.