Создать префаб по сигналу в мировых координатах
Источник сообщает намерение, мир создаёт экземпляр; направление, преобразования родителя и очистка подписок.
Разделить запрос и создание
Источник сообщает мировую позицию и направление через SpawnRequested. Он не ищет родителя и не отдаёт наружу уже созданный объект. Если подписчика нет, запрос не оставит отсоединённый снаряд в памяти. Мир владеет PackedEntity и решает, где разместить экземпляр.
Запустить пример
В C#-проекте 1.0.0 добавьте три C#-файла в корень и Actors/SignalBolt.rprefab по указанному пути. Прикрепите BoltWorld2D к Entity2D отдельного уровня, задайте viewport 640×420, соберите и запустите F6. Белый круг — источник; ЛКМ выпускает синие маркеры к курсору. Они живут три секунды.
Actors/SignalBolt.rprefab
[reckfell_tree format=3]
[ext_resource type="Script" path="asset://SignalBolt2D.cs" id="BoltScript"]
[entity name="SignalBolt" type="Entity2D"]
script = ExtResource("BoltScript")SignalBolt2D.cs
using Reckfell;
public partial class SignalBolt2D : Entity2D
{
public Vector2 WorldVelocity { get; set; }
public double RemainingSeconds { get; set; } = 3.0;
public override void _Draw() => DrawCircle(Vector2.Zero, 8, new Color(0.25f, 0.45f, 1));
public override void _PhysicsProcess(double delta)
{
GlobalPosition += WorldVelocity * (float)delta;
RemainingSeconds -= delta;
if (RemainingSeconds <= 0) QueueFree();
}
}BoltEmitter2D.cs
using Reckfell;
public partial class BoltEmitter2D : Entity2D
{
[Signal]
public delegate void SpawnRequestedEventHandler(Vector2 worldPosition, Vector2 worldDirection);
public bool RequestTowards(Vector2 worldTarget)
{
Vector2 offset = worldTarget - GlobalPosition;
if (!offset.IsFinite() || offset.IsZeroApprox()) return false;
EmitSignal(SignalName.SpawnRequested, GlobalPosition, offset.Normalized());
return true;
}
public override void _UnhandledInput(InputEvent @event)
{
if (@event is InputEventMouseButton { Pressed: true, ButtonIndex: MouseButton.Left })
RequestTowards(GetGlobalMousePosition());
}
public override void _Draw() => DrawCircle(Vector2.Zero, 16, Colors.White);
}BoltWorld2D.cs
using Reckfell;
public partial class BoltWorld2D : Entity2D
{
public BoltEmitter2D Emitter { get; private set; } = null!;
public int CreatedCount { get; private set; }
private PackedEntity _bolt = null!;
public override void _EnterTree()
{
_bolt = ResourceLoader.Load<PackedEntity>("asset://Actors/SignalBolt.rprefab");
if (Emitter is null)
{
Emitter = new BoltEmitter2D { Name = "Emitter", Position = new Vector2(100, 210) };
Emitter.SpawnRequested += OnSpawnRequested;
AddChild(Emitter);
}
else Emitter.SpawnRequested += OnSpawnRequested;
}
public override void _ExitTree() => Emitter.SpawnRequested -= OnSpawnRequested;
public override void _Ready()
{
AddChild(new Label
{
Position = new Vector2(28, 24),
Text = TranslationServer.GetLocale().StartsWith("ru")
? "Сигнал → экземпляр · ЛКМ: выстрел к курсору"
: "Signal → instance · Left click: fire at cursor"
});
}
private void OnSpawnRequested(Vector2 worldPosition, Vector2 worldDirection) => TrySpawn(worldPosition, worldDirection);
public bool TrySpawn(Vector2 worldPosition, Vector2 worldDirection)
{
if (_bolt is null || !worldPosition.IsFinite() || !worldDirection.IsFinite() || worldDirection.IsZeroApprox()) return false;
var bolt = _bolt.Instantiate<SignalBolt2D>();
bolt.Position = ToLocal(worldPosition);
bolt.WorldVelocity = worldDirection.Normalized() * 240;
AddChild(bolt);
CreatedCount++;
return true;
}
}Координаты и жизненный цикл
worldTarget − GlobalPosition вычисляет направление в одном пространстве. Нулевой или нечисловой запрос отклоняется до создания. Получатель нормализует направление и задаёт скорость 240 мировых единиц в секунду. Перед AddChild позиция переводится через ToLocal: _Ready экземпляра уже увидит подготовленные данные. _PhysicsProcess затем меняет GlobalPosition, поэтому скорость не умножается на масштаб родителя.
Снаряд добавляется ребёнком BoltWorld2D, а не источника. Поворот или перемещение источника больше не тянут выпущенный объект. Сам мир по-прежнему остаётся его родителем: перемещение мира тоже изменит иерархическое преобразование. Если мир должен двигаться независимо от снарядов, выберите другой стабильный контейнер и пересчитайте позицию относительно него.
Подписка создаётся до AddChild источника. _ExitTree её снимает; повторный вход подписывает один раз. _Ready не используется для повторной подписки. По окончании жизни QueueFree освобождает экземпляр через очередь. Здесь нет Object Pool: для частого создания сначала измерьте стоимость и отдельно спроектируйте сброс состояния при переиспользовании.
Границы примера
Синий маркер — Entity2D с рисованием и движением. Он не обнаруживает попадания, не имеет физического коллайдера, сетевого владельца или урона. Для столкновений выберите физическое тело и запрос под правила своей игры; простое изменение позиции не предотвращает пролёт через препятствие.
В стенде проверены настоящий сигнал, триггер создания, мировое положение под поворотом/масштабом, скорость, независимость от источника, отказ неверного направления и очистка при выходе/повторном входе. Изображение получено из viewport движка; ручной сценарий клика в native-окне здесь не выполнялся.