Making Space Gal Move - Part 3: It Works
I figured it out. The shitty part is that I don't 100% understand why it works.
I rewrote the collision code from scratch, and discovered, to my chagrin, that the new holistic raycasting method had the exact same problem of getting hung up on corners. What I eventually deduced is that it has to do with the raycasts higher on the bounding box getting snagged on the upper edge, so I just prohibited all the raycasts except the bottom ones from registering a hit on surfaces facing directly up.
Ultimately, I don't have the energy or the expertise to explain how I did it or why, so here's the full source code for my physics system.
using UnityEngine; using System; using System.Collections.Generic; /// <summary> /// A custom Rigidbody for platformer physics. /// </summary> public class GBody : MonoBehaviour { //--------------------- // Static Properties //--------------------- //--------------------- // Members //--------------------- public Collider2D myCollider; public GParticle particle; [Space] public bool isSolid; public bool acceptsInput; public Verb collisionVerb; [Space] public Vector2 input; public Vector2 velocity; [Space] public float gravity; public float terminalVelocity; public float hFriction; public float maxSpeed; public int maxSlope; public bool isGrounded; public bool isWallhanging; public float wallhangModifier; /// <summary> /// Value guide: for x, 1 = blocked to the right, -1 = blocked to the left, 2 is blocked both ways /// for y, 1 = blocked from above, -1 = blocked from below, 2 is blocked both ways /// for both, 0 is not blocked at all. /// </summary> public Vector2 movementBlock; // Private Members private Actor parent; /// <summary> /// For gamefeel movement /// </summary> public Vector2 adjustment; public bool isAdjusting; public int adjustmentTime; public int adjustmentCount; public Vector2 overrider; //--------------------- // Events //--------------------- //--------------------- // Properties //--------------------- //--------------------- // Pseudo-Properties //--------------------- //--------------------- // MonoBehavior Methods //--------------------- // Use this for initialization void Start () { parent = GetComponent<actor>(); } // Update is called once per frame void FixedUpdate () { Vector2 modifier = new Vector2(); float hDamper = 1; float vDamper = 1; if (acceptsInput) { Wallhang(); if (input.x == 0) { hDamper = hFriction; } //Eventually we'll need to check if we're on a ladder, but for now, we'll just always zero out input.y input.y = 0; modifier = input; } velocity += modifier; velocity.x = Mathf.Clamp(velocity.x * hDamper,-maxSpeed,maxSpeed); velocity.y = velocity.y * vDamper; Gravity(); CheckCollisions(); UpdatePosition(); } //--------------------- // Public Methods //--------------------- public void ApplyForce(Vector2 force) { velocity += force; } //--------------------- // Private Methods //--------------------- /// <summary> /// Checks if the Collider is currently touching any other colliders. /// </summary> private void CheckCollisions() { adjustment = Vector2.zero; PreemptiveCollision(); movementBlock = Vector2.zero; GameObject hit = null; // Used for adjusting the check location, because of funkiness with how the coordinates work. // This is basically more shitty number twiddling, but putting it into a holder like this makes it okay /s Vector2 offset = new Vector2(); if(velocity.y > 0) { offset.y = 1; } if(velocity.x < 0) { offset.x = -1; } // Check the space either above or below the collider for (int i = (int)-myCollider.bounds.extents.x; i < myCollider.bounds.extents.x; ++i) { hit = Check(new Vector2(myCollider.transform.position.x + i, myCollider.transform.position.y + offset.y + myCollider.bounds.extents.y * GTools.PosNeg(velocity.y))); if (hit && hit != myCollider.gameObject) { if (isSolid) { movementBlock.y = 1 * GTools.PosNeg(velocity.y); } parent.Collide(hit); } } // More number finicking but this makes it look less like a kludge!!! offset.y = 1; // Check the space either to the left or right of the collider bool madeHit = false; for (int i = (int)-myCollider.bounds.extents.y; i < myCollider.bounds.extents.y;++i) { hit = Check(new Vector2(myCollider.transform.position.x + offset.x + myCollider.bounds.extents.x * GTools.PosNeg(velocity.x), myCollider.transform.position.y + i + offset.y)); if (hit && hit != myCollider.gameObject) { if (isSolid) { madeHit = true; } parent.Collide(hit); } else if (!hit && Mathf.Abs(i - -myCollider.bounds.extents.y) <= maxSlope && madeHit) { // This code is responsible for slopes, and as a side-effect, makes movement over edges a little smoother. adjustment.y = Mathf.Abs(i - -myCollider.bounds.extents.y); madeHit = false; break; } } if (madeHit) { movementBlock.x = 1 * GTools.PosNeg(velocity.x); } } /// <summary> /// Checks for collisions that will occur during the current frame, to prevent interpenetration. /// </summary> private void PreemptiveCollision() { overrider = Vector2.zero; LayerMask mask = 1 << gameObject.layer; mask = ~mask; //Inverts the bits Vector2 checkadjuster = new Vector2(0.5f, -0.5f); // We're going to check points on the corners and middle edges of the collider. // No really clean way to do this, just gotta enter the values manually. // It uses the Unity bounding box, so this code does not need to be adjusted // for different sizes. // This code is meant to unify the physics systems, so we can collide with 2D or 3D colliders, // and unfortunately that means twice as many checks because RaycastHit2D and RaycastHit do // not a common inheritance. List<tuple<raycasthit2d, vector2="">> hits2D = new List<tuple<raycasthit2d, vector2="">>(); List<tuple<raycasthit, vector2="">> hits3D = new List<tuple<raycasthit, vector2="">>(); Vector2 closestDistance = new Vector2(256,256); //Set very high so that any found distance will be closer. // Number finicking zone. Watch out!!! // Top Side Point Vector2 topSidePoint = new Vector2(myCollider.transform.position.x + (myCollider.bounds.extents.x * GTools.PosNeg(velocity.x)) , myCollider.transform.position.y + myCollider.bounds.extents.y); // Middle Side Point Vector2 middleSidePoint = new Vector2(myCollider.transform.position.x + (myCollider.bounds.extents.x * GTools.PosNeg(velocity.x)) , myCollider.transform.position.y); // Bottom Side Point Vector2 bottomSidePoint = new Vector2(myCollider.transform.position.x + (myCollider.bounds.extents.x * GTools.PosNeg(velocity.x)) , myCollider.transform.position.y - myCollider.bounds.extents.y); // Middle Point Vector2 middlePoint = new Vector2(myCollider.transform.position.x, myCollider.transform.position.y + (myCollider.bounds.extents.y * GTools.PosNeg(velocity.y))); hits2D.Add(new Tuple<raycasthit2d, vector2="">( Physics2D.Raycast(topSidePoint, velocity, velocity.magnitude, mask),topSidePoint)); hits2D.Add(new Tuple<raycasthit2d, vector2="">(Physics2D.Raycast(middleSidePoint, velocity, velocity.magnitude, mask),middleSidePoint)); hits2D.Add(new Tuple<raycasthit2d, vector2="">(Physics2D.Raycast(bottomSidePoint, velocity, velocity.magnitude, mask),bottomSidePoint)); hits2D.Add(new Tuple<raycasthit2d, vector2="">(Physics2D.Raycast(middlePoint, velocity, velocity.magnitude, mask),middlePoint)); RaycastHit hit3d1 = new RaycastHit(); Physics.Raycast(topSidePoint, velocity, out hit3d1, velocity.magnitude, mask); hits3D.Add(new Tuple<raycasthit, vector2="">(hit3d1, topSidePoint)); RaycastHit hit3d2 = new RaycastHit(); Physics.Raycast(middleSidePoint, velocity, out hit3d2, velocity.magnitude, mask); hits3D.Add(new Tuple<raycasthit, vector2="">(hit3d2, middleSidePoint)); RaycastHit hit3d3 = new RaycastHit(); Physics.Raycast(bottomSidePoint, velocity, out hit3d3, velocity.magnitude, mask); hits3D.Add(new Tuple<raycasthit, vector2="">(hit3d3, bottomSidePoint)); RaycastHit hit3d4 = new RaycastHit(); Physics.Raycast(middlePoint, velocity, out hit3d4, velocity.magnitude, mask); hits3D.Add(new Tuple<raycasthit, vector2="">(hit3d4, middlePoint)); foreach(Tuple<raycasthit2d,vector2> it in hits2D) { if (it.Item1.collider) { //This code is a little cryptic. Basically we're just trying to find the shortest raycast. // The business with normals is because the higher-originating raycasts have a bad habit of // snagging on corners. I don't understand 100% why it works, but it works. if(it.Item1.distance < closestDistance.magnitude && it.Item1.distance != 0 && !(it.Item1.normal == new Vector2(0,1) && it.Item2.y >= myCollider.transform.position.y )) { closestDistance = it.Item1.point - it.Item2; } } } foreach (Tuple<raycasthit, vector2=""> it in hits3D) { if (it.Item1.collider) { if (it.Item1.distance < closestDistance.magnitude && it.Item1.distance != 0 && !((Vector2)it.Item1.normal == new Vector2(0, 1) && it.Item2.y >= myCollider.transform.position.y)) { closestDistance = (Vector2)it.Item1.point - it.Item2; } } } if(closestDistance != new Vector2(256,256)) { overrider = closestDistance; if(overrider.x < Mathf.Round(overrider.x)) { overrider.x -= (Mathf.Round(overrider.x) - overrider.x); } else { overrider.x = Mathf.Round(overrider.x); } if (overrider.y < Mathf.Round(overrider.y)) { overrider.y -= (Mathf.Round(overrider.y) - overrider.y); } else { overrider.y = Mathf.Round(overrider.y); } Restrict(ref overrider); } } private void Gravity() { if (adjustment != Vector2.zero) { isAdjusting = true; adjustmentCount = adjustmentTime; } float mod = 1; if(isWallhanging) { velocity.y = Mathf.Clamp(velocity.y, terminalVelocity * wallhangModifier, 0); } Gravity(mod); if (adjustment == Vector2.zero && adjustmentCount == 0) { isAdjusting = false; } adjustmentCount = Mathf.Clamp(adjustmentCount - 1, 0, 256); } private void Gravity(float modifier) { if (movementBlock.y == -1 || movementBlock.y == 2) { //Debug.Log("Grounded"); isGrounded = true; } else { if (!isAdjusting) { //Debug.Log(gameObject.name + " is airborne"); velocity.y -= gravity * modifier; velocity.y = Mathf.Clamp(velocity.y, terminalVelocity, 256); isGrounded = false; } } } private void Wallhang() { GameObject hit = Check(new Vector2(myCollider.transform.position.x + (GTools.PosNeg(input.x) * myCollider.bounds.extents.x) + input.x, myCollider.transform.position.y)); if (!isGrounded && (movementBlock.x == input.x || movementBlock.x == 2) && input.x != 0) { if (hit && hit != myCollider) { isWallhanging = true; } } else { isWallhanging = false; } } private void UpdatePosition() { Vector3 oldPosition = transform.localPosition; Restrict(ref velocity); float modX = velocity.x; float modY = velocity.y; // If the preemptive checking found something, we're going to totally override the velocity. if(overrider != Vector2.zero) { modX = overrider.x; modY = overrider.y; } // The slopes adjustments are applied on top. if (adjustment != Vector2.zero) { modX += adjustment.x; modY += adjustment.y; } transform.localPosition = new Vector3(transform.localPosition.x + modX, transform.localPosition.y + modY); // Rounds off the position of the body if it hasn't moved since the last frame. // This ensures consistency in things like jump height from the body's apparent height. // Omit this if you're not handling positions in full integers. if (transform.localPosition.x == oldPosition.x) { transform.localPosition = new Vector3(Mathf.Round(transform.localPosition.x), transform.localPosition.y); } if (transform.localPosition.y == oldPosition.y) { transform.localPosition = new Vector3(transform.localPosition.x, Mathf.Round(transform.localPosition.y)); } } //--------------------- // Static Methods //--------------------- /// <summary> /// Checks a given pixel coordinate for colliders. /// </summary> /// <param name="coords"> /// <returns></returns> public GameObject Check(Vector2 coords) { LayerMask mask = 1 << gameObject.layer; mask = ~mask; //Inverts the bits return Check(coords, mask); } /// <summary> /// Checks a given pixel coordinate for colliders. /// </summary> /// <param name="coords"> /// <returns></returns> public GameObject Check(Vector2 coords,int layerMask) { // THIS IS A VERY NAUGHTY FIX, BUT LET'S GIVE IT A SHOT!!! coords.x += 0.5f; coords.y -= 0.5f; RaycastHit2D hit2D = Physics2D.Raycast(coords, Vector2.down, 0.01f,layerMask); if (hit2D.collider != null && hit2D.collider.GetComponentInParent<gbody>() && !hit2D.collider.GetComponentInParent<gbody>().isSolid) { return null; } Collider[] hits = Physics.OverlapSphere(coords, 0.1f); foreach (Collider it in hits) { if (it != myCollider) { return it.gameObject; } } if (hit2D.collider) { return hit2D.collider.gameObject; } return null; } public void Restrict(ref Vector2 input) { if (movementBlock.y == -1) { input.y = Mathf.Clamp(input.y, 0, 256); } if (movementBlock.y == 1) { input.y = Mathf.Clamp(input.y, -256, 0); } if (movementBlock.x == -1) { input.x = Mathf.Clamp(input.x, 0, 256); } if (movementBlock.x == 1) { input.x = Mathf.Clamp(input.x, -256, 0); } } //--------------------- // Event Methods //--------------------- }
And here's the code for GTools.PosNeg, which the above code will not work very well without:
public static int PosNeg(float input) { if(input > 0) { return 1; } if (input < 0) { return -1; } return 0; }
Space☆Gal
Challenging metroidvania bounty hunting action
Status | In development |
Author | DStecks |
Genre | Platformer |
Tags | 16-bit, Difficult, Female Protagonist, LGBT, Metroidvania, Pixel Art, Retro, Singleplayer, Space, Yuri |
More posts
- Progress ReportJul 07, 2020
- Demo Build 5May 10, 2020
- Demo Build 4Apr 27, 2020
- Making Space Gal Move - Part 2: The StruggleApr 22, 2020
- Demo Build 3Apr 18, 2020
- New Playable DemoApr 12, 2020
- Making Space Gal Move - Part 1: PhysicsApr 12, 2020
- Playable Demo!Apr 10, 2020
Leave a comment
Log in with itch.io to leave a comment.