Search code examples
c#unity-game-enginereflect

How to realistically reflect a 3d sphere in Unity with C#


I've been trying to realistically reflect a 3d sphere on the walls of a box for a while now in Unity. For some reason, the reflection is generally correct, but when the ball hits a wall in certain directions, the reflection is incorrect. To illustrate what happens to the ball upon hitting a wall: T = top wall, R = right wall, L = left wall, and B = bottom wall. Let r = the ball comes/goes to the right, l = for the left, and s = the ball stops/slows down significantly. The instructions below take this format: Xyz, where X = the wall the ball is about to hit, y = the ball's initial direction, z = the reflection. The game has a top-down perspective, and the instructions are based on the wall's perspective. I'm also new to C#, so the code is potentially eye burning.

Instructions: Tll, Trl; Bll, Brl; Rls or after hitting another wall Rlr, Rrl; Lls or after hitting another wall Llr, Lrl

Generally, when the ball stops, it jumps in the air. I wonder if this is because the angle reflects along the wrong axis, but why would this only sometimes happen? Also, when only one key is held, the ball bounces back and forth until it leaves the arena. I know about discrete and continuous hit detection, and the setting is on discrete, but the walls generally contain the ball well enough, with this case being the exception.

What I tried:

  1. Figuring out how to use Vector3.Reflect. I do not understand what variables this function should contain and how to apply this to my code. I did look at the Unity Documentation page for help, but it did not answer my question.
  2. Changing the negative signs, as the angle has to be a reflection on the y-axis, and this does change how the reflections work, but does not solve the problem. The current way the negatives are ordered are the most ideal I found.
  3. Giving the ball a physics material for bounciness.
  4. Adding a small number to the denominator in the arctan equation to help prevent a division by zero. This did not help at all.
  5. Creating different equations (basically changing the negatives) for varying combinations of positive and negative accelerations. Since there is a certain positive or negative acceleration associated with each button press (see Movement script), and there seems to be an issue with said signs, I wondered if associating each acceleration with its own set of equations could solve the problem. It did not work.
  6. Checked if the walls were at different angles.
  7. Deleting the variables xA and yA, or placing them in different spots.
  8. Experimented with finding the speed of the object, but had no idea how to implement it.

The code for the Movement script for the player named Controller:

public class Movement : MonoBehaviour
{

    public static float xAcceleration = 0.0f;
    public static float yAcceleration = 0.0f;

    // Update is called once per frame
    void Update()
    {

        if (Input.GetKey(KeyCode.W))                            //If the key W is pressed:
        {                                                       
            Vector3 position = this.transform.position;         //Variable position is set to transform the players placement in the game.
            if (yAcceleration >= -5 && yAcceleration <= 5)      //If the y vector of the acceleration is >= -5 and <= 5:
            {
                yAcceleration = yAcceleration + 0.01f;          //The y vector of the acceleration increases by 0.01 as long as the key W is pressed.
            }
            position.z = position.z + (0.1f * yAcceleration);   //The position of the object on the z-axis (pretend it is the y-axis in the game world) is transformed by its original position plus its speed times its yAcceleration.
            this.transform.position = position;                 //The gameObject is now transformed to a position equal to the variable position by the z-axis.
        }

        else                                                    //If the key W is let go of:
        {
            Vector3 position = this.transform.position;         
            position.z = position.z + (0.1f * yAcceleration);
            this.transform.position = position;                 //The position of the gameObject continues to update, but its acceleration does not change. Basically, it continues to move forward.
        }

//The rest of the code is very similar to the above, but I included it just in case there was something wrong.

        if (Input.GetKey(KeyCode.S))
        {
            Vector3 position = this.transform.position;
            if (yAcceleration >= -5 && yAcceleration <= 5)
            {
                yAcceleration = (yAcceleration) - 0.01f;
            }
            position.z = position.z + (0.1f * yAcceleration);
            this.transform.position = position;
        }

        else
        {
            Vector3 position = this.transform.position;
            position.z = position.z + (0.1f * yAcceleration);
            this.transform.position = position;
        }

        if (Input.GetKey(KeyCode.A))
        { 
            Vector3 position = this.transform.position;
            if (xAcceleration >= -5 && xAcceleration <= 5)
            {
                xAcceleration = (xAcceleration) - 0.01f;
            }
            position.x = position.x + (0.1f * xAcceleration);
            this.transform.position = position;
        }

        else
        {
            Vector3 position = this.transform.position;
            position.x = position.x + (0.1f * xAcceleration);
            this.transform.position = position;
        }

        if (Input.GetKey(KeyCode.D))
        {
            Vector3 position = this.transform.position;
            if (xAcceleration >= -5 && xAcceleration <= 5)
            {
                xAcceleration = (xAcceleration) + 0.01f;
            }
            position.x = position.x + (0.1f * xAcceleration);
            this.transform.position = position;
        }

        else
        {
            Vector3 position = this.transform.position;
            position.x = position.x + (0.1f * xAcceleration);
            this.transform.position = position;
        }


    }   
}

