physics_world.dart 13.4 KB
Newer Older
1
part of flutter_sprites;
2 3 4 5 6 7 8 9 10 11

enum PhysicsContactType {
  preSolve,
  postSolve,
  begin,
  end
}

typedef void PhysicsContactCallback(PhysicsContactType type, PhysicsContact contact);

12 13 14 15 16 17 18 19
/// A [Node] that performs a 2D physics simulation on any children with a
/// [PhysicsBody] attached. To simulate grand children, they need to be placed
/// in a [PhysicsGroup].
///
/// The PhysicsWorld uses Box2D.dart to perform the actual simulation, but
/// wraps its behavior in a way that is more integrated with the sprite node
/// tree. If needed, you can still access the Box2D world through the [b2World]
/// property.
20 21
class PhysicsWorld extends Node {
  PhysicsWorld(Offset gravity) {
22 23 24 25 26 27 28
    b2World = new box2d.World.withGravity(
      new Vector2(
        gravity.dx / b2WorldToNodeConversionFactor,
        gravity.dy / b2WorldToNodeConversionFactor));
    _init();
  }

29
  PhysicsWorld.fromB2World(this.b2World, this.b2WorldToNodeConversionFactor) {
30 31 32 33 34 35
    _init();
  }

  void _init() {
    _contactHandler = new _ContactHandler(this);
    b2World.setContactListener(_contactHandler);
36 37 38 39 40 41 42 43

    box2d.ViewportTransform transform = new box2d.ViewportTransform(
      new Vector2.zero(),
      new Vector2.zero(),
      1.0
    );
    _debugDraw = new _PhysicsDebugDraw(transform, this);
    b2World.debugDraw = _debugDraw;
44 45
  }

46
  /// The Box2D world used to perform the physics simulations.
47 48 49 50
  box2d.World b2World;

  _ContactHandler _contactHandler;

51 52
  _PhysicsCollisionGroups _collisionGroups = new _PhysicsCollisionGroups();

Hixie's avatar
Hixie committed
53
  List<PhysicsJoint> _joints = <PhysicsJoint>[];
54

Hixie's avatar
Hixie committed
55
  List<box2d.Body> _bodiesScheduledForDestruction = <box2d.Body>[];
56

Hixie's avatar
Hixie committed
57
  List<PhysicsBody> _bodiesScheduledForUpdate = <PhysicsBody>[];
58

59 60
  /// If set to true, a debug image of all physics shapes and joints will
  /// be drawn on top of the [SpriteBox].
61 62 63 64
  bool drawDebug = false;

  Matrix4 _debugDrawTransform ;

65 66
  _PhysicsDebugDraw _debugDraw;

67 68
  /// The conversion factor that is used to convert points in the physics world
  /// node to points in the Box2D physics simulation.
69
  double b2WorldToNodeConversionFactor = 10.0;
70

71
  /// The gravity vector used in the simulation.
72 73 74 75 76 77 78 79 80 81 82
  Offset get gravity {
    Vector2 g = b2World.getGravity();
    return new Offset(g.x, g.y);
  }

  set gravity(Offset gravity) {
    // Convert from points/s^2 to m/s^2
    b2World.setGravity(new Vector2(gravity.dx / b2WorldToNodeConversionFactor,
      gravity.dy / b2WorldToNodeConversionFactor));
  }

83
  /// If set to true, objects can fall asleep if the haven't moved in a while.
84 85 86 87 88 89
  bool get allowSleep => b2World.isAllowSleep();

  set allowSleep(bool allowSleep) {
    b2World.setAllowSleep(allowSleep);
  }

90
  /// True if sub stepping should be used in the simulation.
91 92 93 94 95 96 97
  bool get subStepping => b2World.isSubStepping();

  set subStepping(bool subStepping) {
    b2World.setSubStepping(subStepping);
  }

