reckfellENGINE

Уровень, таймеры и состояние раунда

Свяжите игрока, противников, таймеры и интерфейс одним контроллером без повторных подписок.

На этой странице

Иерархия уровня

Создайте File → Create Scene…, выберите корень Entity2D и сохраните Levels/Arena.rscene. Его положение, поворот и масштаб оставьте исходными. Разместите префабы игрока и интерфейса, затем добавьте контейнер и таймеры:

Текст
Arena (Entity2D, DodgeGame.cs)
├── Player (instance: Prefabs/Player.rprefab)
├── Enemies (Entity2D)
├── SpawnTimer (Timer)
├── ScoreTimer (Timer)
└── Hud (instance: Prefabs/Hud.rprefab)

Имена и пути должны совпадать с кодом. Экземпляру игрока задайте Position = (320, 300). У обоих таймеров Process Callback = Physics, One Shot и Autostart выключены. SpawnTimer.WaitTime = 0.75, ScoreTimer.WaitTime = 1.0.

Добавьте все четыре C#-файла, включая DodgeHud.cs, до общей сборки. Прикрепите DodgeGame.cs к Arena.

Контроллер

Playing отличает активный раунд от начального экрана и поражения. StartRun() сбрасывает счёт и игрока, очищает старые экземпляры и запускает два таймера. EndRun() останавливает их, управление и движение оставшихся противников. Повторный вызов начала активного раунда ничего не делает.

Полный 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);
    }
}

null! у полей подавляет предупреждение C# до заполнения ссылок в _EnterTree(). Это не проверка существования объекта: ошибка в иерархии всё равно требует исправления. Родитель может найти сохранённых детей при своём входе; их готовность завершается до его _Ready().

Создание противника

Enemy.rprefab загружается один раз. Таймер вызывает SpawnEnemy(): создаётся и проверяется корень DodgeEnemy, до присоединения назначается позиция, затем AddChild() запускает жизненный цикл. Локальная X выбирается от 24 до 616, Y = −20. В этом примере корень уровня и контейнер не преобразованы, поэтому эти координаты совпадают с координатами поля.

При неверном корне экземпляр освобождается, ошибка выводится в Output, раунд останавливается. Пропущенный файл также требует исправления, а не тихой подмены другим объектом.

Счёт и перезапуск

Таймер счёта добавляет одно очко за сигнал Timeout, только пока Playing = true. Это игровые секунды: изменение масштаба времени или остановка приложения влияет на таймер. Это не часы реального времени и не сохранение рекорда.

Кнопка интерфейса отправляет StartRequested; источник каждого события подключается ровно в одном месте. После проигрыша следующий запуск удаляет старые круги через QueueFree(). У них есть защита от отложенного попадания. Законченный интерфейс, проверка цикла раунда.

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.