Table of Contents

Water Behaviour

WaterBehaviour

Abstract MonoBehaviour class than can be used as utility for easier water sampling from your game object component.
You can treat is as a MonoBehaviour facade to WaterSampler.
It uses WaterSampler asyncrhonously, so it always generates one frame delay while sampling water, which is totally fine in most cases.

public abstract class WaterBehaviour : MonoBehaviour
{
    // used to configure component from inspector
    [Serializable]
    public class WaterSamplerConfig { /* .. */ }
 
    // access to config that is exposed for configuration via inspector
    public WaterSamplerConfig SamplerConfig {get; };
    public int FrameDividerOffset { get; };
 
    // check this inside InnerFixedUpdate if you want to schedule
    protected bool isSchedulingFrane { get; };
    // direct access to array with points you are going to sample
    protected Vector3[] samplingPoints { get; };
 
 
    // Unity callbacks
    protected virtual void Awake();
    protected virtual void OnEnable();
    protected virtual void OnDisable();
    protected virtual void OnDestroy();
    protected virtual void OnFixedUpdate(); // in most cases, do not override this function (see InnerFixedUpdate)
 
 
    // prepare sampler side before settingg any points
    protected void PrepareSamplingPointsSize(int size);
    protected void SetSamplingPoint(int index, Vector3 point);
    protected void SetSamplingPoints(Vector3[] points);
 
    protected int GetResultCount(); // return amount of available results
    protected WaterSampler.HitResult GetResult(int index);
    protected void GetResults(out ArraySegment<WaterSampler.HitResult> results);
 
 
    // override this function for actual fixed update behaviour
    // only inside this function you can schedule points
    protected virtual void InnerFixedUpdate() { }
}

FloatingBody

FloatingBody is built-in implementation of WaterBehaviour.

public class FloatingBody : WaterBehaviour
{
    // getters and setters for all properties you see in the inspector
    public Vector3 CenterOfMass { get; set; }
    public float BuoyancyForce { get; set; }
    public bool IgnoreMass { get; set; }
    public float NormalAlignment { get; set; }
    public float Instability { get; set; }
    public float SubmergeDrag { get; set; }
    public float SubmergeAngularDrag { get; set; }
 
 
    public float CurrentSubmergeLevel { get; } // range 0-1 thats means fraction of points that are under water
    public Rigidbody Rigidbody { get; } // return attached rigidbody
 
 
    // modify contact points (all in local space of game object)
    public Vector3 GetContactPoint(int index);
    public void SetContactPoint(int index, Vector3 point);
    public void RemoveContactPoint(int index);
    public void AddContactPoint(Vector3 point);
 
    // return last hit result for point with given index
    // useful e.g. to check if given part of body is above or under water without creating additional sampler or object for that
    public WaterSampler.HitResult GetContactPointHit(int index);
}
2023/06/11 23:46 · Bartek Dragon

Example Implementation

This implementation shows how to make cannon ball detecting collision with water.
It is used in attached Dragon Water Demo game.

[RequireComponent(typeof(Rigidbody))]
public class CannonBall : WaterBehaviour
{
    [SerializeField] float speed;
    [SerializeField] ParticleSystem trailParticle;
    [SerializeField] GameObject explosionMediumPrefab;
    [SerializeField] GameObject explosionLargePrefab;
    [SerializeField] GameObject splashPrefab;
    [SerializeField] float shipExplosionForce;
    [SerializeField] float shipExplosionRadius;
 
    Rigidbody _rigidbody;
    protected override void Awake()
    {
        base.Awake();
 
        _rigidbody = GetComponent<Rigidbody>();
    }
 
    private void Start()
    {
        _rigidbody.velocity = transform.forward * speed;
    }
 
    protected override void InnerFixedUpdate()
    {
        if (GetResultCount() > 0)
        {
            var hit = GetResult(0);
            if (hit.HasHit && hit.IsUnderwater)
            {
                HitWater();
            }
        }
 
        if (isSchedulingFrane)
        {
            PrepareSamplingPointsSize(1);
            SetSamplingPoint(0, transform.position);
        }
    }
 
 
    private void OnCollisionEnter(Collision collision)
    {
        var ship = collision.gameObject.GetComponentInParent<ShipController>();
        if (ship != null)
        {
            HitShip(ship);
        }
        else
        {
            HitOther();
        }
    }
 
 
    private void HitShip(ShipController ship)
    {
        ship.FloatingBody.Rigidbody.AddExplosionForce(shipExplosionForce, transform.position, shipExplosionRadius);
        Instantiate(explosionLargePrefab, transform.position, Quaternion.identity);
        DestroyBall();
    }
 
    private void HitOther()
    {
        Instantiate(explosionMediumPrefab, transform.position, Quaternion.identity);
        DestroyBall();
    }
 
    private void HitWater()
    {
        Instantiate(splashPrefab, transform.position, Quaternion.identity);
        DestroyBall();
    }
 
    private void DestroyBall()
    {
        var scale = trailParticle.transform.localScale;
        trailParticle.transform.SetParent(null, true);
        trailParticle.transform.localScale = scale;
        trailParticle.Stop();
        trailParticle.AddComponent<SelfDestroy>().delay = 5;
        Destroy(gameObject);
    }
}