Commit 76b04cdd authored by Viktor Lidholt's avatar Viktor Lidholt

Refactor sprite physics, part 1 (#3711)

parent 4d88752c
...@@ -12,7 +12,6 @@ import 'dart:math' as math; ...@@ -12,7 +12,6 @@ import 'dart:math' as math;
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:ui' as ui show Image; import 'dart:ui' as ui show Image;
import 'package:box2d/box2d.dart' as box2d;
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
...@@ -35,13 +34,6 @@ part 'src/node.dart'; ...@@ -35,13 +34,6 @@ part 'src/node.dart';
part 'src/node3d.dart'; part 'src/node3d.dart';
part 'src/node_with_size.dart'; part 'src/node_with_size.dart';
part 'src/particle_system.dart'; part 'src/particle_system.dart';
part 'src/physics_body.dart';
part 'src/physics_collision_groups.dart';
part 'src/physics_debug.dart';
part 'src/physics_group.dart';
part 'src/physics_joint.dart';
part 'src/physics_shape.dart';
part 'src/physics_world.dart';
part 'src/sound.dart'; part 'src/sound.dart';
part 'src/sprite.dart'; part 'src/sprite.dart';
part 'src/sprite_box.dart'; part 'src/sprite_box.dart';
......
...@@ -134,72 +134,10 @@ class Node { ...@@ -134,72 +134,10 @@ class Node {
void set rotation(double rotation) { void set rotation(double rotation) {
assert(rotation != null); assert(rotation != null);
if (_physicsBody != null && (parent is PhysicsWorld || parent is PhysicsGroup)) {
_updatePhysicsRotation(physicsBody, rotation, parent);
return;
}
_rotation = rotation; _rotation = rotation;
invalidateTransformMatrix(); invalidateTransformMatrix();
} }
void _updatePhysicsRotation(PhysicsBody body, double rotation, Node physicsParent) {
PhysicsWorld world = _physicsWorld(physicsParent);
if (world == null) return;
world._updateRotation(body, _rotationToPhysics(rotation, physicsParent));
}
PhysicsWorld _physicsWorld(Node parent) {
if (parent is PhysicsWorld) {
return parent;
}
else if (parent is PhysicsGroup) {
return _physicsWorld(parent.parent);
}
else {
assert(false);
return null;
}
}
double _rotationToPhysics(double rotation, Node physicsParent) {
if (physicsParent is PhysicsWorld) {
return rotation;
} else if (physicsParent is PhysicsGroup) {
return _rotationToPhysics(rotation + physicsParent.rotation, physicsParent.parent);
} else {
assert(false);
return null;
}
}
double _rotationFromPhysics(double rotation, Node physicsParent) {
if (physicsParent is PhysicsWorld) {
return rotation;
} else if (physicsParent is PhysicsGroup) {
return _rotationToPhysics(rotation - physicsParent.rotation, physicsParent.parent);
} else {
assert(false);
return null;
}
}
void _setRotationFromPhysics(double rotation, Node physicsParent) {
assert(rotation != null);
_rotation = _rotationFromPhysics(rotation, physicsParent);
invalidateTransformMatrix();
}
void teleportRotation(double rotation) {
assert(rotation != null);
if (_physicsBody != null && (parent is PhysicsWorld || parent is PhysicsGroup)) {
rotation = _rotationToPhysics(rotation, parent);
_physicsBody._body.setTransform(_physicsBody._body.position, radians(rotation));
_physicsBody._body.angularVelocity = 0.0;
_physicsBody._body.setType(box2d.BodyType.STATIC);
}
_setRotationFromPhysics(rotation, parent);
}
/// The position of this node relative to its parent. /// The position of this node relative to its parent.
/// ///
...@@ -209,74 +147,10 @@ class Node { ...@@ -209,74 +147,10 @@ class Node {
void set position(Point position) { void set position(Point position) {
assert(position != null); assert(position != null);
if (_physicsBody != null && (parent is PhysicsWorld || parent is PhysicsGroup)) {
_updatePhysicsPosition(this.physicsBody, position, parent);
return;
}
_position = position; _position = position;
invalidateTransformMatrix(); invalidateTransformMatrix();
} }
void _updatePhysicsPosition(PhysicsBody body, Point position, Node physicsParent) {
PhysicsWorld world = _physicsWorld(physicsParent);
if (world == null) return;
world._updatePosition(body, _positionToPhysics(position, physicsParent));
}
Point _positionToPhysics(Point position, Node physicsParent) {
if (physicsParent is PhysicsWorld) {
return position;
} else if (physicsParent is PhysicsGroup) {
// Transform the position
Vector4 parentPos = physicsParent.transformMatrix.transform(new Vector4(position.x, position.y, 0.0, 1.0));
Point newPos = new Point(parentPos.x, parentPos.y);
return _positionToPhysics(newPos, physicsParent.parent);
} else {
assert(false);
return null;
}
}
void _setPositionFromPhysics(Point position, Node physicsParent) {
assert(position != null);
_position = _positionFromPhysics(position, physicsParent);
invalidateTransformMatrix();
}
Point _positionFromPhysics(Point position, Node physicsParent) {
if (physicsParent is PhysicsWorld) {
return position;
} else if (physicsParent is PhysicsGroup) {
// Transform the position
Vector4 parentPos = physicsParent._inverseMatrix().transform(new Vector4(position.x, position.y, 0.0, 1.0));
Point newPos = new Point(parentPos.x, parentPos.y);
return _positionToPhysics(newPos, physicsParent.parent);
} else {
assert(false);
return null;
}
}
void teleportPosition(Point position) {
assert(position != null);
PhysicsWorld world = _physicsWorld(parent);
if (_physicsBody != null && (parent is PhysicsWorld || parent is PhysicsGroup)) {
position = _positionToPhysics(position, parent);
_physicsBody._body.setTransform(
new Vector2(
position.x / world.b2WorldToNodeConversionFactor,
position.y / world.b2WorldToNodeConversionFactor
),
_physicsBody._body.getAngle()
);
_physicsBody._body.linearVelocity = new Vector2.zero();
_physicsBody._body.setType(box2d.BodyType.STATIC);
}
_setPositionFromPhysics(position, parent);
}
/// The skew along the x-axis of this node in degrees. /// The skew along the x-axis of this node in degrees.
/// ///
/// myNode.skewX = 45.0; /// myNode.skewX = 45.0;
...@@ -330,30 +204,10 @@ class Node { ...@@ -330,30 +204,10 @@ class Node {
void set scale(double scale) { void set scale(double scale) {
assert(scale != null); assert(scale != null);
if (_physicsBody != null && (parent is PhysicsWorld || parent is PhysicsGroup)) {
_updatePhysicsScale(physicsBody, scale, parent);
}
_scaleX = _scaleY = scale; _scaleX = _scaleY = scale;
invalidateTransformMatrix(); invalidateTransformMatrix();
} }
void _updatePhysicsScale(PhysicsBody body, double scale, Node physicsParent) {
if (physicsParent == null) return;
_physicsWorld(physicsParent)._updateScale(body, _scaleToPhysics(scale, physicsParent));
}
double _scaleToPhysics(double scale, Node physicsParent) {
if (physicsParent is PhysicsWorld) {
return scale;
} else if (physicsParent is PhysicsGroup) {
return _scaleToPhysics(scale * physicsParent.scale, physicsParent.parent);
} else {
assert(false);
return null;
}
}
/// The horizontal scale of this node relative its parent. /// The horizontal scale of this node relative its parent.
/// ///
/// myNode.scaleX = 5.0; /// myNode.scaleX = 5.0;
...@@ -361,7 +215,6 @@ class Node { ...@@ -361,7 +215,6 @@ class Node {
void set scaleX(double scaleX) { void set scaleX(double scaleX) {
assert(scaleX != null); assert(scaleX != null);
assert(physicsBody == null);
_scaleX = scaleX; _scaleX = scaleX;
invalidateTransformMatrix(); invalidateTransformMatrix();
...@@ -374,7 +227,6 @@ class Node { ...@@ -374,7 +227,6 @@ class Node {
void set scaleY(double scaleY) { void set scaleY(double scaleY) {
assert(scaleY != null); assert(scaleY != null);
assert(physicsBody == null);
_scaleY = scaleY; _scaleY = scaleY;
invalidateTransformMatrix(); invalidateTransformMatrix();
...@@ -403,7 +255,6 @@ class Node { ...@@ -403,7 +255,6 @@ class Node {
void addChild(Node child) { void addChild(Node child) {
assert(child != null); assert(child != null);
assert(child._parent == null); assert(child._parent == null);
assert(!(child is PhysicsGroup) || this is PhysicsGroup || this is PhysicsWorld);
assert(() { assert(() {
Node node = this; Node node = this;
...@@ -420,10 +271,6 @@ class Node { ...@@ -420,10 +271,6 @@ class Node {
_childrenLastAddedOrder += 1; _childrenLastAddedOrder += 1;
child._addedOrder = _childrenLastAddedOrder; child._addedOrder = _childrenLastAddedOrder;
if (_spriteBox != null) _spriteBox._registerNode(child); if (_spriteBox != null) _spriteBox._registerNode(child);
if (child is PhysicsGroup) {
child._attachGroup(child, child._world);
}
} }
/// Removes a child from this node. /// Removes a child from this node.
...@@ -436,10 +283,6 @@ class Node { ...@@ -436,10 +283,6 @@ class Node {
child._spriteBox = null; child._spriteBox = null;
if (_spriteBox != null) _spriteBox._deregisterNode(child); if (_spriteBox != null) _spriteBox._deregisterNode(child);
} }
if (child is PhysicsGroup) {
child._detachGroup(child);
}
} }
/// Removes this node from its parent node. /// Removes this node from its parent node.
...@@ -585,7 +428,7 @@ class Node { ...@@ -585,7 +428,7 @@ class Node {
return _transformMatrixBoxToNode; return _transformMatrixBoxToNode;
} }
Matrix4 _inverseMatrix() { Matrix4 inverseTransformMatrix() {
if (_transformMatrixInverse == null) { if (_transformMatrixInverse == null) {
_transformMatrixInverse = new Matrix4.copy(transformMatrix); _transformMatrixInverse = new Matrix4.copy(transformMatrix);
_transformMatrixInverse.invert(); _transformMatrixInverse.invert();
...@@ -791,31 +634,4 @@ class Node { ...@@ -791,31 +634,4 @@ class Node {
bool handleEvent(SpriteBoxEvent event) { bool handleEvent(SpriteBoxEvent event) {
return false; return false;
} }
// Physics
PhysicsBody _physicsBody;
/// The physics body associated with this node. If a physics body is assigned,
/// and the node is a child of a [PhysicsWorld] or a [PhysicsGroup] the
/// node's position and rotation will be controlled by the body.
///
/// myNode.physicsBody = new PhysicsBody(
/// new PhysicsShapeCircle(Point.zero, 20.0)
/// );
PhysicsBody get physicsBody => _physicsBody;
void set physicsBody(PhysicsBody physicsBody) {
if (parent != null) {
assert(parent is PhysicsWorld);
if (physicsBody == null) {
physicsBody._detach();
} else {
physicsBody._attach(parent, this);
}
}
_physicsBody = physicsBody;
}
} }
part of flutter_sprites;
enum PhysicsBodyType {
static,
dynamic
}
/// A physics body can be assigned to any node to make it simulated by physics.
/// The body has a shape, and physical properties such as density, friction,
/// and velocity.
///
/// Bodies can be either dynamic or static. Dynamic bodies will move and rotate
/// the nodes that are associated with it. Static bodies can be moved by moving
/// or animating the node associated with them.
///
/// For a body to be simulated it needs to be associated with a [Node], through
/// the node's physicsBody property. The node also need to be a child of either
/// a [PhysicsWorld] or a [PhysicsGroup] (which in turn is a child of a
/// [PhysicsWorld] or a [Physics Group]).
class PhysicsBody {
PhysicsBody(this.shape, {
this.tag: null,
this.type: PhysicsBodyType.dynamic,
double density: 1.0,
double friction: 0.0,
double restitution: 0.0,
bool isSensor: false,
Offset linearVelocity: Offset.zero,
double angularVelocity: 0.0,
this.linearDampening: 0.0,
double angularDampening: 0.0,
bool allowSleep: true,
bool awake: true,
bool fixedRotation: false,
bool bullet: false,
bool active: true,
this.gravityScale: 1.0,
String collisionCategory: "Default",
List<Object> collisionMask
}) {
this.density = density;
this.friction = friction;
this.restitution = restitution;
this.isSensor = isSensor;
this.linearVelocity = linearVelocity;
this.angularVelocity = angularVelocity;
this.angularDampening = angularDampening;
this.allowSleep = allowSleep;
this.awake = awake;
this.fixedRotation = fixedRotation;
this.bullet = bullet;
this.active = active;
this.collisionCategory = collisionCategory;
this.collisionMask = collisionMask;
}
Vector2 _lastPosition;
double _lastRotation;
Vector2 _targetPosition;
double _targetAngle;
double _scale;
/// An object associated with this body, normally used for detecting
/// collisions.
///
/// myBody.tag = "SpaceShip";
Object tag;
/// The shape of this physics body. The shape cannot be modified once the
/// body is created. If the shape is required to change, create a new body.
///
/// myShape = myBody.shape;
final PhysicsShape shape;
/// The type of the body. This is either [PhysicsBodyType.dynamic] or
/// [PhysicsBodyType.static]. Dynamic bodies are simulated by the physics,
/// static objects affect the physics but are not moved by the physics.
///
/// myBody.type = PhysicsBodyType.static;
PhysicsBodyType type;
double _density;
/// The density of the body, default value is 1.0. The density has no specific
/// unit, instead densities are relative to each other.
///
/// myBody.density = 0.5;
double get density => _density;
void set density(double density) {
_density = density;
if (_body == null)
return;
for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
f.setDensity(density);
}
}
double _friction;
/// The fricion of the body, the default is 0.0 and the value should be in
/// the range of 0.0 to 1.0.
///
/// myBody.friction = 0.4;
double get friction => _friction;
void set friction(double friction) {
_friction = friction;
if (_body == null)
return;
for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
f.setFriction(friction);
}
}
double _restitution;
double get restitution => _restitution;
/// The restitution of the body, the default is 0.0 and the value should be in
/// the range of 0.0 to 1.0.
///
/// myBody.restitution = 0.5;
void set restitution(double restitution) {
_restitution = restitution;
if (_body == null)
return;
for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
f.setRestitution(restitution);
}
}
bool _isSensor;
/// True if the body is a sensor. Sensors doesn't collide with other bodies,
/// but will return collision callbacks. Use a sensor body to detect if two
/// bodies are overlapping.
///
/// myBody.isSensor = true;
bool get isSensor => _isSensor;
void set isSensor(bool isSensor) {
_isSensor = isSensor;
if (_body == null)
return;
for (box2d.Fixture f = _body.getFixtureList(); f != null; f = f.getNext()) {
f.setSensor(isSensor);
}
}
Offset _linearVelocity;
/// The current linear velocity of the body in points / second.
///
/// myBody.velocity = Offset.zero;
Offset get linearVelocity {
if (_body == null)
return _linearVelocity;
else {
double dx = _body.linearVelocity.x * _physicsWorld.b2WorldToNodeConversionFactor;
double dy = _body.linearVelocity.y * _physicsWorld.b2WorldToNodeConversionFactor;
return new Offset(dx, dy);
}
}
void set linearVelocity(Offset linearVelocity) {
_linearVelocity = linearVelocity;
if (_body != null) {
Vector2 vec = new Vector2(
linearVelocity.dx / _physicsWorld.b2WorldToNodeConversionFactor,
linearVelocity.dy / _physicsWorld.b2WorldToNodeConversionFactor
);
_body.linearVelocity = vec;
}
}
double _angularVelocity;
/// The angular velocity of the body in degrees / second.
///
/// myBody.angularVelocity = 0.0;
double get angularVelocity {
if (_body == null)
return _angularVelocity;
else
return _body.angularVelocity;
}
void set angularVelocity(double angularVelocity) {
_angularVelocity = angularVelocity;
if (_body != null) {
_body.angularVelocity = angularVelocity;
}
}
// TODO: Should this be editable in box2d.Body ?
/// Linear dampening, in the 0.0 to 1.0 range, default is 0.0.
///
/// double dampening = myBody.linearDampening;
final double linearDampening;
double _angularDampening;
/// Angular dampening, in the 0.0 to 1.0 range, default is 0.0.
///
/// myBody.angularDampening = 0.1;
double get angularDampening => _angularDampening;
void set angularDampening(double angularDampening) {
_angularDampening = angularDampening;
if (_body != null)
_body.angularDamping = angularDampening;
}
bool _allowSleep;
/// Allows the body to sleep if it hasn't moved.
///
/// myBody.allowSleep = false;
bool get allowSleep => _allowSleep;
void set allowSleep(bool allowSleep) {
_allowSleep = allowSleep;
if (_body != null)
_body.setSleepingAllowed(allowSleep);
}
bool _awake;
/// True if the body is currently awake.
///
/// bool isAwake = myBody.awake;
bool get awake {
if (_body != null)
return _body.isAwake();
else
return _awake;
}
void set awake(bool awake) {
_awake = awake;
if (_body != null)
_body.setAwake(awake);
}
bool _fixedRotation;
/// If true, the body cannot be rotated by the physics simulation.
///
/// myBody.fixedRotation = true;
bool get fixedRotation => _fixedRotation;
void set fixedRotation(bool fixedRotation) {
_fixedRotation = fixedRotation;
if (_body != null)
_body.setFixedRotation(fixedRotation);
}
bool _bullet;
bool get bullet => _bullet;
/// If true, the body cannot pass through other objects when moved at high
/// speed. Bullet bodies are slower to simulate, so only use this option
/// if neccessary.
///
/// myBody.bullet = true;
void set bullet(bool bullet) {
_bullet = bullet;
if (_body != null) {
_body.setBullet(bullet);
}
}
bool _active;
/// An active body is used in the physics simulation. Set this to false if
/// you want to temporarily exclude a body from the simulation.
///
/// myBody.active = false;
bool get active {
if (_body != null)
return _body.isActive();
else
return _active;
}
void set active(bool active) {
_active = active;
if (_body != null)
_body.setActive(active);
}
double gravityScale;
Object _collisionCategory;
/// The collision category assigned to this body. The default value is
/// "Default". The body will only collide with bodies that have the either
/// the [collisionMask] set to null or has the category in the mask.
///
/// myBody.collisionCategory = "Air";
Object get collisionCategory {
return _collisionCategory;
}
void set collisionCategory(Object collisionCategory) {
_collisionCategory = collisionCategory;
_updateFilter();
}
List<Object> _collisionMask;
/// A list of collision categories that this object will collide with. If set
/// to null (the default value) the body will collide with all other bodies.
///
/// myBody.collisionMask = ["Air", "Ground"];
List<Object> get collisionMask => _collisionMask;
void set collisionMask(List<Object> collisionMask) {
_collisionMask = collisionMask;
_updateFilter();
}
box2d.Filter get _b2Filter {
print("_physicsNode: $_physicsWorld groups: ${_physicsWorld._collisionGroups}");
box2d.Filter f = new box2d.Filter();
f.categoryBits = _physicsWorld._collisionGroups.getBitmaskForKeys([_collisionCategory]);
f.maskBits = _physicsWorld._collisionGroups.getBitmaskForKeys(_collisionMask);
print("Filter: $f category: ${f.categoryBits} mask: ${f.maskBits}");
return f;
}
void _updateFilter() {
if (_body != null) {
box2d.Filter filter = _b2Filter;
for (box2d.Fixture fixture = _body.getFixtureList(); fixture != null; fixture = fixture.getNext()) {
fixture.setFilterData(filter);
}
}
}
PhysicsWorld _physicsWorld;
Node _node;
box2d.Body _body;
List<PhysicsJoint> _joints = <PhysicsJoint>[];
bool _attached = false;
/// Applies a force to the body at the [worldPoint] position in world
/// cordinates.
///
/// myBody.applyForce(new Offset(0.0, 100.0), myNode.position);
void applyForce(Offset force, Point worldPoint) {
assert(_body != null);
Vector2 b2Force = new Vector2(
force.dx / _physicsWorld.b2WorldToNodeConversionFactor,
force.dy / _physicsWorld.b2WorldToNodeConversionFactor);
Vector2 b2Point = new Vector2(
worldPoint.x / _physicsWorld.b2WorldToNodeConversionFactor,
worldPoint.y / _physicsWorld.b2WorldToNodeConversionFactor
);
_body.applyForce(b2Force, b2Point);
}
/// Applice a force to the body at the its center of gravity.
///
/// myBody.applyForce(new Offset(0.0, 100.0));
void applyForceToCenter(Offset force) {
assert(_body != null);
Vector2 b2Force = new Vector2(
force.dx / _physicsWorld.b2WorldToNodeConversionFactor,
force.dy / _physicsWorld.b2WorldToNodeConversionFactor);
_body.applyForceToCenter(b2Force);
}
/// Applies a torque to the body.
///
/// myBody.applyTorque(10.0);
void applyTorque(double torque) {
assert(_body != null);
_body.applyTorque(torque / _physicsWorld.b2WorldToNodeConversionFactor);
}
/// Applies a linear impulse to the body at the [worldPoint] position in world
/// cordinates.
///
/// myBody.applyLinearImpulse(new Offset(0.0, 100.0), myNode.position);
void applyLinearImpulse(Offset impulse, Point worldPoint, [bool wake = true]) {
assert(_body != null);
Vector2 b2Impulse = new Vector2(
impulse.dx / _physicsWorld.b2WorldToNodeConversionFactor,
impulse.dy / _physicsWorld.b2WorldToNodeConversionFactor);
Vector2 b2Point = new Vector2(
worldPoint.x / _physicsWorld.b2WorldToNodeConversionFactor,
worldPoint.y / _physicsWorld.b2WorldToNodeConversionFactor
);
_body.applyLinearImpulse(b2Impulse, b2Point, wake);
}
/// Applies an angular impulse to the body.
///
/// myBody.applyAngularImpulse(20.0);
void applyAngularImpulse(double impulse) {
assert(_body != null);
_body.applyAngularImpulse(impulse / _physicsWorld.b2WorldToNodeConversionFactor);
}
void _attach(PhysicsWorld physicsNode, Node node) {
assert(_attached == false);
_physicsWorld = physicsNode;
// Account for physics groups
Point positionWorld = node._positionToPhysics(node.position, node.parent);
double rotationWorld = node._rotationToPhysics(node.rotation, node.parent);
double scaleWorld = node._scaleToPhysics(node.scale, node.parent);
// Update scale
_scale = scaleWorld;
// Create BodyDef
box2d.BodyDef bodyDef = new box2d.BodyDef();
bodyDef.linearVelocity = new Vector2(linearVelocity.dx, linearVelocity.dy);
bodyDef.angularVelocity = angularVelocity;
bodyDef.linearDamping = linearDampening;
bodyDef.angularDamping = angularDampening;
bodyDef.allowSleep = allowSleep;
bodyDef.awake = awake;
bodyDef.fixedRotation = fixedRotation;
bodyDef.bullet = bullet;
bodyDef.active = active;
bodyDef.gravityScale = gravityScale;
if (type == PhysicsBodyType.dynamic)
bodyDef.type = box2d.BodyType.DYNAMIC;
else
bodyDef.type = box2d.BodyType.STATIC;
// Convert to world coordinates and set position and angle
double conv = physicsNode.b2WorldToNodeConversionFactor;
bodyDef.position = new Vector2(positionWorld.x / conv, positionWorld.y / conv);
bodyDef.angle = radians(rotationWorld);
// Create Body
_body = physicsNode.b2World.createBody(bodyDef);
_createFixtures(physicsNode);
_body.userData = this;
_node = node;
_attached = true;
// Attach any joints
for (PhysicsJoint joint in _joints) {
if (joint.bodyA._attached && joint.bodyB._attached) {
joint._attach(physicsNode);
}
}
}
void _createFixtures(PhysicsWorld physicsNode) {
// Create FixtureDef
box2d.FixtureDef fixtureDef = new box2d.FixtureDef();
fixtureDef.friction = friction;
fixtureDef.restitution = restitution;
fixtureDef.density = density;
fixtureDef.isSensor = isSensor;
fixtureDef.filter = _b2Filter;
// Get shapes
List<box2d.Shape> b2Shapes = <box2d.Shape>[];
List<PhysicsShape> physicsShapes = <PhysicsShape>[];
_addB2Shapes(physicsNode, shape, b2Shapes, physicsShapes);
// Create fixtures
for (int i = 0; i < b2Shapes.length; i++) {
box2d.Shape b2Shape = b2Shapes[i];
PhysicsShape physicsShape = physicsShapes[i];
fixtureDef.shape = b2Shape;
box2d.Fixture fixture = _body.createFixtureFromFixtureDef(fixtureDef);
fixture.userData = physicsShape;
}
}
void _detach() {
if (_attached) {
_physicsWorld._bodiesScheduledForDestruction.add(_body);
_attached = false;
}
}
void _updateScale(PhysicsWorld physicsNode) {
// Destroy old fixtures
for (box2d.Fixture fixture = _body.getFixtureList(); fixture != null; fixture = fixture.getNext()) {
_body.destroyFixture(fixture);
}
// Make sure we create new b2Shapes
shape._invalidate();
// Create new fixtures
_createFixtures(physicsNode);
}
void _addB2Shapes(PhysicsWorld physicsNode, PhysicsShape shape, List<box2d.Shape> b2Shapes, List<PhysicsShape> physicsShapes) {
if (shape is PhysicsShapeGroup) {
for (PhysicsShape child in shape.shapes) {
_addB2Shapes(physicsNode, child, b2Shapes, physicsShapes);
}
} else {
b2Shapes.add(shape.getB2Shape(physicsNode, _scale));
physicsShapes.add(shape);
}
}
}
part of flutter_sprites;
class _PhysicsCollisionGroups {
_PhysicsCollisionGroups() {
// Make sure there is a default entry in the groups
getBitmaskForKeys(["Default"]);
}
Map<Object,int> keyLookup = <Object,int>{};
List<Object> getKeysForBitmask(int bitmask) {
List<Object> keys = [];
keyLookup.forEach((Object key, int value) {
if (value & bitmask != 0) {
keys.add(key);
}
});
return keys;
}
int getBitmaskForKeys(List<Object> keys) {
if (keys == null) {
return 0xffff;
}
int bitmask = 0;
for (Object key in keys) {
int value = keyLookup[key];
if (value == null) {
assert(keyLookup.length < 16);
value = 1 << keyLookup.length;
keyLookup[key] = value;
}
bitmask |= value;
}
return bitmask;
}
}
part of flutter_sprites;
class _PhysicsDebugDraw extends box2d.DebugDraw {
_PhysicsDebugDraw(
box2d.ViewportTransform transform,
this.physicsWorld
) : super(transform) {
appendFlags(
box2d.DebugDraw.JOINT_BIT |
box2d.DebugDraw.CENTER_OF_MASS_BIT |
box2d.DebugDraw.WIREFRAME_DRAWING_BIT
);
}
PhysicsWorld physicsWorld;
Canvas canvas;
@override
void drawSegment(Vector2 p1, Vector2 p2, box2d.Color3i color) {
Paint paint = new Paint()
..color = _toColor(color)
..strokeWidth = 1.0;
canvas.drawLine(_toPoint(p1), _toPoint(p2), paint);
}
@override
void drawSolidPolygon(
List<Vector2> vertices,
int vertexCount,
box2d.Color3i color
) {
Path path = new Path();
Point pt = _toPoint(vertices[0]);
path.moveTo(pt.x, pt.y);
for (int i = 1; i < vertexCount; i++) {
pt = _toPoint(vertices[i]);
path.lineTo(pt.x, pt.y);
}
Paint paint = new Paint()..color = _toColor(color);
canvas.drawPath(path, paint);
}
@override
void drawCircle(Vector2 center, num radius, box2d.Color3i color, [Vector2 axis]) {
Paint paint = new Paint()
..color = _toColor(color)
..style = PaintingStyle.stroke
..strokeWidth = 1.0;
canvas.drawCircle(_toPoint(center), _scale(radius), paint);
}
@override
void drawSolidCircle(Vector2 center, num radius, Vector2 axis, box2d.Color3i color) {
Paint paint = new Paint()
..color = _toColor(color);
canvas.drawCircle(_toPoint(center), _scale(radius), paint);
}
@override
void drawPoint(Vector2 point, num radiusOnScreen, box2d.Color3i color) {
drawSolidCircle(point, radiusOnScreen, null, color);
}
@override
void drawParticles(
List<Vector2> centers,
double radius,
List<box2d.ParticleColor> colors,
int count
) {
// TODO: Implement
}
@override
void drawParticlesWireframe(
List<Vector2> centers,
double radius,
List<box2d.ParticleColor> colors,
int count
) {
// TODO: Implement
}
@override
void drawTransform(box2d.Transform xf, box2d.Color3i color) {
drawCircle(xf.p, 0.1, color);
// TODO: Improve
}
@override
void drawStringXY(num x, num y, String s, box2d.Color3i color) {
// TODO: Implement
}
Color _toColor(box2d.Color3i color) {
return new Color.fromARGB(255, color.x, color.y, color.z);
}
Point _toPoint(Vector2 vec) {
return new Point(
vec.x * physicsWorld.b2WorldToNodeConversionFactor,
vec.y * physicsWorld.b2WorldToNodeConversionFactor
);
}
double _scale(double value) {
return value * physicsWorld.b2WorldToNodeConversionFactor;
}
}
part of flutter_sprites;
/// A [Node] that acts as a middle layer between a [PhysicsWorld] and a node
/// with an assigned [PhysicsBody]. The group's transformations are limited to
/// [position], [rotation], and uniform [scale].
///
/// PhysicsGroup group = new PhysicsGroup();
/// myWorld.addChild(group);
/// group.addChild(myNode);
class PhysicsGroup extends Node {
@override
void set scaleX(double scaleX) {
assert(false);
}
@override
void set scaleY(double scaleX) {
assert(false);
}
@override
void set skewX(double scaleX) {
assert(false);
}
@override
void set skewY(double scaleX) {
assert(false);
}
@override
void set physicsBody(PhysicsBody body) {
assert(false);
}
@override
void set position(Point position) {
super.position = position;
_invalidatePhysicsBodies(this);
}
@override
void set rotation(double rotation) {
super.rotation = rotation;
_invalidatePhysicsBodies(this);
}
@override
void set scale(double scale) {
super.scale = scale;
_invalidatePhysicsBodies(this);
}
void _invalidatePhysicsBodies(Node node) {
if (_world == null) return;
if (node.physicsBody != null) {
// TODO: Add to list
_world._bodiesScheduledForUpdate.add(node.physicsBody);
}
for (Node child in node.children) {
_invalidatePhysicsBodies(child);
}
}
@override
void addChild(Node node) {
super.addChild(node);
PhysicsWorld world = _world;
if (node.physicsBody != null && world != null) {
node.physicsBody._attach(world, node);
}
if (node is PhysicsGroup) {
_attachGroup(this, world);
}
}
void _attachGroup(PhysicsGroup group, PhysicsWorld world) {
for (Node child in group.children) {
if (child is PhysicsGroup) {
_attachGroup(child, world);
} else if (child.physicsBody != null) {
child.physicsBody._attach(world, child);
}
}
}
@override
void removeChild(Node node) {
super.removeChild(node);
if (node.physicsBody != null) {
node.physicsBody._detach();
}
if (node is PhysicsGroup) {
_detachGroup(this);
}
}
void _detachGroup(PhysicsGroup group) {
for (Node child in group.children) {
if (child is PhysicsGroup) {
_detachGroup(child);
} else if (child.physicsBody != null) {
child.physicsBody._detach();
}
}
}
PhysicsWorld get _world {
if (this.parent is PhysicsWorld)
return this.parent;
if (this.parent is PhysicsGroup) {
PhysicsGroup group = this.parent;
return group._world;
}
return null;
}
}
part of flutter_sprites;
typedef void PhysicsJointBreakCallback(PhysicsJoint joint);
/// A joint connects two physics bodies and restricts their movements. Some
/// types of joints also support motors that adds forces to the connected
/// bodies.
abstract class PhysicsJoint {
PhysicsJoint(this._bodyA, this._bodyB, this.breakingForce, this.breakCallback) {
bodyA._joints.add(this);
bodyB._joints.add(this);
}
PhysicsBody _bodyA;
/// The first body connected to the joint.
///
/// PhysicsBody body = myJoint.bodyA;
PhysicsBody get bodyA => _bodyA;
PhysicsBody _bodyB;
/// The second body connected to the joint.
///
/// PhysicsBody body = myJoint.bodyB;
PhysicsBody get bodyB => _bodyB;
/// The maximum force the joint can handle before it breaks. If set to null,
/// the joint will never break.
final double breakingForce;
final PhysicsJointBreakCallback breakCallback;
bool _active = true;
box2d.Joint _joint;
PhysicsWorld _physicsWorld;
void _completeCreation() {
if (bodyA._attached && bodyB._attached) {
_attach(bodyA._physicsWorld);
}
}
void _attach(PhysicsWorld physicsNode) {
if (_joint == null) {
_physicsWorld = physicsNode;
_joint = _createB2Joint(physicsNode);
_physicsWorld._joints.add(this);
}
}
void _detach() {
if (_joint != null && _active) {
_physicsWorld.b2World.destroyJoint(_joint);
_joint = null;
_physicsWorld._joints.remove(this);
}
_active = false;
}
box2d.Joint _createB2Joint(PhysicsWorld physicsNode);
/// If the joint is no longer needed, call the the [destroy] method to detach
/// if from its connected bodies.
void destroy() {
_detach();
}
void _checkBreakingForce(double dt) {
if (breakingForce == null) return;
if (_joint != null && _active) {
Vector2 reactionForce = new Vector2.zero();
_joint.getReactionForce(1.0 / dt, reactionForce);
if (breakingForce * breakingForce < reactionForce.length2) {
// Destroy the joint
destroy();
// Notify any observer
if (breakCallback != null)
breakCallback(this);
}
}
}
}
/// The revolute joint can be thought of as a hinge, a pin, or an axle.
/// An anchor point is defined in global space.
///
/// Revolute joints can be given limits so that the bodies can rotate only to a
/// certain point using [lowerAngle], [upperAngle], and [enableLimit].
/// They can also be given a motor using [enableMotore] together with
/// [motorSpeed] and [maxMotorTorque] so that the bodies will try
/// to rotate at a given speed, with a given torque.
///
/// Common uses for revolute joints include:
/// - wheels or rollers
/// - chains or swingbridges (using multiple revolute joints)
/// - rag-doll joints
/// - rotating doors, catapults, levers
///
/// new PhysicsJointRevolute(
/// nodeA.physicsBody,
/// nodeB.physicsBody,
/// nodeB.position
/// );
class PhysicsJointRevolute extends PhysicsJoint {
PhysicsJointRevolute(
PhysicsBody bodyA,
PhysicsBody bodyB,
this._worldAnchor, {
this.lowerAngle: 0.0,
this.upperAngle: 0.0,
this.enableLimit: false,
PhysicsJointBreakCallback breakCallback,
double breakingForce,
bool enableMotor: false,
double motorSpeed: 0.0,
double maxMotorTorque: 0.0
}) : super(bodyA, bodyB, breakingForce, breakCallback) {
_enableMotor = enableMotor;
_motorSpeed = motorSpeed;
_maxMotorTorque = maxMotorTorque;
_completeCreation();
}
final Point _worldAnchor;
/// The lower angle of the limits of this joint, only used if [enableLimit]
/// is set to true.
final double lowerAngle;
/// The upper angle of the limits of this joint, only used if [enableLimit]
/// is set to true.
final double upperAngle;
/// If set to true, the rotation will be limited to a value between
/// [lowerAngle] and [upperAngle].
final bool enableLimit;
bool _enableMotor;
/// By setting enableMotor to true, the joint will automatically rotate, e.g.
/// this can be used for creating an engine for a wheel. For this to be
/// useful you also need to set [motorSpeed] and [maxMotorTorque].
bool get enableMotor => _enableMotor;
void set enableMotor(bool enableMotor) {
_enableMotor = enableMotor;
if (_joint != null) {
box2d.RevoluteJoint revoluteJoint = _joint;
revoluteJoint.enableMotor(enableMotor);
}
}
double _motorSpeed;
/// Sets the motor speed of this joint, will only work if [enableMotor] is
/// set to true and [maxMotorTorque] is set to a non zero value.
double get motorSpeed => _motorSpeed;
void set motorSpeed(double motorSpeed) {
_motorSpeed = motorSpeed;
if (_joint != null) {
box2d.RevoluteJoint revoluteJoint = _joint;
revoluteJoint.setMotorSpeed(radians(motorSpeed));
}
}
double _maxMotorTorque;
double get maxMotorTorque => _maxMotorTorque;
/// Sets the motor torque of this joint, will only work if [enableMotor] is
/// set to true and [motorSpeed] is set to a non zero value.
void set maxMotorTorque(double maxMotorTorque) {
_maxMotorTorque = maxMotorTorque;
if (_joint != null) {
box2d.RevoluteJoint revoluteJoint = _joint;
revoluteJoint.setMaxMotorTorque(maxMotorTorque);
}
}
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
// Create Joint Definition
Vector2 vecAnchor = new Vector2(
_worldAnchor.x / physicsNode.b2WorldToNodeConversionFactor,
_worldAnchor.y / physicsNode.b2WorldToNodeConversionFactor
);
box2d.RevoluteJointDef b2Def = new box2d.RevoluteJointDef();
b2Def.initialize(bodyA._body, bodyB._body, vecAnchor);
b2Def.enableLimit = enableLimit;
b2Def.lowerAngle = lowerAngle;
b2Def.upperAngle = upperAngle;
b2Def.enableMotor = _enableMotor;
b2Def.motorSpeed = _motorSpeed;
b2Def.maxMotorTorque = _maxMotorTorque;
// Create joint
return physicsNode.b2World.createJoint(b2Def);
}
}
/// The prismatic joint is probably more commonly known as a slider joint.
/// The two joined bodies have their rotation held fixed relative to each
/// other, and they can only move along a specified axis.
///
/// Prismatic joints can be given limits so that the bodies can only move
/// along the axis within a specific range. They can also be given a motor so
/// that the bodies will try to move at a given speed, with a given force.
///
/// Common uses for prismatic joints include:
/// - elevators
/// - moving platforms
/// - sliding doors
/// - pistons
///
/// new PhysicsJointPrismatic(
/// nodeA.physicsBody,
/// nodeB.physicsBody,
/// new Offset(0.0, 1.0)
/// );
class PhysicsJointPrismatic extends PhysicsJoint {
PhysicsJointPrismatic(
PhysicsBody bodyA,
PhysicsBody bodyB,
this.axis, {
double breakingForce,
PhysicsJointBreakCallback breakCallback,
bool enableMotor: false,
double motorSpeed: 0.0,
double maxMotorForce: 0.0
}
) : super(bodyA, bodyB, breakingForce, breakCallback) {
_enableMotor = enableMotor;
_motorSpeed = motorSpeed;
_maxMotorForce = maxMotorForce;
_completeCreation();
}
/// Axis that the movement is restricted to (in global space at the time of
/// creation)
final Offset axis;
bool _enableMotor;
/// For the motor to be effective you also need to set [motorSpeed] and
/// [maxMotorForce].
bool get enableMotor => _enableMotor;
void set enableMotor(bool enableMotor) {
_enableMotor = enableMotor;
if (_joint != null) {
box2d.PrismaticJoint prismaticJoint = _joint;
prismaticJoint.enableMotor(enableMotor);
}
}
double _motorSpeed;
/// Sets the motor speed of this joint, will only work if [enableMotor] is
/// set to true and [maxMotorForce] is set to a non zero value.
double get motorSpeed => _motorSpeed;
void set motorSpeed(double motorSpeed) {
_motorSpeed = motorSpeed;
if (_joint != null) {
box2d.PrismaticJoint prismaticJoint = _joint;
prismaticJoint.setMotorSpeed(motorSpeed / _physicsWorld.b2WorldToNodeConversionFactor);
}
}
double _maxMotorForce;
/// Sets the motor force of this joint, will only work if [enableMotor] is
/// set to true and [motorSpeed] is set to a non zero value.
double get maxMotorForce => _maxMotorForce;
void set maxMotorForce(double maxMotorForce) {
_maxMotorForce = maxMotorForce;
if (_joint != null) {
box2d.PrismaticJoint prismaticJoint = _joint;
prismaticJoint.setMaxMotorForce(maxMotorForce / _physicsWorld.b2WorldToNodeConversionFactor);
}
}
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.PrismaticJointDef b2Def = new box2d.PrismaticJointDef();
b2Def.initialize(bodyA._body, bodyB._body, bodyA._body.position, new Vector2(axis.dx, axis.dy));
b2Def.enableMotor = _enableMotor;
b2Def.motorSpeed = _motorSpeed;
b2Def.maxMotorForce = _maxMotorForce;
return physicsNode.b2World.createJoint(b2Def);
}
}
/// The weld joint attempts to constrain all relative motion between two bodies.
///
/// new PhysicsJointWeld(bodyA.physicsJoint, bodyB.physicsJoint)
class PhysicsJointWeld extends PhysicsJoint {
PhysicsJointWeld(
PhysicsBody bodyA,
PhysicsBody bodyB, {
double breakingForce,
PhysicsJointBreakCallback breakCallback,
this.dampening: 0.0,
this.frequency: 0.0
}
) : super(bodyA, bodyB, breakingForce, breakCallback) {
_completeCreation();
}
final double dampening;
final double frequency;
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.WeldJointDef b2Def = new box2d.WeldJointDef();
Vector2 middle = new Vector2(
(bodyA._body.position.x + bodyB._body.position.x) / 2.0,
(bodyA._body.position.y + bodyB._body.position.y) / 2.0
);
b2Def.initialize(bodyA._body, bodyB._body, middle);
b2Def.dampingRatio = dampening;
b2Def.frequencyHz = frequency;
return physicsNode.b2World.createJoint(b2Def);
}
}
/// A pulley is used to create an idealized pulley. The pulley connects two
/// bodies to ground and to each other. As one body goes up, the other goes
/// down.
///
/// The total length of the pulley rope is conserved according to the initial
/// configuration.
///
/// new PhysicsJointPulley(
/// nodeA.physicsBody,
/// nodeB.physicsBody,
/// new Point(0.0, 100.0),
/// new Point(100.0, 100.0),
/// nodeA.position,
/// nodeB.position,
/// 1.0
/// );
class PhysicsJointPulley extends PhysicsJoint {
PhysicsJointPulley(
PhysicsBody bodyA,
PhysicsBody bodyB,
this.groundAnchorA,
this.groundAnchorB,
this.anchorA,
this.anchorB,
this.ratio, {
double breakingForce,
PhysicsJointBreakCallback breakCallback
}
) : super(bodyA, bodyB, breakingForce, breakCallback) {
_completeCreation();
}
final Point groundAnchorA;
final Point groundAnchorB;
final Point anchorA;
final Point anchorB;
final double ratio;
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.PulleyJointDef b2Def = new box2d.PulleyJointDef();
b2Def.initialize(
bodyA._body,
bodyB._body,
_convertPosToVec(groundAnchorA, physicsNode),
_convertPosToVec(groundAnchorB, physicsNode),
_convertPosToVec(anchorA, physicsNode),
_convertPosToVec(anchorB, physicsNode),
ratio
);
return physicsNode.b2World.createJoint(b2Def);
}
}
/// The gear joint can only connect revolute and/or prismatic joints.
///
/// Like the pulley ratio, you can specify a gear ratio. However, in this case
/// the gear ratio can be negative. Also keep in mind that when one joint is a
/// revolute joint (angular) and the other joint is prismatic (translation),
/// and then the gear ratio will have units of length or one over length.
///
/// new PhysicsJointGear(nodeA.physicsBody, nodeB.physicsBody);
class PhysicsJointGear extends PhysicsJoint {
PhysicsJointGear(
PhysicsBody bodyA,
PhysicsBody bodyB, {
double breakingForce,
PhysicsJointBreakCallback breakCallback,
this.ratio: 1.0
}
) : super(bodyA, bodyB, breakingForce, breakCallback) {
_completeCreation();
}
/// The ratio of the rotation for bodyA relative bodyB.
final double ratio;
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.GearJointDef b2Def = new box2d.GearJointDef();
b2Def.bodyA = bodyA._body;
b2Def.bodyB = bodyB._body;
b2Def.ratio = ratio;
return physicsNode.b2World.createJoint(b2Def);
}
}
/// Keeps a fixed distance between two bodies, [anchorA] and [anchorB] are
/// defined in world coordinates.
class PhysicsJointDistance extends PhysicsJoint {
PhysicsJointDistance(
PhysicsBody bodyA,
PhysicsBody bodyB,
this.anchorA,
this.anchorB, {
double breakingForce,
PhysicsJointBreakCallback breakCallback,
this.length,
this.dampening: 0.0,
this.frequency: 0.0
}
) : super(bodyA, bodyB, breakingForce, breakCallback) {
_completeCreation();
}
/// The anchor of bodyA in world coordinates at the time of creation.
final Point anchorA;
/// The anchor of bodyB in world coordinates at the time of creation.
final Point anchorB;
/// The desired distance between the joints, if not passed in at creation
/// it will be set automatically to the distance between the anchors at the
/// time of creation.
final double length;
/// Dampening factor.
final double dampening;
/// Dampening frequency.
final double frequency;
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.DistanceJointDef b2Def = new box2d.DistanceJointDef();
b2Def.initialize(
bodyA._body,
bodyB._body,
_convertPosToVec(anchorA, physicsNode),
_convertPosToVec(anchorB, physicsNode)
);
b2Def.dampingRatio = dampening;
b2Def.frequencyHz = frequency;
if (length != null)
b2Def.length = length / physicsNode.b2WorldToNodeConversionFactor;
return physicsNode.b2World.createJoint(b2Def);
}
}
/// The wheel joint restricts a point on bodyB to a line on bodyA. The wheel
/// joint also optionally provides a suspension spring.
class PhysicsJointWheel extends PhysicsJoint {
PhysicsJointWheel(
PhysicsBody bodyA,
PhysicsBody bodyB,
this.anchor,
this.axis, {
double breakingForce,
PhysicsJointBreakCallback breakCallback,
this.dampening: 0.0,
this.frequency: 0.0
}
) : super(bodyA, bodyB, breakingForce, breakCallback) {
_completeCreation();
}
/// The rotational point in global space at the time of creation.
final Point anchor;
/// The axis which to restrict the movement to.
final Offset axis;
/// Dampening factor.
final double dampening;
/// Dampening frequency.
final double frequency;
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.WheelJointDef b2Def = new box2d.WheelJointDef();
b2Def.initialize(
bodyA._body,
bodyB._body,
_convertPosToVec(anchor, physicsNode),
new Vector2(axis.dx, axis.dy)
);
b2Def.dampingRatio = dampening;
b2Def.frequencyHz = frequency;
return physicsNode.b2World.createJoint(b2Def);
}
}
/// The friction joint is used for top-down friction. The joint provides 2D
/// translational friction and angular friction.
class PhysicsJointFriction extends PhysicsJoint {
PhysicsJointFriction(
PhysicsBody bodyA,
PhysicsBody bodyB,
this.anchor, {
double breakingForce,
PhysicsJointBreakCallback breakCallback,
this.maxForce: 0.0,
this.maxTorque: 0.0
}
) : super(bodyA, bodyB, breakingForce, breakCallback) {
_completeCreation();
}
final Point anchor;
final double maxForce;
final double maxTorque;
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.FrictionJointDef b2Def = new box2d.FrictionJointDef();
b2Def.initialize(
bodyA._body,
bodyB._body,
_convertPosToVec(anchor, physicsNode)
);
b2Def.maxForce = maxForce / physicsNode.b2WorldToNodeConversionFactor;
b2Def.maxTorque = maxTorque / physicsNode.b2WorldToNodeConversionFactor;
return physicsNode.b2World.createJoint(b2Def);
}
}
class PhysicsJointConstantVolume extends PhysicsJoint {
PhysicsJointConstantVolume(
this.bodies, {
double breakingForce,
PhysicsJointBreakCallback breakCallback,
this.dampening,
this.frequency
}
) : super(null, null, breakingForce, breakCallback) {
assert(bodies.length > 2);
_bodyA = bodies[0];
_bodyB = bodies[1];
_completeCreation();
}
final List<PhysicsBody> bodies;
final double dampening;
final double frequency;
@override
box2d.Joint _createB2Joint(PhysicsWorld physicsNode) {
box2d.ConstantVolumeJointDef b2Def = new box2d.ConstantVolumeJointDef();
for (PhysicsBody body in bodies) {
b2Def.addBody(body._body);
}
b2Def.dampingRatio = dampening;
b2Def.frequencyHz = frequency;
return physicsNode.b2World.createJoint(b2Def);
}
}
Vector2 _convertPosToVec(Point pt, PhysicsWorld physicsNode) {
return new Vector2(
pt.x / physicsNode.b2WorldToNodeConversionFactor,
pt.y / physicsNode.b2WorldToNodeConversionFactor
);
}
part of flutter_sprites;
/// Defines the shape of a [PhysicsBody].
abstract class PhysicsShape {
box2d.Shape _b2Shape;
Object userObject;
box2d.Shape getB2Shape(PhysicsWorld node, double scale) {
if (_b2Shape == null) {
_b2Shape = _createB2Shape(node, scale);
}
return _b2Shape;
}
box2d.Shape _createB2Shape(PhysicsWorld node, double scale);
void _invalidate() {
_b2Shape = null;
}
}
/// Defines a circle shape with a given center [point] and [radius].
///
/// var shape = PhysicsShapeCircle(Point.origin, 20.0);
class PhysicsShapeCircle extends PhysicsShape {
PhysicsShapeCircle(this.point, this.radius);
final Point point;
final double radius;
@override
box2d.Shape _createB2Shape(PhysicsWorld node, double scale) {
box2d.CircleShape shape = new box2d.CircleShape();
shape.p.x = scale * point.x / node.b2WorldToNodeConversionFactor;
shape.p.y = scale * point.y / node.b2WorldToNodeConversionFactor;
shape.radius = scale * radius / node.b2WorldToNodeConversionFactor;
return shape;
}
}
/// Defines a polygon shape from a list of [points];
///
/// var points = [
/// new Point(-10.0, 0.0),
/// new Point(0.0, 10.0),
/// new Point(10.0, 0.0)
/// ];
/// var shape = new PhysicsShapePolygon(points);
class PhysicsShapePolygon extends PhysicsShape {
PhysicsShapePolygon(this.points);
final List<Point> points;
@override
box2d.Shape _createB2Shape(PhysicsWorld node, double scale) {
List<Vector2> vectors = <Vector2>[];
for (Point point in points) {
Vector2 vec = new Vector2(
scale * point.x / node.b2WorldToNodeConversionFactor,
scale * point.y / node.b2WorldToNodeConversionFactor
);
vectors.add(vec);
}
box2d.PolygonShape shape = new box2d.PolygonShape();
shape.set(vectors, vectors.length);
return shape;
}
}
/// Defines a box shape from a [width] and [height].
///
/// var shape = new PhysicsShapeBox(50.0, 100.0);
class PhysicsShapeBox extends PhysicsShape {
PhysicsShapeBox(
this.width,
this.height, [
this.center = Point.origin,
this.rotation = 0.0
]);
final double width;
final double height;
final Point center;
final double rotation;
@override
box2d.Shape _createB2Shape(PhysicsWorld node, double scale) {
box2d.PolygonShape shape = new box2d.PolygonShape();
shape.setAsBox(
scale * width / node.b2WorldToNodeConversionFactor,
scale * height / node.b2WorldToNodeConversionFactor,
new Vector2(
scale * center.x / node.b2WorldToNodeConversionFactor,
scale * center.y / node.b2WorldToNodeConversionFactor
),
radians(rotation)
);
return shape;
}
}
/// Defines a chain shape from a set of [points]. This can be used to create
/// a continuous chain of edges or, if [loop] is set to true, concave polygons.
///
/// var points = [
/// new Point(-10.0, 0.0),
/// new Point(0.0, 10.0),
/// new Point(10.0, 0.0)
/// ];
/// var shape = new PhysicsShapeChain(points);
class PhysicsShapeChain extends PhysicsShape {
PhysicsShapeChain(this.points, [this.loop=false]);
final List<Point> points;
final bool loop;
@override
box2d.Shape _createB2Shape(PhysicsWorld node, double scale) {
List<Vector2> vectors = <Vector2>[];
for (Point point in points) {
Vector2 vec = new Vector2(
scale * point.x / node.b2WorldToNodeConversionFactor,
scale * point.y / node.b2WorldToNodeConversionFactor
);
vectors.add(vec);
}
box2d.ChainShape shape = new box2d.ChainShape();
if (loop)
shape.createLoop(vectors, vectors.length);
else
shape.createChain(vectors, vectors.length);
return shape;
}
}
/// Defines a single edge line shape from [pointA] to [pointB].
///
/// var shape = new PhysicsShapeEdge(
/// new Point(20.0, 20.0),
/// new Point(50.0, 20.0)
/// );
class PhysicsShapeEdge extends PhysicsShape {
PhysicsShapeEdge(this.pointA, this.pointB);
final Point pointA;
final Point pointB;
@override
box2d.Shape _createB2Shape(PhysicsWorld node, double scale) {
box2d.EdgeShape shape = new box2d.EdgeShape();
shape.set(
new Vector2(
scale * pointA.x / node.b2WorldToNodeConversionFactor,
scale * pointA.y / node.b2WorldToNodeConversionFactor
),
new Vector2(
scale * pointB.x / node.b2WorldToNodeConversionFactor,
scale * pointB.y / node.b2WorldToNodeConversionFactor
)
);
return shape;
}
}
/// A group combines several [shapes] into a single shape.
///
/// var s0 = new PhysicsShapeCircle(new Point(-10.0, 0.0), 20.0);
/// var s1 = new PhysicsShapeCircle(new Point(10.0, 0.0), 20.0);
/// var shape = new PhysicsShapeGroup([s0, s1]);
class PhysicsShapeGroup extends PhysicsShape {
PhysicsShapeGroup(this.shapes);
final List<PhysicsShape> shapes;
@override
box2d.Shape _createB2Shape(PhysicsWorld node, double scale) {
return null;
}
@override
void _invalidate() {
for (PhysicsShape shape in shapes) {
shape._invalidate();
}
}
}
part of flutter_sprites;
enum PhysicsContactType {
preSolve,
postSolve,
begin,
end
}
typedef void PhysicsContactCallback(PhysicsContactType type, PhysicsContact contact);
/// 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.
class PhysicsWorld extends Node {
PhysicsWorld(Offset gravity) {
b2World = new box2d.World.withGravity(
new Vector2(
gravity.dx / b2WorldToNodeConversionFactor,
gravity.dy / b2WorldToNodeConversionFactor));
_init();
}
PhysicsWorld.fromB2World(this.b2World, this.b2WorldToNodeConversionFactor) {
_init();
}
void _init() {
_contactHandler = new _ContactHandler(this);
b2World.setContactListener(_contactHandler);
box2d.ViewportTransform transform = new box2d.ViewportTransform(
new Vector2.zero(),
new Vector2.zero(),
1.0
);
_debugDraw = new _PhysicsDebugDraw(transform, this);
b2World.debugDraw = _debugDraw;
}
/// The Box2D world used to perform the physics simulations.
box2d.World b2World;
_ContactHandler _contactHandler;
_PhysicsCollisionGroups _collisionGroups = new _PhysicsCollisionGroups();
List<PhysicsJoint> _joints = <PhysicsJoint>[];
List<box2d.Body> _bodiesScheduledForDestruction = <box2d.Body>[];
List<PhysicsBody> _bodiesScheduledForUpdate = <PhysicsBody>[];
/// If set to true, a debug image of all physics shapes and joints will
/// be drawn on top of the [SpriteBox].
bool drawDebug = false;
Matrix4 _debugDrawTransform ;
_PhysicsDebugDraw _debugDraw;
/// The conversion factor that is used to convert points in the physics world
/// node to points in the Box2D physics simulation.
double b2WorldToNodeConversionFactor = 10.0;
/// The gravity vector used in the simulation.
Offset get gravity {
Vector2 g = b2World.getGravity();
return new Offset(g.x, g.y);
}
void set gravity(Offset gravity) {
// Convert from points/s^2 to m/s^2
b2World.setGravity(new Vector2(gravity.dx / b2WorldToNodeConversionFactor,
gravity.dy / b2WorldToNodeConversionFactor));
}
/// If set to true, objects can fall asleep if the haven't moved in a while.
bool get allowSleep => b2World.isAllowSleep();
void set allowSleep(bool allowSleep) {
b2World.setAllowSleep(allowSleep);
}
/// True if sub stepping should be used in the simulation.
bool get subStepping => b2World.isSubStepping();
void set subStepping(bool subStepping) {
b2World.setSubStepping(subStepping);
}
void _stepPhysics(double dt) {
// 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();
// Remove bodies that were marked for destruction during the update phase
_removeBodiesScheduledForDestruction();
// 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
if (body._lastPosition == null || body._targetPosition == null) {
b2Body.linearVelocity.setZero();
} else {
Vector2 velocity = (body._targetPosition - body._lastPosition) / dt;
b2Body.linearVelocity = velocity;
body._lastPosition = null;
}
// Update angular velocity
if (body._lastRotation == null || body._targetAngle == null) {
b2Body.angularVelocity = 0.0;
} else {
double angularVelocity = (body._targetAngle - body._lastRotation) / dt;
b2Body.angularVelocity = angularVelocity;
body._lastRotation = 0.0;
}
}
// 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;
if (b2Body.getType() == box2d.BodyType.KINEMATIC) {
body._targetPosition = null;
body._targetAngle = null;
}
// Update visual position and rotation
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
);
}
}
// Break joints
for (PhysicsJoint joint in _joints) {
joint._checkBreakingForce(dt);
}
// Remove bodies that were marked for destruction during the simulation
_removeBodiesScheduledForDestruction();
}
void _removeBodiesScheduledForDestruction() {
for (box2d.Body b2Body in _bodiesScheduledForDestruction) {
// Destroy any joints before destroying the body
PhysicsBody body = b2Body.userData;
for (PhysicsJoint joint in body._joints) {
joint._detach();
}
// Destroy the body
b2World.destroyBody(b2Body);
}
_bodiesScheduledForDestruction.clear();
}
void _updatePosition(PhysicsBody body, Point position) {
if (body._lastPosition == null && body.type == PhysicsBodyType.static) {
body._lastPosition = new Vector2.copy(body._body.position);
body._body.setType(box2d.BodyType.KINEMATIC);
}
Vector2 newPos = new Vector2(
position.x / b2WorldToNodeConversionFactor,
position.y / b2WorldToNodeConversionFactor
);
double angle = body._body.getAngle();
if (body.type == PhysicsBodyType.dynamic) {
body._body.setTransform(newPos, angle);
} else {
body._targetPosition = newPos;
body._targetAngle = angle;
}
body._body.setAwake(true);
}
void _updateRotation(PhysicsBody body, double rotation) {
if (body._lastRotation == null)
body._lastRotation = body._body.getAngle();
Vector2 pos = body._body.position;
double newAngle = radians(rotation);
body._body.setTransform(pos, newAngle);
body._body.setAwake(true);
}
void _updateScale(PhysicsBody body, double scale) {
body._scale = scale;
if (body._attached) {
body._updateScale(this);
}
}
@override
void addChild(Node node) {
super.addChild(node);
if (node.physicsBody != null) {
node.physicsBody._attach(this, node);
}
}
@override
void removeChild(Node node) {
super.removeChild(node);
if (node.physicsBody != null) {
node.physicsBody._detach();
}
}
/// 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
/// );
void addContactCallback(PhysicsContactCallback callback, Object tagA, Object tagB, [PhysicsContactType type]) {
_contactHandler.addContactCallback(callback, tagA, tagB, type);
}
@override
void paint(Canvas canvas) {
if (drawDebug) {
_debugDrawTransform = new Matrix4.fromFloat64List(canvas.getTotalMatrix());
}
super.paint(canvas);
}
/// Draws the debug data of the physics world, normally this method isn't
/// invoked directly. Instead, set the [drawDebug] property to true.
void paintDebug(Canvas canvas) {
_debugDraw.canvas = canvas;
b2World.drawDebugData();
}
}
/// 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");
/// }
class PhysicsContact {
PhysicsContact(
this.nodeA,
this.nodeB,
this.shapeA,
this.shapeB,
this.isTouching,
this.isEnabled,
this.touchingPoints,
this.touchingNormal
);
/// The first node as matched in the rules set when adding the callback.
final Node nodeA;
/// The second node as matched in the rules set when adding the callback.
final Node nodeB;
/// The first shape as matched in the rules set when adding the callback.
final PhysicsShape shapeA;
/// The second shape as matched in the rules set when adding the callback.
final PhysicsShape shapeB;
/// True if the two nodes are touching.
final bool isTouching;
/// To ignore the collision to take place, you can set isEnabled to false
/// during the preSolve phase.
bool isEnabled;
/// List of points that are touching, in world coordinates.
final List<Point> touchingPoints;
/// The normal from [shapeA] to [shapeB] at the touchingPoint.
final Offset touchingNormal;
}
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);
PhysicsWorld physicsNode;
List<_ContactCallbackInfo> callbackInfos = <_ContactCallbackInfo>[];
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
List<Point> touchingPoints;
Offset touchingNormal;
// 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);
touchingPoints = <Point>[];
for (Vector2 vec in manifold.points) {
touchingPoints.add(new Point(
vec.x * physicsNode.b2WorldToNodeConversionFactor,
vec.y * physicsNode.b2WorldToNodeConversionFactor
));
}
}
// Create the contact
PhysicsContact contact = new PhysicsContact(
bodyA._node,
bodyB._node,
fixtureA.userData,
fixtureB.userData,
b2Contact.isTouching(),
b2Contact.isEnabled(),
touchingPoints,
touchingNormal
);
// Make callback
info.callback(type, contact);
// Update Box2D contact
b2Contact.setEnabled(contact.isEnabled);
}
}
}
@override
void beginContact(box2d.Contact contact) {
handleCallback(PhysicsContactType.begin, contact, null, null);
}
@override
void endContact(box2d.Contact contact) {
handleCallback(PhysicsContactType.end, contact, null, null);
}
@override
void preSolve(box2d.Contact contact, box2d.Manifold oldManifold) {
handleCallback(PhysicsContactType.preSolve, contact, oldManifold, null);
}
@override
void postSolve(box2d.Contact contact, box2d.ContactImpulse impulse) {
handleCallback(PhysicsContactType.postSolve, contact, null, impulse);
}
}
...@@ -126,8 +126,6 @@ class SpriteBox extends RenderBox { ...@@ -126,8 +126,6 @@ class SpriteBox extends RenderBox {
List<Node> _constrainedNodes; List<Node> _constrainedNodes;
List<PhysicsWorld> _physicsNodes;
Rect _visibleArea; Rect _visibleArea;
Rect get visibleArea { Rect get visibleArea {
...@@ -158,14 +156,12 @@ class SpriteBox extends RenderBox { ...@@ -158,14 +156,12 @@ class SpriteBox extends RenderBox {
void _registerNode(Node node) { void _registerNode(Node node) {
_actionControllers = null; _actionControllers = null;
_eventTargets = null; _eventTargets = null;
_physicsNodes = null;
if (node == null || node.constraints != null) _constrainedNodes = null; if (node == null || node.constraints != null) _constrainedNodes = null;
} }
void _deregisterNode(Node node) { void _deregisterNode(Node node) {
_actionControllers = null; _actionControllers = null;
_eventTargets = null; _eventTargets = null;
_physicsNodes = null;
if (node == null || node.constraints != null) _constrainedNodes = null; if (node == null || node.constraints != null) _constrainedNodes = null;
} }
...@@ -356,17 +352,6 @@ class SpriteBox extends RenderBox { ...@@ -356,17 +352,6 @@ class SpriteBox extends RenderBox {
Matrix4 totalMatrix = new Matrix4.fromFloat64List(canvas.getTotalMatrix()); Matrix4 totalMatrix = new Matrix4.fromFloat64List(canvas.getTotalMatrix());
_rootNode._visit(canvas, totalMatrix); _rootNode._visit(canvas, totalMatrix);
// Draw physics debug
if (_physicsNodes == null)
_rebuildActionControllersAndPhysicsNodes();
for (PhysicsWorld world in _physicsNodes) {
if (world.drawDebug) {
canvas.setMatrix(world._debugDrawTransform.storage);
world.paintDebug(canvas);
}
}
canvas.restore(); canvas.restore();
} }
...@@ -398,7 +383,6 @@ class SpriteBox extends RenderBox { ...@@ -398,7 +383,6 @@ class SpriteBox extends RenderBox {
_callConstraintsPreUpdate(delta); _callConstraintsPreUpdate(delta);
_runActions(delta); _runActions(delta);
_callUpdate(_rootNode, delta); _callUpdate(_rootNode, delta);
_callStepPhysics(delta);
_callConstraintsConstrain(delta); _callConstraintsConstrain(delta);
} }
...@@ -420,13 +404,11 @@ class SpriteBox extends RenderBox { ...@@ -420,13 +404,11 @@ class SpriteBox extends RenderBox {
void _rebuildActionControllersAndPhysicsNodes() { void _rebuildActionControllersAndPhysicsNodes() {
_actionControllers = <ActionController>[]; _actionControllers = <ActionController>[];
_physicsNodes = <PhysicsWorld>[];
_addActionControllersAndPhysicsNodes(_rootNode); _addActionControllersAndPhysicsNodes(_rootNode);
} }
void _addActionControllersAndPhysicsNodes(Node node) { void _addActionControllersAndPhysicsNodes(Node node) {
if (node._actions != null) _actionControllers.add(node._actions); if (node._actions != null) _actionControllers.add(node._actions);
if (node is PhysicsWorld) _physicsNodes.add(node);
for (int i = node.children.length - 1; i >= 0; i--) { for (int i = node.children.length - 1; i >= 0; i--) {
Node child = node.children[i]; Node child = node.children[i];
...@@ -444,15 +426,6 @@ class SpriteBox extends RenderBox { ...@@ -444,15 +426,6 @@ class SpriteBox extends RenderBox {
} }
} }
void _callStepPhysics(double dt) {
if (_physicsNodes == null)
_rebuildActionControllersAndPhysicsNodes();
for (PhysicsWorld physicsNode in _physicsNodes) {
physicsNode._stepPhysics(dt);
}
}
void _callConstraintsPreUpdate(double dt) { void _callConstraintsPreUpdate(double dt) {
if (_constrainedNodes == null) { if (_constrainedNodes == null) {
_constrainedNodes = <Node>[]; _constrainedNodes = <Node>[];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment