private static void PM_FlyDetermineImmediateGeometry( ref Vector3 _vel, ref Vector3 _lastplane, Vector3 _plane, ref int _gflags) { switch (_gflags) { case 0: // plane detected PM_FlyClipVelocity(ref _vel, _plane); _gflags |= (1 << 0); break; case (1 << 0): // potential crease detected if (Mathf.Abs(VectorHeader.Dot(_lastplane, _plane)) < FLY_CREASE_EPSILON) { Vector3 _c = Vector3.Cross(_lastplane, _plane); _c.Normalize(); VectorHeader.ProjectVector(ref _vel, _c); _gflags |= (1 << 1); } else { PM_FlyClipVelocity(ref _vel, _plane); } break; case (1 << 0) | (1 << 1): // corner detected _vel = Vector3.zero; _gflags |= (1 << 2); break; } _lastplane = _plane; }
public static void PM_FlyClipVelocity(ref Vector3 _velocity, Vector3 _plane) { float _m = _velocity.magnitude; if (_m <= 0F) // preventing NaN generation { return; } else if (VectorHeader.Dot(_velocity / _m, _plane) < 0F) // only clip if we're piercing into the infinite plane { VectorHeader.ClipVector(ref _velocity, _plane); } }
// The velocity 'clipping' algorithm that is ran any time a plane is detected throughout // the PM_SlideMove() func execution. // It is responsible for: // Handling velocity orientation along stable planes // Handling velocity clipping along unstable 'wall' planes public static void PM_SlideClipVelocity( ref Vector3 _velocity, bool _stability, Vector3 _plane, bool _groundstability, Vector3 _groundplane, Vector3 _up) { float _m = _velocity.magnitude; if (_m <= 0F) // preventing NaN generation { return; } else { if (VectorHeader.Dot(_velocity / _m, _plane) < 0F) // only clip if we're piercing into the infinite plane { if (_stability) // if stable, just orient and maintain magnitude { // anyways just orient along the newly discovered stable plane //VectorHeader.CrossProjection(ref _velocity, _up, _groundplane); VectorHeader.ClipVector(ref _velocity, _plane); } else { if (_groundstability) // clip along the surface of the ground { // clip normally VectorHeader.ClipVector(ref _velocity, _plane); // orient velocity to ground plane VectorHeader.CrossProjection(ref _velocity, _up, _groundplane); // i'd originally used this but when orienting velocities above certain planes, // issues would arise where velocities would be clipped and projected in the opposite // direction of where the character should be moving, so I'm resorting to orienting // in this particular scenario.... // VectorHeader.ClipVector(ref _vel, _groundplane); } else // wall clip { VectorHeader.ClipVector(ref _velocity, _plane); } } } else { return; } } }
public override void Simulate(ActorArgs _args) { ActorHeader.Actor Actor = _args.Actor; ActorHeader.GroundHit Ground = Actor.Ground; ActorHeader.GroundHit LastGround = Actor.LastGround; Vector3 Velocity = Actor._velocity; Vector3 Wish = _args.ViewWishDir; bool Grounded = Actor.SnapEnabled && Ground.stable; if (Grounded && !LastGround.stable) // Landing { VectorHeader.ClipVector(ref Velocity, Ground.normal); } // Orient Wish Velocity to grounding plane if (Grounded && OrientVelocityToGroundPlane) { VectorHeader.CrossProjection(ref Wish, new Vector3(0, 1, 0), Ground.normal); } else { // Clip Wish Velocity along upward plane if we're not orienting/stable as we may be able to fight gravity if not done VectorHeader.ClipVector(ref Wish, new Vector3(0, 1, 0)); Wish.Normalize(); } //if (Grounded) // Subtract max speed based on stability // BehaviourHeader.DetermineWishVelocity(ref Velocity, Wish, MaximumGroundMoveSpeed, GroundAcceleration * GlobalTime.FDT); //else // BehaviourHeader.DetermineWishVelocity(ref Velocity, Wish, MaximumAirMoveSpeed, AirAcceleration * GlobalTime.FDT); if (Grounded) { BehaviourHeader.ApplyAcceleration(ref Velocity, Wish, MaximumGroundMoveSpeed, GroundAcceleration); } else { BehaviourHeader.ApplyAcceleration(ref Velocity, Wish, MaximumAirMoveSpeed, AirAcceleration); } Actor.SetVelocity(Velocity); return; }
// old acceleration code, not as desireable atm public static void DetermineWishVelocity(ref Vector3 _velocity, Vector3 _wish, float _maxspeed, float _accelspeed) { _velocity += (_wish * _accelspeed); float _vm = _velocity.magnitude; if (_vm <= 0F) { return; } float _newspeed = VectorHeader.Dot(_velocity, _wish); if (_newspeed > _maxspeed) { // Trim (circle strafe) _velocity -= (_wish) * (_newspeed - _maxspeed); } }
// Straight from Q1. // https://github.com/id-Software/Quake/blob/master/QW/client/pmove.c // Lines 412-434 public static void ApplyAcceleration(ref Vector3 _velocity, Vector3 _wish, float _maxspeed, float _accel) { float _addspeed, _accelspeed, _currentspeed; _currentspeed = VectorHeader.Dot(_velocity, _wish); _addspeed = _maxspeed - _currentspeed; if (_addspeed <= 0) // we're traveling further than we need to { return; } _accelspeed = (_maxspeed * _accel * GlobalTime.FDT); // if acceleration delta is greater than our maximum, cap it. if (_accelspeed > _addspeed) { _accelspeed = _addspeed; } _velocity += _wish * _accelspeed; }
// This func is vital to preventing undesirable behaviour throughout the lifetime of the // PM_SlideMove() execution loop. This function is responsible for identifying the geometry around // an actor's position throughout the duration of the move. // It is responsible for: // Handling generic velocity clipping // Handling generic crease projecting // Preventing tunneling at corners/creases at any point in our movement. private static void PM_SlideDetermineImmediateGeometry( ref Vector3 _vel, ref Vector3 _lastplane, bool _stability, Vector3 _plane, Vector3 _groundplane, bool _groundstability, Vector3 _up, ref int _gflags) { switch (_gflags) { case 0: // plane detected PM_SlideClipVelocity(ref _vel, _stability, _plane, _groundstability, _groundplane, _up); _gflags |= (1 << 0); break; case (1 << 0): // potential crease detected float _od = Mathf.Abs(VectorHeader.Dot(_lastplane, _plane)); if (!_stability && _od < FLY_CREASE_EPSILON) { Vector3 _c2 = Vector3.Cross(_lastplane, _plane); _c2.Normalize(); VectorHeader.ProjectVector(ref _vel, _c2); _gflags |= (1 << 1); } else { PM_SlideClipVelocity(ref _vel, _stability, _plane, _groundstability, _groundplane, _up); } break; case (1 << 0) | (1 << 1): // multiple creases detected _vel = Vector3.zero; _gflags |= (1 << 2); break; } _lastplane = _plane; }
public override void Simulate(ActorArgs _args) { ActorHeader.Actor Actor = _args.Actor; ActorHeader.GroundHit Ground = Actor.Ground; Vector3 Forward = _args.ActorView.forward; Vector3 Wish = _args.ViewWishDir; bool Grounded = Actor.SnapEnabled && Ground.stable; bool DashRequest = (GlobalTime.T - DashRequestSnapshot) > 0.1F && DashStamina >= DashCost && (_args.ActionFlags & (1 << 3)) != 0; if (DashRequest) // valid dash { bool UseViewDir = Wish.sqrMagnitude == 0; // Completely eradicate all velocity Actor.SetVelocity(Vector3.zero); DashStamina -= DashCost; Vector3 DashDirection = UseViewDir ? Forward : Wish; VectorHeader.ClipVector(ref DashDirection, new Vector3(0, 1, 0)); DashDirection.Normalize(); DashRequestSnapshot = GlobalTime.T; _args.AssignHold(ApplyDashDuration); Actor.SetVelocity(DashDirection * DashVelocityDelta); DashEvents?.Invoke(_args, DashState.Enter); } else if (Grounded) // If we aren't actually dashing, let's rejuvenate our stamina if we're grounded. { DashStamina += DashGrowthRate * GlobalTime.FDT; DashStamina = Mathf.Min(DashStamina, DashCap); } }
// PM_SlideMove() is one of the several variant Move() funcs available standard with the // Actor package provided. It's entire purpose is to 'slide' and 'snap' the Actor on 'stable' // surfaces whilst also dealing with the conventional issue of movement into and along blocking // planes in the physics scene. Use this method primarily if you plan on keeping your actor level // with the floor. public static void PM_SlideMove( IActorReceiver _rec, Actor _actor, ref Vector3 _pos, ref Vector3 _vel, Quaternion _orient, LayerMask _filter, float _fdt) { /* BASE CASES IN WHICH WE SHOULDN'T MOVE AT ALL */ if (_rec == null) { return; } /* STEPS: * RUN: * GROUND TRACE & GROUND SNAP -> OVERLAP -> PUSHBACK -> CONVEX HULL NORMAL (NEARBY PLANE DETECTION) -> GENERATE GEOMETRY BITMASK -> TRACING -> REPEAT */ ArchetypeHeader.Archetype _arc = _actor.GetArchetype(); SlideSnapType _snaptype = _actor.SnapType; Collider[] _colliders = _actor.Colliders; Collider _self = _arc.Collider(); Vector3[] _normals = _actor.Normals; RaycastHit[] _traces = _actor.Hits; Vector3 _tracepos = _pos; Vector3 _groundtracepos = _pos; Vector3 _lastplane = Vector3.zero; Vector3 _groundtracedir = _orient * new Vector3(0, -1, 0); Vector3 _up = _orient * new Vector3(0, 1, 0); float _tf = 1F; float _skin = ArchetypeHeader.GET_SKINEPSILON(_arc.PrimitiveType()); float _bias = ArchetypeHeader.GET_TRACEBIAS(_arc.PrimitiveType()); int _bumpcount = 0; int _groundbumpcount = 0; int _pushbackcount = 0; int _gflags = 0; GroundHit _ground = _actor.Ground; GroundHit _lastground = _actor.LastGround; _lastground.actorpoint = _ground.actorpoint; _lastground.normal = _ground.normal; _lastground.point = _ground.point; _lastground.stable = _ground.stable; _lastground.snapped = _ground.snapped; _lastground.distance = _ground.distance; _ground.Clear(); float _groundtracelen = (_lastground.stable && _lastground.snapped) ? 0.1F : 0.05F; while (_groundbumpcount++ < MAX_GROUNDBUMPS && _groundtracelen > 0F) { // trace along dir // if detected // if stable : // end trace and determine whether a snap is to occur // else : // clip along floor // continue // else : // break out of loop as no floor was detected _arc.Trace(_groundtracepos + (_up * 0.01F), _groundtracedir, _groundtracelen, _orient, _filter, 0F, QueryTriggerInteraction.Ignore, _traces, out int _groundtraces); ArchetypeHeader.TraceFilters.FindClosestFilterInvalids( ref _groundtraces, out int _i0, _bias, _self, _traces); if (_i0 >= 0) // an intersection has occured, but we aren't sure its ground yet { RaycastHit _closest = _traces[_i0]; _ground.distance = _closest.distance; _ground.normal = _closest.normal; _ground.actorpoint = _groundtracepos; _ground.stable = _actor.DetermineGroundStability(_vel, _closest, _filter); _groundtracepos += _groundtracedir * (_closest.distance); // warp regardless of stablility. We'll only be setting our trace position // to our ground trace position if a stable floor has been determined, and snapping is enabled. if (_ground.stable) { bool _cansnap = _snaptype == SlideSnapType.Always; switch (_snaptype) { case SlideSnapType.Never: _cansnap = false; break; case SlideSnapType.Toggled: _cansnap = _actor.SnapEnabled; break; } if (_cansnap) { _ground.snapped = true; } _rec.OnGroundHit(_ground, _lastground, _filter); // gonna keep the typo bc pog // shoot up check for snap availability _arc.Trace( _groundtracepos, _up, _skin + 0.1F, _orient, _filter, 0F, QueryTriggerInteraction.Ignore, _traces, out int _stepcunt); ArchetypeHeader.TraceFilters.FindClosestFilterInvalids(ref _stepcunt, out int _i1, _bias, _self, _traces); if (_i1 >= 0) { RaycastHit _snap = _traces[_i1]; Vector3 _c = Vector3.Cross(_snap.normal, _ground.normal); _c.Normalize(); Vector3 _f = Vector3.Cross(_up, _c); _f.Normalize(); if (VectorHeader.Dot(_vel, _f) <= 0F) { if (VectorHeader.Dot(_vel, _snap.normal) < 0F) { _rec.OnTraceHit(_snap, _groundtracepos, _vel); } _gflags |= (1 << 1); VectorHeader.ProjectVector(ref _vel, _c); } _groundtracepos += _up * Mathf.Max( Mathf.Min(_snap.distance - _skin, _skin), 0F); } else { _groundtracepos += _up * (_skin); } if (_ground.snapped) { _tracepos = _groundtracepos; _lastplane = _ground.normal; _gflags |= (1 << 0); VectorHeader.ClipVector(ref _vel, _ground.normal); //VectorHeader.CrossProjection(ref _vel, _up, _ground.normal); } _groundtracelen = 0F; } else { // clip, normalize, and continue: VectorHeader.ClipVector(ref _groundtracedir, _closest.normal); _groundtracedir.Normalize(); _groundtracelen -= _closest.distance; } } else // nothing discovered, end out of our ground loop. { _groundtracelen = 0F; } } while (_pushbackcount++ < ActorHeader.MAX_PUSHBACKS) { _arc.Overlap( _tracepos, _orient, _filter, 0F, QueryTriggerInteraction.Ignore, _colliders, out int _overlapsfound); ArchetypeHeader.OverlapFilters.FilterSelf( ref _overlapsfound, _self, _colliders); if (_overlapsfound == 0) // nothing ! { break; } else { for (int _colliderindex = 0; _colliderindex < _overlapsfound; _colliderindex++) { Collider _other = _colliders[_colliderindex]; Transform _otherT = _other.GetComponent <Transform>(); if (Physics.ComputePenetration(_self, _tracepos, _orient, _other, _otherT.position, _otherT.rotation, out Vector3 _normal, out float _distance)) { _tracepos += _normal * (_distance + _skin); PM_SlideDetermineImmediateGeometry(ref _vel, ref _lastplane, _actor.DeterminePlaneStability(_normal, _other), _normal, _ground.normal, _ground.stable && _ground.snapped, _up, ref _gflags); break; } } } } while (_bumpcount++ < ActorHeader.MAX_BUMPS && _tf > 0) { // Begin Trace Vector3 _trace = _vel * _fdt; float _tracelen = _trace.magnitude; // IF unable to trace any further, break and end if (_tracelen <= MIN_DISPLACEMENT) { _tf = 0; } else { _arc.Trace(_tracepos, _trace / _tracelen, _tracelen + _skin, _orient, _filter, 0F, QueryTriggerInteraction.Ignore, _traces, out int _tracecount); ArchetypeHeader.TraceFilters.FindClosestFilterInvalids( ref _tracecount, out int _i0, _bias, _self, _traces); if (_i0 <= -1) // nothing discovered ::: { _tf = 0; // end move _tracepos += _trace; break; } else // discovered an obstruction::: { RaycastHit _closest = _traces[_i0]; Vector3 _normal = _closest.normal; float _rto = _closest.distance / _tracelen; _tf -= _rto; float _dis = _closest.distance - _skin; _tracepos += (_trace / _tracelen) * _dis; // move back along the trace line! _rec.OnTraceHit(_closest, _tracepos, _vel); PM_SlideDetermineImmediateGeometry(ref _vel, ref _lastplane, _actor.DeterminePlaneStability(_normal, _closest.collider), _normal, _ground.normal, _ground.stable && _ground.snapped, _up, ref _gflags); continue; } } } _pos = _tracepos; }
// PM_FlyMove() is one of the Move() variants packaged with the Actor sub-package found in the // decoupling GitHub repository. It's purpose is to allow the player to 'fly' around the physics scene // whilst also keeping into account the colliders and geometric planes that represent your levels. Use // this method primarily if you are dealing with a sort of 'spectating' or 'flying' mechanic for your // actors. public static void PM_FlyMove( IActorReceiver _rec, Actor _actor, ref Vector3 _pos, ref Vector3 _vel, Quaternion _orient, LayerMask _filter, float _fdt) { // STEPS: // RUN : // OVERLAP -> PUSHBACK -> CONVEX HULL NORMAL (NEARBY PLANE DETECTION) -> GENERATE GEOMETRY BITMASK -> TRACING -> REPEAT /* * I've thought long and hard about providing documentation and comments to explain the fuckery that occurs * in these lines of code, and I think it'd be best I do so. */ /* * Initializing our local variables at the very top of our function. I'd like to maintain this codebase to be as * functional as possible, starting with the ActorHeader class. */ /* * * Here i'll be summarizing the usage of each field found below before our discrete resolution loop: * * Archetype - The Archetype class allows us to access each primitive's Archetype class which is required to be bundled * with the Monobehaviour Actor variants. Each Archetype contains its own primitive implementation provided by the Unity Physics API, * and allows us to interface with each primitive without explicitly hardcoding each type's movement. * * Colliders[] - The Colliders[] array is a crucial part to our discrete resolution loop, as it stores all potential colliders discovered in * our archetype's Overlap() query. By storing these Colliders, it allows us to act upon them and attempt to compute a penetration vector and distance. * This penetration vector and distance allows us to push our actor outside of any potentially overlapping colliders. * * Vector3[] Normals - The Normals[] array isn't necessarily important as of this moment. However, I'm thinking I reuse this in some callback for the * IActor interface. * * RaycastHit[] - The RaycastHit[] traces array is crucial for the Continuous Collision Detection (CCD) portion of our resolution loop. * Unity's Physics API uses the RaycastHit structure as the main output component for interfacing with its various casting methods. * In our case, we don't want to generate garbage during our trace, so we use the NonAlloc() variant for our linear cast. * * We then operate on this array and filter it to find the closest valid trace intersection for our interception resolution. * * Vector3 TracePos - We create a copy of our actor's position to operate upon during our movement. This is implemented just in case any last minute * 'safety' detections need to run and determine if the newly calculated position is safe to warp to. * * Vector3 LastPlane - We create a local Vector3 to store the previous plane we've encountered during our resolution loop. * I'd initially settled with Q1's "clip and pray" approach to dealing with creases, followed by a for loop looking for a crease. * However, Q1's initial crease/corner detection loop was is kinda clunky and ugly and I'm not even sure it works 100% of the time. * * I must confess, this method of detection found in PM_DetermineNearbyTopology() isn't pretty either, but it gets the job done perfectly. * * float TimeLeft - I use a fraction instead of the actual distance required, as its generally much cleaner to express what is occuring then to iteratively * subtract the traced distance. 1 = trace has just begun, 0 = trace is finished. Any value in between is a percentage of how much further we need to travel. * * float Skin - the skin variable is important as it prevents tunneling when the Actor gets asymptotically close to the surface of any infinite plane * detected in either the discrete pushback phase or continuous tracing phase. * * int BumpCount - stores the amount of bump iterations that have occured during our resolution loop. * * int PushbackCount - stores the amount of discrete pushbacks that have occured during our resolution loop. * * int GeometryFlags - stores the current state of our Actor's immediate surroundings. 0 = Discovered Plane, 1 = Discovered Crease, 3 = Discovered Corner */ ArchetypeHeader.Archetype _arc = _actor.GetArchetype(); Collider[] _colliders = _actor.Colliders; Collider _self = _arc.Collider(); Vector3[] _normals = _actor.Normals; RaycastHit[] _traces = _actor.Hits; Vector3 _tracepos = _pos; Vector3 _lastplane = Vector3.zero; float _tf = 1F; float _skin = ArchetypeHeader.GET_SKINEPSILON(_arc.PrimitiveType()); int _bumpcount = 0; int _pushbackcount = 0; int _gflags = 0; // Attempt an Overlap Pushback at this current position: while (_pushbackcount++ < ActorHeader.MAX_PUSHBACKS) { _arc.Overlap( _tracepos, _orient, _filter, 0F, QueryTriggerInteraction.Ignore, _colliders, out int _overlapsfound); ArchetypeHeader.OverlapFilters.FilterSelf( ref _overlapsfound, _self, _colliders); if (_overlapsfound == 0) // nothing ! { break; } else { for (int _colliderindex = 0; _colliderindex < _overlapsfound; _colliderindex++) { Collider _other = _colliders[_colliderindex]; Transform _otherT = _other.GetComponent <Transform>(); if (Physics.ComputePenetration(_self, _tracepos, _orient, _other, _otherT.position, _otherT.rotation, out Vector3 _normal, out float _distance)) { _tracepos += _normal * (_distance + _skin); if (VectorHeader.Dot(_vel, _normal) < 0F) // In this overlap, we want the immediate normals { PM_FlyDetermineImmediateGeometry(ref _vel, ref _lastplane, _normal, ref _gflags); } break; } } } } while (_bumpcount++ <= ActorHeader.MAX_BUMPS && _tf > 0) { // Begin Trace Vector3 _trace = _vel * _fdt; float _tracelen = _trace.magnitude; // IF unable to trace any further, break and end if (_tracelen <= MIN_DISPLACEMENT) { _tf = 0; break; } else { _arc.Trace( _tracepos, _trace / _tracelen, _tracelen + _skin, _orient, _filter, 0F, QueryTriggerInteraction.Ignore, _traces, out int _tracecount); ArchetypeHeader.TraceFilters.FindClosestFilterInvalids( ref _tracecount, out int _i0, ArchetypeHeader.GET_TRACEBIAS(_arc.PrimitiveType()), _self, _traces); if (_i0 <= -1) // nothing discovered::: { _tf = 0; // end move _tracepos += _trace; break; } else // discovered an obstruction::: { RaycastHit _closest = _traces[_i0]; float _rto = _closest.distance / _tracelen; _tf -= _rto; float _dis = Mathf.Max(_closest.distance - _skin, 0F); _tracepos += (_trace / _tracelen) * _dis; // move back along the trace line! PM_FlyDetermineImmediateGeometry(ref _vel, ref _lastplane, _closest.normal, ref _gflags); // determine our topology state continue; } } } _pos = _tracepos; }