Spheres don't always collide (and some other questions)

Jun 6, 2010 at 5:04 PM

Hi,

I too (like many other here) want to thank you for your work. I've been trying to get my own physics working in my game but failed epicly. A few days ago I decided some things are better left to be done by people who actually know what they're doing. In this case you and your physics engine. So far the integration has been fairly succesful!

But of course I have questions!

At this moment I have many spheres moving towards the center of the screen. Once close to the center they're removed only to be replaced by another which will also move towards center. This is realised by adjusting the acceleration of the rigidbody. Then there's one sphere which is playercontrolled. This one is not affected by acceleration, but by calling ApplyForceImpulse based on the keys pressed by the player. Eventually the player will need to dodge the other spheres. But for now it's more fun to bump in on them now that I have working physics! However, sometimes when I want to bump a sphere the collision is completely ignored. After looking closely I noticed it wasn't just the playersphere, but the other spheres don't always properly collide with each other as well. Then when I move back, to try to hit it again it suddenly works again. This too is happening with collision between the other spheres. Any clues what could cause this behaviour? And more importantly, how to fix it? It's really important the collisions do happen between the player and the other spheres. Collision between the other sphere's are less important (though still a pity if it can't be resolved).

What's the difference between ApplyForceImpulse and ApplyWorldForce?

In the near-future I want to increase and decrease the size of objects. Of course the CollisionShapes will have to scale too. I couldn't find a Scale property in the CollisionGroup, so I guess I'll have to in/decrease the size of each shape within the group myself. Is there harm in doing so, or is there a better way to accomplish this?

Does a RigidBody rely on the existance of collisionshapes? I notice some odd behaviour when I appied forces/impulses to rigidbodies wihtout collisionshapes. 

I'm looking forward to your response. Thanks in advance!

Greetz,
Koen

Coordinator
Jun 6, 2010 at 10:22 PM

Hey Koen,  thanks for trying out my framework!

1) As far as CollisionSpheres not colliding - is there any chance you are moving CollisionShapes by just changing Position/Orientation properties on the shape, associated CollisionGroup or associated RigidBody or by calling UpdateTransform on the associated CollisionGroup/RigidBody?  Collision response is completely based on Velocity/Rotation and updating positions on CollisionShapes by the previous ways won't allow collisions to react correctly.  In any case, try moving shapes by calling RigidBody.ApplyXXXForce/Impusle instead of callin RigidBody.UpdateTransform or modifying the Position/Orientation directly.  The general rule is to place your shapes initially by calling Rigidy.UpdateTransform, but during gameplay, attempt to use the RigidBody.Apply<whatever> methods.

2) The difference between Impulse and Force - ApplyXXXForce/Torque is sort of an accumulator of forces that get applied at the beginning of the next simulation step.  ApplyXXXImpulse gets applied at the instance that it's called.  Since, collisions and other constraints are iteratively solved, the impulse methods are normally used during contraint solving so forces are applied instantaneously and can affect subsequent iterations.  But as far as when to use which - If you need immediate results of a force application, use ApplyXXImpluse.  Otherwise, use ApplyXXForce/torque.

3) Changing the scale of a CollisionSphere (Radius), may require a call to the associated RigidBody.CalculateMassProperties

4) RigidBodies don't rely on CollisionShapes but you'll need to set the Mass and IntertiaTensor properties in order to get better simulation.

Good luck!

Jun 7, 2010 at 8:56 PM

Thank you for your fast response!

I doublechecked the code. The initial position is done by UpdateTransform. After that only the Acceleration property and ApplyForceImpulse method are used for updating the position. The playercontrolled sphere's orientation was adjusted using UpdateTransform based on the direction it moves. I've disabled this for now, but the collisions still don't always react properly.

I've tried increasing the 'detail' of the physicsengine by decreasing the TargetStepTime to 1/16th of a frametime. This should cause the physicscomponent to run in 16 smaller steps instead of one big step, yes? I've also increased CollisionIterationCount to 4 and ContactIterationCount to 10. This did not seem to have any influence on the ammount of times the collisions are not detected.

Perhaps the facts below are relvant as well?

The spheres move on the X and Z coordinates only. All spheres are created on a Y = 0 position and well, because they're spheres they stay there :-).

The spheres have various sizes, ranging radius from 1f to 8f.

