Today we started building Framework v2.
The v1 framework got us from zero to a working prototype in ten days, but it had grown organically and the seams were showing. Components were tightly coupled to the campfire game. There was no standard way to query game objects (“give me all enemies in this zone”). Adding a new component type meant copying an existing one and gutting it. The framework was useful, but it wasn’t reusable.
v2 is a ground-up rethink.
What v2 introduces
Node architecture — Every game entity is a Node. Nodes have a standard lifecycle (init, start, destroy), can declare inputs and outputs (signals), and are composed from smaller components. This is the ECS-adjacent pattern that a lot of modern game frameworks use, adapted for Roblox’s Instance hierarchy.
NodeRegistry — A central registry where nodes register themselves on creation. Any system can query the registry: “give me all nodes with the enemy tag,” “find the nearest node of type Checkpoint.” This replaces the ad-hoc CollectionService usage and manual reference passing from v1.
Components built today:
- PathFollower — Moves entities along waypoint paths with configurable speed and easing
- Hatcher — Spawns entities over time using a shared
SpawnerCoremodule - Dropper — Spawns items with physics (gravity, scatter)
- Zone — Spatial regions with enter/exit detection
- NodePool — Object pooling for frequently created/destroyed entities
- PathedConveyor — Moves objects along a path automatically (conveyor belts, assembly lines)
- Checkpoint — Save/restore points for player progress
Unified filter system
Multiple components needed to select subsets of entities — “all enemies,” “all entities in zone X,” “all entities that are alive.” Instead of each component rolling its own filtering, we built a shared filter system with composable predicates:
local filter = Filters.all(
Filters.hasTag("enemy"),
Filters.inZone("arena"),
Filters.isAlive()
)
local targets = NodeRegistry.query(filter)
Filters.all(), Filters.any(), and Filters.not() compose arbitrarily. The same filter works whether you’re querying the NodeRegistry, filtering a NodePool’s managed instances, or selecting targets for a Hatcher to spawn.
This is the kind of shared abstraction that v1 was missing. In v1, the ZoneController had its own overlap detection, the WaveController had its own entity tracking, and the Orchestrator had its own asset discovery. Same concept, three implementations. In v2, one filter system serves all of them.
We also wrote the Orchestrator design spec before building it — planning the v2 coordinator’s API on paper before committing to code. After the turret system (coming in two days) taught us the cost of building first and designing second, we started speccing complex components before implementation.