// ╔════════════════════════════════════════════════════════════════╗ // ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║ // ║ Licensed under Unity Asset Store Terms of Service: ║ // ║ https://unity.com/legal/as-terms ║ // ║ Use permitted only in compliance with the License. ║ // ║ Distributed "AS IS", without warranty of any kind. ║ // ╚════════════════════════════════════════════════════════════════╝ #region using System.Collections.Generic; using NWH.Common.Input; using NWH.Common.Vehicles; using UnityEngine; using UnityEngine.Events; using UnityEngine.SceneManagement; #if UNITY_EDITOR using NWH.NUI; using UnityEditor; #endif #endregion namespace NWH.Common.SceneManagement { /// /// Manages switching between multiple vehicles in a scene with support for both instant /// and character-based (enter/exit) modes. /// /// /// /// VehicleChanger supports two modes: /// - Instant switching: Press a button to cycle through vehicles immediately /// - Character-based: Player must walk to a vehicle and enter/exit at designated points /// /// /// In character-based mode, the player can only enter vehicles when near an EnterExitPoint /// and the vehicle is moving slowly enough. This creates more realistic vehicle switching /// similar to GTA-style games. /// /// /// Inactive vehicles can optionally be put to sleep to improve performance when managing /// many vehicles in a scene. /// /// [DefaultExecutionOrder(500)] public class VehicleChanger : MonoBehaviour { /// /// Represents the player's spatial relationship to vehicles. /// public enum CharacterLocation { /// /// Player is too far from any vehicle to interact. /// OutOfRange, /// /// Player is close enough to enter a vehicle. /// Near, /// /// Player is currently inside a vehicle. /// Inside, } /// /// Index of the current vehicle in vehicles list. /// [Tooltip(" Index of the current vehicle in vehicles list.")] public int activeVehicleIndex; /// /// Is vehicle changing character based? When true changing vehicles will require getting close to them /// to be able to enter, opposed to pressing a button to switch between vehicles. /// [Tooltip( "Is vehicle changing character based? When true changing vehicles will require getting close to them\r\nto be able to enter, opposed to pressing a button to switch between vehicles.")] public bool characterBased; /// /// Game object representing a character. Can also be another vehicle. /// [Tooltip(" Game object representing a character. Can also be another vehicle.")] public GameObject characterObject; /// /// Maximum distance at which the character will be able to enter the vehicle. /// [Range(0.2f, 3f)] [Tooltip(" Maximum distance at which the character will be able to enter the vehicle.")] public float enterDistance = 2f; /// /// Tag of the object representing the point from which the enter distance will be measured. Useful if you want to /// enable you character to enter only when near the door. /// [Tooltip( "Tag of the object representing the point from which the enter distance will be measured. Useful if you want to enable you character to enter only when near the door.")] public string enterExitTag = "EnterExitPoint"; /// /// When the location is Near, the player can enter the vehicle. /// [Tooltip("When the location is Near, the player can enter the vehicle.")] public CharacterLocation location = CharacterLocation.OutOfRange; /// /// Maximum speed at which the character will be able to enter / exit the vehicle. /// [Tooltip(" Maximum speed at which the character will be able to enter / exit the vehicle.")] public float maxEnterExitVehicleSpeed = 2f; /// /// Event invoked when all vehicles are deactivated (e.g., when exiting in character-based mode). /// public UnityEvent onDeactivateAll = new(); /// /// Event invoked whenever the active vehicle changes. /// public UnityEvent onVehicleChanged = new(); /// /// Should the vehicles that the player is currently not using be put to sleep to improve performance? /// [Tooltip( " Should the vehicles that the player is currently not using be put to sleep to improve performance?")] public bool putOtherVehiclesToSleep = true; /// /// Should the player start inside the vehicle? /// [Tooltip("Should the player start inside the vehicle?")] public bool startInVehicle; /// /// List of all of the vehicles that can be selected and driven in the scene. /// [Tooltip("List of all of the vehicles that can be selected and driven in the scene.")] public List vehicles = new(); private GameObject[] _enterExitPoints; private bool _enterExitPointsDirty = true; private GameObject _nearestEnterExitPoint; /// /// The vehicle the player is nearest to, or in case the player is inside the vehicle, the vehicle the player is inside /// of. /// private Vehicle _nearestVehicle; private Vector3 _relativeEnterPosition; public static VehicleChanger Instance { get; private set; } private static Vehicle ActiveVehicle { get { if (Instance == null) { return null; } return Instance.activeVehicleIndex < 0 || Instance.activeVehicleIndex >= Instance.vehicles.Count ? null : Instance.vehicles[Instance.activeVehicleIndex]; } } private void Awake() { Instance = this; // Remove null vehicles from the vehicles list for (int i = vehicles.Count - 1; i >= 0; i--) { if (vehicles[i] == null) { Debug.LogWarning("There is a null reference in the vehicles list. Removing. Make sure that" + " vehicles list does not contain any null references."); vehicles.RemoveAt(i); } } } private void OnEnable() { _enterExitPointsDirty = true; SceneManager.sceneLoaded += OnSceneLoaded; } private void OnDisable() { SceneManager.sceneLoaded -= OnSceneLoaded; } private void OnSceneLoaded(Scene scene, LoadSceneMode mode) { _enterExitPointsDirty = true; } private void Start() { if (characterBased && !startInVehicle) { DeactivateAllIncludingActive(); } else { DeactivateAllExceptActive(); } if (startInVehicle && ActiveVehicle != null) { EnterVehicle(ActiveVehicle); _relativeEnterPosition = new Vector3(-2.5f, 1f, 0.5f); // There was no enter/exit point, make a guess } } private void Update() { if (!characterBased) { bool changeVehicleInput = InputProvider.CombinedInput(i => i.ChangeVehicle()); if (changeVehicleInput) { NextVehicle(); } } else if (characterObject != null) { if (location != CharacterLocation.Inside) { location = CharacterLocation.OutOfRange; if (!characterObject.activeSelf) { characterObject.SetActive(true); } // Cache enter/exit points until scene changes if (_enterExitPointsDirty) { _enterExitPoints = GameObject.FindGameObjectsWithTag(enterExitTag); _enterExitPointsDirty = false; } _nearestEnterExitPoint = null; float nearestSqrDist = Mathf.Infinity; foreach (GameObject eep in _enterExitPoints) { float sqrDist = Vector3.SqrMagnitude(characterObject.transform.position - eep.transform.position); if (sqrDist < nearestSqrDist) { nearestSqrDist = sqrDist; _nearestEnterExitPoint = eep; } } if (_nearestEnterExitPoint == null) { return; } if (Vector3.Magnitude(Vector3.ProjectOnPlane( _nearestEnterExitPoint.transform.position - characterObject.transform.position, Vector3.up)) < enterDistance) { location = CharacterLocation.Near; _nearestVehicle = _nearestEnterExitPoint.GetComponentInParent(); } } bool changeVehiclePressed = InputProvider.CombinedInput(i => i.ChangeVehicle()); if (InputProvider.Instances.Count > 0 && changeVehiclePressed) { // Enter vehicle if (location == CharacterLocation.Near && _nearestVehicle.Speed < maxEnterExitVehicleSpeed) { EnterVehicle(_nearestVehicle); } // Exit vehicle else if (location == CharacterLocation.Inside && _nearestVehicle.Speed < maxEnterExitVehicleSpeed) { ExitVehicle(_nearestVehicle); } } } } /// /// Puts the player inside the specified vehicle and activates it. /// In character-based mode, stores the entry position for later exit. /// /// Vehicle to enter. public void EnterVehicle(Vehicle v) { _nearestVehicle = v; if (characterBased) { characterObject.SetActive(false); _relativeEnterPosition = v.transform.InverseTransformPoint(characterObject.transform.position); location = CharacterLocation.Inside; } Instance.ChangeVehicle(v); } /// /// Removes the player from the vehicle and spawns the character object nearby. /// Character is positioned at the stored entry location. /// /// Vehicle to exit. public void ExitVehicle(Vehicle v) { // Call deactivate all to deactivate on the same frame, preventing 2 audio listeners warning. Instance.DeactivateAllIncludingActive(); location = CharacterLocation.OutOfRange; if (characterBased) { characterObject.transform.position = v.transform.TransformPoint(_relativeEnterPosition); characterObject.transform.forward = v.transform.right; characterObject.transform.up = Vector3.up; characterObject.SetActive(true); } } /// /// Adds a vehicle to the managed vehicles list if not already present. /// Newly registered vehicles are automatically disabled unless they are the active vehicle. /// /// Vehicle to register. public void RegisterVehicle(Vehicle v) { if (!vehicles.Contains(v)) { vehicles.Add(v); if (activeVehicleIndex != vehicles.Count - 1) { v.enabled = false; } } } /// /// Removes a vehicle from the managed vehicles list. /// If the vehicle was active, automatically switches to the next vehicle. /// /// Vehicle to deregister. public void DeregisterVehicle(Vehicle v) { if (ActiveVehicle == v) { NextVehicle(); } vehicles.Remove(v); } /// /// Changes vehicle to requested vehicle. /// /// Index of a vehicle in Vehicles list. public void ChangeVehicle(int index) { if (vehicles.Count == 0) { return; } activeVehicleIndex = index; if (activeVehicleIndex >= vehicles.Count) { activeVehicleIndex = 0; } DeactivateAllExceptActive(); onVehicleChanged.Invoke(); } /// /// Switches to the specified vehicle if it exists in the vehicles list. /// /// Vehicle reference to switch to. public void ChangeVehicle(Vehicle ac) { int vehicleIndex = vehicles.IndexOf(ac); if (vehicleIndex >= 0) { ChangeVehicle(vehicleIndex); } } /// /// Switches to the next vehicle in the list, wrapping to the first vehicle when reaching the end. /// public void NextVehicle() { if (vehicles.Count == 1) { return; } ChangeVehicle(activeVehicleIndex + 1); } /// /// Switches to the previous vehicle in the list, wrapping to the last vehicle when at the beginning. /// public void PreviousVehicle() { if (vehicles.Count == 1) { return; } int previousIndex = activeVehicleIndex == 0 ? vehicles.Count - 1 : activeVehicleIndex - 1; ChangeVehicle(previousIndex); } /// /// Enables the current active vehicle and optionally disables all others based on putOtherVehiclesToSleep setting. /// public void DeactivateAllExceptActive() { for (int i = 0; i < vehicles.Count; i++) { if (i == activeVehicleIndex) { vehicles[i].enabled = true; } else if (putOtherVehiclesToSleep) { vehicles[i].enabled = false; } } } /// /// Disables all managed vehicles including the currently active one. /// Used when exiting vehicles in character-based mode. /// public void DeactivateAllIncludingActive() { for (int i = 0; i < vehicles.Count; i++) { vehicles[i].enabled = false; } onDeactivateAll.Invoke(); } } } #if UNITY_EDITOR namespace NWH.Common.SceneManagement { [CustomEditor(typeof(VehicleChanger))] [CanEditMultipleObjects] public class VehicleChangerEditor : NUIEditor { public override bool OnInspectorNUI() { if (!base.OnInspectorNUI()) { return false; } VehicleChanger sc = drawer.GetObject(); drawer.BeginSubsection("Vehicles"); drawer.ReorderableList("vehicles"); //drawer.Field("deactivateAll"); drawer.Field("putOtherVehiclesToSleep"); drawer.Field("activeVehicleIndex"); if (Application.isPlaying) { drawer.Label("Active Vehicle: " + (Vehicle.ActiveVehicle == null ? "None" : Vehicle.ActiveVehicle.name)); } drawer.EndSubsection(); drawer.BeginSubsection("Character-based Switching"); if (drawer.Field("characterBased").boolValue) { drawer.Field("characterObject"); drawer.Field("enterDistance", true, "m"); drawer.Field("startInVehicle"); drawer.Field("enterExitTag"); drawer.Field("maxEnterExitVehicleSpeed", true, "m/s"); drawer.Field("location", false); } drawer.EndSubsection(); drawer.EndEditor(this); return true; } public override bool UseDefaultMargins() { return false; } } } #endif