MODFORGEWORLD
← Knowledge base

Moving Platforms & Riding Entities

moving-platforms-riding-entitiesv1updated 6h ago

Moving Platforms & Riding Entities

Part of the 7DTD Modding Knowledgebase. How to make a runtime-moved collider (elevator car, moving platform — see Moving Blocks at Runtime) carry players, zombies and vehicles without blocking their own movement. Everything verified against decompiled V2.x (2026-07, Elevator mod).


The wrong way: per-frame SetPosition on riders

Teleporting every entity in the car volume each frame "works" but stomps movement:

  • EntityPlayerLocal.SetPositionvp_FPController.SetPosition, which resets m_PrevPos / m_SmoothPosition / m_FixedPosition and calls Physics.SyncTransforms() — every frame. The UFPS controller's own motor fights it; riding feels frozen/jittery and input barely registers.
  • It drags bystanders: anything whose AABB clips the moving volume (someone half-on a landing the car passes) gets yanked.

Layer table (TagManager, V2.x)

Read from 7DaysToDie_Data\data.unity3d → TagManager via UnityPy (obj.read_typetree()['layers']; UnityPy must run under the Python that has it installed — C:/Python311/python.exe on this machine):

#Layer#Layer
0Default16TerrainCollision
1TransparentFX17Physics Dead
2Ignore Raycast18Grass
3CC RemotePlayer Physics19LargeEntityBlocker
4Water20CC Local Physics
5UI21Physics
7LightLocal22UnderwaterEffect
8NoShadow23Trees
9Background Image24LocalPlayer
10HoldingItem25NotInReflections
11RenderInTexture26ReflectionsOnly
12NGUI28Terrain
13Items29Wires
14NoCollision30Glass
15CC Physics31Volumes

Collision-matrix facts that matter for platforms (PhysicsManager m_LayerCollisionMatrix, same dump):

  • 13 "Items" (vanilla falling-block layer) collides with vehicles (21) and items, but with no character controller — not local player (20), not zombies (15), not remote players (3). A proxy on 13 is not ground to anyone walking; riders stay on it only if you teleport them.
  • 16 "TerrainCollision" (chunk colliders) collides with every mover: 20, 15, 3, 21, 13, 17, 24. Put a rideable platform's colliders here and players/zombies/vehicles/items treat it exactly like world geometry — they can step on and off at will.
  • The vp_FPController ground probe (SphereCast mask 1084850184) hits layers {3, 15, 16, 19, 21, 23, 30} only. A platform outside that set never registers as ground for the local player.

Move everything per RENDER frame, not per fixed step

Rule learned the hard way (Elevator, 2026-07): any part of the platform-rider system that advances at fixed-step rate (~50 Hz) stair-steps ~5 cm per tick against 100+ fps rendering and reads as shake/blur — the camera judders against the world and the car mesh. This rules out the two "proper physics" approaches:

  • rb.MovePosition from FixedUpdate + interpolation renders the platform smoothly, but every rider still moves in fixed steps.
  • The engine's UFPS moving-platform code (below) tracks the player only in FixedUpdate and deliberately snaps vertical smoothing while the platform's y moves (SmoothMove: m_SmoothPosition.y = Transform.position.y when m_Platform y changed) — vertical judder is by design there; only the smoothing patch-out and per-frame platform motion would fix it, and the smoothing lerp is far too slow (τ ≈ 1 s) to lean on.

The butter-smooth recipe: advance the glide in Update, set the proxy root's transform.position every frame (world - Origin.position, which also absorbs Origin repositions for free), and carry riders per frame as below. Driven vehicles still ride fine on a per-frame-teleported kinematic collider: their suspension re-solves each physics step and absorbs the missing contact velocity (bounce amplitude ≈ g·dt² — invisible).

Local player: state-preserving controller shift (per frame)

Two dead ends first:

  • EntityPlayerLocal.SetPositionvp_FPController.SetPosition resets m_PrevPos/m_SmoothPosition/m_FixedPosition + Physics.SyncTransforms() every call — per-frame use stomps input and feels frozen.
  • The dormant UFPS platform support (m_Platform, UpdatePlatformMove, jump-off velocity inheritance) is functional and revivable — vanilla's trigger m_GroundHit...layer == 28 is dead because the ground probe mask excludes 28 — via a postfix on vp_FPController.UpdateCollisions setting __instance.m_Platform when grounded on your proxy. It gives free walking and jumping, but stair-steps the camera at fixed-step rate (see above). Works; not smooth. Don't ship it for a slow vertical platform.

What ships: shift the controller and all of its cached positions by the platform's per-frame delta, in one piece. A uniform translation is invisible to the motor — walking/sprint/jump input keeps working, nothing is reset, no fall impacts, and the camera glides at render rate:

vp_FPController c = player.vp_FPController;          // publicized property
if (c == null || !c.m_Grounded) return;              // airborne: don't carry
Collider ground = c.m_GroundHit.collider;
if (ground == null || !ground.transform.IsChildOf(proxyRoot.transform)) return;
Vector3 shift = new Vector3(0f, dy, 0f);
c.transform.position += shift;
c.m_SmoothPosition   += shift;
c.m_FixedPosition    += shift;   // FixedUpdate rewinds transform to this — must move too
c.m_PrevPos          += shift;   // used for velocity derivation
player.fallDistance = 0f;

