Bridge API
Namespace GodotECS.Bridge. Main-thread bridge nodes and ECS request components for audio, navigation, signals and GDScript interoperability.
Documentation target:
GodotECScommit699338bcefd3edc31ff6b82a054932402f4d2970.
Components
public struct AudioEvent : IBufferElementData
{
public int BankId;
public Godot.Vector3 Pos;
public float Volume;
}
public struct NavPath : IBufferElementData
{
public Godot.Vector3 Point;
}
public struct NavRequest : IComponentData
{
public Godot.Vector3 Target;
public int RequestId;
public bool Pending;
}
public struct BridgeClock : ISingleton
{
public uint Tick;
}NetworkId is not defined in GodotECS.Bridge; the single type is GodotECS.Core.NetworkId { uint Id }.
Audio
public interface IAudioSink
{
void Play(int bankId, Godot.Vector3 pos, float volume);
int Played { get; }
}
public sealed class NullAudioSink : IAudioSink
{
public int Played { get; }
public void Play(int bankId, Godot.Vector3 pos, float volume);
}
public partial class AudioBridge : Godot.Node
{
public const int MaxVoices = 32;
public const int MaxPending = 1024;
public IAudioSink Sink { get; set; }
public int PendingCount { get; }
public void RegisterBank(int bankId, Godot.AudioStream stream);
public void PushBank(int bankId, Godot.Vector3 pos, float volume);
public static List<AudioEvent> ClusterCap(List<AudioEvent> src, int cap);
public int Flush();
}MaxVoices is both the real voice-pool size and the maximum number of events played per flush. MaxPending caps the input queue at 1024 so an emitter cannot create unbounded backlog.
When the queue is full, PushBank finds the quietest pending event. A new event replaces it only when the new volume is higher; otherwise the new event is dropped. This preserves strong events under load instead of simply discarding the newest request.
Flush() reuses an internal scratch list. When pending count is <= MaxVoices it skips sorting; above the cap it sorts by descending volume and retains the loudest 32. The queue is then cleared. With a non-null Sink, playback goes only through the sink; with Sink == null, the real 32-player AudioStreamPlayer3D pool is used. This avoids double playback when tests inject a sink.
The real path ignores unregistered/null banks. Volume is clamped to [0.0001, 1] before conversion to dB. _Ready() creates the 32 Voice0…Voice31 players, and _Process() flushes a non-empty queue automatically.
ClusterCap() remains the allocation-owning compatibility helper; the hot Flush() path uses its reused scratch buffer instead.
Signals
public partial class EcsSignals : Godot.Node
{
public const int MaxPendingPerQueue = 4096;
[Godot.Signal] public delegate void EntityDiedEventHandler(int index, int version);
[Godot.Signal] public delegate void HitPlayerEventHandler(int damage);
public readonly List<Entity> EmittedDeaths;
public readonly List<int> EmittedHits;
public int PendingDeaths { get; }
public int PendingHits { get; }
public void EnqueueDeath(Entity e);
public void EnqueueHit(int damage);
public int EmitPending();
}Both pending queues are protected by a lock, so enqueue/read operations are thread-safe. Each queue is capped at 4096; on overflow the oldest item is dropped. EnqueueDeath(Entity.Null) is ignored.
EmitPending() drains all deaths first, then all hits, appends them to EmittedDeaths / EmittedHits, emits the Godot signals, and returns the total emitted. _Process() performs the drain automatically when either queue is non-empty.
Navigation
public interface INavQuery
{
bool TryGetPath(Godot.Vector3 from, Godot.Vector3 to, List<Godot.Vector3> outPoints);
}
public sealed class StraightLineQuery : INavQuery
{
public bool TryGetPath(Godot.Vector3 from, Godot.Vector3 to, List<Godot.Vector3> outPoints);
}
public partial class NavBridge : Godot.Node
{
public const int PathsPerFrame = 64;
public INavQuery Query { get; set; }
public int Budget { get; set; }
public static int ProcessBudget(World world, INavQuery query, int budget);
}ProcessBudget resolves up to budget pending requests. Failed TryGetPath calls do not consume budget and leave the request pending.
The implementation caches one EntityQuery per World using a ConditionalWeakTable, so it iterates only entities matching NavRequest instead of copying/scanning all live entities each physics frame. The cache does not keep disposed worlds alive. A small static point buffer is reused between requests.
Budget <= 0, null world/query, or an empty matching query returns 0. The result path replaces the NavPath buffer and clears NavRequest.Pending.
GDScript facade
public partial class GdScriptApi : Godot.Node
{
public int GetEntityCount();
public uint GetTick();
public T GetSingleton<T>() where T : struct, ISingleton;
public int GetPhysicsHz();
public float GetLodBias();
public Godot.Vector2I SpawnScene(EntityScene scene, Godot.Vector3 pos);
public bool KillEntity(int index, int version);
public int FlushKills();
public bool TryGetEntityPos(int index, int version, out Godot.Vector3 pos);
public Godot.Vector3 GetEntityPos(int index, int version);
}All facade calls target World.Default.
SpawnScene returns the root as Vector2I(X = Index, Y = Version), not a bare index. (-1, 0) represents null/empty. The version is required to reject ABA errors caused by recycled entity indices.
KillEntity is deferred through an internal ECB and is automatically flushed in _Process, with an additional flush in _ExitTree. TryGetEntityPos validates both index and version; GetEntityPos returns Vector3.Zero for dead/stale entities or entities without LocalTransform.
GetPhysicsHz() and GetLodBias() read the core QualityConfig singleton. These are passive quality hooks; built-in simulation does not automatically apply LodBias.