I ported a little game I originally made in Macroquad to Love2d. But I have also added additional features, like additional animations and screen shake. I have used HUMP for various utilities. shack for screen shake. HC for collision and tiny-ecs for an entity component system. You are a circle with a gun that shoots automatically at other enemies. The enemies follow you around and your goal is to dodge them. The game looks like this:
Here is the code:
local Gamestate = require("hump.gamestate")
local Vector = require("hump.vector")
local Timer = require("hump.timer")
local Camera = require("hump.camera")
local HC = require("HC")
local tiny = require("tiny")
local screen = require("shack")
-- Different scenes for the game.
local game = {}
local gameover = {}
-- Processing logic here for enemies, bullets, and tank.
local processEnemies = tiny.processingSystem()
processEnemies.filter = tiny.filter("radius&pos&angle&speed&shape&!gun_length")
function processEnemies:onAdd(e)
Timer.tween(0.5, e, {angle=2*math.pi}, 'linear', function()
e.speed = 50
end)
end
function processEnemies:process(e, dt)
love.graphics.setLineWidth(5)
love.graphics.arc( "line", "open",
e.pos.x, e.pos.y,
e.radius, 0, e.angle)
e.pos = e.pos + (tank.pos - e.pos):normalizeInplace() * dt * e.speed
e.shape:moveTo(e.pos.x, e.pos.y)
end
local processBullet = tiny.processingSystem()
processBullet.filter = tiny.filter("pos&shape&speed&bullet")
function processBullet:onAdd(e)
e.pos = tank.pos
e.shape:moveTo(e.pos.x, e.pos.y)
e.dir = tank.gun_vec:normalized()
end
function processBullet:process(e, dt)
love.graphics.setLineWidth(5)
love.graphics.circle("fill", e.pos.x, e.pos.y, 5)
e.pos = e.pos + e.dir * dt * e.speed
e.shape:moveTo(e.pos.x, e.pos.y)
for shape, delta in pairs(HC.collisions(e.shape)) do
if enemies[shape] ~= nil then
world:remove(enemies[shape])
table.remove(enemies[shape])
end
end
end
local processTank = tiny.processingSystem()
processTank.filter = tiny.filter("radius&pos&angle&gun_length&gun_vec")
function processTank:onAdd(e)
local drawGun = function()
Timer.tween(0.5, e, {gun_length=20}, 'linear',
function()
e.speed = 100
end)
end
Timer.tween(0.5, e, {angle=2*math.pi}, 'linear', drawGun)
end
function tankKeypress(e, dt)
-- Handle key presses.
local delta = Vector(0, 0)
if love.keyboard.isDown('left') then
delta.x = -1
elseif love.keyboard.isDown('right') then
delta.x = 1
end
if love.keyboard.isDown('up') then
delta.y = -1
elseif love.keyboard.isDown('down') then
delta.y = 1
end
delta:normalizeInplace()
e.pos = e.pos + delta * dt * e.speed
end
function tankCollision(e, dt)
-- Update collision information.
e.shape:moveTo(e.pos.x, e.pos.y)
e.shot_radius:moveTo(e.pos.x, e.pos.y)
local gun_angle = 0
for shape, delta in pairs(HC.collisions(e.shot_radius)) do
if enemies[shape] ~= nil then
local sx, sy = shape:center()
gun_angle = math.atan2(sy - e.pos.y, sx - e.pos.x)
break
end
end
for shape, delta in pairs(HC.collisions(e.shape)) do
if enemies[shape] ~= nil then
screen:setShake(20)
local new_health = healthbar.health - 0.5
Timer.tween(0.5, healthbar, {
health=math.max(new_health, 0)
}, "linear")
if healthbar.health <= 0 then
Gamestate.switch(gameover)
end
break
end
end
return gun_angle
end
function processTank:process(e, dt)
tankKeypress(e, dt)
local gun_angle = tankCollision(e, dt)
-- Draw tank.
e.gun_vec = Vector(e.gun_length, 0):rotated(gun_angle)
love.graphics.setLineWidth(5)
love.graphics.arc( "line", "open",
e.pos.x, e.pos.y,
e.radius, 0, e.angle)
love.graphics.line(e.pos.x, e.pos.y,
e.pos.x + e.gun_vec.x,
e.pos.y + e.gun_vec.y)
end
function game:draw()
-- Draw healthbar.
love.graphics.line(10, 10,
10 + healthbar.health, 10)
camera:attach()
screen:apply()
world:update(love.timer.getDelta())
camera:detach()
end
function game:update(dt)
-- Handle timers and screen shake libraries.
Timer.update(dt)
screen:update(dt)
-- Handle camera.
local CAMERA_SLOW_FACTOR = 100
local dx, dy = tank.pos.x - camera.x, tank.pos.y - camera.y
camera:move(dx / CAMERA_SLOW_FACTOR, dy / CAMERA_SLOW_FACTOR)
end
function gameover:draw()
love.graphics.print('The End',
love.graphics.getWidth() / 2,
love.graphics.getHeight() / 2)
end
function game:init()
-- For checking if an enemy is in range of a player.
tank = {
pos=Vector(300, 400), gun_vec=Vector(1,0),
gun_length=0, speed=0, angle=0, radius=20,
shape=HC.circle(300, 400, 20),
shot_radius=HC.circle(300, 400, 50)
}
enemies = {}
healthbar = {health = 60}
world = tiny.world(processTank, processEnemies, processBullet, tank)
camera = Camera(tank.pos.x, tank.pos.y)
-- Spawn enemies.
Timer.every(1, function()
local angle = love.math.random()
local x = tank.pos.x + 300 * math.cos(2 * math.pi * angle)
local y = tank.pos.y + 300 * math.sin(2 * math.pi * angle)
local shape = HC.circle(x, y, 20)
local enemy = {
pos=Vector(x, y), speed=0, angle=0, radius=20,
shape=shape
}
tiny.add(world, enemy)
enemies[shape] = enemy
end)
-- Spawn bullets.
Timer.every(5, function()
tiny.add(world, {
speed=100,
shape=HC.point(0,0),
pos=Vector(0, 0),
bullet = {}
})
end)
end
function love.load()
Gamestate.registerEvents()
Gamestate.switch(game)
end