Untitled Magic Game

Added on September 9, 2026

Systems Engineering Luau Networking Custom Physics

Overview

I am currently developing this untitled magic game on Roblox. Please note that this is a private project currently in active development and is not released or playable by the public. It is heavily inspired by Noita and focuses completely on deep technical craftsmanship. The project features an emergent spell building engine, high speed binary network replication, an interactive 3D card deck UI for wand customization, and a decoupled Model View Controller architecture.

In addition to the systems engineering, I also created some of the pixel art myself. Using Aseprite, I manually drew the icons for the Bolt, Explosive Projectile, Bounce, Explosive Bounce, and Slithering Path spells (the remaining spell art is temporarily AI generated while in development).



The Engineering Journey

The entire premise of the game relies on giving players the freedom to break the rules. I wanted players to craft absurd, game crashing wands just like in Noita.

The first massive hurdle was figuring out how to let players stack spell modifiers infinitely. Traditional object oriented approaches get incredibly messy when you try to apply a Bouncing modifier to a Trigger modifier that then splits into a Hexagon formation. I solved this by building the engine around Continuation Passing Style.

By having modifiers yield functions that dynamically compose parameters for the next spells in the sequence, I created an environment where behaviors can wrap each other infinitely using higher order composition. I even built a virtual UI pass GetSpellCastTree that evaluates the full wand cycle non destructively so the HUD can render the exact spell hierarchy before the player even clicks.

Spell Engine: Continuation-Passing Style Architecture

graph TD Player([Player Clicks Wand])-->Activate[Wand:Activate] Activate-->Preactivate[Wand:Preactivate] subgraph Engine [CPS Core Engine] Preactivate-->CastSpell[Wand:CastSpell] CastSpell-->Multicast{Modifier: Triple Form} Multicast-->|Continuation 1|SpellA[Bolt] Multicast-->|Continuation 2|SpellB{Modifier: Explosive Projectile} SpellB-->|Nested Continuation|SpellC[Bolt] Multicast-->|Continuation 3|SpellD[Bolt] end SpellA-->Physics[Projectile Engine Vector Math] SpellC-->Physics SpellD-->Physics subgraph UI [HUD Preview] GetSpellCastTree[GetSpellCastTree] -.-> |Virtual Pass| Tree[Hierarchical UI Tree] end style Player fill:#0f172a,stroke:#38bdf8,stroke-width:2px,color:#fff style Multicast fill:#1e1e2e,stroke:#c084fc,stroke-width:2px,color:#fff style SpellB fill:#1e1e2e,stroke:#fbbf24,stroke-width:2px,color:#fff style Physics fill:#022c22,stroke:#10b981,stroke-width:2px,color:#fff
-- Continuation Passing Style Wrapper Composition
local function compose(f, g)
    return function(val)
        return g(f(val))
    end
end

-- Example Spell Modifier: Explosive Trigger Bolt
{
    Name = "boltWithTrigger",
    Type = SpellType.Projectile,
    Cast = function(wand, spell, castState)
        return 1, function(nextSpells)
            local tracker = wand:CastProjectile("Bolt", castState, spell)
            local nextSpell = nextSpells[1]
            if nextSpell then
                tracker.onHit = function(hitProjectile)
                    nextSpell.cast({
                        cf = CFrame.lookAt(hitProjectile.position, hitProjectile.position + hitProjectile.velocity),
                        bounceFromNormal = hitProjectile.normal
                    })
                end
            end
            return { projectiles = {tracker} }
        end
    end
}

Once players could cast hundreds of projectiles at once, the standard Roblox physics and humanoid AI instantly bottlenecked. I had to rip out the default instances completely.

Instead I built a pure mathematical vector engine combined with custom hitboxes. For the enemies I implemented custom flocking steering behaviors. A separation force checks neighboring hitboxes and applies an inverted vector to prevent clipping, while a repulsion force prevents overshooting the target. To make them navigate the dungeon efficiently I wrote a custom string pulling algorithm that fires spherecasts scaled to the agent radius, pruning unnecessary path nodes when a clear line of sight exists.

-- Pure Vector Math Separation & Repulsion Steering
local steering = Vector2.zero

