juzzlin / DustRacing2D

Dust Racing 2D is a traditional top-down car racing game including a level editor.
http://juzzlin.github.io/DustRacing2D/index.html
GNU General Public License v3.0
297 stars 67 forks source link

Improvement of Physics #37

Open KarlZeilhofer opened 5 years ago

KarlZeilhofer commented 5 years ago

Hi, great Game, just played it half an hour! :+1: Thank you very much for providing such a great content.

Is there anything we can do about the physics of the car? It feels like driving a front motor car with rear wheels drive and 70/30 percent weight distribution between front and rear.

Would love to have a 4 wheel drive with balanced weight distribution :)

Greets, Karl

(v2.0.1 from the ubuntu repo on linux mint)

juzzlin commented 5 years ago

I'm glad to hear you liked the game :)

The behavior of the car is mostly defined by the tire models (tire.hpp/tire.cpp) initialized in the constructor of the car class (car.cpp). I haven't been working on this project for a while, but I think it could be nice to load these car parameters from a JSON or something. I might actually implement that. At some point I didn't know anymore what is good and what is bad (you become blind to your own game) so it would be nice to have someone to give his opinion of the balance of the car etc.

KarlZeilhofer commented 5 years ago

Thanks for that quick answer. I'll give it a try to change parameters in the sources. Very easy to use in Qt is an ini-file (QSettings). Would you accept a pull request, if it implements the car tuning in an QSettings ini-file?

juzzlin commented 5 years ago

I'll accept if you use XML or JSON :) QSettings is kinda deprecated and not the best option for this kind of stuff in my opinion. The game already uses QtXml for tracks. For JSON this is like magic: https://github.com/nlohmann/json and I prefer it to Qt's JSON implementation. That could be just added to src/game/contrib/nlohmann/ or something like that. IMO the most easy-to-use solution as the API is just like std::map.

juzzlin commented 5 years ago

For the default parameters we could have something like:

{
  "power": 5000,
  "mass": 1500,
  "frontFriction": 0.85,
  "rearFriction": 0.95,
  ...
}

Maybe even tire locations.

I also noticed that some basic parameters are in car.hpp in Car::Description. This would indeed need some refactoring :)

KarlZeilhofer commented 5 years ago

Indeed, the json lib from nlohmann looks very impressive. But currently I'm stuck on compiling the game at all, see #39

KarlZeilhofer commented 5 years ago

I'm just collecting my suggestions and thoughts here:

Inertia:

physicsComponent().setMomentOfInertia(desc.mass * 3.0);
for a rectangle, the inertia is 1/12(a^2+b^2) x mass. For a car I suggest 1.4x4m, which gives about 1.5 x mass instead of programmed 3 x mass. This is the inertia for the rotation about the center of gravity. Translated to the rear axle, this gives about 3.75m, if the axle is 1.5m behind the center of gravity.

Suggested value is 1.5, as it feels a lot better during game play.

@juzzlin Do you know, what center of rotation is assumed for that inertia?

Strange usage of angular impulse

m_angularVelocity += totAngularAcceleration * step + m_angularImpulse; in myphysicscomponent.cpp. Here the angular impulse is used, as it would be an angular velocity. If it is a physical impulse, it should be divided by the momentum of inertia first.

Locations of Tires for angular Momentum

Why is the location of front tires different than that of the rear tires?

MCVector3dF Car::leftFrontTireLocation() const
{
    return m_leftFrontTire->location();
}
...
MCVector3dF Car::leftRearTireLocation() const
{
    return MCMathUtil::rotatedVector(m_leftRearTirePos, angle()) + MCVector2dF(location());
}

Both are symmetrically placed around the car's center (of gravity?).

, m_leftFrontTirePos(14, 9, 0)
, m_rightFrontTirePos(14, -9, 0)
, m_leftRearTirePos(-14, 9, 0)
, m_rightRearTirePos(-14, -9, 0)

Even though, the units are a bit strange. This are definitely not meters.
@juzzlin can you say something about the unit used for the tire locations?

EDIT: the whole car has a length of 48 units, and the axle distance is 28, 58% of the length. To compare it with a BMW 1er series, they have 266cm/422cm = 63%. A VW Golf has 258/420 = 61%. The tire distances could be increased a bit therefore.

Unit of linear Dimensions

As the tire position from above, also the calculations for the speed in km/h is strange:

    m_speedInKmh = m_absSpeed * 3.6 * 2.75;

There is some factor of 2.75 (EDIT: is a fuzzy factor to look good) but this applied to a wheel location of 14, would give 5m distance from the center of gravity in driving direction. That's a factor of ~4 off (1.25m would be plausible)

In scene.cpp: static const float METERS_PER_UNIT = 0.05f;
So each Unit in the scene represents 5cm physical length. But that factor doesn't seem to be taken into account for the physics.

Center of Rotation

It seems when steering at higher speeds e.g. to the left side, the tail of the car moves to the right in first place. I have to investigate that further. The test I did: drive fast along a straight line, steer, and observe the distance between the tail and that line. I think this is what gives the feeling of "head dominated" dynamics, like driving a tadpole (German: Kaulquappe) or a car with rear wheel steering.

Braking

The braking distance ist about 4 car lengths with 100km/h speed. with a car of 4m length this is about 16m. That is way too short. About 36m is the minimum for top sports cars.

Gravity

is set in SI units to 9.81m/s² :+1:

Acceleration

Here comes the accelerationFriction in, which is by default 0.75,
set to desc.accelerationFriction = 0.55f * Game::instance().difficultyProfile().accelerationFrictionMultiplier(true);

