using System.Text.Json.Nodes; namespace ShrimpleCharacterController; [Icon("nordic_walking")] public class ShrimpleCharacterController : Component { /// /// Manually update this by calling Move() or let it always be simulated /// [Property] [Group("Options")] public bool ManuallyUpdate { get; set; } = true; /// /// If pushing against a wall, scale the velocity based on the wall's angle (False is useful for NPCs that get stuck on corners) /// [Property] [Group("Options")] public bool ScaleAgainstWalls { get; set; } = true; [Sync] float _traceWidth { get; set; } = 16f; /// /// Width of our trace /// [Property] [Group("Trace")] [Range(1f, 64f, 1f, true, true)] public float TraceWidth { get => _traceWidth; set { _traceWidth = value; Bounds = BuildBounds(); _shrunkenBounds = Bounds.Grow(-SkinWidth); } } [Sync] float _traceHeight { get; set; } = 72f; /// /// Height of our trace /// [Property] [Group("Trace")] [Range(1f, 256f, 1f, true, true)] public float TraceHeight { get => _traceHeight; set { _traceHeight = value; Bounds = BuildBounds(); _shrunkenBounds = Bounds.Grow(-SkinWidth); } } /// /// Rotate the trace with the gameobject /// [Property] [Group("Trace")] public bool RotateWithGameObject { get; set; } = true; /// /// Use a cylinder trace instead of a box trace
/// [WARNING] This is a PHYSICAL TRACE, so it's more expensive than the normal box trace ///
[Property] [Group("Trace")] public bool CylinderTrace { get; set; } = false; /// /// Which tags it should ignore /// [Property] [Group("Trace")] public TagSet IgnoreTags { get; set; } = new TagSet(); /// /// Max amount of trace calls whenever the simulation doesn't reach its target (Slide and collide bounces) /// [Property] [Group("Trace")] [Range(1, 20, 1, true, true)] public int MaxBounces { get; set; } = 5; /// /// How fast you accelerate while on the ground (Units per second) /// [Property] [Group("Movement")] [Range(0f, 3000f, 10f, false)] [HideIf("GroundStickEnabled", false)] public float GroundAcceleration { get; set; } = 1000f; /// /// How fast you decelerate while on the ground (Units per second) /// [Property] [Group("Movement")] [Range(0f, 3000f, 10f, false)] [HideIf("GroundStickEnabled", false)] public float GroundDeceleration { get; set; } = 1500f; /// /// How fast you accelerate while in the air (Units per second) /// [Property] [Group("Movement")] [Range(0f, 3000f, 10f, false)] public float AirAcceleration { get; set; } = 300f; /// /// How fast you decelerate while in the air (Units per second) /// [Property] [Group("Movement")] [Range(0f, 3000f, 10f, false)] public float AirDeceleration { get; set; } = 0f; /// /// Do we ignore the friction of the surface you're standing on or not? /// [Property] [Group("Movement")] public bool IgnoreGroundSurface { get; set; } = false; /// /// Is this MoveHelper meant for horizontal grounded movement? (false = For flying or noclip) /// [Property] [Group("Movement")] public bool IgnoreZ { get; set; } = true; /// /// Do we ignore Z when it's near 0 (So that gravity affects you when not moving) /// [Property] [Title("Ignore Z When Zero")] [Group("Movement")] [HideIf("IgnoreZ", true)] public bool IgnoreZWhenZero { get; set; } = true; /// /// Tolerance from a 90° surface before it's considered a wall (Ex. Tolerance 1 = Between 89° and 91° can be a wall, 0.1 = 89.9° to 90.1°) /// [Group("Movement")] [Property] [Range(0f, 10f, 0.1f, false)] public float WallTolerance { get; set; } = 1f; /// /// Player feels like it's gripping walls too much? Try more Grip Factor Reduction! /// [Group("Movement")] [Property] [Range(1f, 10f, 0.1f, true)] public float GripFactorReduction { get; set; } = 1f; /// /// Stick the MoveHelper to the ground (IsOnGround will default to false if disabled) /// [FeatureEnabled("GroundStick")] [Property] public bool GroundStickEnabled { get; set; } = true; /// /// How steep terrain can be for you to stand on without slipping /// [Property] [Feature("GroundStick")] [Range(0f, 89f, 1f, true, true)] public float MaxGroundAngle { get; set; } = 60f; /// /// How far from the ground the MoveHelper is going to stick (Useful for going down stairs!) /// [Property] [Feature("GroundStick")] [Range(1f, 32f, 1f, false)] public float GroundStickDistance { get; set; } = 12f; /// /// Enable steps climbing (+1 Trace call) /// [FeatureEnabled("Steps")] [Property] public bool StepsEnabled { get; set; } = true; /// /// How high steps can be for you to climb on /// [Feature("Steps")] [Property] [Range(1f, 132f, 1f, false)] public float StepHeight { get; set; } = 12f; /// /// How deep it checks for steps (Minimum depth) /// [Feature("Steps")] [Property] [Range(0.1f, 8f, 0.1f, false)] public float StepDepth { get; set; } = 2f; /// /// Tolerance from a 90° surface before it's considered a valid step (Ex. Tolerance 1 = Between 89° and 91° can be a step, 0.1 = 89.9° to 90.1°) /// [Feature("Steps")] [Property] [Range(0f, 10f, 0.1f, false)] public float StepTolerance { get; set; } = 1f; /// /// Enable to ability to walk on a surface that's too steep if it's equal or smaller than a step (+1 Trace call when on steep terrain) /// [Feature("Steps")] [Property] public bool PseudoStepsEnabled { get; set; } = true; /// /// Instead of colliding with these tags the MoveHelper will be pushed away (Make sure the tags are in IgnoreTags as well!) /// [FeatureEnabled("Push")] [Property] public bool PushEnabled { get; set; } = false; [Sync] Dictionary _pushTagsWeight { get; set; } = new Dictionary() { { "player", 1f } }; /// /// Which tags will push this MoveHelper away and with how much force (Make sure they are also included in IgnoreTags!) (+1 Trace call) /// [Property] [Feature("Push")] public Dictionary PushTagsWeight { get => _pushTagsWeight; set { _pushTagsWeight = value; _pushTags = BuildPushTags(); } } /// /// Apply gravity to this MoveHelper when not on the ground /// [FeatureEnabled("Gravity")] [Property] public bool GravityEnabled { get; set; } = true; private bool _useSceneGravity = true; /// /// Use the scene's gravity or our own /// [Property] [Feature("Gravity")] public bool UseSceneGravity { get => _useSceneGravity; set { _useSceneGravity = value; _appliedGravity = BuildGravity(); } } private bool _useVectorGravity = false; /// /// Use a Vector3 gravity instead of a single float (Use this if you want to use a custom gravity) /// [Property] [Feature("Gravity")] [HideIf("UseSceneGravity", true)] public bool UseVectorGravity { get => _useVectorGravity; set { _useVectorGravity = value; _appliedGravity = BuildGravity(); } } private bool _usingFloatGravity => !UseVectorGravity && !UseSceneGravity; private bool _usingVectorGravity => UseVectorGravity && !UseSceneGravity; private float _gravity = -850f; /// /// Units per second squared (Default is -850f) /// [Property] [Feature("Gravity")] [Range(-2000, 2000, 1, false)] [ShowIf("_usingFloatGravity", true)] public float Gravity { get => _gravity; set { _gravity = value; _appliedGravity = BuildGravity(); } } private Vector3 _vectorGravity = new Vector3(0f, 0f, -850f); /// /// Units per second squared (Default is 0f, 0f, -850f)
/// Changes which way sticks to the ground ///
[Property] [Feature("Gravity")] [ShowIf("_usingVectorGravity", true)] public Vector3 VectorGravity { get => _vectorGravity; set { _vectorGravity = value; _appliedGravity = BuildGravity(); } } private Vector3 _appliedGravity; public Vector3 AppliedGravity => _appliedGravity; /// /// Check if the MoveHelper is stuck and try to get it to unstuck (+Trace calls if stuck) /// [FeatureEnabled("Unstuck")] [Property] public bool UnstuckEnabled { get; set; } = true; /// /// How many trace calls it will attempt to get the MoveHelper unstuck /// [Property] [Feature("Unstuck")] [Range(1, 50, 1, false)] public int MaxUnstuckTries { get; set; } = 20; /// /// The simulated target velocity for our MoveHelper (Units per second, we apply Time.Delta inside) /// [Sync] public Vector3 WishVelocity { get; set; } /// /// The resulting velocity after the simulation is done (Units per second) /// [Sync] public Vector3 Velocity { get; set; } /// /// Is the MoveHelper currently touching the ground /// [Sync] public bool IsOnGround { get; set; } /// /// The current ground normal you're standing on (Always Vector3.Zero if IsOnGround false) /// public Vector3 GroundNormal { get; private set; } = Vector3.Zero; public Vector3 Up { get; set; } = Vector3.Up; /// /// The current ground angle you're standing on (Always 0f if IsOnGround false) /// public float GroundAngle => Vector3.GetAngle(GroundNormal, Up); /// /// The current surface you're standing on /// public Surface GroundSurface { get; private set; } /// /// The gameobject you're currently standing on /// public GameObject GroundObject { get; set; } /// /// Is the MoveHelper currently pushing against a wall /// public bool IsPushingAgainstWall { get; private set; } /// /// The current wall normal you're pushing against (Always Vector3.Zero if IsPushingAgainstWall false) /// public Vector3 WallNormal { get; private set; } = Vector3.Zero; /// /// The gameobject you're currently pushing on /// public GameObject WallObject { get; set; } /// /// Is the MoveHelper standing on a terrain too steep to stand on (Always false if IsOnGround false) /// [Sync] public bool IsSlipping { get; private set; } // TODO IMPLEMENT /// /// The MoveHelper is stuck and we can't get it out /// [Sync] public bool IsStuck { get; private set; } /// /// To avoid getting stuck due to imprecision we shrink the bounds before checking and compensate for it later /// public float SkinWidth; public float AppliedWidth => TraceWidth / 2f * WorldScale.x; // The width of the MoveHelper in world units public float AppliedDepth => TraceWidth / 2f * WorldScale.y; // The depth of the MoveHelper in world units public float AppliedHeight => TraceHeight / 2f * WorldScale.z; // The height of the MoveHelper in world units private Vector3 _offset => (RotateWithGameObject ? WorldRotation.Up : Vector3.Up) * AppliedHeight; // The position of the MoveHelper in world units /// /// The bounds of this MoveHelper generated from the TraceWidth and TraceHeight /// public BBox Bounds { get; set; } private BBox _shrunkenBounds; private string[] _pushTags; private Vector3 _lastVelocity; /// /// If another MoveHelper moved at the same time and they're stuck, let this one know that the other already unstuck for us /// public ShrimpleCharacterController UnstuckTarget; public override int ComponentVersion => 1; protected override void OnStart() { SkinWidth = Math.Min(Math.Max(0.1f, TraceWidth * 0.05f), GroundStickDistance); // SkinWidth is 5% of the total width Bounds = BuildBounds(); _shrunkenBounds = Bounds.Grow(-SkinWidth); _pushTags = BuildPushTags(); } protected override void DrawGizmos() { if (Gizmo.IsSelected) { Gizmo.GizmoDraw draw = Gizmo.Draw; draw.Color = Color.Blue; var bounds = BuildBounds(); if (CylinderTrace) draw.LineCylinder(Vector3.Zero, WorldRotation.Up * (bounds.Maxs.z - bounds.Mins.z), bounds.Maxs.x, bounds.Maxs.x, 24); else draw.LineBBox(bounds.Translate(Vector3.Up * TraceHeight / 2f * GameObject.WorldScale.z)); } } private BBox BuildBounds() { var x = GameObject.WorldScale.x; var y = GameObject.WorldScale.y; var z = GameObject.WorldScale.z; var width = TraceWidth / 2f * x; var depth = TraceWidth / 2f * y; var height = TraceHeight / 2f * z; return new BBox(new Vector3(-width, -depth, -height), new Vector3(width, depth, height)); } private Vector3 BuildGravity() => UseSceneGravity ? Scene.PhysicsWorld.Gravity : UseVectorGravity ? VectorGravity : new Vector3(0f, 0f, Gravity); private string[] BuildPushTags() { return PushTagsWeight.Keys.ToArray(); } /// /// Casts the current bounds from to and returns the scene trace result /// /// /// /// /// public SceneTraceResult BuildTrace(BBox bounds, Vector3 from, Vector3 to) { SceneTrace builder = new SceneTrace(); // Empty trace builder if (CylinderTrace) builder = Game.SceneTrace.Cylinder(bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to); else builder = Game.SceneTrace.Box(bounds, from, to); builder = builder .IgnoreGameObjectHierarchy(GameObject) .WithoutTags(IgnoreTags); if (RotateWithGameObject) builder = builder.Rotated(GameObject.WorldRotation); return builder.Run(); } private SceneTraceResult BuildPushTrace(BBox bounds, Vector3 from, Vector3 to) { SceneTrace builder = new SceneTrace(); // Empty trace builder if (CylinderTrace) builder = Game.SceneTrace.Cylinder(bounds.Maxs.z - bounds.Mins.z, bounds.Maxs.x, from, to); else builder = Game.SceneTrace.Box(bounds, from, to); builder = builder .IgnoreGameObjectHierarchy(GameObject) .WithAnyTags(_pushTags); // Check for only the push tags if (RotateWithGameObject) builder = builder.Rotated(GameObject.WorldRotation); return builder.Run(); } /// /// Detach the MoveHelper from the ground and launch it somewhere (Units per second) /// /// public void Punch(in Vector3 amount) { IsOnGround = false; Velocity += amount; } /// /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper /// /// Just calculate but don't update position public MoveHelperResult Move(bool manualUpdate = false) => Move(Time.Delta, manualUpdate); /// /// Apply the WishVelocity, update the Velocity and the Position of the GameObject by simulating the MoveHelper /// /// The time step /// Just calculate but don't update position public MoveHelperResult Move(float delta, bool manualUpdate = false) { var goalVelocity = CalculateGoalVelocity(delta); // Calculate the goal velocity using our Acceleration and Deceleration values // KNOWN ISSUE: Velocity starts to build up to massive amounts when trying to climb terrain too steep? // SIMULATE PUSH FORCES // if (PushEnabled) { var pushTrace = BuildPushTrace(Bounds, WorldPosition + _offset, WorldPosition); // Build a trace but using the Push tags instead of the Ignore tags if (pushTrace.Hit) // We're inside any of the push tags { foreach (var tag in pushTrace.GameObject.Tags) { if (PushTagsWeight.TryGetValue(tag, out var tagWeight)) { var otherPosition = pushTrace.GameObject.WorldPosition.WithZ(WorldPosition.z); // Only horizontal pushing var pushDirection = (otherPosition - WorldPosition).Normal; var pushVelocity = pushDirection * tagWeight * 50f; // I find 50 u/s to be a good amount to push if the weight is 1.0 (!!!) goalVelocity -= pushVelocity; } } } } var moveHelperResult = CollideAndSlide(goalVelocity, WorldPosition + _offset, delta); // Simulate the MoveHelper var finalPosition = moveHelperResult.Position; var finalVelocity = moveHelperResult.Velocity; // SIMULATE GRAVITY // if (GravityEnabled && Gravity != 0f) { if (!IsOnGround || IsSlipping || !GroundStickEnabled) { var gravity = AppliedGravity * delta; var gravityResult = CollideAndSlide(gravity, moveHelperResult.Position, delta, gravityPass: true); // Apply and simulate the gravity step finalPosition = gravityResult.Position; finalVelocity += gravityResult.Velocity; } } _lastVelocity = Velocity * delta; if (!manualUpdate) { Velocity = finalVelocity; WorldPosition = finalPosition - _offset; // Actually updating the position is "expensive" so we only do it once at the end } return new MoveHelperResult(finalPosition, finalVelocity); } /// /// Sometimes we have to update only the position but not the velocity (Like when climbing steps or getting unstuck) so we can't have Position rely only on Velocity /// public struct MoveHelperResult { public Vector3 Position; public Vector3 Velocity; public MoveHelperResult(Vector3 position, Vector3 velocity) { Position = position; Velocity = velocity; } } private MoveHelperResult CollideAndSlide(Vector3 velocity, Vector3 position, float delta, int depth = 0, bool gravityPass = false) => CollideAndSlide(new MoveHelperResult(position, velocity), delta, depth, gravityPass); private MoveHelperResult CollideAndSlide(MoveHelperResult current, float delta, int depth = 0, bool gravityPass = false) { if (depth >= MaxBounces) return current; var velocity = current.Velocity * delta; // I like to set Velocity as units/second but we have to deal with units/tick here var position = current.Position; // GROUND AND UNSTUCK CHECK // if (depth == 0) // Only check for the first step since it's impossible to get stuck on other steps { var groundTrace = BuildTrace(_shrunkenBounds, position, position + AppliedGravity.Normal * (GroundStickDistance + SkinWidth * 1.1f)); // Compensate for floating inaccuracy if (groundTrace.StartedSolid) { IsStuck = true; if (UnstuckEnabled) { if (UnstuckTarget == null) { IsStuck = !TryUnstuck(position, out var result); if (!IsStuck) { position = result; // Update the new position if (groundTrace.GameObject != null) if (groundTrace.GameObject.Components.TryGet(out var otherHelper)) otherHelper.UnstuckTarget = this; // We already solved this, no need to unstuck the other helper } else { return new MoveHelperResult(position, Vector3.Zero); // Mission failed, bail out! } } else { UnstuckTarget = null; // Alright the other MoveHelper got us unstuck so just do nothing } } } else { var hasLanded = !IsOnGround && Vector3.Dot(Velocity, AppliedGravity) >= 0f && groundTrace.Hit && groundTrace.Distance <= SkinWidth * 2f; // Wasn't on the ground and now is var isGrounded = IsOnGround && groundTrace.Hit; // Was already on the ground and still is, this helps stick when going down stairs IsOnGround = hasLanded || isGrounded; GroundSurface = IsOnGround ? groundTrace.Surface : null; GroundNormal = IsOnGround ? groundTrace.Normal : -AppliedGravity.Normal; GroundObject = IsOnGround ? groundTrace.GameObject : null; IsSlipping = IsOnGround && GroundAngle > MaxGroundAngle; if (IsSlipping && !gravityPass && Vector3.Dot(velocity, AppliedGravity) < 0f) velocity = velocity.WithZ(0f); // If we're slipping ignore any extra velocity we had if (IsOnGround && GroundStickEnabled && !IsSlipping) { position = groundTrace.EndPosition + -AppliedGravity.Normal * SkinWidth; // Place on the ground velocity = Vector3.VectorPlaneProject(velocity, GroundNormal); // Follow the ground you're on without projecting Z } IsStuck = false; } } if (velocity.IsNearlyZero(0.01f)) // Not worth continuing, reduces small stutter { return new MoveHelperResult(position, Vector3.Zero); } var toTravel = velocity.Length + SkinWidth; var targetPosition = position + velocity.Normal * toTravel; var travelTrace = BuildTrace(_shrunkenBounds, position, targetPosition); if (travelTrace.Hit) { var travelled = velocity.Normal * Math.Max(travelTrace.Distance - SkinWidth, 0f); var leftover = velocity - travelled; // How much leftover velocity still needs to be simulated var angle = Vector3.GetAngle(-AppliedGravity.Normal, travelTrace.Normal); if (toTravel >= SkinWidth && travelTrace.Distance < SkinWidth) travelled = Vector3.Zero; if (angle <= MaxGroundAngle) // Terrain we can walk on { if (gravityPass || !IsOnGround) leftover = Vector3.VectorPlaneProject(leftover, travelTrace.Normal); // Don't project the vertical velocity after landing else it boosts your horizontal velocity else leftover = leftover.ProjectAndScale(travelTrace.Normal); // Project the velocity along the terrain IsPushingAgainstWall = false; WallObject = null; } else { var climbedStair = false; if (angle >= 90f - WallTolerance && angle <= 90f + WallTolerance) // Check for walls IsPushingAgainstWall = true; // We're pushing against a wall if (true) //StepsEnabled { var isStep = angle >= 90f - StepTolerance && angle <= 90f + StepTolerance; // Log.Info($"Step? {isStep} — Angle: {angle:F2}, StepTolerance: {StepTolerance}"); // Log.Info($"Step detected: angle={angle}, isStep={isStep}, PseudoStepsEnabled={PseudoStepsEnabled}"); if (isStep || PseudoStepsEnabled) { if (IsOnGround) { var stepHorizontal = Vector3.VectorPlaneProject(velocity, AppliedGravity).Normal * StepDepth; var stepVertical = -AppliedGravity.Normal * (StepHeight + SkinWidth); var stepStart = travelTrace.EndPosition + stepHorizontal + stepVertical; var stepEnd = travelTrace.EndPosition + stepHorizontal; // Log.Info($"StepTrace start: {stepStart}, end: {stepEnd}"); var stepTrace = BuildTrace(_shrunkenBounds, stepStart, stepEnd); // Gizmo.Draw.Arrow(stepStart, stepEnd); var stepAngle = Vector3.GetAngle(stepTrace.Normal, -AppliedGravity.Normal); // Log.Info($"StepTrace hit: {stepTrace.Hit}, angle: {stepAngle:F2}, startedSolid: {stepTrace.StartedSolid}"); if (!stepTrace.StartedSolid && stepTrace.Hit && stepAngle <= MaxGroundAngle) { if (isStep || (!IsSlipping && PseudoStepsEnabled)) { var stepHeight = Vector3.Dot(stepTrace.EndPosition - travelTrace.EndPosition, Up); var absStepHeight = Math.Abs(stepHeight); if(absStepHeight >= stepHeight) { var stepTravelled = Up * absStepHeight; position += stepTravelled; climbedStair = true; IsPushingAgainstWall = false; WallObject = null; } // Log.Info($"Climbed stair! Height: {absStepHeight:F4}, New Pos: {position}"); } } } } } if (IsPushingAgainstWall) { // Scale our leftover velocity based on the angle of approach relative to the wall // (Perpendicular = 0%, Parallel = 100%) var scale = ScaleAgainstWalls ? 1f - Vector3.Dot(-travelTrace.Normal.Normal / GripFactorReduction, velocity.Normal) : 1f; var wallLeftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject(leftover, travelTrace.Normal.Normal) : leftover.ProjectAndScale(travelTrace.Normal.Normal); leftover = (wallLeftover * scale).WithZ(wallLeftover.z); WallObject = travelTrace.GameObject; WallNormal = travelTrace.Normal; } else { if (!climbedStair) { var scale = IsSlipping ? 1f : 1f - Vector3.Dot(-travelTrace.Normal / GripFactorReduction, velocity.Normal); leftover = ScaleAgainstWalls ? Vector3.VectorPlaneProject(leftover, travelTrace.Normal) * scale : leftover.ProjectAndScale(travelTrace.Normal); } } } if (travelled.Length <= 0.01f && leftover.Length <= 0.01f) return new MoveHelperResult(position + travelled, travelled / delta); var newResult = CollideAndSlide(new MoveHelperResult(position + travelled, leftover / delta), delta, depth + 1, gravityPass); // Simulate another bounce for the leftover velocity from the latest position var currentResult = new MoveHelperResult(newResult.Position, travelled / delta + newResult.Velocity); // Use the new bounce's position and combine the velocities return currentResult; } if (depth == 0 && !gravityPass) { IsPushingAgainstWall = false; WallObject = null; } return new MoveHelperResult(position + velocity, velocity / delta); // We didn't hit anything? Ok just keep going then :-) } private float CalculateGoalSpeed(Vector3 wishVelocity, Vector3 velocity, Vector3 surfaceNormal, bool isAccelerating, float delta) { Vector3 wishDir = Vector3.VectorPlaneProject(wishVelocity, surfaceNormal).Normal; Vector3 currentDir = Vector3.VectorPlaneProject(velocity, surfaceNormal).Normal; bool isSameDirection = velocity.IsNearlyZero(1f) || Vector3.Dot(wishDir, currentDir) >= 0f; float acceleration = IsOnGround ? GroundAcceleration : AirAcceleration; float deceleration = IsOnGround ? GroundDeceleration : AirDeceleration; float goalSpeed = isAccelerating ? acceleration : (!isSameDirection ? Math.Max(acceleration, deceleration) : deceleration); if (!IgnoreGroundSurface && GroundSurface != null) goalSpeed *= GroundSurface.Friction; return goalSpeed * delta; } private Vector3 CalculateGoalVelocity(float delta) { Vector3 surfaceNormal = Up; var wishVelocityProjected = Vector3.VectorPlaneProject(WishVelocity, surfaceNormal); var velocityProjected = Vector3.VectorPlaneProject(Velocity, surfaceNormal); bool isAccelerating = wishVelocityProjected.Length >= velocityProjected.Length; float goalSpeed = CalculateGoalSpeed(wishVelocityProjected, Velocity, surfaceNormal, isAccelerating, delta); Vector3 goalVelocity = Velocity.MoveTowards(wishVelocityProjected, goalSpeed); return goalVelocity; } public bool TryUnstuck(Vector3 position, out Vector3 result) { if (_lastVelocity == Vector3.Zero) _lastVelocity = -AppliedGravity.Normal; var velocityLength = _lastVelocity.Length + SkinWidth; var startPos = position - _lastVelocity.Normal * velocityLength; // Try undoing the last velocity 1st var endPos = position; for (int i = 0; i < MaxUnstuckTries + 1; i++) { if (i == 1) startPos = position + -AppliedGravity.Normal * 2f; // Try going up 2nd if (i > 1) startPos = position + Vector3.Random.Normal * ((float)i / 2f); // Start randomly checking 3rd if (startPos - endPos == Vector3.Zero) // No difference! continue; var unstuckTrace = BuildTrace(_shrunkenBounds, startPos, endPos); if (!unstuckTrace.StartedSolid) { result = unstuckTrace.EndPosition - _lastVelocity.Normal * SkinWidth / 4f; _lastVelocity = Vector3.Zero; return true; } } result = position; return false; } /// /// Debug don't use /// /// /// /// private bool TestPosition(Vector3 position, string title) { var testTrace = BuildTrace(_shrunkenBounds, position, position); if (testTrace.StartedSolid) { Log.Info($"[{RealTime.Now}]{title} {GameObject.Name} started solid at {position} against {testTrace.GameObject}"); return true; } return false; } protected override void OnFixedUpdate() { base.OnFixedUpdate(); if (!ManuallyUpdate && Active) Move(); } [JsonUpgrader(typeof(ShrimpleCharacterController), 1)] private static void FloatGravityUpgrader(JsonObject json) { json.Remove("Gravity", out var newNode); json["_gravity"] = newNode; } }