新增动态水物理插件

This commit is contained in:
Bob.Song
2026-02-27 17:44:21 +08:00
parent a6e061d9ce
commit 60744d113d
2218 changed files with 698551 additions and 189 deletions

View File

@@ -0,0 +1,110 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
using NWH.NUI;
#endif
#endregion
namespace NWH.Common.Vehicles
{
/// <summary>
/// Automatically enables/disables a GameObject based on parent vehicle's active state.
/// Useful for optimizing performance by disabling effects, audio, or visuals when a vehicle is inactive.
/// </summary>
/// <remarks>
/// Attach this component to child objects that should only be active when the vehicle is being
/// simulated. When the vehicle is put to sleep (disabled), attached objects are also disabled,
/// saving processing time for effects that wouldn't be visible anyway.
/// </remarks>
[DefaultExecutionOrder(21)]
public partial class FollowVehicleState : MonoBehaviour
{
private Vehicle _vc;
private void OnEnable()
{
_vc = GetComponentInParent<Vehicle>();
if (_vc == null)
{
Debug.LogError("VehicleController not found.");
}
_vc.onEnable.AddListener(OnVehicleWake);
_vc.onDisable.AddListener(OnVehicleSleep);
if (_vc.enabled)
{
OnVehicleWake();
}
else
{
OnVehicleSleep();
}
}
private void OnDisable()
{
// Clean up event listeners to prevent memory leaks
if (_vc != null)
{
_vc.onEnable.RemoveListener(OnVehicleWake);
_vc.onDisable.RemoveListener(OnVehicleSleep);
}
}
private void OnVehicleWake()
{
gameObject.SetActive(true);
}
private void OnVehicleSleep()
{
gameObject.SetActive(false);
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Vehicles
{
[CustomEditor(typeof(FollowVehicleState))]
[CanEditMultipleObjects]
public partial class FollowVehicleStateEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Info("Enables/disables the GameObject based on Vehicle state (awake/asleep).");
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 08f34355d7a235f499204d23250af006
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,81 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Vehicles
{
/// <summary>
/// Attribute that marks a field to be displayed in runtime settings UI.
/// Allows players to adjust vehicle parameters during gameplay.
/// </summary>
[AttributeUsage(AttributeTargets.Field)]
public partial class ShowInSettings : Attribute
{
/// <summary>
/// Maximum value for the setting slider.
/// </summary>
public float max = 1f;
/// <summary>
/// Minimum value for the setting slider.
/// </summary>
public float min;
/// <summary>
/// Display name for the setting in the UI.
/// </summary>
public string name;
/// <summary>
/// Increment step for the slider. Smaller values allow finer adjustment.
/// </summary>
public float step = 0.1f;
/// <summary>
/// Creates a settings attribute with a custom display name.
/// </summary>
public ShowInSettings(string name)
{
this.name = name;
}
/// <summary>
/// Creates a settings attribute with specified min, max, and step values.
/// </summary>
public ShowInSettings(float min, float max, float step = 0.1f)
{
this.min = min;
this.max = max;
this.step = step;
}
/// <summary>
/// Creates a settings attribute with custom name and value constraints.
/// </summary>
public ShowInSettings(string name, float min, float max, float step = 0.1f)
{
this.name = name;
this.min = min;
this.max = max;
this.step = step;
}
public ShowInSettings()
{
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a0a7c580cead1345a77b1082d6b525d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,61 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Vehicles
{
/// <summary>
/// Attribute that marks a field or property to be displayed in the runtime telemetry UI.
/// Used for monitoring vehicle parameters during gameplay and debugging.
/// </summary>
[AttributeUsage(AttributeTargets.All)]
public partial class ShowInTelemetry : Attribute
{
/// <summary>
/// Minimum value for the field (used for progress bar visualization).
/// </summary>
public float Min { get; set; } = float.NaN;
/// <summary>
/// Maximum value for the field (used for progress bar visualization).
/// </summary>
public float Max { get; set; } = float.NaN;
/// <summary>
/// Format string for displaying the value (e.g., "0.00", "0.0").
/// </summary>
public string Format { get; set; } = null;
/// <summary>
/// Unit of measurement (e.g., "km/h", "RPM", "N", "°").
/// </summary>
public string Unit { get; set; } = null;
/// <summary>
/// Display priority. 0 = highest (always visible), 3 = lowest (detailed info).
/// </summary>
public int Priority { get; set; } = 1;
/// <summary>
/// Creates a ShowInTelemetry attribute with optional parameters.
/// </summary>
public ShowInTelemetry(float min = float.NaN, float max = float.NaN, string format = null, string unit = null, int priority = 1)
{
Min = min;
Max = max;
Format = format;
Unit = unit;
Priority = priority;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 82a0970af98f7f04ab501aaec0abdcb1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,309 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
#endregion
namespace NWH.Common.Vehicles
{
/// <summary>
/// Base class for all NWH vehicles including VehiclePhysics2.VehicleController and DWP2.AdvancedShipController.
/// </summary>
[DisallowMultipleComponent]
[RequireComponent(typeof(Rigidbody))]
public abstract class Vehicle : MonoBehaviour
{
/// <summary>
/// Any input below this value will register as no input.
/// </summary>
public const float INPUT_DEADZONE = 0.02f;
/// <summary>
/// Any speed below this value will register as no speed.
/// </summary>
public const float SPEED_DEADZONE = 0.2f;
/// <summary>
/// Anything below this can be considered 0.
/// </summary>
public const float SMALL_NUMBER = 1e-5f;
/// <summary>
/// Like SMALL_NUMBER but a bit bigger.
/// </summary>
public const float KINDA_SMALL_NUMBER = 0.01f;
public static List<Vehicle> ActiveVehicles = new();
/// <summary>
/// Called when active vehicle is changed.
/// First parameter is the previously active vehicle and the second parameter is the currently active vehicle
/// (at the time of the callback). Params can be null.
/// </summary>
public static UnityEvent<Vehicle, Vehicle> onActiveVehicleChanged = new();
/// <summary>
/// True if the vehicle can be driven by the player. False if the
/// vehicle is passive (such as a trailer). A vehicle that has isPlayerControllable
/// can be the ActiveVehicle.
/// </summary>
public bool isPlayerControllable = true;
/// <summary>
/// Cached value of vehicle rigidbody.
/// </summary>
[Tooltip(" Cached value of vehicle rigidbody.")]
[NonSerialized]
public Rigidbody vehicleRigidbody;
/// <summary>
/// Cached value of vehicle transform.
/// </summary>
[Tooltip(" Cached value of vehicle transform.")]
[NonSerialized]
public Transform vehicleTransform;
// Points to the last enabled vehicle.
public static Vehicle ActiveVehicle
{
get
{
int activeVehicleCount = ActiveVehicles.Count;
if (activeVehicleCount == 0)
{
return null;
}
return ActiveVehicles[activeVehicleCount - 1];
}
}
public virtual void Awake()
{
vehicleTransform = transform;
vehicleRigidbody = GetComponent<Rigidbody>();
vehicleRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
}
public virtual void FixedUpdate()
{
// Pre-calculate values
_prevLocalVelocity = LocalVelocity;
Velocity = vehicleRigidbody.linearVelocity;
LocalVelocity = transform.InverseTransformDirection(Velocity);
LocalAcceleration = (LocalVelocity - _prevLocalVelocity) / Time.fixedDeltaTime;
LocalForwardVelocity = LocalVelocity.z;
LocalForwardAcceleration = LocalAcceleration.z;
VelocityMagnitude = Velocity.magnitude;
AngularVelocity = vehicleRigidbody.angularVelocity;
AngularVelocityMagnitude = AngularVelocity.magnitude;
}
public virtual void OnEnable()
{
onEnable.Invoke();
if (isPlayerControllable)
{
if (!ActiveVehicles.Contains(this))
{
Vehicle previouslyActiveVehicle = ActiveVehicle;
ActiveVehicles.Add(this);
Vehicle currentlyActiveVehicle = ActiveVehicle;
onActiveVehicleChanged.Invoke(previouslyActiveVehicle, currentlyActiveVehicle);
}
}
}
public virtual void OnDisable()
{
onDisable.Invoke();
if (isPlayerControllable)
{
Vehicle previouslyActiveVehicle = ActiveVehicle;
ActiveVehicles.RemoveAll(vehicle => vehicle == this);
Vehicle currentlyActiveVehicle = ActiveVehicle;
onActiveVehicleChanged.Invoke(previouslyActiveVehicle, currentlyActiveVehicle);
}
}
#region CAMERA
/// <summary>
/// True when camera is inside vehicle (cockpit, cabin, etc.).
/// Set by the 'CameraInsideVehicle' component.
/// Used for audio effects.
/// </summary>
public bool CameraInsideVehicle
{
get { return _cameraInsideVehicle; }
set
{
if (_cameraInsideVehicle && !value)
{
onCameraExitVehicle.Invoke();
}
else if (!CameraInsideVehicle && value)
{
onCameraEnterVehicle.Invoke();
}
_cameraInsideVehicle = value;
}
}
private bool _cameraInsideVehicle;
/// <summary>
/// Called when the camera enters the vehicle.
/// </summary>
public UnityEvent onCameraEnterVehicle = new();
/// <summary>
/// Called when the camera exits the vehicle.
/// </summary>
public UnityEvent onCameraExitVehicle = new();
#endregion
#region EVENTS
/// <summary>
/// Called when vehicle is put to sleep.
/// </summary>
[Tooltip(" Called when vehicle is put to sleep.")]
[NonSerialized]
public UnityEvent onDisable = new();
/// <summary>
/// Called when vehicle is woken up.
/// </summary>
[Tooltip(" Called when vehicle is woken up.")]
[NonSerialized]
public UnityEvent onEnable = new();
#endregion
#region MULTIPLAYER
/// <summary>
/// Determines if vehicle is running locally is synchronized over active multiplayer framework.
/// </summary>
[Tooltip(" Determines if vehicle is running locally is synchronized over active multiplayer framework.")]
private bool _multiplayerIsRemote;
/// <summary>
/// True if the vehicle is a client (remote) and not simulated.
/// If true the input is expected to be synced through the network.
/// </summary>
public bool MultiplayerIsRemote
{
get { return _multiplayerIsRemote; }
set
{
if (_multiplayerIsRemote && !value)
{
onMultiplayerStatusChanged.Invoke(false);
}
else if (!_multiplayerIsRemote && value)
{
onMultiplayerStatusChanged.Invoke(true);
}
_multiplayerIsRemote = value;
}
}
/// <summary>
/// Invoked when MultiplayerIsRemote value gets changed.
/// Is true if remote.
/// </summary>
public UnityEvent<bool> onMultiplayerStatusChanged = new();
#endregion
#region PHYSICAL_PROPERTIES
/// <summary>
/// Cached acceleration in local coordinates (z-forward)
/// </summary>
public Vector3 LocalAcceleration { get; private set; }
/// <summary>
/// Cached acceleration in forward direction in local coordinates (z-forward).
/// </summary>
public float LocalForwardAcceleration { get; private set; }
/// <summary>
/// Cached velocity in forward direction in local coordinates (z-forward).
/// </summary>
public float LocalForwardVelocity { get; private set; }
/// <summary>
/// Cached velocity in m/s in local coordinates.
/// </summary>
[ShowInTelemetry]
public Vector3 LocalVelocity { get; private set; }
/// <summary>
/// Cached speed of the vehicle in the forward direction. ALWAYS POSITIVE.
/// For positive/negative version use SpeedSigned.
/// </summary>
[ShowInTelemetry(format: "0.0", unit: "m/s")]
public float Speed
{
get { return LocalForwardVelocity < 0 ? -LocalForwardVelocity : LocalForwardVelocity; }
}
/// <summary>
/// Cached speed of the vehicle in the forward direction. Can be positive (forward) or negative (reverse).
/// Equal to LocalForwardVelocity.
/// </summary>
[ShowInTelemetry(format: "0.0", unit: "m/s")]
public float SpeedSigned
{
get { return LocalForwardVelocity; }
}
/// <summary>
/// Cached velocity of the vehicle in world coordinates.
/// </summary>
public Vector3 Velocity { get; protected set; }
/// <summary>
/// Cached velocity magnitude of the vehicle in world coordinates.
/// </summary>
public float VelocityMagnitude { get; protected set; }
/// <summary>
/// Cached angular velocity of the vehicle.
/// </summary>
public Vector3 AngularVelocity { get; protected set; }
/// <summary>
/// Cached angular velocity maginitude of the vehicle.
/// </summary>
public float AngularVelocityMagnitude { get; protected set; }
private Vector3 _prevLocalVelocity;
#endregion
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5a2bed2e81607714099d879a07950fe7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,540 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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
{
/// <summary>
/// Manages switching between multiple vehicles in a scene with support for both instant
/// and character-based (enter/exit) modes.
/// </summary>
/// <remarks>
/// <para>
/// 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
/// </para>
/// <para>
/// 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.
/// </para>
/// <para>
/// Inactive vehicles can optionally be put to sleep to improve performance when managing
/// many vehicles in a scene.
/// </para>
/// </remarks>
[DefaultExecutionOrder(500)]
public class VehicleChanger : MonoBehaviour
{
/// <summary>
/// Represents the player's spatial relationship to vehicles.
/// </summary>
public enum CharacterLocation
{
/// <summary>
/// Player is too far from any vehicle to interact.
/// </summary>
OutOfRange,
/// <summary>
/// Player is close enough to enter a vehicle.
/// </summary>
Near,
/// <summary>
/// Player is currently inside a vehicle.
/// </summary>
Inside,
}
/// <summary>
/// Index of the current vehicle in vehicles list.
/// </summary>
[Tooltip(" Index of the current vehicle in vehicles list.")]
public int activeVehicleIndex;
/// <summary>
/// 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.
/// </summary>
[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;
/// <summary>
/// Game object representing a character. Can also be another vehicle.
/// </summary>
[Tooltip(" Game object representing a character. Can also be another vehicle.")]
public GameObject characterObject;
/// <summary>
/// Maximum distance at which the character will be able to enter the vehicle.
/// </summary>
[Range(0.2f, 3f)]
[Tooltip(" Maximum distance at which the character will be able to enter the vehicle.")]
public float enterDistance = 2f;
/// <summary>
/// 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.
/// </summary>
[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";
/// <summary>
/// When the location is Near, the player can enter the vehicle.
/// </summary>
[Tooltip("When the location is Near, the player can enter the vehicle.")]
public CharacterLocation location = CharacterLocation.OutOfRange;
/// <summary>
/// Maximum speed at which the character will be able to enter / exit the vehicle.
/// </summary>
[Tooltip(" Maximum speed at which the character will be able to enter / exit the vehicle.")]
public float maxEnterExitVehicleSpeed = 2f;
/// <summary>
/// Event invoked when all vehicles are deactivated (e.g., when exiting in character-based mode).
/// </summary>
public UnityEvent onDeactivateAll = new();
/// <summary>
/// Event invoked whenever the active vehicle changes.
/// </summary>
public UnityEvent onVehicleChanged = new();
/// <summary>
/// Should the vehicles that the player is currently not using be put to sleep to improve performance?
/// </summary>
[Tooltip(
" Should the vehicles that the player is currently not using be put to sleep to improve performance?")]
public bool putOtherVehiclesToSleep = true;
/// <summary>
/// Should the player start inside the vehicle?
/// </summary>
[Tooltip("Should the player start inside the vehicle?")]
public bool startInVehicle;
/// <summary>
/// List of all of the vehicles that can be selected and driven in the scene.
/// </summary>
[Tooltip("List of all of the vehicles that can be selected and driven in the scene.")]
public List<Vehicle> vehicles = new();
private GameObject[] _enterExitPoints;
private bool _enterExitPointsDirty = true;
private GameObject _nearestEnterExitPoint;
/// <summary>
/// The vehicle the player is nearest to, or in case the player is inside the vehicle, the vehicle the player is inside
/// of.
/// </summary>
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<SceneInputProviderBase>(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<Vehicle>();
}
}
bool changeVehiclePressed = InputProvider.CombinedInput<SceneInputProviderBase>(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);
}
}
}
}
/// <summary>
/// Puts the player inside the specified vehicle and activates it.
/// In character-based mode, stores the entry position for later exit.
/// </summary>
/// <param name="v">Vehicle to enter.</param>
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);
}
/// <summary>
/// Removes the player from the vehicle and spawns the character object nearby.
/// Character is positioned at the stored entry location.
/// </summary>
/// <param name="v">Vehicle to exit.</param>
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);
}
}
/// <summary>
/// Adds a vehicle to the managed vehicles list if not already present.
/// Newly registered vehicles are automatically disabled unless they are the active vehicle.
/// </summary>
/// <param name="v">Vehicle to register.</param>
public void RegisterVehicle(Vehicle v)
{
if (!vehicles.Contains(v))
{
vehicles.Add(v);
if (activeVehicleIndex != vehicles.Count - 1)
{
v.enabled = false;
}
}
}
/// <summary>
/// Removes a vehicle from the managed vehicles list.
/// If the vehicle was active, automatically switches to the next vehicle.
/// </summary>
/// <param name="v">Vehicle to deregister.</param>
public void DeregisterVehicle(Vehicle v)
{
if (ActiveVehicle == v)
{
NextVehicle();
}
vehicles.Remove(v);
}
/// <summary>
/// Changes vehicle to requested vehicle.
/// </summary>
/// <param name="index"> Index of a vehicle in Vehicles list. </param>
public void ChangeVehicle(int index)
{
if (vehicles.Count == 0)
{
return;
}
activeVehicleIndex = index;
if (activeVehicleIndex >= vehicles.Count)
{
activeVehicleIndex = 0;
}
DeactivateAllExceptActive();
onVehicleChanged.Invoke();
}
/// <summary>
/// Switches to the specified vehicle if it exists in the vehicles list.
/// </summary>
/// <param name="ac">Vehicle reference to switch to.</param>
public void ChangeVehicle(Vehicle ac)
{
int vehicleIndex = vehicles.IndexOf(ac);
if (vehicleIndex >= 0)
{
ChangeVehicle(vehicleIndex);
}
}
/// <summary>
/// Switches to the next vehicle in the list, wrapping to the first vehicle when reaching the end.
/// </summary>
public void NextVehicle()
{
if (vehicles.Count == 1)
{
return;
}
ChangeVehicle(activeVehicleIndex + 1);
}
/// <summary>
/// Switches to the previous vehicle in the list, wrapping to the last vehicle when at the beginning.
/// </summary>
public void PreviousVehicle()
{
if (vehicles.Count == 1)
{
return;
}
int previousIndex = activeVehicleIndex == 0 ? vehicles.Count - 1 : activeVehicleIndex - 1;
ChangeVehicle(previousIndex);
}
/// <summary>
/// Enables the current active vehicle and optionally disables all others based on putOtherVehiclesToSleep setting.
/// </summary>
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;
}
}
}
/// <summary>
/// Disables all managed vehicles including the currently active one.
/// Used when exiting vehicles in character-based mode.
/// </summary>
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<VehicleChanger>();
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

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bc64a64f2888b1942b9b64877d834931
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,150 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 UnityEngine;
using UnityEngine.Rendering;
#if UNITY_EDITOR
using UnityEditor;
using NWH.NUI;
#endif
#endregion
namespace NWH.Common.Vehicles
{
/// <summary>
/// Manages vehicle reflection probe settings, switching between baked and realtime modes
/// based on vehicle activity to optimize performance.
/// </summary>
/// <remarks>
/// Realtime reflection probes are expensive. This component switches to cheaper baked probes
/// when the vehicle is inactive, maintaining visual quality while improving performance.
/// The probe is automatically re-baked when the vehicle becomes inactive to capture the
/// current environment state.
/// </remarks>
[RequireComponent(typeof(ReflectionProbe))]
[DefaultExecutionOrder(19)]
public partial class VehicleReflectionProbe : MonoBehaviour
{
/// <summary>
/// Type of reflection probe to use.
/// </summary>
public enum ProbeType
{
/// <summary>
/// Pre-rendered cubemap, low cost but static.
/// </summary>
Baked,
/// <summary>
/// Updates every frame, high cost but accurate.
/// </summary>
Realtime,
}
/// <summary>
/// Probe type to use when vehicle is inactive/sleeping. Default is Baked for performance.
/// </summary>
public ProbeType asleepProbeType = ProbeType.Baked;
/// <summary>
/// Probe type to use when vehicle is active. Default is Realtime for accurate reflections.
/// </summary>
public ProbeType awakeProbeType = ProbeType.Realtime;
/// <summary>
/// Automatically bake the probe when vehicle becomes inactive to capture environment state.
/// </summary>
public bool bakeOnSleep = true;
/// <summary>
/// Bake the probe once on start for initial environment capture.
/// </summary>
public bool bakeOnStart = true;
private ReflectionProbe _reflectionProbe;
private Vehicle _vc;
private void OnEnable()
{
_vc = GetComponentInParent<Vehicle>();
if (_vc == null)
{
Debug.LogError("VehicleController not found.");
}
_reflectionProbe = GetComponent<ReflectionProbe>();
_vc.onEnable.AddListener(OnVehicleEnable);
_vc.onDisable.AddListener(OnVehicleDisable);
_reflectionProbe.refreshMode = ReflectionProbeRefreshMode.EveryFrame;
if (bakeOnStart)
{
_reflectionProbe.RenderProbe();
}
}
private void OnVehicleEnable()
{
_reflectionProbe.mode = awakeProbeType == ProbeType.Baked
? ReflectionProbeMode.Baked
: ReflectionProbeMode.Realtime;
}
private void OnVehicleDisable()
{
_reflectionProbe.mode = asleepProbeType == ProbeType.Baked
? ReflectionProbeMode.Baked
: ReflectionProbeMode.Realtime;
if (bakeOnSleep && _reflectionProbe.isActiveAndEnabled)
{
_reflectionProbe.RenderProbe();
}
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Vehicles
{
[CustomEditor(typeof(VehicleReflectionProbe))]
[CanEditMultipleObjects]
public partial class VehicleReflectionProbeEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("awakeProbeType");
drawer.Field("asleepProbeType");
drawer.Field("bakeOnStart");
drawer.Field("bakeOnSleep");
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 677f12722777bfc478d18a520b979bc2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: