From f34116f1da8465851d684620b6b94e0a3f3c0fbc Mon Sep 17 00:00:00 2001
From: Matthew Kosarek
- Now that we have that understanding, we can begin setting up our rigidbody data structure.
+ Now that we have an understanding of these two fundamental fields of physics, we can begin setting up our rigidbody data structure.
The Kinematics Data Structure
- As you can see, the base data structure exactly mirrors what we already know from 2D newtonian physics.
+ As you can see, the base data structure exactly mirrors what we already know from 2D newtonian physics. Every frame, we will have some force applied to our rigidbody. We will use that force to get accleration, which, when differentied with respect to time, yields velocity and, ultimately, the new position of our rigidbody. For all of this to work, of course, we need a constant mass for this object.
struct Rigidbody {
Vector2 force = { 0, 0 };
- Vector2 acceleration = { 0, 0 };
Vector2 velocity = { 0, 0 };
Vector2 position = { 0, 0 };
float32 mass = 1.f;
};
- Now, let's put that Rigidbody data structure to work! As I mentioned earlier, you can think of dynamics as the input to the system. What we're going to do now is add a way to + Now, let's put that Rigidbody data structure to work! As I mentioned earlier, you can think of dynamics as the input to the system. What we're going to do now is add a way apply some sort of force to our rigidbody instantaneously.
struct Rigidbody {
Vector2 force = { 0, 0 };
@@ -115,31 +114,131 @@
Vector2 position = { 0, 0 };
float32 mass = 1.f;
- void applyForce(Vector2 f) {
- force += f;
+ void update(float32 deltaTimeSeconds) {
+ applyGravity(deltaTimeSeconds);
+
+ Vector2 acceleration = force / mass;
+ velocity += (acceleration * deltaTimeSeconds);
+ position += (velocity * deltaTimeSeconds);
+ force = Vector2 { 0.f, 0.f };
}
void applyGravity(float32 deltaTimeSeconds) {
- velocity += (Vector2 { 0.f, -50.f } * deltaTimeSeconds);
+ velocity += (Vector2 { 0.f, -9.8.f } * deltaTimeSeconds);
}
+ void applyForce(Vector2 f) {
+ force += f;
+ }
+};
+
+ We have three new functions here:
+
+ Although it might be good enough for your use case, allow me to explain why the previous approach is neither realistic nor reliable:
+
+ When a force is applied in the real world, it doesn't just get applied for a single moment (i.e. frame) in time: it gets applied over time, or for a given duration of time. At the moment, our current implementation fails to account for this. forces are applied for a given frame, and then forgotten about.
+
+ Our current approach has another problem too: the applied force is not frame-rate independent. If you were to apply a force of 50N in the Y direction right now, slower computers would experience larger resultant velocities because their deltaTimeSeconds would be much larger. This is generally something that you'd want to avoid in most applications.
+
+ One potential fix for this is to use impulses:
+
+
struct Impulse {
+ Vector2 force = { 0, 0 };
+ float32 timeOfApplicationSeconds = 0.25f;
+ float32 timeAppliedSeconds = 0.f;
+ bool isDead = false;
+};
+
+const int32 NUM_IMPULSES = 4;
+
+struct Rigidbody {
+ int32 numImpulses = 0;
+ Impulse activeImpulses[NUM_IMPULSES];
+ Vector2 velocity = { 0, 0 };
+ Vector2 position = { 0, 0 };
+ float32 mass = 1.f;
+
void update(float32 deltaTimeSeconds) {
applyGravity(deltaTimeSeconds);
-
+
+ // Add up all of the forces acting at this moment
+ Vector2 force;
+ for (int32 idx = 0; idx < numImpulses; idx++) {
+ Impulse& i = activeImpulses[idx];
+
+ float32 nextTimeAppliedSeconds = i.timeAppliedSeconds + deltaTimeSeconds;
+ if (nextTimeAppliedSeconds >= i.timeOfApplicationSeconds) {
+ nextTimeAppliedSeconds = i.timeOfApplicationSeconds;
+ i.isDead = true;
+ }
+
+ float32 impulseDtSeconds = nextTimeAppliedSeconds - i.timeAppliedSeconds;
+ force += i.force * impulseDtSeconds;
+ i.timeAppliedSeconds = nextTimeAppliedSeconds;
+ }
+
Vector2 acceleration = force / mass;
- velocity += (acceleration * deltaTimeSeconds);
+ velocity += (acceleration * impulseDtSeconds);
position += (velocity * deltaTimeSeconds);
- force = Vector2 { 0.f, 0.f };
+
+ // Cleanup any impulses that have expired in the mean time
+ for (int32 idx = 0; idx < numImpulses; idx++) {
+ if (activeImpulses[idx].isDead) {
+ for (int j = idx + 1; j < numImpulses; j++) {
+ activeImpulses[j - 1] = activeImpulses[j];
+ }
+
+ idx = idx - 1;
+ numImpulses--;
+ }
+ }
+ }
+
+ void applyGravity(float32 deltaTimeSeconds) {
+ velocity += (Vector2 { 0.f, -9.8f } * deltaTimeSeconds);
+ }
+
+ void applyImpulse(Impulse i) {
+ if (numImpulses > NUM_IMPULSES) {
+ printf("Unable to apply impulse. Buffer full.\n");
+ return;
+ }
+
+ activeImpulses[numImpulses] = i;
+ numImpulses++;
}
};
-
+
+ While a bit more verbose than our previous example, this approach has more reliable behavior. Forces are no longer treated as single moments in time, but rather "forces applied over time". Because we ensure that the force is applied over time, we guarantee that all users see the same amount of force applied, regardless of frame-rate.- That's all there is to a rigidbody system with 2D linear forces. Now let's see it in action. Click 'Play' on the WebAssembly demo below to see a square bouncing around the screen. When you drag the pointer through the square, we will apply a force equivalent to how fast you were moving your mouse in the direction that you were moving it. (The speed is capped in the demo, or else things get a little out of hand.) + Click 'Play' on the WebAssembly demo below to see a square bouncing around the screen. When you drag the pointer through the square, we will apply an impulse equivalent to how fast you were moving your mouse in the direction that you were moving it.