DialogueComponent doodles

This commit is contained in:
2025-09-26 15:47:26 +02:00
committed by Michal Pikulski
parent c8cbc45f04
commit b07eea6aae
35 changed files with 1025 additions and 373 deletions

View File

@@ -0,0 +1,181 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using Interactions;
namespace Core
{
/// <summary>
/// Central registry for pickups and item slots.
/// Mirrors the singleton pattern used by PuzzleManager.
/// </summary>
public class ItemManager : MonoBehaviour
{
private static ItemManager _instance;
private static bool _isQuitting;
public static ItemManager Instance
{
get
{
if (_instance == null && Application.isPlaying && !_isQuitting)
{
_instance = FindAnyObjectByType<ItemManager>();
if (_instance == null)
{
var go = new GameObject("ItemManager");
_instance = go.AddComponent<ItemManager>();
// DontDestroyOnLoad(go);
}
}
return _instance;
}
}
private readonly HashSet<Pickup> _pickups = new HashSet<Pickup>();
private readonly HashSet<ItemSlot> _itemSlots = new HashSet<ItemSlot>();
// Central events forwarded from registered pickups/slots
// Broadcasts when any registered pickup was picked up (passes the picked item data)
public event Action<PickupItemData> OnItemPickedUp;
// Broadcasts when any registered ItemSlot reports a correct item slotted
// Args: slot's itemData (the slot definition), then the slotted item data
public event Action<PickupItemData, PickupItemData> OnCorrectItemSlotted;
void Awake()
{
_instance = this;
}
void Start()
{
// Subscribe to scene load completed so we can clear registrations when scenes change
// Access Instance directly to ensure the service is initialized and we get the event hookup.
SceneManagerService.Instance.SceneLoadCompleted += OnSceneLoadCompleted;
}
void OnDestroy()
{
// Unsubscribe from SceneManagerService
if (SceneManagerService.Instance != null)
SceneManagerService.Instance.SceneLoadCompleted -= OnSceneLoadCompleted;
// Ensure we clean up any subscriptions from registered items when the manager is destroyed
ClearAllRegistrations();
}
void OnApplicationQuit()
{
_isQuitting = true;
}
private void OnSceneLoadCompleted(string sceneName)
{
// Clear all registrations when a new scene is loaded, so no stale references persist
ClearAllRegistrations();
}
/// <summary>
/// Unsubscribe all pickup/slot event handlers and clear registries and manager events.
/// </summary>
private void ClearAllRegistrations()
{
// Unsubscribe pickup handlers
var pickupsCopy = new List<Pickup>(_pickups);
foreach (var p in pickupsCopy)
{
if (p != null)
p.OnItemPickedUp -= Pickup_OnItemPickedUp;
}
_pickups.Clear();
// Unsubscribe slot handlers
var slotsCopy = new List<ItemSlot>(_itemSlots);
foreach (var s in slotsCopy)
{
if (s != null)
s.OnCorrectItemSlotted -= ItemSlot_OnCorrectItemSlotted;
}
_itemSlots.Clear();
// Clear manager-level event subscribers
OnItemPickedUp = null;
OnCorrectItemSlotted = null;
}
public void RegisterPickup(Pickup pickup)
{
if (pickup == null) return;
// only subscribe if newly added to avoid duplicate subscriptions
if (_pickups.Add(pickup))
{
pickup.OnItemPickedUp += Pickup_OnItemPickedUp;
}
}
public void UnregisterPickup(Pickup pickup)
{
if (pickup == null) return;
if (_pickups.Remove(pickup))
{
pickup.OnItemPickedUp -= Pickup_OnItemPickedUp;
}
}
public void RegisterItemSlot(ItemSlot slot)
{
if (slot == null) return;
if (_itemSlots.Add(slot))
{
slot.OnCorrectItemSlotted += ItemSlot_OnCorrectItemSlotted;
}
}
public void UnregisterItemSlot(ItemSlot slot)
{
if (slot == null) return;
if (_itemSlots.Remove(slot))
{
slot.OnCorrectItemSlotted -= ItemSlot_OnCorrectItemSlotted;
}
}
// Handler that forwards pickup events
private void Pickup_OnItemPickedUp(PickupItemData data)
{
OnItemPickedUp?.Invoke(data);
}
// Handler that forwards correct-slot events
private void ItemSlot_OnCorrectItemSlotted(PickupItemData slotData, PickupItemData slottedItem)
{
OnCorrectItemSlotted?.Invoke(slotData, slottedItem);
}
/// <summary>
/// Returns the current slot state for the given item data by searching registered slots.
/// If the item is currently slotted in a slot, returns that slot's state; otherwise returns ItemSlotState.None.
/// </summary>
public ItemSlotState GetSlotStatusForItem(PickupItemData itemData)
{
if (itemData == null) return ItemSlotState.None;
foreach (var slot in _itemSlots)
{
var slottedObj = slot.GetSlottedObject();
if (slottedObj == null) continue;
var pickup = slottedObj.GetComponent<Pickup>();
if (pickup == null) continue;
if (pickup.itemData == itemData)
{
return slot.CurrentSlottedState;
}
}
return ItemSlotState.None;
}
public IEnumerable<Pickup> Pickups => _pickups;
public IEnumerable<ItemSlot> ItemSlots => _itemSlots;
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a78fe78378e6426da43710f6d0ae84ba
timeCreated: 1758888493

View File

@@ -1,63 +1,466 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using Core;
using Interactions;
using UnityEngine;
using UnityEngine.InputSystem;
using PuzzleS;
namespace Dialogue
{
[AddComponentMenu("Apple Hills/Dialogue/Dialogue Component")]
public class DialogueComponent : MonoBehaviour
{
[SerializeField]
private RuntimeDialogueGraph runtimeGraph;
private Dictionary<string, RuntimeDialogueNode> _nodeLookup = new Dictionary<string, RuntimeDialogueNode>();
private RuntimeDialogueNode _currentNode;
private void Start()
[SerializeField] private RuntimeDialogueGraph dialogueGraph;
private RuntimeDialogueNode currentNode;
private int currentLineIndex;
private bool isWaitingForCondition;
// Events
public event Action<string, string> OnDialogueLineChanged; // speaker name, dialogue text
public event Action OnDialogueCompleted;
public event Action<bool> OnConditionStatusChanged; // Whether the condition is now met
// References to managers
private ItemManager itemManager;
private PuzzleManager puzzleManager;
// Properties
public bool IsActive { get; private set; }
public bool IsCompleted { get; private set; }
public string CurrentSpeakerName => dialogueGraph?.speakerName;
private void Awake()
{
foreach (var node in runtimeGraph.allNodes)
{
_nodeLookup[node.nodeID] = node;
}
// Auto-injection of managers
itemManager = FindFirstObjectByType<ItemManager>();
puzzleManager = FindFirstObjectByType<PuzzleManager>();
if(string.IsNullOrEmpty(runtimeGraph.entryNodeID))
if (itemManager == null)
Debug.LogWarning("DialogueComponent: ItemManager not found in scene!");
if (puzzleManager == null)
Debug.LogWarning("DialogueComponent: PuzzleManager not found in scene!");
}
public void StartDialogue()
{
if (dialogueGraph == null)
{
EndDialogue();
Debug.LogError("DialogueComponent: No dialogue graph assigned!");
return;
}
ShowNode(runtimeGraph.entryNodeID);
// Reset state
IsActive = true;
IsCompleted = false;
isWaitingForCondition = false;
currentLineIndex = 0;
// Set to entry node
currentNode = dialogueGraph.GetNodeByID(dialogueGraph.entryNodeID);
// Register for events based on current node
RegisterForEvents();
// Try to process the current node
ProcessCurrentNode();
}
private void Update()
public bool CanAdvance()
{
if(Mouse.current.leftButton.wasPressedThisFrame && _currentNode != null)
{
if(string.IsNullOrEmpty(_currentNode.nextNodeID))
{
EndDialogue();
}
else
{
ShowNode(_currentNode.nextNodeID);
}
}
// Can't advance if dialogue is not active or is completed
if (!IsActive || IsCompleted) return false;
// Check if we're waiting for a condition
if (isWaitingForCondition) return false;
// Check if we have more lines in the current node
if (currentLineIndex < currentNode.dialogueLines.Count - 1 ||
(currentNode.loopThroughLines && currentNode.dialogueLines.Count > 0))
return true;
// Check if we have a next node
return !string.IsNullOrEmpty(currentNode.nextNodeID);
}
private void ShowNode(string nodeID)
public void Advance()
{
if (!_nodeLookup.ContainsKey(nodeID))
if (!CanAdvance()) return;
// If we have more lines in the current node, advance to the next line
if (currentLineIndex < currentNode.dialogueLines.Count - 1)
{
EndDialogue();
currentLineIndex++;
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
return;
}
_currentNode = _nodeLookup[nodeID];
Debug.Log($"{runtimeGraph.speakerName}: {_currentNode.dialogueLine}");
// If we should loop through lines, reset the index
if (currentNode.loopThroughLines && currentNode.dialogueLines.Count > 0)
{
currentLineIndex = 0;
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
return;
}
// Otherwise, move to the next node
MoveToNextNode();
}
private void EndDialogue()
public void AdvanceToNextNode()
{
Application.Quit();
if (!IsActive || IsCompleted) return;
// Force move to the next node, regardless of current line index
MoveToNextNode();
}
public string GetCurrentDialogueLine()
{
if (currentNode == null || currentNode.dialogueLines.Count == 0) return string.Empty;
if (currentLineIndex < 0 || currentLineIndex >= currentNode.dialogueLines.Count)
return string.Empty;
return currentNode.dialogueLines[currentLineIndex];
}
// Methods to handle dialogue responses for item slots
public void HandleItemSlotInteraction(string slotID, string itemID, bool isForbiddenItem)
{
if (!IsActive || IsCompleted || currentNode == null ||
currentNode.nodeType != RuntimeDialogueNodeType.WaitOnSlot)
return;
// If this is the slot we're waiting for
if (currentNode.slotItemID == slotID)
{
// If correct item is slotted, move to next node
if (itemID == slotID)
{
MoveToNextNode();
}
// If it's a forbidden item, show the forbidden dialogue
else if (isForbiddenItem && currentNode.forbiddenItemLines.Count > 0)
{
ShowResponseLines(currentNode.forbiddenItemLines, currentNode.loopThroughForbiddenLines);
}
// Otherwise show incorrect item dialogue
else if (currentNode.incorrectItemLines.Count > 0)
{
ShowResponseLines(currentNode.incorrectItemLines, currentNode.loopThroughIncorrectLines);
}
}
}
private void ShowResponseLines(List<string> lines, bool loopThrough)
{
StartCoroutine(ShowResponseRoutine(lines, loopThrough));
}
private IEnumerator ShowResponseRoutine(List<string> lines, bool loopThrough)
{
// Store original node and line index
var originalNode = currentNode;
var originalLineIndex = currentLineIndex;
// Show each response line
for (int i = 0; i < lines.Count; i++)
{
// Break if dialogue state has changed
if (currentNode != originalNode || !IsActive || IsCompleted)
break;
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, lines[i]);
// Wait for input to continue
yield return new WaitForSeconds(2f); // Wait time between lines, can be adjusted
// If we should loop and we're at the end, start over
if (loopThrough && i == lines.Count - 1)
i = -1;
// Break after first iteration if not looping
if (!loopThrough && i == 0)
break;
}
// Restore original dialogue line
if (currentNode == originalNode && IsActive && !IsCompleted)
{
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
}
}
private void MoveToNextNode()
{
// Unregister from events based on current node
UnregisterFromEvents();
// If there's no next node, complete the dialogue
if (string.IsNullOrEmpty(currentNode.nextNodeID))
{
IsActive = false;
IsCompleted = true;
OnDialogueCompleted?.Invoke();
return;
}
// Move to the next node
currentNode = dialogueGraph.GetNodeByID(currentNode.nextNodeID);
currentLineIndex = 0;
// Register for events based on new node
RegisterForEvents();
// Process the new node
ProcessCurrentNode();
}
private void ProcessCurrentNode()
{
if (currentNode == null)
{
Debug.LogError("DialogueComponent: Current node is null!");
return;
}
// Handle different node types
switch (currentNode.nodeType)
{
case RuntimeDialogueNodeType.Dialogue:
isWaitingForCondition = false;
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
break;
case RuntimeDialogueNodeType.WaitOnPuzzleStep:
HandlePuzzleStepNode();
break;
case RuntimeDialogueNodeType.WaitOnPickup:
HandlePickupNode();
break;
case RuntimeDialogueNodeType.WaitOnSlot:
HandleSlotNode();
break;
case RuntimeDialogueNodeType.End:
IsActive = false;
IsCompleted = true;
OnDialogueCompleted?.Invoke();
break;
default:
Debug.LogError($"DialogueComponent: Unknown node type {currentNode.nodeType}");
break;
}
}
private void HandlePuzzleStepNode()
{
if (puzzleManager == null)
{
Debug.LogError("DialogueComponent: PuzzleManager is required for WaitOnPuzzleStep nodes!");
MoveToNextNode();
return;
}
// Check if the puzzle step is already completed
if (IsPuzzleStepComplete(currentNode.puzzleStepID))
{
isWaitingForCondition = false;
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
return;
}
// Otherwise, wait for the puzzle step
isWaitingForCondition = true;
OnConditionStatusChanged?.Invoke(false);
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
}
private void HandlePickupNode()
{
if (itemManager == null)
{
Debug.LogError("DialogueComponent: ItemManager is required for WaitOnPickup nodes!");
MoveToNextNode();
return;
}
// Check if the item is already picked up
if (IsItemPickedUp(currentNode.pickupItemID))
{
isWaitingForCondition = false;
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
return;
}
// Otherwise, wait for the item pickup
isWaitingForCondition = true;
OnConditionStatusChanged?.Invoke(false);
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
}
private void HandleSlotNode()
{
if (itemManager == null)
{
Debug.LogError("DialogueComponent: ItemManager is required for WaitOnSlot nodes!");
MoveToNextNode();
return;
}
// Check if the slot already has the correct item
if (IsItemSlotted(currentNode.slotItemID))
{
isWaitingForCondition = false;
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
MoveToNextNode();
return;
}
// Otherwise, wait for the correct item to be slotted
isWaitingForCondition = true;
OnConditionStatusChanged?.Invoke(false);
OnDialogueLineChanged?.Invoke(CurrentSpeakerName, GetCurrentDialogueLine());
}
private void RegisterForEvents()
{
if (currentNode == null) return;
switch (currentNode.nodeType)
{
case RuntimeDialogueNodeType.WaitOnPuzzleStep:
if (puzzleManager != null)
puzzleManager.OnStepCompleted += OnStepCompleted;
break;
case RuntimeDialogueNodeType.WaitOnPickup:
if (itemManager != null)
itemManager.OnItemPickedUp += OnItemPickedUp;
break;
case RuntimeDialogueNodeType.WaitOnSlot:
if (itemManager != null)
itemManager.OnCorrectItemSlotted += OnCorrectItemSlotted;
break;
}
}
private void UnregisterFromEvents()
{
if (currentNode == null) return;
switch (currentNode.nodeType)
{
case RuntimeDialogueNodeType.WaitOnPuzzleStep:
if (puzzleManager != null)
puzzleManager.OnStepCompleted -= OnStepCompleted;
break;
case RuntimeDialogueNodeType.WaitOnPickup:
if (itemManager != null)
itemManager.OnItemPickedUp -= OnItemPickedUp;
break;
case RuntimeDialogueNodeType.WaitOnSlot:
if (itemManager != null)
itemManager.OnCorrectItemSlotted -= OnCorrectItemSlotted;
break;
}
}
// Event handlers for PuzzleManager
private void OnStepCompleted(PuzzleStepSO step)
{
if (!IsActive || !isWaitingForCondition || currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPuzzleStep)
return;
if (step.stepId == currentNode.puzzleStepID)
{
isWaitingForCondition = false;
OnConditionStatusChanged?.Invoke(true);
MoveToNextNode();
}
}
// Event handlers for ItemManager
private void OnItemPickedUp(PickupItemData item)
{
if (!IsActive || !isWaitingForCondition || currentNode.nodeType != RuntimeDialogueNodeType.WaitOnPickup)
return;
if (item.itemId == currentNode.pickupItemID)
{
isWaitingForCondition = false;
OnConditionStatusChanged?.Invoke(true);
MoveToNextNode();
}
}
private void OnCorrectItemSlotted(PickupItemData slotDefinition, PickupItemData slottedItem)
{
if (!IsActive || !isWaitingForCondition || currentNode.nodeType != RuntimeDialogueNodeType.WaitOnSlot)
return;
if (slotDefinition.itemId == currentNode.slotItemID)
{
isWaitingForCondition = false;
OnConditionStatusChanged?.Invoke(true);
MoveToNextNode();
}
}
// Helper methods
private bool IsPuzzleStepComplete(string stepID)
{
if (puzzleManager == null) return false;
// Use the public method instead of accessing the private field
return puzzleManager.IsPuzzleStepCompleted(stepID);
}
private bool IsItemPickedUp(string itemID)
{
if (itemManager == null) return false;
// Check if any picked up item has this ID
foreach (var pickup in itemManager.Pickups)
{
if (pickup.isPickedUp && pickup.itemData != null && pickup.itemData.itemId == itemID)
{
return true;
}
}
return false;
}
private bool IsItemSlotted(string slotID)
{
if (itemManager == null) return false;
// Check if any slot has the correct item with this ID
foreach (var slot in itemManager.ItemSlots)
{
if (slot.CurrentSlottedState == ItemSlotState.Correct &&
slot.itemData != null && slot.itemData.itemId == slotID)
{
return true;
}
}
return false;
}
// Editor functionality
public void SetDialogueGraph(RuntimeDialogueGraph graph)
{
dialogueGraph = graph;
}
}
}
}

