using UnityEngine;
using System.Collections.Generic;
namespace Synaptic
{
///
/// Water surface component - attach to water plane
/// Provides water height queries and wave animation
///
public class WaterSurface : MonoBehaviour
{
[Header("Wave Settings")]
public float waveSpeed = 1f;
public float waveStrength = 0.1f;
public float waveFrequency = 1f;
public Vector2 waveDirectionA = new Vector2(1, 0);
public Vector2 waveDirectionB = new Vector2(0, 1);
[Header("Physics")]
public float waterDensity = 1000f; // kg/m³ (water = 1000)
private static List activeSurfaces = new List();
public static WaterSurface GetWaterSurfaceAt(Vector3 position)
{
foreach (var surface in activeSurfaces)
{
if (surface.IsPointAboveWater(position))
{
return surface;
}
}
return null;
}
void OnEnable()
{
if (!activeSurfaces.Contains(this))
{
activeSurfaces.Add(this);
}
}
void OnDisable()
{
activeSurfaces.Remove(this);
}
///
/// Get water height at world position using Gerstner waves
///
public float GetWaterHeight(Vector3 worldPosition)
{
float baseHeight = transform.position.y;
float time = Time.time * waveSpeed;
// Gerstner wave calculation
float height = 0f;
height += GerstnerWaveHeight(worldPosition, waveDirectionA, waveStrength, waveFrequency * 10f, time);
height += GerstnerWaveHeight(worldPosition, waveDirectionB, waveStrength * 0.5f, waveFrequency * 7f, time * 1.3f);
height += GerstnerWaveHeight(worldPosition, new Vector2(0.7f, 0.7f), waveStrength * 0.3f, waveFrequency * 5f, time * 0.8f);
return baseHeight + height;
}
private float GerstnerWaveHeight(Vector3 position, Vector2 direction, float steepness, float wavelength, float time)
{
float k = 2f * Mathf.PI / wavelength;
float c = Mathf.Sqrt(9.8f / k);
Vector2 d = direction.normalized;
float f = k * (Vector2.Dot(d, new Vector2(position.x, position.z)) - c * time);
float a = steepness / k;
return a * Mathf.Sin(f);
}
///
/// Check if a point is within the water area (XZ bounds)
///
public bool IsPointAboveWater(Vector3 point)
{
// Simple bounds check using renderer bounds
var renderer = GetComponent();
if (renderer != null)
{
var bounds = renderer.bounds;
return point.x >= bounds.min.x && point.x <= bounds.max.x &&
point.z >= bounds.min.z && point.z <= bounds.max.z;
}
// Fallback to transform scale
Vector3 localPoint = transform.InverseTransformPoint(point);
return Mathf.Abs(localPoint.x) <= 0.5f && Mathf.Abs(localPoint.z) <= 0.5f;
}
///
/// Get wave normal at position for physics calculations
///
public Vector3 GetWaveNormal(Vector3 worldPosition)
{
float delta = 0.1f;
float h0 = GetWaterHeight(worldPosition);
float hx = GetWaterHeight(worldPosition + Vector3.right * delta);
float hz = GetWaterHeight(worldPosition + Vector3.forward * delta);
Vector3 tangentX = new Vector3(delta, hx - h0, 0).normalized;
Vector3 tangentZ = new Vector3(0, hz - h0, delta).normalized;
return Vector3.Cross(tangentZ, tangentX).normalized;
}
}
///
/// Buoyancy component - makes objects float on water
/// Attach to any Rigidbody that should float
///
[RequireComponent(typeof(Rigidbody))]
public class Buoyancy : MonoBehaviour
{
[Header("Buoyancy Settings")]
[Tooltip("Points where buoyancy force is applied")]
public Transform[] floatPoints;
[Tooltip("How much the object floats (1 = neutrally buoyant)")]
[Range(0f, 3f)]
public float buoyancyStrength = 1.5f;
[Tooltip("Underwater drag multiplier")]
public float underwaterDrag = 3f;
[Tooltip("Underwater angular drag multiplier")]
public float underwaterAngularDrag = 1f;
[Header("Effects")]
public bool createSplashOnEnter = true;
public GameObject splashPrefab;
public float splashThreshold = 2f; // Minimum velocity to create splash
private Rigidbody rb;
private float originalDrag;
private float originalAngularDrag;
private bool wasUnderwater = false;
private WaterSurface currentWater;
void Start()
{
rb = GetComponent();
originalDrag = rb.linearDamping;
originalAngularDrag = rb.angularDamping;
// Auto-generate float points if not set
if (floatPoints == null || floatPoints.Length == 0)
{
GenerateFloatPoints();
}
}
void GenerateFloatPoints()
{
var collider = GetComponent();
if (collider != null)
{
var bounds = collider.bounds;
var points = new List();
// Create 4 corner points + center
Vector3[] offsets = new Vector3[]
{
new Vector3(-0.4f, -0.5f, -0.4f),
new Vector3(0.4f, -0.5f, -0.4f),
new Vector3(-0.4f, -0.5f, 0.4f),
new Vector3(0.4f, -0.5f, 0.4f),
new Vector3(0, -0.5f, 0)
};
foreach (var offset in offsets)
{
var point = new GameObject("FloatPoint").transform;
point.parent = transform;
point.localPosition = Vector3.Scale(offset, bounds.size);
points.Add(point);
}
floatPoints = points.ToArray();
}
}
void FixedUpdate()
{
currentWater = WaterSurface.GetWaterSurfaceAt(transform.position);
if (currentWater == null)
{
// Reset drag when out of water
rb.linearDamping = originalDrag;
rb.angularDamping = originalAngularDrag;
wasUnderwater = false;
return;
}
bool isUnderwater = false;
int underwaterPoints = 0;
foreach (var point in floatPoints)
{
if (point == null) continue;
float waterHeight = currentWater.GetWaterHeight(point.position);
float depth = waterHeight - point.position.y;
if (depth > 0)
{
isUnderwater = true;
underwaterPoints++;
// Calculate buoyancy force
float displacementMultiplier = Mathf.Clamp01(depth / 0.5f);
float buoyancyForce = currentWater.waterDensity * Physics.gravity.magnitude * displacementMultiplier * buoyancyStrength;
// Apply force at float point
Vector3 force = Vector3.up * buoyancyForce / floatPoints.Length;
rb.AddForceAtPosition(force, point.position, ForceMode.Force);
// Add wave influence
Vector3 waveNormal = currentWater.GetWaveNormal(point.position);
rb.AddForceAtPosition(waveNormal * buoyancyForce * 0.1f, point.position, ForceMode.Force);
}
}
// Apply underwater drag
if (isUnderwater)
{
float submergedRatio = (float)underwaterPoints / floatPoints.Length;
rb.linearDamping = Mathf.Lerp(originalDrag, underwaterDrag, submergedRatio);
rb.angularDamping = Mathf.Lerp(originalAngularDrag, underwaterAngularDrag, submergedRatio);
}
else
{
rb.linearDamping = originalDrag;
rb.angularDamping = originalAngularDrag;
}
// Splash effect on water entry
if (createSplashOnEnter && isUnderwater && !wasUnderwater)
{
if (rb.linearVelocity.magnitude > splashThreshold)
{
CreateSplash();
}
}
wasUnderwater = isUnderwater;
}
void CreateSplash()
{
if (splashPrefab != null)
{
float waterHeight = currentWater.GetWaterHeight(transform.position);
Vector3 splashPos = new Vector3(transform.position.x, waterHeight, transform.position.z);
Instantiate(splashPrefab, splashPos, Quaternion.identity);
}
else
{
// Create simple particle splash
float waterHeight = currentWater.GetWaterHeight(transform.position);
Vector3 splashPos = new Vector3(transform.position.x, waterHeight, transform.position.z);
var splashGO = new GameObject("Splash");
splashGO.transform.position = splashPos;
var ps = splashGO.AddComponent();
var main = ps.main;
main.startLifetime = 1f;
main.startSpeed = 3f;
main.startSize = 0.1f;
main.startColor = new Color(0.8f, 0.9f, 1f, 0.7f);
main.gravityModifier = 1f;
main.maxParticles = 50;
main.duration = 0.3f;
main.loop = false;
var emission = ps.emission;
emission.rateOverTime = 0;
emission.SetBurst(0, new ParticleSystem.Burst(0f, 30));
var shape = ps.shape;
shape.shapeType = ParticleSystemShapeType.Hemisphere;
shape.radius = 0.3f;
ps.Play();
Destroy(splashGO, 2f);
}
}
void OnDrawGizmosSelected()
{
if (floatPoints == null) return;
Gizmos.color = Color.cyan;
foreach (var point in floatPoints)
{
if (point != null)
{
Gizmos.DrawWireSphere(point.position, 0.1f);
}
}
}
}
///
/// Water interaction trigger - creates ripples and splashes when objects enter
///
[RequireComponent(typeof(Collider))]
public class WaterInteraction : MonoBehaviour
{
[Header("Ripple Settings")]
public bool createRipples = true;
public float rippleInterval = 0.5f;
public GameObject ripplePrefab;
[Header("Splash Settings")]
public bool createSplashes = true;
public float minSplashVelocity = 1f;
public GameObject splashPrefab;
private float lastRippleTime;
void OnTriggerEnter(Collider other)
{
if (!createSplashes) return;
var rb = other.GetComponent();
if (rb != null && rb.linearVelocity.magnitude > minSplashVelocity)
{
CreateSplashAt(other.ClosestPoint(transform.position), rb.linearVelocity.magnitude);
}
}
void OnTriggerStay(Collider other)
{
if (!createRipples) return;
if (Time.time - lastRippleTime < rippleInterval) return;
var rb = other.GetComponent();
if (rb != null && rb.linearVelocity.magnitude > 0.1f)
{
CreateRippleAt(other.ClosestPoint(transform.position));
lastRippleTime = Time.time;
}
}
void CreateSplashAt(Vector3 position, float intensity)
{
if (splashPrefab != null)
{
var splash = Instantiate(splashPrefab, position, Quaternion.identity);
Destroy(splash, 3f);
}
}
void CreateRippleAt(Vector3 position)
{
if (ripplePrefab != null)
{
var ripple = Instantiate(ripplePrefab, position, Quaternion.Euler(90, 0, 0));
Destroy(ripple, 2f);
}
}
}
}