新增动态水物理插件

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,8 @@
fileFormatVersion: 2
guid: 8ac6e1af05281f046ad68e9cd8202764
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,85 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.AssetInfo
{
/// <summary>
/// ScriptableObject containing metadata and URLs for an NWH asset.
/// Used by the welcome window and asset information systems.
/// </summary>
[CreateAssetMenu(fileName = "AssetInfo", menuName = "NWH/AssetInfo", order = 0)]
public class AssetInfo : ScriptableObject
{
/// <summary>
/// Display name of the asset.
/// </summary>
public string assetName = "Asset";
/// <summary>
/// Unity Asset Store URL for this asset.
/// </summary>
public string assetURL = "https://assetstore.unity.com/packages/tools/physics/nwh-vehicle-physics-2-166252";
/// <summary>
/// URL to the changelog documentation page.
/// </summary>
public string changelogURL = "";
/// <summary>
/// Discord server invite link for support and community.
/// </summary>
public string discordURL = "https://discord.gg/59CQGEJ";
/// <summary>
/// URL to the main documentation page.
/// </summary>
public string documentationURL = "";
/// <summary>
/// Support email contact link.
/// </summary>
public string emailURL = "mailto:arescec@gmail.com";
/// <summary>
/// Unity Forum thread URL for this asset.
/// </summary>
public string forumURL = "";
/// <summary>
/// URL to quick start guide documentation.
/// </summary>
public string quickStartURL = "";
/// <summary>
/// URL to upgrade notes between versions.
/// </summary>
public string upgradeNotesURL = "";
/// <summary>
/// Current version string of the asset.
/// </summary>
public string version = "1.0";
/// <summary>
/// Recent updates/changes in the current version (3-5 bullet points).
/// </summary>
[TextArea(3, 10)]
public string[] recentUpdates = new string[0];
/// <summary>
/// NWH publisher page URL on Unity Asset Store.
/// </summary>
public string publisherURL = "https://assetstore.unity.com/publishers/14460";
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0968c894b2cd49fbb5dacdb1b4b5f4bb
timeCreated: 1593277002

View File

@@ -0,0 +1,162 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build;
using UnityEngine;
#endregion
namespace NWH.Common.AssetInfo
{
/// <summary>
/// Base class providing editor initialization utilities for NWH packages.
/// Handles scripting defines and welcome window display on package import.
/// </summary>
public class CommonInitializationMethods
{
private static Queue<AssetInfo> _welcomeWindowQueue = new Queue<AssetInfo>();
private static bool _queueProcessScheduled = false;
/// <summary>
/// Adds a scripting define symbol to the current build target if not already present.
/// </summary>
/// <param name="symbol">Scripting define symbol to add (e.g., "NWH_NVP2")</param>
protected static void AddDefines(string symbol)
{
NamedBuildTarget namedBuildTarget = NamedBuildTarget.FromBuildTargetGroup(EditorUserBuildSettings.selectedBuildTargetGroup);
string currentSymbols =
PlayerSettings.GetScriptingDefineSymbols(namedBuildTarget);
string newSymbols = string.Join(";", new HashSet<string>(currentSymbols.Split(';')) { symbol, });
if (currentSymbols != newSymbols)
{
PlayerSettings.SetScriptingDefineSymbols(namedBuildTarget, newSymbols);
}
}
/// <summary>
/// Displays welcome window for specified asset on first import or version update.
/// Uses EditorPrefs to track display state per version. If NWH_ALWAYS_SHOW_WELCOME_WINDOW
/// is defined, always displays window regardless of EditorPrefs state.
/// Windows are queued and displayed sequentially to support multiple packages.
/// Only shows on editor start, not on script reload (uses SessionState).
/// </summary>
/// <param name="assetName">Display name of the asset (must match AssetInfo.assetName)</param>
protected static void ShowWelcomeWindow(string assetName)
{
// Skip if welcome windows have already been processed this editor session (prevents script reload triggers)
if (SessionState.GetBool("NWH_WelcomeWindows_Processed", false))
{
return;
}
if (!GetAssetInfo(assetName, out AssetInfo assetInfo))
{
return;
}
#if NWH_ALWAYS_SHOW_WELCOME_WINDOW
_welcomeWindowQueue.Enqueue(assetInfo);
#else
string key = $"{assetInfo.assetName}_{assetInfo.version}_WW"; // Welcome Window key
if (EditorPrefs.GetBool(key, false) == false)
{
EditorPrefs.SetBool(key, true);
_welcomeWindowQueue.Enqueue(assetInfo);
}
#endif
// Schedule queue processing after all InitializeOnLoadMethod callbacks complete
if (!_queueProcessScheduled)
{
_queueProcessScheduled = true;
EditorApplication.delayCall += ProcessWelcomeWindowQueue;
}
}
/// <summary>
/// Processes queued welcome windows. Marks session as processed and shows first window.
/// Called via EditorApplication.delayCall after all InitializeOnLoadMethod callbacks complete.
/// </summary>
private static void ProcessWelcomeWindowQueue()
{
// Mark as processed to prevent script reload from showing windows again
SessionState.SetBool("NWH_WelcomeWindows_Processed", true);
// Show first window in queue
ShowNextWelcomeWindow();
}
/// <summary>
/// Shows next welcome window from queue. Called when previous window closes.
/// </summary>
internal static void ShowNextWelcomeWindow()
{
if (_welcomeWindowQueue.Count > 0)
{
AssetInfo assetInfo = _welcomeWindowQueue.Dequeue();
ConstructWelcomeWindow(assetInfo);
}
}
/// <summary>
/// Creates and displays WelcomeMessageWindow with specified AssetInfo.
/// Uses CreateInstance to allow multiple independent window instances.
/// </summary>
/// <param name="assetInfo">AssetInfo containing package metadata and URLs</param>
private static void ConstructWelcomeWindow(AssetInfo assetInfo)
{
WelcomeMessageWindow window = ScriptableObject.CreateInstance<WelcomeMessageWindow>();
window.assetInfo = assetInfo;
window.titleContent = new GUIContent(assetInfo.assetName);
window.onCloseCallback = ShowNextWelcomeWindow;
window.Show();
}
/// <summary>
/// Locates and loads AssetInfo asset by name using AssetDatabase search.
/// Works with packages in both Assets/ and Packages/ folders.
/// </summary>
/// <param name="assetName">Asset name to search for</param>
/// <param name="assetInfo">Loaded AssetInfo if found, null otherwise</param>
/// <returns>True if AssetInfo was found and loaded successfully</returns>
private static bool GetAssetInfo(string assetName, out AssetInfo assetInfo)
{
string searchFilter = $"{assetName} AssetInfo t:AssetInfo";
string[] guids = AssetDatabase.FindAssets(searchFilter);
if (guids.Length == 0)
{
Debug.LogWarning($"Could not find AssetInfo for '{assetName}'");
assetInfo = null;
return false;
}
string assetPath = AssetDatabase.GUIDToAssetPath(guids[0]);
assetInfo = AssetDatabase.LoadAssetAtPath<AssetInfo>(assetPath);
if (assetInfo == null)
{
Debug.LogWarning($"Could not load AssetInfo at path {assetPath}");
return false;
}
return true;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 09ae1b3ddcee82a4bb4716f1fc485721
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,271 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.Serialization;
#if UNITY_EDITOR
using NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.Cameras
{
/// <summary>
/// Switches between the camera objects that are children to this object and contain camera tag,
/// in order they appear in the hierarchy or in order they are added to the vehicle cameras list.
/// </summary>
[DefaultExecutionOrder(20)]
public class CameraChanger : MonoBehaviour
{
/// <summary>
/// If true vehicleCameras list will be filled through cameraTag.
/// </summary>
[Tooltip(" If true vehicleCameras list will be filled through cameraTag.")]
public bool autoFindCameras = true;
/// <summary>
/// List of cameras that the changer will cycle through. Leave empty if you want cameras to be automatically detected.
/// To be detected cameras need to have camera tag and be children of the object this script is attached to.
/// </summary>
[FormerlySerializedAs("vehicleCameras")]
[Tooltip(
"List of cameras that the changer will cycle through. Leave empty if you want cameras to be automatically detected." +
" To be detected cameras need to have camera tag and be children of the object this script is attached to.")]
public List<GameObject> cameras = new();
/// <summary>
/// Index of the camera from vehicle cameras list that will be active first.
/// </summary>
[Tooltip(" Index of the camera from vehicle cameras list that will be active first.")]
public int currentCameraIndex;
private Vehicle _vehicle;
/// <summary>
/// Has to be OnEnable as to run before the VehicleController initialization.
/// </summary>
private void Awake()
{
_vehicle = GetComponentInParent<Vehicle>();
if (!_vehicle)
{
Debug.LogError("None of the parent objects of CameraChanger contain VehicleController.");
}
_vehicle.onEnable.AddListener(EnableCurrentDisableOthers);
_vehicle.onDisable.AddListener(DisableAllCameras);
_vehicle.onMultiplayerStatusChanged.AddListener(OnMultiplayerInstanceTypeChanged);
if (!_vehicle)
{
Debug.Log("None of the parents of camera changer contain VehicleController component. " +
"Make sure that the camera changer is amongst the children of VehicleController object.");
}
if (autoFindCameras)
{
cameras = new List<GameObject>();
foreach (Camera cam in GetComponentsInChildren<Camera>(true))
{
cameras.Add(cam.gameObject);
}
}
if (cameras.Count == 0)
{
Debug.LogWarning("No cameras could be found by CameraChanger. Either add cameras manually or " +
"add them as children to the game object this script is attached to.");
}
}
private void Update()
{
if (_vehicle.enabled && !_vehicle.MultiplayerIsRemote && InputProvider.Instances.Count > 0)
{
bool changeCamera = InputProvider.CombinedInput<SceneInputProviderBase>(i => i.ChangeCamera());
if (changeCamera)
{
NextCamera();
CheckIfInside();
}
}
}
private void OnMultiplayerInstanceTypeChanged(bool isRemote)
{
if (isRemote)
{
DisableAllCameras();
}
}
private void EnableCurrentDisableOthers()
{
if (_vehicle.MultiplayerIsRemote)
{
return;
}
int cameraCount = cameras.Count;
for (int i = 0; i < cameraCount; i++)
{
if (cameras[i] == null)
{
continue;
}
if (i == currentCameraIndex)
{
cameras[i].SetActive(true);
AudioListener al = cameras[i].GetComponent<AudioListener>();
if (al != null)
{
al.enabled = true;
}
}
else
{
cameras[i].SetActive(false);
AudioListener al = cameras[i].GetComponent<AudioListener>();
if (al != null)
{
al.enabled = false;
}
}
}
}
private void DisableAllCameras()
{
int cameraCount = cameras.Count;
for (int i = 0; i < cameraCount; i++)
{
cameras[i].SetActive(false);
AudioListener al = cameras[i].GetComponent<AudioListener>();
if (al != null)
{
al.enabled = false;
}
}
}
/// <summary>
/// Activates the next camera in the list, cycling back to the first camera when reaching the end.
/// Automatically disables all other cameras and their AudioListeners.
/// </summary>
public void NextCamera()
{
if (cameras.Count <= 0)
{
return;
}
currentCameraIndex++;
if (currentCameraIndex >= cameras.Count)
{
currentCameraIndex = 0;
}
EnableCurrentDisableOthers();
}
/// <summary>
/// Activates the previous camera in the list, cycling back to the last camera when reaching the beginning.
/// Automatically disables all other cameras and their AudioListeners.
/// </summary>
public void PreviousCamera()
{
if (cameras.Count <= 0)
{
return;
}
currentCameraIndex--;
if (currentCameraIndex < 0)
{
currentCameraIndex = cameras.Count - 1;
}
EnableCurrentDisableOthers();
}
private void CheckIfInside()
{
if (cameras.Count == 0 || cameras[currentCameraIndex] == null)
{
return;
}
CameraInsideVehicle civ = cameras[currentCameraIndex]?.GetComponent<CameraInsideVehicle>();
if (civ != null)
{
_vehicle.CameraInsideVehicle = civ.isInsideVehicle;
}
else
{
_vehicle.CameraInsideVehicle = false;
}
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Cameras
{
[CustomEditor(typeof(CameraChanger))]
[CanEditMultipleObjects]
public class CameraChangerEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("currentCameraIndex");
if (drawer.Field("autoFindCameras").boolValue)
{
drawer.Info(
"When using autoFindCameras make sure that all the cameras are direct children of the object this script is attached to.");
}
else
{
drawer.ReorderableList("cameras");
}
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: ebbf52df36e2d984489f1a6c789c9e95
timeCreated: 1510062979
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,80 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.Common.Vehicles;
using UnityEngine;
#if UNITY_EDITOR
using NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.Cameras
{
/// <summary>
/// Empty component that should be attached to the cameras that are inside the vehicle if interior sound change is to
/// be used.
/// </summary>
public class CameraInsideVehicle : MonoBehaviour
{
/// <summary>
/// Is the camera inside vehicle?
/// </summary>
[Tooltip(" Is the camera inside vehicle?")]
public bool isInsideVehicle = true;
private Vehicle _vehicle;
private void Awake()
{
_vehicle = GetComponentInParent<Vehicle>();
Debug.Assert(_vehicle != null,
"CameraInsideVehicle needs to be attached to an object containing a Vehicle script.");
}
private void Update()
{
_vehicle.CameraInsideVehicle = isInsideVehicle;
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Cameras
{
[CustomEditor(typeof(CameraInsideVehicle))]
[CanEditMultipleObjects]
public class CameraInsideVehicleEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("isInsideVehicle");
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,12 @@
fileFormatVersion: 2
guid: b660c5d2e66c601438dc90ebb20d7357
timeCreated: 1516720286
licenseType: Store
MonoImporter:
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,657 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.Common.Input;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;
#if UNITY_EDITOR
using NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.Cameras
{
/// <summary>
/// Camera that can be dragged with the mouse. Supports Forza-style dynamics.
/// </summary>
public class CameraMouseDrag : VehicleCamera
{
public enum POVType
{
FirstPerson,
ThirdPerson,
}
// ═══════════════════════════════════════════════════════════════
// POV & Basic Settings
// ═══════════════════════════════════════════════════════════════
[Tooltip("Camera POV type. First person camera will invert controls.\r\nZoom is not available in 1st person.")]
public POVType povType = POVType.ThirdPerson;
[Tooltip("Can the camera be rotated by the user?")]
public bool allowRotation = true;
[Tooltip("Can the camera be panned by the user?")]
public bool allowPanning = true;
// ═══════════════════════════════════════════════════════════════
// Distance & Position
// ═══════════════════════════════════════════════════════════════
[Range(0, 100f)]
[Tooltip("Base distance from target at which camera will be positioned.")]
public float distance = 6f;
[Range(0, 100f)]
[Tooltip("Minimum distance that will be reached when zooming in.")]
public float minDistance = 3.0f;
[Range(0, 100f)]
[Tooltip("Maximum distance that will be reached when zooming out.")]
public float maxDistance = 13.0f;
[Range(0, 15)]
[Tooltip("Sensitivity of the middle mouse button / wheel.")]
public float zoomSensitivity = 1f;
[Tooltip("Look position offset from the target center.")]
public Vector3 targetPositionOffset = Vector3.zero;
// ═══════════════════════════════════════════════════════════════
// Rotation
// ═══════════════════════════════════════════════════════════════
[FormerlySerializedAs("followTargetsRotation")]
[Tooltip("If true the camera will rotate with the vehicle along the X and Y axis.")]
public bool followTargetPitchAndYaw = true;
[Tooltip("If true the camera will rotate with the vehicle along the Z axis.")]
public bool followTargetRoll;
[Tooltip("Sensitivity of rotation input.")]
public Vector2 rotationSensitivity = new(3f, 3f);
[Range(-90, 90)]
[Tooltip("Maximum vertical angle the camera can achieve.")]
public float verticalMaxAngle = 80.0f;
[Range(-90, 90)]
[Tooltip("Minimum vertical angle the camera can achieve.")]
public float verticalMinAngle = -20.0f;
[Tooltip("Initial rotation around the X axis (up/down)")]
public float initXRotation = 10f;
[Tooltip("Initial rotation around the Y axis (left/right)")]
public float initYRotation;
[Range(0, 1)]
[Tooltip("Smoothing of the camera rotation.")]
public float rotationSmoothing = 0.08f;
// ═══════════════════════════════════════════════════════════════
// Panning
// ═══════════════════════════════════════════════════════════════
[Tooltip("Sensitivity of panning input.")]
public Vector2 panningSensitivity = new(0.06f, 0.06f);
// ═══════════════════════════════════════════════════════════════
// Auto-Centering (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
[Tooltip("Camera gradually returns behind vehicle after manual rotation stops.")]
public bool useAutoCenter = true;
[Range(0f, 5f)]
[Tooltip("Seconds of no rotation input before auto-centering starts.")]
public float autoCenterDelay = 1.5f;
[Range(0.5f, 10f)]
[Tooltip("Speed at which camera returns to center. Higher = faster.")]
public float autoCenterSpeed = 2f;
[Range(0f, 10f)]
[Tooltip("Minimum vehicle speed (m/s) required for auto-centering. Prevents centering when stationary.")]
public float autoCenterMinSpeed = 2f;
// ═══════════════════════════════════════════════════════════════
// Speed-Based FOV (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
[Tooltip("Dynamically adjust FOV based on vehicle speed.")]
public bool useSpeedFOV = true;
[Range(30f, 90f)]
[Tooltip("Field of view when stationary.")]
public float baseFOV = 60f;
[Range(30f, 120f)]
[Tooltip("Maximum field of view at high speed.")]
public float maxFOV = 75f;
[Range(10f, 200f)]
[Tooltip("Speed (m/s) at which maximum FOV is reached.")]
public float fovSpeedRange = 50f;
[Range(0f, 1f)]
[Tooltip("Smoothing applied to FOV transitions.")]
public float fovSmoothing = 0.3f;
// ═══════════════════════════════════════════════════════════════
// Speed-Based Distance (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
[Tooltip("Camera pulls back at higher speeds.")]
public bool useSpeedDistance = true;
[Range(0f, 0.2f)]
[Tooltip("Extra distance added per m/s of speed.")]
public float speedDistanceMultiplier = 0.05f;
[Range(0f, 10f)]
[Tooltip("Maximum additional distance from speed.")]
public float maxSpeedDistance = 3f;
// ═══════════════════════════════════════════════════════════════
// Speed-Based Height (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
[Tooltip("Camera rises at higher speeds.")]
public bool useSpeedHeight = true;
[Range(0f, 0.1f)]
[Tooltip("Extra height added per m/s of speed.")]
public float speedHeightMultiplier = 0.02f;
[Range(0f, 5f)]
[Tooltip("Maximum additional height from speed.")]
public float maxSpeedHeight = 1f;
// ═══════════════════════════════════════════════════════════════
// Look-Ahead (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
[Tooltip("Camera anticipates turns by rotating into the direction of steering.")]
public bool useLookAhead = true;
[Range(0f, 30f)]
[Tooltip("Maximum degrees of yaw offset when turning.")]
public float lookAheadIntensity = 15f;
[Range(0f, 1f)]
[Tooltip("Smoothing applied to look-ahead. Higher = slower response.")]
public float lookAheadSmoothing = 0.2f;
// ═══════════════════════════════════════════════════════════════
// Camera Shake
// ═══════════════════════════════════════════════════════════════
[Tooltip("Should camera movement on acceleration be used?")]
public bool useShake = true;
[Range(0f, 1f)]
[Tooltip("Maximum head movement from the initial position.")]
public float shakeMaxOffset = 0.2f;
[Range(0f, 1f)]
[Tooltip("How much will the head move around for the given g-force.")]
public float shakeIntensity = 0.125f;
[Range(0f, 1f)]
[Tooltip("Smoothing of the head movement.")]
public float shakeSmoothing = 0.3f;
[Tooltip("Movement intensity per axis. Set to 0 to disable movement on that axis or negative to reverse it.")]
public Vector3 shakeAxisIntensity = new(1f, 0.5f, 1f);
// ═══════════════════════════════════════════════════════════════
// Private Fields
// ═══════════════════════════════════════════════════════════════
// Core
private Vector3 _initialPosition;
private bool _isFirstFrame;
private Rigidbody _rigidbody;
private Camera _camera;
// Rotation
private Vector2 _rot;
private Vector3 _lookDir;
private Vector3 _lookDirVel;
private Vector3 _newLookDir;
private Vector3 _lookAtPosition;
private Vector3 _pan;
// Velocity tracking
private Vector3 _rbLocalVelocity;
private Vector3 _rbPrevLocalVelocity;
private Vector3 _rbLocalAcceleration;
private float _rbSpeed;
// Shake
private Vector3 _acceleration;
private Vector3 _accelerationChangeVelocity;
private Vector3 _localAcceleration;
private Vector3 _newPositionOffset;
private Vector3 _offsetChangeVelocity;
private Vector3 _positionOffset;
private Vector3 _prevAcceleration;
// Auto-center
private float _timeSinceLastRotationInput;
// Speed dynamics
private float _currentFOV;
private float _fovVelocity;
private float _speedDistanceOffset;
private float _speedDistanceVelocity;
private float _speedHeightOffset;
private float _speedHeightVelocity;
// Look-ahead
private float _lookAheadOffset;
private float _lookAheadVelocity;
private bool PointerOverUI
{
get
{
return EventSystem.current != null &&
EventSystem.current.IsPointerOverGameObject();
}
}
private void Start()
{
_initialPosition = transform.localPosition;
_rigidbody = target?.GetComponent<Rigidbody>();
_camera = GetComponent<Camera>();
distance = Mathf.Clamp(distance, minDistance, maxDistance);
_rot.x = initXRotation;
_rot.y = initYRotation;
_isFirstFrame = true;
// Initialize FOV
_currentFOV = baseFOV;
if (_camera != null)
{
_camera.fieldOfView = baseFOV;
}
}
private void FixedUpdate()
{
if (_rigidbody == null)
{
return;
}
_rbPrevLocalVelocity = _rbLocalVelocity;
_rbLocalVelocity = transform.InverseTransformDirection(_rigidbody.linearVelocity);
if (Time.fixedDeltaTime > 0f)
{
_rbLocalAcceleration = (_rbLocalVelocity - _rbPrevLocalVelocity) / Time.fixedDeltaTime;
}
_rbSpeed = _rbLocalVelocity.z < 0 ? -_rbLocalVelocity.z : _rbLocalVelocity.z;
}
private void LateUpdate()
{
if (target == null)
{
return;
}
bool isThirdPerson = povType == POVType.ThirdPerson;
bool hadRotationInput = false;
// ═══════════════════════════════════════════════════════════════
// Input Phase
// ═══════════════════════════════════════════════════════════════
bool pointerOverUI = PointerOverUI;
if (!pointerOverUI)
{
Vector2 rotationInput = InputProvider.CombinedInput<SceneInputProviderBase>(i => i.CameraRotation());
Vector2 panningInput = InputProvider.CombinedInput<SceneInputProviderBase>(i => i.CameraPanning());
float zoomInput = InputProvider.CombinedInput<SceneInputProviderBase>(i => i.CameraZoom());
bool rotationModifier =
InputProvider.CombinedInput<SceneInputProviderBase>(i => i.CameraRotationModifier());
bool panningModifier = InputProvider.CombinedInput<SceneInputProviderBase>(i => i.CameraPanningModifier());
if (allowRotation && rotationModifier)
{
float rotMagnitude = rotationInput.sqrMagnitude;
if (rotMagnitude > 0.001f)
{
_rot.y += rotationInput.x * rotationSensitivity.x;
_rot.x -= rotationInput.y * rotationSensitivity.y;
hadRotationInput = true;
_timeSinceLastRotationInput = 0f;
}
}
if (allowPanning && panningModifier)
{
float pX = panningInput.x * panningSensitivity.x;
float pY = panningInput.y * panningSensitivity.y;
_pan -= target.InverseTransformDirection(transform.right * pX);
_pan -= target.InverseTransformDirection(transform.up * pY);
}
_rot.x = ClampAngle(_rot.x, verticalMinAngle, verticalMaxAngle);
if (isThirdPerson && (zoomInput > 0.0001f || zoomInput < -0.0001f))
{
distance -= zoomInput * zoomSensitivity;
}
}
// ═══════════════════════════════════════════════════════════════
// Auto-Center Phase (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
if (isThirdPerson && useAutoCenter && !hadRotationInput)
{
_timeSinceLastRotationInput += Time.deltaTime;
if (_timeSinceLastRotationInput > autoCenterDelay && _rbSpeed > autoCenterMinSpeed)
{
float centerLerp = autoCenterSpeed * Time.deltaTime;
_rot.x = Mathf.Lerp(_rot.x, initXRotation, centerLerp);
_rot.y = Mathf.Lerp(_rot.y, initYRotation, centerLerp);
}
}
// ═══════════════════════════════════════════════════════════════
// Look-Ahead Phase (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
float effectiveLookAheadOffset = 0f;
if (isThirdPerson && useLookAhead && _rigidbody != null)
{
// Use angular velocity for turn anticipation
float angularVelY = _rigidbody.angularVelocity.y;
// Also factor in lateral velocity for drifts
float lateralVel = Vector3.Dot(_rigidbody.linearVelocity, target.right);
float turnFactor = angularVelY * 2f + lateralVel * 0.1f;
float targetLookAhead = Mathf.Clamp(turnFactor * lookAheadIntensity, -lookAheadIntensity, lookAheadIntensity);
_lookAheadOffset = Mathf.SmoothDamp(_lookAheadOffset, targetLookAhead, ref _lookAheadVelocity, lookAheadSmoothing);
effectiveLookAheadOffset = _lookAheadOffset;
}
// ═══════════════════════════════════════════════════════════════
// Speed Dynamics Phase (Third-Person Only)
// ═══════════════════════════════════════════════════════════════
float effectiveDistance = distance;
Vector3 effectiveTargetOffset = targetPositionOffset;
if (isThirdPerson)
{
// Speed-based FOV
if (useSpeedFOV && _camera != null)
{
float speedNormalized = Mathf.Clamp01(_rbSpeed / fovSpeedRange);
float targetFOV = Mathf.Lerp(baseFOV, maxFOV, speedNormalized);
_currentFOV = Mathf.SmoothDamp(_currentFOV, targetFOV, ref _fovVelocity, fovSmoothing);
_camera.fieldOfView = _currentFOV;
}
// Speed-based distance
if (useSpeedDistance)
{
float targetSpeedDistance = Mathf.Min(_rbSpeed * speedDistanceMultiplier, maxSpeedDistance);
_speedDistanceOffset = Mathf.SmoothDamp(_speedDistanceOffset, targetSpeedDistance, ref _speedDistanceVelocity, 0.2f);
effectiveDistance = distance + _speedDistanceOffset;
}
// Speed-based height
if (useSpeedHeight)
{
float targetSpeedHeight = Mathf.Min(_rbSpeed * speedHeightMultiplier, maxSpeedHeight);
_speedHeightOffset = Mathf.SmoothDamp(_speedHeightOffset, targetSpeedHeight, ref _speedHeightVelocity, 0.2f);
effectiveTargetOffset.y += _speedHeightOffset;
}
}
// ═══════════════════════════════════════════════════════════════
// Position/Rotation Calculation
// ═══════════════════════════════════════════════════════════════
Vector3 forwardVector = followTargetPitchAndYaw ? target.forward : Vector3.forward;
Vector3 rightVector = followTargetPitchAndYaw ? target.right : Vector3.right;
Vector3 upVector = followTargetPitchAndYaw ? target.up : Vector3.up;
_lookAtPosition = target.position +
target.TransformDirection(effectiveTargetOffset + _pan);
// Apply look-ahead to yaw
float effectiveYaw = _rot.y + effectiveLookAheadOffset;
_newLookDir = Quaternion.AngleAxis(_rot.x, rightVector) * forwardVector;
_newLookDir = Quaternion.AngleAxis(effectiveYaw, upVector) * _newLookDir;
_lookDir = _isFirstFrame
? _newLookDir
: Vector3.SmoothDamp(_lookDir, _newLookDir, ref _lookDirVel, rotationSmoothing);
_lookDir = Vector3.Normalize(_lookDir);
if (isThirdPerson)
{
effectiveDistance = Mathf.Clamp(effectiveDistance, minDistance, maxDistance + maxSpeedDistance);
Vector3 targetPosition = _lookAtPosition - _lookDir * effectiveDistance;
transform.position = targetPosition;
transform.forward = _lookDir;
// Check for ground
if (Physics.Raycast(transform.position, -Vector3.up, out RaycastHit hit, 0.5f))
{
transform.position = hit.point + Vector3.up * 0.5f;
}
transform.rotation =
Quaternion.LookRotation(_lookDir, followTargetRoll ? target.up : Vector3.up);
}
else
{
transform.localPosition = _initialPosition + _pan;
transform.rotation =
Quaternion.LookRotation(_lookDir, followTargetRoll ? target.up : Vector3.up);
}
// ═══════════════════════════════════════════════════════════════
// Camera Shake Phase
// ═══════════════════════════════════════════════════════════════
if (useShake)
{
_prevAcceleration = _acceleration;
_acceleration = _rbLocalAcceleration;
_localAcceleration = Vector3.zero;
if (target != null)
{
_localAcceleration = target.TransformDirection(_acceleration);
}
if (!_isFirstFrame)
{
_newPositionOffset = Vector3.SmoothDamp(_prevAcceleration, _localAcceleration,
ref _accelerationChangeVelocity,
shakeSmoothing) / 100f * shakeIntensity;
_newPositionOffset = Vector3.Scale(_newPositionOffset, shakeAxisIntensity);
_positionOffset = Vector3.SmoothDamp(_positionOffset, _newPositionOffset, ref _offsetChangeVelocity,
shakeSmoothing);
_positionOffset = Vector3.ClampMagnitude(_positionOffset, shakeMaxOffset);
transform.position -= target.TransformDirection(_positionOffset) *
Mathf.Clamp01(_rbSpeed * 0.5f);
}
}
_isFirstFrame = false;
}
public void OnDrawGizmosSelected()
{
Gizmos.DrawWireSphere(_lookAtPosition, 0.1f);
Gizmos.DrawRay(_lookAtPosition, _lookDir);
}
private void OnEnable()
{
_isFirstFrame = true;
}
public float ClampAngle(float angle, float min, float max)
{
angle = Mathf.Repeat(angle + 180f, 360f) - 180f;
return Mathf.Clamp(angle, min, max);
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Cameras
{
[CustomEditor(typeof(CameraMouseDrag))]
[CanEditMultipleObjects]
public class CameraMouseDragEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
CameraMouseDrag cam = (CameraMouseDrag)target;
bool isThirdPerson = cam.povType == CameraMouseDrag.POVType.ThirdPerson;
drawer.Field("target");
drawer.BeginSubsection("POV");
drawer.Field("povType");
drawer.EndSubsection();
if (isThirdPerson)
{
drawer.BeginSubsection("Distance & Position");
drawer.Field("distance");
drawer.Field("minDistance");
drawer.Field("maxDistance");
drawer.Field("zoomSensitivity");
drawer.Field("targetPositionOffset");
drawer.EndSubsection();
}
drawer.BeginSubsection("Rotation");
drawer.Field("allowRotation");
drawer.Field("followTargetPitchAndYaw");
drawer.Field("followTargetRoll");
drawer.Field("rotationSensitivity");
drawer.Field("verticalMaxAngle");
drawer.Field("verticalMinAngle");
drawer.Field("initXRotation");
drawer.Field("initYRotation");
drawer.Field("rotationSmoothing");
drawer.EndSubsection();
drawer.BeginSubsection("Panning");
if (drawer.Field("allowPanning").boolValue)
{
drawer.Field("panningSensitivity");
}
drawer.EndSubsection();
// Third-person only features
if (isThirdPerson)
{
drawer.BeginSubsection("Auto-Centering");
drawer.Info("Camera returns behind vehicle after manual rotation stops.");
if (drawer.Field("useAutoCenter").boolValue)
{
drawer.Field("autoCenterDelay");
drawer.Field("autoCenterSpeed");
drawer.Field("autoCenterMinSpeed");
}
drawer.EndSubsection();
drawer.BeginSubsection("Speed-Based FOV");
drawer.Info("FOV increases at high speeds for sense of velocity.");
if (drawer.Field("useSpeedFOV").boolValue)
{
drawer.Field("baseFOV");
drawer.Field("maxFOV");
drawer.Field("fovSpeedRange");
drawer.Field("fovSmoothing");
}
drawer.EndSubsection();
drawer.BeginSubsection("Speed-Based Distance");
drawer.Info("Camera pulls back at higher speeds.");
if (drawer.Field("useSpeedDistance").boolValue)
{
drawer.Field("speedDistanceMultiplier");
drawer.Field("maxSpeedDistance");
}
drawer.EndSubsection();
drawer.BeginSubsection("Speed-Based Height");
drawer.Info("Camera rises at higher speeds.");
if (drawer.Field("useSpeedHeight").boolValue)
{
drawer.Field("speedHeightMultiplier");
drawer.Field("maxSpeedHeight");
}
drawer.EndSubsection();
drawer.BeginSubsection("Look-Ahead");
drawer.Info("Camera anticipates turns by looking into corners.");
if (drawer.Field("useLookAhead").boolValue)
{
drawer.Field("lookAheadIntensity");
drawer.Field("lookAheadSmoothing");
}
drawer.EndSubsection();
}
drawer.BeginSubsection("Camera Shake");
drawer.Info("Movement introduced as a result of acceleration.");
if (drawer.Field("useShake").boolValue)
{
drawer.Field("shakeMaxOffset");
drawer.Field("shakeIntensity");
drawer.Field("shakeSmoothing");
drawer.Field("shakeAxisIntensity");
}
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,74 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.Cameras
{
/// <summary>
/// Base class for vehicle camera implementations with automatic target detection.
/// </summary>
/// <seealso cref="CameraChanger"/>
/// <seealso cref="CameraInsideVehicle"/>
/// <seealso cref="CameraMouseDrag"/>
public class VehicleCamera : MonoBehaviour
{
/// <summary>
/// Transform to track. Auto-detects parent Rigidbody if not assigned.
/// </summary>
[Tooltip(
"Transform that this script is targeting. Can be left empty if head movement is not being used.")]
public Transform target;
public virtual void Awake()
{
if (target == null)
{
target = GetComponentInParent<Rigidbody>()?.transform;
}
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Cameras
{
[CustomEditor(typeof(VehicleCamera))]
[CanEditMultipleObjects]
public class VehicleCameraEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a01c2715f1cc472ebdd1fecad54410f3
timeCreated: 1609878006

View File

@@ -0,0 +1,47 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.CoM
{
/// <summary>
/// Interface for objects that contribute mass and affect vehicle center of mass calculations.
/// Implemented by fuel tanks, cargo systems, and other variable mass components.
/// </summary>
/// <remarks>
/// Mass affectors allow dynamic vehicle physics by contributing their mass and position
/// to the overall center of mass calculation. As fuel depletes or cargo loads change,
/// the vehicle's handling characteristics update automatically.
/// </remarks>
public interface IMassAffector
{
/// <summary>
/// Current mass of this affector in kilograms.
/// Should return variable values for fuel tanks, cargo, etc.
/// </summary>
/// <returns>Mass in kg</returns>
float GetMass();
/// <summary>
/// World position of this affector's center of mass.
/// Used for weighted center of mass calculations.
/// </summary>
Vector3 GetWorldCenterOfMass();
/// <summary>
/// Returns transform of the mass affector.
/// </summary>
Transform GetTransform();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0a3d7462a39d468281892f29f24f478e
timeCreated: 1609878311

View File

@@ -0,0 +1,88 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.CoM
{
/// <summary>
/// Simple mass affector implementation that contributes a fixed mass at its transform position
/// to the vehicle's center of mass calculations.
/// </summary>
/// <remarks>
/// Use this component for static mass contributions like passengers, cargo, or equipment.
/// For dynamic masses like fuel tanks, create a custom IMassAffector implementation that
/// returns varying mass values.
/// </remarks>
public class MassAffector : MonoBehaviour, IMassAffector
{
/// <summary>
/// Mass contribution of this affector in kilograms.
/// </summary>
public float mass = 100.0f;
/// <summary>
/// Returns the mass of this affector.
/// </summary>
/// <returns>Mass in kilograms.</returns>
public float GetMass()
{
return mass;
}
/// <summary>
/// Returns the transform of this mass affector.
/// </summary>
public Transform GetTransform()
{
return transform;
}
/// <summary>
/// Returns the world position of this mass affector's center of mass.
/// </summary>
public Vector3 GetWorldCenterOfMass()
{
return transform.position;
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.CoM
{
[CustomEditor(typeof(MassAffector))]
[CanEditMultipleObjects]
public class MassAffectorEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("mass", true, "kg");
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 2027acb4e3713ee4abb61754e4f34f2a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 0f529537840cdb34e9b744ffe2fec59b, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,615 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.Common.Utility;
using NWH.Common.Vehicles;
using UnityEngine;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.CoM
{
/// <summary>
/// Dynamic center of mass and inertia calculation system that updates Rigidbody properties
/// based on attached mass affectors like fuel tanks, cargo loads, and passengers.
/// </summary>
/// <remarks>
/// <para>
/// VariableCenterOfMass enables realistic vehicle physics behavior by automatically adjusting
/// center of mass and inertia tensor as vehicle loading changes. This affects handling characteristics,
/// stability, and acceleration response without requiring complex rigidbody hierarchies.
/// </para>
/// <para>
/// The system calculates total mass, weighted center of mass position, and inertia contributions
/// from all IMassAffector components. Changes in fuel level, cargo loading, or passenger weight
/// immediately affect vehicle dynamics, creating realistic weight distribution effects.
/// </para>
/// <para>
/// Critical for vehicle realism: Front-heavy vehicles understeer more, rear-heavy vehicles
/// may oversteer, and high center of mass increases rollover tendency. The system updates
/// these characteristics dynamically based on actual mass distribution.
/// </para>
/// </remarks>
/// <seealso cref="IMassAffector"/>
/// <seealso cref="MassAffector"/>
/// <seealso cref="NWH.Common.Vehicles.Vehicle"/>
[DisallowMultipleComponent]
[DefaultExecutionOrder(-1000)]
[RequireComponent(typeof(Rigidbody))]
public class VariableCenterOfMass : MonoBehaviour
{
/// <summary>
/// Objects attached or part of the vehicle affecting its center of mass and inertia.
/// </summary>
[NonSerialized]
public IMassAffector[] affectors;
/// <summary>
/// Base mass of the object, without IMassAffectors.
/// </summary>
[Tooltip("Base mass of the object, without IMassAffectors.")]
public float baseMass = 1400f;
/// <summary>
/// Center of mass of the object. Auto calculated. To adjust center of mass use centerOfMassOffset.
/// </summary>
[Tooltip(
"Center of mass of the rigidbody. Needs to be readjusted when new colliders are added.")]
public Vector3 centerOfMass = Vector3.zero;
/// <summary>
/// Combined center of mass, including the Rigidbody and any IMassAffectors.
/// </summary>
public Vector3 combinedCenterOfMass = Vector3.zero;
/// <summary>
/// Total inertia tensor. Includes Rigidbody and IMassAffectors.
/// </summary>
public Vector3 combinedInertiaTensor;
/// <summary>
/// Total mass of the object with masses of IMassAffectors counted in.
/// </summary>
[Tooltip("Total mass of the object with masses of IMassAffectors counted in.")]
public float combinedMass = 1400f;
/// <summary>
/// Object dimensions in [m]. X - width, Y - height, Z - length.
/// It is important to set the correct dimensions or otherwise inertia might be calculated incorrectly.
/// </summary>
[Tooltip(
"Object dimensions in [m]. X - width, Y - height, Z - length.\r\nIt is important to set the correct dimensions or otherwise inertia might be calculated incorrectly.")]
public Vector3 dimensions = new(1.8f, 1.6f, 4.6f);
/// <summary>
/// Vector by which the inertia tensor of the rigidbody will be scaled on Start().
/// Due to the uniform density of the rigidbodies, versus the very non-uniform density of a vehicle, inertia can feel
/// off.
/// Use this to adjust inertia tensor values.
/// </summary>
[Tooltip(
" Vector by which the inertia tensor of the rigidbody will be scaled on Start().\r\n Due to the unform density of the rigidbodies, versus the very non-uniform density of a vehicle, inertia can feel\r\n off.\r\n Use this to adjust inertia tensor values.")]
public Vector3 inertiaTensor = new(1000f, 1000f, 1000f);
/// <summary>
/// When enabled the Unity-calculated center of mass will be used.
/// </summary>
[Tooltip(
"When enabled the Unity-calculated center of mass will be used.")]
public bool useDefaultCenterOfMass = true;
/// <summary>
/// When true inertia settings will be ignored and default Rigidbody inertia tensor will be used.
/// </summary>
[Tooltip("When true inertia settings will be ignored and default Rigidbody inertia tensor will be used.")]
public bool useDefaultInertia = true;
/// <summary>
/// Should the default Rigidbody mass be used?
/// </summary>
public bool useDefaultMass = true;
/// <summary>
/// If true, the script will search for any IMassAffectors attached as a child (recursively)
/// of this script and use them when calculating mass, center of mass and inertia tensor.
/// </summary>
public bool useMassAffectors;
/// <summary>
/// When true, properties will be recalculated in the next FixedUpdate.
/// Call MarkDirty() when mass affectors change to trigger update.
/// </summary>
[Tooltip("When true, properties will be recalculated in the next FixedUpdate. Automatically managed.")]
public bool isDirty = true;
private Rigidbody _rigidbody;
private void Initialize()
{
_rigidbody = GetComponent<Rigidbody>();
if (useDefaultMass)
{
baseMass = _rigidbody.mass;
}
if (useDefaultInertia)
{
inertiaTensor = _rigidbody.inertiaTensor;
}
if (useDefaultCenterOfMass)
{
centerOfMass = _rigidbody.centerOfMass;
}
affectors = GetMassAffectors();
UpdateAllProperties();
}
private void Awake()
{
Initialize();
}
private void FixedUpdate()
{
if (isDirty)
{
UpdateAllProperties();
isDirty = false;
}
}
/// <summary>
/// Mark properties as needing recalculation.
/// Call this when mass affectors change (fuel consumption, cargo loading, etc.).
/// </summary>
public void MarkDirty()
{
isDirty = true;
}
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if (!Application.isPlaying)
{
Initialize();
UpdateAllProperties();
}
// CoM
Gizmos.color = Color.yellow;
Vector3 worldCoM = transform.TransformPoint(centerOfMass);
Gizmos.DrawSphere(worldCoM, 0.03f);
Handles.Label(worldCoM, "CoM");
// Mass Affectors
Gizmos.color = Color.cyan;
if (affectors == null)
{
return;
}
for (int i = 0; i < affectors.Length; i++)
{
if (affectors[i] == null)
{
continue;
}
Gizmos.DrawSphere(affectors[i].GetTransform().position, 0.05f);
}
// Dimensions
if (!useDefaultInertia)
{
Transform t = transform;
Vector3 fwdOffset = t.forward * dimensions.z * 0.5f;
Vector3 rightOffset = t.right * dimensions.x * 0.5f;
Vector3 upOffset = t.up * dimensions.y * 0.5f;
Gizmos.color = Color.blue;
Gizmos.DrawLine(worldCoM + fwdOffset, worldCoM - fwdOffset);
Gizmos.color = Color.red;
Gizmos.DrawLine(worldCoM + rightOffset, worldCoM - rightOffset);
Gizmos.color = Color.green;
Gizmos.DrawLine(worldCoM + upOffset, worldCoM - upOffset);
}
#endif
}
private void OnValidate()
{
_rigidbody = GetComponent<Rigidbody>();
affectors = GetMassAffectors();
}
/// <summary>
/// Recalculates all Rigidbody properties (mass, center of mass, and inertia) based on current settings and affectors.
/// Called automatically when isDirty flag is set.
/// </summary>
public void UpdateAllProperties()
{
if (!useDefaultMass)
{
UpdateMass();
}
if (!useDefaultCenterOfMass)
{
UpdateCoM();
}
if (!useDefaultInertia)
{
UpdateInertia();
}
}
/// <summary>
/// Calculates and applies the total mass to the Rigidbody.
/// Includes mass from affectors if useMassAffectors is enabled.
/// </summary>
public void UpdateMass()
{
if (useMassAffectors)
{
combinedMass = CalculateMass();
}
else
{
combinedMass = baseMass;
}
_rigidbody.mass = combinedMass;
}
/// <summary>
/// Calculates and applies the CoM to the Rigidbody.
/// </summary>
public void UpdateCoM()
{
if (useMassAffectors)
{
combinedCenterOfMass = centerOfMass + CalculateRelativeCenterOfMassOffset();
}
else
{
combinedCenterOfMass = centerOfMass;
}
_rigidbody.centerOfMass = combinedCenterOfMass;
}
/// <summary>
/// Calculates and applies the inertia tensor to the Rigidbody.
/// </summary>
public void UpdateInertia(bool applyUnchanged = false)
{
if (useMassAffectors)
{
combinedInertiaTensor = inertiaTensor + CalculateInertiaTensorOffset(dimensions);
}
else
{
combinedInertiaTensor = inertiaTensor;
}
// Inertia tensor of constrained rigidbody will be 0 which causes errors when trying to set.
if (combinedInertiaTensor.x > 0 && combinedInertiaTensor.y > 0 && combinedInertiaTensor.z > 0)
{
_rigidbody.inertiaTensor = combinedInertiaTensor;
_rigidbody.inertiaTensorRotation = Quaternion.identity;
}
}
/// <summary>
/// Updates list of IMassAffectors attached to this object.
/// Call after IMassAffector has been added or removed from the object.
/// </summary>
public IMassAffector[] GetMassAffectors()
{
return GetComponentsInChildren<IMassAffector>(true);
}
/// <summary>
/// Calculates the mass of the Rigidbody and attached mass affectors.
/// </summary>
public float CalculateMass()
{
float massSum = baseMass;
if (affectors == null)
{
return massSum;
}
foreach (IMassAffector affector in affectors)
{
if (affector == null || affector.GetTransform() == null)
{
continue;
}
if (affector.GetTransform().gameObject.activeInHierarchy)
{
massSum += affector.GetMass();
}
}
return massSum;
}
/// <summary>
/// Calculates the center of mass of the Rigidbody and attached mass affectors.
/// </summary>
public Vector3 CalculateRelativeCenterOfMassOffset()
{
Vector3 offset = Vector3.zero;
if (useMassAffectors && affectors != null)
{
float massSum = CalculateMass();
for (int i = 0; i < affectors.Length; i++)
{
if (affectors[i] == null || affectors[i].GetTransform() == null)
{
continue;
}
offset += transform.InverseTransformPoint(affectors[i].GetWorldCenterOfMass()) *
(affectors[i].GetMass() / massSum);
}
}
return offset;
}
/// <summary>
/// Calculates the inertia tensor of the Rigidbody and attached mass affectors.
/// </summary>
public Vector3 CalculateInertiaTensorOffset(Vector3 dimensions)
{
Vector3 affectorInertiaSum = Vector3.zero;
if (affectors == null)
{
return affectorInertiaSum;
}
for (int i = 0; i < affectors.Length; i++)
{
IMassAffector affector = affectors[i];
if (affector == null || affector.GetTransform() == null)
{
continue;
}
if (affector.GetTransform().gameObject.activeInHierarchy)
{
float mass = affector.GetMass();
Vector3 affectorLocalPos = transform.InverseTransformPoint(affector.GetTransform().position);
float x = Vector3.ProjectOnPlane(affectorLocalPos, Vector3.right).magnitude * mass;
float y = Vector3.ProjectOnPlane(affectorLocalPos, Vector3.up).magnitude * mass;
float z = Vector3.ProjectOnPlane(affectorLocalPos, Vector3.forward).magnitude * mass;
affectorInertiaSum.x += x * x;
affectorInertiaSum.y += y * y;
affectorInertiaSum.z += z * z;
}
}
return affectorInertiaSum;
}
/// <summary>
/// Calculates inertia tensor for a cuboid with given dimensions and mass.
/// Uses parallel axis theorem for rectangular prism approximation.
/// </summary>
/// <param name="dimensions">Object dimensions in meters (width, height, length)</param>
/// <param name="mass">Total mass in kilograms</param>
/// <returns>Inertia tensor components (Ix, Iy, Iz) in kg⋅m²</returns>
public static Vector3 CalculateInertia(Vector3 dimensions, float mass)
{
float c = 1f / 12f * mass;
float Ix = c * (dimensions.y * dimensions.y + dimensions.z * dimensions.z);
float Iy = c * (dimensions.x * dimensions.x + dimensions.z * dimensions.z);
float Iz = c * (dimensions.y * dimensions.y + dimensions.x * dimensions.x);
return new Vector3(Ix, Iy, Iz);
}
private void Reset()
{
_rigidbody = GetComponent<Rigidbody>();
Bounds bounds = gameObject.FindBoundsIncludeChildren();
dimensions = new Vector3(bounds.extents.x * 2f, bounds.extents.y * 2f, bounds.extents.z * 2f);
Debug.Log($"Detected dimensions of {name} as {dimensions} [m]. If incorrect, adjust manually.");
if (dimensions.x < Vehicle.SMALL_NUMBER)
{
dimensions.x = Vehicle.SMALL_NUMBER;
}
if (dimensions.y < Vehicle.SMALL_NUMBER)
{
dimensions.y = Vehicle.SMALL_NUMBER;
}
if (dimensions.z < Vehicle.SMALL_NUMBER)
{
dimensions.z = Vehicle.SMALL_NUMBER;
}
centerOfMass = _rigidbody.centerOfMass;
baseMass = _rigidbody.mass;
combinedMass = baseMass;
inertiaTensor = _rigidbody.inertiaTensor;
}
/// <summary>
/// Gets the combined center of mass position in world space coordinates.
/// </summary>
/// <returns>World space position of the center of mass</returns>
public Vector3 GetWorldCenterOfMass()
{
return transform.TransformPoint(combinedCenterOfMass);
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.CoM
{
[CustomEditor(typeof(VariableCenterOfMass))]
public class VariableCenterOfMassEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
VariableCenterOfMass vcom = (VariableCenterOfMass)target;
if (vcom == null)
{
drawer.EndEditor();
return false;
}
Rigidbody parentRigidbody = vcom.gameObject.GetComponentInParent<Rigidbody>(true);
if (parentRigidbody == null)
{
drawer.EndEditor();
return false;
}
if (!Application.isPlaying)
{
foreach (Object o in targets)
{
VariableCenterOfMass t = (VariableCenterOfMass)o;
t.affectors = t.GetMassAffectors();
t.UpdateAllProperties();
}
}
drawer.BeginSubsection("Mass Affectors");
if (drawer.Field("useMassAffectors").boolValue)
{
if (vcom.affectors != null)
{
if (!Application.isPlaying)
{
vcom.affectors = vcom.GetMassAffectors();
}
for (int i = 0; i < vcom.affectors.Length; i++)
{
IMassAffector affector = vcom.affectors[i];
if (affector == null || affector.GetTransform() == null)
{
continue;
}
string positionStr = i == 0 ? "(this)" : $"Position = {affector.GetTransform().localPosition}";
drawer.Label(
$"{affector.GetTransform().name} | Mass = {affector.GetMass()} | {positionStr}");
}
}
}
drawer.EndSubsection();
// MASS
drawer.BeginSubsection("Mass");
if (!drawer.Field("useDefaultMass").boolValue)
{
float newMass = drawer.Field("baseMass", true, "kg").floatValue;
parentRigidbody.mass = newMass;
if (vcom.useMassAffectors)
{
drawer.Field("combinedMass", false, "kg");
}
}
drawer.EndSubsection();
// CENTER OF MASS
drawer.BeginSubsection("Center Of Mass");
if (!drawer.Field("useDefaultCenterOfMass").boolValue)
{
drawer.Field("centerOfMass");
if (vcom.useMassAffectors)
{
drawer.Field("combinedCenterOfMass", false);
}
}
drawer.EndSubsection();
// INERTIA
drawer.BeginSubsection("Inertia");
if (!drawer.Field("useDefaultInertia").boolValue)
{
drawer.Field("inertiaTensor", true, "kg m2");
if (vcom.useMassAffectors)
{
drawer.Field("combinedInertiaTensor", false, "kg m2");
}
drawer.BeginSubsection("Calculate Inertia From Dimensions");
{
drawer.Field("dimensions", true, "m");
if (drawer.Button("Calculate"))
{
vcom.inertiaTensor =
VariableCenterOfMass.CalculateInertia(vcom.dimensions, parentRigidbody.mass);
EditorUtility.SetDirty(vcom);
}
}
}
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 818177d2061f4558b66bee63b4cc78f6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {fileID: 2800000, guid: 6fde4d0f34470624fbed94f695585e63, type: 3}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 60fc4fb477a32b64bad8e6a902e1c871
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,60 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
using UnityEngine;
using UnityEngine.UI;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Displays the name of the currently active main camera in a Text component.
/// Updates every 0.1 seconds.
/// </summary>
[RequireComponent(typeof(Text))]
public class DemoCameraNameDisplay : MonoBehaviour
{
private Text cameraText;
private void Awake()
{
cameraText = GetComponent<Text>();
StartCoroutine(CameraNameCoroutine());
}
private IEnumerator CameraNameCoroutine()
{
while (true)
{
Camera cameraMain = Camera.main;
if (cameraMain != null)
{
cameraText.text = cameraMain.name;
}
else
{
cameraText.text = "[no main camera]";
}
yield return new WaitForSeconds(0.1f);
}
}
private void OnDestroy()
{
StopAllCoroutines();
}
}
}

View File

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

View File

@@ -0,0 +1,57 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 UnityEditor;
using UnityEngine;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Sets Time.fixedDeltaTime to a specific value for demo scenes.
/// Default is 0.008333s (120Hz) for optimal physics performance.
/// </summary>
[DefaultExecutionOrder(-500)]
public class DemoDtSetter : MonoBehaviour
{
/// <summary>
/// Target physics update rate in seconds. Default 0.008333s equals 120Hz.
/// </summary>
public float fixedDeltaTime = 0.008333f; // 120Hz default.
private void Awake()
{
#if UNITY_EDITOR
if (fixedDeltaTime <= 0.0001f)
{
return;
}
if (Time.fixedDeltaTime <= 0.0001f)
{
return;
}
if (!EditorPrefs.GetBool("DemoDtSetter Warning"))
{
Debug.Log(
$"[Show Once] DemoDtSetter: Setting Time.fixedDeltaTime to {fixedDeltaTime} ({1f / fixedDeltaTime} Hz) " +
$"from the current {Time.fixedDeltaTime} ({1f / Time.fixedDeltaTime} Hz). " +
"Remove the script from the __SceneManager to disable this, but note that the Sports Car damper stiffness " +
"might need to be reduced to prevent jitter.");
Time.fixedDeltaTime = fixedDeltaTime;
EditorPrefs.SetBool("DemoDtSetter Warning", true);
}
#endif
}
}
}

View File

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

View File

@@ -0,0 +1,50 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Moves a Rigidbody in a sinusoidal oscillation pattern.
/// Useful for creating moving platforms or obstacles in demo scenes.
/// </summary>
public class DemoOscillator : MonoBehaviour
{
/// <summary>
/// Speed of the oscillation in Hz. Higher values result in faster movement.
/// </summary>
public float speed = 1f;
/// <summary>
/// Maximum displacement from the starting position in each axis.
/// </summary>
public Vector3 travel;
private Rigidbody _rb;
private Vector3 initPos;
private void Start()
{
_rb = GetComponent<Rigidbody>();
initPos = transform.position;
}
private void FixedUpdate()
{
float sinValue = Mathf.Sin(Time.time * speed);
_rb.MovePosition(initPos + travel * sinValue);
}
}
}

View File

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

View File

@@ -0,0 +1,45 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Continuously rotates a Rigidbody at a constant rate.
/// Useful for rotating platforms or visual elements in demo scenes.
/// </summary>
public class DemoRotator : MonoBehaviour
{
/// <summary>
/// Rotation speed in degrees per second for each axis.
/// </summary>
public Vector3 rotation;
private Transform _cachedTransform;
private Rigidbody _rb;
private Vector3 _scaledRotation;
private void Start()
{
_rb = GetComponent<Rigidbody>();
_cachedTransform = transform;
}
private void FixedUpdate()
{
_scaledRotation = rotation * Time.fixedDeltaTime;
_rb.MoveRotation(_cachedTransform.rotation * Quaternion.Euler(_scaledRotation));
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9cd1053c43b92914a848c55fe7027672
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.Collections;
using NWH.Common.Vehicles;
using UnityEngine;
using UnityEngine.UI;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Displays the name and type of the currently active vehicle in a Text component.
/// Updates every 0.1 seconds.
/// </summary>
[RequireComponent(typeof(Text))]
public class DemoVehicleNameDisplay : MonoBehaviour
{
private Text vehicleText;
private void Awake()
{
vehicleText = GetComponent<Text>();
StartCoroutine(VehicleNameCoroutine());
}
private IEnumerator VehicleNameCoroutine()
{
while (true)
{
Vehicle vehicle = Vehicle.ActiveVehicle;
if (vehicle != null)
{
vehicleText.text = $"{vehicle.name} [{vehicle.GetType().Name}]";
}
else
{
vehicleText.text = "[no active vehicle]";
}
yield return new WaitForSeconds(0.1f);
}
}
private void OnDestroy()
{
StopAllCoroutines();
}
}
}

View File

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

View File

@@ -0,0 +1,51 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.UI;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Controls the display of a welcome message panel in demo scenes.
/// Shows the message when running outside the editor.
/// </summary>
public class DemoWelcomeMessage : MonoBehaviour
{
/// <summary>
/// Button used to close the welcome message panel.
/// </summary>
public Button closeButton;
/// <summary>
/// GameObject containing the welcome message UI.
/// </summary>
public GameObject welcomeMessageGO;
private void Start()
{
if (!Application.isEditor)
{
welcomeMessageGO.SetActive(true);
}
closeButton.onClick.AddListener(Close);
}
private void Close()
{
welcomeMessageGO.SetActive(false);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 404404949c2d4e4b8b17c373de46d923
timeCreated: 1593263505

View File

@@ -0,0 +1,125 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 ENABLE_INPUT_SYSTEM
using UnityEngine.InputSystem;
#endif
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Simple script that drags Rigidbody behind the mouse cursor when MMB is held down.
/// </summary>
public class DragObject : MonoBehaviour
{
private Camera _cam;
private Vector3 _direction;
private float _distance;
private bool _dragging;
private bool _draggingButtonWasPressed;
private bool _draggingButtonWasReleased;
private Vector3 _force;
private float _forceMagnitude;
private float _forceXz;
private Vector3 _globalHitPoint;
private RaycastHit _hit;
private Vector3 _localHitPoint;
private Vector2 _mousePosition;
private Ray _mouseRay;
private Rigidbody _rb;
private Vector3 _rbScreenPos;
private Vector3 _resultantForce;
private void Start()
{
_cam = GetComponent<Camera>();
}
private void Update()
{
if (_cam == null)
{
return;
}
#if ENABLE_INPUT_SYSTEM
_mousePosition = Mouse.current.position.ReadValue();
_mouseRay = _cam.ScreenPointToRay(_mousePosition);
_draggingButtonWasPressed = Mouse.current.middleButton.wasPressedThisFrame;
_draggingButtonWasReleased = Mouse.current.middleButton.wasReleasedThisFrame;
#elif ENABLE_LEGACY_INPUT_MANAGER
_mousePosition = UnityEngine.Input.mousePosition;
_mouseRay = _cam.ScreenPointToRay(_mousePosition);
_draggingButtonWasPressed = UnityEngine.Input.GetKeyDown(KeyCode.Mouse2);
_draggingButtonWasReleased = UnityEngine.Input.GetKeyUp(KeyCode.Mouse2);
#endif
_mouseRay = _cam.ScreenPointToRay(_mousePosition);
if (_draggingButtonWasPressed && !_dragging)
{
if (Physics.Raycast(_mouseRay, out _hit, 600f))
{
_rb = _hit.transform.GetComponent<Rigidbody>();
if (_rb != null)
{
_dragging = true;
_localHitPoint = _rb.transform.InverseTransformPoint(_hit.point);
}
else
{
_dragging = false;
}
}
}
if (_draggingButtonWasReleased)
{
_dragging = false;
}
}
private void FixedUpdate()
{
if (_dragging)
{
_globalHitPoint = _rb.transform.TransformPoint(_localHitPoint);
_rbScreenPos = _cam.WorldToScreenPoint(_globalHitPoint);
_distance = Vector2.Distance(_mousePosition, _rbScreenPos);
_forceMagnitude = _distance * _rb.mass * 0.1f;
_direction = ((Vector3)_mousePosition - _rbScreenPos).normalized;
_force = _forceMagnitude * _direction;
_forceXz = _force.x + _force.z;
_resultantForce = new Vector3(_forceXz * transform.right.x, _force.y, _forceXz * transform.right.z);
_resultantForce = Vector3.ClampMagnitude(_resultantForce, _rb.mass * 100f);
_rb.AddForceAtPosition(_resultantForce, _globalHitPoint);
}
}
private void OnDrawGizmos()
{
if (_dragging)
{
Gizmos.color = Color.green;
Gizmos.DrawSphere(_globalHitPoint, 0.01f);
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 84bc63739ffe466a9c8804e2c882873f
timeCreated: 1593436050

View File

@@ -0,0 +1,50 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using NWH.NUI;
using UnityEditor;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Custom inspector for DragObject demo component.
/// </summary>
[CustomEditor(typeof(DragObject))]
[CanEditMultipleObjects]
public class DragObjectEditor : NUIEditor
{
/// <summary>
/// Draws custom inspector GUI for DragObject.
/// </summary>
/// <returns>True if inspector should continue drawing</returns>
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a1815302ab69459eb48d1d8c93c0c30e
timeCreated: 1593466455

View File

@@ -0,0 +1,239 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 UnityEngine;
using UnityEngine.UI;
#endregion
// Based on: https://forum.unity.com/threads/fpstotext-free-fps-framerate-calculator-with-options.463667/
namespace NWH.Common.Demo
{
/// <summary>
/// Displays the current framerate in a Text component with optional color coding.
/// Supports both instantaneous and averaged FPS measurements.
/// </summary>
[RequireComponent(typeof(Text))]
public class FpsToText : MonoBehaviour
{
/// <summary>
/// Text color when framerate is below badBelow threshold.
/// </summary>
public Color bad = Color.red;
/// <summary>
/// FPS threshold below which the color changes to bad (red).
/// </summary>
public int badBelow = 30;
/// <summary>
/// Round FPS to nearest integer for cleaner display.
/// </summary>
public bool forceIntResult = true;
/// <summary>
/// Text color when framerate is above okayBelow threshold.
/// </summary>
public Color good = Color.green;
/// <summary>
/// Use averaging over multiple samples instead of single frame measurement.
/// </summary>
public bool groupSampling = true;
/// <summary>
/// Text color when framerate is between badBelow and okayBelow.
/// </summary>
public Color okay = Color.yellow;
/// <summary>
/// FPS threshold below which the color changes to okay (yellow).
/// </summary>
public int okayBelow = 60;
/// <summary>
/// Number of samples to average when groupSampling is enabled.
/// </summary>
public int sampleSize = 120;
/// <summary>
/// Use Time.smoothDeltaTime instead of Time.deltaTime for calculations.
/// </summary>
public bool smoothed = true;
/// <summary>
/// Update text display every N frames. 1 = every frame.
/// </summary>
public int updateTextEvery = 1;
/// <summary>
/// Enable color coding based on framerate thresholds.
/// </summary>
public bool useColors = true;
/// <summary>
/// Use Environment.TickCount instead of Time.deltaTime for calculations.
/// </summary>
public bool useSystemTick;
private float _fps;
private float[] _fpsSamples;
private int _sampleIndex;
private int _sysFrameRate;
private int _sysLastFrameRate;
private int _sysLastSysTick;
private Text _targetText;
private int _textUpdateIndex;
private System.Text.StringBuilder _stringBuilder;
protected virtual void Start()
{
_targetText = GetComponent<Text>();
_fpsSamples = new float[sampleSize];
for (int i = 0; i < _fpsSamples.Length; i++)
{
_fpsSamples[i] = 0.001f;
}
_stringBuilder = new System.Text.StringBuilder(16);
if (!_targetText)
{
enabled = false;
}
}
protected virtual void Update()
{
if (_targetText == null)
{
return;
}
if (groupSampling)
{
Group();
}
else
{
SingleFrame();
}
_sampleIndex = _sampleIndex < sampleSize - 1 ? _sampleIndex + 1 : 0;
_textUpdateIndex = _textUpdateIndex > updateTextEvery ? 0 : _textUpdateIndex + 1;
if (_textUpdateIndex == updateTextEvery)
{
_stringBuilder.Clear();
_stringBuilder.Append((int)_fps);
_targetText.text = _stringBuilder.ToString();
}
if (!useColors)
{
return;
}
if (_fps < badBelow)
{
_targetText.color = bad;
return;
}
_targetText.color = _fps < okayBelow ? okay : good;
}
protected virtual void Reset()
{
sampleSize = 20;
updateTextEvery = 1;
smoothed = true;
useColors = true;
good = Color.green;
okay = Color.yellow;
bad = Color.red;
okayBelow = 60;
badBelow = 30;
useSystemTick = false;
forceIntResult = true;
}
protected virtual void SingleFrame()
{
if (useSystemTick)
{
_fps = GetSystemFramerate();
}
else
{
float deltaTime = smoothed ? Time.smoothDeltaTime : Time.deltaTime;
_fps = deltaTime > 0.0001f ? 1f / deltaTime : 0f;
}
if (forceIntResult)
{
_fps = (int)_fps;
}
}
protected virtual void Group()
{
if (useSystemTick)
{
_fpsSamples[_sampleIndex] = GetSystemFramerate();
}
else
{
float deltaTime = smoothed ? Time.smoothDeltaTime : Time.deltaTime;
_fpsSamples[_sampleIndex] = deltaTime > 0.0001f ? 1f / deltaTime : 0f;
}
_fps = 0;
bool loop = true;
int i = 0;
while (loop)
{
if (i == sampleSize - 1)
{
loop = false;
}
_fps += _fpsSamples[i];
i++;
}
_fps /= _fpsSamples.Length;
if (forceIntResult)
{
_fps = (int)_fps;
}
}
protected virtual int GetSystemFramerate()
{
if (Environment.TickCount - _sysLastSysTick >= 1000)
{
_sysLastFrameRate = _sysFrameRate;
_sysFrameRate = 0;
_sysLastSysTick = Environment.TickCount;
}
_sysFrameRate++;
return _sysLastFrameRate;
}
}
}

View File

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

View File

@@ -0,0 +1,137 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.Common.Input;
using UnityEngine;
using UnityEngine.EventSystems;
#endregion
namespace NWH.Common.Demo
{
/// <summary>
/// Simple first-person controller using physics-based movement.
/// Useful for testing and navigating demo scenes on foot.
/// </summary>
/// <remarks>
/// Based on Unity Community Wiki example. Uses Rigidbody for physics-accurate movement
/// with mouse-look camera control.
/// </remarks>
[RequireComponent(typeof(Rigidbody))]
[RequireComponent(typeof(CapsuleCollider))]
public class RigidbodyFPSController : MonoBehaviour
{
/// <summary>
/// Downward acceleration force in m/s^2.
/// </summary>
public float gravity = 10.0f;
/// <summary>
/// Maximum height of jumps in meters.
/// </summary>
public float jumpHeight = 2.0f;
/// <summary>
/// Maximum upward look angle in degrees.
/// </summary>
public float maximumY = 60f;
/// <summary>
/// Maximum velocity change per fixed update, controls acceleration responsiveness.
/// </summary>
public float maxVelocityChange = 10.0f;
/// <summary>
/// Maximum downward look angle in degrees.
/// </summary>
public float minimumY = -60f;
/// <summary>
/// Horizontal mouse look sensitivity.
/// </summary>
public float sensitivityX = 15f;
/// <summary>
/// Vertical mouse look sensitivity.
/// </summary>
public float sensitivityY = 15f;
/// <summary>
/// Movement speed in meters per second.
/// </summary>
public float speed = 10.0f;
private Vector2 _cameraRotationInput;
private bool _grounded;
private Vector2 _movement;
private Rigidbody _rb;
private float _rotationY;
private bool PointerOverUI
{
get { return EventSystem.current.IsPointerOverGameObject(); }
}
private void Awake()
{
_rb = GetComponent<Rigidbody>();
_rb.freezeRotation = true;
_rb.useGravity = false;
}
private void LateUpdate()
{
_movement = InputProvider.CombinedInput<SceneInputProviderBase>(i => i.CharacterMovement());
_cameraRotationInput = InputProvider.CombinedInput<SceneInputProviderBase>(i => i.CameraRotation());
if (_grounded)
{
// Calculate how fast we should be moving
Vector3 targetVelocity = new(_movement.x, 0, _movement.y);
targetVelocity = transform.TransformDirection(targetVelocity);
targetVelocity *= speed;
// Apply a force that attempts to reach our target velocity
Vector3 velocity = _rb.linearVelocity;
Vector3 velocityChange = targetVelocity - velocity;
velocityChange.x = Mathf.Clamp(velocityChange.x, -maxVelocityChange, maxVelocityChange);
velocityChange.z = Mathf.Clamp(velocityChange.z, -maxVelocityChange, maxVelocityChange);
velocityChange.y = 0;
_rb.AddForce(velocityChange, ForceMode.VelocityChange);
}
float timeFactor = Time.deltaTime * 20f;
float rotationX = transform.localEulerAngles.y + _cameraRotationInput.x * sensitivityX * timeFactor;
_rotationY += _cameraRotationInput.y * sensitivityY * timeFactor;
_rotationY = Mathf.Clamp(_rotationY, minimumY, maximumY);
transform.localEulerAngles = new Vector3(-_rotationY, rotationX, 0);
_rb.AddForce(new Vector3(0, -gravity * _rb.mass, 0));
_grounded = false;
}
private float CalculateJumpVerticalSpeed()
{
// From the jump height and gravity we deduce the upwards speed
// for the character to reach at the apex.
return Mathf.Sqrt(2 * jumpHeight * gravity);
}
private void OnCollisionStay()
{
_grounded = true;
}
}
}

View File

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

View File

@@ -0,0 +1,58 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using NWH.NUI;
using UnityEditor;
#endregion
namespace NWH.Common.Demo.Editor
{
/// <summary>
/// Custom inspector for RigidbodyFPSController demo component.
/// </summary>
[CustomEditor(typeof(RigidbodyFPSController))]
[CanEditMultipleObjects]
public class RigidbodyFPSControllerEditor : NUIEditor
{
/// <summary>
/// Draws custom inspector GUI for RigidbodyFPSController.
/// </summary>
/// <returns>True if inspector should continue drawing</returns>
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("gravity");
drawer.Field("maximumY");
drawer.Field("maxVelocityChange");
drawer.Field("minimumY");
drawer.Field("sensitivityX");
drawer.Field("sensitivityY");
drawer.Field("speed");
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b2fefeb2837a4d9b8a048fd5e3ef15ec
timeCreated: 1593353781

View File

@@ -0,0 +1,638 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using NWH.NUI;
using UnityEditor;
using UnityEngine;
#endregion
namespace NWH.Common.AssetInfo
{
/// <summary>
/// EditorWindow displaying welcome message with package information and useful links.
/// Shown on first import or version update via CommonInitializationMethods.
/// </summary>
public class WelcomeMessageWindow : EditorWindow
{
/// <summary>
/// AssetInfo containing package metadata and URLs to display.
/// </summary>
public AssetInfo assetInfo;
/// <summary>
/// Callback invoked when window is closed. Used to trigger next window in queue.
/// </summary>
internal System.Action onCloseCallback;
/// <summary>
/// Currently selected sidebar category index.
/// </summary>
private int selectedSidebarIndex = 0;
/// <summary>
/// Cached logo texture for package branding.
/// </summary>
private Texture2D logoTexture;
/// <summary>
/// Scroll position for content area.
/// </summary>
private Vector2 scrollPosition;
/// <summary>
/// Unity callback when window is enabled.
/// Logo loading moved to OnGUI due to Unity lifecycle (assetInfo not set until after OnEnable).
/// </summary>
private void OnEnable()
{
// Logo will be loaded lazily in OnGUI when assetInfo is available
}
/// <summary>
/// Loads package-specific logo texture with fallback to generic NWH logo.
/// </summary>
private void LoadLogoTexture()
{
if (assetInfo == null)
{
return;
}
// Try to load package-specific logo based on asset name
string assetName = assetInfo.assetName;
string logoPath = null;
if (assetName.Contains("Vehicle Physics"))
{
logoPath = "NWH Vehicle Physics 2/Editor/logo_light";
}
else if (assetName.Contains("Dynamic Water Physics"))
{
logoPath = "Dynamic Water Physics 2/Logos/dwp_logo";
}
else if (assetName.Contains("Aerodynamics"))
{
logoPath = "NWH Aerodynamics/Editor/NAE Logo";
}
else if (assetName.Contains("Wheel Controller"))
{
logoPath = "Wheel Controller 3D/Editor/logo_wc3d_light";
}
if (!string.IsNullOrEmpty(logoPath))
{
logoTexture = Resources.Load<Texture2D>(logoPath);
}
// Fallback to generic NWH logo
if (logoTexture == null)
{
logoPath = "Editor/NWHLogoSquare";
logoTexture = Resources.Load<Texture2D>(logoPath);
}
}
/// <summary>
/// Draws welcome message GUI with package info, documentation links, and support resources.
/// </summary>
/// <param name="assetInfo">AssetInfo containing package metadata</param>
/// <param name="width">Window width in pixels</param>
public static void DrawWelcomeMessage(AssetInfo assetInfo, float width = 300f)
{
if (assetInfo == null)
{
Debug.LogWarning("AssetInfo is null");
return;
}
GUIStyle style = new(EditorStyles.helpBox);
style.margin = new RectOffset(10, 10, 10, 12);
style.padding = new RectOffset(10, 10, 10, 12);
GUILayout.BeginVertical(style, GUILayout.Width(width - 35f));
GUILayout.Space(8);
GUILayout.Label($"Welcome to {assetInfo.assetName}", EditorStyles.boldLabel);
GUILayout.Space(15);
GUILayout.Label($"Thank you for purchasing {assetInfo.assetName}.\n" +
"Check out the following links:");
GUILayout.Space(10);
GUILayout.Label("Existing customer?", EditorStyles.centeredGreyMiniLabel);
if (GUILayout.Button("Upgrade Notes"))
{
Application.OpenURL(assetInfo.upgradeNotesURL);
}
if (GUILayout.Button("Changelog"))
{
Application.OpenURL(assetInfo.changelogURL);
}
GUILayout.Space(5);
GUILayout.Label("New to the asset?", EditorStyles.centeredGreyMiniLabel);
if (GUILayout.Button("Quick Start"))
{
Application.OpenURL(assetInfo.quickStartURL);
}
if (GUILayout.Button("Documentation"))
{
Application.OpenURL(assetInfo.documentationURL);
}
GUILayout.Space(15);
GUILayout.Label("Also, don't forget to join us at Discord:", EditorStyles.centeredGreyMiniLabel);
if (GUILayout.Button("Discord Server"))
{
Application.OpenURL(assetInfo.discordURL);
}
GUILayout.Space(15);
GUILayout.Label("Don't have Discord? You can also contact us through:", EditorStyles.centeredGreyMiniLabel);
if (GUILayout.Button("Email"))
{
Application.OpenURL(assetInfo.emailURL);
}
if (GUILayout.Button("Forum"))
{
Application.OpenURL(assetInfo.forumURL);
}
GUILayout.Space(15);
GUILayout.Label("Enjoying the asset? Please consider leaving a review, \n" +
"it means a lot to us developers. Thank you.", EditorStyles.centeredGreyMiniLabel);
if (GUILayout.Button("Leave a Review"))
{
Application.OpenURL(assetInfo.assetURL);
}
GUILayout.EndVertical();
}
/// <summary>
/// Unity callback to draw window GUI.
/// </summary>
private void OnGUI()
{
if (assetInfo == null)
{
return;
}
// Lazy load logo on first draw if needed
if (logoTexture == null)
{
LoadLogoTexture();
}
// Set minimum window size
minSize = new Vector2(650f, 450f);
// Draw logo at top
DrawLogoSection();
// Draw main content area (sidebar + content)
EditorGUILayout.BeginHorizontal();
// Draw sidebar
DrawSidebar();
// Draw content based on selection
DrawContent();
EditorGUILayout.EndHorizontal();
}
/// <summary>
/// Draws logo section at top of window with background and padding.
/// </summary>
private void DrawLogoSection()
{
// Background box for logo section with NWH brand color
Color originalColor = GUI.backgroundColor;
GUI.backgroundColor = NUISettings.editorHeaderColor;
GUIStyle logoBackgroundStyle = new GUIStyle(EditorStyles.helpBox)
{
padding = new RectOffset(20, 20, 15, 15)
};
EditorGUILayout.BeginVertical(logoBackgroundStyle, GUILayout.Height(120));
GUILayout.FlexibleSpace();
if (logoTexture != null)
{
Rect logoRect = GUILayoutUtility.GetRect(100, 90, GUILayout.ExpandWidth(true));
GUI.DrawTexture(logoRect, logoTexture, ScaleMode.ScaleToFit);
}
else
{
// Fallback: show asset name if logo fails to load
GUIStyle titleStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 18,
alignment = TextAnchor.MiddleCenter
};
GUILayout.Label(assetInfo.assetName, titleStyle);
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndVertical();
GUI.backgroundColor = originalColor;
GUILayout.Space(5);
}
/// <summary>
/// Draws sidebar with category selection buttons.
/// </summary>
private void DrawSidebar()
{
EditorGUILayout.BeginVertical(EditorStyles.helpBox, GUILayout.Width(200));
GUILayout.Space(10);
GUILayout.Label("Categories", EditorStyles.boldLabel);
GUILayout.Space(15);
string[] categories = { "Getting Started", "What's New", "Documentation", "Support & Community", "Other Assets" };
for (int i = 0; i < categories.Length; i++)
{
GUIStyle buttonStyle = new GUIStyle(GUI.skin.button)
{
alignment = TextAnchor.MiddleLeft,
padding = new RectOffset(10, 10, 8, 8),
fontStyle = selectedSidebarIndex == i ? FontStyle.Bold : FontStyle.Normal
};
// NWH brand color for selected item
if (selectedSidebarIndex == i)
{
Color originalColor = GUI.backgroundColor;
GUI.backgroundColor = NUISettings.lightBlueColor;
if (GUILayout.Button(categories[i], buttonStyle, GUILayout.Height(32)))
{
selectedSidebarIndex = i;
}
GUI.backgroundColor = originalColor;
}
else
{
if (GUILayout.Button(categories[i], buttonStyle, GUILayout.Height(32)))
{
selectedSidebarIndex = i;
}
}
GUILayout.Space(5);
}
GUILayout.FlexibleSpace();
EditorGUILayout.EndVertical();
}
/// <summary>
/// Draws content area based on selected sidebar category.
/// </summary>
private void DrawContent()
{
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
EditorGUILayout.BeginVertical(GUILayout.ExpandWidth(true));
GUILayout.Space(10);
switch (selectedSidebarIndex)
{
case 0: // Getting Started
DrawGettingStartedContent();
break;
case 1: // What's New
DrawWhatsNewContent();
break;
case 2: // Documentation
DrawDocumentationContent();
break;
case 3: // Support & Community
DrawSupportContent();
break;
case 4: // Other Assets
DrawOtherAssetsContent();
break;
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndScrollView();
}
/// <summary>
/// Draws Getting Started category content.
/// </summary>
private void DrawGettingStartedContent()
{
// Welcome section
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 };
GUILayout.Label($"Welcome to {assetInfo.assetName}!", headerStyle);
GUILayout.Space(8);
GUILayout.Label($"Thank you for purchasing {assetInfo.assetName}. " +
"We're excited to have you on board!", EditorStyles.wordWrappedLabel);
GUILayout.Space(20);
// Quick Start section
DrawSectionHeader("Quick Start");
GUILayout.Label("Get up and running quickly with our comprehensive Quick Start guide.",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("Open Quick Start Guide", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.quickStartURL);
}
GUILayout.Space(25);
// Existing Customer section
DrawSectionHeader("Existing Customer?");
GUILayout.Label("Already familiar with the asset? Check what's new in this version.",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("View Upgrade Notes", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.upgradeNotesURL);
}
}
/// <summary>
/// Draws Documentation category content.
/// </summary>
private void DrawDocumentationContent()
{
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 };
GUILayout.Label("Documentation & Resources", headerStyle);
GUILayout.Space(15);
// Full Documentation section
DrawSectionHeader("Full Documentation");
GUILayout.Label("Complete reference for all features, components, and API documentation.",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("Open Documentation", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.documentationURL);
}
GUILayout.Space(25);
// Changelog section
DrawSectionHeader("Changelog");
GUILayout.Label("View complete history of changes, fixes, and improvements across all versions.",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("View Changelog", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.changelogURL);
}
}
/// <summary>
/// Draws Support & Community category content.
/// </summary>
private void DrawSupportContent()
{
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 };
GUILayout.Label("Support & Community", headerStyle);
GUILayout.Space(15);
// Discord Community section
DrawSectionHeader("Discord Community");
GUILayout.Label("Join our Discord server for quick help, discussions, and community support.",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("Join Discord Server", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.discordURL);
}
GUILayout.Space(25);
// Direct Support section
DrawSectionHeader("Direct Support");
GUILayout.Label("Contact us directly for technical support and assistance.",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("Email Support", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.emailURL);
}
GUILayout.Space(10);
if (GUILayout.Button("Forum", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.forumURL);
}
EditorGUILayout.EndHorizontal();
GUILayout.Space(25);
// Review section
DrawSectionHeader("Enjoying the Asset?");
GUILayout.Label("Please consider leaving a review. It means a lot to us developers and helps others discover our work!",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("Leave a Review on Asset Store", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.assetURL);
}
}
/// <summary>
/// Draws What's New category content showing recent updates.
/// </summary>
private void DrawWhatsNewContent()
{
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 };
GUILayout.Label($"What's New in {assetInfo.assetName}", headerStyle);
GUILayout.Space(8);
// Version info
GUIStyle versionStyle = new GUIStyle(EditorStyles.label)
{
fontSize = 12,
fontStyle = FontStyle.Italic
};
GUILayout.Label($"Version {assetInfo.version}", versionStyle);
GUILayout.Space(15);
// Recent updates
if (assetInfo.recentUpdates != null && assetInfo.recentUpdates.Length > 0)
{
DrawSectionHeader("Recent Updates");
GUILayout.Space(5);
foreach (string update in assetInfo.recentUpdates)
{
if (!string.IsNullOrEmpty(update))
{
EditorGUILayout.BeginHorizontal();
GUILayout.Label("•", GUILayout.Width(15));
GUILayout.Label(update, EditorStyles.wordWrappedLabel);
EditorGUILayout.EndHorizontal();
GUILayout.Space(5);
}
}
GUILayout.Space(20);
// Link to full changelog
GUILayout.Label("For complete version history and detailed changes:",
EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("View Full Changelog", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.changelogURL);
}
}
else
{
GUILayout.Label("No recent updates information available.",
EditorStyles.centeredGreyMiniLabel);
GUILayout.Space(15);
if (GUILayout.Button("View Full Changelog", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.changelogURL);
}
}
}
/// <summary>
/// Draws Other Assets category content showing other NWH products.
/// </summary>
private void DrawOtherAssetsContent()
{
GUIStyle headerStyle = new GUIStyle(EditorStyles.boldLabel) { fontSize = 14 };
GUILayout.Label("Other NWH Assets", headerStyle);
GUILayout.Space(15);
GUILayout.Label("Explore our other high-quality Unity assets:",
EditorStyles.wordWrappedLabel);
GUILayout.Space(20);
// Define other NWH assets (excluding current one)
string currentAsset = assetInfo.assetName;
if (!currentAsset.Contains("Vehicle Physics"))
{
DrawAssetCard("NWH Vehicle Physics 2",
"Complete vehicle physics solution with realistic wheel physics, sound, damage, and more.",
"https://assetstore.unity.com/packages/tools/physics/nwh-vehicle-physics-2-166252?aid=1011ljhgE");
}
if (!currentAsset.Contains("Dynamic Water Physics"))
{
DrawAssetCard("Dynamic Water Physics 2",
"Advanced water simulation with buoyancy, ship controllers, and realistic water interactions.",
"https://assetstore.unity.com/packages/tools/physics/dynamic-water-physics-2-147990?aid=1011ljhgE");
}
if (!currentAsset.Contains("Wheel Controller"))
{
DrawAssetCard("Wheel Controller 3D",
"Standalone wheel physics controller with advanced tire friction and suspension simulation.",
"https://assetstore.unity.com/packages/tools/physics/wheel-controller-3d-49574?aid=1011ljhgE");
}
if (!currentAsset.Contains("Aerodynamics"))
{
DrawAssetCard("NWH Aerodynamics",
"Realistic aerodynamics simulation for aircraft and vehicles with customizable airfoils.",
"https://assetstore.unity.com/packages/tools/physics/nwh-aerodynamics-288831?aid=1011ljhgE");
}
GUILayout.Space(15);
// Link to publisher page
DrawSectionHeader("View All Assets");
if (GUILayout.Button("Visit NWH Publisher Page", GUILayout.Height(35)))
{
Application.OpenURL(assetInfo.publisherURL);
}
}
/// <summary>
/// Helper method to draw a section header with consistent styling.
/// </summary>
private void DrawSectionHeader(string title)
{
GUIStyle sectionStyle = new GUIStyle(EditorStyles.boldLabel)
{
fontSize = 12
};
Color originalColor = GUI.contentColor;
GUI.contentColor = NUISettings.propertyHeaderColor;
GUILayout.Label(title, sectionStyle);
GUI.contentColor = originalColor;
}
/// <summary>
/// Helper method to draw an asset card with name, description, and link.
/// </summary>
private void DrawAssetCard(string assetName, string description, string url)
{
GUIStyle cardStyle = new GUIStyle(EditorStyles.helpBox)
{
padding = new RectOffset(12, 12, 10, 10)
};
EditorGUILayout.BeginVertical(cardStyle);
GUILayout.Label(assetName, EditorStyles.boldLabel);
GUILayout.Space(5);
GUILayout.Label(description, EditorStyles.wordWrappedLabel);
GUILayout.Space(8);
if (GUILayout.Button("View on Asset Store", GUILayout.Height(30)))
{
Application.OpenURL(url);
}
EditorGUILayout.EndVertical();
GUILayout.Space(12);
}
/// <summary>
/// Unity callback when window is destroyed. Triggers next window in queue.
/// </summary>
private void OnDestroy()
{
onCloseCallback?.Invoke();
}
}
}
#endif

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 776e7980898ca2e488c38390076a85a9

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 7644a1abcbe0cde4cadfd526de32b486
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7fe077b4c52343fa8e5847a73b494315
timeCreated: 1593368872

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 NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.Input
{
/// <summary>
/// Scene input provider using Unity's legacy Input Manager system.
/// Requires input axes and buttons to be configured in Project Settings > Input Manager.
/// </summary>
public class InputManagerSceneInputProvider : SceneInputProviderBase
{
public override bool ChangeCamera()
{
return InputUtils.TryGetButtonDown("ChangeCamera", KeyCode.C);
}
public override Vector2 CameraRotation()
{
return new Vector2(InputUtils.TryGetAxis("CameraRotationX"), InputUtils.TryGetAxis("CameraRotationY"));
}
public override Vector2 CameraPanning()
{
return new Vector2(InputUtils.TryGetAxis("CameraPanningX"), InputUtils.TryGetAxis("CameraPanningY"));
}
public override bool CameraRotationModifier()
{
return InputUtils.TryGetButton("CameraRotationModifier", KeyCode.Mouse0) || !requireCameraRotationModifier;
}
public override bool CameraPanningModifier()
{
return InputUtils.TryGetButton("CameraPanningModifier", KeyCode.Mouse1) || !requireCameraPanningModifier;
}
public override float CameraZoom()
{
return InputUtils.TryGetAxis("CameraZoom");
}
public override bool ChangeVehicle()
{
return InputUtils.TryGetButtonDown("ChangeVehicle", KeyCode.V);
}
public override Vector2 CharacterMovement()
{
return new Vector2(InputUtils.TryGetAxis("FPSMovementX"), InputUtils.TryGetAxis("FPSMovementY"));
}
public override bool ToggleGUI()
{
return InputUtils.TryGetButtonDown("ToggleGUI", KeyCode.Tab);
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Input
{
[CustomEditor(typeof(InputManagerSceneInputProvider))]
public class InputManagerSceneInputProviderEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("requireCameraRotationModifier");
drawer.Field("requireCameraPanningModifier");
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ec707ef612184d5e9a0dfbf4bdba1e7d
timeCreated: 1593368891

View File

@@ -0,0 +1,148 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Input
{
/// <summary>
/// Base class from which all input providers inherit.
/// </summary>
public abstract class InputProvider : MonoBehaviour
{
/// <summary>
/// List of all InputProviders in the scene.
/// </summary>
public static List<InputProvider> Instances = new();
public virtual void Awake()
{
Instances.Add(this);
}
public virtual void OnDestroy()
{
Instances.Remove(this);
}
/// <summary>
/// Returns combined input of all InputProviders present in the scene.
/// Result will be a sum of all inputs of the selected type.
/// T is a type of InputProvider that the input will be retrieved from.
/// </summary>
public static int CombinedInput<T>(Func<T, int> selector) where T : InputProvider
{
int sum = 0;
int count = Instances.Count;
for (int i = 0; i < count; i++)
{
if (i >= Instances.Count)
{
break;
}
InputProvider ip = Instances[i];
if (ip != null && ip is T provider)
{
sum += selector(provider);
}
}
return sum;
}
/// <summary>
/// Returns combined input of all InputProviders present in the scene.
/// Result will be a sum of all inputs of the selected type.
/// T is a type of InputProvider that the input will be retrieved from.
/// </summary>
public static float CombinedInput<T>(Func<T, float> selector) where T : InputProvider
{
float sum = 0;
int count = Instances.Count;
for (int i = 0; i < count; i++)
{
if (i >= Instances.Count)
{
break;
}
InputProvider ip = Instances[i];
if (ip != null && ip is T provider)
{
sum += selector(provider);
}
}
return sum;
}
/// <summary>
/// Returns combined input of all InputProviders present in the scene.
/// Result will be positive if any InputProvider has the selected input set to true.
/// T is a type of InputProvider that the input will be retrieved from.
/// </summary>
public static bool CombinedInput<T>(Func<T, bool> selector) where T : InputProvider
{
int count = Instances.Count;
for (int i = 0; i < count; i++)
{
if (i >= Instances.Count)
{
break;
}
InputProvider ip = Instances[i];
if (ip != null && ip is T provider && selector(provider))
{
return true;
}
}
return false;
}
/// <summary>
/// Returns combined input of all InputProviders present in the scene.
/// Result will be a sum of all inputs of the selected type.
/// T is a type of InputProvider that the input will be retrieved from.
/// </summary>
public static Vector2 CombinedInput<T>(Func<T, Vector2> selector) where T : InputProvider
{
Vector2 sum = Vector2.zero;
int count = Instances.Count;
for (int i = 0; i < count; i++)
{
if (i >= Instances.Count)
{
break;
}
InputProvider ip = Instances[i];
if (ip != null && ip is T provider)
{
sum += selector(provider);
}
}
return sum;
}
}
}

View File

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

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4c1d12a1d99348ce902816a92f250e14
timeCreated: 1593369086

View File

@@ -0,0 +1,135 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.Input
{
/// <summary>
/// Unity Input System implementation of scene input provider.
/// Handles camera controls and scene navigation using the new Input System.
/// </summary>
public class InputSystemSceneInputProvider : SceneInputProviderBase
{
public SceneInputActions sceneInputActions;
private bool _panningModifier;
private bool _rotationModifier;
public override void Awake()
{
base.Awake();
sceneInputActions = new SceneInputActions();
sceneInputActions.Enable();
sceneInputActions.CameraControls.CameraRotationModifier.started += ctx => _rotationModifier = true;
sceneInputActions.CameraControls.CameraRotationModifier.canceled += ctx => _rotationModifier = false;
sceneInputActions.CameraControls.CameraPanningModifier.started += ctx => _panningModifier = true;
sceneInputActions.CameraControls.CameraPanningModifier.canceled += ctx => _panningModifier = false;
}
public override bool ChangeCamera()
{
return sceneInputActions.CameraControls.ChangeCamera.triggered;
}
public override Vector2 CameraRotation()
{
return sceneInputActions.CameraControls.CameraRotation.ReadValue<Vector2>();
}
public override Vector2 CameraPanning()
{
return sceneInputActions.CameraControls.CameraPanning.ReadValue<Vector2>();
}
public override bool CameraRotationModifier()
{
return _rotationModifier || !requireCameraRotationModifier;
}
public override bool CameraPanningModifier()
{
return _panningModifier || !requireCameraPanningModifier;
}
public override float CameraZoom()
{
return sceneInputActions.CameraControls.CameraZoom.ReadValue<float>() * 0.1f;
}
public override bool ChangeVehicle()
{
return sceneInputActions.SceneControls.ChangeVehicle.triggered;
}
public override Vector2 CharacterMovement()
{
return sceneInputActions.SceneControls.FPSMovement.ReadValue<Vector2>();
}
public override bool ToggleGUI()
{
return sceneInputActions.SceneControls.ToggleGUI.triggered;
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Input
{
[CustomEditor(typeof(InputSystemSceneInputProvider))]
public class InputSystemSceneInputProviderEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Info("Input settings for Unity's new input system can be changed by modifying 'SceneInputActions' " +
"file (double click on it to open).");
drawer.Field("requireCameraRotationModifier");
drawer.Field("requireCameraPanningModifier");
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9c48880c19d04dabb53da07d15024184
timeCreated: 1593369107

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,528 @@
{
"name": "SceneInputActions",
"maps": [
{
"name": "CameraControls",
"id": "f9b2c2eb-8265-4430-a0ac-4cf8495a2002",
"actions": [
{
"name": "ChangeCamera",
"type": "Button",
"id": "71ec0b0c-0911-4b04-a2cc-424b01ebe88e",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "CameraRotation",
"type": "Value",
"id": "8f870466-b390-4fae-a439-ccb19a4537c2",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "CameraPanning",
"type": "Value",
"id": "08d3e09d-7ab8-4f42-976a-530f947fe4c8",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "CameraRotationModifier",
"type": "Button",
"id": "124e3374-e4a2-4e74-b0cf-c8959a11ac39",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "CameraPanningModifier",
"type": "Button",
"id": "ce8eda53-b48a-45c4-83c7-3f0b44ad36f7",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "CameraZoom",
"type": "Value",
"id": "018cdf61-e865-49da-9064-33dc2ae63580",
"expectedControlType": "Analog",
"processors": "",
"interactions": "",
"initialStateCheck": true
}
],
"bindings": [
{
"name": "",
"id": "24fa1b4b-fa43-49bc-ba60-3aedbe8d6c1f",
"path": "<Keyboard>/c",
"interactions": "",
"processors": "",
"groups": "",
"action": "ChangeCamera",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "530b85ac-4cae-49f9-804b-3a0dbaeb4a7b",
"path": "<Gamepad>/start",
"interactions": "",
"processors": "",
"groups": "",
"action": "ChangeCamera",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "6d0ae04c-f252-4dd6-824a-27baa3d26db7",
"path": "<Mouse>/delta",
"interactions": "",
"processors": "ScaleVector2(x=0.2,y=0.2)",
"groups": "",
"action": "CameraRotation",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "Gamepad",
"id": "2cb8a8bc-5e28-4393-bc30-fe55c9d9ffc7",
"path": "2DVector(mode=2)",
"interactions": "",
"processors": "InvertVector2",
"groups": "",
"action": "CameraRotation",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "a144950a-0314-41fb-b0a3-0fa7943d12f1",
"path": "<Gamepad>/rightStick/up",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraRotation",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "a039c90a-129d-43ea-b2ec-bffde20e618a",
"path": "<Gamepad>/rightStick/down",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraRotation",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "e78f01bc-414a-4ba8-83e0-02deb5f631c6",
"path": "<Gamepad>/rightStick/left",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraRotation",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "3cd6cbde-a6f8-4da4-8cc2-9c8c1edc133e",
"path": "<Gamepad>/rightStick/right",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraRotation",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "9e78c527-4641-4f9b-98e4-fb7f87edf64d",
"path": "<Mouse>/delta",
"interactions": "",
"processors": "ScaleVector2(x=0.2,y=0.2)",
"groups": "",
"action": "CameraPanning",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "3968d956-a143-403b-87e5-0b91afb999eb",
"path": "<Mouse>/leftButton",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraRotationModifier",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8f3a4e0e-6782-4b53-8c26-e06e68d8e1ee",
"path": "<Gamepad>/rightStickPress",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraRotationModifier",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "8a15b75c-fd20-4def-8b73-5d8273fe3364",
"path": "<Mouse>/rightButton",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraPanningModifier",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "dee7ac85-80d0-4018-bbe7-114eecc930ae",
"path": "<Gamepad>/leftStickPress",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraPanningModifier",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "Gamepad",
"id": "93e86e22-3ea3-4e7f-b800-9fc9575e9190",
"path": "2DVector",
"interactions": "",
"processors": "InvertVector2",
"groups": "",
"action": "CameraPanning",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "9e35bef4-dec2-47ce-a040-063273bd2183",
"path": "<Gamepad>/rightStick/up",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraPanning",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "ca312712-12f3-438c-a542-d998b4fca387",
"path": "<Gamepad>/rightStick/down",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraPanning",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "16a19e86-eb75-40ef-a937-cc69f5c57971",
"path": "<Gamepad>/rightStick/left",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraPanning",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "338131ba-47f8-4fc8-b137-39be986200ed",
"path": "<Gamepad>/rightStick/right",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraPanning",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "2553a5ac-0892-4d77-a408-8b5fced329a8",
"path": "<Mouse>/scroll/y",
"interactions": "",
"processors": "Scale(factor=0.1)",
"groups": "",
"action": "CameraZoom",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "Gamepad",
"id": "aeaabcc3-6825-4a24-b1b3-13b3a70fff59",
"path": "1DAxis(whichSideWins=1)",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraZoom",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "negative",
"id": "d55890ea-00d2-483e-9b0a-e2ba85f4b2dd",
"path": "<Gamepad>/dpad/down",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraZoom",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "positive",
"id": "3ce859ed-6297-4bab-b40b-d6436bacd5ab",
"path": "<Gamepad>/dpad/up",
"interactions": "",
"processors": "",
"groups": "",
"action": "CameraZoom",
"isComposite": false,
"isPartOfComposite": true
}
]
},
{
"name": "SceneControls",
"id": "abb87e97-bffa-439c-a42d-7b1a9497c4cc",
"actions": [
{
"name": "ChangeVehicle",
"type": "Button",
"id": "a6ddd2a4-de73-4949-8b79-fef6d4b4bc3f",
"expectedControlType": "",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "FPSMovement",
"type": "Value",
"id": "347a1c7d-d6ca-4838-9d67-ca3bece4074f",
"expectedControlType": "Vector2",
"processors": "",
"interactions": "",
"initialStateCheck": true
},
{
"name": "ToggleGUI",
"type": "Button",
"id": "420fdb48-6cea-444b-8cd6-256097129d3b",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "DragObjectModifier",
"type": "Button",
"id": "1fd9ef37-8fcf-43c4-9b96-ed432f843af4",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
},
{
"name": "ShowCursor",
"type": "Button",
"id": "4566d436-6301-4d31-bd9b-984b19b6cc9b",
"expectedControlType": "Button",
"processors": "",
"interactions": "",
"initialStateCheck": false
}
],
"bindings": [
{
"name": "",
"id": "02e5b759-a74a-41e1-af72-80c6990f0d95",
"path": "<Keyboard>/v",
"interactions": "",
"processors": "",
"groups": "",
"action": "ChangeVehicle",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "01597fdb-29e0-4e77-a920-ba59240fe6d6",
"path": "<Gamepad>/select",
"interactions": "",
"processors": "",
"groups": "",
"action": "ChangeVehicle",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "Keyboard",
"id": "59431748-63e9-4210-8dd9-590e23bcdf0c",
"path": "2DVector(mode=1)",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "e0b8875d-06d4-467d-b8f0-61da2e804895",
"path": "<Keyboard>/w",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "87e4ea7c-c07f-491e-8dc6-36f79dbf9805",
"path": "<Keyboard>/s",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "9bba77a1-921c-493d-b881-6f14f1eb377b",
"path": "<Keyboard>/a",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "cfa4cf6d-3fe9-4930-847f-b59a8277a8fc",
"path": "<Keyboard>/d",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "Gamepad",
"id": "208efade-fdb9-49b9-a679-eb44b6ed6ac2",
"path": "2DVector(mode=2)",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": true,
"isPartOfComposite": false
},
{
"name": "up",
"id": "8087e701-d9e1-454b-8b70-50813a31516b",
"path": "<Gamepad>/leftStick/up",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "down",
"id": "0b594ea6-b48c-4805-9cea-77058ade6d6a",
"path": "<Gamepad>/leftStick/down",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "left",
"id": "e7548400-0520-4980-aded-b6d0ac753e4a",
"path": "<Gamepad>/leftStick/left",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "right",
"id": "b46be990-1176-4948-8642-dddc1bf5ee6c",
"path": "<Gamepad>/leftStick/right",
"interactions": "",
"processors": "",
"groups": "",
"action": "FPSMovement",
"isComposite": false,
"isPartOfComposite": true
},
{
"name": "",
"id": "9f9f8d86-cd0b-4953-8490-e72ab4b7d8f0",
"path": "<Keyboard>/tab",
"interactions": "",
"processors": "",
"groups": "",
"action": "ToggleGUI",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "2d06a9ed-570c-45df-ae1b-aec7652096fd",
"path": "<Mouse>/middleButton",
"interactions": "",
"processors": "",
"groups": "",
"action": "DragObjectModifier",
"isComposite": false,
"isPartOfComposite": false
},
{
"name": "",
"id": "2685a4d7-beae-479c-a63b-f7cd494f9c8a",
"path": "<Keyboard>/leftCtrl",
"interactions": "",
"processors": "",
"groups": "",
"action": "ShowCursor",
"isComposite": false,
"isPartOfComposite": false
}
]
}
],
"controlSchemes": []
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: e6287ac585a812c479a27c6d0be9f915
ScriptedImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 2
userData:
assetBundleName:
assetBundleVariant:
script: {fileID: 11500000, guid: 8404be70184654265930450def6a9037, type: 3}
generateWrapperCode: 1
wrapperCodePath:
wrapperClassName:
wrapperCodeNamespace: NWH.Common.Input

View File

@@ -0,0 +1,134 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Input
{
/// <summary>
/// Utility methods for safe input retrieval with automatic fallback to default keys.
/// Prevents errors when Input Manager bindings are missing.
/// </summary>
public class InputUtils
{
private static int _warningCount;
/// <summary>
/// Attempts to retrieve button state from Input Manager, falls back to KeyCode if binding is missing.
/// </summary>
/// <param name="buttonName">Input Manager button name to query.</param>
/// <param name="altKey">Fallback KeyCode to use if binding is missing.</param>
/// <param name="showWarning">Display warning message when falling back to default key.</param>
/// <returns>True if button is currently held down.</returns>
public static bool TryGetButton(string buttonName, KeyCode altKey, bool showWarning = true)
{
try
{
return UnityEngine.Input.GetButton(buttonName);
}
catch
{
// Make sure warning is not spammed as some users tend to ignore the warning and never set up the input,
// resulting in bad performance in editor.
if (_warningCount < 100 && showWarning)
{
Debug.LogWarning(buttonName +
" input binding missing, falling back to default. Check Input section in manual for more info.");
_warningCount++;
}
return UnityEngine.Input.GetKey(altKey);
}
}
/// <summary>
/// Attempts to retrieve button press from Input Manager, falls back to KeyCode if binding is missing.
/// </summary>
/// <param name="buttonName">Input Manager button name to query.</param>
/// <param name="altKey">Fallback KeyCode to use if binding is missing.</param>
/// <param name="showWarning">Display warning message when falling back to default key.</param>
/// <returns>True on the frame the button was pressed.</returns>
public static bool TryGetButtonDown(string buttonName, KeyCode altKey, bool showWarning = true)
{
try
{
return UnityEngine.Input.GetButtonDown(buttonName);
}
catch
{
if (_warningCount < 100 && showWarning)
{
Debug.LogWarning(buttonName +
" input binding missing, falling back to default. Check Input section in manual for more info.");
_warningCount++;
}
return UnityEngine.Input.GetKeyDown(altKey);
}
}
/// <summary>
/// Attempts to retrieve axis value from Input Manager, returns 0 if binding is missing.
/// </summary>
/// <param name="axisName">Input Manager axis name to query.</param>
/// <param name="showWarning">Display warning message when axis is missing.</param>
/// <returns>Axis value between -1 and 1, or 0 if binding is missing.</returns>
public static float TryGetAxis(string axisName, bool showWarning = true)
{
try
{
return UnityEngine.Input.GetAxis(axisName);
}
catch
{
if (_warningCount < 100 && showWarning)
{
Debug.LogWarning(axisName +
" input binding missing. Check Input section in manual for more info.");
_warningCount++;
}
}
return 0;
}
/// <summary>
/// Attempts to retrieve raw axis value from Input Manager, returns 0 if binding is missing.
/// Raw axes return only -1, 0, or 1 without smoothing.
/// </summary>
/// <param name="axisName">Input Manager axis name to query.</param>
/// <param name="showWarning">Display warning message when axis is missing.</param>
/// <returns>Raw axis value (-1, 0, or 1), or 0 if binding is missing.</returns>
public static float TryGetAxisRaw(string axisName, bool showWarning = true)
{
try
{
return UnityEngine.Input.GetAxisRaw(axisName);
}
catch
{
if (_warningCount < 100 && showWarning)
{
Debug.LogWarning(axisName +
" input binding missing. Check Input section in manual for more info.");
_warningCount++;
}
}
return 0;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e808b630f95a45838f09520152a38a93
timeCreated: 1593368933

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: deaa0d3d555c4273bde9ce8e169f76b6
timeCreated: 1593369326

View File

@@ -0,0 +1,51 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.EventSystems;
using UnityEngine.UI;
#endregion
namespace NWH.Common.Input
{
/// <summary>
/// Extended Unity UI Button with state tracking for mobile input handling.
/// Provides hasBeenClicked and isPressed flags for easier input polling.
/// </summary>
[DefaultExecutionOrder(1000)]
public class MobileInputButton : Button
{
/// <summary>
/// True for one frame after the button is clicked. Automatically resets to false.
/// </summary>
public bool hasBeenClicked;
/// <summary>
/// True while the button is being held down. Updates every frame.
/// </summary>
public bool isPressed;
private void Update()
{
isPressed = IsPressed();
hasBeenClicked = false;
}
public override void OnPointerDown(PointerEventData eventData)
{
base.OnPointerDown(eventData);
hasBeenClicked = true;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d2117e63fb06445ababc499401d5c062
timeCreated: 1593369326

View File

@@ -0,0 +1,126 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.Common.Input
{
/// <summary>
/// Scene input provider for mobile platforms using on-screen UI buttons.
/// Requires MobileInputButton components assigned to changeCameraButton and changeVehicleButton fields.
/// </summary>
public class MobileSceneInputProvider : SceneInputProviderBase
{
/// <summary>
/// UI button for changing camera. Should reference a MobileInputButton in the scene.
/// </summary>
public MobileInputButton changeCameraButton;
/// <summary>
/// UI button for changing vehicle. Should reference a MobileInputButton in the scene.
/// </summary>
public MobileInputButton changeVehicleButton;
public override bool ChangeCamera()
{
return changeCameraButton != null && changeCameraButton.hasBeenClicked;
}
public override bool ChangeVehicle()
{
return changeVehicleButton != null && changeVehicleButton.hasBeenClicked;
}
public override Vector2 CharacterMovement()
{
return Vector2.zero;
}
public override bool ToggleGUI()
{
return false;
}
public override Vector2 CameraRotation()
{
return Vector2.zero;
}
public override Vector2 CameraPanning()
{
return Vector2.zero;
}
public override bool CameraRotationModifier()
{
return false;
}
public override bool CameraPanningModifier()
{
return false;
}
public override float CameraZoom()
{
return 0;
}
}
}
#if UNITY_EDITOR
namespace NWH.Common.Input
{
/// <summary>
/// Editor for MobileInputProvider.
/// </summary>
[CustomEditor(typeof(MobileSceneInputProvider))]
public class MobileSceneInputProviderEditor : NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.BeginSubsection("Scene Buttons");
drawer.Field("changeVehicleButton");
drawer.Field("changeCameraButton");
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 907294c7523f458dba9b3b3e8955d681
timeCreated: 1593369326

View File

@@ -0,0 +1,117 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Input
{
/// <summary>
/// InputProvider for scene and camera related behavior.
/// </summary>
public abstract class SceneInputProviderBase : InputProvider
{
/// <summary>
/// If true a button press will be required to unlock camera panning.
/// </summary>
[Tooltip(" If true a button press will be required to unlock camera panning.")]
public bool requireCameraPanningModifier = true;
/// <summary>
/// If true a button press will be required to unlock camera rotation.
/// </summary>
[Tooltip(" If true a button press will be required to unlock camera rotation.")]
public bool requireCameraRotationModifier = true;
/// <summary>
/// Returns true when the change camera button is pressed.
/// </summary>
public virtual bool ChangeCamera()
{
return false;
}
/// <summary>
/// Returns camera rotation input as a Vector2 (x = horizontal, y = vertical).
/// </summary>
public virtual Vector2 CameraRotation()
{
return Vector2.zero;
}
/// <summary>
/// Returns camera panning input as a Vector2 (x = horizontal, y = vertical).
/// </summary>
public virtual Vector2 CameraPanning()
{
return Vector2.zero;
}
/// <summary>
/// Returns true when the camera rotation modifier button is held.
/// If requireCameraRotationModifier is false, always returns true.
/// </summary>
public virtual bool CameraRotationModifier()
{
return !requireCameraRotationModifier;
}
/// <summary>
/// Returns true when the camera panning modifier button is held.
/// If requireCameraPanningModifier is false, always returns true.
/// </summary>
public virtual bool CameraPanningModifier()
{
return !requireCameraPanningModifier;
}
/// <summary>
/// Returns camera zoom input value. Positive = zoom in, negative = zoom out.
/// </summary>
public virtual float CameraZoom()
{
return 0;
}
/// <summary>
/// Returns true when the change vehicle button is pressed.
/// </summary>
public virtual bool ChangeVehicle()
{
return false;
}
/// <summary>
/// Returns character movement input as a Vector2 (x = horizontal, y = forward/back).
/// </summary>
public virtual Vector2 CharacterMovement()
{
return Vector2.zero;
}
/// <summary>
/// Returns true when the toggle GUI button is pressed.
/// </summary>
public virtual bool ToggleGUI()
{
return false;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9acc2544c2f24c428877837814870a58
timeCreated: 1593334735

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e4b83cddccfe6934f9df3ce424f9519b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 553b71a9f03c40a4d8551653823451e4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a804ce60c07e08648b7961d49d7c2999
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 57bf16eb97e2c164f997df4cf6c27e38
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 B

View File

@@ -0,0 +1,103 @@
fileFormatVersion: 2
guid: 3fec6694e308c524bb292f84e8eafdc2
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 B

View File

@@ -0,0 +1,103 @@
fileFormatVersion: 2
guid: 87a3220b3c062c0449c545ddc8473dc8
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,103 @@
fileFormatVersion: 2
guid: 38e8124ac66cf414bbe9ba84c4628ae3
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 10
mipmaps:
mipMapMode: 0
enableMipMap: 1
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: -1
aniso: -1
mipBias: -100
wrapU: -1
wrapV: -1
wrapW: -1
nPOTScale: 1
lightmap: 0
compressionQuality: 50
spriteMode: 0
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 0
textureShape: 1
singleChannelComponent: 0
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID:
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
spritePackingTag:
pSDRemoveMatte: 0
pSDShowRemoveMatteOption: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,301 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
#endregion
namespace NWH.NUI
{
public static class EditorCache
{
// Workaround to get around the issue of .NET 2.0, asmdef and dynamic.
private static readonly Dictionary<string, float> heightCache = new();
private static readonly Dictionary<string, ReorderableList> reorderableListCache = new();
private static readonly Dictionary<string, bool> guiWasEnabledCache = new();
private static readonly Dictionary<string, NUIEditor> nuiEditorCache = new();
private static readonly Dictionary<string, bool> isExpandedCache = new();
private static readonly Dictionary<string, int> tabIndexCache = new();
private static readonly Dictionary<string, Texture2D> texture2DCache = new();
private static readonly Dictionary<string, SerializedProperty> serializedPropertyCache = new();
// Auto-clear cache on assembly reload to prevent unbounded growth
[UnityEditor.Callbacks.DidReloadScripts]
private static void OnScriptsReloaded()
{
ClearAll();
}
public static bool GetHeightCacheValue(string key, ref float value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return heightCache.TryGetValue(key, out value);
}
public static bool GetReorderableListCacheValue(string key, ref ReorderableList value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return reorderableListCache.TryGetValue(key, out value);
}
public static bool GetGuiWasEnabledCValue(string key, ref bool value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return guiWasEnabledCache.TryGetValue(key, out value);
}
public static bool GetNUIEditorCacheValue(string key, ref NUIEditor value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return nuiEditorCache.TryGetValue(key, out value);
}
public static bool GetIsExpandedCacheValue(string key, ref bool value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return isExpandedCache.TryGetValue(key, out value);
}
public static bool GetTabIndexCacheValue(string key, ref int value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return tabIndexCache.TryGetValue(key, out value);
}
public static bool GetTexture2DCacheValue(string key, ref Texture2D value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return texture2DCache.TryGetValue(key, out value);
}
public static bool GetSerializedPropertyCacheValue(string key, ref SerializedProperty value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
return serializedPropertyCache.TryGetValue(key, out value);
}
public static bool SetHeightCacheValue(string key, float value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
heightCache[key] = value;
return true;
}
public static bool SetReorderableListCacheValue(string key, ReorderableList value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
reorderableListCache[key] = value;
return true;
}
public static bool SetGuiWasEnabledCacheValue(string key, bool value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
guiWasEnabledCache[key] = value;
return true;
}
public static bool SetNUIEditorCacheValue(string key, NUIEditor value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
nuiEditorCache[key] = value;
return true;
}
public static bool SetIsExpandedCacheValue(string key, bool value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
isExpandedCache[key] = value;
return true;
}
public static bool SetTabIndexCacheValue(string key, int value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
tabIndexCache[key] = value;
return true;
}
public static bool SetTexture2DCacheValue(string key, Texture2D value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
texture2DCache[key] = value;
return true;
}
public static bool SetSerializedPropertyCacheValue(string key, SerializedProperty value)
{
if (string.IsNullOrEmpty(key))
{
return false;
}
serializedPropertyCache[key] = value;
return true;
}
/// <summary>
/// Clears all cached data. Useful to prevent unbounded memory growth during long editor sessions.
/// </summary>
public static void ClearAll()
{
heightCache.Clear();
reorderableListCache.Clear();
guiWasEnabledCache.Clear();
nuiEditorCache.Clear();
isExpandedCache.Clear();
tabIndexCache.Clear();
texture2DCache.Clear();
serializedPropertyCache.Clear();
}
// // Store data for each property as property drawer gets reused multiple times and local values overwritten
// private static readonly Dictionary<string, dynamic> Cache = new Dictionary<string, dynamic>
// {
// {"height", new Dictionary<string, float>()},
// {"ReorderableList", new Dictionary<string, ReorderableList>()},
// {"guiWasEnabled", new Dictionary<string, bool>()},
// {"NUIEditor", new Dictionary<string, NUIEditor>()},
// {"isExpanded", new Dictionary<string, bool>()},
// {"tabIndex", new Dictionary<string, int>()},
// {"Texture2D", new Dictionary<string, Texture2D>()},
// {"SerializedProperty", new Dictionary<string, SerializedProperty>()},
// };
// public static bool GetCachedValue<T>(string variableName, ref T value, string key)
// {
// if (string.IsNullOrEmpty(key))
// {
// return false;
// }
//
// if (!Cache.ContainsKey(variableName) || !Cache[variableName].ContainsKey(key))
// {
// return false;
// }
//
// value = Cache[variableName][key];
// return true;
// }
//
//
// public static bool SetCachedValue<T>(string variableName, T value, string key)
// {
// if (string.IsNullOrEmpty(key))
// {
// return false;
// }
//
// if (Cache.ContainsKey(variableName))
// {
// if (!Cache[variableName].ContainsKey(key))
// {
// Cache[variableName].Add(key, value);
// }
// else
// {
// Cache[variableName][key] = value;
// }
//
// return true;
// }
//
// return false;
// }
}
}
#endif

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,87 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using UnityEditor;
#endregion
namespace NWH.NUI
{
/// <summary>
/// Base custom editor class for NWH NUI (NWH User Interface) system.
/// Provides common infrastructure for drawing foldable inspector sections with documentation links.
/// </summary>
[CanEditMultipleObjects]
public class NUIEditor : Editor
{
/// <summary>
/// NUIDrawer instance used to render inspector GUI.
/// </summary>
public NUIDrawer drawer = new();
/// <summary>
/// Unity callback to draw inspector GUI. Delegates to OnInspectorNUI.
/// </summary>
public override void OnInspectorGUI()
{
OnInspectorNUI();
}
/// <summary>
/// Draws custom NUI inspector. Override this method in derived classes to add custom GUI.
/// Initializes drawer and renders collapsible header.
/// </summary>
/// <returns>True if header is expanded and GUI should continue, false if collapsed</returns>
public virtual bool OnInspectorNUI()
{
if (drawer == null)
{
drawer = new NUIDrawer();
}
drawer.documentationBaseURL = GetDocumentationBaseURL();
drawer.BeginEditor(serializedObject);
if (!drawer.Header(serializedObject.targetObject.GetType().Name))
{
drawer.EndEditor();
return false;
}
return true;
}
/// <summary>
/// Gets base URL for documentation links. Override in derived classes to specify package-specific docs.
/// </summary>
/// <returns>Base documentation URL</returns>
public virtual string GetDocumentationBaseURL()
{
return "http://nwhvehiclephysics.com";
}
/// <summary>
/// Disables default Unity inspector margins for custom NUI layout control.
/// </summary>
/// <returns>Always false to disable default margins</returns>
public override bool UseDefaultMargins()
{
return false;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,73 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using UnityEditor;
using UnityEngine;
#endregion
namespace NWH.NUI
{
/// <summary>
/// Custom NWH.NUI property drawer with links to documentation.
/// </summary>
public class NUIPropertyDrawer : PropertyDrawer
{
protected NUIDrawer drawer = new();
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return drawer.GetHeight(NUIDrawer.GenerateKey(property));
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
OnNUI(position, property, label);
}
public virtual string GetDocumentationBaseURL()
{
return "http://nwhvehiclephysics.com";
}
public virtual bool OnNUI(Rect position, SerializedProperty property, GUIContent label)
{
if (drawer == null)
{
drawer = new NUIDrawer();
}
drawer.documentationBaseURL = GetDocumentationBaseURL();
drawer.BeginProperty(position, property, label);
string name = property.FindPropertyRelative("name")?.stringValue;
if (string.IsNullOrEmpty(name))
{
name = property.displayName;
}
if (!drawer.Header(name))
{
drawer.EndProperty();
return false;
}
return true;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,78 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.NUI
{
/// <summary>
/// Global configuration settings for NWH NUI (NWH User Interface) editor system.
/// Defines layout constants and color scheme used across all NWH custom editors.
/// </summary>
public static class NUISettings
{
/// <summary>
/// Standard height for inspector fields in pixels.
/// </summary>
public const float fieldHeight = 23f;
/// <summary>
/// Vertical spacing between inspector fields in pixels.
/// </summary>
public const float fieldSpacing = 3f;
/// <summary>
/// Resources folder path for NUI assets.
/// </summary>
public const string resourcesPath = "NUI/";
/// <summary>
/// Margin around text elements in pixels.
/// </summary>
public const float textMargin = 2f;
/// <summary>
/// Header background color for ScriptableObject editors.
/// </summary>
public static Color scriptableObjectHeaderColor = new Color32(220, 122, 32, 255);
/// <summary>
/// Header background color for MonoBehaviour editors.
/// </summary>
public static Color editorHeaderColor = new Color32(20, 125, 211, 255);
/// <summary>
/// Header background color for property drawers.
/// </summary>
public static Color propertyHeaderColor = new Color32(78, 152, 213, 255);
/// <summary>
/// UI tint color indicating disabled state.
/// </summary>
public static Color disabledColor = new(1f, 0.5f, 0.5f);
/// <summary>
/// UI tint color indicating enabled state.
/// </summary>
public static Color enabledColor = new(0.5f, 1f, 0.5f);
/// <summary>
/// Light blue accent color for UI elements.
/// </summary>
public static Color lightBlueColor = new Color32(70, 170, 220, 255);
/// <summary>
/// Light grey color for secondary UI elements.
/// </summary>
public static Color lightGreyColor = new Color32(192, 192, 192, 255);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4569c8453c17406d83a470d6e849d57f
timeCreated: 1581892208

View File

@@ -0,0 +1,145 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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. ║
// ╚════════════════════════════════════════════════════════════════╝
#if UNITY_EDITOR
#region
using System;
using System.Collections;
using System.Linq;
using System.Reflection;
using UnityEditor;
#endregion
namespace NWH.NUI
{
public static class SerializedPropertyHelper
{
/// <summary>
/// Gets the object the property represents.
/// </summary>
/// <param name="prop"> </param>
/// <returns> </returns>
public static object GetTargetObjectOfProperty(SerializedProperty prop)
{
if (prop == null)
{
return null;
}
string path = prop.propertyPath.Replace(".Array.data[", "[");
object obj = prop.serializedObject.targetObject;
string[] elements = path.Split('.');
foreach (string element in elements)
{
if (element.Contains("["))
{
string elementName = element.Substring(0, element.IndexOf("["));
int index = Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "")
.Replace("]", ""));
obj = GetValue_Imp(obj, elementName, index);
}
else
{
obj = GetValue_Imp(obj, element);
}
}
return obj;
}
/// <summary>
/// Gets the object that the property is a member of
/// </summary>
/// <param name="prop"> </param>
/// <returns> </returns>
public static object GetTargetObjectWithProperty(SerializedProperty prop)
{
string path = prop.propertyPath.Replace(".Array.data[", "[");
object obj = prop.serializedObject.targetObject;
string[] elements = path.Split('.');
foreach (string element in elements.Take(elements.Length - 1))
{
if (element.Contains("["))
{
string elementName = element.Substring(0, element.IndexOf("["));
int index = Convert.ToInt32(element.Substring(element.IndexOf("[")).Replace("[", "")
.Replace("]", ""));
obj = GetValue_Imp(obj, elementName, index);
}
else
{
obj = GetValue_Imp(obj, element);
}
}
return obj;
}
private static object GetValue_Imp(object source, string name)
{
if (source == null)
{
return null;
}
Type type = source.GetType();
while (type != null)
{
FieldInfo f = type.GetField(name, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if (f != null)
{
return f.GetValue(source);
}
PropertyInfo p = type.GetProperty(name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance |
BindingFlags.IgnoreCase);
if (p != null)
{
return p.GetValue(source, null);
}
type = type.BaseType;
}
return null;
}
private static object GetValue_Imp(object source, string name, int index)
{
IEnumerable enumerable = GetValue_Imp(source, name) as IEnumerable;
if (enumerable == null)
{
return null;
}
IEnumerator enm = enumerable.GetEnumerator();
//while (index-- >= 0)
// enm.MoveNext();
//return enm.Current;
for (int i = 0; i <= index; i++)
{
if (!enm.MoveNext())
{
return null;
}
}
return enm.Current;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,16 @@
{
"name": "NWH.Common",
"rootNamespace": "",
"references": [
"GUID:75469ad4d38634e559750d17036d5f7c"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: c76e28da8ce572043b1fb2da95817e18
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d7451ff9348d187449ec4a6dc9f72de3
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,106 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Utility
{
/// <summary>
/// Extension methods for AnimationCurve manipulation and processing.
/// </summary>
public static class AnimationCurveExtensions
{
/// <summary>
/// Smooths out a scripting-generated AnimationCurve by calculating appropriate tangents.
/// Creates smooth transitions between keyframes.
/// </summary>
/// <param name="inCurve">The curve to smooth.</param>
/// <returns>A new smoothed AnimationCurve.</returns>
public static AnimationCurve MakeSmooth(this AnimationCurve inCurve)
{
AnimationCurve outCurve = new();
for (int i = 0; i < inCurve.keys.Length; i++)
{
float inTangent = 0;
float outTangent = 0;
bool intangentSet = false;
bool outtangentSet = false;
Vector2 point1;
Vector2 point2;
Vector2 deltapoint;
Keyframe key = inCurve[i];
if (i == 0)
{
inTangent = 0;
intangentSet = true;
}
if (i == inCurve.keys.Length - 1)
{
outTangent = 0;
outtangentSet = true;
}
if (!intangentSet)
{
point1.x = inCurve.keys[i - 1].time;
point1.y = inCurve.keys[i - 1].value;
point2.x = inCurve.keys[i].time;
point2.y = inCurve.keys[i].value;
deltapoint = point2 - point1;
inTangent = deltapoint.y / deltapoint.x;
}
if (!outtangentSet)
{
point1.x = inCurve.keys[i].time;
point1.y = inCurve.keys[i].value;
point2.x = inCurve.keys[i + 1].time;
point2.y = inCurve.keys[i + 1].value;
deltapoint = point2 - point1;
outTangent = deltapoint.y / deltapoint.x;
}
key.inTangent = inTangent;
key.outTangent = outTangent;
outCurve.AddKey(key);
}
return outCurve;
}
/// <summary>
/// Samples an AnimationCurve at regular intervals and returns the values as an array.
/// Useful for pre-calculating curve values for performance-critical code.
/// </summary>
/// <param name="self">The curve to sample.</param>
/// <param name="resolution">Number of samples to take. Higher values provide more precision.</param>
/// <returns>Array of sampled values from 0 to 1.</returns>
public static float[] GenerateCurveArray(this AnimationCurve self, int resolution = 256)
{
float[] returnArray = new float[resolution];
for (int j = 0; j < resolution; j++)
{
returnArray[j] = self.Evaluate(j / (float)resolution);
}
return returnArray;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 34b51e6bc0d3493cbab61632bb388338
timeCreated: 1607279175

View File

@@ -0,0 +1,54 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.Utility
{
/// <summary>
/// Extension methods for array manipulation.
/// </summary>
public static class ArrayExtensions
{
/// <summary>
/// Efficiently fills an array by repeating a pattern of values.
/// Uses doubling strategy for performance.
/// </summary>
/// <typeparam name="T">Type of array elements.</typeparam>
/// <param name="destinationArray">Array to fill.</param>
/// <param name="value">Pattern of values to repeat throughout the array.</param>
public static void Fill<T>(this T[] destinationArray, params T[] value)
{
int destinationLength = destinationArray.Length;
if (destinationLength == 0)
{
return;
}
int valueLength = value.Length;
// set the initial array value
Array.Copy(value, destinationArray, valueLength);
int arrayToFillHalfLength = destinationLength / 2;
int copyLength;
for (copyLength = valueLength; copyLength < arrayToFillHalfLength; copyLength <<= 1)
{
Array.Copy(destinationArray, 0, destinationArray, copyLength, copyLength);
}
Array.Copy(destinationArray, 0, destinationArray, copyLength,
destinationLength - copyLength);
}
}
}

View File

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

View File

@@ -0,0 +1,86 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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;
#endregion
namespace NWH.Common.Utility
{
/// <summary>
/// Extension methods for GameObject and Transform operations.
/// </summary>
public static class GameObjectExtensions
{
/// <summary>
/// Calculates the combined bounds of all MeshRenderers in a GameObject and its children.
/// </summary>
/// <param name="gameObject">GameObject to calculate bounds for.</param>
/// <returns>Combined bounds encapsulating all child renderers.</returns>
public static Bounds FindBoundsIncludeChildren(this GameObject gameObject)
{
Bounds bounds = new();
foreach (MeshRenderer mr in gameObject.GetComponentsInChildren<MeshRenderer>())
{
bounds.Encapsulate(mr.bounds);
}
return bounds;
}
/// <summary>
/// Searches for a component in parent GameObjects, with option to include inactive objects.
/// More flexible than Unity's built-in GetComponentInParent.
/// </summary>
/// <typeparam name="T">Type of component to find.</typeparam>
/// <param name="transform">Starting transform.</param>
/// <param name="includeInactive">Include inactive GameObjects in search.</param>
/// <returns>First component of type T found in parents, or null if none found.</returns>
public static T GetComponentInParent<T>(this Transform transform, bool includeInactive = true)
where T : Component
{
Transform here = transform;
T result = null;
while (here && !result)
{
if (includeInactive || here.gameObject.activeSelf)
{
result = here.GetComponent<T>();
}
here = here.parent;
}
return result;
}
/// <summary>
/// Searches for a component in parents first, then children if not found.
/// Combines functionality of GetComponentInParent and GetComponentInChildren.
/// </summary>
/// <typeparam name="T">Type of component to find.</typeparam>
/// <param name="transform">Starting transform.</param>
/// <param name="includeInactive">Include inactive GameObjects in search.</param>
/// <returns>First component of type T found, or null if none found.</returns>
public static T GetComponentInParentsOrChildren<T>(this Transform transform, bool includeInactive = true)
where T : Component
{
T result = transform.GetComponentInParent<T>(includeInactive);
if (result == null)
{
result = transform.GetComponentInChildren<T>(includeInactive);
}
return result;
}
}
}

View File

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

View File

@@ -0,0 +1,771 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 Unity.Collections.LowLevel.Unsafe;
using UnityEngine;
#endregion
namespace NWH.Common.Utility
{
/// <summary>
/// Collection of geometric utility functions for 3D math operations.
/// Includes vector math, mesh calculations, triangle operations, and spatial queries.
/// </summary>
public static class GeomUtility
{
/// <summary>
/// Checks if two Vector3 values are approximately equal within a threshold.
/// Uses squared magnitude for performance.
/// </summary>
/// <param name="a">First vector.</param>
/// <param name="b">Second vector.</param>
/// <param name="threshold">Maximum squared distance to consider equal.</param>
/// <returns>True if vectors are within threshold distance.</returns>
public static bool NearEqual(this Vector3 a, Vector3 b, float threshold = 0.01f)
{
return Vector3.SqrMagnitude(a - b) < threshold;
}
/// <summary>
/// Checks if two Quaternion values are approximately equal.
/// </summary>
/// <param name="a">First quaternion.</param>
/// <param name="b">Second quaternion.</param>
/// <returns>True if angle between quaternions is less than 0.1 degrees.</returns>
public static bool Equal(this Quaternion a, Quaternion b)
{
return Mathf.Abs(Quaternion.Angle(a, b)) < 0.1f;
}
/// <summary>
/// Clamps the magnitude of a vector between minimum and maximum values.
/// </summary>
/// <param name="v">Vector to clamp.</param>
/// <param name="min">Minimum magnitude.</param>
/// <param name="max">Maximum magnitude.</param>
/// <returns>Vector with clamped magnitude.</returns>
public static Vector3 ClampMagnitude(this Vector3 v, float min, float max)
{
float mag = v.magnitude;
if (mag == 0)
{
return Vector3.zero;
}
return Mathf.Clamp(mag, min, max) / mag * v;
}
/// <summary>
/// Calculates a perpendicular vector to the given vector.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Perpendicular vector.</returns>
public static Vector3 Perpendicular(this Vector3 v)
{
return new Vector3(CopySign(v.z, v.x), CopySign(v.z, v.y), -CopySign(Mathf.Abs(v.x) + Mathf.Abs(v.y), v.z));
}
/// <summary>
/// Copies the sign from one float to the magnitude of another.
/// </summary>
/// <param name="mag">Magnitude value.</param>
/// <param name="sgn">Sign donor value.</param>
/// <returns>Magnitude with the sign of sgn.</returns>
public static float CopySign(float mag, float sgn)
{
ref uint magI = ref UnsafeUtility.As<float, uint>(ref mag);
ref uint sgnI = ref UnsafeUtility.As<float, uint>(ref sgn);
uint result = (magI & ~(1u << 31)) | (sgnI & (1u << 31));
return UnsafeUtility.As<uint, float>(ref result);
}
/// <summary>
/// Returns a vector with only the largest component preserved (rounded to 1 or -1), others set to 0.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Vector with dominant axis isolated.</returns>
public static Vector3 RoundedMax(this Vector3 v)
{
int maxIndex = -1;
float maxValue = -Mathf.Infinity;
for (int i = 0; i < 3; i++)
{
float value = Mathf.Abs(v[i]);
if (value > maxValue)
{
maxValue = value;
maxIndex = i;
}
}
for (int i = 0; i < 3; i++)
{
v[i] = i == maxIndex ? Mathf.Sign(v[i]) * 1f : 0f;
}
return v;
}
/// <summary>
/// Finds the nearest point on an infinite line to a given point.
/// </summary>
/// <param name="linePnt">Point on the line.</param>
/// <param name="lineDir">Direction of the line.</param>
/// <param name="pnt">Point to find nearest point from.</param>
/// <returns>Nearest point on the line.</returns>
public static Vector3 NearestPointOnLine(Vector3 linePnt, Vector3 lineDir, Vector3 pnt)
{
lineDir.Normalize(); //this needs to be a unit vector
Vector3 v = pnt - linePnt;
float d = Vector3.Dot(v, lineDir);
return linePnt + lineDir * d;
}
/// <summary>
/// Calculates the distance between a point and a line segment.
/// </summary>
/// <param name="pt">Point to measure from.</param>
/// <param name="p1">First endpoint of segment.</param>
/// <param name="p2">Second endpoint of segment.</param>
/// <returns>Distance to the segment.</returns>
public static float FindDistanceToSegment(Vector3 pt, Vector3 p1, Vector3 p2)
{
float dx = p2.x - p1.x;
float dy = p2.y - p1.y;
if (dx == 0 && dy == 0)
{
// It's a point not a line segment.
dx = pt.x - p1.x;
dy = pt.y - p1.y;
return Mathf.Sqrt(dx * dx + dy * dy);
}
// Calculate the t that minimizes the distance.
float t = ((pt.x - p1.x) * dx + (pt.y - p1.y) * dy) /
(dx * dx + dy * dy);
// See if this represents one of the segment's
// end points or a point in the middle.
if (t < 0)
{
dx = pt.x - p1.x;
dy = pt.y - p1.y;
}
else if (t > 1)
{
dx = pt.x - p2.x;
dy = pt.y - p2.y;
}
else
{
Vector3 closest = new(p1.x + t * dx, p1.y + t * dy);
dx = pt.x - closest.x;
dy = pt.y - closest.y;
}
return Mathf.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// Calculates squared distance between two points. Faster than regular distance.
/// </summary>
/// <param name="a">First point.</param>
/// <param name="b">Second point.</param>
/// <returns>Squared distance.</returns>
public static float SquareDistance(Vector3 a, Vector3 b)
{
float x = a.x - b.x;
float y = a.y - b.y;
float z = a.z - b.z;
return x * x + y * y + z * z;
}
/// <summary>
/// Finds the intersection point between a line and a plane.
/// </summary>
/// <param name="planePoint">Point on the plane.</param>
/// <param name="planeNormal">Normal vector of the plane.</param>
/// <param name="linePoint">Point on the line.</param>
/// <param name="lineDirection">Direction of the line.</param>
/// <returns>Intersection point, or Vector3.zero if parallel.</returns>
public static Vector3 LinePlaneIntersection(Vector3 planePoint, Vector3 planeNormal, Vector3 linePoint,
Vector3 lineDirection)
{
if (Vector3.Dot(planeNormal, lineDirection.normalized) == 0)
{
return Vector3.zero;
}
float t = (Vector3.Dot(planeNormal, planePoint) - Vector3.Dot(planeNormal, linePoint)) /
Vector3.Dot(planeNormal, lineDirection.normalized);
return linePoint + lineDirection.normalized * t;
}
/// <summary>
/// Finds a point along the chord line of a quad at the specified percentage.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner.</param>
/// <param name="chordPercent">Position along chord (0-1).</param>
/// <returns>Point on chord line.</returns>
public static Vector3 FindChordLine(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float chordPercent)
{
return QuadLerp(a, b, c, d, 0.5f, chordPercent);
}
/// <summary>
/// Finds a point along the span line of a quad at the specified percentage.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner.</param>
/// <param name="spanPercent">Position along span (0-1).</param>
/// <returns>Point on span line.</returns>
public static Vector3 FindSpanLine(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float spanPercent)
{
return QuadLerp(a, b, c, d, spanPercent, 0.5f);
}
/// <summary>
/// Calculates the area of a quadrilateral defined by four points.
/// </summary>
/// <param name="A">First corner.</param>
/// <param name="B">Second corner.</param>
/// <param name="C">Third corner.</param>
/// <param name="D">Fourth corner.</param>
/// <returns>Area of the quad.</returns>
public static float FindArea(Vector3 A, Vector3 B, Vector3 C, Vector3 D)
{
return TriArea(A, B, D) + TriArea(B, C, D);
}
/// <summary>
/// Finds the center point of a quad or triangle defined by 3 or 4 points.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner (can equal first corner for triangle).</param>
/// <returns>Center point.</returns>
public static Vector3 FindCenter(Vector3 a, Vector3 b, Vector3 c, Vector3 d)
{
if (a == d)
{
return (a + b + c) / 4f;
}
return (a + b + c + d) / 4f;
}
/// <summary>
/// Calculates the distance between two points projected along a normal vector.
/// </summary>
/// <param name="a">First point.</param>
/// <param name="b">Second point.</param>
/// <param name="normal">Normal vector to project along.</param>
/// <returns>Distance along normal.</returns>
public static float DistanceAlongNormal(Vector3 a, Vector3 b, Vector3 normal)
{
Vector3 dir = b - a;
return Vector3.Project(dir, normal).magnitude;
}
/// <summary>
/// Checks if a point lies inside a triangle.
/// </summary>
/// <param name="A">First triangle vertex.</param>
/// <param name="B">Second triangle vertex.</param>
/// <param name="C">Third triangle vertex.</param>
/// <param name="P">Point to test.</param>
/// <param name="dotThreshold">Tolerance for point-on-plane test.</param>
/// <returns>True if point is inside triangle.</returns>
public static bool PointInTriangle(Vector3 A, Vector3 B, Vector3 C, Vector3 P, float dotThreshold = 0.001f)
{
if (SameSide(P, A, B, C) && SameSide(P, B, A, C) && SameSide(P, C, A, B))
{
Vector3 vc1 = Vector3.Cross(B - A, C - A).normalized;
if (Mathf.Abs(Vector3.Dot(P - A, vc1)) <= dotThreshold)
{
return true;
}
}
return false;
}
private static bool SameSide(Vector3 p1, Vector3 p2, Vector3 A, Vector3 B)
{
Vector3 cp1 = Vector3.Cross(B - A, p1 - A).normalized;
Vector3 cp2 = Vector3.Cross(B - A, p2 - A).normalized;
if (Vector3.Dot(cp1, cp2) > 0)
{
return true;
}
return false;
}
/// <summary>
/// Checks if a 2D point is inside the screen rectangle.
/// </summary>
/// <param name="point">Point to check.</param>
/// <returns>True if point is inside screen bounds.</returns>
public static bool PointIsInsideRect(Vector2 point)
{
return new Rect(0, 0, Screen.width, Screen.height).Contains(point);
}
/// <summary>
/// Checks if two float values are nearly equal within an epsilon threshold.
/// </summary>
/// <param name="a">First value.</param>
/// <param name="b">Second value.</param>
/// <param name="epsilon">Maximum difference to consider equal.</param>
/// <returns>True if values are within epsilon.</returns>
public static bool NearlyEqual(this float a, float b, double epsilon)
{
return Mathf.Abs(a - b) < epsilon;
}
/// <summary>
/// Calculates the area of a triangle from three points.
/// </summary>
/// <param name="p1">First point.</param>
/// <param name="p2">Second point.</param>
/// <param name="p3">Third point.</param>
/// <returns>Area of the triangle.</returns>
public static float AreaFromThreePoints(Vector3 p1, Vector3 p2, Vector3 p3)
{
Vector3 u, v;
u.x = p2.x - p1.x;
u.y = p2.y - p1.y;
u.z = p2.z - p1.z;
v.x = p3.x - p1.x;
v.y = p3.y - p1.y;
v.z = p3.z - p1.z;
Vector3 crossUV = Vector3.Cross(u, v);
return Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z) * 0.5f;
}
/// <summary>
/// Calculates the area of a quadrilateral from four points.
/// </summary>
/// <param name="p1">First point.</param>
/// <param name="p2">Second point.</param>
/// <param name="p3">Third point.</param>
/// <param name="p4">Fourth point.</param>
/// <returns>Area of the quadrilateral.</returns>
public static float AreaFromFourPoints(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4)
{
return AreaFromThreePoints(p1, p2, p4) + AreaFromThreePoints(p2, p3, p4);
}
/// <summary>
/// Calculates area of a single triangle from it's three points.
/// </summary>
public static float TriArea(Vector3 p1, Vector3 p2, Vector3 p3)
{
Vector3 u, v, crossUV;
u.x = p2.x - p1.x;
u.y = p2.y - p1.y;
u.z = p2.z - p1.z;
v.x = p3.x - p1.x;
v.y = p3.y - p1.y;
v.z = p3.z - p1.z;
crossUV = Vector3.Cross(u, v);
return Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z) * 0.5f;
}
/// <summary>
/// Calculates area of a complete mesh.
/// </summary>
public static float MeshArea(Mesh mesh)
{
if (mesh.vertices.Length == 0)
{
return 0;
}
float area = 0;
Vector3[] verts = mesh.vertices;
int[] tris = mesh.triangles;
for (int i = 0; i < tris.Length; i += 3)
{
area += TriArea(verts[tris[i]], verts[tris[i + 1]], verts[tris[i + 2]]);
}
return area;
}
/// <summary>
/// Calculates area of a mesh as viewed from the direction vector.
/// </summary>
public static float ProjectedMeshArea(Mesh mesh, Vector3 direction)
{
float area = 0;
Vector3[] verts = mesh.vertices;
int[] tris = mesh.triangles;
Vector3[] normals = mesh.normals;
int count = 0;
for (int i = 0; i < tris.Length; i += 3)
{
area += TriArea(verts[tris[i]], verts[tris[i + 1]], verts[tris[i + 2]], direction);
count++;
}
return area;
}
/// <summary>
/// Calculates the area of a rectangle from four corner points.
/// </summary>
/// <param name="p1">First corner.</param>
/// <param name="p2">Second corner.</param>
/// <param name="p3">Third corner.</param>
/// <param name="p4">Fourth corner.</param>
/// <returns>Area of the rectangle.</returns>
public static float RectArea(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 p4)
{
return TriArea(p1, p2, p4) + TriArea(p2, p3, p4);
}
/// <summary>
/// Find mesh center by averaging. Returns local center.
/// </summary>
public static Vector3 FindMeshCenter(Mesh mesh)
{
if (mesh.vertices.Length == 0)
{
return Vector3.zero;
}
Vector3 sum = Vector3.zero;
int count = 0;
if (mesh != null)
{
foreach (Vector3 vert in mesh.vertices)
{
sum += vert;
count++;
}
}
return sum / count;
}
public static float TriArea(Vector3 p1, Vector3 p2, Vector3 p3, Vector3 view)
{
Vector3 u, v, crossUV, normal;
float crossMagnitude;
u.x = p2.x - p1.x;
u.y = p2.y - p1.y;
u.z = p2.z - p1.z;
v.x = p3.x - p1.x;
v.y = p3.y - p1.y;
v.z = p3.z - p1.z;
crossUV = Vector3.Cross(u, v);
crossMagnitude = Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z);
// Normal
if (crossMagnitude == 0)
{
normal.x = normal.y = normal.z = 0f;
}
else
{
normal.x = crossUV.x / crossMagnitude;
normal.y = crossUV.y / crossMagnitude;
normal.z = crossUV.z / crossMagnitude;
}
float angle = Vector3.Angle(normal, view);
float cos = Mathf.Cos(angle);
if (cos < 0)
{
return 0;
}
return Mathf.Sqrt(crossUV.x * crossUV.x + crossUV.y * crossUV.y + crossUV.z * crossUV.z) * 0.5f * cos;
}
/// <summary>
/// Calculates the signed volume contribution of a triangle relative to the origin.
/// </summary>
/// <param name="p1">First vertex.</param>
/// <param name="p2">Second vertex.</param>
/// <param name="p3">Third vertex.</param>
/// <returns>Signed volume.</returns>
public static float SignedVolumeOfTriangle(Vector3 p1, Vector3 p2, Vector3 p3)
{
float v321 = p3.x * p2.y * p1.z;
float v231 = p2.x * p3.y * p1.z;
float v312 = p3.x * p1.y * p2.z;
float v132 = p1.x * p3.y * p2.z;
float v213 = p2.x * p1.y * p3.z;
float v123 = p1.x * p2.y * p3.z;
return 1.0f / 6.0f * (-v321 + v231 + v312 - v132 - v213 + v123);
}
/// <summary>
/// Calculates the volume enclosed by a mesh.
/// </summary>
/// <param name="mesh">Mesh to calculate volume for.</param>
/// <returns>Volume of the mesh.</returns>
public static float VolumeOfMesh(Mesh mesh)
{
float volume = 0;
Vector3[] vertices = mesh.vertices;
int[] triangles = mesh.triangles;
for (int i = 0; i < mesh.triangles.Length; i += 3)
{
Vector3 p1 = vertices[triangles[i + 0]];
Vector3 p2 = vertices[triangles[i + 1]];
Vector3 p3 = vertices[triangles[i + 2]];
volume += SignedVolumeOfTriangle(p1, p2, p3);
}
return Mathf.Abs(volume);
}
/// <summary>
/// Transforms a point from local to world space without applying scale.
/// </summary>
/// <param name="transform">Transform to use.</param>
/// <param name="position">Local position.</param>
/// <returns>World position without scale.</returns>
public static Vector3 TransformPointUnscaled(this Transform transform, Vector3 position)
{
return Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one).MultiplyPoint3x4(position);
}
/// <summary>
/// Transforms a point from world to local space without applying scale.
/// </summary>
/// <param name="transform">Transform to use.</param>
/// <param name="position">World position.</param>
/// <returns>Local position without scale.</returns>
public static Vector3 InverseTransformPointUnscaled(this Transform transform, Vector3 position)
{
return Matrix4x4.TRS(transform.position, transform.rotation, Vector3.one).inverse
.MultiplyPoint3x4(position);
}
/// <summary>
/// Changes the layer of a transform and all its children recursively.
/// </summary>
/// <param name="trans">Root transform.</param>
/// <param name="name">Layer name.</param>
public static void ChangeLayersRecursively(this Transform trans, string name)
{
trans.gameObject.layer = LayerMask.NameToLayer(name);
foreach (Transform child in trans)
{
child.ChangeLayersRecursively(name);
}
}
/// <summary>
/// Changes the color of a GameObject's material.
/// </summary>
/// <param name="gameObject">GameObject to modify.</param>
/// <param name="color">New color.</param>
public static void ChangeObjectColor(GameObject gameObject, Color color)
{
gameObject.GetComponent<MeshRenderer>().material.SetColor("_Color", color);
}
/// <summary>
/// Changes the alpha value of a GameObject's material color.
/// </summary>
/// <param name="gameObject">GameObject to modify.</param>
/// <param name="alpha">New alpha value (0-1).</param>
public static void ChangeObjectAlpha(GameObject gameObject, float alpha)
{
MeshRenderer mr = gameObject.GetComponent<MeshRenderer>();
Color currentColor = mr.material.GetColor("_Color");
currentColor.a = alpha;
mr.material.SetColor("_Color", currentColor);
}
/// <summary>
/// Returns a vector with absolute values of all components.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Vector with absolute values.</returns>
public static Vector3 Vector3Abs(Vector3 v)
{
return new Vector3(Mathf.Abs(v.x), Mathf.Abs(v.y), Mathf.Abs(v.z));
}
/// <summary>
/// Rounds all components of a vector to nearest integer.
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Rounded vector.</returns>
public static Vector3 Vector3RoundToInt(Vector3 v)
{
return new Vector3(Mathf.RoundToInt(v.x), Mathf.RoundToInt(v.y), Mathf.RoundToInt(v.z));
}
/// <summary>
/// Returns a vector with reciprocal values (1/x, 1/y, 1/z).
/// </summary>
/// <param name="v">Input vector.</param>
/// <returns>Vector with reciprocal values.</returns>
public static Vector3 Vector3OneOver(Vector3 v)
{
return new Vector3(1f / v.x, 1f / v.y, 1f / v.z);
}
/// <summary>
/// Rounds a value to the nearest multiple of step.
/// </summary>
/// <param name="value">Value to round.</param>
/// <param name="step">Step size.</param>
/// <returns>Rounded value.</returns>
public static float RoundToStep(float value, float step)
{
return Mathf.Round(value / step) * step;
}
/// <summary>
/// Rounds a value to the nearest multiple of step.
/// </summary>
/// <param name="value">Value to round.</param>
/// <param name="step">Step size.</param>
/// <returns>Rounded value.</returns>
public static float RoundToStep(int value, int step)
{
return Mathf.RoundToInt(Mathf.Round(value / step) * step);
}
/// <summary>
/// Rotates a point around a pivot by the specified angles.
/// </summary>
/// <param name="point">Point to rotate.</param>
/// <param name="pivot">Pivot point.</param>
/// <param name="angles">Euler angles for rotation.</param>
/// <returns>Rotated point.</returns>
public static Vector3 RotatePointAroundPivot(Vector3 point, Vector3 pivot, Vector3 angles)
{
return Quaternion.Euler(angles) * (point - pivot) + pivot;
}
/// <summary>
/// Performs bilinear interpolation on a quad defined by four points.
/// </summary>
/// <param name="a">First corner.</param>
/// <param name="b">Second corner.</param>
/// <param name="c">Third corner.</param>
/// <param name="d">Fourth corner.</param>
/// <param name="u">U parameter (0-1).</param>
/// <param name="v">V parameter (0-1).</param>
/// <returns>Interpolated point.</returns>
public static Vector3 QuadLerp(Vector3 a, Vector3 b, Vector3 c, Vector3 d, float u, float v)
{
Vector3 abu = Vector3Lerp(a, b, u);
Vector3 dcu = Vector3Lerp(d, c, u);
return Vector3Lerp(abu, dcu, v);
}
/// <summary>
/// Linear interpolation between two vectors with value clamping.
/// </summary>
/// <param name="v1">Start vector.</param>
/// <param name="v2">End vector.</param>
/// <param name="value">Interpolation value (0-1).</param>
/// <returns>Interpolated vector.</returns>
public static Vector3 Vector3Lerp(Vector3 v1, Vector3 v2, float value)
{
if (value > 1.0f)
{
return v2;
}
if (value < 0.0f)
{
return v1;
}
return new Vector3(v1.x + (v2.x - v1.x) * value,
v1.y + (v2.y - v1.y) * value,
v1.z + (v2.z - v1.z) * value);
}
/// <summary>
/// Calculates the magnitude of a quaternion.
/// </summary>
/// <param name="q">Quaternion.</param>
/// <returns>Magnitude.</returns>
public static float QuaternionMagnitude(Quaternion q)
{
return Mathf.Sqrt(q.w * q.w + q.x * q.x + q.y * q.y + q.z * q.z);
}
}
}

Some files were not shown because too many files have changed in this diff Show More