View File

@@ -1,3 +1,3 @@
fileFormatVersion: 2
guid: 749c3dece1c14b82845c175203a2e7dc
timeCreated: 1758873871
guid: 25bbad45f1fa4183b30ad76c62256fd6
timeCreated: 1758891211

View File

@@ -4,19 +4,50 @@ using UnityEngine;
namespace Dialogue
{
[Serializable]
public enum RuntimeDialogueNodeType
{
Dialogue,
WaitOnPuzzleStep,
WaitOnPickup,
WaitOnSlot,
End
}
[Serializable]
public class RuntimeDialogueGraph : ScriptableObject
{
public string entryNodeID;
public string speakerName;
public List<RuntimeDialogueNode> allNodes = new List<RuntimeDialogueNode>();
// Helper method to find a node by ID
public RuntimeDialogueNode GetNodeByID(string id)
{
return allNodes.Find(n => n.nodeID == id);
}
}
[Serializable]
public class RuntimeDialogueNode
{
public string nodeID;
public string dialogueLine;
public RuntimeDialogueNodeType nodeType;
public string nextNodeID;
// Basic dialogue
public List<string> dialogueLines = new List<string>();
public bool loopThroughLines;
// Conditional nodes
public string puzzleStepID; // For WaitOnPuzzleStep
public string pickupItemID; // For WaitOnPickup
public string slotItemID; // For WaitOnSlot
// For WaitOnSlot - different responses
public List<string> incorrectItemLines = new List<string>();
public bool loopThroughIncorrectLines;
public List<string> forbiddenItemLines = new List<string>();
public bool loopThroughForbiddenLines;
}
}