Adding and removing of CollisionShapes to Rigidbodies, and Rigidbodies to the PhysicsComponent is a dynamic proces. I do not have a static ammount of spheres pre-created.

Thx again!
Koen

Coordinator
Jun 8, 2010 at 11:57 PM

Can you try not modifying Acceleration but, instead, try using ApplyForce every frame?  Adding and removing rigidbodies shouldn't be an issue.  Let me know the results.

Jun 9, 2010 at 9:35 PM
First thing I noticed was the small spheres are moving and the big ones are not (or barely .. ). I guess that's because ApplyWorldForce takes weight into account, where Acceleration does not? Secondly, the issue still remains... I'm working on a simplefied version of the issue with just a XNA game class and a reference to Oops.XNA.Framework. Once I got that set up I'll post it here. Hopefully it make things clearer why the situation happens :)
Jun 10, 2010 at 9:23 PM

ugh, guess what...

I got approximately the same game, but far more simplefied. Mainly by removing the wrapper layer and removing non-physics related things. And of course all goes well now. I must even say, extremely well! Quite a bit better then I expected from it in the first place. I guess that means yet another compliment in your direction :-)

I guess this is beyond the scope where you can help. Because it's clear now the issue is purely in the wrapper I created. Apperently there's something ugly left. Back to simplefying things until it works then :-)

Thank you for your time, it's truly much appreciated!

Jun 11, 2010 at 12:00 AM
Edited Jun 11, 2010 at 12:03 AM

I just figured out what the problem was. It's the QuadtreeCollisionSystem combined with adding CollisionShapes AFTER adding the rigidbody to the PhysicsComponent. The actual game used a Quadtree and delayed addition of CollisionShapes. The simplefied version of the game used SweepAndPrune and direct addition of shapes.

The Quadtree seems to handle it pretty well when CollisionShapes are added after the rigidbody was added to the PhysicsComponent. But appears to be less accurate in detecting collisions.

The SweepAndPrune system is more precise, but can't handle shapes being added on a later moment. This is easily fixed by removing and re-adding the CollisionGroup to the collisionsystem where it was already member of. Not a very neat way, but it works.

I've added plenty of comments to the test-case I was working on, including an easy 'switch' to test the different cases mentioned here (look for the value "issueMode"). Now you can see what's happening. Perhaps you'll understand why the issues happened, and if they should be fixed within Oops? Just create a new XNA Game, replace the Game Class file with the code below and add a reference to the Oops Framework in the project.

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Oops.Xna.Framework.Physics;
using Oops.Xna.Framework.Physics.CollisionSystems;
using System;
using System.Collections.Generic;

namespace Oops_Physics_Issue
{
    public class Game1 : Microsoft.Xna.Framework.Game
    {
        enum IssueMode
        {
            // Situation how it currently works in my game. 
            // SweepAndPruneCollisionSystem
            // Delayed addition of CollisionShape
            // RigidBody is re-added after CollisionShape is added, causes it to refresh and prevent problems, like issueMode=Nonworking_SweepAndPrune
            Working,

            // Same result as issueMode=Working
            // SweepAndPruneCollisionSystem
            // RigidBody is added to PhysicsComponent after CollisionShape is attached to it. Instantly works fine, no workaround needed.
            Working_neat,

            // Original test case, the reason I start asking questions ;)
            // QuadTreeCollisionSystem
            // Delayed addition of CollisionShape
            // Issue: Sometimes collisions are not detected. Take your time to fly around and bump spheres. The following may happen:
            // Small spheres get completely ignored
            // Bigger spheres get (too much) penetration
            Nonworking_QuadTree,

            // Situation after replacing QuadTreeCollisionSystem with SweepAndPrunCollisionSystem
            // SweepAndPrunCollisionSystem
            // Delayed addition of CollisionShape
            // Issue: Collisions are completely ignored, with exception of the first few created spheres.
            Nonworking_SweepAndPrune // issue when adding the collisionshape, after the rigidbody was added to the physicscomponent
        }

        // Issue you wish to see/test
        IssueMode issueMode = IssueMode.Nonworking_SweepAndPrune;

        // Physics stuff
        PhysicsComponent physics;
        RigidBody player;

        // Render stuff.
        // These roughly make a circle
        static Vector3 vertex0 = Vector3.Forward;
        static Vector3 vertex1 = Vector3.Transform(Vector3.Forward, Quaternion.CreateFromYawPitchRoll((float)(Math.PI / 3), 0, 0));
        static Vector3 vertex2 = Vector3.Transform(Vector3.Forward, Quaternion.CreateFromYawPitchRoll((float)(Math.PI / 3) * 2, 0, 0));
        static Vector3 vertex3 = Vector3.Transform(Vector3.Forward, Quaternion.CreateFromYawPitchRoll((float)(Math.PI / 3) * 3, 0, 0));
        static Vector3 vertex4 = Vector3.Transform(Vector3.Forward, Quaternion.CreateFromYawPitchRoll((float)(Math.PI / 3) * 4, 0, 0));
        static Vector3 vertex5 = Vector3.Transform(Vector3.Forward, Quaternion.CreateFromYawPitchRoll((float)(Math.PI / 3) * 5, 0, 0));

        VertexPositionColor[] vertices = new VertexPositionColor[12] {
            new VertexPositionColor(vertex0, Color.White),
            new VertexPositionColor(vertex2, Color.White),
            new VertexPositionColor(vertex1, Color.White),

            new VertexPositionColor(vertex0, Color.White),
            new VertexPositionColor(vertex3, Color.White),
            new VertexPositionColor(vertex2, Color.White),

            new VertexPositionColor(vertex0, Color.White),
            new VertexPositionColor(vertex4, Color.White),
            new VertexPositionColor(vertex3, Color.White),

            new VertexPositionColor(vertex0, Color.White),
            new VertexPositionColor(vertex5, Color.White),
            new VertexPositionColor(vertex4, Color.White)
        };

        VertexDeclaration vertexDeclaration;
        BasicEffect basicEffect;
        VertexBuffer vertexBuffer;

        // Parameters to affect gravity to the center of the screen.
        // Seems quite a lot, I admit :)
        // The point is, the object circles around the center while steadily getting closer to it.
        // Once very close, the object gets "sucked" in.
        private float maxOrbitVelocity = 13f;
        private float maxOrbitForce = 1.3f;
        private float maxPullVelocity = 2500;
        private float minPullVelocity = 100;
        private const float maxOrbitVelocityDistance = 150;
        private const float minPullVelocityDistance = 75;
        private const float destroyDistance = 25f;

        // Time in seconds to fast forward. Each time numpad +(plus) is pressed, an additional 10 sec is added
        float fastForwardTime = 30; // First 30 seconds are fast :)

        // Sphere spawning sequence stuff
        float timeSinceLastSphere = 0;
        const float timeBetweenSpheres = 0.5f; // 2 spheres per second
        static Random randomizer = new Random(); // Randomizer, for randomized sizes and positions


        public Game1()
        {
            new GraphicsDeviceManager(this);

            if (issueMode == IssueMode.Nonworking_QuadTree)
            {
                // QuadtreeCollisionSystem doesn't seem to work well
                // Spawnradius of the spheres is 300. So a BoundingBox of -500 to 500 should be enough.
                // Not a clue how many levels would be best, 5 should be decent?
                physics = new PhysicsComponent(this, new QuadtreeCollisionSystem(new BoundingBox(Vector3.One * -500, Vector3.One * 500), 5));
            }
            else
            {
                // SweepAndPruneCollisionSystem is working fine, if CollisionShapes are directly added.
                physics = new PhysicsComponent(this, new SweepAndPruneCollisionSystem());
            }

            // Physics will run as part of the update thread of my game, which is already on a different thread.
            physics.AsyncEnabled = false;
            physics.Enabled = true;
        }

        protected override void Initialize()
        {
            base.Initialize();

            // My game uses Xen, which completely wraps up the original XNA Game class. Components are possible, but not adviced.
            // So, just call initialize manually. I read in a different discussion that it should not be a problem.
            physics.Initialize();
        }

        protected override void LoadContent()
        {
            // Some render stuff initialization
            //spriteBatch = new SpriteBatch(GraphicsDevice);

            vertexDeclaration = new VertexDeclaration(GraphicsDevice, VertexPositionColor.VertexElements);

            basicEffect = new BasicEffect(GraphicsDevice, null);
            basicEffect.LightingEnabled = false;

            vertexBuffer = new VertexBuffer(GraphicsDevice, VertexPositionColor.SizeInBytes * 12, BufferUsage.None);
            vertexBuffer.SetData<VertexPositionColor>(vertices);

            // Add the playercontrolled sphere to the game
            player = new RigidBody();
            player.Acceleration = Vector3.Zero;
            player.LinearMotionThreshold = 0;
            player.AngularDamping = 8f;
            player.LinearDamping = 20f;

            CollisionSphere shape = new CollisionSphere();
            shape.Radius = 5f;
            player.CollisionGroup.CollisionShapes.Add(shape);
            player.CalculateMassProperties(1f);

            physics.RigidBodies.Add(player);
        }

        protected override void Update(GameTime gameTime)
        {
            // Fastforward while there's fastforward time
            while (fastForwardTime > 0  && gameTime.ElapsedGameTime.TotalSeconds > 0)
            {
                fastForwardTime -= (float)gameTime.ElapsedGameTime.TotalSeconds;

                Update(gameTime, true);
            }

            // Regular update
            Update(gameTime, false);
        }

        bool plusWasDown = false;
        private void Update(GameTime gameTime, bool isFastForward)
        {
            List<RigidBody> destroyedRigidBodies = new List<RigidBody>();
            Vector3 moveDir;
            KeyboardState state = Keyboard.GetState();

            // Quit game key
            if (state.IsKeyDown(Keys.Escape))
                this.Exit();

            // Fast forward key (Numpad +)
            if (state.IsKeyDown(Keys.Add))
            {
                if (!plusWasDown && !isFastForward)
                {
                    fastForwardTime += 10;
                }
                plusWasDown = true;
            }
            else
                plusWasDown = false;


            // Spawn new spheres
            SpawnSpheres(gameTime);

            foreach (RigidBody r in physics.RigidBodies)
            {
                if (r == player)
                {
                    // Player: Apply movement based on keys
                    moveDir = Vector3.Zero;

                    if (state.IsKeyDown(Keys.W))
                        moveDir += Vector3.Forward;
                    if (state.IsKeyDown(Keys.A))
                        moveDir += Vector3.Left;
                    if (state.IsKeyDown(Keys.S))
                        moveDir += Vector3.Backward;
                    if (state.IsKeyDown(Keys.D))
                        moveDir += Vector3.Right;

                    if (moveDir.Length() > 0)
                    {
                        moveDir = Vector3.Normalize(moveDir);

                        // This is quite a bit of force. This is required for the "arcade" feel of the sphere.
                        // It moves and stops at nearly instantly
                        player.ApplyForceImpulse(moveDir * 20000);
                    }
                }
                else
                {
                    // Sphere: Pull to center of screen
                    if (r.Position.Length() > destroyDistance)
                        r.Acceleration = GetForce(r);
                    else
                        destroyedRigidBodies.Add(r);
                }
            }

            // Remove rigidbodies that reached the centerpoint
            foreach (RigidBody r in destroyedRigidBodies)
            {
                physics.RigidBodies.Remove(r);
            }

            // Physics Update synchronous. 
            // This is deliberate, because the entire update routine allready runs on a separate thread in my game.             
            physics.Update(gameTime);

            base.Update(gameTime);
        }

        private Vector3 GetForce(RigidBody rigidBody)
        {
            // Some calculation to get the right force for pulling and orbitting, based on current position and velocity of the sphere.
            float distance = rigidBody.Position.Length();
            float distanceScalar = MathHelper.Clamp(GetScalar(destroyDistance, maxOrbitVelocityDistance, distance), 0, 1);
            float desiredOrbitVelocity = distanceScalar * maxOrbitVelocity;

            distanceScalar = MathHelper.Clamp(GetScalar(destroyDistance, minPullVelocityDistance, distance), 0, 1);
            float desiredPullVelocity = MathHelper.Clamp((1 - distanceScalar) * maxPullVelocity, minPullVelocity, maxPullVelocity);

            Vector3 orbitDirection = Vector3.Cross(Vector3.Normalize(rigidBody.Position), Vector3.Up);
            float orbitVelocity = Vector3.Dot(orbitDirection, rigidBody.Velocity);
            Vector3 pullDirection = Vector3.Normalize(-rigidBody.Position);
            float pullVelocity = Vector3.Dot(pullDirection, rigidBody.Velocity);

            Vector3 force = Vector3.Zero;
            force += -rigidBody.Velocity * (1 - distanceScalar) * 0.05f;

            if (orbitVelocity < desiredOrbitVelocity)
            {
                force += orbitDirection * maxOrbitForce;
            }

            if (pullVelocity < desiredPullVelocity)
                force += pullDirection * (desiredPullVelocity - pullVelocity) * 0.05f * ((1 - distanceScalar) + 0.2f);

            return force * 5f;
        }

