Body step 2.1: climbing, mantling, crawling, legs, playground, 144 Hz

- Step rule + lower-body collider: no walking onto tables, steps <= 0.35 m
- Structural hand strength: two-hand mantle/pull-up works, one hand can't
- Arms aim from the physical shoulder horizontally, keep True Body pose vertically
- Crawl pose, crawl carry, 35 deg slope limit, ground feed-forward ignores
  play-space corrections (fixes a runaway feedback loop)
- Grab static world (climbing), procedural visual legs
- Playground sandbox; menu strength slider, spectator (old drag) and turn toggles
- Physics 144 Hz with interpolation; legacy chase-body spikes pin 90 Hz
- RagdollSpike: 27 phases incl. mantle, pull-up, crawl, ledge drop, ramps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
FoxyzandClaude Opus 5.5 committed 2026-10-02 07:56:08 +03:00
1 parent d83273dd2f
commit 2aff2b61da
26 files changed
+1243 -70

No files matched your search

+243
View File
@@ -0,0 +1,243 @@
using Godot;
using System.Collections.Generic;
// The body-physics sandbox (Nimso-sandbox style), built in code around the spawn/table area:
// EAST — mantling ledges (0.3 … 1.5 m), a 1 m platform with stairs, a walkable 18° ramp and a
// too-steep 40° one, a climbing wall with holds onto a 2.4 m top, a pull-up bar, monkey bars.
// WEST — physics toys: a plank bridge on two blocks (walk on it / knock it off), loose planks,
// a seesaw, a hanging swing board (grab it and hang), a crate pyramid, a heavy box,
// barrels, a balance beam.
// SOUTH — a crawl tunnel (0.6 m clearance), a duck-under bar (1.2 m) and a doorway.
// Static pieces are world (layer 1) and grabbable for climbing (PropGrabber grabs any static body
// not in "no_grab"); dynamic pieces are "prop"s (grab/push) and come back with Reset().
public partial class Playground : Node3D
{
private readonly List<(RigidBody3D Body, Transform3D Start)> _dynamic = new();
private const uint WorldLayer = 1, WorldMask = 0b11101, PropMask = 0b11101;
private static readonly Color Concrete = new(0.62f, 0.62f, 0.6f), Wood = new(0.72f, 0.55f, 0.33f),
DarkWood = new(0.5f, 0.36f, 0.2f), Metal = new(0.45f, 0.47f, 0.52f), Hold = new(0.9f, 0.45f, 0.15f),
Blue = new(0.3f, 0.45f, 0.75f), Green = new(0.35f, 0.6f, 0.35f), Red = new(0.7f, 0.25f, 0.2f);
public override void _Ready()
{
BuildEast();
BuildWest();
BuildSouth();
}
public void Reset()
{
foreach (var (body, start) in _dynamic)
{
if (!IsInstanceValid(body))
continue;
body.GlobalTransform = start;
body.LinearVelocity = Vector3.Zero;
body.AngularVelocity = Vector3.Zero;
body.ResetPhysicsInterpolation();
}
}
// ---------------------------------------------------------------- builders
private StaticBody3D Block(string name, Vector3 size, Vector3 center, Color color, Basis? basis = null)
{
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(basis ?? Basis.Identity, center) };
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
body.AddChild(Mesh(new BoxMesh { Size = size }, color));
AddChild(body);
return body;
}
private StaticBody3D Bar(string name, Vector3 from, Vector3 to, float radius, Color color)
{
Vector3 axis = to - from;
float len = axis.Length();
Vector3 y = axis / len;
Vector3 x = Mathf.Abs(y.Y) < 0.99f ? Vector3.Up.Cross(y).Normalized() : Vector3.Right;
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(new Basis(x, y, x.Cross(y)), (from + to) * 0.5f) };
body.AddChild(new CollisionShape3D { Shape = new CylinderShape3D { Radius = radius, Height = len } });
body.AddChild(Mesh(new CylinderMesh { TopRadius = radius, BottomRadius = radius, Height = len, RadialSegments = 12 }, color));
AddChild(body);
return body;
}
private RigidBody3D Prop(string name, Shape3D shape, Mesh mesh, float mass, Vector3 center, Color color, Basis? basis = null, float friction = 0.8f)
{
RigidBody3D body = new()
{
Name = name,
Mass = mass,
Transform = new Transform3D(basis ?? Basis.Identity, center),
CollisionMask = PropMask,
ContactMonitor = true,
MaxContactsReported = 4,
PhysicsMaterialOverride = new PhysicsMaterial { Friction = friction, Bounce = 0.05f },
};
body.AddToGroup("prop");
body.AddChild(new CollisionShape3D { Shape = shape });
body.AddChild(Mesh(mesh, color));
AddChild(body);
_dynamic.Add((body, body.GlobalTransform));
return body;
}
private RigidBody3D BoxProp(string name, Vector3 size, float mass, Vector3 center, Color color, Basis? basis = null) =>
Prop(name, new BoxShape3D { Size = size }, new BoxMesh { Size = size }, mass, center, color, basis);
private static MeshInstance3D Mesh(Mesh mesh, Color color)
{
MeshInstance3D m = new() { Mesh = mesh };
m.SetSurfaceOverrideMaterial(0, new StandardMaterial3D { AlbedoColor = color, Roughness = 0.75f });
return m;
}
private void Sign(string text, Vector3 at)
{
AddChild(new Label3D
{
Text = text,
FontSize = 96,
PixelSize = 0.002f,
OutlineSize = 12,
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
Position = at,
});
}
// ---------------------------------------------------------------- zones
private void BuildEast()
{
// Mantling ledges: walk up to one, put both hands on it and press yourself up.
float[] heights = { 0.3f, 0.6f, 0.9f, 1.2f, 1.5f };
for (int i = 0; i < heights.Length; i++)
{
float h = heights[i];
Block($"Ledge{h:0.0}", new Vector3(1.2f, h, 1.2f), new Vector3(4.5f + i * 1.9f, h * 0.5f, -8f), Concrete);
Sign($"{h:0.0} m", new Vector3(4.5f + i * 1.9f, h + 0.35f, -7.3f));
}
Sign("Mantle: both hands on the ledge, push down", new Vector3(8.3f, 2.4f, -8f));
// Platform 1 m high with stairs (0.17 m risers) on the south side and ramps on the north.
Block("Platform", new Vector3(3f, 1f, 3f), new Vector3(8f, 0.5f, -1.5f), Concrete);
for (int i = 0; i < 5; i++)
{
float top = 0.17f * (i + 1);
Block($"Stair{i}", new Vector3(1.2f, top, 0.3f), new Vector3(8f, top * 0.5f, 0.15f + (4 - i) * 0.3f), Concrete.Lightened(0.1f));
}
float walkable = Mathf.DegToRad(18f);
float rampLen = 1f / Mathf.Sin(walkable);
// Negative X rotation lifts the slab's +Z end, which touches the platform's north edge.
Block("RampWalkable", new Vector3(1.2f, 0.1f, rampLen), new Vector3(7f, 0.5f, -3f - Mathf.Cos(walkable) * rampLen * 0.5f),
Green, new Basis(Vector3.Right, -walkable));
float steep = Mathf.DegToRad(40f);
float steepLen = 1f / Mathf.Sin(steep);
Block("RampSteep", new Vector3(1.2f, 0.1f, steepLen), new Vector3(9f, 0.5f, -3f - Mathf.Cos(steep) * steepLen * 0.5f),
Red, new Basis(Vector3.Right, -steep));
Sign("18° ramp", new Vector3(7f, 1.6f, -4.5f));
Sign("40° (too steep)", new Vector3(9f, 1.6f, -4.5f));
// Climbing wall: holds on the west face, top at 2.4 m to climb onto.
Block("ClimbWall", new Vector3(1.5f, 2.4f, 3f), new Vector3(12.25f, 1.2f, -1.5f), Concrete.Darkened(0.15f));
RandomNumberGenerator rng = new() { Seed = 5 };
for (float y = 0.4f; y < 2.35f; y += 0.35f)
{
for (int k = 0; k < 4; k++)
{
float z = -2.7f + k * 0.8f + rng.RandfRange(-0.15f, 0.15f);
Block("Hold", new Vector3(0.08f, 0.06f, 0.22f), new Vector3(11.46f, y + rng.RandfRange(-0.06f, 0.06f), z), Hold);
}
}
Sign("Climbing wall: grip the holds", new Vector3(11f, 3f, -1.5f));
// Pull-up bar and monkey bars.
Bar("PostA", new Vector3(5f, 0f, 2.5f), new Vector3(5f, 2.25f, 2.5f), 0.05f, Metal);
Bar("PostB", new Vector3(6.4f, 0f, 2.5f), new Vector3(6.4f, 2.25f, 2.5f), 0.05f, Metal);
Bar("PullUpBar", new Vector3(5f, 2.1f, 2.5f), new Vector3(6.4f, 2.1f, 2.5f), 0.025f, Metal);
Sign("Pull-up bar", new Vector3(5.7f, 2.6f, 2.5f));
Bar("MonkeyRailL", new Vector3(5f, 2.25f, 4.5f), new Vector3(10f, 2.25f, 4.5f), 0.04f, Metal);
Bar("MonkeyRailR", new Vector3(5f, 2.25f, 5.1f), new Vector3(10f, 2.25f, 5.1f), 0.04f, Metal);
foreach (float x in new[] { 5f, 10f })
foreach (float z in new[] { 4.5f, 5.1f })
Bar("MonkeyPost", new Vector3(x, 0f, z), new Vector3(x, 2.25f, z), 0.05f, Metal);
for (float x = 5.4f; x < 9.8f; x += 0.4f)
Bar("Rung", new Vector3(x, 2.2f, 4.5f), new Vector3(x, 2.2f, 5.1f), 0.022f, Metal);
Sign("Monkey bars", new Vector3(7.5f, 2.8f, 4.8f));
}
private void BuildWest()
{
// Plank bridge: two blocks with a loose 9 kg plank across — walk on it, or knock it off.
Block("BridgeBlockA", new Vector3(1f, 0.8f, 1f), new Vector3(-4.5f, 0.4f, -5f), Concrete);
Block("BridgeBlockB", new Vector3(1f, 0.8f, 1f), new Vector3(-8f, 0.4f, -5f), Concrete);
BoxProp("BridgePlank", new Vector3(3.4f, 0.06f, 0.4f), 9f, new Vector3(-6.25f, 0.83f, -5f), Wood);
Sign("Plank bridge", new Vector3(-6.25f, 1.6f, -5f));
// Loose planks to pick up, carry, lean, swing (light enough to wield, long enough to lever).
for (int i = 0; i < 3; i++)
BoxProp($"Plank{i}", new Vector3(2f, 0.04f, 0.2f), 4f, new Vector3(-5f, 0.02f + i * 0.045f, -2.6f + i * 0.05f), Wood.Lightened(0.05f * i),
new Basis(Vector3.Up, 0.1f * i));
// Seesaw: a plank hinged on a pivot block.
Block("SeesawPivot", new Vector3(0.3f, 0.45f, 0.5f), new Vector3(-8.5f, 0.225f, -1.5f), Metal);
RigidBody3D seesaw = BoxProp("Seesaw", new Vector3(3.2f, 0.06f, 0.45f), 12f, new Vector3(-8.5f, 0.48f, -1.5f), DarkWood);
HingeJoint3D pivot = new()
{
Transform = new Transform3D(new Basis(Vector3.Right, Vector3.Up, Vector3.Back), new Vector3(-8.5f, 0.48f, -1.5f)),
NodeA = GetPathTo(GetNode("SeesawPivot")),
NodeB = GetPathTo(seesaw),
};
AddChild(pivot);
Sign("Seesaw", new Vector3(-8.5f, 1.3f, -1.5f));
// Swing board: a 10 kg board hinged at its top edge from a 2.4 m beam — grab it and hang.
Bar("SwingPostA", new Vector3(-5f, 0f, 1.5f), new Vector3(-5f, 2.45f, 1.5f), 0.05f, Metal);
Bar("SwingPostB", new Vector3(-7f, 0f, 1.5f), new Vector3(-7f, 2.45f, 1.5f), 0.05f, Metal);
StaticBody3D beam = Bar("SwingBeam", new Vector3(-5f, 2.4f, 1.5f), new Vector3(-7f, 2.4f, 1.5f), 0.05f, Metal);
RigidBody3D board = BoxProp("SwingBoard", new Vector3(1.2f, 1.2f, 0.05f), 10f, new Vector3(-6f, 1.75f, 1.5f), Wood);
HingeJoint3D swing = new()
{
Transform = new Transform3D(new Basis(Vector3.Back, Vector3.Up, Vector3.Left), new Vector3(-6f, 2.35f, 1.5f)),
NodeA = GetPathTo(beam),
NodeB = GetPathTo(board),
};
AddChild(swing);
Sign("Hanging board", new Vector3(-6f, 2.9f, 1.5f));
// Crate pyramid (4 kg each), a heavy 25 kg box, barrels.
Vector3 basePos = new(-9.5f, 0f, 2f);
int n = 0;
for (int row = 0; row < 3; row++)
for (int col = 0; col < 3 - row; col++)
BoxProp($"Crate{n++}", Vector3.One * 0.4f, 4f, basePos + new Vector3((col + row * 0.5f) * 0.42f, 0.2f + row * 0.401f, 0f), Wood.Darkened(0.1f));
BoxProp("HeavyBox", Vector3.One * 0.7f, 25f, new Vector3(-9f, 0.35f, 4f), Blue);
for (int i = 0; i < 2; i++)
Prop($"Barrel{i}", new CylinderShape3D { Radius = 0.28f, Height = 0.85f }, new CylinderMesh { TopRadius = 0.28f, BottomRadius = 0.28f, Height = 0.85f }, 12f,
new Vector3(-7.5f + i * 0.7f, 0.425f, 4.5f), Red);
// Balance beam.
Block("BalanceBeam", new Vector3(0.12f, 0.3f, 4f), new Vector3(-11f, 0.15f, -2f), DarkWood);
Sign("Balance beam", new Vector3(-11f, 1.1f, -2f));
}
private void BuildSouth()
{
// Crawl tunnel: 0.6 m clearance, 3 m long.
Block("TunnelRoof", new Vector3(1.4f, 0.1f, 3f), new Vector3(-2.5f, 0.65f, 7f), Concrete);
Block("TunnelWallL", new Vector3(0.1f, 0.6f, 3f), new Vector3(-3.15f, 0.3f, 7f), Concrete);
Block("TunnelWallR", new Vector3(0.1f, 0.6f, 3f), new Vector3(-1.85f, 0.3f, 7f), Concrete);
Sign("Crawl tunnel (0.6 m)", new Vector3(-2.5f, 1.2f, 5.3f));
// Duck-under bar at 1.2 m and a doorway (1.9 × 0.8 m).
Bar("DuckPostA", new Vector3(1f, 0f, 7f), new Vector3(1f, 1.3f, 7f), 0.05f, Metal);
Bar("DuckPostB", new Vector3(2.6f, 0f, 7f), new Vector3(2.6f, 1.3f, 7f), 0.05f, Metal);
Bar("DuckBar", new Vector3(1f, 1.25f, 7f), new Vector3(2.6f, 1.25f, 7f), 0.03f, Metal);
Sign("Duck under (1.2 m)", new Vector3(1.8f, 1.8f, 7f));
Block("DoorL", new Vector3(0.2f, 2.1f, 0.3f), new Vector3(4.1f, 1.05f, 7f), Wood);
Block("DoorR", new Vector3(0.2f, 2.1f, 0.3f), new Vector3(5.1f, 1.05f, 7f), Wood);
Block("DoorTop", new Vector3(1.2f, 0.2f, 0.3f), new Vector3(4.6f, 2.0f, 7f), Wood);
Sign("Doorway", new Vector3(4.6f, 2.5f, 7f));
}
}