  void _stepPhysics(double dt) {
98 99 100 101 102 103 104 105
    // Update transformations of bodies whose groups have moved
    for (PhysicsBody body in _bodiesScheduledForUpdate) {
      Node node = body._node;
      node._updatePhysicsPosition(body, node.position, node.parent);
      node._updatePhysicsRotation(body, node.rotation, node.parent);
    }
    _bodiesScheduledForUpdate.clear();

106 107 108
    // Remove bodies that were marked for destruction during the update phase
    _removeBodiesScheduledForDestruction();

109 110 111 112 113 114 115 116 117 118 119 120 121
    // Assign velocities and momentum to static and kinetic bodies
    for (box2d.Body b2Body = b2World.bodyList; b2Body != null; b2Body = b2Body.getNext()) {
      // Fetch body
      PhysicsBody body = b2Body.userData;

      // Skip all dynamic bodies
      if (b2Body.getType() == box2d.BodyType.DYNAMIC) {
        body._lastPosition = null;
        body._lastRotation = null;
        continue;
      }

      // Update linear velocity
122
      if (body._lastPosition == null || body._targetPosition == null) {
123 124 125 126 127 128 129 130
        b2Body.linearVelocity.setZero();
      } else {
        Vector2 velocity = (body._targetPosition - body._lastPosition) / dt;
        b2Body.linearVelocity = velocity;
        body._lastPosition = null;
      }

      // Update angular velocity
131
      if (body._lastRotation == null || body._targetAngle == null) {
132 133 134 135 136 137 138 139
        b2Body.angularVelocity = 0.0;
      } else {
        double angularVelocity = (body._targetAngle - body._lastRotation) / dt;
        b2Body.angularVelocity = angularVelocity;
        body._lastRotation = 0.0;
      }
    }

140 141 142 143 144 145 146
    // Calculate a step in the simulation
    b2World.stepDt(dt, 10, 10);

    // Iterate over the bodies
    for (box2d.Body b2Body = b2World.bodyList; b2Body != null; b2Body = b2Body.getNext()) {
      // Update visual position and rotation
      PhysicsBody body = b2Body.userData;
147 148 149 150 151 152 153

      if (b2Body.getType() == box2d.BodyType.KINEMATIC) {
        body._targetPosition = null;
        body._targetAngle = null;
      }

      // Update visual position and rotation
154 155 156 157 158 159 160 161 162 163 164 165 166 167
      if (body.type == PhysicsBodyType.dynamic) {
        body._node._setPositionFromPhysics(
          new Point(
            b2Body.position.x * b2WorldToNodeConversionFactor,
            b2Body.position.y * b2WorldToNodeConversionFactor
          ),
          body._node.parent
        );

        body._node._setRotationFromPhysics(
          degrees(b2Body.getAngle()),
          body._node.parent
        );
      }
168
    }
169

170 171 172 173 174
    // Break joints
    for (PhysicsJoint joint in _joints) {
      joint._checkBreakingForce(dt);
    }

175 176 177 178 179 180
    // Remove bodies that were marked for destruction during the simulation
    _removeBodiesScheduledForDestruction();
  }

  void _removeBodiesScheduledForDestruction() {
    for (box2d.Body b2Body in _bodiesScheduledForDestruction) {
181 182 183 184 185 186 187
      // Destroy any joints before destroying the body
      PhysicsBody body = b2Body.userData;
      for (PhysicsJoint joint in body._joints) {
        joint._detach();
      }

      // Destroy the body
188 189 190
      b2World.destroyBody(b2Body);
    }
    _bodiesScheduledForDestruction.clear();
191 192 193
  }

  void _updatePosition(PhysicsBody body, Point position) {
194 195 196 197
    if (body._lastPosition == null && body.type == PhysicsBodyType.static) {
      body._lastPosition = new Vector2.copy(body._body.position);
      body._body.setType(box2d.BodyType.KINEMATIC);
    }
198

199 200 201 202 203
    Vector2 newPos = new Vector2(
      position.x / b2WorldToNodeConversionFactor,
      position.y / b2WorldToNodeConversionFactor
    );
    double angle = body._body.getAngle();
204 205 206 207 208 209 210

    if (body.type == PhysicsBodyType.dynamic) {
      body._body.setTransform(newPos, angle);
    } else {
      body._targetPosition = newPos;
      body._targetAngle = angle;
    }
211 212 213 214
    body._body.setAwake(true);
  }

