新增动态水物理插件

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,101 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.DWP2.SailController
{
/// <summary>
/// Applies wind drag forces to the hull of a sailing vessel based on the global wind conditions.
/// Calculates the projected area of the hull facing the wind and applies an appropriate drag force.
/// Does not account for the waterline - dimensions should represent only the hull portion above water.
/// Requires a WindGenerator to be present in the scene.
/// </summary>
public class HullWindApplicator : MonoBehaviour
{
/// <summary>
/// Physical dimensions of the hull above the waterline.
/// X represents width, Y represents height, Z represents length.
/// These dimensions are used to calculate the projected area when wind hits the hull from different angles.
/// </summary>
[Tooltip("The dimensions of the object (x = width, y = height, z = length).")]
[SerializeField]
public Vector3 dimensions;
/// <summary>
/// Drag coefficient applied to the wind force calculation.
/// Higher values result in stronger wind resistance.
/// Typical values range from 0.5 to 2.0 depending on hull shape and surface characteristics.
/// </summary>
[Tooltip("The drag coefficient of the object.")]
[SerializeField]
public float dragCoefficient = 1.0f;
private Rigidbody _rb;
private void Start()
{
_rb = GetComponentInParent<Rigidbody>();
Debug.Assert(_rb != null, "Rigidbody not found on self or parents.");
}
private void FixedUpdate()
{
if (WindGenerator.Instance == null)
{
return;
}
// Calculate the frontal area of the object facing the wind
Vector3 windDirection = WindGenerator.Instance.CurrentWind.normalized;
float projectedArea = Mathf.Abs(Vector3.Dot(dimensions, windDirection));
// Calculate the wind force
float windSpeed = WindGenerator.Instance.CurrentWind.magnitude;
float windForceMagnitude = 0.5f * dragCoefficient * projectedArea * Mathf.Pow(windSpeed, 2);
Vector3 windForce = windDirection * windForceMagnitude;
// Apply the wind force to the Rigidbody
_rb.AddForce(windForce);
}
}
}
#if UNITY_EDITOR
namespace NWH.DWP2.SailController
{
[CustomEditor(typeof(HullWindApplicator))]
[CanEditMultipleObjects]
public class HullWindApplicatorEditor : DWP2NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("dimensions");
drawer.Field("dragCoefficient");
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,438 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.DWP2.SailController
{
/// <summary>
/// Manages sail physics by calculating and applying lift and drag forces based on sail geometry, orientation, and wind conditions.
/// Uses four corner transforms (a, b, c, d) to define sail shape, enabling dynamic sail configurations including furling and multi-part sails.
/// Calculates apparent wind from true wind and vessel velocity, then applies aerodynamic forces at the sail's surface-weighted center.
/// The corner transforms follow a clockwise pattern: a (bottom left/front), b (top left/front), c (top right/rear), d (bottom right/rear).
/// Corner transforms can be attached to different parents to enable sail furling or complex rigging systems.
/// Use SailRotator for user-controlled sail rotation.
/// Requires a WindGenerator in the scene and a SailPreset for aerodynamic coefficients.
/// </summary>
public class SailController : MonoBehaviour
{
private void Awake()
{
_rb = GetComponentInParent<Rigidbody>();
Debug.Assert(_rb != null, "Rigidbody not found on the sail or any of the parent GameObjects.");
}
private void Start()
{
if (WindGenerator.Instance == null)
{
Debug.LogError("WindGenerator not found in the scene. SailController will not work.");
}
if (sailPreset == null)
{
Debug.LogError($"SailPreset not assigned to SailController {name}");
}
}
private void FixedUpdate()
{
Debug.Assert(a != null, $"{name}: Transform 'a' is not assigned.");
Debug.Assert(b != null, $"{name}: Transform 'b' is not assigned.");
Debug.Assert(c != null, $"{name}: Transform 'c' is not assigned.");
Debug.Assert(d != null, $"{name}: Transform 'd' is not assigned.");
// Update the sail positions, directions and geometry
UpdateCachedPositions();
SailCenter = CalculateSurfaceWeightedCenter();
SailArea = CalculateSailArea();
SailForward = CalculateSailForward();
SailUp = CalculateSailUp();
SailRight = CalculateSailRight(SailUp, SailForward);
// Calculate velocities
ShipVelocity = _rb.linearVelocity;
TrueWind = WindGenerator.Instance.CurrentWind;
ApparentWind = CalculateApparentWind(ShipVelocity, TrueWind);
AngleOfAttack = CalculateAngleOfAttack(ApparentWind, SailForward);
// Calculate and apply force
SailForce = CalculateSailForce();
_rb.AddForceAtPosition(SailForce, SailCenter);
}
private void OnDrawGizmos()
{
#if UNITY_EDITOR
if (a == null || b == null || c == null || d == null)
{
return;
}
UpdateCachedPositions();
Vector3 center = CalculateSurfaceWeightedCenter();
// Draw sail directions
if (!Application.isPlaying) // Calculate directions if out of play mode
{
SailForward = CalculateSailForward();
SailUp = CalculateSailUp();
SailRight = CalculateSailRight(SailUp, SailForward);
}
Gizmos.color = Color.blue;
Gizmos.DrawRay(center, SailForward);
Gizmos.color = Color.green;
Gizmos.DrawRay(center, SailUp);
Gizmos.color = Color.red;
Gizmos.DrawRay(center, SailRight);
// Draw sail shape
if (a != null && b != null && c != null && d != null)
{
Gizmos.color = Color.white;
Gizmos.DrawLine(_pA, _pB);
Gizmos.DrawLine(_pB, _pC);
Gizmos.DrawLine(_pC, _pD);
Gizmos.DrawLine(_pD, _pA);
Gizmos.DrawLine(_pA, _pC);
Gizmos.DrawLine(_pB, _pD);
Gizmos.DrawSphere(_pA, 0.1f);
Gizmos.DrawSphere(_pB, 0.1f);
Gizmos.DrawSphere(_pC, 0.1f);
Gizmos.DrawSphere(_pD, 0.1f);
Handles.Label(_pA, "A (bottom left)");
Handles.Label(_pB, "B (top left)");
Handles.Label(_pC, "C (top right)");
Handles.Label(_pD, "D (bottom right");
}
// RUNTIME ONLY FROM THIS POINT FORWARD
if (!Application.isPlaying)
{
return;
}
// Draw force point (center)
Gizmos.color = Color.red;
Gizmos.DrawSphere(center, 0.05f);
// Draw wind
Gizmos.color = Color.green;
Gizmos.DrawRay(center, TrueWind * 0.2f);
Handles.Label(center + TrueWind * 0.2f, $"True Wind ({TrueWind.magnitude} m/s)");
Gizmos.color = Color.yellow;
Gizmos.DrawRay(center, ApparentWind * 0.2f);
Handles.Label(center + ApparentWind * 0.2f, $"Apparent Wind ({ApparentWind.magnitude} m/s)");
Gizmos.color = Color.red;
Gizmos.DrawRay(center, SailForce * 0.001f);
Handles.Label(center + SailForce * 0.001f, $"Force ({SailForce.magnitude} N)");
Gizmos.color = Color.white;
Gizmos.DrawRay(center, _liftForceDirection);
Handles.Label(center + _liftForceDirection, "Lift Force Dir.");
Gizmos.color = Color.gray;
Gizmos.DrawRay(center, _dragForceDirection);
Handles.Label(center + _dragForceDirection, "Drag Force Dir.");
Gizmos.color = Color.magenta;
Gizmos.DrawRay(center - Vector3.up * 0.1f, ShipVelocity * 0.1f);
Handles.Label(center - Vector3.up * 0.1f + ShipVelocity * 0.1f,
$"Ship Velocity ({ShipVelocity.magnitude} m/s)");
#endif
}
private void UpdateCachedPositions()
{
_pA = a.position;
_pB = b.position;
_pC = c.position;
_pD = d.position;
}
private Vector3 CalculateSurfaceWeightedCenter()
{
return GeometryUtils.CalculateSurfaceWeightedCenter(_pA, _pB, _pC, _pD);
}
private float CalculateSailArea()
{
return GeometryUtils.CalculateQuadrilateralArea(_pA, _pB, _pC, _pD);
}
private Vector3 CalculateSailForward()
{
return (_pA - _pD).normalized;
}
private Vector3 CalculateSailUp()
{
return ((_pB + _pC) * 0.5f - (_pA + _pD) * 0.5f).normalized;
}
private Vector3 CalculateSailRight(Vector3 sailUp, Vector3 sailForward)
{
return Vector3.Cross(sailUp, sailForward).normalized;
}
private Vector3 CalculateApparentWind(Vector3 boatVelocity, Vector3 trueWind)
{
return trueWind - boatVelocity;
}
private float CalculateAngleOfAttack(Vector3 apparentWind, Vector3 sailForward)
{
return Vector3.SignedAngle(sailForward, apparentWind, Vector3.up);
}
private Vector3 CalculateSailForce()
{
float apparentWindSpeed = ApparentWind.magnitude;
float dynamicPressure = 0.5f * airDensity * apparentWindSpeed * apparentWindSpeed;
float liftCoefficient = sailPreset.liftCoefficientVsAoACurve.Evaluate(AngleOfAttack) * sailPreset.liftScale;
_liftForce = liftCoefficient * dynamicPressure * SailArea;
float dragCoefficient = sailPreset.dragCoefficientVsAoACurve.Evaluate(AngleOfAttack) * sailPreset.dragScale;
_dragForce = dragCoefficient * dynamicPressure * SailArea;
_liftForceDirection = SailRight * Mathf.Sign(Vector3.Dot(ApparentWind, SailRight));
_dragForceDirection = ApparentWind.normalized;
Vector3 totalForce = _liftForceDirection * _liftForce + _dragForceDirection * _dragForce;
// Compensate for the lean
totalForce *= Vector3.Dot(SailUp, Vector3.up);
return totalForce;
}
#region UserSettings
/// <summary>
/// Bottom left corner of the sail for square sails, or bottom front corner for triangular sails.
/// First point in the clockwise corner definition pattern.
/// Can be attached to any parent transform to enable dynamic sail configurations.
/// </summary>
[Tooltip("Bottom left sail corner if square sail.\r\nOtherwise bottom front.")]
public Transform a;
/// <summary>
/// Top left corner of the sail for square sails, or top front corner for triangular sails.
/// Second point in the clockwise corner definition pattern.
/// Can be attached to any parent transform to enable dynamic sail configurations.
/// </summary>
[Tooltip("Top left sail corner if square sail.\r\nOtherwise top front.")]
public Transform b;
/// <summary>
/// Top right corner of the sail for square sails, or top rear corner for triangular sails.
/// Third point in the clockwise corner definition pattern.
/// Can be attached to any parent transform to enable dynamic sail configurations.
/// </summary>
[Tooltip("Top right sail corner if square sail.\r\nOtherwise top rear.")]
public Transform c;
/// <summary>
/// Bottom right corner of the sail for square sails, or bottom rear corner for triangular sails.
/// Fourth point in the clockwise corner definition pattern.
/// Can be attached to any parent transform to enable dynamic sail configurations.
/// </summary>
[Tooltip("Bottom right sail corner if square sail.\r\nOtherwise bottom rear.")]
public Transform d;
/// <summary>
/// Defines the aerodynamic characteristics of the sail through lift and drag coefficient curves.
/// Contains the relationship between angle of attack and force coefficients.
/// </summary>
public SailPreset sailPreset;
/// <summary>
/// Air density in kg/m³ used in aerodynamic force calculations.
/// Standard sea level value is 1.225 kg/m³.
/// Can be adjusted as a multiplier to scale all sail forces uniformly without modifying the preset.
/// </summary>
[Tooltip(
"The air density. Can also be used\r\nas a force coefficient as this affects both lift and drag forces equally.")]
public float airDensity = 1.225f;
#endregion
#region SailCalculated
/// <summary>
/// Surface-weighted center point of the sail in world space.
/// All aerodynamic forces are applied at this position.
/// Calculated from the quadrilateral formed by corner points a, b, c, and d.
/// </summary>
public Vector3 SailCenter { get; private set; }
/// <summary>
/// Total surface area of the sail in square meters.
/// Calculated from the quadrilateral formed by corner points a, b, c, and d.
/// Used in aerodynamic force calculations along with dynamic pressure.
/// </summary>
public float SailArea { get; private set; }
/// <summary>
/// Forward direction vector of the sail in world space.
/// Determined by the vector from corner point d to corner point a.
/// Used to calculate the angle of attack relative to the apparent wind.
/// </summary>
public Vector3 SailForward { get; private set; }
/// <summary>
/// Up direction vector of the sail in world space.
/// Calculated from the average of the top edge to the average of the bottom edge.
/// Used to determine sail orientation and compensate for heel angle.
/// </summary>
public Vector3 SailUp { get; private set; }
/// <summary>
/// Right direction vector of the sail in world space.
/// Derived from the cross product of SailUp and SailForward.
/// Defines the direction of lift force generation perpendicular to the sail plane.
/// </summary>
public Vector3 SailRight { get; private set; }
private Vector3 _liftForceDirection;
private Vector3 _dragForceDirection;
private float _liftForce;
private float _dragForce;
#endregion
#region WindCalculated
/// <summary>
/// True wind vector from the WindGenerator, representing the actual environmental wind.
/// Does not account for vessel motion.
/// Measured in meters per second.
/// </summary>
public Vector3 TrueWind { get; private set; }
/// <summary>
/// Current velocity of the vessel's Rigidbody in world space.
/// Used to calculate apparent wind by combining with true wind.
/// Measured in meters per second.
/// </summary>
public Vector3 ShipVelocity { get; private set; }
/// <summary>
/// Total aerodynamic force vector applied to the vessel at the sail center point.
/// Combines lift and drag forces based on sail geometry, apparent wind, and aerodynamic coefficients.
/// Measured in Newtons.
/// </summary>
public Vector3 SailForce { get; private set; }
/// <summary>
/// Wind experienced by the sail relative to the moving vessel.
/// Calculated as the vector difference between true wind and vessel velocity.
/// This is the effective wind that generates aerodynamic forces on the sail.
/// Measured in meters per second.
/// </summary>
public Vector3 ApparentWind { get; private set; }
/// <summary>
/// Angle in degrees between the sail's forward direction and the apparent wind direction.
/// Measured on the horizontal plane using Vector3.up as the reference axis.
/// Used to determine lift and drag coefficients from the SailPreset curves.
/// Positive values indicate wind from the starboard side, negative values from port side.
/// </summary>
public float AngleOfAttack { get; private set; }
#endregion
#region Cached
private Vector3 _pA;
private Vector3 _pB;
private Vector3 _pC;
private Vector3 _pD;
private Rigidbody _rb;
#endregion
}
}
#if UNITY_EDITOR
namespace NWH.DWP2.SailController
{
[CustomEditor(typeof(SailController))]
[CanEditMultipleObjects]
public class SailControllerEditor : DWP2NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
SailController sailController = (SailController)target;
if (Application.isPlaying)
{
drawer.BeginSubsection("Debug Info");
drawer.Label($"AoA: {sailController.AngleOfAttack}");
drawer.Label($"Force Mag.: {sailController.SailForce.magnitude}");
drawer.Label($"Force: {sailController.SailForce}");
drawer.EndSubsection();
}
drawer.BeginSubsection("Geometry");
drawer.Field("a");
drawer.Field("b");
drawer.Field("c");
drawer.Field("d");
drawer.EndSubsection();
drawer.BeginSubsection("Physics");
drawer.Field("sailPreset");
drawer.Field("airDensity");
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,140 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.DWP2.SailController
{
/// <summary>
/// Defines the aerodynamic characteristics of a sail through lift and drag coefficient curves.
/// Contains the relationship between angle of attack and force generation.
/// Different sail types (square, lateen, bermuda, etc.) can be represented by different presets.
/// Can be created via Assets > Create > NWH > DWP2 > SailPreset.
/// </summary>
[CreateAssetMenu(fileName = "SailPreset", menuName = "NWH/DWP2/SailPreset", order = 1)]
public class SailPreset : ScriptableObject
{
/// <summary>
/// Optional description of the sail type and its characteristics.
/// </summary>
public string description;
/// <summary>
/// Defines how drag coefficient varies with angle of attack.
/// X-axis represents angle of attack in degrees (-180 to 180).
/// Y-axis represents the drag coefficient multiplier.
/// Drag acts in the direction of the apparent wind.
/// </summary>
public AnimationCurve dragCoefficientVsAoACurve = new();
/// <summary>
/// Global multiplier for all drag forces.
/// Values greater than 1 increase drag, values less than 1 reduce it.
/// Does not affect the shape of the drag curve.
/// </summary>
public float dragScale = 1f;
/// <summary>
/// Defines how lift coefficient varies with angle of attack.
/// X-axis represents angle of attack in degrees (-180 to 180).
/// Y-axis represents the lift coefficient multiplier.
/// Lift acts perpendicular to the sail plane.
/// </summary>
public AnimationCurve liftCoefficientVsAoACurve = new();
/// <summary>
/// Global multiplier for all lift forces.
/// Values greater than 1 increase lift, values less than 1 reduce it.
/// Does not affect the shape of the lift curve.
/// </summary>
public float liftScale = 1f;
private void Reset()
{
liftCoefficientVsAoACurve = GetDefaultLiftCurve();
dragCoefficientVsAoACurve = GetDefaultDragCurve();
}
private AnimationCurve GetDefaultDragCurve()
{
AnimationCurve dragCurve = new();
for (float angle = -180f; angle <= 180f; angle += 20f)
{
float angleRadians = angle * Mathf.Deg2Rad;
float forceCoefficient = Mathf.Sin(angleRadians);
dragCurve.AddKey(angle, forceCoefficient);
}
return dragCurve;
}
private AnimationCurve GetDefaultLiftCurve()
{
AnimationCurve liftCurve = new();
for (float angle = -180f; angle <= 180f; angle += 20f)
{
float angleRadians = angle * Mathf.Deg2Rad;
float forceCoefficient = Mathf.Cos(angleRadians * 2f);
liftCurve.AddKey(angle, forceCoefficient);
}
return liftCurve;
}
}
}
#if UNITY_EDITOR
namespace NWH.DWP2.SailController
{
[CustomEditor(typeof(SailPreset))]
[CanEditMultipleObjects]
public class SailPresetEditor : DWP2NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
SailPreset sailPreset = (SailPreset)target;
EditorGUILayout.Space(30f);
EditorGUILayout.LabelField("Description:");
sailPreset.description = EditorGUILayout.TextArea(sailPreset.description, GUILayout.Height(60f));
drawer.Space(100f);
drawer.BeginSubsection("Drag");
drawer.Field("dragScale", true, "x100%");
drawer.Field("dragCoefficientVsAoACurve", true, null, "Drag Coeff. vs AoA");
drawer.EndSubsection();
drawer.BeginSubsection("Lift");
drawer.Field("liftScale", true, "x100%");
drawer.Field("liftCoefficientVsAoACurve", true, null, "Lift Coeff. vs AoA");
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

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

View File

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

View File

@@ -0,0 +1,160 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 209974261ad72654ea8ea4f529e3c63b, type: 3}
m_Name: BermudaSailPreset
m_EditorClassIdentifier:
description: 'Typical sail configuration for a modern sailboat.
Generates
force mostly from lift.
'
liftScale: 1
liftCoefficientVsAoACurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: -180
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: -150
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: -100
value: 0.2
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0.33333334
- serializedVersion: 3
time: -30
value: 1.5
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.3697629
outWeight: 0.33333334
- serializedVersion: 3
time: 0
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 30
value: 1.5
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.15104294
outWeight: 0
- serializedVersion: 3
time: 100
value: 0.2
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 150
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: 180
value: 0
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
dragScale: 1
dragCoefficientVsAoACurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: -190
value: 0.15
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
- serializedVersion: 3
time: -90
value: 1.3
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.19646317
outWeight: 0.09987462
- serializedVersion: 3
time: 0
value: 0.15
inSlope: 0.00037129602
outSlope: 0.00037129602
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0.14262822
- serializedVersion: 3
time: 90
value: 1.3
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.06089728
outWeight: 0.09204839
- serializedVersion: 3
time: 180
value: 0.15
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0
outWeight: 0
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a129b9f6ea8d1744da29b5f0f89022f2
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,371 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 209974261ad72654ea8ea4f529e3c63b, type: 3}
m_Name: SquareSailPreset
m_EditorClassIdentifier:
description: 123
liftScale: 0
liftCoefficientVsAoACurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: -180
value: 1
inSlope: -0.011697784
outSlope: -0.011697784
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -160
value: 0.7660443
inSlope: -0.020658795
outSlope: -0.020658795
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -140
value: 0.17364815
inSlope: -0.031651106
outSlope: -0.031651106
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -120
value: -0.4999999
inSlope: -0.027833518
outSlope: -0.027833518
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -100
value: -0.9396926
inSlope: -0.010992317
outSlope: -0.010992317
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -80
value: -0.9396926
inSlope: 0.010992314
outSlope: 0.010992314
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -60
value: -0.50000006
inSlope: 0.027833521
outSlope: 0.027833521
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -40
value: 0.17364822
inSlope: 0.03165111
outSlope: 0.03165111
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -20
value: 0.76604444
inSlope: 0.020658795
outSlope: 0.020658795
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0
value: 1
inSlope: 0
outSlope: 0
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 20
value: 0.76604444
inSlope: -0.020658795
outSlope: -0.020658795
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 40
value: 0.17364822
inSlope: -0.03165111
outSlope: -0.03165111
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 60
value: -0.50000006
inSlope: -0.027833521
outSlope: -0.027833521
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 80
value: -0.9396926
inSlope: -0.010992314
outSlope: -0.010992314
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 100
value: -0.9396926
inSlope: 0.010992317
outSlope: 0.010992317
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 120
value: -0.4999999
inSlope: 0.027833518
outSlope: 0.027833518
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 140
value: 0.17364815
inSlope: 0.031651106
outSlope: 0.031651106
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 160
value: 0.7660443
inSlope: 0.020658795
outSlope: 0.020658795
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 180
value: 1
inSlope: 0.011697784
outSlope: 0.011697784
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4
dragScale: 1
dragCoefficientVsAoACurve:
serializedVersion: 2
m_Curve:
- serializedVersion: 3
time: -180
value: 0.00000008742278
inSlope: -0.017101016
outSlope: -0.017101016
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -160
value: -0.3420202
inSlope: -0.016069693
outSlope: -0.016069693
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -140
value: -0.64278764
inSlope: -0.01310013
outSlope: -0.01310013
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -120
value: -0.8660254
inSlope: -0.008550502
outSlope: -0.008550502
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -100
value: -0.9848077
inSlope: -0.0029695586
outSlope: -0.0029695586
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -80
value: -0.9848077
inSlope: 0.002969557
outSlope: 0.002969557
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -60
value: -0.86602545
inSlope: 0.008550504
outSlope: 0.008550504
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -40
value: -0.6427876
inSlope: 0.013100133
outSlope: 0.013100133
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: -20
value: -0.34202012
inSlope: 0.01606969
outSlope: 0.01606969
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 0
value: 0
inSlope: 0.017101007
outSlope: 0.017101007
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 20
value: 0.34202012
inSlope: 0.01606969
outSlope: 0.01606969
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 40
value: 0.6427876
inSlope: 0.013100133
outSlope: 0.013100133
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 60
value: 0.86602545
inSlope: 0.008550504
outSlope: 0.008550504
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 80
value: 0.9848077
inSlope: 0.002969557
outSlope: 0.002969557
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 100
value: 0.9848077
inSlope: -0.0029695586
outSlope: -0.0029695586
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 120
value: 0.8660254
inSlope: -0.008550502
outSlope: -0.008550502
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 140
value: 0.64278764
inSlope: -0.01310013
outSlope: -0.01310013
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 160
value: 0.3420202
inSlope: -0.016069693
outSlope: -0.016069693
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
- serializedVersion: 3
time: 180
value: -0.00000008742278
inSlope: -0.017101016
outSlope: -0.017101016
tangentMode: 0
weightedMode: 0
inWeight: 0.33333334
outWeight: 0.33333334
m_PreInfinity: 2
m_PostInfinity: 2
m_RotationOrder: 4

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8c52d46290a38a24b9e5b3645fbd472e
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 11400000
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,89 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.DWP2.ShipController;
using UnityEngine;
#if UNITY_EDITOR
using NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.DWP2.SailController
{
/// <summary>
/// Rotates a transform based on player input from the AdvancedShipController.
/// Used to control sail orientation in response to the RotateSail input axis.
/// Typically attached to a sail boom or mast that rotates horizontally.
/// Requires an AdvancedShipController component on a parent GameObject.
/// </summary>
public class SailRotator : MonoBehaviour
{
/// <summary>
/// Local axis around which the transform rotates.
/// Default (0, 1, 0) rotates around the Y-axis.
/// Use negative values to reverse rotation direction.
/// Magnitude is ignored - only direction matters.
/// </summary>
[Tooltip("Rotation axis of this transform.\r\nUse -1 to reverse the rotation.")]
public Vector3 rotationAxis = new(0, 1, 0);
/// <summary>
/// Maximum rotation speed in degrees per second when input is at full deflection.
/// Higher values allow faster sail adjustment.
/// Combined with rotationAxis and player input to determine final rotation.
/// </summary>
[Tooltip(
"Rotation speed of this transform in deg/s.\r\nMultiplied by the rotationAxis to get the final rotation.")]
public float rotationSpeed = 50f;
private AdvancedShipController _shipController;
private void Awake()
{
_shipController = GetComponentInParent<AdvancedShipController>();
Debug.Assert(_shipController != null, "SailController requires the AdvancedShipController to" +
" be attached to one of the parents (does not have to be direct parent).");
}
private void Update()
{
float rotationAngle = _shipController.input.RotateSail * rotationSpeed * Time.deltaTime;
transform.Rotate(rotationAngle * rotationAxis);
}
}
}
#if UNITY_EDITOR
namespace NWH.DWP2.SailController
{
[CustomEditor(typeof(SailRotator))]
[CanEditMultipleObjects]
public class SailRotatorEditor : DWP2NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Field("rotationSpeed");
drawer.Field("rotationAxis");
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,197 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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 Random = UnityEngine.Random;
#if UNITY_EDITOR
using NWH.NUI;
using UnityEditor;
#endif
#endregion
namespace NWH.DWP2.SailController
{
/// <summary>
/// Generates procedural wind with randomized gusts for sail simulation.
/// Creates realistic wind conditions by varying speed and direction over time.
/// Provides a singleton instance accessible to all sail and wind-related components.
/// Wind direction uses world coordinates where 0 degrees points along the positive Z-axis.
/// Only one WindGenerator should exist per scene.
/// </summary>
public class WindGenerator : MonoBehaviour
{
/// <summary>
/// Singleton instance accessible globally.
/// Used by SailController and HullWindApplicator to get current wind conditions.
/// </summary>
public static WindGenerator Instance;
/// <summary>
/// Primary wind direction in degrees on the horizontal plane.
/// 0 degrees corresponds to positive Z-axis (north in Unity coordinates).
/// Rotation follows the right-hand rule around the Y-axis.
/// Wind will vary around this base direction within the limits of maxDirectionVariation.
/// </summary>
[Tooltip("Base wind direction in degrees with 0 degrees indicating Z-forward ('north').")]
public float baseDirection;
/// <summary>
/// Average wind speed in meters per second.
/// Wind will vary around this base speed within the limits of maxSpeedVariation.
/// Typical sailing winds range from 5 m/s (light breeze) to 20 m/s (strong wind).
/// </summary>
[Tooltip("Base wind speed in m/s.")]
public float baseSpeed = 10.0f;
/// <summary>
/// Maximum angular deviation from baseDirection during wind gusts.
/// Measured in degrees.
/// Higher values create more unpredictable wind shifts.
/// Realistic values range from 15 to 45 degrees.
/// </summary>
[Tooltip("Maximum possible variation of the direction in degrees from the\r\nbaseDirection.")]
public float maxDirectionVariation = 30f;
/// <summary>
/// Maximum speed deviation from baseSpeed during wind gusts.
/// Measured in meters per second.
/// Higher values create stronger variations between lulls and gusts.
/// Can be positive or negative relative to base speed.
/// </summary>
[Tooltip("Maximum possible variation/deviation of the wind from the baseSpeed.")]
public float maxSpeedVariation = 5f;
/// <summary>
/// Longest possible duration between wind condition changes.
/// Measured in seconds.
/// New wind conditions are selected randomly between minVariationInterval and this value.
/// </summary>
[Tooltip("Maximum interval between the wind variations / changes.")]
public float maxVariationInterval = 6.0f;
/// <summary>
/// Shortest possible duration between wind condition changes.
/// Measured in seconds.
/// New wind conditions are selected randomly between this value and maxVariationInterval.
/// </summary>
[Tooltip("Minimum interval between the wind variations / changes.")]
public float minVariationInterval = 2.0f;
private float _currentInterval = 1f;
private float _smoothingDirectionVelocity;
private float _smoothingSpeedVelocity;
private float _targetDirection;
private float _targetSpeed;
/// <summary>
/// Current wind vector in world space.
/// Combines CurrentDirection and CurrentSpeed into a directional velocity vector.
/// Updated every FixedUpdate with smooth damping between gust transitions.
/// Measured in meters per second.
/// </summary>
public Vector3 CurrentWind { get; private set; }
/// <summary>
/// Current wind direction in degrees on the horizontal plane.
/// Smoothly interpolates between randomized target directions.
/// 0 degrees corresponds to positive Z-axis.
/// </summary>
public float CurrentDirection { get; private set; }
/// <summary>
/// Current wind speed magnitude in meters per second.
/// Smoothly interpolates between randomized target speeds.
/// </summary>
public float CurrentSpeed { get; private set; }
private void Awake()
{
if (Instance != null)
{
Debug.LogWarning("The scene has more than one WindGenerator. The previous one(s) will be ignored.");
}
Instance = this;
}
private void Start()
{
StartCoroutine(GustCoroutine());
}
private void FixedUpdate()
{
CurrentDirection = Mathf.SmoothDamp(CurrentDirection, _targetDirection,
ref _smoothingDirectionVelocity, _currentInterval);
CurrentSpeed = Mathf.SmoothDamp(CurrentSpeed, _targetSpeed,
ref _smoothingSpeedVelocity, _currentInterval);
CurrentWind = Quaternion.AngleAxis(CurrentDirection, Vector3.up) * Vector3.forward * CurrentSpeed;
}
private IEnumerator GustCoroutine()
{
while (true)
{
_targetSpeed = baseSpeed + Random.Range(-maxSpeedVariation, maxSpeedVariation);
_targetDirection = baseDirection + Random.Range(-maxDirectionVariation, maxDirectionVariation);
_currentInterval = Random.Range(minVariationInterval, maxVariationInterval);
yield return new WaitForSeconds(_currentInterval);
}
}
}
}
#if UNITY_EDITOR
namespace NWH.DWP2.SailController
{
[CustomEditor(typeof(WindGenerator))]
[CanEditMultipleObjects]
public class WindGeneratorEditor : DWP2NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
WindGenerator windGenerator = (WindGenerator)target;
if (Application.isPlaying)
{
drawer.Label($"Current Wind: {windGenerator.CurrentSpeed:0.0} from {windGenerator.CurrentDirection:0}");
}
drawer.BeginSubsection("Base");
drawer.Field("baseSpeed");
drawer.Field("baseDirection");
drawer.EndSubsection();
drawer.BeginSubsection("Variation");
drawer.Field("maxSpeedVariation");
drawer.Field("maxDirectionVariation");
drawer.Field("minVariationInterval");
drawer.Field("maxVariationInterval");
drawer.EndSubsection();
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

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

View File

@@ -0,0 +1,79 @@
// ╔════════════════════════════════════════════════════════════════╗
// ║ 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.DWP2.SailController
{
/// <summary>
/// Visual indicator that rotates to show wind direction.
/// Displays apparent wind when attached as a child of a SailController.
/// Displays true wind from the WindGenerator when not under a SailController.
/// Useful for debugging sail setup and helping players understand wind conditions.
/// The transform's forward direction will point into the wind.
/// </summary>
public class WindIndicator : MonoBehaviour
{
private SailController _sailController;
private void Awake()
{
_sailController = GetComponentInParent<SailController>();
}
private void Update()
{
if (_sailController != null)
{
// Show apparent wind.
transform.rotation = Quaternion.LookRotation(_sailController.ApparentWind.normalized,
transform.parent.up);
}
else if (WindGenerator.Instance != null)
{
// Show true wind.
transform.rotation = Quaternion.LookRotation(WindGenerator.Instance.CurrentWind.normalized,
transform.parent.up);
}
}
}
}
#if UNITY_EDITOR
namespace NWH.DWP2.SailController
{
[CustomEditor(typeof(WindIndicator))]
[CanEditMultipleObjects]
public class WindIndicatorEditor : DWP2NUIEditor
{
public override bool OnInspectorNUI()
{
if (!base.OnInspectorNUI())
{
return false;
}
drawer.Info("Shows apparent wind if placed as a child of the SailController, " +
"or true wind otherwise.");
drawer.EndEditor(this);
return true;
}
}
}
#endif

View File

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