- 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>
244 lines
13 KiB
C#
244 lines
13 KiB
C#
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));
|
||
}
|
||
}
|