Spawning hazards and rounds
One controller connects timer, player, hazard prefab and UI, clearing old instances on restart.
Prepare the level
Arena needs Player and Hud instances, an empty Entity3D named Mobs, and SpawnTimer. Set the timer to Wait Time = 1.2, Process Callback = Physics, with One Shot and Autostart off. Keep level and Mobs transforms at their defaults, scale 1.
Attach Scripts/SquashGame.cs to the root. Add SquashHud.cs and the other classes before building.
Complete SquashGame.cs
using Reckfell;
public partial class SquashGame : Entity3D
{
public bool Playing { get; private set; }
public int Score { get; private set; }
private SquashPlayer _player = null!;
private Entity3D _mobs = null!;
private Timer _spawnTimer = null!;
private SquashHud _hud = null!;
private PackedEntity? _template;
private readonly RandomNumberGenerator _random = new();
public override void _EnterTree()
{
_player = GetEntity<SquashPlayer>("Player");
_mobs = GetEntity<Entity3D>("Mobs");
_spawnTimer = GetEntity<Timer>("SpawnTimer");
_hud = GetEntity<SquashHud>("Hud");
_player.Hit += EndRun;
_spawnTimer.Timeout += OnSpawnTimeout;
_hud.StartRequested += StartRun;
}
public override void _ExitTree()
{
if (ReckfellObject.IsInstanceValid(_player)) _player.Hit -= EndRun;
if (ReckfellObject.IsInstanceValid(_spawnTimer)) _spawnTimer.Timeout -= OnSpawnTimeout;
if (ReckfellObject.IsInstanceValid(_hud)) _hud.StartRequested -= StartRun;
if (ReckfellObject.IsInstanceValid(_mobs))
foreach (SquashMob mob in _mobs.GetChildren()) mob.Squashed -= OnSquashed;
}
public override void _Ready()
{
_player.Active = false;
_random.Randomize();
_hud.SetScore(0);
_template = ResourceLoader.Load("asset://Prefabs/Mob.rprefab") as PackedEntity;
if (_template == null || !_template.CanInstantiate()) _hud.ShowSetupError();
else _hud.ShowReady();
}
public void StartRun()
{
if (Playing || _template == null) return;
foreach (Entity mob in _mobs.GetChildren()) mob.QueueFree();
Playing = true;
Score = 0;
_player.StartAt(new Vector3(0, 0.9f, 0));
_hud.SetScore(0);
_hud.ShowPlaying();
_spawnTimer.Start();
}
public SquashMob? SpawnMob()
{
if (!Playing || _template == null || _mobs.GetChildCount() >= 32) return null;
Entity instance = _template.Instantiate();
if (instance is not SquashMob mob)
{
instance?.Free();
ReckfellApi.PushError("Mob.rprefab must have a SquashMob root.");
EndRun();
return null;
}
float side = _random.RandfRange(-6, 6);
mob.Position = _random.RandiRange(0, 3) switch
{
0 => new Vector3(-6, 0.4f, side),
1 => new Vector3(6, 0.4f, side),
2 => new Vector3(side, 0.4f, -6),
_ => new Vector3(side, 0.4f, 6)
};
mob.MoveDirection = _player.Position - mob.Position;
mob.Squashed += OnSquashed;
_mobs.AddChild(mob);
return mob;
}
public void EndRun()
{
if (!Playing) return;
Playing = false;
_player.Active = false;
_player.Velocity = Vector3.Zero;
_spawnTimer.Stop();
foreach (Entity mob in _mobs.GetChildren()) mob.SetPhysicsProcess(false);
_hud.ShowGameOver();
}
private void OnSpawnTimeout() => SpawnMob();
private void OnSquashed()
{
if (Playing) _hud.SetScore(++Score);
}
}Where instances appear
Prefabs/Mob.rprefab loads once. Spawning creates a SquashMob; an incompatible root is freed, an error is logged and the round stops. Position is selected on one of four sides of the X/Z = ±6 square, at Y = 0.4.
Initial direction points toward the player's current position. Position and direction are assigned before AddChild(), so the lifecycle starts with its initial data in place. These are container-local coordinates; transforming the container changes world placement.
Count limit
With 32 children in Mobs, another request returns null. A queued-for-deletion object still counts until freed. Each hazard also has a lifetime limit. This bounds objects in the local prototype; it is not world streaming.
Starting and ending
StartRun() clears score and old instances, returns the player to (0, 0.9, 0) and starts the timer. Calling it during an active round changes nothing. EndRun() disables controls, timer and remaining hazards' physics processing while preserving final score.
Each new hazard gets one Squashed handler. _EnterTree() obtains references and _ExitTree() removes subscriptions. Do not duplicate those connections in the editor. See score and UI.