Better ECS-ization

Added Tomatoes. Separated collision detection and resolution.
This commit is contained in:
Sage Vaillancourt 2025-03-03 13:37:10 -05:00
parent af5b494cdc
commit 22ff80cea7
11 changed files with 182 additions and 55 deletions

View File

@ -8,13 +8,17 @@ function Burger.reset(o)
y = 50,
}
o.velocity = {
x = 100 * math.random(),
x = 50 + (150 * math.random()),
y = 150 * (math.random() - 1),
}
o.size = {
x = 10,
y = 10,
}
o.canBeBounced = {
flat = { x = 0, y = 0 },
mult = { x = 1, y = 1 },
}
o.mass = T.marker
return o
end

View File

@ -1,26 +1,23 @@
Booster = {}
local gfx <const> = playdate.graphics
local function newBooster(x, y)
return setmetatable({
local size = { x = 40, y = 10 }
return {
score = 1,
expireAfterCollision = true,
size = size,
position = { x = x, y = y },
size = { x = 40, y = 10 },
}, { __index = Booster })
canBounce = {
flat = { x = 122, y = 190 },
mult = { x = 1, y = -0.5 },
},
drawAsRectangle = { size = size },
}
end
function Booster:elasticity(velocity)
velocity.y = (math.abs(velocity.y) + 190) * -0.5
velocity.x = velocity.x + 50
end
function Booster:draw()
gfx.fillRect(self.position.x, self.position.y, self.size.x, self.size.y)
end
local boosterIndex = 1
local boosterCache = {}
for i = 1, 200 do
boosterIndex = 1
boosterCache = {}
for i = 1, 100 do
boosterCache[i] = newBooster(-999, 999)
end

View File

@ -0,0 +1,36 @@
Tomato = {}
local function newTomato(x, y)
local size = { x = 20, y = 20 }
return {
score = 15,
expireAfterCollision = true,
position = { x = x, y = y },
size = size,
drawAsRectangle = { size = size },
}
end
local tomatoIndex = 1
local tomatoCache = {}
for i = 1, 100 do
tomatoCache[i] = newTomato(-999, 999)
end
function Tomato.cacheSize()
return #tomatoCache
end
-- Tomatos should be "initialized" almost always increasing in X value
function Tomato.get(x, y)
local tomato = tomatoCache[tomatoIndex]
tomato.position.x = x
tomato.position.y = y
tomatoIndex = tomatoIndex + 1
if tomatoIndex > #tomatoCache then
tomatoIndex = 1
end
return tomato
end

View File

