Kukuru3

tweet face mail
 
 using Embers.Actions;
 using Embers.GameMechanics;
 
 using K3;
 
 using System;
 
 using UnityEngine;
 
 // stolen from Kromlech, and not very good. 
 namespace Embers.Character {
 
     public enum MoveRegimes {
         Free,
         AnimatorControlled,
         AxesToFacing,
         AxesToFacingImmediate,
     }
 
     public interface IHasCharacterMotionState {
         /// <summary> Velocity relative to facing</summary>
         Vector2 RelativeVelocity { get; }
         Vector2 RelativeDesiredVelocity { get; }
         Vector3 ObservedVelocity { get; }
         bool ContactWithGround { get; }
         bool InCombatStance { get; }
 
         Vector3 DesiredWorldspaceMotionAxis { get; }
 
         event Action<float> JustHitGround;
 
         MoveRegimes MoveRegime { get; }
 
         // bool MovementHijackedByAnimator { get; }
         bool IsRagdolling { get; }
         bool IsSliding { get; }
         bool IsSlipping { get; }
         bool Autopilot { get; }
 
         void SetRegime(MoveRegimes regime);
 
         void SetRagdollActive(bool active);
         void OverrideDesiredVelocity(Vector3 newDesiredV, float accel);
         void ForceRotateTowards(Vector3 worldHeading);
 
         Immersion ImmersionLevel { get; }
         event ImmersionChangedDelegate ImmersionChanged;
         public CollisionFlags CurrentCollisionFlags { get; }
 
         (Vector3 bottom, Vector3 top) GetCharacterControllerCapsulePoints(Vector3? atPosition = null);
     }
 
     public enum Immersion {
         NoImmersion,
         WalkingOnPuddle,
         WadingThroughFluid,
     }
 
     public delegate void ImmersionChangedDelegate(Immersion old, Immersion @new);
 
     public class GameCharacter : MonoBehaviour, IHasCharacterMotionState, ICanGetTripped {
 
         // CharacterController unityCharController;
         // CombatStanceController combatStanceController;
         // public CharacterState State { get; } = new CharacterState();
         
         public Camera WorldCamera { get; set;}
 
         public GameCharacterData Data { get; private set; }
 
         #pragma warning disable 649
         [SerializeField] private float walkSpeed;
         [SerializeField] private float runSpeed;
         [SerializeField] private float acceleration;
         [SerializeField] float rotateAdjustSpeed;
         [SerializeField] float combatModeSpeed;
         [SerializeField] float combatModeAcceleration;
         [SerializeField] SkeletalCharacter skeleton;
         [SerializeField] AnimationCurve accelerationVsVelocity;
 
         
         [Header("Slide")]
         [SerializeField] float slideDecelerationBonus;
 
         [SerializeField] float slideStartThreshold;
         [SerializeField] float slideEndThreshold;
         [SerializeField] float slideDotProductMinimum;
         [Header("Water interactions")]
         [SerializeField] float waterTesterShallow;
         [SerializeField] float waterTesterWadingDepth;
         [SerializeField] [Range(0.3f, 1f)]float wadeWalkSpeed;
         [SerializeField] float wadingAccel;
         [SerializeField] float wadingDecel;
 
         [Header("Footing (slide when near edge)")]
         [SerializeField] float footingRadius; // we raycast directly downward a small narrow capsule, fitting the character controller, but with this, smaller radius...
         [SerializeField] float footingForwardBias; // ... and offset this much in the direction of the possible slide (can be 0)...
         [SerializeField] float noFootingSlideForce; // ... and if nothing is below us, we apply this much "slipping" external force 
         #pragma warning restore 649
 
         public bool ContactWithGround => Data.UnityCharacterController.isGrounded;
         public bool IsRagdolling => movementAdaptsToRagdoll || skeleton.CurrentRagdollStrength > float.Epsilon;
         public bool InCombatStance => Data.StanceController.InCombatStance;
         public bool IsSliding { get; private set; }
         /// <summary> The character is slipping due to no footing.</summary>
         public bool IsSlipping { get; private set; }
 
         Vector3 lockOnWorldDirection;
         Vector3 desiredVelocity;
         Vector3 currentVelocity;
         bool previouslyInContact;
 
         // [Header("Autopilot")]
         private Vector3 autopilotTargetV;
         private float autopilotAccel;
 
 
         float currentReelFactor; // "Reeling" is temporary minor loss of velocity control
 
         private CollisionFlags currentCollisionFlags;
         //public bool MovementHijackedByAnimator { get; private set; }
 
         public MoveRegimes MoveRegime { get; private set; }
         public Immersion ImmersionLevel { get; private set; }
 
         public Vector3 ObservedVelocity => averageVelocity.GetAverage();
         public Vector2 RelativeVelocity { get; private set; }
         public Vector2 RelativeDesiredVelocity { get; private set; }
 
         private K3.Utility.RollingAverageVector3 averageVelocity = new K3.Utility.RollingAverageVector3(3);
 
         public event Action<float> JustHitGround;
         public event ImmersionChangedDelegate ImmersionChanged;
 
         bool movementAdaptsToRagdoll;
         Vector3 externalVelocityDelta;
         ICharacterInputCollector inputCollector;
         Collider[] physicsOverlapResults = new Collider[1];
 
         /// <summary>Happens when the character is executing a targeted approach, instead of responding to input axes</summary>
         public bool Autopilot { get; private set; }
 
         public CollisionFlags CurrentCollisionFlags { get; private set; }
 
         public Vector3 DesiredWorldspaceMotionAxis { get; private set; }
 
 
         private void Awake() {
             inputCollector = new CharacterInputCollector();
             Data = new GameCharacterData(this, gameObject);
             foreach (var c in GetComponentsInChildren<IHasCharacterReference>())
                 c.Character = Data;
             if (WorldCamera == null) WorldCamera = EMB.SceneObjects.MainGameCamera;
         }
 
 
         internal void ApplyExternalForce(Vector3 f) {
             externalVelocityDelta += f;
         }
 
         private void Update() {
             if (Data.UnityCharacterController == null) return;
 
             Data.Inputs = inputCollector.CollectInputs();
 
             if (Input.GetKeyDown(KeyCode.R) && Input.GetKey(KeyCode.LeftControl)) {
                 SetRagdollActive(!movementAdaptsToRagdoll);
                 var rb = skeleton.RagdollPelvis.GetComponent<Rigidbody>();
                 rb.velocity = rb.velocity / 2 + new Vector3(4f, -5f, 0f);
             }
 
             Data.State.Process();
 
             Data.Actor.DoInteractionEvaluation();
 
             bool eatInputs = EMB.GetModule<Modules.InventoryInteractionsModule>().HandleInputs(Data);
             if (eatInputs) inputCollector.InvalidateInputs();
 
             if (!eatInputs) { 
                 EMB.GetModule<Modules.CombatModule>().ProcessAttacks(Data);
                 EMB.GetModule<Modules.InteractionsModule>().ProcessInteractions(Data);
                 EMB.GetModule<Modules.GeneralCharacterActionsModule>().TryConvertInputsIntoActions(Data);
             }
 
             if (movementAdaptsToRagdoll) { 
                 UpdateRagdollMovement();
             } else {
                 UpdateControlledMovement();
             }
         }
 
 
         public void SetRagdollActive(bool active) {
             movementAdaptsToRagdoll = active;
             skeleton.FullRagdollMode = movementAdaptsToRagdoll;
 
             if (movementAdaptsToRagdoll) {
                 DropAllHeldItems();
                 skeleton.StartRagdoll(ObservedVelocity, 0.3f);
                 Data.UnityCharacterController.enabled = false;
             } else {
                 Data.UnityCharacterController.enabled = true;
             }
         }
 
         private void UpdateRagdollMovement() {
             if (skeleton.IsRagdollDetached) {
                 transform.position = skeleton.RagdollCenter;
                 var desiredForward = -skeleton.RagdollPelvis.right;
 
                 desiredForward = Vector3.ProjectOnPlane(desiredForward, Vector3.up);
                 transform.rotation = Quaternion.LookRotation(desiredForward, Vector3.up);
             }
         }
 
         private void FixedUpdate() {
 
             ProcessImmersion();
             UpdateVelocity();
             ProcessFooting();
             // you are expected to set the flag manually every tick if you want the behaviour to be continuous
             Autopilot = false;
             ProcessContactWithGround();
             previouslyInContact = ContactWithGround;
         }
 
         private void ProcessContactWithGround() {
             var velocityY = ObservedVelocity.y;
 
             if (ContactWithGround != previouslyInContact) {
                 if (ContactWithGround) {
                     if (velocityY < -0.0001f) {
                         JustHitGround?.Invoke(velocityY);
                         ProcessJustHitGround(velocityY);
                     }
                 }
             }
         }
 
         void ProcessFooting() {
             IsSlipping = false;
             // return;
             bool canSlip = ContactWithGround && (MoveRegime != MoveRegimes.AnimatorControlled);
 
             if (canSlip) {
                 //var slipDirection = ObservedVelocity;
                 //slipDirection = Vector3.ProjectOnPlane(slipDirection, Vector3.up);
                 //slipDirection.Normalize();
 
                 if (ShouldSlipOverEdge()) {
                     var slipDirection = GetSlipDirection();
                     if (slipDirection.HasValue) {
                         ApplyExternalForce(slipDirection.Value * noFootingSlideForce * Time.fixedDeltaTime);
                     }
                 }
             }
         }
 
         private Vector3? GetSlipDirection() {
             const int ANGLE_SAMPLES = 8;
             var r = new Ray(transform.position, Vector3.down);
             var resultant = Vector3.zero;
             var resultantFound = false;
             for (var i = 0; i < ANGLE_SAMPLES; i++) {
                 var angle = 360 * i / ANGLE_SAMPLES;
                 r.origin = transform.TransformPoint( Mathf.Cos(Mathf.Deg2Rad * angle), 0, Mathf.Sin(Mathf.Deg2Rad * angle)) + Vector3.up * 0.2f;
                 if (Physics.Raycast(r, 0.3f, 1 << 6)) {
                     resultant += transform.position - r.origin;
                     resultantFound = true;
                 }
             }
             if (resultantFound) return resultant;
             return default;
         }
 
         private bool ShouldSlipOverEdge() {
             var ray = new Ray(transform.position + transform.forward * footingForwardBias + Vector3.up * footingRadius, -Vector3.up);
             if (Physics.SphereCast(ray, footingRadius, out var hitinfo, 0.1f, 1 << 6)) return false;
             return true;
         }
 
         private void ProcessImmersion() {
             var old = ImmersionLevel;
             var level = Immersion.NoImmersion;
 
             var waterTestRadius = Data.UnityCharacterController.radius;
             if (Physics.OverlapSphereNonAlloc(transform.position + transform.up * waterTesterWadingDepth, waterTestRadius, physicsOverlapResults, 1 << 4, QueryTriggerInteraction.Collide) > 0) {
                 level = Immersion.WadingThroughFluid;
             } else if (Physics.OverlapSphereNonAlloc(transform.position + transform.up * waterTesterShallow, waterTestRadius, physicsOverlapResults, 1 << 4, QueryTriggerInteraction.Collide) > 0) {
                 level = Immersion.WalkingOnPuddle;
             }
             ImmersionLevel = level;
             if (ImmersionLevel != old) ImmersionChanged?.Invoke(old, ImmersionLevel);
         }
 
 
         /// <summary>There is no ragdoll</summary>
         private void UpdateControlledMovement() {
             if (Autopilot) {
                 AutopilotControls();
             } else {
                 HackAndSlashControls(Data.Inputs);
             }
         }
 
         private void ProcessJustHitGround(float vY) {
             var ferocity = vY * vY;
             if (ferocity > 0.1f) { 
                 currentVelocity.x /= (1f + ferocity * 0.3f);
                 currentVelocity.z /= (1f + ferocity * 0.3f);
                 currentReelFactor = ferocity.Map(0.1f, 10f, 0.1f, 1.2f, true);
                 // Debug.Log($"hit ground, ferocity = {ferocity:F}, reel = {currentReelFactor:F2}s");
             }
         }
 
         private void AutopilotControls() {
             var maxMoveSpeedInThisRegime = walkSpeed;
             if (ImmersionLevel == Immersion.WadingThroughFluid) maxMoveSpeedInThisRegime *= wadeWalkSpeed;
             var planarV = autopilotTargetV;
             planarV.y = 0;
             if (autopilotTargetV.magnitude > maxMoveSpeedInThisRegime)
                 autopilotTargetV = autopilotTargetV.normalized * maxMoveSpeedInThisRegime;
             desiredVelocity = autopilotTargetV;
             RotateToFaceHeading(desiredVelocity);
             currentCollisionFlags = Data.UnityCharacterController.Move(currentVelocity * Time.deltaTime);
         }
 
         private void HackAndSlashControls(CharacterInputState inputs) {
 
             if (!Data.UnityCharacterController.enabled) return;
             //if (Data.Inputs.defend)
             //    Data.StanceController.InCombatStance = !InCombatStance;
 
             var maxMoveSpeedInThisRegime = walkSpeed;
             if (inputs.atlethics.Holding()) maxMoveSpeedInThisRegime = runSpeed;
             if (InCombatStance) maxMoveSpeedInThisRegime = combatModeSpeed;
 
             if (ImmersionLevel == Immersion.WadingThroughFluid) maxMoveSpeedInThisRegime = wadeWalkSpeed;
 
             if (currentReelFactor > 0f) { maxMoveSpeedInThisRegime = 0f; currentReelFactor -= Time.deltaTime; }
             // if (MovementSpeedCap.HasValue && maxMoveSpeedInThisRegime > MovementSpeedCap.Value) maxMoveSpeedInThisRegime = MovementSpeedCap.Value;
 
             // the good news is that the principle for movement is the same regardless of 
             // combat mode or not.
             var worldCamHorRot = GetWorldCameraHorizontalRotation();
             var inputAxesRelative = new Vector3(inputs.moveAxis.vector.x, 0, inputs.moveAxis.vector.y);
             var m = inputAxesRelative.magnitude;
             if (m > 1f) inputAxesRelative.Normalize();
             const float DoNotMoveSlowerThanThisIfMovingAtAll = 0.4f;
             if (m < DoNotMoveSlowerThanThisIfMovingAtAll) inputAxesRelative = inputAxesRelative.normalized * DoNotMoveSlowerThanThisIfMovingAtAll;
 
             DesiredWorldspaceMotionAxis = worldCamHorRot * inputAxesRelative;
             
             desiredVelocity = DesiredWorldspaceMotionAxis * maxMoveSpeedInThisRegime;
 
             var planarWorldSpeed = Data.UnityCharacterController.velocity;
             planarWorldSpeed.y = 0f;
 
             HandleRotationBasedOnMoveRegime(planarWorldSpeed);
 
             CurrentCollisionFlags = Data.UnityCharacterController.Move(currentVelocity * Time.deltaTime);
         }
 
         private void HandleRotationBasedOnMoveRegime(Vector3 planarWorldSpeed) {
             switch (MoveRegime) {
                 case MoveRegimes.Free:
                     if (InCombatStance) {
                         lockOnWorldDirection = Data.StanceController.AbsoluteDirectionToTarget;
                         RotateToFaceHeading(lockOnWorldDirection);
                     } else {
                         if (planarWorldSpeed.sqrMagnitude >= 0.02f && !IsSliding)
                             RotateToFaceHeading(planarWorldSpeed.normalized);
                     }
                     break;
                 case MoveRegimes.AnimatorControlled:
                     break;
                 case MoveRegimes.AxesToFacing:
                 case MoveRegimes.AxesToFacingImmediate:
                     if (DesiredWorldspaceMotionAxis.sqrMagnitude > 0.1f) {
                         RotateToFaceHeading(DesiredWorldspaceMotionAxis.normalized, MoveRegime == MoveRegimes.AxesToFacingImmediate);
                     }
                     break;
                 default:
                     throw new NotImplementedException("Unhandled!");
             }
         }
 
         public void ForceRotateTowards(Vector3 worldHeading) {
             RotateToFaceHeading(worldHeading, false);
         }
 
         private void RotateToFaceHeading(Vector3 worldheading, bool immediate = false) {
             if (worldheading.sqrMagnitude < 0.01f) return;
 
             worldheading = Vector3.ProjectOnPlane(worldheading, Vector3.up);
             worldheading.Normalize();
             Quaternion targetRot = Quaternion.LookRotation(worldheading, Vector3.up);
 
             if (immediate) {
                 transform.rotation = targetRot;
             } else { 
                 Quaternion currentRot = transform.rotation;
                 var angle = Quaternion.Angle(currentRot, targetRot);
                 if (angle > float.Epsilon) {
                     transform.rotation = Quaternion.Slerp(currentRot, targetRot, rotateAdjustSpeed * Time.deltaTime / angle);
                 }
             }
         }
 
         public void OverrideDesiredVelocity(Vector3 target, float accel) {
             Autopilot = true;
             autopilotTargetV = target;
             autopilotAccel = accel;
             //UpdateVelocityTowardsDesiredVelocity(target, accel);
             //if (!ContactWithGround) currentVelocity += Physics.gravity * Time.fixedDeltaTime;
         }
 
         private void UpdateVelocity() {
             if (Data.UnityCharacterController.enabled) {
                 var effectiveAccelMagnitude = CalculateCurrentAcceleration();
                 bool moveHijacked = MoveRegime != MoveRegimes.Free;
                 if (moveHijacked) {
                     if (ContactWithGround) {
                         effectiveAccelMagnitude = acceleration * 2f;
                         UpdateVelocityTowardsDesiredVelocity(Vector3.zero, effectiveAccelMagnitude);
                     }
                 } else {
                     currentVelocity = Data.UnityCharacterController.velocity;
                     if (ContactWithGround) {
                         if (IsSliding) {
                             UpdateVelocityTowardsDesiredVelocity(Vector3.zero, effectiveAccelMagnitude, effectiveAccelMagnitude + slideDecelerationBonus); // ideally, dependent on the ground
                         } else {
                             float decelMagnitude = effectiveAccelMagnitude;
                             if (ImmersionLevel > Immersion.WalkingOnPuddle) decelMagnitude = wadingDecel;
                             UpdateVelocityTowardsDesiredVelocity(desiredVelocity, effectiveAccelMagnitude, decelMagnitude);
                         }
                     } else {
                         currentVelocity += Physics.gravity * Time.fixedDeltaTime; // there used to be a multiplier here
                     }
                 }
             } else {
                 if (movementAdaptsToRagdoll) {
                     // this.ObservedAbsoluteVelocity = this.currentPhysicsBasedVelocity
                     this.currentVelocity = skeleton.RagdollVelocity;
                     this.desiredVelocity = Vector3.zero;
                 } else {
                     this.currentVelocity = Vector3.zero;
                     this.desiredVelocity = Vector3.zero;
                 }
             }
 
             currentVelocity += externalVelocityDelta;
             externalVelocityDelta = default;
 
             // previouslyInContact = Data.UnityCharacterController.isGrounded;
             averageVelocity.Add(currentVelocity);
 
             var relativeV = transform.InverseTransformDirection(ObservedVelocity);
             RelativeVelocity = new Vector2(relativeV.x, relativeV.z);
             relativeV = transform.InverseTransformDirection(desiredVelocity);
             RelativeDesiredVelocity = new Vector2(relativeV.x, relativeV.z);
 
             UpdateSlide();
             if (ContactWithGround) currentVelocity.y = -1f; // stick to ground force
         }
 
         private float CalculateCurrentAcceleration() {
             var velocityNormalized = RelativeVelocity.magnitude / runSpeed;
             var accelMultiplierBasedOnVelocity = this.accelerationVsVelocity.Evaluate(velocityNormalized);
             var effectiveAccel = acceleration * accelMultiplierBasedOnVelocity;
             if (this.InCombatStance) effectiveAccel = combatModeAcceleration;
             if (this.ImmersionLevel == Immersion.WadingThroughFluid) effectiveAccel = wadingAccel;
             return effectiveAccel;
         }
 
         private void UpdateSlide() {
             IsSliding = false;
             if (!ContactWithGround) return;
 
             var v = ObservedVelocity.magnitude;
             var threshold = IsSliding ? slideEndThreshold : slideStartThreshold;
             
             if (RelativeDesiredVelocity.sqrMagnitude > 0.1f && v > threshold) {
                 var dot = Vector3.Dot(RelativeDesiredVelocity.normalized, new Vector2(0, 1));
                 // make sure there is no slide when rapidly changing direction at low speeds:
                 var effectiveSlideDotProductMinimum = slideDotProductMinimum * v.Map(walkSpeed, runSpeed, 0.1f, 1f, true);
 
                 IsSliding = dot < effectiveSlideDotProductMinimum; // very naive formula but okay
             }
         }
 
         private void UpdateVelocityTowardsDesiredVelocity(Vector3 targetV, float accel, float? decel = null) {
             if (!ContactWithGround) throw new System.Exception("Do not call this unless we are grounded!");
             if (!decel.HasValue) decel = accel;
 
             var planarV = Vector3.ProjectOnPlane(currentVelocity, Vector3.up);
             var decellerating = (targetV.magnitude < planarV.magnitude * 0.8f);
             
             planarV = Vector3.MoveTowards(planarV, targetV, Time.fixedDeltaTime * (decellerating ? decel.Value : accel)); // this can also result in antigravity lol
             currentVelocity = planarV;
         }
 
         private Quaternion GetWorldCameraHorizontalRotation() {
             var forward = WorldCamera.transform.forward;
             forward.y = 0;
             forward.Normalize();
             var rotation = Quaternion.LookRotation(forward, Vector3.up);
             return rotation;
         }
 
         void OnControllerColliderHit(ControllerColliderHit hit) {
             Rigidbody body = hit.collider.attachedRigidbody;
             if (hit.collider.gameObject.layer == 6)
                 TryRagdollOnWallSlam(hit.normal, hit.point);
 
             //dont move the rigidbody if the character is on top of it
             if (currentCollisionFlags == CollisionFlags.Below) return;
 
             if (body == null || body.isKinematic) return;
             body.AddForceAtPosition(Data.UnityCharacterController.velocity * 0.1f, hit.point, ForceMode.Impulse);
         }
 
         private void TryRagdollOnWallSlam(Vector3 normal, Vector3 hitPoint) {
             if (!IsRagdolling) {
                 var dot = Vector3.Dot(normal, ObservedVelocity.normalized);
                 var vertical = normal.y > 0.8f;
                 var ferocity = -dot * ObservedVelocity.sqrMagnitude;
                 if (!vertical) ferocity = -dot * RelativeVelocity.sqrMagnitude;
 
                 var relativeH = transform.InverseTransformPoint(hitPoint).y;
 
                 // arbitrary criteria
                 if (ferocity > runSpeed * 0.75f && relativeH > 0.45f && !vertical) {
                     SetRagdollActive(true);
                 } else if (ferocity > 8f && vertical) {
                     SetRagdollActive(true);
                 }
             }
             
         }
 
         public void SetRegime(MoveRegimes regime) {
             this.MoveRegime = regime;
         }
 
         public void TryTrip() {
             bool shouldTrip = this.ContactWithGround 
                 && !this.movementAdaptsToRagdoll 
                 && !this.InCombatStance 
                 && this.ObservedVelocity.magnitude > runSpeed * 0.9f;
             if (shouldTrip) {
                 SetRagdollActive(true);
             }
         }
 
         private void DropAllHeldItems() {
             var wielder = GetComponentInChildren<CombatSystem.IWielder>();
             foreach (var slot in new[] { Items.ItemWieldSlots.Primary, Items.ItemWieldSlots.Offhand }) {
                 var item = wielder.GetExistingItemInSlot(slot);
                 if (item != null) {
                     item.Data.DropInPlace();
                     wielder.HandleWieldingStopped(item.InSlot, item.Subindex);
                 }
             }
         }
 
         (Vector3 bottom, Vector3 top) IHasCharacterMotionState.GetCharacterControllerCapsulePoints(Vector3? atPosition) {
             var ucc = Data.UnityCharacterController;
             if (!atPosition.HasValue) atPosition = transform.position;
             var p0 = atPosition.Value + Vector3.up * (ucc.skinWidth + ucc.radius);
             return (p0, p0 + Vector3.up * (ucc.height + ucc.radius));
         }
 
         private void OnTriggerEnter(Collider other) {
             var trigger = other.GetComponent<IInteractiveTrigger>();
             if (trigger != null) trigger.StartInteraction(this);
         }
 
         private void OnTriggerStay(Collider other) {
             var trigger = other.GetComponent<IInteractiveTrigger>();
             if (trigger != null) trigger.ContinueInteraction(this);
         }
 
         private void OnTriggerExit(Collider other) {
             var trigger = other.GetComponent<IInteractiveTrigger>();
             if (trigger != null) trigger.EndInteraction(this);
         }
     }
 
 }

Embers of Ishtar

Brutal dungeon crawler - in intermittent development

Gallery: