Vehicle model driving up the vertical planes

Discuss any questions about BEPUphysics or problems encountered.
Post Reply
FallenShard
Posts: 2
Joined: Tue Apr 26, 2016 5:59 pm

Vehicle model driving up the vertical planes

Post by FallenShard »

Greetings all,

as the thread topic states, we're making a driving game as a student project using MonoGame and BEPU and we have this issue where the car is able to drive up the surfaces orthogonal to the ground plane, e.g. of the buildings (i.e. right wheels on the ground, left wheels on a wall, or the other way around, so the car is tilted 45 deg). Is there any setting in the vehicle model that we've missed or any other way/suggestions to counter this issue? I posted some code and screenshots if they can help.
Basically, the thing below happens when I drive straight, instead of correcting me to drive along the wall.
Image
Image

Also, here's the full code for the "Car" class. World scale is in meters.

Code: Select all

public class Car : GameObject, ICollidable
    {
        public Player Player { get; set; }
        protected Space m_space;

        private const float m_wheelRadius = 0.375f;
        private const float m_wheelWidth = 0.2f;
        protected float m_maxBackwardSpeed = -13;

        protected float m_maxForwardSpeed = 50;
        protected float m_maxTurnAngle = (float)Math.PI / 6;
        protected float m_turnSpeed = MathHelper.Pi; // Turning speed of the wheels in radians per second.
        private readonly float[] m_dimensionsBottom = { 2f, .75f, 4.5f }; // width, height, length
        private readonly float[] m_dimensionsTop = { 2f, .3f, 2f }; // width, height, length
        private const float m_massBottom = 60;
        private const float m_massTop = 1;

        protected readonly int MOTORWHEEL_L = 0, MOTORWHEEL_R = 2;

        protected Vehicle m_vehicle;

        private Model m_bodyModel, m_carGlass;
        private Model[] m_wheelModel;

        protected Vector3 m_position;

        protected bool m_isSteering = false;

        public Vector3 Position { get { return m_position; } }

        public Vector3 SpawnLocation { get; set; }
        // spawn orientation is an angle [-180, 180] in XZ plane, relative to vector (0,0,-1)
        public float SpawnOrientationDeg { get; set;  }

        public Vector3 Velocity { get; private set; }

        public float MaxForwardSpeed { get { return m_maxForwardSpeed; } }

        public Vector3 WorldForwardDirection {
            get { return MathConverter.Convert(WorldForwardDirectionBP); } }
        public Vector3 WorldForwardWheelDirection {
            get { return MathConverter.Convert(WorldForwardWheelDirectionBP); } }
        public BPVector3 WorldForwardDirectionBP {
            get { return m_vehicle.Wheels[0].WorldForwardDirection / m_vehicle.Wheels[0].WorldForwardDirection.Length(); } }
        public BPVector3 WorldForwardWheelDirectionBP {
            get { return m_vehicle.Wheels[1].WorldForwardDirection / m_vehicle.Wheels[1].WorldForwardDirection.Length(); } }

        private float m_health = GameConstants.MaxCarHealth;
        public float Health
        {
            get { return m_health; }
            set { m_health = value; }
        }

        private static Random rng = new Random();

        private List<Powerup> m_inventory;
        public ReadOnlyCollection<Powerup> Inventory
        {
            get { return m_inventory.AsReadOnly(); }
        }

        public double SpeedInKph
        {
            get { return Velocity.Length() * 3600 / 1000; }
        }

        // spawn orientation is an angle [-180, 180] in XZ plane, relative to vector (0,0,-1)
        public Car(Space space, Vector3 spawnLocation, float spawnOrientationDeg)
        {
            m_space = space;
            m_position = spawnLocation;
            SpawnLocation = spawnLocation;
            SpawnOrientationDeg = spawnOrientationDeg;

            var bodies = new List<CompoundShapeEntry>
            {
                new CompoundShapeEntry( new BoxShape(m_dimensionsBottom[0], m_dimensionsBottom[1], m_dimensionsBottom[2]),
                                        new BPVector3(0, 0f, 0f),
                                        m_massBottom),
                new CompoundShapeEntry( new BoxShape(m_dimensionsTop[0], m_dimensionsTop[1], m_dimensionsTop[2]),
                                        new BPVector3(0, .75f / 2 + .3f / 2, .5f),
                                        m_massTop)
            };

            var body = new CompoundBody(bodies, m_massBottom + m_massTop);
            body.CollisionInformation.LocalPosition = new BPVector3(0, 0.6f, 0); // .5f = car stuck, -.5f = car flips easily
            body.Position = MathConverter.Convert(m_position); //At first, just keep it out of the way.
            body.Orientation = 
                BPQuaternion.CreateFromAxisAngle(BPVector3.Up, MathHelper.ToRadians(SpawnOrientationDeg));
            m_vehicle = new Vehicle(body);

            var localWheelRotation = BPQuaternion.CreateFromAxisAngle(new BPVector3(0, 0, 1), MathHelper.PiOver2);
            //The wheel model used is not aligned initially with how a wheel would normally look, so rotate them.
            BPMatrix wheelGraphicRotation = BPMatrix.CreateFromAxisAngle(BPVector3.Forward, MathHelper.PiOver2);
            for (int i = 0; i < 4; i++)
            {
                int factor1 = (i < 2) ? -1 : 1;

                BPVector3 pos;
                Wheel w;
                if (IsFrontWheel(i))
                {
                    pos = new BPVector3(factor1 * 0.659f, -0.05f, -1.479f);
                    w = new Wheel(
                            new CylinderCastWheelShape(m_wheelRadius, m_wheelWidth, localWheelRotation, wheelGraphicRotation, false),
                            new WheelSuspension(2000, 100f, BPVector3.Down, 0.325f, pos),
                            new WheelDrivingMotor(2.5f, 30000, 10000),
                            new WheelBrake(1.5f, 2, .02f),
                            new WheelSlidingFriction(12f, 10f));
                }
                else
                {
                    pos = new BPVector3(factor1 * 0.659f, -0.05f, 1.201f);
                    w = new Wheel(
                        new CylinderCastWheelShape(m_wheelRadius, m_wheelWidth, localWheelRotation, wheelGraphicRotation, false),
                        new WheelSuspension(2000, 100f, BPVector3.Down, 0.325f, pos),
                        new WheelDrivingMotor(2.5f, 30000, 10000),
                        new WheelBrake(1.5f, 15.0f, .02f),
                        new WheelSlidingFriction(8f, 10f));
                }         
                m_vehicle.AddWheel(w);
            }

            foreach (Wheel wheel in m_vehicle.Wheels)
            {
                //This is a cosmetic setting that makes it looks like the car doesn't have antilock brakes.
                wheel.Shape.FreezeWheelsWhileBraking = true;

                //By default, wheels use as many iterations as the space.  By lowering it,
                //performance can be improved at the cost of a little accuracy.
                //However, because the suspension and friction are not really rigid,
                //the lowered accuracy is not so much of a problem.
                wheel.Suspension.SolverSettings.MaximumIterationCount = 1;
                wheel.Brake.SolverSettings.MaximumIterationCount = 1;
                wheel.SlidingFriction.SolverSettings.MaximumIterationCount = 1;
                wheel.DrivingMotor.SolverSettings.MaximumIterationCount = 1;
            }

            space.Add(m_vehicle);

            body.CollisionInformation.Events.PairCreated += OnPairCreated;
            body.CollisionInformation.Tag = new CollisionTag(this);

            m_inventory = new List<Powerup>();
        }

        private bool IsFrontWheel(int i)
        {
            return i % 2 == 1;
        }

        private bool IsBackWheel(int i)
        {
            return !IsFrontWheel(i);
        }

        private void OnPairCreated(EntityCollidable sender, BEPUphysics.BroadPhaseEntries.BroadPhaseEntry other, BEPUphysics.NarrowPhaseSystems.Pairs.NarrowPhasePair pair)
        {
            CollisionTag tag = other.Tag as CollisionTag;
            if(tag != null)
            {
                tag.Owner.CollideWith(this);
            }
        }

        public void CollideWith(Car other)
        {
            float momentumDiff = (other.m_vehicle.Body.LinearMomentum - m_vehicle.Body.LinearMomentum).Length();
            // TODO: other ways to compute damage other than momentum difference?
            // should we take in the relative mass of both cars?
            // or wait until after the collision has been computed, so we can see which car was affected more?
            m_health -= momentumDiff / 100;
        }

        public void CollideWith(FruitStand other)
        {
            // Fruitstands handle all the interesting stuff in this interaction
        }

        public void CollideWith(GameObject other)
        {
            // by default, do nothing when interacting with other objects
            // TODO: maybe take a little bit of damage, if it's a wall or street debris?
        }

        public override void GetContent(FSGame game)
        {
            m_bodyModel = game.Models["carbody"];
            m_carGlass = game.Models["carglass"];
            ObjectBoundingBox = (BoundingBox)((Dictionary<string, object>)m_bodyModel.Tag)["BoundingBox"];
            BoundingBox = ObjectBoundingBox;
            TransformAABB();

            m_wheelModel = new Model[4];

            for (int i = 0; i < 2; i++)
                m_wheelModel[i] = game.Models["carwheel_l"];
            for (int i = 2; i < 4; i++)
                m_wheelModel[i] = game.Models["carwheel_r"];

            RenderingComponents.Add(new CarRenderingComponent(m_bodyModel, m_wheelModel, m_vehicle, this));
            RenderingComponents.Add(new RenderingComponent(m_carGlass, this));
            SoundComponent = new CarSoundComponent(game.SoundEffects["carEngine2"], this);
        }

        public override void Update(GameTime gameTime)
        {
            if(m_health <= 0)
            {
                Reset();
                return;
            }
            //ModelMatrix = Matrix.CreateRotationY(m_angleY) * Matrix.CreateTranslation(m_position);
            if (!m_isSteering)
                Desteer((float)gameTime.ElapsedGameTime.TotalSeconds);
            ModelMatrix = MathConverter.Convert(m_vehicle.Body.WorldTransform);
            m_position = ModelMatrix.Translation;
            TransformAABB();

            Velocity = MathConverter.Convert(m_vehicle.Body.LinearVelocity);
        }

        public virtual void FireMissile(GameActionEventArgs args)
        {
            string missile = "Large scary missile!";
            Console.WriteLine("Firing: " + missile);
        }

        public virtual void GrabStuff(GameActionEventArgs args)
        {
            Console.WriteLine("Grabbed item with id: " + 2 + ". " + "Good stuff.");
        }
        
        public virtual void HelloWorld(GameActionEventArgs args)
        {
            Console.WriteLine("HELLO WORLD!");
        }

        public virtual void UsePowerup(GameActionEventArgs args)
        {
            if (m_inventory.Count > 0)
            {
                Powerup first = m_inventory.First();
                m_inventory.RemoveAt(0);
                first.invoke();
            }
        }

        public void Brake(GameStateEventArgs args)
        {
            if (!args.IsLastEvent)
            {
                //Brake
                //foreach (Wheel wheel in m_vehicle.Wheels)
                //{
                //    wheel.Brake.IsBraking = true;

                //}
                m_vehicle.Wheels[MOTORWHEEL_L].Brake.IsBraking = true;
                m_vehicle.Wheels[MOTORWHEEL_R].Brake.IsBraking = true;
                m_vehicle.Wheels[MOTORWHEEL_L].SlidingFriction.StaticCoefficient = 4f;
                m_vehicle.Wheels[MOTORWHEEL_R].SlidingFriction.StaticCoefficient = 4f;

            }
            else
            {
                ////Release brake
                //foreach (Wheel wheel in m_vehicle.Wheels)
                //{
                //    wheel.Brake.IsBraking = false;
                //}
                m_vehicle.Wheels[1].Brake.IsBraking = false;
                m_vehicle.Wheels[3].Brake.IsBraking = false;
                m_vehicle.Wheels[MOTORWHEEL_L].Brake.IsBraking = false;
                m_vehicle.Wheels[MOTORWHEEL_R].Brake.IsBraking = false;
                m_vehicle.Wheels[MOTORWHEEL_L].SlidingFriction.StaticCoefficient = 10f;
                m_vehicle.Wheels[MOTORWHEEL_R].SlidingFriction.StaticCoefficient = 10f;
            }
        }

        public void SteerLeft(GameStateEventArgs args)
        {
            if (!args.IsLastEvent)
            {
                m_isSteering = true;
                float dt = (float)args.GameTime.ElapsedGameTime.TotalSeconds;
                float angle = Math.Max(m_vehicle.Wheels[1].Shape.SteeringAngle - m_turnSpeed * dt, -m_maxTurnAngle);
                m_vehicle.Wheels[1].Shape.SteeringAngle = angle;
                m_vehicle.Wheels[3].Shape.SteeringAngle = angle;
            }
            else
                m_isSteering = false;
        }

        public void SteerRight(GameStateEventArgs args)
        {
            if (!args.IsLastEvent)
            {
                m_isSteering = true;
                float dt = (float)args.GameTime.ElapsedGameTime.TotalSeconds;
                float angle = Math.Min(m_vehicle.Wheels[1].Shape.SteeringAngle + m_turnSpeed * dt, m_maxTurnAngle);
                m_vehicle.Wheels[1].Shape.SteeringAngle = angle;
                m_vehicle.Wheels[3].Shape.SteeringAngle = angle;
            }
            else
                m_isSteering = false;
        }

        public void RangedSteerLeft(GameRangeEventArgs args)
        {
            if (args.RangeValue != 0)
            {
                m_isSteering = true;
                float dt = (float)args.GameTime.ElapsedGameTime.TotalSeconds;
                float angle = args.RangeValue * Math.Max(m_vehicle.Wheels[1].Shape.SteeringAngle - m_turnSpeed * dt, -m_maxTurnAngle);
                m_vehicle.Wheels[1].Shape.SteeringAngle = angle;
                m_vehicle.Wheels[3].Shape.SteeringAngle = angle;
            }
            else
                m_isSteering = false;
        }

        public void RangedSteerRight(GameRangeEventArgs args)
        {
            if (args.RangeValue != 0)
            {
                m_isSteering = true;
                float dt = (float)args.GameTime.ElapsedGameTime.TotalSeconds;
                float angle = args.RangeValue * Math.Min(m_vehicle.Wheels[1].Shape.SteeringAngle + m_turnSpeed * dt, m_maxTurnAngle);
                m_vehicle.Wheels[1].Shape.SteeringAngle = angle;
                m_vehicle.Wheels[3].Shape.SteeringAngle = angle;
            }
            else
                m_isSteering = false;
        }

        public void Accelerate(GameStateEventArgs args)
        {
            if (!args.IsLastEvent)
            {
                m_vehicle.Wheels[MOTORWHEEL_L].DrivingMotor.TargetSpeed = m_maxForwardSpeed;
                m_vehicle.Wheels[MOTORWHEEL_R].DrivingMotor.TargetSpeed = m_maxForwardSpeed;
            }
            else
            {
                m_vehicle.Wheels[MOTORWHEEL_L].DrivingMotor.TargetSpeed = 0;
                m_vehicle.Wheels[MOTORWHEEL_R].DrivingMotor.TargetSpeed = 0;
            }
        }
        public void AddSpeedBoost()
        {
            // we could call ApplyImpulse(), but we're applying this at the center of mass in the direction of the velocity, so no need to calculate angular impulse
            m_vehicle.Body.LinearVelocity += GameConstants.SpeedBoostAmount * BEPUutilities.Vector3.Normalize(m_vehicle.Body.LinearVelocity);
        }
        public void AddHealth()
        {
            m_health = Math.Min(GameConstants.MaxCarHealth, m_health + GameConstants.HealthPowerupAmount);
        }

        public void Decelerate(GameStateEventArgs args)
        {
            if (!args.IsLastEvent)
            {
                m_vehicle.Wheels[MOTORWHEEL_L].DrivingMotor.TargetSpeed = m_maxBackwardSpeed;
                m_vehicle.Wheels[MOTORWHEEL_R].DrivingMotor.TargetSpeed = m_maxBackwardSpeed;
            }
            else
            {
                m_vehicle.Wheels[MOTORWHEEL_L].DrivingMotor.TargetSpeed = 0;
                m_vehicle.Wheels[MOTORWHEEL_R].DrivingMotor.TargetSpeed = 0;
            }
        }

        private void Desteer(float dt)
        {
            //Neither key was pressed, so de-steer.
            if (m_vehicle.Wheels[1].Shape.SteeringAngle > 0)
            {
                float angle = Math.Max(m_vehicle.Wheels[1].Shape.SteeringAngle - m_turnSpeed * dt, 0);
                m_vehicle.Wheels[1].Shape.SteeringAngle = angle;
                m_vehicle.Wheels[3].Shape.SteeringAngle = angle;
            }
            else
            {
                float angle = Math.Min(m_vehicle.Wheels[1].Shape.SteeringAngle + m_turnSpeed * dt, 0);
                m_vehicle.Wheels[1].Shape.SteeringAngle = angle;
                m_vehicle.Wheels[3].Shape.SteeringAngle = angle;
            }
        }

        public void Reset()
        {
            m_health = GameConstants.MaxCarHealth;
            m_vehicle.Body.LinearMomentum = BPVector3.Zero;
            m_vehicle.Body.AngularMomentum = BPVector3.Zero;
            m_position = SpawnLocation;
            m_vehicle.Body.Position = MathConverter.Convert(SpawnLocation);
            m_vehicle.Body.Orientation = 
                BPQuaternion.CreateFromAxisAngle(BPVector3.Up, MathHelper.ToRadians(SpawnOrientationDeg));
        }

        public void Pickup(Powerup powerup)
        {
            powerup.Owner = this;
            m_inventory.Add(powerup);
        }

        public virtual void Dispose() { }

        public virtual void OnNewRound(object sender, NewRoundEventArgs args)
        {
            throw new NotImplementedException("This method is implemented only in AI car");
        }
    }
