0
\$\begingroup\$

I'm working on a side scrolling game where among other movement options the player can perform a "bullet jump". The bullet jump fills a similar role to a double jump, but instead of just jumping in midair, it launches the player in the direction of the mouse cursor. Unfortunately, I can't seem to find any way of doing this using Character Controller, so I've turned to Rigidbody physics to handle the air launch of the player.

As I can't use both Rigidbody and Character Controller, I'm forced to pick one, and as I'm a novice, I decided I'd turn to Stack Exchange for help. I am aware that Character Controller was the given answer for this question on many other occasions, but due to the momentum focus of the game and the nature of the bullet jump mechanic, I feel it's worth asking this question in the context of my specific game.

\$\endgroup\$

2 Answers 2

0
\$\begingroup\$

Neither.

Well, that depends. What are you trying to achieve? If you're looking for classic 90s era platformer physics, then absolutely not, you can't use anything that relies on the Unity physics engine, because 90s era platformer physics are not realistic.

A common method that's proven well for me is to use raycast based collision detection to determine whether or not your character will collide with a collision layer and adjust their Vector3 velocity accordingly.

Here's the code I've used for my platformer games, most recently, Demons with Shotguns.

public class RaycastCollisionDetection : IEntityCollisionDetection {
    private BoxCollider _collider;
    private Rect _collisionRect;
    private LayerMask _collisionMask;
    private LayerMask _playerMask;

    public GameObject ObjectCollidedWith { get; set; }

    public bool OnGround { get; set; }
    public bool SideCollision { get; set; }
    public bool PlayerCollisionX { get; set; }
    public bool PlayerCollisionY { get; set; }
    public Vector3 HitNormal { get; set; }

    public void Init(GameObject entityGo) {
        _collisionMask = LayerMask.NameToLayer("Collisions");
        _playerMask = LayerMask.NameToLayer("Player");
        _collider = entityGo.GetComponent<BoxCollider>();
    }

    public Vector3 Move(Vector3 moveAmount, GameObject entityGo) {
        float deltaX = moveAmount.x;
        float deltaY = moveAmount.y;
        Vector3 entityPosition = entityGo.transform.position;                        
        // Resolve any possible collisions below and above the entity.
        deltaY = YAxisCollisions(deltaY, Mathf.Sign(deltaX), entityPosition);
        // Resolve any possible collisions left and right of the entity.
        // Check if our deltaX value is 0 to avoid unnecessary collision detection.
        if (deltaX != 0) {
            deltaX = XAxisCollisions(deltaX, entityPosition);
        }
        if (deltaX != 0 && deltaY != 0 && !SideCollision && !OnGround) {
            DiagonalCollisions(ref deltaX, ref deltaY, entityPosition);
        }

        Vector3 finalTransform = new Vector2(deltaX, deltaY);
        return finalTransform;
    }

    private float XAxisCollisions(float deltaX, Vector3 entityPosition) {
        SideCollision = false;
        PlayerCollisionX = false;
        ObjectCollidedWith = null;
        // It's VERY important that the entity's collider doesn't change
        // shape during the game. This will cause irregular raycast hits
        // and most likely cause things to go through layers.
        // Ensure sprites use a fixed collider size for all frames.
        _collisionRect = GetNewCollisionRect();
        // Increase this value if you want the rays to start and end
        // outside of the entity's collider bounds.
        float margin = 0.04f;
        int numOfRays = 4;
        Vector3 rayStartPoint = new Vector3(_collisionRect.center.x,
            _collisionRect.yMin + margin, entityPosition.z);
        Vector3 rayEndPoint = new Vector3(_collisionRect.center.x,
            _collisionRect.yMax - margin, entityPosition.z);
        float distance = (_collisionRect.width / 2) + Mathf.Abs(deltaX);

        for (int i = 0; i < numOfRays; ++i) {
            float lerpAmount = (float) i / ((float) numOfRays - 1);
            Vector3 origin = Vector3.Lerp(rayStartPoint, rayEndPoint, lerpAmount);
            Ray ray = new Ray(origin, new Vector2(Mathf.Sign(deltaX), 0));
            Debug.DrawRay(ray.origin, ray.direction, Color.white);
            RaycastHit hit;
            // Bit shift the layers to tell Unity to NOT ignore them.
            if (Physics.Raycast(ray, out hit, distance, 1 << _collisionMask) ||
                Physics.Raycast(ray, out hit, distance, 1 << _playerMask)) {
                HitNormal = hit.normal;
                Debug.DrawRay(ray.origin, ray.direction, Color.yellow);
                float x = Mathf.Sign(deltaX) == -1
                    ? _collisionRect.xMin
                    : _collisionRect.xMax;
                // Give a small amount of skin space to prevent snagging.
                float skinSpace = 0.005f;
                deltaX = (_collisionRect.center.x + hit.distance * ray.direction.x - x) + skinSpace;
                if (hit.transform.gameObject.layer == _playerMask) {
                    PlayerCollisionX = true;
                    deltaX = 0;
                    ObjectCollidedWith = hit.transform.gameObject;
                } else {
                    SideCollision = true;
                }
                break;
            }
        }

        return deltaX;
    }

