reckfellENGINE

Level, timers and round state

Connect player, hazards, timers and UI with one controller and no duplicate subscriptions.

On this page

Level hierarchy

Use File → Create Scene…, select an Entity2D root and save as Levels/Arena.rscene. Keep its default position, rotation and scale. Place the player and UI prefabs, then add a container and timers:

Text
Arena (Entity2D, DodgeGame.cs)
├── Player (instance: Prefabs/Player.rprefab)
├── Enemies (Entity2D)
├── SpawnTimer (Timer)
├── ScoreTimer (Timer)
└── Hud (instance: Prefabs/Hud.rprefab)

Names and paths must match the code. Set the player instance's Position to (320, 300). Both timers use Process Callback = Physics, with One Shot and Autostart off. Set SpawnTimer.WaitTime = 0.75 and ScoreTimer.WaitTime = 1.0.

Add all four C# files, including DodgeHud.cs, before the full build. Attach DodgeGame.cs to Arena.

Controller

Playing distinguishes a running round from the initial screen and defeat. StartRun() resets score and player, clears old instances and starts two timers. EndRun() stops the timers, controls and remaining hazards. Starting an already active round does nothing.

Complete DodgeGame.cs
C#
using Reckfell;

public partial class DodgeGame : Entity2D
{
    public bool Playing { get; private set; }
    public int Score { get; private set; }
    private DodgePlayer _player = null!;
    private Entity2D _enemies = null!;
    private Timer _spawnTimer = null!;
    private Timer _scoreTimer = null!;
    private DodgeHud _hud = null!;
    private PackedEntity? _enemyTemplate;
    private readonly RandomNumberGenerator _random = new();

    public override void _EnterTree()
    {
        _player = GetEntity<DodgePlayer>("Player");
        _enemies = GetEntity<Entity2D>("Enemies");
        _spawnTimer = GetEntity<Timer>("SpawnTimer");
        _scoreTimer = GetEntity<Timer>("ScoreTimer");
        _hud = GetEntity<DodgeHud>("Hud");
        _player.Hit += EndRun;
        _spawnTimer.Timeout += OnSpawnTimeout;
        _scoreTimer.Timeout += OnScoreTimeout;
        _hud.StartRequested += StartRun;
    }

    public override void _ExitTree()
    {
        if (ReckfellObject.IsInstanceValid(_player)) _player.Hit -= EndRun;
        if (ReckfellObject.IsInstanceValid(_spawnTimer)) _spawnTimer.Timeout -= OnSpawnTimeout;
        if (ReckfellObject.IsInstanceValid(_scoreTimer)) _scoreTimer.Timeout -= OnScoreTimeout;
        if (ReckfellObject.IsInstanceValid(_hud)) _hud.StartRequested -= StartRun;
    }

    public override void _Ready()
    {
        _player.Active = false;
        _random.Randomize();
        _hud.SetScore(0);
        _enemyTemplate = ResourceLoader.Load("asset://Prefabs/Enemy.rprefab") as PackedEntity;
        if (_enemyTemplate == null || !_enemyTemplate.CanInstantiate())
        {
            _hud.ShowSetupError();
            return;
        }
        _hud.ShowReady();
    }

    public void StartRun()
    {
        if (Playing || _enemyTemplate == null)
            return;
        foreach (Entity enemy in _enemies.GetChildren())
            enemy.QueueFree();
        Score = 0;
        Playing = true;
        _player.StartAt(new Vector2(320, 300));
        _hud.SetScore(Score);
        _hud.ShowPlaying();
        _spawnTimer.Start();
        _scoreTimer.Start();
    }

    public DodgeEnemy? SpawnEnemy()
    {
        if (!Playing || _enemyTemplate == null)
            return null;
        Entity instance = _enemyTemplate.Instantiate();
        if (instance is not DodgeEnemy enemy)
        {
            instance?.Free();
            ReckfellApi.PushError("Enemy.rprefab must have a DodgeEnemy root.");
            EndRun();
            return null;
        }
        enemy.Position = new Vector2(_random.RandfRange(24, 616), -20);
        _enemies.AddChild(enemy);
        return enemy;
    }

    public void EndRun()
    {
        if (!Playing)
            return;
        Playing = false;
        _player.Active = false;
        _player.Velocity = Vector2.Zero;
        _spawnTimer.Stop();
        _scoreTimer.Stop();
        foreach (Entity enemy in _enemies.GetChildren())
            enemy.SetPhysicsProcess(false);
        _hud.ShowGameOver();
    }

    private void OnSpawnTimeout() => SpawnEnemy();
    private void OnScoreTimeout()
    {
        if (Playing)
            _hud.SetScore(++Score);
    }
}

The fields' null! suppresses C# warnings until _EnterTree() assigns references. It does not check object existence; hierarchy errors still need fixing. A parent can find saved children during its own entry, and children finish readiness before its _Ready().

Creating a hazard

Enemy.rprefab loads once. The timer calls SpawnEnemy(): create and validate a DodgeEnemy root, assign position before attachment, then enter the lifecycle with AddChild(). Local X is selected from 24–616, Y = −20. This example's level and container transforms are unchanged, so these coordinates match arena coordinates.

An incompatible root is freed, an error is printed and the round stops. A missing file also requires correction rather than silently substituting another object.

Score and restart

The score timer adds one point per Timeout, only while Playing = true. These are game seconds: time scaling and stopping the application affect the timer. It is neither a wall clock nor persistent high-score storage.

The UI button emits StartRequested, with each event connected in one place. After defeat, the next start queues old circles for deletion; they guard against delayed hits. See the complete UI and round-cycle verification.

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.