This is an engineering devlog. If you just want to use the Modular Kit Snapping Tool, you don't need any of this — go read the docs. If you're a tools programmer who's about to build something similar, or you're curious why a "snap system" is harder than it looks, read on.
The shape of the problem
A modular kit snapping system has to answer four questions every time two pieces meet:
- Should these two pieces connect at all? (a Wall and a Floor probably shouldn't connect at the floor's middle, but a Wall and a Door should connect at the doorway)
- Where exactly do they connect? (which point on piece A meets which point on piece B)
- In what orientation? (after connecting, A should face B's connection face, not the other way)
- Is this connection valid? (under the project's rules — a window can't be a foundation)
The natural temptation is to model these with actor sockets, because UE actor sockets already have a name, a transform, and an associated mesh. Most internal snap systems we've seen start there.
We tried it. It didn't work. Here's why.
Why actor sockets fail this problem
UE actor sockets are FName-keyed transforms attached to a USkeletalMeshComponent or via mesh sockets on a UStaticMeshComponent. They were designed for two use cases:
- Attaching things to character bones (a sword to a hand socket)
- Marking specific mesh locations for spawning effects (a muzzle flash on a weapon)
Both of those are one-way attachment problems. The thing being attached doesn't need to negotiate with the socket — it just goes there.
Modular kit snapping is a two-way negotiation problem. Both pieces need to agree:
- Their types are compatible
- Their faces are anti-parallel
- They're within distance tolerance
- One of them isn't an anchor that should refuse to move
None of that fits in a socket name. You can encode it in the socket name (Snap_Wall_Right_Optional_50 is a real thing we've seen), but at that point you've reinvented a data structure inside a string, and now you have a second problem: every script that touches sockets has to parse the convention, and the convention varies per studio.
Actor sockets are a string. We needed a record.
Why we rejected USceneComponent subclasses-without-properties too
The next natural attempt: subclass USceneComponent and use the class itself as the type marker (UWallSnapComponent, UFloorSnapComponent, ...). This is more idiomatic UE than name parsing, and you get type safety from the engine for free.
It also doesn't work — for the opposite reason. With one subclass per type, you can't easily express:
- A snap point that connects across types (Wall ↔ Door)
- A rule set that changes which types are compatible per-project
- Per-instance overrides
You'd end up with a combinatorial explosion of subclasses or a parallel string-typed override on each component anyway.
What we landed on
A single USnapPointComponent class with a small, deliberately flat property set:
ESnapPointType SnapType; // 8 enum values
ESnapRule SnapRule; // MustConnect / Optional / Blocked
ESnapDirection SnapDirection; // ±X, ±Y, ±Z
FName SocketID; // optional, for lookup
TArray<ESnapPointType> CompatibleTypes; // per-component override
float SnapRadius;
Six properties, no subclasses, no name conventions, no inheritance hierarchy.
The interesting design choices are the things we didn't add.
We didn't add a "snap to grid" property
Tempting, because grid snapping is what people ask for. But the moment you encode a grid into the snap point, you've coupled your kit to that grid's resolution. A grid is a property of the kit, not of an individual snap point. We pushed it out to the rule set (GridSize).
In practice the runtime doesn't even use GridSize for matching — it's there for downstream tooling that might want to round transforms. Snap matching uses radius and angle tolerance, both of which work continuously without a grid.
We didn't add a "compatibility list" as the primary mechanism
CompatibleTypes exists on the component, but it's intentionally a fallback. The primary mechanism is the rule set's compatibility matrix. Why? Because compatibility is a project-wide policy, not a per-component decision. A studio standardizing on "Walls connect to Doors" wants that to be a single asset they version-control, not 200 component instances each carrying the same array.
We kept the per-component list because there are real one-off cases (a special wall piece that connects to nothing), but the rule set is what we point teams at.
We didn't add scoring weights
The matching algorithm is naive on purpose. For each non-anchor piece, we test every snap point against every already-registered snap point, score by distance + angle, take the best. That's it.
We considered weighting by type priority, by user-assigned weight, by historical match preference, by piece "anchorness" gradient. We tried two of those internally. Both made the system harder to predict.
A snap system has to be predictable above all else. A level designer placing a piece needs to know, looking at the snap points on screen, which one will win. If the algorithm is "closest, most anti-parallel, with these tie-breakers," they can predict it. If it includes user-assigned weights, they have to remember the weights. If it learns from history, they're guessing.
We dropped scoring complexity. The algorithm is "closest match wins" and the documentation says exactly that.
We didn't add multiplayer replication on the manager
This is the question we get most. The answer is: replication is a coupling problem, not a snap problem.
A modular kit snap system needs to work in three contexts:
- Editor — placing pieces in a level, no replication possible
- Single-player runtime — placing pieces in a survival game, no replication needed
- Multiplayer runtime — placing pieces in a co-op survival game, replication needed
If we replicated the SnapConnectionManager automatically, we'd force every editor user to inherit replication overhead they don't need, and we'd impose a specific replication model on every multiplayer game that doesn't want ours.
So we made the manager a non-replicated UActorComponent and exposed RegisterPiece / MakeConnection / BreakConnection as BlueprintCallable. A multiplayer game wires those calls through their own RPCs and replication strategy. The 5% of users who need replication get to choose their model. The 95% who don't pay zero overhead.
This is a deliberate trade-off and it's the right one for a focused plugin. A full building system makes the opposite trade — and that's why a full building system is a different product.
What MustConnect validation actually does
Validation is the feature we use the most internally and the one we'd recommend to anyone evaluating this category.
The implementation is mundane: walk every tracked piece, count MustConnect snap points where bIsConnected == false, return the boolean. About 30 lines.
The non-obvious part is the editor surface. We exposed:
- A live count in the Details Panel (snap point count, connected count, mandatory unconnected count)
- A red dot in the viewport visualizer for unconnected
MustConnectpoints - A
OnValidationResultBlueprint event for hooking into editor utilities or in-game tools
That last one is the bit that makes this useful. Wiring OnValidationResult to a level review widget — or to your CI — turns "doorway opens to nothing" from a playtest discovery into a build-time error.
If you build any editor tool on top of this plugin, build that one. It pays for itself the first time it catches a broken doorway.
What we'd do differently
If we shipped v2, we'd add:
- A per-snap-point preview component — a transparent ghost mesh that previews where the connection would land. Useful for runtime placement UX.
- A bulk import for snap point setup — for kits with 100+ pieces, adding snap points one at a time is tedious. A "configure all walls in this folder" tool would help.
- A "connection stability" metric — for procgen use cases, knowing how strongly connected a kit is (how many redundant connections exist) is useful.
We left these out because v1 had to be focused. The tool does one thing — snap pieces together with rules — and ships at $14.99. Adding the v2 features would push it toward the "full building system" category, which is a different product at a different price point.
What this is not, again
We say this in the docs and the launch post and we'll say it here:
This is not a procedural building generator. It snaps pieces you've already placed.
This is not a runtime placement system. It snaps pieces you spawn, but it doesn't include placement ghosts, can-place validation, inventory integration, or any of the survival-game scaffolding.
This is not a multiplayer building framework. No replication on the manager.
It's a focused tool. If you need any of the above, look at full building systems instead — see our comparison post for which category to pick.
Closing
The best engineering decisions on this plugin were the ones to leave things out. We could have shipped a 5,000-line system that did everything. Instead we shipped 1,600 lines that do one thing and stop.
If you find yourself reaching for a feature that isn't there, that's a signal — either you need a different category of tool, or you can build the thing on top of our Blueprint API. Both are fine. The one we wanted to avoid was the third option: us bundling that feature into the core and making the focused tool less focused for everyone.
Read the docs or grab the plugin. If you're a tools programmer with opinions about any of this, we genuinely want to hear them — reply to this email or hit the contact page.