    private float YAxisCollisions(float deltaY, float dirX, Vector3 entityPosition) {
        OnGround = false;
        PlayerCollisionY = false;
        ObjectCollidedWith = null;
        // It's VERY important that the entity's collider doesn't change
        // shape during the game. This will cause irregular raycast hits
        // and most likely cause things to go through layers.
        // Ensure sprites use a fixed collider size for all frames.
        _collisionRect = GetNewCollisionRect();
        // Increase this value if you want the rays to start and end
        // outside of the entity's collider bounds.
        float margin = 0.04f;
        int numOfRays = 4;
        Vector3 rayStartPoint = new Vector3(_collisionRect.xMin + margin,
            _collisionRect.center.y, entityPosition.z);
        Vector3 rayEndPoint = new Vector3(_collisionRect.xMax - margin,
            _collisionRect.center.y, entityPosition.z);
        float distance = (_collisionRect.height / 2) + Mathf.Abs(deltaY);

        for (int i = 0; i < numOfRays; ++i) {
            float lerpAmount = (float) i / ((float) numOfRays - 1);
            // If we are facing left, start the rays on the left side,
            // else start the ray rays on the right side.
            // This will help ensure precise castings on the corners.
            Vector3 origin = dirX == -1
                ? Vector3.Lerp(rayStartPoint, rayEndPoint, lerpAmount)
                : Vector3.Lerp(rayEndPoint, rayStartPoint, lerpAmount);
            Ray ray = new Ray(origin, new Vector2(0, Mathf.Sign(deltaY)));
            //Debug.DrawRay(ray.origin, ray.direction, Color.white);
            RaycastHit hit;
            // Bit shift the layers to tell Unity to NOT ignore them.
            if (Physics.Raycast(ray, out hit, distance, 1 << _collisionMask) ||
                Physics.Raycast(ray, out hit, distance, 1 << _playerMask)) {
                HitNormal = hit.normal;
                //Debug.DrawRay(ray.origin, ray.direction, Color.yellow);
                float y = Mathf.Sign(deltaY) == -1
                    ? _collisionRect.yMin
                    : _collisionRect.yMax;
                // Give a small amount of skin space to prevent snagging.
                float skinSpace = 0.0005f;
                deltaY = (_collisionRect.center.y + hit.distance * ray.direction.y - y) + skinSpace;
                // Only flag player collision if we collide with them while traveling down.
                if (hit.collider.gameObject.layer == _playerMask && Mathf.Sign(deltaY) == -1) {
                    PlayerCollisionY = true;
                    ObjectCollidedWith = hit.transform.gameObject;
                }
                OnGround = true;
                break;
            }
        }

        return deltaY;
    }

    private void DiagonalCollisions(ref float deltaX, ref float deltaY, Vector3 entityPosition) {            
        _collisionRect = GetNewCollisionRect();         
        float distance = (_collisionRect.height / 2) + Mathf.Abs(deltaX);

        Ray ray = new Ray(_collisionRect.center, new Vector2(Mathf.Sign(deltaX), Mathf.Sign(deltaY)));
        Debug.DrawRay(ray.origin, ray.direction, Color.white);
        RaycastHit hit;

        if (Physics.Raycast(ray, out hit, distance, 1 << _collisionMask)) {                
            HitNormal = hit.normal;
            Debug.DrawRay(ray.origin, ray.direction, Color.yellow);                             
            // Stop deltaX and let entity drop by deltaY.
            deltaX = 0;

            SideCollision = true;
        }
    }

    private Rect GetNewCollisionRect() {
        return new Rect(
            _collider.bounds.min.x,
            _collider.bounds.min.y,
            _collider.bounds.size.x,
            _collider.bounds.size.y);
    }
}

So for your game, you would need to determine when the player performed a "bullet jump" and adjust the movement and collision calculations accordingly based on your game's design.

For more info, visit Overdeveloped.

\$\endgroup\$
1
\$\begingroup\$

Unity5 has a standard asset for a character controller script that works with a RigidBody. I recommend using it or making another such script and avoiding using the built-in character controller component because that one makes your character incompatible with the physics engine.

If you use Assets>Import Package you should be able to find the standard one. It comes with a little robot sprite.

For your bullet jump effect, you will then be able to apply a force to your character and other, future characters using the same script.

\$\endgroup\$
3
  • \$\begingroup\$ Does it? I've heard pretty consistently that RigidBody and CharacterController should not be mixed. That advice may have been pre Unity 5, but I feel like I'd have run into some mention of this before now. \$\endgroup\$ Oct 25, 2016 at 4:25
  • 1
    \$\begingroup\$ You are right, the standard character controller component is incompatible, but there is a C# script in the standard assets that uses RigidBody and the physics engine for movement. Its class name is Platform Character 2D and it can be imported from the Standard Assets 2D. \$\endgroup\$ Oct 25, 2016 at 15:09
  • \$\begingroup\$ Thanks, I'll definitely take a look at it. I've been rebuilding my controller from the ground up, and am just about to start tackling the bullet jump again, using physics to just AddForce would be a lot easier than tracking momentum manually with variables. \$\endgroup\$ Oct 27, 2016 at 4:49

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .