Break my work
Eight systems from my real projects, rebuilt to run in the browser. One of them is playable with a keyboard, one is a room you light yourself, and the rest have a control that breaks them on purpose, so you can see exactly where the naive approach falls over.
Hand-written logic ported over from Luau. No game engine, no libraries, nothing off a shelf.
Movement & momentumPlayable
The controller I built for Anomalous, cut down to two dimensions. Sprint into a low block and you'll vault it without touching another key. Slide under the tall ones, jump the gaps. Speed you earn is speed you keep, so holding a clean line is faster.
function Controller:UpdateSlide(dt, moveDir)
local vel, speed = self.Velocity, self.Velocity.Magnitude
if speed < MIN_SLIDE_SPEED then return self:ExitSlide() end
-- steering bleeds speed, so a straight line is the fast line
local turn = 1 - vel.Unit:Dot(moveDir.Unit)
speed *= (1 - turn * STEER_COST * dt * 60)
self.Velocity = vel.Unit * (speed * SLIDE_FRICTION ^ (dt * 60))
end
Dynamic light & shadowInteractive
The trick behind Grimworks' look, flattened to 2D. Every light in this room computes true line-of-sight shadows against the geometry, every frame. Your cursor is the flashlight. Click to set lamps down, then cut the power and watch the blackout play out.
function Light:CastShadows(walls)
for _, wall in walls do
for _, edge in wall:Edges() do
local a, b = edge.A, edge.B
-- push both ends of the edge away from the light,
-- far past the edge of the room
local a2 = a + (a - self.Position).Unit * FAR
local b2 = b + (b - self.Position).Unit * FAR
-- everything inside that quad never sees this light
self:EraseQuad(a, a2, b2, b)
end
end
end
Lag compensation
The dashed ring is where the server thinks the target is. The solid orb is what a player on your connection sees. Drag the ping up and try to land a hit, then switch to rewind and try the same thing.
function VerifyHit(player, target)
local pos = target.Position
if (pos - player.Aim).Magnitude < 5 then
return true
end
end
function Rewind:Verify(player, ping)
local t = workspace.Time - ping
local old = self:GetHistory(t)
return (old - player.Aim).Magnitude < 5
end
Interpolation
Three ways to draw the same player from exactly the same packets. Drop the tick rate and add packet loss: the raw lane teleports, the naive lane rubber-bands, and the buffered lane keeps gliding because it renders a beat in the past on purpose.
function Buffer:Sample(now)
local target = now - INTERP_DELAY -- deliberately behind
for i = #self._frames - 1, 1, -1 do
local a, b = self._frames[i], self._frames[i + 1]
if a.t <= target and b.t >= target then
local alpha = (target - a.t) / (b.t - a.t)
return a.state:Lerp(b.state, alpha)
end
end
end
Spatial hashing
Same 250 objects, same movement, same collisions coming out the other end. The only thing that changes is how many pairs get tested to find them.
for i, a in pairs(parts) do
for j, b in pairs(parts) do
-- every pair, every frame
if (a.Pos - b.Pos).Mag < 5 then
collide(a, b)
end
end
end
function SpatialHash:Query(part)
local cell = self:GetCell(part.Pos)
for _, n in pairs(cell) do
-- only the neighbours
collide(part, n)
end
end
Flow fields
Five hundred agents, one path calculation. Drag to draw walls and they reroute straight away, because the field gets solved once for the whole map instead of once per agent.
function FlowField:Update(target)
-- 1. flood fill outward from the goal
self:CalculateDistances(target)
-- 2. every cell points at its cheapest neighbour
for x, y in grid do
grid[x][y].Vector = self:GetLowest(x, y)
end
end
Heuristic detection
Basic anti-cheat bans anyone moving too fast, which also bans everyone on bad wifi. Watching how much the speed jumps around, not just the average, tells them apart. Toggle either one on and give it a second to gather data.
Average and variance both nominal. Player is moving normally.
function AntiCheat:Analyze(history)
local variance = Math.Variance(history)
local avg = Math.Average(history)
-- high average + low variance = not a human
if avg > 100 and variance < 50 then
return "FLIGHT_DETECTED"
end
end
Atomic saves
Start a transfer, then kill the connection while the money is still moving. In the unsafe version it just disappears. In the atomic version the write never committed, so it rolls back on its own.
function Trade:Process(p1, p2, amt)
p1.Cash.Value -= amt
wait(2) -- anything can happen here
p2.Cash.Value += amt
end
function Atomic:Tx(key, fn)
DataStore:UpdateAsync(key, function(d)
-- commits or it never happened
return fn(d)
end)
end
Seen enough?
All of that is hand written. It runs a lot better inside an actual game than it does in a browser tab.
Kyakz