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).
Gameplay & Media Gallery
3D Wand Deck Customization
Grabbing 3D cards and slotting them on a physical deck to customize wands.
Spell Casting & Replication
Dynamic spell multicasts, triggers, and projectile physics replication.
Environmental Destruction
Real time voxel like terrain carving using Roblox GeometryService boolean operations.
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
-- 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
-- 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.