Grounding stays stable because the shift preserves relative geometry exactly: the fixed-step ground probe sees the same arrangement every time. Jumping clears m_Grounded next fixed step, which ends the carry cleanly.

Vehicles

Driven vehicles are dynamic rigidbodies (layer 21) — they ride the proxy colliders through their suspension with no help (see per-frame note above).

Scripted-locomotion vehicles (Route B, e.g. Oppressor) are a platform-side blind spot: their soft-floor logic reads World.GetHeight (voxel heightmap), which reports the floor gone while the platform's blocks ride as a proxy — the vehicle sinks through and snaps back up when the blocks land. The manual carry can't save it: the vehicle's own per-tick SetPosition overrides the carry every tick. Fix belongs in the vehicle: make its floor sensing collider-aware (max of voxel floor and a downward SphereCast on layer 16) — see the "Soft floor" section of Vehicles.

Parked vehicles are a different beast: isKinematic = true and they settle through Entity.aabbEntityCollisionvoxel-AABB collision that ignores Unity colliders entirely. A parked car on your platform must be carried manually (vertical delta + clamp motion.y up to 0 + fallDistance = 0).

Zombies / animals / remote players

Entity locomotion dispatch (Entity.entityCollision): entities with a m_characterController collide via PhysX (ccEntityCollisionCharacterControllerKinematic, a wrapper around the KinematicCharacterController package); entities without one use voxel AABB (aabbEntityCollision). Zombie/animal CCs are on layer 15, so a layer-16 platform is solid ground to them — but the game constructs KCC motors with InteractiveRigidbodyHandling = false, so KCC's own moving-platform ride is off. Descent works by gravity alone; for ascent (and voxel-collision entities) apply the per-frame vertical delta manually via SetPosition(pos + dy) — additive, so their own AI movement keeps working.

Filter who you carry — probe straight down from the entity and require the hit to be your platform, so real floors between entity and platform win:

Vector3 origin = rider.position - Origin.position + Vector3.up * 0.4f;
bool riding = Physics.SphereCast(origin, 0.3f, Vector3.down, out var hit, 2f,
                  1 << 16) && hit.transform.IsChildOf(proxyRoot.transform);

Skip EntityPlayerLocal (platform patch above), skip rider.AttachedToEntity != null (seated — moves with the mount), skip EntityVehicle with !vehicleRB.isKinematic (driven — solver carries it).

Block-targeting rays now hit the platform (layer 16)

Voxel.raycastNew masks include 16 but exclude 13 — that's why vanilla falling blocks hide on 13. Resolution is tag-driven, not layer-driven: an untagged collider hit returns a generic non-block hit (bHitValid, voxelRayHitInfo.transform set), and T_Block-tagged clones resolve via FindMasterBlockForEntityModelBlock with a graceful fallback when the voxel underneath is air. Net effect: bullets/melee stop on the moving car without damaging anything, no NREs observed in the paths read. If a platform must be transparent to targeting, layer 13 is the hiding spot — but then nothing can walk on it.

Crushing what a DESCENDING platform lands on

Left to physics, a layer-16 proxy floor coming down on a creature standing in the shaft below just grinds it into the terrain (half-buried, still alive) — the proxy collider pushes the KCC down, the KCC pins it to the ground. To make the car crush instead (Elevator, 2026-07), kill anything the underside descends onto with vanilla falling-block damage:

victim.DamageEntity(DamageSource.fallingBlock, 1_000_000, true, 1f);

DamageSource.fallingBlock is the ready-made (External, Crushing) preset (same one EntityFallingBlock uses). A huge strength one-shots even a high-gamestage feral; _criticalHit:true triggers dismemberment/gib. No attacker id, so FriendlyFireCheck(null) (base returns true) and the zombie-vs-zombie guard (needs a non-null attacker) both pass — it lands on zombies fine.

Run it only on descent (dy < 0), and separate victims (below the car) from riders (on/inside the cab) so you never crush who you're carrying:

  • Vertical gate — the key discriminator: victim.position.y > carBottom + 0.2 → skip. carBottom = the car's world-space underside (BoundsMin.y + floatOffset). Anyone standing on the roof or on the cab floor sits above the underside; only things whose feet are below it are being landed on. This alone excludes all riders (grounded or mid-jump), unlike the downward StandingOnCar probe which misses an airborne cab occupant.
  • Solid-above confirm — CarOverhead: cast a ray up from just above the feet (position + up*0.1, always below the underside, so it never starts embedded) over GetHeight() + 0.3 + abs(dy), mask 1<<16, require hit.transform.IsChildOf(proxyRoot). This (a) only fires where the car is actually solid over that column — an L-shaped car's open columns don't crush — and (b) since layer 16 is shared with terrain, a real block between them is hit first (IsChildOf false) and shields the victim.

Two robustness notes: cast a ray, not a SphereCast — a SphereCast started overlapping the ground floor hits the documented zero-distance gotcha, and a ray up from +0.1 above the feet points away from the ground so it never snags it. And extend the reach by abs(dy) because at high speed × low fps a single step (up to ~2 m at the 32 m/s cap) can otherwise skip the contact frame entirely — the extension guarantees the sweep is caught.

Filter order: as EntityAlive (drops vehicles/items) → skip EntityPlayer and IsDead() → skip AttachedToEntity != null → vertical gate → CarOverhead. Query candidates with GetEntitiesInBounds over the footprint extended ~3 m below the underside.