The 3D hazard prefab
A 0.8 × 0.8 × 0.8 body, arena movement, one-time signal and bounded lifetime.
Assemble the prefab
Create Prefabs/Mob.rprefab with a CharacterBody3D root named Mob and CollisionShape3D / MeshInstance3D children. Use a BoxShape3D and BoxMesh, both Size (0.8, 0.8, 0.8), with a light StandardMaterial3D. Child local positions are 0 and scales are 1.
The root occupies layer 3 (numeric Collision Layer = 4) and detects only floor layer 1 (Collision Mask = 1). Hazards do not block one another. No externally downloaded model is needed.
Complete code
Attach Scripts/SquashMob.cs to the root:
using Reckfell;
public partial class SquashMob : CharacterBody3D
{
[Signal] public delegate void SquashedEventHandler();
[Export] public float Speed { get; set; } = 2;
[Export] public Vector3 MoveDirection { get; set; } = Vector3.Forward;
private double _age;
private bool _squashed;
public override void _PhysicsProcess(double delta)
{
_age += delta;
Vector3 direction = new Vector3(MoveDirection.X, 0, MoveDirection.Z).Normalized();
Vector3 velocity = direction * Speed;
velocity.Y = IsOnFloor() ? 0 : Velocity.Y - 24 * (float)delta;
Velocity = velocity;
MoveAndSlide();
if (_age > 10 || Mathf.Abs(Position.X) > 8 || Mathf.Abs(Position.Z) > 8)
QueueFree();
}
public bool TrySquash()
{
if (_squashed || IsQueuedForDeletion()) return false;
_squashed = true;
EmitSignal(SignalName.Squashed);
QueueFree();
return true;
}
}Movement
MoveDirection specifies X/Z direction. The vertical component is discarded and the remainder normalized; zero direction leaves the hazard stationary. Speed uses units per second, and gravity keeps the body on the floor. MoveAndSlide() applies the time step.
The controller spawns at Y = 0.4, placing the cube bottom on the floor. Direction toward the player's position is chosen once at spawn and not recomputed. This is straight-line movement, not obstacle-aware pathfinding.
Deletion and score
An instance is removed beyond X/Z = ±8 or after 10 game seconds, bounding its lifetime even when it never meets the player.
TrySquash() returns true once. It sets _squashed before the signal, then calls QueueFree(). A repeat in the same frame cannot award another point. The controller handles Squashed; the hazard does not own score.
Test in Arena.rscene, with its floor, camera and built scripts. See instance spawning.