View File

@@ -2,6 +2,7 @@
using UnityEngine;
using UnityEngine.Events;
using System; // for Action<T>
using Core; // register with ItemManager
namespace Interactions
{
@@ -174,5 +175,16 @@ namespace Interactions
Interactable.BroadcastInteractionComplete(false);
}
}
// Register with ItemManager when enabled
void Start()
{
ItemManager.Instance?.RegisterItemSlot(this);
}
void OnDestroy()
{
ItemManager.Instance?.UnregisterItemSlot(this);
}
}
}

View File

@@ -1,6 +1,7 @@
using Input;
using UnityEngine;
using System; // added for Action<T>
using Core; // register with ItemManager
namespace Interactions
{
@@ -12,6 +13,9 @@ namespace Interactions
protected Interactable Interactable;
private PlayerTouchController _playerRef;
protected FollowerController FollowerController;
// Track if the item has been picked up
public bool isPickedUp { get; private set; }
// Event: invoked when the item was picked up successfully
public event Action<PickupItemData> OnItemPickedUp;
@@ -34,6 +38,14 @@ namespace Interactions
ApplyItemData();
}
/// <summary>
/// Register with ItemManager on Start
/// </summary>
void Start()
{
ItemManager.Instance?.RegisterPickup(this);
}
/// <summary>
/// Unity OnDestroy callback. Cleans up event handlers.
/// </summary>
@@ -44,6 +56,9 @@ namespace Interactions
Interactable.interactionStarted.RemoveListener(OnInteractionStarted);
Interactable.characterArrived.RemoveListener(OnCharacterArrived);
}
// Unregister from ItemManager
ItemManager.Instance?.UnregisterPickup(this);
}
#if UNITY_EDITOR
@@ -96,9 +111,10 @@ namespace Interactions
bool wasPickedUp = (combinationResult == FollowerController.CombinationResult.NotApplicable);
Interactable.BroadcastInteractionComplete(wasPickedUp);
// Invoke native C# event when the item was picked up successfully
// Update pickup state and invoke event when the item was picked up successfully
if (wasPickedUp)
{
isPickedUp = true;
OnItemPickedUp?.Invoke(itemData);
}
}

