Runtime objects
Spawn prefabs at a marker, limit instances, queue deletion and set a lifetime.
Setup
Requires a C# project and Crate.rprefab with an Entity3D root. Create this hierarchy in the level:
World (Entity3D, RuntimeSpawner.cs)
├── SpawnPoint (Marker3D)
└── Spawned (Entity3D)SpawnPoint defines the new instance's position and rotation. Spawned contains only objects created by this script. Its transform may differ from World.
In Input Map, bind spawn_object to E and remove_object to Q. Attach RuntimeSpawner.cs to World, build C# and assign Crate.rprefab to Template in the inspector. The camera and lighting must cover the spawn point.
RuntimeSpawner.cs
using Reckfell;
public partial class RuntimeSpawner : Entity3D
{
[Export] public PackedEntity? Template { get; set; }
[Export] public int MaxInstances { get; set; } = 64;
private Marker3D _point = null!;
private Entity3D _container = null!;
public override void _Ready()
{
_point = GetEntity<Marker3D>("SpawnPoint");
_container = GetEntity<Entity3D>("Spawned");
}
public Entity3D? Spawn()
{
if (Template == null)
{
ReckfellApi.PushError("Assign a PackedEntity to Template.");
return null;
}
if (MaxInstances <= 0 || _container.GetChildCount() >= MaxInstances)
return null;
Transform3D parentTransform = _container.GlobalTransform;
Transform3D spawnTransform = _point.GlobalTransform;
if (!parentTransform.IsFinite() || !spawnTransform.IsFinite() ||
Mathf.IsZeroApprox(parentTransform.Basis.Determinant()))
{
ReckfellApi.PushError("Spawn transforms must be finite; Spawned must have invertible scale.");
return null;
}
Entity entity = Template.Instantiate();
if (entity is not Entity3D instance)
{
entity?.Free();
ReckfellApi.PushError("Template root must inherit Entity3D.");
return null;
}
instance.Transform = parentTransform.AffineInverse() * spawnTransform;
_container.AddChild(instance);
return instance;
}
public void RemoveOldest()
{
foreach (Entity child in _container.GetChildren())
{
if (child.IsQueuedForDeletion())
continue;
child.QueueFree();
return;
}
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event.IsActionPressed("spawn_object"))
Spawn();
else if (@event.IsActionPressed("remove_object"))
RemoveOldest();
else
return;
GetViewport().SetInputAsHandled();
}
}Transforms and lifecycle
Instantiate() creates a separate tree that has not entered the level yet. The formula converts the marker's world transform into Spawned local coordinates. It is assigned before AddChild, so the new object's _Ready() sees the intended position and rotation.
Assigning position after AddChild() lets _Ready() run with the old transform. This matters when startup scripts perform physics queries or create child effects.
E adds an instance; Q removes the first remaining instance. QueueFree() completes deletion at the end of the frame. Until then, the object remains in the child list. Checking IsQueuedForDeletion() lets multiple removal calls in one frame select different objects.
A stored C# reference does not mean the native object still exists after deletion. For references that outlive deletion, check ReckfellObject.IsInstanceValid(reference) before access.
Limits and repeated deletion
MaxInstances limits the children of Spawned. At 64, the sixty-fifth Spawn() call returns null; zero and negative values disable creation. Objects already queued for deletion still occupy slots until actual deletion at the end of the frame. This prevents rapidly piling new instances on top of those still alive.
RemoveOldest() skips children already queued for deletion. Two calls in one frame therefore remove two different objects rather than repeatedly selecting the first. For a projectile hit, add your own duplicate-event guard before QueueFree() — other calls can still reach the object during this frame.
Spawned must contain only this spawner's instances: other children also count toward the limit and may be removed. Use a separate parent for permanent decoration. Do not give the container a zero scale: its world transform must be invertible. The example rejects nonfinite transforms and a nearly singular scale before creating an object.
Deleting after a lifetime
To make an instance disappear after three seconds, attach Scripts/TimedLifetime.cs to the root of Crate.rprefab and save the prefab. It replaces the root's previous script; one object does not carry two ordinary C# scripts at once in Reckfell.
using Reckfell;
public partial class TimedLifetime : Entity3D
{
[Export] public double LifetimeSeconds { get; set; } = 3.0;
private double _remaining;
public override void _EnterTree()
{
_remaining = LifetimeSeconds;
SetProcess(true);
}
public override void _Process(double delta)
{
_remaining -= delta;
if (_remaining > 0.0)
return;
SetProcess(false);
QueueFree();
}
}Instead of a separate timer for every object, this example counts delta in its own update. On expiry it disables processing and queues deletion. Zero or negative duration deletes on the first update. With the default inherited process mode, pausing the game stops this countdown.
Check it: press E, wait for the crate to disappear, then press E again. The released slot becomes available. This deletes objects rather than pooling them: once QueueFree() completes, the old instance cannot return to the tree. If an object is only temporarily removed with RemoveChild(), reentry runs _EnterTree() and restarts the countdown.
Persistence and errors
These objects exist only in the running game. AddChild() does not write them to the source .rscene or assign Owner. To restore spawned objects after restarting, store their prefab identifiers and transforms in game save data.
| Symptom | Cause |
|---|---|
Assign a PackedEntity | Template was not assigned |
Template root must inherit Entity3D | The prefab root is 2D, Control or another incompatible type |
GetEntity error | SpawnPoint / Spawned names or types differ from the example |
| Instance exists but is invisible | No mesh, spawn point outside the view, or a hidden object |
| No world collisions | The visible mesh lacks a physics body and Shape |
Reference: PackedEntity, Entity3D, Entity.