  void _updateRotation(PhysicsBody body, double rotation) {
215 216
    if (body._lastRotation == null)
      body._lastRotation = body._body.getAngle();
217

218 219 220 221 222 223
    Vector2 pos = body._body.position;
    double newAngle = radians(rotation);
    body._body.setTransform(pos, newAngle);
    body._body.setAwake(true);
  }

224 225 226 227 228 229 230 231
  void _updateScale(PhysicsBody body, double scale) {
    body._scale = scale;

    if (body._attached) {
      body._updateScale(this);
    }
  }

232 233 234 235 236 237 238 239 240 241 242 243 244 245
  void addChild(Node node) {
    super.addChild(node);
    if (node.physicsBody != null) {
      node.physicsBody._attach(this, node);
    }
  }

  void removeChild(Node node) {
    super.removeChild(node);
    if (node.physicsBody != null) {
      node.physicsBody._detach();
    }
  }

246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
  /// Adds a contact callback, the callback will be invoked when bodies collide
  /// in the world.
  ///
  /// To match specific sets bodies, use the [tagA] and [tagB]
  /// which will be matched to the tag property that is set on the
  /// [PhysicsBody]. If [tagA] or [tagB] is set to null, it will match any
  /// body.
  ///
  /// By default, callbacks are made at four different times during a
  /// collision; preSolve, postSolve, begin, and end. If you are only interested
  /// in one of these events you can pass in a [type].
  ///
  ///     myWorld.addContactCallback(
  ///       (PhysicsContactType type, PhysicsContact contact) {
  ///         print("Collision between ship and asteroid");
  ///       },
  ///       "Ship",
  ///       "Asteroid",
  ///       PhysicsContactType.begin
  ///     );
266 267 268 269
  void addContactCallback(PhysicsContactCallback callback, Object tagA, Object tagB, [PhysicsContactType type]) {
    _contactHandler.addContactCallback(callback, tagA, tagB, type);
  }

Adam Barth's avatar
Adam Barth committed
270
  void paint(Canvas canvas) {
271 272 273
    if (drawDebug) {
      _debugDrawTransform = new Matrix4.fromFloat64List(canvas.getTotalMatrix());
    }
274 275 276
    super.paint(canvas);
  }

277 278
  /// Draws the debug data of the physics world, normally this method isn't
  /// invoked directly. Instead, set the [drawDebug] property to true.
Adam Barth's avatar
Adam Barth committed
279
  void paintDebug(Canvas canvas) {
280 281
    _debugDraw.canvas = canvas;
    b2World.drawDebugData();
282 283 284
  }
}

285 286 287 288 289 290 291
/// Contains information about a physics collision and is normally passed back
/// in callbacks from the [PhysicsWorld].
///
///     void myCallback(PhysicsContactType type, PhysicsContact contact) {
///       if (contact.isTouching)
///         print("Bodies are touching");
///     }
292 293 294 295 296 297 298
class PhysicsContact {
  PhysicsContact(
    this.nodeA,
    this.nodeB,
    this.shapeA,
    this.shapeB,
    this.isTouching,
299 300 301
    this.isEnabled,
    this.touchingPoints,
    this.touchingNormal
302 303
  );

304
  /// The first node as matched in the rules set when adding the callback.
305
  final Node nodeA;
306 307

  /// The second node as matched in the rules set when adding the callback.
308
  final Node nodeB;
309 310

  /// The first shape as matched in the rules set when adding the callback.
311
  final PhysicsShape shapeA;
312 313

  /// The second shape as matched in the rules set when adding the callback.
314
  final PhysicsShape shapeB;
315 316

  /// True if the two nodes are touching.
317
  final isTouching;
318 319 320

  /// To ignore the collision to take place, you can set isEnabled to false
  /// during the preSolve phase.
321
  bool isEnabled;
322 323

  /// List of points that are touching, in world coordinates.
324
  final List<Point> touchingPoints;
325 326

  /// The normal from [shapeA] to [shapeB] at the touchingPoint.
327
  final Offset touchingNormal;
328 329 330 331 332 333 334 335 336 337 338 339 340 341
}

