slimecing

a fighting game featuring slimes and swords
Log | Files | Refs | README

CharacterMovementController.cs (14879B)


      1 using Slimecing.Environment;
      2 using Slimecing.Environment.Moving;
      3 using Slimecing.SimpleComponents.Movement;
      4 using UnityEngine;
      5 
      6 namespace Slimecing.Characters
      7 {
      8     [RequireComponent(typeof(Rigidbody))]
      9     [RequireComponent(typeof(Rotatable))]
     10     [RequireComponent(typeof(Collider))]
     11     public abstract class CharacterMovementController : MonoBehaviour
     12     {
     13         [Header("Components")]
     14         [SerializeField] protected Rigidbody rb;
     15         public Rigidbody playerRigidbody => rb;
     16         [SerializeField] protected Rotatable rotatable;
     17         [SerializeField] private Collider playerCollider;
     18 
     19         [Header("Player Movement Settings")]
     20         [SerializeField] protected float movementSpeed;
     21         [SerializeField] private bool useGravity;
     22         public bool UseGravity { get => useGravity;
     23             set => useGravity = value;
     24         }
     25         [SerializeField] protected Vector3 gravityDirection = Vector3.down;
     26         public Vector3 GravityDirection { get => gravityDirection;
     27             set => gravityDirection = value;
     28         }
     29         [SerializeField] protected float gravityForce;
     30         public float GravityForce { get => gravityForce;
     31             set => gravityForce = value;
     32         }
     33         [SerializeField] protected float simulatedFallingMass;
     34         public float SimulatedFallingMass { get => simulatedFallingMass;
     35             set => simulatedFallingMass = value;
     36         }
     37         [SerializeField] protected float minVelocityMagnitude;
     38 
     39         [Header("Slope Settings")]
     40         [SerializeField] private Vector3 slopeCheckPoint;
     41         [SerializeField] protected float slopeCheckRadius;
     42         [SerializeField] protected float maxAngle;
     43 
     44         [Header("Ledge Settings")]
     45         [SerializeField] private Vector3 ledgeCheckPoint;
     46         [SerializeField] private float ledgeCheckRadius;
     47 
     48         [Header("Grounding Settings")]
     49         [SerializeField] private Vector3 castOrigin;
     50         [SerializeField] protected float groundProbingDistance;
     51         [SerializeField] protected float groundingSphereCastRadius;
     52         [SerializeField] private LayerMask hitLayerMask;
     53 
     54         [Header("Other Settings")]
     55         [SerializeField] private bool preserveInteractableRigidbodyVelocity;
     56         const int MaxHitsBuffer = 3;
     57 
     58         protected Vector2 move;
     59         protected Vector3 calculatedGravity;
     60 
     61         private bool _isGrounded;
     62         public bool IsGrounded() => _isGrounded;
     63         private bool _canBeGrounded;
     64 
     65         private Vector3 _bodyVelocity;
     66         private Vector3 _attachedRigidbodyVelocity;
     67         private Rigidbody _lastAttachedRigidbody;
     68         private bool _isMovingFromAttachedRigidbody;
     69 
     70         private Vector3 _calculatedTargetVelocity;
     71         public Vector3 calculatedTargetVelocity
     72         {
     73             get => _calculatedTargetVelocity;
     74             set => _calculatedTargetVelocity = value;
     75         }
     76 
     77         public Rigidbody attachedRigidbody { get; private set; }
     78         
     79         public Vector3 internalTransformPosition { get; set; }
     80 
     81         private RaycastHit _internalGroundHit;
     82         private RaycastHit _internalSlopeHit;
     83         private RaycastHit _lastGoodNormalHit;
     84         private RaycastHit[] _internalCharacterHits = new RaycastHit[MaxHitsBuffer];
     85 
     86         public Vector2 MoveInput => move;
     87 
     88         protected void Awake()
     89         {
     90             internalTransformPosition = Vector3.zero;
     91             calculatedGravity = gravityDirection * gravityForce;
     92         }
     93         
     94         private void OnEnable()
     95         {
     96             MoverSimulationManager.CheckAlive();
     97             MoverSimulationManager.RegisterCharMover(this);
     98         }
     99 
    100         private void OnDisable()
    101         {
    102             MoverSimulationManager.UnregisterCharMover(this);
    103         }
    104 
    105         public abstract void GetMoveInputH(float h);
    106         public abstract void GetMoveInputV(float v);
    107 
    108         public void AddForceTo(Vector3 direction, float amount)
    109         {
    110             rb.AddForce(direction.normalized * amount, ForceMode.Impulse);
    111         }
    112         
    113         private void OnDrawGizmos()
    114         {
    115             Gizmos.color = Color.cyan;
    116             Gizmos.DrawWireSphere(transform.TransformPoint(slopeCheckPoint), slopeCheckRadius);
    117 
    118             Gizmos.color = Color.black;
    119             Gizmos.DrawWireSphere(transform.TransformPoint(ledgeCheckPoint), ledgeCheckRadius);
    120 
    121             Color changeColor = _isGrounded ? Color.green : Color.red;
    122             Gizmos.color = changeColor;
    123             Gizmos.DrawRay(transform.TransformPoint(castOrigin), gravityDirection.normalized * groundingSphereCastRadius);
    124             Gizmos.DrawWireSphere(transform.TransformPoint(castOrigin), groundingSphereCastRadius);
    125 
    126             Debug.DrawLine(transform.position, transform.position + PlayerUpVector() * 2, Color.cyan);
    127             Debug.DrawLine(transform.position, transform.position + Forward() * 2, Color.yellow);
    128         }
    129 
    130         public virtual void TickCharacter(float deltaTime)
    131         {
    132             GroundCheck(groundProbingDistance, groundingSphereCastRadius);
    133 
    134             InteractableRigidbodyCheck(deltaTime);
    135 
    136             Rotate(deltaTime);
    137             Move(deltaTime);
    138             
    139             if (!_isGrounded && useGravity) rb.AddForce(simulatedFallingMass * calculatedGravity);
    140 
    141             if (rb.velocity.magnitude < minVelocityMagnitude)
    142             {
    143                 rb.velocity = Vector3.zero;
    144             }
    145 
    146             _isGrounded = false;
    147         }
    148 
    149         private void InteractableRigidbodyCheck(float deltaTime)
    150         {
    151             if (!IsGrounded()) return;
    152             _lastAttachedRigidbody = attachedRigidbody;
    153             if (_lastGoodNormalHit.collider.attachedRigidbody)
    154             {
    155                 Rigidbody validRigidbody = GetInteractableRigidbody(_lastGoodNormalHit.collider);
    156                 if (validRigidbody)
    157                 {
    158                     attachedRigidbody = validRigidbody;
    159                 }
    160             }
    161             else
    162             {
    163                 attachedRigidbody = null;
    164             }
    165 
    166             Vector3 tmpVelocityFromInteractableRigidbody = Vector3.zero;
    167             if (attachedRigidbody)
    168             {
    169                 tmpVelocityFromInteractableRigidbody =
    170                     GetVelocityFromRigidbodyMovement(attachedRigidbody, transform.position, deltaTime);
    171             }
    172 
    173             if (preserveInteractableRigidbodyVelocity && _lastAttachedRigidbody != null &&
    174                 attachedRigidbody != _lastAttachedRigidbody)
    175             {
    176                 var velocity = rb.velocity;
    177                 velocity += _attachedRigidbodyVelocity;
    178                 velocity -= tmpVelocityFromInteractableRigidbody;
    179                 rb.AddForce(velocity, ForceMode.VelocityChange);
    180             }
    181 
    182             _attachedRigidbodyVelocity = Vector3.zero;
    183             if (attachedRigidbody)
    184             {
    185                 _attachedRigidbodyVelocity = tmpVelocityFromInteractableRigidbody;
    186                 Vector3 newForward = Vector3
    187                     .ProjectOnPlane(
    188                         Quaternion.Euler(attachedRigidbody.angularVelocity * (Mathf.Rad2Deg * deltaTime)) * Forward(),
    189                         PlayerGroundedUpVector()).normalized;
    190                 rb.rotation = Quaternion.LookRotation(newForward, PlayerGroundedUpVector());
    191             }
    192 
    193             rb.position += _attachedRigidbodyVelocity * deltaTime;
    194             internalTransformPosition = _attachedRigidbodyVelocity * deltaTime;
    195         }
    196 
    197         private Vector3 PlayerGroundedUpVector()
    198         {
    199             return transform.rotation * Vector3.up;
    200         }
    201 
    202         private Vector3 GetVelocityFromRigidbodyMovement(Rigidbody interactiveRb, Vector3 transformPosition, float deltaTime)
    203         {
    204             if (!(deltaTime > 0f)) return Vector3.zero;
    205             Vector3 moverVelocity = interactiveRb.velocity;
    206 
    207             if (interactiveRb.angularVelocity == Vector3.zero) return moverVelocity;
    208             
    209             Vector3 centerOfRot = interactiveRb.position + interactiveRb.centerOfMass;
    210             Vector3 centerOfRotOffset = transformPosition - centerOfRot;
    211             Quaternion rotationFromMoverRigidbody = Quaternion.Euler(interactiveRb.angularVelocity * (Mathf.Rad2Deg * deltaTime));
    212             Vector3 finalPointPos = centerOfRot + (rotationFromMoverRigidbody * centerOfRotOffset);
    213             moverVelocity += (finalPointPos - transformPosition) / deltaTime;
    214 
    215             return moverVelocity;
    216 
    217         }
    218 
    219         private Rigidbody GetInteractableRigidbody(Collider col)
    220         {
    221             if (!col.attachedRigidbody) return null;
    222             
    223             if (col.attachedRigidbody.gameObject.GetComponent<RigidbodyMover>())
    224             {
    225                 return col.attachedRigidbody;
    226             }
    227 
    228             if (!col.attachedRigidbody.isKinematic)
    229             {
    230                 return col.attachedRigidbody;
    231             }
    232 
    233             return null;
    234         }
    235 
    236         protected abstract void Move(float deltaTime);
    237 
    238         protected abstract void Rotate(float deltaTime);
    239 
    240         public float GetGroundAngle()
    241         {
    242             if (!_isGrounded) return 90f;
    243             return Vector3.Angle(_internalGroundHit.normal, transform.forward);
    244         }
    245 
    246         private bool IsStableOnNormal(Vector3 normal)
    247         {
    248             return Vector3.Angle(transform.up, normal) <= maxAngle;
    249         }
    250 
    251         protected Vector3 PlayerUpVector()
    252         {
    253             if (!_isGrounded) return transform.up;
    254             if (_internalSlopeHit.transform == null) return transform.up;
    255             return _internalSlopeHit.normal;
    256         }
    257         protected Vector3 Forward()
    258         {
    259             if (!_isGrounded) return transform.forward;
    260             if (_internalSlopeHit.transform == null) return transform.forward;
    261             return Vector3.Cross(transform.right, _internalSlopeHit.normal);
    262         }
    263         public void SetBackToNormalForces()
    264         {
    265             rb.velocity = new Vector3(move.x * movementSpeed, rb.velocity.y, move.y * movementSpeed);
    266         }
    267         
    268         public void GroundCheck(float length, float radius)
    269         {
    270             var ray =
    271                 new Ray(
    272                     transform.TransformPoint(castOrigin),
    273                     gravityDirection);
    274 
    275             if (GroundSweep(ray, radius, length, out var groundSweepHit))
    276             {
    277                 _canBeGrounded = true;
    278                 SlopeColliderValidate(groundSweepHit, slopeCheckRadius);
    279             }
    280             else
    281             {
    282                 _canBeGrounded = false;
    283                 _isGrounded = false;
    284             }
    285         }
    286 
    287         private bool GroundSweep(Ray position, float radius, float distance, out RaycastHit closestHit)
    288         {
    289             closestHit = new RaycastHit();
    290             int num =
    291                 Physics.SphereCastNonAlloc(
    292                     position,
    293                     radius,
    294                     _internalCharacterHits,
    295                     distance,
    296                     hitLayerMask,
    297                     QueryTriggerInteraction.Ignore
    298                     );
    299 
    300             bool foundValidHit = false;
    301             float closestDistance = Mathf.Infinity;
    302 
    303             for (int i = 0; i < num; i++)
    304             {
    305                 if (_internalCharacterHits[i].distance > 0f)
    306                 {
    307                     if (_internalCharacterHits[i].distance < closestDistance)
    308                     {
    309                         closestHit = _internalCharacterHits[i];
    310                         closestHit.distance -= 0.1f;
    311                         closestDistance = _internalCharacterHits[i].distance;
    312                         _internalGroundHit = closestHit;
    313                         foundValidHit = true;
    314                     }
    315                 }
    316             }
    317 
    318             return foundValidHit;
    319         }
    320 
    321         private void OnCollisionStay(Collision collision)
    322         {
    323             if (_canBeGrounded)
    324             {
    325                 float minFlatness = 1f - Mathf.Sin(Mathf.Deg2Rad * maxAngle);
    326                 for (int i = 0; i < collision.contactCount; i++)
    327                 {
    328                     if (i > MaxHitsBuffer) break;
    329 
    330                     ContactPoint c = collision.GetContact(i);
    331 
    332                     float flatness = Vector3.Dot(Vector3.up, c.normal);
    333                     if (flatness < 0f)
    334                         continue;
    335 
    336                     if (flatness >= minFlatness)
    337                     {
    338                         _isGrounded = true;
    339                     }
    340                 }
    341             }
    342         }
    343 
    344         private void SlopeColliderValidate(RaycastHit tempHit, float radius)
    345         {
    346             Collider[] colBuffer = new Collider[MaxHitsBuffer];
    347             int num =
    348             Physics.OverlapSphereNonAlloc(
    349                 transform.TransformPoint(slopeCheckPoint),
    350                 radius,
    351                 colBuffer,
    352                 hitLayerMask,
    353                 QueryTriggerInteraction.Ignore
    354                 );
    355 
    356             if (LedgeCheck(ledgeCheckRadius)) {
    357                 _internalSlopeHit = _lastGoodNormalHit;
    358                 return;
    359             }
    360 
    361             for (int i = 0; i < num; i++)
    362             {
    363                 RaycastHit hit;
    364                 Physics.Linecast(transform.position, colBuffer[i].transform.position, out hit, hitLayerMask, QueryTriggerInteraction.Ignore);
    365                 if (((colBuffer[i].transform != tempHit.transform) || (hit.normal != tempHit.normal)) && IsStableOnNormal(tempHit.normal) && CheckIfValidCollider(hit.collider))
    366                 {
    367                     if (IsStableOnNormal(hit.normal)) 
    368                     {
    369                         _internalSlopeHit = hit;
    370                         _lastGoodNormalHit = _internalSlopeHit;
    371                     }
    372                     
    373                     return;
    374                 }
    375             }
    376 
    377             if(IsStableOnNormal(tempHit.normal))
    378             {
    379                 _internalSlopeHit = _internalGroundHit;
    380                 _lastGoodNormalHit = _internalSlopeHit;
    381                 return;
    382             }
    383 
    384             _internalSlopeHit = _lastGoodNormalHit;
    385         }
    386 
    387         private bool LedgeCheck(float radius)
    388         {
    389             Collider[] colBuffer = new Collider[MaxHitsBuffer];
    390             int num =
    391             Physics.OverlapSphereNonAlloc(
    392                 transform.TransformPoint(ledgeCheckPoint),
    393                 radius,
    394                 colBuffer,
    395                 hitLayerMask,
    396                 QueryTriggerInteraction.Ignore
    397                 );
    398 
    399             if (num.Equals(0))
    400             {
    401                 return true;
    402             }
    403 
    404             for (int i = 0; i < num; i++)
    405             {
    406                 if (CheckIfValidCollider(colBuffer[i]))
    407                 {
    408                     return false;
    409                 }
    410             }
    411 
    412             return true;
    413         }
    414 
    415         private bool CheckIfValidCollider(Collider col)
    416         {
    417             return col != playerCollider && col != null;
    418         }
    419 
    420     }
    421 }