The Majingari Framework is a modular foundation designed to streamline game initialization, player management. It is built using patterns familiar to Unreal Engine developers, providing a structured approach to Unity development
The framework adopts Unreal's decoupled architecture, separating data, rules, and physical representations.
| Feature | Unreal Engine Equivalent | Majingari Framework | Lifetime |
|---|---|---|---|
| Global Manager | GameInstance |
GameInstance |
Application |
| Scene Rules | GameMode |
GameModeManager |
Per-Scene |
| Input & Logic | PlayerController |
PlayerController |
Persistent |
| Player Data | PlayerState |
PlayerState |
Persistent |
| Physical Body | Pawn |
Pawn / PlayerPawn |
Per-GameMode |
| Input Handler | PlayerInput |
PlayerInput |
Per-GameMode |
| HUD | HUD |
HUD |
Per-GameMode |
| Global Data | GameState |
GameState |
Per-GameMode |
To begin using the framework, you must initialize the World Settings. This acts as the central hub for your project's configuration.
Tip
You can quickly access or create your settings by navigating to Majingari Framework > Get World Settings in the Unity menu.
- Create Settings: Using the menu item above will generate a
GameWorldSettings.assetfile in yourAssets/Resourcesfolder. - Assign Game Instance: Ensure a Game Instance (such as the provided
PersistentGameInstance) is assigned to the settings asset. - Attach World Config: Create and attach a
WorldConfigScriptableObject to define how your scenes behave. - Assign Player Prefabs: Set the PlayerController and PlayerState prefabs. These are created once at boot and persist for the application lifetime.
The framework uses a "Map-to-Mode" architecture. You define a WorldConfig asset, which contains:
- Map List: A dictionary-style mapping of scene names to specific GameModeManagers.
- Default Game Mode: A fallback mode used for any scene not explicitly listed.
The GameModeManager is a ScriptableObject that defines the "rules" for a specific scene. It manages:
- Managers: Automatically instantiates your
GameStateprefab. - Per-GameMode Components: Spawns PlayerPawn, PlayerInput, and HUD for each player when the mode activates, and cleans them up when it deactivates.
- Camera Logic: Assigns a CameraHandler to manage the player's view.
When transitioning between GameModes, persistent components (Controller + State) survive while per-GameMode components (Pawn, Input, HUD) are destroyed and recreated by the new GameMode.
Note
PlayerController and PlayerState prefabs are defined in GameWorldSettings and spawned once at boot. GameModeManager only sets up per-mode components (Pawn, Input, HUD) for players that already exist.
The framework decouples player logic into distinct components with different lifetimes, mirroring Unreal Engine's player architecture:
Persistent (survive GameMode transitions, defined in GameWorldSettings):
- PlayerController: The brain that possesses a pawn and links input to state. Created once, persists for the application lifetime.
- PlayerState: Stores persistent data for that specific player.
Per-GameMode (created/destroyed each GameMode transition, defined in GameModeManager):
- PlayerPawn: The physical representation of the player in the world.
- PlayerInput: Handles the Unity Input System actions.
- HUD: The player's UI widgets.
Use the PlayerAccessor utility for easy access to player components from any script:
// Main player shortcuts
var pawn = PlayerAccessor.GetMainPlayerPawn<MyPawn>();
var controller = PlayerAccessor.GetMainPlayerController<MyController>();
var input = PlayerAccessor.GetMainPlayerInput<MyInput>();
var state = PlayerAccessor.GetMainPlayerState<MyState>();
// Safe try-get patterns
if (PlayerAccessor.TryGetPlayerController<MyController>(out var ctrl)) {
ctrl.DoSomething();
}
// Multi-player support (by index)
var player2Pawn = PlayerAccessor.GetPlayerPawn<MyPawn>(index: 1);
int playerCount = PlayerAccessor.GetPlayerCount();PlayerController persists across GameMode transitions. Override lifecycle hooks to respond:
public class MyController : PlayerController {
// Called each time a new GameMode sets up this controller with Input, Pawn, HUD
protected override void OnGameModeSetup() {
var ship = GetPawn<PlayerShip>();
var input = GetInput<MyInput>();
}
// Called when the current GameMode cleans up (Input, Pawn, HUD destroyed)
protected override void OnGameModeCleanup() { }
// Called when possessing/unpossessing a pawn (also works mid-GameMode)
protected override void OnPossess(PlayerPawn pawn) { }
protected override void OnUnPossess(PlayerPawn pawn) { }
}
// Switch pawns at runtime
controller.Possess(newPawn);
controller.UnPossess();Player 0 is spawned automatically at boot. For split-screen or late-joining local players, spawn additional players through GameInstance:
// Spawn a new persistent player (Controller + State)
var newController = GameInstance.Instance<MyGame>().SpawnPlayer();Unity does not natively allow referencing objects between different scenes in the inspector. This framework provides a CrossSceneReference system to bypass this limitation.
- Setup: Add a
CrossSceneAnchorcomponent to the object you want to reference. - Usage: Mark your field in a script with the
[CrossSceneReference]attribute. - Resolution: The CrossSceneManager automatically resolves these links via GUID when the scene loads.
The framework provides an async scene loading system with built-in loading screen support.
The LevelManager orchestrates scene transitions and GameMode lifecycle:
// Load a new level with loading screen
await GameInstance.Instance.LevelManager.LoadLevelAsync("GameplayScene");
// Check loading state
if (GameInstance.Instance.LevelManager.IsLoading) { /* ... */ }Two loading screen implementations are available:
| Class | UI System | Description |
|---|---|---|
LoadingStreamerDefault |
UI Toolkit | Creates UI at runtime - no prefabs/UXML required. Uses RuntimeLoadingPanel. |
LoadingStreamerCanvas |
Canvas | Legacy Canvas-based implementation for projects using uGUI. |
Both support:
- Async fade in/out with configurable speed
- Task coalescing - multiple callers can await the same fade operation
- Cancellation tokens - caller cancellation doesn't affect other waiters
- ForceCancel() - immediately cancel all pending operations
Extend LoadingStreamer to create custom loading screens:
public class MyLoadingScreen : LoadingStreamer {
protected override async Task StartLoadingAsync(CancellationToken token) {
// Show your loading UI
}
protected override async Task StopLoadingAsync(CancellationToken token) {
// Hide your loading UI
}
}The framework provides a complete UI management system with widget caching, navigation stacks, and model-based data binding.
// Show/Hide widgets
HUD.Show<GameplayHUD>();
HUD.Hide<GameplayHUD>();
// Get reference to active widget
var hud = HUD.Get<GameplayHUD>();
// Async variants (wait for animations)
await HUD.ShowAsync<GameplayHUD>();
await HUD.HideAsync<GameplayHUD>();Pass data models to widgets for clean separation of concerns:
// Define a model
public struct PlayerHUDModel {
public float healthPercent;
public int score;
}
// Show with model
HUD.Show<PlayerHUD, PlayerHUDModel>(new PlayerHUDModel {
healthPercent = 0.8f,
score = 1000
});
// Widget implementation
public class PlayerHUD : UIWidget<PlayerHUDModel> {
protected override void OnSetup(PlayerHUDModel model) {
healthBar.value = model.healthPercent;
scoreText.text = model.score.ToString();
}
}Built-in stack-based navigation for menu flows:
// Push screens onto stack
HUD.Push<SettingsMenu>();
HUD.Push<AudioSettings>();
// Go back one screen
HUD.Back();
// Return to root
HUD.PopToRoot();
// Query stack
int depth = HUD.StackCount();
HUD.ClearStack();| Method | Description |
|---|---|
Show() / ShowAsync() |
Display the widget |
Hide() / HideAsync() |
Hide the widget |
Setup(model) |
Pass data to the widget (deferred if inactive) |
Refresh() |
Re-apply current model data |
The network system is built on Unity Netcode for GameObjects with a layered architecture:
UNetcodeConnectionHandler (Main Entry Point)
โโโ SessionManager (Session lifecycle)
โโโ LANDiscoveryService (Local network discovery)
โโโ ConnectionApprovalValidator (Join validation)
โโโ NetworkManager (Unity Netcode)
Step 1: Create NetworkConfig asset
- Right-click > Create > MFramework/Config Object/Network Config
- Configure settings (see table below)
Step 2: Scene Setup
- Add a GameObject with
NetworkManagercomponent - Add
UNetcodeConnectionHandler(or your custom subclass) to the same GameObject - In the Inspector:
- Assign your NetworkConfig asset
- Enable LAN Support checkbox if you need LAN discovery
- Set Default Player Name (fallback if PlayerPrefs not set)
Step 3: Initialize at Runtime
// Option A: Let UNetcodeConnectionHandler use its serialized NetworkConfig
connectionHandler.Initialize(connectionHandler.networkConfig);
// Option B: Load config from Resources
var config = Resources.Load<NetworkConfig>("Network Config");
connectionHandler.Initialize(config);Important
Services are only available after Initialize() is called. Call Shutdown() when done (e.g., OnApplicationQuit).
Bootstrap Pattern (Recommended):
public class NetworkBootstrap : MonoBehaviour {
[SerializeField] private NetworkConfig networkConfig;
[SerializeField] private UNetcodeConnectionHandler connectionHandler;
private void Start() {
// Auto-find if not assigned
networkConfig ??= Resources.Load<NetworkConfig>("Network Config");
connectionHandler ??= FindObjectOfType<UNetcodeConnectionHandler>();
connectionHandler.Initialize(networkConfig);
}
private void OnApplicationQuit() {
connectionHandler?.Shutdown();
}
}| Field | Default | Description |
|---|---|---|
protocolVersion |
1 | Increment for breaking network changes |
discoveryPort |
47777 | UDP port for LAN discovery |
discoveryTimeout |
5f | Scan duration in seconds |
broadcastInterval |
1f | Broadcast frequency in seconds |
sessionStaleTimeout |
3f | Session timeout threshold |
maxConnectPayload |
256 | Max connection data size in bytes |
connectionTimeout |
10f | Connection attempt timeout |
defaultMaxPlayers |
4 | Default session capacity |
After Initialize(), services are available via ServiceLocator:
var networkService = ServiceLocator.Resolve<INetworkService>();
var sessionManager = ServiceLocator.Resolve<ISessionManager>();
// Only available if LAN Support is enabled in Inspector
var lanDiscovery = ServiceLocator.Resolve<ILANDiscoveryService>();The network system provides both async (recommended) and sync (fire-and-forget) APIs:
| Method | Returns | Description |
|---|---|---|
HostSessionAsync() |
Task<ConnectionStatus> |
Waits until hosting starts or fails |
JoinSessionAsync() |
Task<ConnectionStatus> |
Waits until connected or rejected |
LeaveSessionAsync() |
Task |
Waits until fully disconnected |
ScanAsync() |
IAsyncEnumerable<DiscoveryEvent> |
Yields discovery events as they occur |
FindSessionAsync() |
Task<DiscoveredSession> |
Finds first session matching predicate |
All async methods support CancellationToken for timeout/cancellation.
var networkService = ServiceLocator.Resolve<INetworkService>();
var settings = new SessionSettings {
sessionName = "My Game Session",
maxPlayers = 4,
password = null,
mapIndex = 0
};
// Add custom metadata (available in LAN discovery)
settings.SetCustomData("gameMode", "TeamDeathmatch");
settings.SetCustomData("mapName", "Arena");
// Await until hosting starts or fails
var status = await networkService.HostSessionAsync(settings);
if (status == ConnectionStatus.Hosting) {
Debug.Log($"Hosting: {networkService.CurrentSession.SessionName}");
LoadGameScene();
} else {
Debug.LogError($"Failed to host: {status.ToMessage()}");
}var networkService = ServiceLocator.Resolve<INetworkService>();
var cts = new CancellationTokenSource();
// Join by IP address
var status = await networkService.JoinSessionAsync(
address: "192.168.1.100",
port: 7777,
password: null,
cts.Token
);
// Or join a discovered session (from LAN scan)
var status = await networkService.JoinSessionAsync(
session,
password: null,
cts.Token
);
// Handle result
switch (status) {
case ConnectionStatus.Connected:
Debug.Log("Successfully joined!");
LoadGameScene();
break;
case ConnectionStatus.ServerFull:
ShowError("Server is full");
break;
case ConnectionStatus.IncorrectPassword:
ShowError("Wrong password");
break;
case ConnectionStatus.Timeout:
ShowError("Connection timed out");
break;
default:
ShowError($"Failed: {status.ToMessage()}");
break;
}
// Cancel connection attempt from UI button
public void OnCancelClicked() => cts.Cancel();// Wait until fully disconnected
await networkService.LeaveSessionAsync();
LoadMainMenu();
// Full shutdown (unregisters services, call on app quit)
connectionHandler.Shutdown();Note
LAN Discovery is only available if LAN Support is enabled in the Inspector.
The LAN Discovery system uses an async enumerable pattern for clean, modern C# code.
Session Browser (scan with timeout):
var lanDiscovery = ServiceLocator.Resolve<ILANDiscoveryService>();
var cts = new CancellationTokenSource();
try {
await foreach (var evt in lanDiscovery.ScanAsync(
timeout: TimeSpan.FromSeconds(10),
cancellationToken: cts.Token)) {
switch (evt.Type) {
case DiscoveryEventType.ScanStarted:
// Show loading indicator
break;
case DiscoveryEventType.Discovered:
AddToUI(evt.Session);
break;
case DiscoveryEventType.Updated:
UpdateInUI(evt.Session);
break;
case DiscoveryEventType.Lost:
RemoveFromUI(evt.Session);
break;
case DiscoveryEventType.ScanComplete:
// Hide loading, enable refresh button
break;
}
}
}
catch (OperationCanceledException) {
// User cancelled or left screen
}
// Access current sessions anytime during scan
var sessions = lanDiscovery.DiscoveredSessions;Quick Join / Matchmaking:
// Find first available session matching criteria
var session = await lanDiscovery.FindSessionAsync(
predicate: s => !s.IsFull && !s.HasPassword && s.IsCompatible(protocolVersion),
timeout: TimeSpan.FromSeconds(15),
cancellationToken: cts.Token
);
if (session != null) {
// Use async join for clean flow
var status = await networkService.JoinSessionAsync(session, cancellationToken: cts.Token);
if (status == ConnectionStatus.Connected) {
LoadGameScene();
}
}
else {
// No session found - offer to host
}Continuous Scanning (no timeout):
// Scan indefinitely until cancelled
await foreach (var evt in lanDiscovery.ScanAsync(timeout: null, cancellationToken: cts.Token)) {
RefreshUI(lanDiscovery.DiscoveredSessions);
}DiscoveryEvent Types:
| Type | Description |
|---|---|
ScanStarted |
Scan has begun. Session is null. |
Discovered |
New session found on network. |
Updated |
Existing session data changed (player count, etc.). |
Lost |
Session stopped responding and was removed. |
ScanComplete |
Scan finished (timeout reached). Session is null. |
Key Behaviors:
- Sessions are cleared automatically when
ScanAsync()starts DiscoveredSessionslist is updated before each event is yielded- Scan runs until timeout or cancellation (whichever comes first)
- Pass
timeout: nullfor indefinite scanning
| Status | Meaning |
|---|---|
Disconnected |
Not connected |
Connecting |
Connection in progress |
Connected |
Successfully joined as client |
Hosting |
Running as server/host |
ServerFull |
Session has reached maxPlayers |
IncorrectPassword |
Wrong or missing password |
ProtocolMismatch |
Client/server version mismatch |
InvalidPlayerName |
Player name empty or too long |
Timeout |
Connection attempt timed out |
Banned |
Client is banned from session |
Helper methods:
status.IsConnected(); // true if Connected or Hosting
status.IsRejection(); // true if rejection reason (ServerFull, etc.)
status.ToMessage(); // User-friendly error messageExtend UNetcodeConnectionHandler to customize behavior:
public class MyConnectionHandler : UNetcodeConnectionHandler {
[Header("Game Settings")]
[SerializeField] private int defaultCharacter = 0;
// --- Factory methods (return custom implementations) ---
protected override SessionManager CreateSessionManager(NetworkConfig config) {
return new MySessionManager(config);
}
protected override LANDiscoveryService CreateLANDiscoveryService(
NetworkConfig config, ISessionManager sessionManager) {
return new MyLANDiscoveryService(config, sessionManager, GetTransportPort);
}
// --- Server-side callbacks ---
protected override void OnServerStartedCustom() {
Debug.Log("Server ready for connections");
}
protected override void OnClientJoined(ulong clientId) {
var payload = GetClientPayload(clientId);
Debug.Log($"Player {payload.playerName} joined");
}
protected override void OnClientLeft(ulong clientId) {
Debug.Log($"Client {clientId} disconnected");
}
// --- Client-side callbacks ---
protected override void OnLocalClientConnected() {
Debug.Log("Connected to server!");
// Load game scene, show lobby UI, etc.
}
protected override void OnLocalClientDisconnected() {
Debug.Log("Disconnected from server");
// Return to main menu, show reconnect dialog, etc.
}
// --- Validation callbacks ---
protected override void OnConnectionApproved(ulong clientId, ConnectionPayload payload) {
var customData = payload.GetCustomData<MyJoinData>();
// Setup player based on custom data
}
protected override void OnConnectionRejected(ulong clientId, ConnectionStatus reason, ConnectionPayload payload) {
Debug.LogWarning($"Connection rejected: {reason.ToMessage()}");
}
// --- Spawn customization ---
protected override void ConfigurePlayerSpawn(
NetworkManager.ConnectionApprovalRequest request,
NetworkManager.ConnectionApprovalResponse response,
ConnectionPayload payload) {
base.ConfigurePlayerSpawn(request, response, payload);
var customData = payload.GetCustomData<MyJoinData>();
if (customData != null) {
response.Position = GetTeamSpawnPoint(customData.teamIndex);
}
}
}Send custom data when connecting:
[Serializable]
public class MyJoinData {
public int selectedCharacter;
public string selectedSkin;
public int teamIndex;
}
public class MyConnectionHandler : UNetcodeConnectionHandler {
[SerializeField] private int defaultCharacter = 0;
[SerializeField] private string defaultSkin = "default";
// Client: Send custom data when joining
protected override ConnectionPayload CreateClientPayload(string password) {
var payload = base.CreateClientPayload(password);
payload.SetCustomData(new MyJoinData {
selectedCharacter = defaultCharacter,
selectedSkin = defaultSkin,
teamIndex = 0
});
return payload;
}
// Server: Read custom data on client join
protected override void OnClientJoined(ulong clientId) {
base.OnClientJoined(clientId);
var payload = GetClientPayload(clientId);
var joinData = payload?.GetCustomData<MyJoinData>();
if (joinData != null) {
Debug.Log($"Client {clientId} selected character {joinData.selectedCharacter}");
// Spawn their character, assign team, etc.
}
}
}Extend session management for game-specific logic:
// Custom session info with game-specific properties
public class MySessionInfo : SessionInfo {
public string GameMode { get; private set; }
public string MapName { get; private set; }
public MySessionInfo(SessionSettings settings, int protocolVersion)
: base(settings, protocolVersion) { }
protected override void OnCreated(SessionSettings settings) {
GameMode = settings.GetCustomData("gameMode", "Deathmatch");
MapName = settings.GetCustomData("mapName", "Default");
}
}
// Custom session manager with validation
public class MySessionManager : SessionManager {
public MySessionManager(NetworkConfig config) : base(config) { }
protected override SessionInfo CreateSessionInfo(SessionSettings settings, int protocolVersion) {
return new MySessionInfo(settings, protocolVersion);
}
protected override bool ValidateCustom(ConnectionPayload payload, out ConnectionStatus rejectionReason) {
rejectionReason = ConnectionStatus.Success;
var joinData = payload.GetCustomData<MyJoinData>();
if (joinData != null) {
// Example: Validate character selection
if (joinData.selectedCharacter < 0 || joinData.selectedCharacter > 10) {
rejectionReason = ConnectionStatus.GenericFailure;
return false;
}
}
return true;
}
}
// Register in your connection handler
public class MyConnectionHandler : UNetcodeConnectionHandler {
protected override SessionManager CreateSessionManager(NetworkConfig config) {
return new MySessionManager(config);
}
}Broadcast game-specific data for session browser:
[Serializable]
public class MyDiscoveryData {
public string gameMode;
public string mapName;
public bool isRanked;
}
public class MyLANDiscoveryService : LANDiscoveryService {
public MyLANDiscoveryService(NetworkConfig config, ISessionManager sessionManager, Func<ushort> getTransportPort)
: base(config, sessionManager, getTransportPort) { }
// Server: Add custom data to broadcast
protected override DiscoveryResponseData CreateResponseData(SessionInfo session) {
var response = base.CreateResponseData(session);
if (session is MySessionInfo mySession) {
response.SetCustomData(new MyDiscoveryData {
gameMode = mySession.GameMode,
mapName = mySession.MapName,
isRanked = false
});
}
return response;
}
// Client: Parse custom data when session discovered
protected override void OnSessionDiscoveredInternal(DiscoveredSession session, DiscoveryResponseData response) {
var customData = response.GetCustomData<MyDiscoveryData>();
if (customData != null) {
session.SetMetadata("gameMode", customData.gameMode);
session.SetMetadata("mapName", customData.mapName);
session.SetMetadata("isRanked", customData.isRanked);
}
}
}The Majingari Save System provides a high-performance, asynchronous, and modular way to handle persistent game data. It is inspired by Unreal Engineโs SaveGame architecture but enhanced with a multi-slot management system and flexible serialization.
The system is built on four primary pillars:
- SaveData: The base class for any data you want to persist. It includes built-in versioning and a "dirty" flag system to optimize save operations.
- SaveDataService: The central engine that handles asynchronous file I/O and manages the lifecycle of your saves.
- SaveSlot: Metadata containers that track information like
totalPlayTime,lastSaveTime, and custom display names for each save file. - ISaveListener: An interface for systems (like an Inventory or Quest manager) to subscribe to bulk save operations, ensuring all data is synchronized at once.
You can choose the serialization format that best fits your project's needs:
| Format | Class | Description |
|---|---|---|
| Binary | BinarySaveSerializer |
Compact, fast, and secure; ideal for production. |
| JSON | JsonSaveSerializer |
Human-readable; perfect for debugging and modding. |
| Compressed | CompressedJsonSaveSerializer |
JSON convenience with GZip compression for smaller file sizes. |
Create a class that inherits from SaveData and define your serializable fields.
[Serializable]
public class PlayerProfileData : SaveData {
public override string FileName => "PlayerProfile";
public string playerName;
public int currentLevel;
}Initialize the service via your GameInstance or a bootstrap script.
// Setup with 3 save slots and JSON serialization
var service = new SaveDataService(slotCount: 3, serializer: new JsonSaveSerializer());
await service.InitializeAsync();
// Select the first slot
service.SetCurrentSlot(0); All operations are asynchronous to prevent frame stutters during disk I/O.
// Saving
var myData = new PlayerProfileData { playerName = "Hero", currentLevel = 10 };
await service.SaveAsync(myData);
// Loading
var loadedData = await service.LoadAsync<PlayerProfileData>("PlayerProfile");Tip
Use service.UpdateAllAsync() to trigger a save on all registered ISaveListener objects. This is perfect for "Checkpoints" or auto-save triggers.
โ ๏ธ Deprecated: These TextMeshPro-based localizers are for legacy Canvas/uGUI projects only. For UI Toolkit projects, use the localization features in Majinfwork UI Toolkit Extension (com.majingari.ui) with Unity Localization package instead.
| Component | Description |
|---|---|
LocalizerBasicText |
Localizes TextMeshPro text and fonts |
LocalizerFontText |
Font-only localization for text components |
LocalizerInputField |
Localizes input field placeholders |
LocalizerValueText |
Localizes text with dynamic value substitution |
LocalizationDropdown |
Pre-built dropdown for language switching at runtime |
For new projects using UI Toolkit:
- Static text: Use UXML
@Table/Keysyntax (most performant) - Dynamic text: Use
LabelRef,ButtonRef, etc. withInitializeLocalization()+Arguments
A custom ticking system that integrates with Unity's PlayerLoop for decoupled updates:
public class MyComponent : MonoBehaviour, ITickObject {
void OnEnable() => this.RegisterTick();
void OnDisable() => this.UnregisterTick();
public void Tick() {
// Called every frame, independent of MonoBehaviour
}
}
// Also supports fixed tick
public class PhysicsComponent : MonoBehaviour, IFixedTickObject {
public void FixedTick() {
// Called every fixed update
}
}Three pooling implementations for different use cases:
| Class | Use Case | Example |
|---|---|---|
PoolByReference |
Prefab pooling with extension methods | prefab.InstantiatePoolRef() |
PoolByUnityObject<T> |
Manual pool for Unity objects | Custom capacity/callbacks |
PoolByObject<T> |
Generic C# object pooling | Non-Unity classes |
PoolByReference (most common):
// Spawn from pool (uses prefab as key)
prefab.InstantiatePoolRef(position, rotation, out Transform instance);
// Return to pool
instance.ReleasePoolRef(prefab);Non-allocating physics helpers with pre-allocated buffers:
// Non-allocating overlap (reuses internal buffer)
List<Collider> results = new();
PhysxExtension.OverlapSphere(ref results, position, radius, layerMask);
// Obstruction check with optional destructible support
bool blocked = PhysxExtension.IsObstructed(from, to, layerMask, maxDistance);This project is licensed under the MIT License.