This is the code for the collider and reflection:

public class Collider : MonoBehaviour
{

    public float xA;
    public float yA;

    void OnCollisionEnter(Collision collision)          //If a gameObject enters the collision of another object, this immediately happens once.
    {
        if (gameObject.tag == "Boundary")               //If the gameObject has a tag named Boundary:
        {
            yA = -Movement.yAcceleration;                                                                       //yA stores the value of yAcceleration after being called from script Movement as a negative. Its a reflection.
            Movement.xAcceleration = (Movement.xAcceleration * -Mathf.Atan(yA / Movement.xAcceleration));       //xAcceleration is changed based on this equation: A * artan(A_y / A_x). The 0.000001 was here, adding to A_x to help prevent a 0 as the denominator.
            xA = Movement.xAcceleration;                                                                        //This is declared now...
            Movement.yAcceleration = (Movement.yAcceleration * Mathf.Atan(Movement.yAcceleration / xA));        //This uses xA because Movement.xAcceleration is changed, and the yAcceleration calculation is based on the xAcceleration prior the collision.
            
        }
    }

    void OnCollisionStay(Collision collision)
    {
        if (gameObject.tag == "Boundary")
        {         
            
            yA = Movement.yAcceleration;                                                                        //The same thing happens as before.
            Movement.xAcceleration = (Movement.xAcceleration * -Mathf.Atan(yA / Movement.xAcceleration));
            xA = Movement.xAcceleration;
            Movement.yAcceleration = (Movement.yAcceleration * Mathf.Atan(Movement.yAcceleration / xA));

            Movement.xAcceleration = -Movement.xAcceleration / 2;                                               //On collision, the ball is reflected across the x-axis at half its speed.
            Movement.yAcceleration = Movement.yAcceleration / 2;                                                //yAcceleration is half its original value.
            
        }   
    }
}

The picture below is the game setup. I apologize that it is a link; I do not have enough Reputation to merit a loaded image on this page. Also, if anything is unclear, please message me.

https://i.sstatic.net/VREV4.png

I would really appreciate the help. Thanks!


Solution

  • One very important note here: As soon as there is any Rigidbody involved you do not want to set any values through the .transform - This breaks the physics and collision detection!

    Your Movement should rather alter the behavior of the Rigidbody e.g. by simply changing its Rigibody.velocity

    I would then also place the collision check directly into the balls's component and check whether you hit a wall ("Boundary")

    Then another note: Your code is currently frame-rate dependent. It means that if your target device runs with only 30 frames per second you will add 0.3 per second to the acceleration. If you run however on a more powerful device that manages to run with 200 frames per second then you add 2 per second.

    You should rather define the de/increase per second and multiply it by Time.deltaTime

    All together maybe something like this

    public class Movement : MonoBehaviour
    {
        // Adjust these settings via the Inspector
        [SerializeField] private float _maxMoveSpeed = 5f;
        [SerializeField] private float _speedIncreasePerSecond = 1f;
    
        // Already reference this via the Inspector
        [SerializeField] private Rigidbody _rigidbody;
    
        private void Awake()
        {
            if(!_rigidbody) _rigidbody = GetComponent<Rigidbody>();
        }
    
        // Get User Input in Update
        private void Update()
        {
            var velocity = _rigidbody.velocity;
            velocity.y = 0;
    
            if (Input.GetKey(KeyCode.W) && velocity.z < _maxMoveSpeed)                            
            {                                                       
                velocity.z += _speedIncreasePerSecond * Time.deltaTime;          
            }
    
            if (Input.GetKey(KeyCode.S) && velocity.z > -_maxMoveSpeed)
            {
                velocity.z -= _speedIncreasePerSecond * Time.deltaTime;
            }
    
            if (Input.GetKey(KeyCode.A) && velocity.x > -_maxMoveSpeed)
            { 
                velocity.x -= _speedIncreasePerSecond * Time.deltaTime;
            }
    
            if (Input.GetKey(KeyCode.D) && velocity.x < _maxMoveSpeed)
            {
                velocity.x += _speedIncreasePerSecond * Time.deltaTime;
            }
    
            // clamp to the max speed in case you move diagonal
            if(velocity.magnitude > _maxMoveSpeed)
            {
                velocity = velocity.normalized * _maxMoveSpeed;
            }
    
            _rigidbody.velocity = velocity;
        }
    }
    

    And then finally simply add a PhysicsMaterial with desired settings to the walls and ball.

    I used Friction = 0f and Bounciness = 0.7f for ball and walls. For slow movements you also might want/have to adjust the Bounce Threshold in the Project's Physics Settings otherwise there will be no bouncing if the velocity is smaller then 2 by default.

    This depends a bit on your definition of "realistic". I disabled gravity so the ball also has no rotation and angular friction:

    enter image description here