Spawnable props, the limits that keep a server alive, and the tools that move them.
Requires dot-core.
Server-authoritative, and not predicted
Rigid-body physics is not reproducible across machines — the solver’s iteration order and the last bits of every float differ — so a predicted prop is a prop that is corrected constantly. The server owns every prop; the client draws what it is told; the tools send intent.
var spawner := DotPropSpawner.new()
spawner.authoritative = true # on the server only
spawner.catalogue = DotPropCatalogue.load_json("res://props.json").value
spawner.limits = DotPropLimits.new()
spawner.world_ref = DotNodeRef.of_path(^"../World")
add_child(spawner)
spawner.refused.connect(func(player, prop, reason): tell(player, reason))
spawner.advance(delta)
spawner.spawn(&"crate", player_id, aim_point, Basis.IDENTITY)
{
"format": 1,
"props": [
{"id": "crate", "name": "Crate", "category": "props",
"scene": "res://props/crate.tscn", "mass": 20},
{"id": "safe", "category": "props",
"scene": "res://props/safe.tscn", "mass": 900, "cost": 8},
{"id": "pillar", "category": "scenery",
"scene": "res://props/pillar.tscn", "can_grab": false}
]
}
The tools
var gun := DotPhysGun.new()
gun.spawner = spawner
gun.wielder = player_id
gun.grab(space_state, eye_position, aim_direction, view_basis)
gun.hold(eye_position, aim_direction, view_basis, delta) # every physics tick
gun.push(scroll_amount, delta)
gun.release()
gun.freeze_held()
var grav := DotGravGun.new()
grav.pull(space_state, eye_position, aim_direction)
grav.carry(eye_position, aim_direction, delta)
grav.punt(space_state, eye_position, aim_direction)
The physics gun holds with a spring, not a transform: setting a transform on a rigid body fights the solver and produces a prop that jitters and shoves everything it touches.
Breaking a prop, and standing on one
DotPropDamage holds hit points for props and applies damage to nobody. The numbers live on DotPropDef — max_health and break_impact_speed — so a definition stays checkable without loading its scene; the state is keyed by instance id, so a breakable prop is still a plain scene with a RigidBody3D at its root. break_impact_speed is a speed rather than damage, because a crate has to survive being walked into and must not survive a bus. max_health = 0 is indestructible, since every prop written before the field decodes to zero. When something explodes it describes the blast and shoves other props; what that does to a person is dot-combat’s. Signals: damaged, broken, exploded.
DotPropCarry is what makes a crate an object rather than scenery: a character motor sweeps a shape and slides, so without it a crate is exactly as solid and as immovable as the floor. stand() presses a player’s weight into the prop under them, ride() carries them with it — angular velocity included, or a player on a spinning crate drifts toward the middle — and push() shoves one. It reads the motor’s ground_id, which is a local handle and deliberately not part of the simulation.
Props from a folder of models
DotPropFolder.load_into() turns a folder of .glb and .gltf files into catalogue props with no code: a model that appears in a scanned directory is a prop, a subfolder is its category, and a props.json or a JSON sidecar next to a model (and a .png thumbnail) says anything else, such as a name, category or credit. DotPropModel is the model file as a body’s picture, hull and mass. The collision is shaped like the model, so a prop is not a box around its bounds. DotPropCatalogue.follow_content() lists a player’s subscribed Workshop props in solo; online shows none, and the game’s own ids win.
Toys
Toys are networked physics toys and interactables a map places by data: balls, a see-saw, a roundabout, a trampoline, a bell. DotPropToyBox holds them. The server simulates and clients draw what it sends.
- Shooting and blasting.
shot(collider, point, direction, damage)turns a hit-scan’s damage into a push and returns the toy’s index (or -1 when it was not one).shot_along(origin, direction, reach, damage, wall_mask)is for a game whose own trace never meets a toy, because a toy’s footprint is never in the physics space.blast(at, radius, speed)throws toys at the same speed whatever they weigh, and a shot bell rings. - Carrying. A roundabout carries the people standing on it round its hub, through
DotPropCarry.toys. - Cvars.
sv_toysswitches them off,sv_toys_awakecaps how many simulate at once, andsv_toys_pushsets how hard walking into one pushes it.DotPropToyCatalogue.merge_json()adds or retunes toys.
Where a game plugs in
- DotPropCatalogue / DotPropDef
What can be spawned. Id, name, category, scene, mass, cost,
can_grab, health, and ametadictionary. A delivered prop carries acontent_id, andensure_content()fetches the packs the catalogue’s props live in.- DotPropDef.meta.scripta PATH
A prop that has code. A path, not a
class_name— a mounted.pckcannot registerclass_nameglobals, so a path is the only shape a delivered prop can take. See game-playground.- DotPropLimits
Per-player counts, per-player frozen counts, a spawn cooldown, an undo depth, a total cap, and grab mass limits. Each one is defending against a specific way a sandbox server dies.
- DotPropTool subclass
effective_reach,target,may_act_on. A new sandbox tool, or a weapon.DotPhysGunandDotGravGunare the shipped ones.- Signals
spawned,removed,refusedon the spawner;grabbed,released,freeze_changedonDotPhysGun.
Ownership and undo are two lists
When they were one, trimming it to the undo depth removed props from their
owner’s account as well as their history — so a player could hold undo_depth
props against their budget and any number beyond it for free, and
clear_player left the rest in the world.
`DotPropDef.mass` has to reach the body
A catalogue saying 900 kg over a scene saved at 20 kg gives a prop a physics gun refuses for being too heavy and a gravity gun throws like a beach ball. It shows the moment a game ships one scene for a dozen props, which is the cheap way to do it.