class _ContactCallbackInfo {
  _ContactCallbackInfo(this.callback, this.tagA, this.tagB, this.type);

  PhysicsContactCallback callback;
  Object tagA;
  Object tagB;
  PhysicsContactType type;
}

class _ContactHandler extends box2d.ContactListener {
  _ContactHandler(this.physicsNode);

342
  PhysicsWorld physicsNode;
343

Hixie's avatar
Hixie committed
344
  List<_ContactCallbackInfo> callbackInfos = <_ContactCallbackInfo>[];
345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387

  void addContactCallback(PhysicsContactCallback callback, Object tagA, Object tagB, PhysicsContactType type) {
    callbackInfos.add(new _ContactCallbackInfo(callback, tagA, tagB, type));
  }

  void handleCallback(PhysicsContactType type, box2d.Contact b2Contact, box2d.Manifold oldManifold, box2d.ContactImpulse impulse) {
    // Get info about the contact
    PhysicsBody bodyA = b2Contact.fixtureA.getBody().userData;
    PhysicsBody bodyB = b2Contact.fixtureB.getBody().userData;
    box2d.Fixture fixtureA = b2Contact.fixtureA;
    box2d.Fixture fixtureB = b2Contact.fixtureB;

    // Match callback with added callbacks
    for (_ContactCallbackInfo info in callbackInfos) {
      // Check that type is matching
      if (info.type != null && info.type != type)
        continue;

      // Check if there is a match
      bool matchA = (info.tagA == null) || info.tagA == bodyA.tag;
      bool matchB = (info.tagB == null) || info.tagB == bodyB.tag;

      bool match = (matchA && matchB);
      if (!match) {
        // Check if there is a match if we swap a & b
        bool matchA = (info.tagA == null) || info.tagA == bodyB.tag;
        bool matchB = (info.tagB == null) || info.tagB == bodyA.tag;

        match = (matchA && matchB);
        if (match) {
          // Swap a & b
          PhysicsBody tempBody = bodyA;
          bodyA = bodyB;
          bodyB = tempBody;

          box2d.Fixture tempFixture = fixtureA;
          fixtureA = fixtureB;
          fixtureB = tempFixture;
        }
      }

      if (match) {
        // We have contact and a matched callback, setup contact info
388 389 390 391 392 393 394 395
        List<Point> touchingPoints = null;
        Offset touchingNormal = null;

        // Fetch touching points, if any
        if (b2Contact.isTouching()) {
          box2d.WorldManifold manifold = new box2d.WorldManifold();
          b2Contact.getWorldManifold(manifold);
          touchingNormal = new Offset(manifold.normal.x, manifold.normal.y);
Hixie's avatar
Hixie committed
396
          touchingPoints = <Point>[];
397 398 399 400 401 402 403 404 405
          for (Vector2 vec in manifold.points) {
            touchingPoints.add(new Point(
              vec.x * physicsNode.b2WorldToNodeConversionFactor,
              vec.y * physicsNode.b2WorldToNodeConversionFactor
            ));
          }
        }

        // Create the contact
406 407 408 409 410 411
        PhysicsContact contact = new PhysicsContact(
          bodyA._node,
          bodyB._node,
          fixtureA.userData,
          fixtureB.userData,
          b2Contact.isTouching(),
412 413 414
          b2Contact.isEnabled(),
          touchingPoints,
          touchingNormal
415
        );
416

417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
        // Make callback
        info.callback(type, contact);

        // Update Box2D contact
        b2Contact.setEnabled(contact.isEnabled);
      }
    }
  }

  void beginContact(box2d.Contact contact) {
    handleCallback(PhysicsContactType.begin, contact, null, null);
  }

  void endContact(box2d.Contact contact) {
    handleCallback(PhysicsContactType.end, contact, null, null);
  }

  void preSolve(box2d.Contact contact, box2d.Manifold oldManifold) {
    handleCallback(PhysicsContactType.preSolve, contact, oldManifold, null);
  }
  void postSolve(box2d.Contact contact, box2d.ContactImpulse impulse) {
    handleCallback(PhysicsContactType.postSolve, contact, null, impulse);
  }
}