/// <summary> /// Reset the collision flags of this gameObject to only None /// </summary> protected CollisionFlags2D ResetCollisionFlags(CollisionFlags2D collisions) { collisions |= CollisionFlags2D.None; collisions &= ~CollisionFlags2D.Back; collisions &= ~CollisionFlags2D.Top; collisions &= ~CollisionFlags2D.Bottom; collisions &= ~CollisionFlags2D.Front; return(collisions); }
private void Awake() { characterTransform = GetComponent <Transform>(); characterBody = GetComponent <Rigidbody2D>(); characterBody.isKinematic = true; contactFilter.SetLayerMask(Physics2D.GetLayerCollisionMask(gameObject.layer)); contactFilter.useLayerMask = true; hitList = new List <RaycastHit2D>(16); collisionFlags = CollisionFlags2D.None; characterUpDirection = Vector2.up; }
/** * // Useful just before teleporting the controller, for example. * public void ResetSlopes() * { * switchCeiling = switchGround = false; * ground = upGround = downGround = null; * ceiling = upCeiling = downCeiling = null; * groundTriggers.Clear(); * groundTriggers.TrimExcess(); * ceilingTriggers.Clear(); * ceilingTriggers.TrimExcess(); * } * /**/ #endregion #region Monobehaviour void Awake() { collisionFlags = new CollisionFlags2D(); if (!self) { self = GetComponent <Transform>(); } if (!box) { box = GetComponent <BoxCollider2D>(); } //stepSize = 0.1f; //groundTriggers = new List<SlopeZone>(); //ceilingTriggers = new List<SlopeZone>(); }
/// <summary> /// Move character while being contrained by collisions /// </summary> /// <param name="deltaPosition"></param> public void Move(Vector2 deltaPosition) { //--------------------------------------------------------------------------------------------------------- // Reset the collision flags before invoking the movement method. If the user needs the flags from the // previous call they should be used before invoking this method. At this point the character will // move and recieve new flags. //--------------------------------------------------------------------------------------------------------- collisionFlags = ResetCollisionFlags(collisionFlags); //--------------------------------------------------------------------------------------------------------- // We first get our up direction and calculate the dot product with respect to the deltaposition direction. // We also calculate the total distance to travel this frame. //--------------------------------------------------------------------------------------------------------- Vector2 upDirection = characterUpDirection; //--------------------------------------------------------------------------------------------------------- // Initial cast. Here we use the full movement vector before splitting into side/up movement. If no // collisions are found we can early exit the method. //--------------------------------------------------------------------------------------------------------- if (!MoveInitial(characterBody, deltaPosition, contactFilter, hitList, contactOffset)) { return; } //--------------------------------------------------------------------------------------------------------- // We decompose our movement into a normal and tangent vector with respect to the characters up direction. // The normal vector points in the direction of the up direction while the tangent Vector points // in the perpendicular direction to the normal with respect to the deltaposition. Remember that the // character up direction must be normalized and non zero. //--------------------------------------------------------------------------------------------------------- Vector2 vertVector = upDirection * Vector2.Dot(upDirection, deltaPosition); Vector2 sideVector = deltaPosition - vertVector; //--------------------------------------------------------------------------------------------------------- // Run the horizontal and vertical move methods. Note that we do the horizontal movement first. This // is by design. Changing the order of these calls requires a full remake of the controller. //--------------------------------------------------------------------------------------------------------- collisionFlags |= MoveHorizontal(characterTransform, characterBody, sideVector, upDirection, contactFilter, hitList, contactOffset, positionIterations, collisionFlags, slopeLimit, slideOnGround, slideOnCeilings, forceSlide); collisionFlags |= MoveVertical(characterTransform, characterBody, vertVector, upDirection, contactFilter, hitList, contactOffset, positionIterations, collisionFlags, slopeLimit, slideOnGround, slideOnCeilings, forceSlide); }
/// <summary> /// Contains casting and movement logic for the vertical movement of the character. This is a secondary movement meaning /// i only handles simple logic like moving down or up if not on the ground. /// </summary> /// <param name="body"></param> /// <param name="direction"></param> /// <param name="filter"></param> /// <param name="results"></param> /// <param name="skinWidth"></param> /// <param name="maxIterations"></param> protected CollisionFlags2D MoveVertical(Transform transform, Rigidbody2D body, Vector2 direction, Vector2 upDirection, ContactFilter2D filter, List <RaycastHit2D> results, float skinWidth, int maxIterations, CollisionFlags2D flags, float maxAngle, bool slideGround, bool slideCeilings, bool forceSlideCharacter) { //--------------------------------------------------------------------------------------------------------- // Start up code before iteration //--------------------------------------------------------------------------------------------------------- Vector2 currentPosition = body.position; Vector2 currentDirection = direction; Vector2 targetPosition = currentPosition + currentDirection; int i = 0; while (i < maxIterations) { //--------------------------------------------------------------------------------------------------------- // Cast in the horizontal direction //--------------------------------------------------------------------------------------------------------- float distance = currentDirection.magnitude; int hits = body.Cast(currentDirection, filter, results, distance + skinWidth); //--------------------------------------------------------------------------------------------------------- // Nothing hit, move straight to target position //--------------------------------------------------------------------------------------------------------- if (hits <= 0) { currentPosition = targetPosition; break; } //--------------------------------------------------------------------------------------------------------- // Vertical collision detection // We first sort the list of hits and get the closest one. Then we update our the character controllers // position if the movement is larger than the skin width. //--------------------------------------------------------------------------------------------------------- results.Sort((x, y) => y.distance.CompareTo(x.distance)); RaycastHit2D hit = results[0]; currentPosition = hit.distance > skinWidth ? currentPosition + currentDirection.normalized * (hit.distance - skinWidth) : currentPosition; //--------------------------------------------------------------------------------------------------------- // We decide if we hit ceiling or ground //--------------------------------------------------------------------------------------------------------- float normalDotUp = Vector2.Dot(hit.normal, upDirection); Vector2 verticalDirection = Vector2.Dot(hit.normal, upDirection) >= 0 ? upDirection : -upDirection; float angle = Vector2.Angle(verticalDirection, hit.normal); //--------------------------------------------------------------------------------------------------------- // Here we decide if we should apply collision responses on the given geometry during the vertical // collision call. We first find out if the character hits the ground or ceiling, then we // calculate the collision flags accordingly. In the end we apply collision responses. //--------------------------------------------------------------------------------------------------------- bool applyCollisionResponse = false; if (normalDotUp >= 0) { if (angle > maxAngle && slideGround && forceSlideCharacter) { applyCollisionResponse = true; } else { flags |= CollisionFlags2D.Bottom; } } else { if (angle > maxAngle && slideCeilings) { applyCollisionResponse = true; flags |= CollisionFlags2D.Top; } } targetPosition = applyCollisionResponse ? CollisionResponse(currentPosition, targetPosition, hit.normal, 0, 0) : currentPosition; //--------------------------------------------------------------------------------------------------------- // Calculate new target position and Move the body to current position //--------------------------------------------------------------------------------------------------------- currentDirection = targetPosition - currentPosition; body.position = currentPosition; //--------------------------------------------------------------------------------------------------------- // Exit the method if we reached our destination. This prevents the loop from iterating all the way // to maxIterations each time, saving computation time. //--------------------------------------------------------------------------------------------------------- if (currentDirection.sqrMagnitude < Mathf.Epsilon * Mathf.Epsilon) { break; } i++; } //--------------------------------------------------------------------------------------------------------- // Final position update //--------------------------------------------------------------------------------------------------------- body.position = currentPosition; return(flags); }
/// <summary> /// Contains the primary logic for the horizontal casting and movement of the character controller. This is a dominant /// movement method, meaning the vertical movement is adjusted according to the horizontal movement. This method /// moves the character up/down slopes, checks the slope limit and does slope clamping. Also does ground /// checks in each iteration. /// </summary> /// <param name="body"></param> /// <param name="direction"></param> /// <param name="filter"></param> /// <param name="results"></param> /// <param name="skinWidth"></param> /// <param name="maxIterations"></param> protected CollisionFlags2D MoveHorizontal(Transform transform, Rigidbody2D body, Vector2 direction, Vector2 upDirection, ContactFilter2D filter, List <RaycastHit2D> results, float skinWidth, int maxIterations, CollisionFlags2D flags, float maxAngle, bool slideGround, bool slideCeilings, bool forceSlideCharacter) { //--------------------------------------------------------------------------------------------------------- // Start up code before iteration //--------------------------------------------------------------------------------------------------------- Vector2 currentPosition = body.position; Vector2 currentDirection = direction; Vector2 targetPosition = currentPosition + currentDirection; int i = 0; while (i < maxIterations) { //--------------------------------------------------------------------------------------------------------- // Cast in the horizontal direction //--------------------------------------------------------------------------------------------------------- float distance = currentDirection.magnitude; int hits = body.Cast(currentDirection, filter, results, distance + skinWidth); //--------------------------------------------------------------------------------------------------------- // Nothing hit, move straight to target position //--------------------------------------------------------------------------------------------------------- if (hits <= 0) { currentPosition = targetPosition; break; } //--------------------------------------------------------------------------------------------------------- // Horizontal collision detection // We first sort the list of hits and get the closest one. Then we update our the character controllers // position if the movement is larger than the skin width. //--------------------------------------------------------------------------------------------------------- results.Sort((x, y) => y.distance.CompareTo(x.distance)); RaycastHit2D hit = results[0]; currentPosition = hit.distance > skinWidth ? currentPosition + currentDirection.normalized * (hit.distance - skinWidth) : currentPosition; //--------------------------------------------------------------------------------------------------------- // We decide if we hit ceiling or ground //--------------------------------------------------------------------------------------------------------- float normalDotUp = Vector2.Dot(hit.normal, upDirection); Vector2 verticalDirection = normalDotUp >= 0 ? upDirection : -upDirection; float angle = Vector2.Angle(verticalDirection, hit.normal); //--------------------------------------------------------------------------------------------------------- // Here we decide if we should apply collision responses on the given geometry during the horizontal // collision call. We first find out if the character hits the ground or ceiling. After this // we calculate if the character hit with it's back or front and set the collision flags // accordingly. In the end we apply collision responses //--------------------------------------------------------------------------------------------------------- bool applyCollisionResponse = false; if (normalDotUp >= 0) { if (angle <= maxAngle && slideGround) { applyCollisionResponse = true; } } else { if (angle <= maxAngle && slideCeilings) { applyCollisionResponse = true; } } if (Vector2.Dot(transform.right, hit.normal) < 0) { flags |= CollisionFlags2D.Front; } else { flags |= CollisionFlags2D.Back; } targetPosition = applyCollisionResponse ? CollisionResponse(currentPosition, targetPosition, hit.normal, 0, 0) : currentPosition; //--------------------------------------------------------------------------------------------------------- // Move body to current position and get ready for the next iteration. If this is the last iteration, // the values calculated here will not be used. //--------------------------------------------------------------------------------------------------------- currentDirection = targetPosition - currentPosition; body.position = currentPosition; //--------------------------------------------------------------------------------------------------------- // If we already reached our target we can exit the method. Saves us iterations in case we have many // position updates. If we didn't have this, the method would iterate to the end each time //--------------------------------------------------------------------------------------------------------- if (currentDirection.sqrMagnitude < Mathf.Epsilon * Mathf.Epsilon) { break; } i++; } //--------------------------------------------------------------------------------------------------------- // Final position update //--------------------------------------------------------------------------------------------------------- body.position = currentPosition; return(flags); }