-- Separate from other enemies to prevent clipping
for _, otherEnemy in self.enemies:GetAll() do
    if otherEnemy.id == enemy.id then continue end
    local offset = enemy.position - otherEnemy.position
    local otherDistance = offset.Magnitude
    local desiredDistance = enemy.radius + otherEnemy.radius

    if otherDistance > 0 and otherDistance < desiredDistance then
        local strength = (desiredDistance - otherDistance) / desiredDistance
        steering += offset.Unit * strength
    end
end

-- Maintain optimal attack range from target
if distanceFromTarget < attackRange then
    local strength = (attackRange - distanceFromTarget) / attackRange
    steering -= delta.Unit * strength
end
-- Custom String Pulling with Agent Radius Spherecasts
for i = #rawPath, currentPathIndex + 1, -1 do
    local targetPos = rawPath[i]
    local delta = targetPos - currentPos
    local distance = delta.Magnitude
    local direction = delta / distance

    -- Prune nodes if a clear line of sight exists for the agent's full radius
    local hit = workspace:Spherecast(currentPos, agentRadius, direction * distance, rayParams)

    if not hit then
        furthestVisibleIndex = i
        break
    end
end

But simulating all this custom physics created a new problem: network lag. To replicate hundreds of fast moving projectiles and enemies without choking the server, I threw away standard RemoteEvents and engineered a custom binary networking layer.

It uses a declarative schema to map entity states to bit aligned byte layouts. It dynamically selects byte writers based on active entity counts to save every bit of network bandwidth. It streams byte packed updates with strict assertion checking, reducing remote event payload sizes by over 70 percent. To eliminate runtime garbage collection stutters from constantly allocating and destroying these entities, I engineered a circular ID memory pool.

-- RotaryIdMap: Circular Entity Pool to eliminate GC stutters
function RotaryIdMap:GetNextId(): number
    local startPointer = self.nextPointer

    repeat
        local id = self.nextPointer
        self.nextPointer = (id + 1) % self.maxIds

        if self.values[id] == nil then
            return id
        end
    until self.nextPointer == startPointer

    error("RotaryIdMap is full")
end

High-Speed Binary Replication Flow

sequenceDiagram participant V as Client View (Renderer) participant C as Client Model (Engine) participant S as Server Model (Engine) participant R as Server (Replicator) C->>S: Request Spell Cast (RPC) S->>S: Instantiate Authoritative Projectile S->>R: Serialize Creation State R->>C: Broadcast Compressed Initial Payload Note over C: Instantiate Replica C->>V: Signal View: Create 3D Mesh par Independent Simulation S->>S: Simulate Physics (Vector Math) and C->>C: Simulate Physics Locally C->>V: Signal View: Update Frame end loop Periodic Sync S->>R: Flush Correction Buffer R->>C: Broadcast Binary Delta Sync Note over C: Read u16 ID & Assert State Size C->>C: Math Interpolation to Server State end S->>S: Impact / Max Age Reached S->>R: Serialize Destruction Packet R->>C: Broadcast Destroy Signal C->>V: Signal View: Explode/Destroy Mesh Note over S, C: IDs Recycled to Rotary Map
-- Server Binary State Replicator Flush
function ServerCorrectionReplicator:Flush()
    local totalBytes = CORRECTION_COUNT_BYTES + count * (self.idSize + self.stateSize)
    local output = buffer.create(totalBytes)
    buffer.writeu16(output, 0, count)

    for _, entity in self.entities do
        self.writeId(output, offset, self.getId(entity))
        buffer.copy(output, offset + self.idSize, self.getStateBuffer(entity), 0, self.stateSize)
        offset += self.idSize + self.stateSize
    end

    assert(offset == totalBytes, `Packet mismatch: wrote {offset} bytes, expected {totalBytes}`)
    self.remote:FireAllClients(output)
end

Of course you cannot have a Noita inspired game without blowing up the environment. To emulate chaotic terrain carving, I built a highly performant destruction system using real time boolean operations. It utilizes Roblox GeometryService to dynamically subtract explosive spheres from the environment. To prevent the main thread from stalling during heavy multicasts, the system breaks large maps into spatial chunks so boolean operations are strictly isolated. The resulting mesh parts are cached and offloaded to the client for rendering, keeping the server focused strictly on collision bounds.

Finally to tie it all together, I wanted the UI to feel tactile. Instead of traditional flat 2D menus, players interact with a physical 3D UI. You literally grab 3D spell cards in world space and slot them onto a physical deck to assemble your wands. The physical ordering directly drives the virtual pass, giving real time visual feedback on how the chaotic math under the hood will resolve.