View File

@@ -1,18 +1,58 @@
using UnityEngine;
using System.Collections.Generic;
using System;
[CreateAssetMenu(fileName = "PickupItemData", menuName = "Game/Pickup Item Data")]
public class PickupItemData : ScriptableObject
{
[SerializeField] private string _itemId;
public string itemName;
[TextArea]
public string description;
public Sprite mapSprite;
// Read-only property for itemId
public string itemId => _itemId;
// Auto-generate ID on creation or validation
void OnValidate()
{
// Only generate if empty
if (string.IsNullOrEmpty(_itemId))
{
_itemId = GenerateItemId();
#if UNITY_EDITOR
// Mark the asset as dirty to ensure the ID is saved
UnityEditor.EditorUtility.SetDirty(this);
#endif
}
}
private string GenerateItemId()
{
// Use asset name as the basis for the ID to keep it somewhat readable
string baseName = name.Replace(" ", "").ToLowerInvariant();
// Add a unique suffix based on a GUID
string uniqueSuffix = Guid.NewGuid().ToString().Substring(0, 8);
return $"{baseName}_{uniqueSuffix}";
}
// Method to manually regenerate ID if needed (for editor scripts)
public void RegenerateId()
{
_itemId = GenerateItemId();
}
public static bool AreEquivalent(PickupItemData a, PickupItemData b)
{
if (ReferenceEquals(a, b)) return true;
if (a is null || b is null) return false;
// First compare by itemId if available
if (!string.IsNullOrEmpty(a.itemId) && !string.IsNullOrEmpty(b.itemId))
return a.itemId == b.itemId;
// Compare by itemName as a fallback
return a.itemName == b.itemName;
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
@@ -200,6 +201,16 @@ namespace PuzzleS
return _unlockedSteps.Contains(step);
}
/// <summary>
/// Checks if a puzzle step with the specified ID has been completed
/// </summary>
/// <param name="stepId">The ID of the puzzle step to check</param>
/// <returns>True if the step has been completed, false otherwise</returns>
public bool IsPuzzleStepCompleted(string stepId)
{
return _completedSteps.Any(step => step.stepId == stepId);
}
void OnApplicationQuit()
{
_isQuitting = true;