Maps are editor-built scenes: [Tool] Playground/Crag, headless rebuild for agents
scenes/maps/playground.tscn and crag.tscn are generated by the map scripts' Rebuild (inspector button, or godot-mono --headless --path . tools/rebuild_maps.tscn) and loaded as saved by the game, so hand edits stick. The crag route is saved as Marker3D nodes and crag_spike checks the saved scene. Layout verified identical to the code-built one (tools/map_dump.tscn). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
3e300c7f04
commit
df7a2c5a89
17 files changed
+6863
-65
No files matched your search
+89
-26
@@ -12,11 +12,20 @@ using System.Collections.Generic;
|
||||
// 6. a 0.9 m chimney with holds on both walls up the third tier;
|
||||
// 7. the summit (26 m) with throwable props. Way down: aim at the "Crag" teleport marker at the
|
||||
// base (or jump — no fall damage).
|
||||
// The cliff faces south (+Z) toward spawn. Every route point is recorded in Route so crag_spike
|
||||
// can check reach; route bodies in RoutePieces so it can check nothing intersects them.
|
||||
// The cliff faces south (+Z) toward spawn. EDITOR-BUILT: this is the root of scenes/maps/crag.tscn
|
||||
// and Rebuild() generates everything into that scene (MapScene: inspector button, or
|
||||
// tools/rebuild_maps.tscn headless); the game loads the saved scene. The route is saved too: every
|
||||
// route point is a Marker3D under "Route" (metadata kind/label, in climbing order), route bodies are in
|
||||
// group "crag_route" and rock in "crag_mass" — Route/RoutePieces/MassPieces are read back from the
|
||||
// saved nodes on _Ready, so crag_spike checks the scene as saved (and as hand-edited).
|
||||
[Tool]
|
||||
public partial class Crag : Node3D
|
||||
{
|
||||
[ExportToolButton("Rebuild (replaces all children)")]
|
||||
public Callable RebuildButton => Callable.From(RebuildInEditor);
|
||||
|
||||
public enum Step { Stand, Hold }
|
||||
public const string RouteNode = "Route", RouteGroup = "crag_route", MassGroup = "crag_mass";
|
||||
public readonly List<(Step Kind, Vector3 Point, string Name)> Route = new();
|
||||
public readonly List<(CollisionObject3D Body, Aabb Box)> RoutePieces = new();
|
||||
public readonly List<(CollisionObject3D Body, Aabb Box)> MassPieces = new();
|
||||
@@ -37,25 +46,76 @@ public partial class Crag : Node3D
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (Engine.IsEditorHint())
|
||||
return;
|
||||
MapScene.CollectProps(this, _dynamic);
|
||||
ReadLayout();
|
||||
}
|
||||
|
||||
public void Reset() => MapScene.Reset(_dynamic);
|
||||
|
||||
private void RebuildInEditor()
|
||||
{
|
||||
if (!MapScene.CanRebuildInEditor(this))
|
||||
return;
|
||||
Rebuild();
|
||||
MapScene.MarkUnsaved();
|
||||
}
|
||||
|
||||
// Replaces every child with the generated layout, owned by this map (saved with its scene).
|
||||
public void Rebuild()
|
||||
{
|
||||
MapScene.Clear(this);
|
||||
_rng.Seed = 26; // same rubble/talus every rebuild
|
||||
_route = new Node3D { Name = RouteNode };
|
||||
AddChild(_route);
|
||||
BuildMass();
|
||||
BuildRoute();
|
||||
BuildBase();
|
||||
BuildSummitProps();
|
||||
MapScene.Adopt(this);
|
||||
ReadLayout();
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
// Route points and piece bounds from the (saved) nodes.
|
||||
public void ReadLayout()
|
||||
{
|
||||
foreach (var (body, start) in _dynamic)
|
||||
Route.Clear();
|
||||
RoutePieces.Clear();
|
||||
MassPieces.Clear();
|
||||
if (GetNodeOrNull<Node>(RouteNode) is Node route)
|
||||
foreach (Node child in route.GetChildren())
|
||||
if (child is Marker3D m)
|
||||
Route.Add(((Step)(int)m.GetMeta("kind"), m.GlobalPosition, (string)m.GetMeta("label")));
|
||||
foreach (Node child in GetChildren())
|
||||
{
|
||||
if (!IsInstanceValid(body))
|
||||
if (child is not CollisionObject3D body)
|
||||
continue;
|
||||
body.GlobalTransform = start;
|
||||
body.LinearVelocity = Vector3.Zero;
|
||||
body.AngularVelocity = Vector3.Zero;
|
||||
body.ResetPhysicsInterpolation();
|
||||
if (body.IsInGroup(RouteGroup))
|
||||
RoutePieces.Add((body, Bounds(body)));
|
||||
else if (body.IsInGroup(MassGroup))
|
||||
MassPieces.Add((body, Bounds(body)));
|
||||
}
|
||||
}
|
||||
|
||||
// World bounds of a box or a cylinder piece (a cylinder's ends are flat: radius across its axis only).
|
||||
private static Aabb Bounds(CollisionObject3D body)
|
||||
{
|
||||
CollisionShape3D cs = body.GetChildOrNull<CollisionShape3D>(0);
|
||||
Transform3D t = body.GlobalTransform * cs.Transform;
|
||||
if (cs.Shape is CylinderShape3D cyl)
|
||||
{
|
||||
Vector3 from = t * new Vector3(0f, -cyl.Height * 0.5f, 0f), to = t * new Vector3(0f, cyl.Height * 0.5f, 0f);
|
||||
Vector3 d = (to - from).Normalized(), r = new(
|
||||
cyl.Radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.X * d.X)), cyl.Radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Y * d.Y)), cyl.Radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Z * d.Z)));
|
||||
return new Aabb(from - r, Vector3.Zero).Expand(from + r).Expand(to - r).Expand(to + r);
|
||||
}
|
||||
Vector3 size = ((BoxShape3D)cs.Shape).Size;
|
||||
return t * new Aabb(-size * 0.5f, size);
|
||||
}
|
||||
|
||||
private Node3D _route;
|
||||
|
||||
// ---------------------------------------------------------------- the rock
|
||||
|
||||
private void BuildMass()
|
||||
@@ -241,8 +301,9 @@ public partial class Crag : Node3D
|
||||
// The way down: a teleport marker at the base (aim at it from the summit).
|
||||
Node3D marker = GD.Load<PackedScene>("res://scenes/teleport_marker.tscn").Instantiate<Node3D>();
|
||||
marker.Set("MarkerLabel", "Crag");
|
||||
marker.Name = "CragMarker";
|
||||
marker.Position = new Vector3(1.5f, 0f, -9.5f);
|
||||
AddChild(marker);
|
||||
AddChild(marker, true);
|
||||
}
|
||||
|
||||
private void BuildSummitProps()
|
||||
@@ -258,45 +319,47 @@ public partial class Crag : Node3D
|
||||
|
||||
// ---------------------------------------------------------------- builders
|
||||
|
||||
private void AddStep(Step kind, Vector3 point, string name) => Route.Add((kind, point, name));
|
||||
// A route point: an editor-visible Marker3D under "Route", in climbing order.
|
||||
private void AddStep(Step kind, Vector3 point, string name)
|
||||
{
|
||||
Marker3D m = new() { Name = kind == Step.Hold ? "Hold" : "Stand", Position = point };
|
||||
m.SetMeta("kind", (int)kind);
|
||||
m.SetMeta("label", name);
|
||||
_route.AddChild(m, true);
|
||||
}
|
||||
|
||||
private StaticBody3D Mass(string name, Vector3 size, Vector3 center, Color color, Basis? basis = null)
|
||||
{
|
||||
StaticBody3D b = Block(name, size, center, color, true, basis);
|
||||
MassPieces.Add((b, BoxAabb(size, b.Transform)));
|
||||
b.AddToGroup(MassGroup, true);
|
||||
return b;
|
||||
}
|
||||
|
||||
private StaticBody3D RouteBlock(string name, Vector3 size, Vector3 center, Color color, bool castShadow = true)
|
||||
{
|
||||
StaticBody3D b = Block(name, size, center, color, castShadow);
|
||||
RoutePieces.Add((b, BoxAabb(size, b.Transform)));
|
||||
b.AddToGroup(RouteGroup, true);
|
||||
return b;
|
||||
}
|
||||
|
||||
private StaticBody3D RouteBar(string name, Vector3 from, Vector3 to, float radius, Color color)
|
||||
{
|
||||
StaticBody3D b = Bar(name, from, to, radius, color);
|
||||
// Bounds of the cylinder: radius across the axis only (the ends are flat).
|
||||
Vector3 d = (to - from).Normalized(), r = new Vector3(
|
||||
radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.X * d.X)), radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Y * d.Y)), radius * Mathf.Sqrt(Mathf.Max(0f, 1f - d.Z * d.Z)));
|
||||
RoutePieces.Add((b, new Aabb(from - r, Vector3.Zero).Expand(from + r).Expand(to - r).Expand(to + r)));
|
||||
b.AddToGroup(RouteGroup, true);
|
||||
return b;
|
||||
}
|
||||
|
||||
private static Aabb BoxAabb(Vector3 size, Transform3D t) => t * new Aabb(-size * 0.5f, size);
|
||||
|
||||
private StaticBody3D Block(string name, Vector3 size, Vector3 center, Color color, bool castShadow, Basis? basis = null, bool grab = true)
|
||||
{
|
||||
StaticBody3D body = new() { Name = name, CollisionLayer = WorldLayer, CollisionMask = WorldMask, Transform = new Transform3D(basis ?? Basis.Identity, center) };
|
||||
if (!grab)
|
||||
body.AddToGroup(PropGrabber.NoGrabGroup);
|
||||
body.AddToGroup(PropGrabber.NoGrabGroup, true);
|
||||
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
|
||||
MeshInstance3D mesh = Mesh(new BoxMesh { Size = size }, color);
|
||||
if (!castShadow)
|
||||
mesh.CastShadow = GeometryInstance3D.ShadowCastingSetting.Off;
|
||||
body.AddChild(mesh);
|
||||
AddChild(body);
|
||||
AddChild(body, true);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -309,7 +372,7 @@ public partial class Crag : Node3D
|
||||
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);
|
||||
AddChild(body, true);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -325,11 +388,10 @@ public partial class Crag : Node3D
|
||||
MaxContactsReported = 4,
|
||||
PhysicsMaterialOverride = new PhysicsMaterial { Friction = 0.8f, Bounce = 0.05f },
|
||||
};
|
||||
body.AddToGroup("prop");
|
||||
body.AddToGroup(MapScene.PropGroup, true);
|
||||
body.AddChild(new CollisionShape3D { Shape = shape });
|
||||
body.AddChild(Mesh(mesh, color));
|
||||
AddChild(body);
|
||||
_dynamic.Add((body, body.GlobalTransform));
|
||||
AddChild(body, true);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -347,12 +409,13 @@ public partial class Crag : Node3D
|
||||
{
|
||||
AddChild(new Label3D
|
||||
{
|
||||
Name = "Sign",
|
||||
Text = text,
|
||||
FontSize = 96,
|
||||
PixelSize = 0.002f * scale,
|
||||
OutlineSize = 12,
|
||||
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
|
||||
Position = at,
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// Regression for the crag (no player): the route is climbable on paper — consecutive route points
|
||||
// Regression for the crag (no player), checked on the SAVED scenes/maps/crag.tscn (route = its
|
||||
// Route markers; rebuild with tools/rebuild_maps.tscn): the route is climbable on paper — consecutive route points
|
||||
// within human reach, every point in free space, route pieces not buried in each other or the
|
||||
// rock — the summit is high enough, and it's in plain view from spawn. Prints "CRAG: overall PASS".
|
||||
public partial class CragSpike : Node3D
|
||||
@@ -23,8 +24,9 @@ public partial class CragSpike : Node3D
|
||||
floor.AddToGroup(PropGrabber.NoGrabGroup);
|
||||
floor.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = new Vector3(80, 0.1f, 80) }, Position = new Vector3(0, -0.05f, 0) });
|
||||
AddChild(floor);
|
||||
AddChild(new Playground());
|
||||
_crag = new Crag();
|
||||
// The SAVED scenes (as the game loads them, hand edits included), not a fresh rebuild.
|
||||
AddChild(GD.Load<PackedScene>("res://scenes/maps/playground.tscn").Instantiate());
|
||||
_crag = GD.Load<PackedScene>("res://scenes/maps/crag.tscn").Instantiate<Crag>();
|
||||
AddChild(_crag);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// Shared plumbing for the editor-built maps (Playground, Crag). Each map is the ROOT of its own scene
|
||||
// (scenes/maps/*.tscn) with a [Tool] script whose Rebuild() regenerates every child from code and
|
||||
// gives it to the scene (Owner = the map root) so it is saved into the .tscn like hand-made nodes.
|
||||
// The game never regenerates: it loads what was saved, so edits made in the editor stick (until the
|
||||
// next Rebuild, which replaces ALL children). Rebuild from the inspector button with the map scene
|
||||
// open, or headless for agents: godot-mono --headless --path . tools/rebuild_maps.tscn
|
||||
public static class MapScene
|
||||
{
|
||||
public const string PropGroup = "prop";
|
||||
|
||||
// The inspector button only works with the map scene itself open (nodes owned by an instanced
|
||||
// sub-scene's root aren't saved into the outer scene).
|
||||
public static bool CanRebuildInEditor(Node map)
|
||||
{
|
||||
if (map.GetTree()?.EditedSceneRoot == map)
|
||||
return true;
|
||||
GD.PushWarning($"{map.Name}: open its own scene (scenes/maps/) to rebuild it — it's instanced here.");
|
||||
return false;
|
||||
}
|
||||
|
||||
public static void Clear(Node map)
|
||||
{
|
||||
foreach (Node child in map.GetChildren())
|
||||
{
|
||||
map.RemoveChild(child);
|
||||
child.Free();
|
||||
}
|
||||
}
|
||||
|
||||
// Every generated node belongs to the map scene; children of instanced sub-scenes (e.g. a
|
||||
// teleport marker) keep their own owner.
|
||||
public static void Adopt(Node map)
|
||||
{
|
||||
foreach (Node child in map.GetChildren())
|
||||
Adopt(map, child);
|
||||
}
|
||||
|
||||
private static void Adopt(Node root, Node node)
|
||||
{
|
||||
if (node.Owner == null)
|
||||
node.Owner = root;
|
||||
if (node.SceneFilePath != "")
|
||||
return;
|
||||
foreach (Node child in node.GetChildren())
|
||||
Adopt(root, child);
|
||||
}
|
||||
|
||||
public static void MarkUnsaved()
|
||||
{
|
||||
#if TOOLS
|
||||
if (Engine.IsEditorHint())
|
||||
EditorInterface.Singleton.MarkSceneAsUnsaved();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Runtime: the dynamic props (group "prop") and where they start, for Reset().
|
||||
public static void CollectProps(Node map, List<(RigidBody3D Body, Transform3D Start)> props)
|
||||
{
|
||||
props.Clear();
|
||||
foreach (Node child in map.GetChildren())
|
||||
if (child is RigidBody3D body && body.IsInGroup(PropGroup))
|
||||
props.Add((body, body.GlobalTransform));
|
||||
}
|
||||
|
||||
public static void Reset(List<(RigidBody3D Body, Transform3D Start)> props)
|
||||
{
|
||||
foreach (var (body, start) in props)
|
||||
{
|
||||
if (!GodotObject.IsInstanceValid(body))
|
||||
continue;
|
||||
body.GlobalTransform = start;
|
||||
body.LinearVelocity = Vector3.Zero;
|
||||
body.AngularVelocity = Vector3.Zero;
|
||||
body.ResetPhysicsInterpolation();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://d0oo5qx6v57bs
|
||||
+42
-26
@@ -1,7 +1,9 @@
|
||||
using Godot;
|
||||
using System.Collections.Generic;
|
||||
|
||||
// The body-physics sandbox (Nimso-sandbox style), built in code around the spawn/table area:
|
||||
// The body-physics sandbox (Nimso-sandbox style) around the spawn/table area. EDITOR-BUILT: this is the
|
||||
// root of scenes/maps/playground.tscn and Rebuild() generates the pieces below into that scene
|
||||
// (MapScene: inspector button, or tools/rebuild_maps.tscn headless); the game loads the saved scene.
|
||||
// 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,
|
||||
@@ -10,8 +12,12 @@ using System.Collections.Generic;
|
||||
// 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().
|
||||
[Tool]
|
||||
public partial class Playground : Node3D
|
||||
{
|
||||
[ExportToolButton("Rebuild (replaces all children)")]
|
||||
public Callable RebuildButton => Callable.From(RebuildInEditor);
|
||||
|
||||
private readonly List<(RigidBody3D Body, Transform3D Start)> _dynamic = new();
|
||||
private const uint WorldLayer = 1, WorldMask = 0b11101, PropMask = 0b11101;
|
||||
|
||||
@@ -21,22 +27,28 @@ public partial class Playground : Node3D
|
||||
|
||||
public override void _Ready()
|
||||
{
|
||||
if (!Engine.IsEditorHint())
|
||||
MapScene.CollectProps(this, _dynamic);
|
||||
}
|
||||
|
||||
public void Reset() => MapScene.Reset(_dynamic);
|
||||
|
||||
private void RebuildInEditor()
|
||||
{
|
||||
if (!MapScene.CanRebuildInEditor(this))
|
||||
return;
|
||||
Rebuild();
|
||||
MapScene.MarkUnsaved();
|
||||
}
|
||||
|
||||
// Replaces every child with the generated layout, owned by this map (saved with its scene).
|
||||
public void Rebuild()
|
||||
{
|
||||
MapScene.Clear(this);
|
||||
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();
|
||||
}
|
||||
MapScene.Adopt(this);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- builders
|
||||
@@ -46,7 +58,7 @@ public partial class Playground : Node3D
|
||||
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);
|
||||
AddChild(body, true);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -59,7 +71,7 @@ public partial class Playground : Node3D
|
||||
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);
|
||||
AddChild(body, true);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -75,11 +87,10 @@ public partial class Playground : Node3D
|
||||
MaxContactsReported = 4,
|
||||
PhysicsMaterialOverride = new PhysicsMaterial { Friction = friction, Bounce = 0.05f },
|
||||
};
|
||||
body.AddToGroup("prop");
|
||||
body.AddToGroup(MapScene.PropGroup, true);
|
||||
body.AddChild(new CollisionShape3D { Shape = shape });
|
||||
body.AddChild(Mesh(mesh, color));
|
||||
AddChild(body);
|
||||
_dynamic.Add((body, body.GlobalTransform));
|
||||
AddChild(body, true);
|
||||
return body;
|
||||
}
|
||||
|
||||
@@ -97,13 +108,14 @@ public partial class Playground : Node3D
|
||||
{
|
||||
AddChild(new Label3D
|
||||
{
|
||||
Name = "Sign",
|
||||
Text = text,
|
||||
FontSize = 96,
|
||||
PixelSize = 0.002f,
|
||||
OutlineSize = 12,
|
||||
Billboard = BaseMaterial3D.BillboardModeEnum.FixedY,
|
||||
Position = at,
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- zones
|
||||
@@ -183,13 +195,16 @@ public partial class Playground : Node3D
|
||||
// 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);
|
||||
// Joint paths resolve relative to the joint (a sibling here) and are saved that way; set
|
||||
// before AddChild so the joint never exists without its bodies.
|
||||
HingeJoint3D pivot = new()
|
||||
{
|
||||
Name = "SeesawHinge",
|
||||
Transform = new Transform3D(new Basis(Vector3.Right, Vector3.Up, Vector3.Back), new Vector3(-8.5f, 0.48f, -1.5f)),
|
||||
NodeA = GetNode<Node>("SeesawPivot").GetPath(), // absolute: joint paths resolve relative to the joint
|
||||
NodeB = seesaw.GetPath(),
|
||||
NodeA = $"../{GetNode<Node>("SeesawPivot").Name}",
|
||||
NodeB = $"../{seesaw.Name}",
|
||||
};
|
||||
AddChild(pivot);
|
||||
AddChild(pivot, true);
|
||||
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.
|
||||
@@ -199,11 +214,12 @@ public partial class Playground : Node3D
|
||||
RigidBody3D board = BoxProp("SwingBoard", new Vector3(1.2f, 1.2f, 0.05f), 10f, new Vector3(-6f, 1.75f, 1.5f), Wood);
|
||||
HingeJoint3D swing = new()
|
||||
{
|
||||
Name = "SwingHinge",
|
||||
Transform = new Transform3D(new Basis(Vector3.Back, Vector3.Up, Vector3.Left), new Vector3(-6f, 2.35f, 1.5f)),
|
||||
NodeA = beam.GetPath(),
|
||||
NodeB = board.GetPath(),
|
||||
NodeA = $"../{beam.Name}",
|
||||
NodeB = $"../{board.Name}",
|
||||
};
|
||||
AddChild(swing);
|
||||
AddChild(swing, true);
|
||||
Sign("Hanging board", new Vector3(-6f, 2.9f, 1.5f));
|
||||
|
||||
// Crate pyramid (4 kg each), a heavy 25 kg box, barrels.
|
||||
|
||||
@@ -1128,7 +1128,8 @@ public partial class RagdollSpike : Node3D
|
||||
private void SetupPlayground()
|
||||
{
|
||||
Rest();
|
||||
_playground = new Playground { Position = new Vector3(0f, 0f, 40f) }; // far from the player
|
||||
_playground = GD.Load<PackedScene>("res://scenes/maps/playground.tscn").Instantiate<Playground>(); // the saved scene
|
||||
_playground.Position = new Vector3(0f, 0f, 40f); // far from the player
|
||||
_stage.AddChild(_playground);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user