        // Helper method used by GetForce
        public static float GetScalar(float min, float max, float interpolate)
        {
            max -= min;
            interpolate -= min;
            return interpolate / max;
        }

        // A simple as possible draw method
        protected override void Draw(GameTime gameTime)
        {
            GraphicsDevice.Clear(Color.Black);

            GraphicsDevice.VertexDeclaration = vertexDeclaration;
            GraphicsDevice.Vertices[0].SetSource(vertexBuffer, 0, VertexPositionColor.SizeInBytes);

            foreach (RigidBody r in physics.RigidBodies)
            {

                basicEffect.World = Matrix.CreateScale(((CollisionSphere)r.CollisionGroup.CollisionShapes[0]).Radius) * r.Orientation * Matrix.CreateTranslation(r.Position);
                Quaternion cameraOrientation = new Quaternion(-0.7071068f, 0, 0, 0.7071068f);
                basicEffect.View = Matrix.CreateLookAt(Vector3.Up * 500, Vector3.Transform(Vector3.Forward, cameraOrientation) + (Vector3.Up * -500), Vector3.Transform(Vector3.Up, cameraOrientation));
                basicEffect.Projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.ToRadians(45), (float)GraphicsDevice.Viewport.Width / (float)GraphicsDevice.Viewport.Height, 1.0f, 10000.0f);

                basicEffect.Begin();
                foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
                {
                    pass.Begin();
                    GraphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, 12);
                    pass.End();
                }
                basicEffect.End();
            }

            base.Draw(gameTime);
        }

        private void SpawnSpheres(GameTime gameTime)
        {
            RigidBody rigidBody;
            CollisionSphere collisionSphere;
            Vector3 spawnPosition;

            // Time stuff, make sure the 2 spheres per second really is 2 spheres per second
            timeSinceLastSphere += (float)gameTime.ElapsedGameTime.TotalSeconds;
            while (timeSinceLastSphere >= timeBetweenSpheres)
            {
                timeSinceLastSphere -= timeBetweenSpheres;

                // Limit the ammount of spheres. In case you want to see one big chunk of spheres and set the destroyDistance to zero.
                if (physics.RigidBodies.Count < 300)
                {
                    // Random position in a circle around the centre of the screen
                    spawnPosition = Vector3.Transform(Vector3.Forward * 300f, Matrix.CreateFromYawPitchRoll(MathHelper.TwoPi * (float)randomizer.NextDouble(), 0, 0));

                    rigidBody = new RigidBody();
                    rigidBody.Acceleration = Vector3.Zero;
                    rigidBody.LinearDamping = 0.35f;
                    rigidBody.UpdateTransform(ref spawnPosition);

                    // Add rigidbody without collisionshape. This was the original situation. Which causes issues with SweepAndPrune if not refreshed later on
                    if (issueMode != IssueMode.Working_neat)
                        physics.RigidBodies.Add(rigidBody);

                    collisionSphere = new CollisionSphere();
                    collisionSphere.Radius = (float)Math.Pow((float)(randomizer.NextDouble() * 0.3f) + 1f, 8f);
                    rigidBody.CollisionGroup.CollisionShapes.Add(collisionSphere);
                    rigidBody.CalculateMassProperties(1f);

                    // Add rigidbody with collisionShape.
                    if (issueMode == IssueMode.Working_neat)
                        physics.RigidBodies.Add(rigidBody);

                    // Rigidbody allready added, but without shape. Now readd rigidbody to refresh the collisiongroup
                    if (issueMode == IssueMode.Working)
                    {
                        physics.RigidBodies.Remove(rigidBody);
                        physics.RigidBodies.Add(rigidBody);
                    }
                }
            }
        }
    }
}

Coordinator
Jun 11, 2010 at 4:59 AM
Edited Jun 11, 2010 at 5:04 AM

Awesome! Thanks for the work you put in figuring this out.  I'll take a look and see what's what.