for humans and set to
desc.accelerationFriction = (0.3f + 0.4f * float(index + 1) / NUM_CARS) * Game::instance().difficultyProfile().accelerationFrictionMultiplier(false); for bots.
The friction results to 0.55 x (0.70, 0.85, 1.0) = (0.39, 0.47, 0.55) for humans for easy, medium and hard profile, refer to float DifficultyProfile::accelerationFrictionMultiplier(bool isHuman) const.

The max Force for acceleration is now calculated by the mass, the gravity and this friction factor - seems to be plausible. Since we have a 2 wheel drive, and best tires have about 1.1 friction and 1.1/2 = 0.55. Why lower skilled players get lower friction, that's somehow strange, but ok.

The engines driving force is calculated with the scaled velocity (in scene units/second), which gives a wrong force by a factor of about ten times too low.

juzzlin commented 5 years ago

The center of rotation is assumed to be the center of the object. Of course that's not entirely correct as many other things.

According to the documentation in PhysicsComponent header the angular "impulse" is supposed to be rad/s. I did this many years ago so I don't remember anymore why is that. Seems to be incorrect.

The tire locations are partly like that because they should also match the graphics. The units are "scene units" and I remember there's a conversion factor somewhere. Yeah...seems to be in MCWorld. There's metersPerUnit() which is then used in some calculations. I had plans to get rid of this but didn't have enough time.

The speed calculation has a magic number to make the value look "better" in the game. The raw speed should be in m/s. I hope it's not some messed up "scene units" / s :)

I like your observations and appreciate all improvements and corrections to the physics calculations.

juzzlin commented 5 years ago

One more thing in the case you haven't noticed. You can run some physics framework tests in the build dir by: $ ctest

It starts the CTest test runner and runs some unit tests that are done with Qt's test framework.

KarlZeilhofer commented 5 years ago

@juzzlin Thanks for your comments and help.

juzzlin commented 5 years ago

I used git grep metersPerUnit to find out the value is being used in MCImpulseGenerator. However, it would be cool if everything was in meters.

You are right about the braking distance. It's way too powerful and also goes to reverse too easily. I think that it was needed so that the computer players could brake hard enough. Of course that could be handled also in some other way like making it realistic for the human players only.

KarlZeilhofer commented 5 years ago

@juzzlin due to this lines

    m_absSpeed   = physicsComponent().speed();
    m_speedInKmh = m_absSpeed * 3.6 * 2.75;

I don't believe the car's speed is in m/s. The length of a car is 48 scene units, which is 2.4m - that's quite short, isn't it? I think the constant METERS_PER_UNIT should be changed from 0.05 to 0.1, then we have a 4.8m long car.

KarlZeilhofer commented 5 years ago

Of course that could be handled also in some other way like making it realistic for the human players only.

:smile: I think, physics should be the same for all players?! Otherwise it's unfair.

juzzlin commented 5 years ago

The linear velocity (calculation) is in m/s, because it depends only on mass, acceleration etc. But the projection to the game scene and its dimensions is another thing :) So, the speed is in m/s, but it's incorrect with respect to the size of the car.

KarlZeilhofer commented 5 years ago

@juzzlin one question: Any suggestions, where I should do the calculations between scene coordinates and the physical coordinates?

I have pushed now all my in-code comments, if you would like to have a look: https://github.com/KarlZeilhofer/DustRacing2D/tree/devs/physics
I'm using the Qt Creators TODO plugin, to locate that comments easily. But you could also grep for TODO and FIXME.

As a first step, I would like to do following things:

juzzlin commented 5 years ago

Looks like a good list.

Maybe it'd be the "best" to call the conversion functions of MCWorld on-the-fly wherever needed..? Of course the perfect solution would be to have everything in SI (except rendering stuff in renderers, view objects and scene) but I guess that would be too big of a change for now :)

KarlZeilhofer commented 5 years ago

You are right with the velocity. But the transformation is needed somewhere.

In first place, I'll try to improve the physics as they are. One big thing I understood now, is that the tires only generate forces, when they already slide. That algorithm seems to be too simple/unstable - somehow. But describing the system isn't an easy task, I think - though I once learned that thing at the university. If you have Qt installed, you may try to run https://github.com/KarlZeilhofer/inverse-tripple-pendulum
It's also a sort of a game :smile:

juzzlin commented 5 years ago

I tried your app and it worked very well. I just wish I understood the math :)

The tire model uses a "trick" I learned somewhere to limit and lose grip and make it slide:

impulse.clampFast(parent().physicsComponent().mass() * 7.0f * m_car.tireWearFactor());

KarlZeilhofer commented 5 years ago

I wrote the pendulum app some years ago, I'm not sure, if I could do the maths today again. :grin:

I think, this code generates the sliding anyway:

        MCVector2dF normalForceVector =
            MCVector2dF::projection(tireVelocityMaxUnityVector, tireAxisVector) *
                (m_isOffTrack ? m_offTrackFriction : m_friction) * 
                    -MCWorld::instance().gravity().k() * parent().physicsComponent().mass(); 

As I understand it, it generates a force proportional to the sliding speed, which is clamped here already, since tireVelocityMaxUnityVector is limited to length 1. Therefore I don't see the necessity for the 2nd limitation you mentioned. Your 2nd limitation could also be written as
mass . 0.7 . g. So you assume a maximum grip coefficient of 70% with g=10m/s². The 1st formula clips basically to m_friction.

juzzlin commented 5 years ago

@KarlZeilhofer Are you still working on this?

KarlZeilhofer commented 5 years ago

Not really. I couldn't manage to convert everything to metric units, and a realistic physics engine is also not easy to implement. Though I've tweaked some values, and in my opinion it is now more fun to play.

I've published my current WIP in commit adfd59a3c093a4e7e7e305221a0c8d3af3a18d6b

juzzlin commented 5 years ago

Would it be possible to make a cleaned-up pull request so it would be easier to see the changes?