@ -13,10 +13,12 @@ import("../lib/tiny.lua")
import("tiny-tools.lua")
import("systems/filter-types.lua")
import("systems/collision-detection.lua")
import("systems/collision-resolution.lua")
import("systems/draw.lua")
import("systems/gravity.lua")
import("systems/velocity.lua")
import("booster.lua")
import("ingredients/booster.lua")
import("ingredients/tomato.lua")
import("burger.lua")
local tiny <const> = tiny
@ -25,18 +27,17 @@ playdate.display.setRefreshRate(50)
gfx.setBackgroundColor(gfx.kColorWhite)
burger = Burger.new()
local floorSize = { x = 10000, y = 10 }
floor = {
position = { x = 0, y = 230 },
size = { x = 10000, y = 10 },
position = { x = 0, y = 235 },
size = floorSize,
canBounce = {
flat = { x = 0, y = 0 },
mult = { x = 0.9, y = -0.5 },
},
drawAsRectangle = { size = floorSize },
isSolid = true,
}
function floor:elasticity(velocity)
velocity.x = velocity.x * 0.9
velocity.y = velocity.y * -0.7
end
function floor:draw()
gfx.fillRect(floor.position.x, floor.position.y, floor.size.x, floor.size.y)
end
Camera = {
pan = {
@ -45,7 +46,32 @@ Camera = {
},
}
world = tiny.world(fallSystem, velocitySystem, collisionDetection, drawSystem, burger, floor)
Score = {
points = 0,
}
-- TODO: Shops with random upgrades instead of fixed ones.
function Score:add(add)
self.points = self.points + add
end
function Score:draw()
gfx.drawText("Z|" .. self.points, 360, 5)
end
world = tiny.world(
fallSystem,
velocitySystem,
collisionResolution,
collisionDetection,
drawSystem,
drawRectanglesSystem,
burger,
floor
)
for i = 1, Booster.cacheSize() do
world:addEntity(Booster.get(i * 60 + (math.random(0, 50)), math.random(50, 200)))
end
@ -57,19 +83,26 @@ function playdate.update()
playdate.drawFPS(5, 5)
Camera.pan.x = math.min(0, -burger.position.x + 200)
Camera.pan.y = burger.position.y + 120
Camera.pan.y = math.max(0, -burger.position.y + 120)
local newestX = Booster.newestBooster().position.x
local panX = -Camera.pan.x
printTable({ panX = panX, newestX = newestX })
local offset = 600
if newestX < panX then
Booster.get(panX + offset + (math.random(0, 50)), math.random(50, 200))
if newestX + 300 < panX then
if math.random() > 0.5 then
world:addEntity(Tomato.get(panX + offset + (math.random(0, 50)), math.random(-500, 150)))
else
-- Implicitly updates cached boosters
Booster.get(panX + offset + (math.random(0, 50)), math.random(-500, 150))
end
end
floor.position.x = -Camera.pan.x - offset
gfx.setDrawOffset(Camera.pan.x, Camera.pan.y)
world:update(deltaSeconds)
gfx.setDrawOffset(0, 0)
Score:draw()
if playdate.buttonJustPressed(playdate.kButtonA) then
Burger.reset(burger)
for i = 1, Booster.cacheSize() do

View File

@ -1,28 +1,28 @@
collisionDetection = tiny.filteredSystem(
{ position = T.XyPair, size = T.XyPair, elasticity = T.Elasticity, isSolid = Maybe(T.bool) },
collisionDetection = filteredSystem(
{ position = T.XyPair, size = T.XyPair, isSolid = Maybe(T.bool) },
-- Here, the entity, e, refers to some entity that the burger global(!) may be colliding with.
function(e)
if not burger.velocity then
function(e, _, system)
local collider = burger
if not collider.velocity then
return
end
local burgerTop = burger.position.y
local burgerBottom = burger.position.y + burger.size.y
local colliderTop = collider.position.y
local colliderBottom = collider.position.y + collider.size.y
local entityTop = e.position.y
local entityBottom = entityTop + e.size.y
local withinY = (entityTop > burgerTop and entityTop < burgerBottom)
or (entityBottom > burgerTop and entityBottom < burgerBottom)
local withinY = (entityTop > colliderTop and entityTop < colliderBottom)
or (entityBottom > colliderTop and entityBottom < colliderBottom)
if not withinY then
return
end
if burger.position.x < e.position.x + e.size.x and burger.position.x + burger.size.x > e.position.x then
if e.isSolid then
-- Assumes impact from the top
burger.position.y = entityTop - burger.size.y
if collider.position.x < e.position.x + e.size.x and collider.position.x + collider.size.x > e.position.x then
system.world:addEntity({ collisionBetween = { e, collider } })
if e.expireAfterCollision then
system.world:removeEntity(e)
end
e:elasticity(burger.velocity)
end
end
)

View File

@ -0,0 +1,38 @@
collisionResolution = filteredSystem({ collisionBetween = T.Collision }, function(e, _, system)
local collider, probablyBurger = e.collisionBetween[1], e.collisionBetween[2]
local colliderTop = collider.position.y
if collider.isSolid then
-- Assumes impact from the top
probablyBurger.position.y = colliderTop - probablyBurger.size.y
end
if collider.canBounce and probablyBurger.canBeBounced then
probablyBurger.velocity.x = probablyBurger.velocity.x
+ collider.canBounce.flat.x
+ probablyBurger.canBeBounced.flat.x
probablyBurger.velocity.x = probablyBurger.velocity.x
* collider.canBounce.mult.x
* probablyBurger.canBeBounced.mult.x
-- abs() makes sure we always push upward
probablyBurger.velocity.y = math.abs(probablyBurger.velocity.y)
+ collider.canBounce.flat.y
+ probablyBurger.canBeBounced.flat.y
probablyBurger.velocity.y = probablyBurger.velocity.y
* collider.canBounce.mult.y
* probablyBurger.canBeBounced.mult.y
end
if collider.score then
Score:add(collider.score)
end
if collider.expireAfterCollision then
system.world:removeEntity(collider)
end
system.world:removeEntity(e)
end)

View File

@ -1,3 +1,9 @@
drawSystem = tiny.filteredSystem({ draw = T.SelfFunction }, function(e, dt)
local gfx <const> = playdate.graphics
drawSystem = filteredSystem({ draw = T.SelfFunction }, function(e, dt)
e:draw()
end)
drawRectanglesSystem = filteredSystem({ position = T.XyPair, drawAsRectangle = { size = T.XyPair } }, function(e, dt)
gfx.fillRect(e.position.x, e.position.y, e.drawAsRectangle.size.x, e.drawAsRectangle.size.y)
end)

View File

@ -1,6 +1,11 @@
---@alias XyPair { x: number, y: number }
---@alias Elasticity fun(self, velocity: XyPair)
---@alias Entity table
---@alias Collision { collisionBetween: Entity[] }
---@type Entity
local Entity = {}
T = {
---@type XyPair
@ -12,11 +17,19 @@ T = {
marker = {},
---@type fun(self)
SelfFunction = function(self) end,
---@type Elasticity
Elasticity = {
isRigid = true,
apply = function() end,
--- Actor
CanBounce = {
isSolid = true,
flat = { x = 1, y = 1 },
mult = { x = 1, y = 1 },
},
--- Receiver
CanBeBounced = {
flat = { x = 1, y = 1 },
mult = { x = 1, y = 1 },
},
---@type Collision
Collision = { Entity, Entity },
}
---@generic T

View File

@ -1,4 +1,4 @@
local G = -300
fallSystem = tiny.filteredSystem({ velocity = T.XyPair, mass = T.marker }, function(e, dt)
fallSystem = filteredSystem({ velocity = T.XyPair, mass = T.marker }, function(e, dt)
e.velocity.y = e.velocity.y - (G * dt) - (0.5 * dt * dt)
end)

View File

@ -1,6 +1,6 @@
local sqrt = math.sqrt
velocitySystem = tiny.filteredSystem({ position = T.XyPair, velocity = T.XyPair }, function(e, dt)
velocitySystem = filteredSystem({ position = T.XyPair, velocity = T.XyPair }, function(e, dt)
if sqrt((e.velocity.x * e.velocity.x) + (e.velocity.y * e.velocity.y)) < 0.1 then
e.velocity = nil
world:addEntity(e)

View File

@ -13,7 +13,7 @@ local isSimulator = playdate.isSimulator
---@param shape T
---@param process fun(entity: T, dt: number, system: System)
---@return
function tiny.filteredSystem(shape, process)
function filteredSystem(shape, process)
local system = tiny.processingSystem()
local keys = {}
for key, value in pairs(shape) do