Help would be greatly appreciated,
FallenShard
Norbo
Site Admin
Posts: 4929
Joined: Tue Jul 04, 2006 4:45 am

Re: Vehicle model driving up the vertical planes

Post by Norbo »

This tends to happen when the wheel positions are set far enough out that they can hit things before the car body does. Since the vehicle's wheels are actually convex casts and not physical bodies, they'll detect a hit at T=0 as if the suspension was fully compressed, and the car will get pushed up.

Some options:
1) Move the wheels in so the current car body protects the wheels.
2) Modify the car body to have additional collision volumes that cover the top part of the wheel casts.
3) Use a rigid body and constraints vehicle. This will be a bit slower and can be trickier to make stable, but this wallclimbing problem will never happen with physical wheels. The SuspensionCarDemo, SuspensionCarDemo2, TankDemo, and ReverseTrikeDemo show examples of rigid body based vehicles.
FallenShard
Posts: 2
Joined: Tue Apr 26, 2016 5:59 pm

Re: Vehicle model driving up the vertical planes

Post by FallenShard »

Thanks for the quick reply!

We've tried the options 1) and 2), and it didn't lead to any improvements (except that the vehicle started feeling wobbly, which isn't desired). Can we assume that there's no other way to set the vehicle properties to make it work? Otherwise, we'll try to implement the option 3) and see how that works for us.
Norbo
Site Admin
Posts: 4929
Joined: Tue Jul 04, 2006 4:45 am

Re: Vehicle model driving up the vertical planes

Post by Norbo »

I took a closer look at the code. My assumption that it was related to the wheels colliding with external geometry was wrong, sorry; the wheels are already well within the body. The exact same configuration with a rigid body based vehicle would actually produce the same undesired behavior.

The core issue here is that the BEPUphysicsDemos VehicleInput is tuned like an arcade game. Motors like rockets, tires like glue, and the body's center of mass tuned to keep it from flipping under its own immense accelerations.

So here's a few things which should actually help:
1) Reduce the WheelSlidingFriction values. Upon impacting a wall, the force has to go somewhere; if the sliding friction is so high that it's easier to push up, the car will go up. A coefficient of 8-10 basically stops any kind of sideways motion.
2) Reduce ground clearance. Collisions would tend to be lower relative to the center of gravity, and would be more prone to sliding rather than lifting.
3) Reduce the driving motor friction/strength. It's practically unbounded by default, apart from the coefficient. As it is, it could almost climb cliff faces.

A completely realistic car may still climb or flip itself in this particular situation, though. If the above aren't enough, it may require some nonphysical tweaks, like monitoring the roll of the vehicle and applying angular velocity changes to stabilize it.
Post Reply