1456 lines
52 KiB
C#
1456 lines
52 KiB
C#
/*
|
|
* Farseer Physics Engine based on Box2D.XNA port:
|
|
* Copyright (c) 2010 Ian Qvist
|
|
*
|
|
* Box2D.XNA port of Box2D:
|
|
* Copyright (c) 2009 Brandon Furtwangler, Nathan Furtwangler
|
|
*
|
|
* Original source Box2D:
|
|
* Copyright (c) 2006-2009 Erin Catto http://www.gphysics.com
|
|
*
|
|
* This software is provided 'as-is', without any express or implied
|
|
* warranty. In no event will the authors be held liable for any damages
|
|
* arising from the use of this software.
|
|
* Permission is granted to anyone to use this software for any purpose,
|
|
* including commercial applications, and to alter it and redistribute it
|
|
* freely, subject to the following restrictions:
|
|
* 1. The origin of this software must not be misrepresented; you must not
|
|
* claim that you wrote the original software. If you use this software
|
|
* in a product, an acknowledgment in the product documentation would be
|
|
* appreciated but is not required.
|
|
* 2. Altered source versions must be plainly marked as such, and must not be
|
|
* misrepresented as being the original software.
|
|
* 3. This notice may not be removed or altered from any source distribution.
|
|
*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using FarseerPhysics.Collision;
|
|
using FarseerPhysics.Common;
|
|
using FarseerPhysics.Controllers;
|
|
using FarseerPhysics.Dynamics.Contacts;
|
|
using FarseerPhysics.Dynamics.Joints;
|
|
using Microsoft.Xna.Framework;
|
|
|
|
namespace FarseerPhysics.Dynamics
|
|
{
|
|
/// <summary>
|
|
/// Contains filter data that can determine whether an object should be processed or not.
|
|
/// </summary>
|
|
public abstract class FilterData
|
|
{
|
|
public Category DisabledOnCategories = Category.None;
|
|
|
|
public int DisabledOnGroup;
|
|
public Category EnabledOnCategories = Category.All;
|
|
public int EnabledOnGroup;
|
|
|
|
public virtual bool IsActiveOn(Body body)
|
|
{
|
|
if (body == null || !body.Enabled || body.IsStatic)
|
|
return false;
|
|
|
|
if (body.FixtureList == null)
|
|
return false;
|
|
|
|
foreach (Fixture fixture in body.FixtureList)
|
|
{
|
|
//Disable
|
|
if ((fixture.CollisionGroup == DisabledOnGroup) &&
|
|
fixture.CollisionGroup != 0 && DisabledOnGroup != 0)
|
|
return false;
|
|
|
|
if ((fixture.CollisionCategories & DisabledOnCategories) != Category.None)
|
|
return false;
|
|
|
|
if (EnabledOnGroup != 0 || EnabledOnCategories != Category.All)
|
|
{
|
|
//Enable
|
|
if ((fixture.CollisionGroup == EnabledOnGroup) &&
|
|
fixture.CollisionGroup != 0 && EnabledOnGroup != 0)
|
|
return true;
|
|
|
|
if ((fixture.CollisionCategories & EnabledOnCategories) != Category.None &&
|
|
EnabledOnCategories != Category.All)
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the category.
|
|
/// </summary>
|
|
/// <param name="category">The category.</param>
|
|
public void AddDisabledCategory(Category category)
|
|
{
|
|
DisabledOnCategories |= category;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the category.
|
|
/// </summary>
|
|
/// <param name="category">The category.</param>
|
|
public void RemoveDisabledCategory(Category category)
|
|
{
|
|
DisabledOnCategories &= ~category;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether this body ignores the the specified controller.
|
|
/// </summary>
|
|
/// <param name="category">The category.</param>
|
|
/// <returns>
|
|
/// <c>true</c> if the object has the specified category; otherwise, <c>false</c>.
|
|
/// </returns>
|
|
public bool IsInDisabledCategory(Category category)
|
|
{
|
|
return (DisabledOnCategories & category) == category;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the category.
|
|
/// </summary>
|
|
/// <param name="category">The category.</param>
|
|
public void AddEnabledCategory(Category category)
|
|
{
|
|
EnabledOnCategories |= category;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Removes the category.
|
|
/// </summary>
|
|
/// <param name="category">The category.</param>
|
|
public void RemoveEnabledCategory(Category category)
|
|
{
|
|
EnabledOnCategories &= ~category;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines whether this body ignores the the specified controller.
|
|
/// </summary>
|
|
/// <param name="category">The category.</param>
|
|
/// <returns>
|
|
/// <c>true</c> if the object has the specified category; otherwise, <c>false</c>.
|
|
/// </returns>
|
|
public bool IsInEnabledCategory(Category category)
|
|
{
|
|
return (EnabledOnCategories & category) == category;
|
|
}
|
|
}
|
|
|
|
[Flags]
|
|
public enum WorldFlags
|
|
{
|
|
/// <summary>
|
|
/// Flag that indicates a new fixture has been added to the world.
|
|
/// </summary>
|
|
NewFixture = (1 << 0),
|
|
|
|
/// <summary>
|
|
/// Flag that clear the forces after each time step.
|
|
/// </summary>
|
|
ClearForces = (1 << 2),
|
|
|
|
SubStepping = (1 << 4),
|
|
}
|
|
|
|
/// <summary>
|
|
/// The world class manages all physics entities, dynamic simulation,
|
|
/// and asynchronous queries.
|
|
/// </summary>
|
|
public class World
|
|
{
|
|
/// <summary>
|
|
/// Fires whenever a body has been added
|
|
/// </summary>
|
|
public BodyDelegate BodyAdded;
|
|
|
|
/// <summary>
|
|
/// Fires whenever a body has been removed
|
|
/// </summary>
|
|
public BodyDelegate BodyRemoved;
|
|
|
|
internal Queue<Contact> ContactPool = new Queue<Contact>(256);
|
|
|
|
/// <summary>
|
|
/// Fires whenever a fixture has been added
|
|
/// </summary>
|
|
public FixtureDelegate FixtureAdded;
|
|
|
|
/// <summary>
|
|
/// Fires whenever a fixture has been removed
|
|
/// </summary>
|
|
public FixtureDelegate FixtureRemoved;
|
|
|
|
internal WorldFlags Flags;
|
|
|
|
/// <summary>
|
|
/// Fires whenever a joint has been added
|
|
/// </summary>
|
|
public JointDelegate JointAdded;
|
|
|
|
/// <summary>
|
|
/// Fires whenever a joint has been removed
|
|
/// </summary>
|
|
public JointDelegate JointRemoved;
|
|
|
|
public ControllerDelegate ControllerAdded;
|
|
|
|
public ControllerDelegate ControllerRemoved;
|
|
|
|
private float _invDt0;
|
|
public Island Island = new Island();
|
|
private Body[] _stack = new Body[64];
|
|
private bool _stepComplete;
|
|
private HashSet<Body> _bodyAddList = new HashSet<Body>();
|
|
private HashSet<Body> _bodyRemoveList = new HashSet<Body>();
|
|
private HashSet<Joint> _jointAddList = new HashSet<Joint>();
|
|
private HashSet<Joint> _jointRemoveList = new HashSet<Joint>();
|
|
private TOIInput _input = new TOIInput();
|
|
|
|
/// <summary>
|
|
/// If false, the whole simulation stops. It still processes added and removed geometries.
|
|
/// </summary>
|
|
public bool Enabled = true;
|
|
|
|
#if (!SILVERLIGHT)
|
|
private Stopwatch _watch = new Stopwatch();
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="World"/> class.
|
|
/// </summary>
|
|
private World()
|
|
{
|
|
Flags = WorldFlags.ClearForces;
|
|
|
|
ControllerList = new List<Controller>();
|
|
BreakableBodyList = new List<BreakableBody>();
|
|
BodyList = new List<Body>(32);
|
|
JointList = new List<Joint>(32);
|
|
}
|
|
|
|
public World(Vector2 gravity, AABB span)
|
|
: this()
|
|
{
|
|
Gravity = gravity;
|
|
ContactManager = new ContactManager(new QuadTreeBroadPhase(span));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="World"/> class.
|
|
/// </summary>
|
|
/// <param name="gravity">The gravity.</param>
|
|
public World(Vector2 gravity)
|
|
: this()
|
|
{
|
|
ContactManager = new ContactManager(new DynamicTreeBroadPhase());
|
|
Gravity = gravity;
|
|
}
|
|
|
|
public List<Controller> ControllerList { get; private set; }
|
|
|
|
public List<BreakableBody> BreakableBodyList { get; private set; }
|
|
|
|
public float UpdateTime { get; private set; }
|
|
|
|
public float ContinuousPhysicsTime { get; private set; }
|
|
|
|
public float ControllersUpdateTime { get; private set; }
|
|
|
|
public float AddRemoveTime { get; private set; }
|
|
|
|
public float ContactsUpdateTime { get; private set; }
|
|
|
|
public float SolveUpdateTime { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Get the number of broad-phase proxies.
|
|
/// </summary>
|
|
/// <value>The proxy count.</value>
|
|
public int ProxyCount
|
|
{
|
|
get { return ContactManager.BroadPhase.ProxyCount; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Change the global gravity vector.
|
|
/// </summary>
|
|
/// <value>The gravity.</value>
|
|
public Vector2 Gravity;
|
|
|
|
/// <summary>
|
|
/// Set flag to control automatic clearing of forces after each time step.
|
|
/// </summary>
|
|
/// <value><c>true</c> if it should auto clear forces; otherwise, <c>false</c>.</value>
|
|
public bool AutoClearForces
|
|
{
|
|
set
|
|
{
|
|
if (value)
|
|
{
|
|
Flags |= WorldFlags.ClearForces;
|
|
}
|
|
else
|
|
{
|
|
Flags &= ~WorldFlags.ClearForces;
|
|
}
|
|
}
|
|
get { return (Flags & WorldFlags.ClearForces) == WorldFlags.ClearForces; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the contact manager for testing.
|
|
/// </summary>
|
|
/// <value>The contact manager.</value>
|
|
public ContactManager ContactManager { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Get the world body list.
|
|
/// </summary>
|
|
/// <value>Thehead of the world body list.</value>
|
|
public List<Body> BodyList { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Get the world joint list.
|
|
/// </summary>
|
|
/// <value>The joint list.</value>
|
|
public List<Joint> JointList { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Get the world contact list. With the returned contact, use Contact.GetNext to get
|
|
/// the next contact in the world list. A null contact indicates the end of the list.
|
|
/// </summary>
|
|
/// <value>The head of the world contact list.</value>
|
|
public List<Contact> ContactList
|
|
{
|
|
get { return ContactManager.ContactList; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Enable/disable single stepped continuous physics. For testing.
|
|
/// </summary>
|
|
public bool EnableSubStepping
|
|
{
|
|
set
|
|
{
|
|
if (value)
|
|
{
|
|
Flags |= WorldFlags.SubStepping;
|
|
}
|
|
else
|
|
{
|
|
Flags &= ~WorldFlags.SubStepping;
|
|
}
|
|
}
|
|
get { return (Flags & WorldFlags.SubStepping) == WorldFlags.SubStepping; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a rigid body.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
internal void AddBody(Body body)
|
|
{
|
|
Debug.Assert(!_bodyAddList.Contains(body), "You are adding the same body more than once.");
|
|
|
|
if (!_bodyAddList.Contains(body))
|
|
_bodyAddList.Add(body);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroy a rigid body.
|
|
/// Warning: This automatically deletes all associated shapes and joints.
|
|
/// </summary>
|
|
/// <param name="body">The body.</param>
|
|
public void RemoveBody(Body body)
|
|
{
|
|
Debug.Assert(!_bodyRemoveList.Contains(body),
|
|
"The body is already marked for removal. You are removing the body more than once.");
|
|
|
|
if (!_bodyRemoveList.Contains(body))
|
|
_bodyRemoveList.Add(body);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a joint to constrain bodies together. This may cause the connected bodies to cease colliding.
|
|
/// </summary>
|
|
/// <param name="joint">The joint.</param>
|
|
public void AddJoint(Joint joint)
|
|
{
|
|
Debug.Assert(!_jointAddList.Contains(joint), "You are adding the same joint more than once.");
|
|
|
|
if (!_jointAddList.Contains(joint))
|
|
_jointAddList.Add(joint);
|
|
}
|
|
|
|
private void RemoveJoint(Joint joint, bool doCheck)
|
|
{
|
|
if (doCheck)
|
|
{
|
|
Debug.Assert(!_jointRemoveList.Contains(joint),
|
|
"The joint is already marked for removal. You are removing the joint more than once.");
|
|
}
|
|
|
|
if (!_jointRemoveList.Contains(joint))
|
|
_jointRemoveList.Add(joint);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Destroy a joint. This may cause the connected bodies to begin colliding.
|
|
/// </summary>
|
|
/// <param name="joint">The joint.</param>
|
|
public void RemoveJoint(Joint joint)
|
|
{
|
|
RemoveJoint(joint, true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// All adds and removes are cached by the World duing a World step.
|
|
/// To process the changes before the world updates again, call this method.
|
|
/// </summary>
|
|
public void ProcessChanges()
|
|
{
|
|
ProcessAddedBodies();
|
|
ProcessAddedJoints();
|
|
|
|
ProcessRemovedBodies();
|
|
ProcessRemovedJoints();
|
|
}
|
|
|
|
private void ProcessRemovedJoints()
|
|
{
|
|
if (_jointRemoveList.Count > 0)
|
|
{
|
|
foreach (Joint joint in _jointRemoveList)
|
|
{
|
|
bool collideConnected = joint.CollideConnected;
|
|
|
|
// Remove from the world list.
|
|
JointList.Remove(joint);
|
|
|
|
// Disconnect from island graph.
|
|
Body bodyA = joint.BodyA;
|
|
Body bodyB = joint.BodyB;
|
|
|
|
// Wake up connected bodies.
|
|
bodyA.Awake = true;
|
|
|
|
// WIP David
|
|
if (!joint.IsFixedType())
|
|
{
|
|
bodyB.Awake = true;
|
|
}
|
|
|
|
// Remove from body 1.
|
|
if (joint.EdgeA.Prev != null)
|
|
{
|
|
joint.EdgeA.Prev.Next = joint.EdgeA.Next;
|
|
}
|
|
|
|
if (joint.EdgeA.Next != null)
|
|
{
|
|
joint.EdgeA.Next.Prev = joint.EdgeA.Prev;
|
|
}
|
|
|
|
if (joint.EdgeA == bodyA.JointList)
|
|
{
|
|
bodyA.JointList = joint.EdgeA.Next;
|
|
}
|
|
|
|
joint.EdgeA.Prev = null;
|
|
joint.EdgeA.Next = null;
|
|
|
|
// WIP David
|
|
if (!joint.IsFixedType())
|
|
{
|
|
// Remove from body 2
|
|
if (joint.EdgeB.Prev != null)
|
|
{
|
|
joint.EdgeB.Prev.Next = joint.EdgeB.Next;
|
|
}
|
|
|
|
if (joint.EdgeB.Next != null)
|
|
{
|
|
joint.EdgeB.Next.Prev = joint.EdgeB.Prev;
|
|
}
|
|
|
|
if (joint.EdgeB == bodyB.JointList)
|
|
{
|
|
bodyB.JointList = joint.EdgeB.Next;
|
|
}
|
|
|
|
joint.EdgeB.Prev = null;
|
|
joint.EdgeB.Next = null;
|
|
}
|
|
|
|
// WIP David
|
|
if (!joint.IsFixedType())
|
|
{
|
|
// If the joint prevents collisions, then flag any contacts for filtering.
|
|
if (collideConnected == false)
|
|
{
|
|
ContactEdge edge = bodyB.ContactList;
|
|
while (edge != null)
|
|
{
|
|
if (edge.Other == bodyA)
|
|
{
|
|
// Flag the contact for filtering at the next time step (where either
|
|
// body is awake).
|
|
edge.Contact.FlagForFiltering();
|
|
}
|
|
|
|
edge = edge.Next;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (JointRemoved != null)
|
|
{
|
|
JointRemoved(joint);
|
|
}
|
|
}
|
|
|
|
_jointRemoveList.Clear();
|
|
}
|
|
}
|
|
|
|
private void ProcessAddedJoints()
|
|
{
|
|
if (_jointAddList.Count > 0)
|
|
{
|
|
foreach (Joint joint in _jointAddList)
|
|
{
|
|
// Connect to the world list.
|
|
JointList.Add(joint);
|
|
|
|
// Connect to the bodies' doubly linked lists.
|
|
joint.EdgeA.Joint = joint;
|
|
joint.EdgeA.Other = joint.BodyB;
|
|
joint.EdgeA.Prev = null;
|
|
joint.EdgeA.Next = joint.BodyA.JointList;
|
|
|
|
if (joint.BodyA.JointList != null)
|
|
joint.BodyA.JointList.Prev = joint.EdgeA;
|
|
|
|
joint.BodyA.JointList = joint.EdgeA;
|
|
|
|
// WIP David
|
|
if (!joint.IsFixedType())
|
|
{
|
|
joint.EdgeB.Joint = joint;
|
|
joint.EdgeB.Other = joint.BodyA;
|
|
joint.EdgeB.Prev = null;
|
|
joint.EdgeB.Next = joint.BodyB.JointList;
|
|
|
|
if (joint.BodyB.JointList != null)
|
|
joint.BodyB.JointList.Prev = joint.EdgeB;
|
|
|
|
joint.BodyB.JointList = joint.EdgeB;
|
|
|
|
Body bodyA = joint.BodyA;
|
|
Body bodyB = joint.BodyB;
|
|
|
|
// If the joint prevents collisions, then flag any contacts for filtering.
|
|
if (joint.CollideConnected == false)
|
|
{
|
|
ContactEdge edge = bodyB.ContactList;
|
|
while (edge != null)
|
|
{
|
|
if (edge.Other == bodyA)
|
|
{
|
|
// Flag the contact for filtering at the next time step (where either
|
|
// body is awake).
|
|
edge.Contact.FlagForFiltering();
|
|
}
|
|
|
|
edge = edge.Next;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (JointAdded != null)
|
|
JointAdded(joint);
|
|
|
|
// Note: creating a joint doesn't wake the bodies.
|
|
}
|
|
|
|
_jointAddList.Clear();
|
|
}
|
|
}
|
|
|
|
private void ProcessAddedBodies()
|
|
{
|
|
if (_bodyAddList.Count > 0)
|
|
{
|
|
foreach (Body body in _bodyAddList)
|
|
{
|
|
// Add to world list.
|
|
BodyList.Add(body);
|
|
|
|
if (BodyAdded != null)
|
|
BodyAdded(body);
|
|
}
|
|
|
|
_bodyAddList.Clear();
|
|
}
|
|
}
|
|
|
|
private void ProcessRemovedBodies()
|
|
{
|
|
if (_bodyRemoveList.Count > 0)
|
|
{
|
|
foreach (Body body in _bodyRemoveList)
|
|
{
|
|
Debug.Assert(BodyList.Count > 0);
|
|
|
|
// You tried to remove a body that is not contained in the BodyList.
|
|
// Are you removing the body more than once?
|
|
Debug.Assert(BodyList.Contains(body));
|
|
|
|
// Delete the attached joints.
|
|
JointEdge je = body.JointList;
|
|
while (je != null)
|
|
{
|
|
JointEdge je0 = je;
|
|
je = je.Next;
|
|
|
|
RemoveJoint(je0.Joint, false);
|
|
}
|
|
body.JointList = null;
|
|
|
|
// Delete the attached contacts.
|
|
ContactEdge ce = body.ContactList;
|
|
while (ce != null)
|
|
{
|
|
ContactEdge ce0 = ce;
|
|
ce = ce.Next;
|
|
ContactManager.Destroy(ce0.Contact);
|
|
}
|
|
body.ContactList = null;
|
|
|
|
// Delete the attached fixtures. This destroys broad-phase proxies.
|
|
for (int i = 0; i < body.FixtureList.Count; i++)
|
|
{
|
|
body.FixtureList[i].DestroyProxies(ContactManager.BroadPhase);
|
|
body.FixtureList[i].Destroy();
|
|
}
|
|
|
|
body.FixtureList = null;
|
|
|
|
// Remove world body list.
|
|
BodyList.Remove(body);
|
|
|
|
if (BodyRemoved != null)
|
|
BodyRemoved(body);
|
|
}
|
|
|
|
_bodyRemoveList.Clear();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Take a time step. This performs collision detection, integration,
|
|
/// and consraint solution.
|
|
/// </summary>
|
|
/// <param name="dt">The amount of time to simulate, this should not vary.</param>
|
|
public void Step(float dt)
|
|
{
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
_watch.Start();
|
|
#endif
|
|
|
|
ProcessChanges();
|
|
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
AddRemoveTime = _watch.ElapsedTicks;
|
|
#endif
|
|
//If there is no change in time, no need to calculate anything.
|
|
if (dt == 0 || !Enabled)
|
|
{
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
{
|
|
_watch.Stop();
|
|
_watch.Reset();
|
|
}
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
// If new fixtures were added, we need to find the new contacts.
|
|
if ((Flags & WorldFlags.NewFixture) == WorldFlags.NewFixture)
|
|
{
|
|
ContactManager.FindNewContacts();
|
|
Flags &= ~WorldFlags.NewFixture;
|
|
}
|
|
|
|
TimeStep step;
|
|
step.inv_dt = 1.0f / dt;
|
|
step.dt = dt;
|
|
step.dtRatio = _invDt0 * dt;
|
|
|
|
//Update controllers
|
|
for (int i = 0; i < ControllerList.Count; i++)
|
|
{
|
|
ControllerList[i].Update(dt);
|
|
}
|
|
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
ControllersUpdateTime = _watch.ElapsedTicks - AddRemoveTime;
|
|
#endif
|
|
|
|
// Update contacts. This is where some contacts are destroyed.
|
|
ContactManager.Collide();
|
|
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
ContactsUpdateTime = _watch.ElapsedTicks - (AddRemoveTime + ControllersUpdateTime);
|
|
#endif
|
|
// Integrate velocities, solve velocity raints, and integrate positions.
|
|
Solve(ref step);
|
|
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
SolveUpdateTime = _watch.ElapsedTicks - (AddRemoveTime + ControllersUpdateTime + ContactsUpdateTime);
|
|
#endif
|
|
|
|
// Handle TOI events.
|
|
if (Settings.ContinuousPhysics)
|
|
{
|
|
SolveTOI(ref step);
|
|
}
|
|
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
ContinuousPhysicsTime = _watch.ElapsedTicks -
|
|
(AddRemoveTime + ControllersUpdateTime + ContactsUpdateTime + SolveUpdateTime);
|
|
#endif
|
|
_invDt0 = step.inv_dt;
|
|
|
|
if ((Flags & WorldFlags.ClearForces) != 0)
|
|
{
|
|
ClearForces();
|
|
}
|
|
|
|
for (int i = 0; i < BreakableBodyList.Count; i++)
|
|
{
|
|
BreakableBodyList[i].Update();
|
|
}
|
|
|
|
#if (!SILVERLIGHT)
|
|
if (Settings.EnableDiagnostics)
|
|
{
|
|
_watch.Stop();
|
|
//AddRemoveTime = 1000 * AddRemoveTime / Stopwatch.Frequency;
|
|
|
|
UpdateTime = _watch.ElapsedTicks;
|
|
_watch.Reset();
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call this after you are done with time steps to clear the forces. You normally
|
|
/// call this after each call to Step, unless you are performing sub-steps. By default,
|
|
/// forces will be automatically cleared, so you don't need to call this function.
|
|
/// </summary>
|
|
public void ClearForces()
|
|
{
|
|
for (int i = 0; i < BodyList.Count; i++)
|
|
{
|
|
Body body = BodyList[i];
|
|
body.Force = Vector2.Zero;
|
|
body.Torque = 0.0f;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Query the world for all fixtures that potentially overlap the
|
|
/// provided AABB.
|
|
///
|
|
/// Inside the callback:
|
|
/// Return true: Continues the query
|
|
/// Return false: Terminate the query
|
|
/// </summary>
|
|
/// <param name="callback">A user implemented callback class.</param>
|
|
/// <param name="aabb">The aabb query box.</param>
|
|
public void QueryAABB(Func<Fixture, bool> callback, ref AABB aabb)
|
|
{
|
|
ContactManager.BroadPhase.Query(proxyId =>
|
|
{
|
|
FixtureProxy proxy = ContactManager.BroadPhase.GetProxy(proxyId);
|
|
return callback(proxy.Fixture);
|
|
}, ref aabb);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ray-cast the world for all fixtures in the path of the ray. Your callback
|
|
/// controls whether you get the closest point, any point, or n-points.
|
|
/// The ray-cast ignores shapes that contain the starting point.
|
|
///
|
|
/// Inside the callback:
|
|
/// return -1: ignore this fixture and continue
|
|
/// return 0: terminate the ray cast
|
|
/// return fraction: clip the ray to this point
|
|
/// return 1: don't clip the ray and continue
|
|
/// </summary>
|
|
/// <param name="callback">A user implemented callback class.</param>
|
|
/// <param name="point1">The ray starting point.</param>
|
|
/// <param name="point2">The ray ending point.</param>
|
|
public void RayCast(RayCastCallback callback, Vector2 point1, Vector2 point2)
|
|
{
|
|
RayCastInput input = new RayCastInput();
|
|
input.MaxFraction = 1.0f;
|
|
input.Point1 = point1;
|
|
input.Point2 = point2;
|
|
|
|
ContactManager.BroadPhase.RayCast((rayCastInput, proxyId) =>
|
|
{
|
|
FixtureProxy proxy = ContactManager.BroadPhase.GetProxy(proxyId);
|
|
Fixture fixture = proxy.Fixture;
|
|
int index = proxy.ChildIndex;
|
|
RayCastOutput output;
|
|
bool hit = fixture.RayCast(out output, ref rayCastInput, index);
|
|
|
|
if (hit)
|
|
{
|
|
float fraction = output.Fraction;
|
|
Vector2 point = (1.0f - fraction) * input.Point1 +
|
|
fraction * input.Point2;
|
|
return callback(fixture, point, output.Normal, fraction);
|
|
}
|
|
|
|
return input.MaxFraction;
|
|
}, ref input);
|
|
}
|
|
|
|
private void Solve(ref TimeStep step)
|
|
{
|
|
// Size the island for the worst case.
|
|
Island.Reset(BodyList.Count,
|
|
ContactManager.ContactList.Count,
|
|
JointList.Count,
|
|
ContactManager);
|
|
|
|
// Clear all the island flags.
|
|
foreach (Body b in BodyList)
|
|
{
|
|
b.Flags &= ~BodyFlags.Island;
|
|
}
|
|
|
|
for (int i = 0; i < ContactManager.ContactList.Count; i++)
|
|
{
|
|
Contact c = ContactManager.ContactList[i];
|
|
c.Flags &= ~ContactFlags.Island;
|
|
}
|
|
foreach (Joint j in JointList)
|
|
{
|
|
j.IslandFlag = false;
|
|
}
|
|
|
|
// Build and simulate all awake islands.
|
|
int stackSize = BodyList.Count;
|
|
if (stackSize > _stack.Length)
|
|
_stack = new Body[Math.Max(_stack.Length * 2, stackSize)];
|
|
|
|
for (int index = BodyList.Count - 1; index >= 0; index--)
|
|
{
|
|
Body seed = BodyList[index];
|
|
if ((seed.Flags & (BodyFlags.Island)) != BodyFlags.None)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (seed.Awake == false || seed.Enabled == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// The seed can be dynamic or kinematic.
|
|
if (seed.BodyType == BodyType.Static)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Reset island and stack.
|
|
Island.Clear();
|
|
int stackCount = 0;
|
|
_stack[stackCount++] = seed;
|
|
seed.Flags |= BodyFlags.Island;
|
|
|
|
// Perform a depth first search (DFS) on the constraint graph.
|
|
while (stackCount > 0)
|
|
{
|
|
// Grab the next body off the stack and add it to the island.
|
|
Body b = _stack[--stackCount];
|
|
Debug.Assert(b.Enabled);
|
|
Island.Add(b);
|
|
|
|
// Make sure the body is awake.
|
|
b.Awake = true;
|
|
|
|
// To keep islands as small as possible, we don't
|
|
// propagate islands across static bodies.
|
|
if (b.BodyType == BodyType.Static)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Search all contacts connected to this body.
|
|
for (ContactEdge ce = b.ContactList; ce != null; ce = ce.Next)
|
|
{
|
|
Contact contact = ce.Contact;
|
|
|
|
// Has this contact already been added to an island?
|
|
if ((contact.Flags & ContactFlags.Island) != ContactFlags.None)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Is this contact solid and touching?
|
|
if (!ce.Contact.Enabled || !ce.Contact.IsTouching())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip sensors.
|
|
bool sensorA = contact.FixtureA.IsSensor;
|
|
bool sensorB = contact.FixtureB.IsSensor;
|
|
if (sensorA || sensorB)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Island.Add(contact);
|
|
contact.Flags |= ContactFlags.Island;
|
|
|
|
Body other = ce.Other;
|
|
|
|
// Was the other body already added to this island?
|
|
if ((other.Flags & BodyFlags.Island) != BodyFlags.None)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Debug.Assert(stackCount < stackSize);
|
|
_stack[stackCount++] = other;
|
|
other.Flags |= BodyFlags.Island;
|
|
}
|
|
|
|
// Search all joints connect to this body.
|
|
for (JointEdge je = b.JointList; je != null; je = je.Next)
|
|
{
|
|
if (je.Joint.IslandFlag)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Body other = je.Other;
|
|
|
|
// WIP David
|
|
//Enter here when it's a non-fixed joint. Non-fixed joints have a other body.
|
|
if (other != null)
|
|
{
|
|
// Don't simulate joints connected to inactive bodies.
|
|
if (other.Enabled == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Island.Add(je.Joint);
|
|
je.Joint.IslandFlag = true;
|
|
|
|
if ((other.Flags & BodyFlags.Island) != BodyFlags.None)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Debug.Assert(stackCount < stackSize);
|
|
_stack[stackCount++] = other;
|
|
other.Flags |= BodyFlags.Island;
|
|
}
|
|
else
|
|
{
|
|
Island.Add(je.Joint);
|
|
je.Joint.IslandFlag = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
Island.Solve(ref step, ref Gravity);
|
|
|
|
// Post solve cleanup.
|
|
for (int i = 0; i < Island.BodyCount; ++i)
|
|
{
|
|
// Allow static bodies to participate in other islands.
|
|
Body b = Island.Bodies[i];
|
|
if (b.BodyType == BodyType.Static)
|
|
{
|
|
b.Flags &= ~BodyFlags.Island;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Synchronize fixtures, check for out of range bodies.
|
|
foreach (Body b in BodyList)
|
|
{
|
|
// If a body was not in an island then it did not move.
|
|
if ((b.Flags & BodyFlags.Island) != BodyFlags.Island)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (b.BodyType == BodyType.Static)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Update fixtures (for broad-phase).
|
|
b.SynchronizeFixtures();
|
|
}
|
|
|
|
// Look for new contacts.
|
|
ContactManager.FindNewContacts();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find TOI contacts and solve them.
|
|
/// </summary>
|
|
/// <param name="step">The step.</param>
|
|
private void SolveTOI(ref TimeStep step)
|
|
{
|
|
Island.Reset(2 * Settings.MaxTOIContacts, Settings.MaxTOIContacts, 0, ContactManager);
|
|
|
|
if (_stepComplete)
|
|
{
|
|
for (int i = 0; i < BodyList.Count; i++)
|
|
{
|
|
BodyList[i].Flags &= ~BodyFlags.Island;
|
|
BodyList[i].Sweep.Alpha0 = 0.0f;
|
|
}
|
|
|
|
for (int i = 0; i < ContactManager.ContactList.Count; i++)
|
|
{
|
|
Contact c = ContactManager.ContactList[i];
|
|
|
|
// Invalidate TOI
|
|
c.Flags &= ~(ContactFlags.TOI | ContactFlags.Island);
|
|
c.TOICount = 0;
|
|
c.TOI = 1.0f;
|
|
}
|
|
}
|
|
|
|
// Find TOI events and solve them.
|
|
for (; ; )
|
|
{
|
|
// Find the first TOI.
|
|
Contact minContact = null;
|
|
float minAlpha = 1.0f;
|
|
|
|
for (int i = 0; i < ContactManager.ContactList.Count; i++)
|
|
{
|
|
Contact c = ContactManager.ContactList[i];
|
|
|
|
// Is this contact disabled?
|
|
if (c.Enabled == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Prevent excessive sub-stepping.
|
|
if (c.TOICount > Settings.MaxSubSteps)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
float alpha;
|
|
if ((c.Flags & ContactFlags.TOI) == ContactFlags.TOI)
|
|
{
|
|
// This contact has a valid cached TOI.
|
|
alpha = c.TOI;
|
|
}
|
|
else
|
|
{
|
|
Fixture fA = c.FixtureA;
|
|
Fixture fB = c.FixtureB;
|
|
|
|
// Is there a sensor?
|
|
if (fA.IsSensor || fB.IsSensor)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Body bA = fA.Body;
|
|
Body bB = fB.Body;
|
|
|
|
BodyType typeA = bA.BodyType;
|
|
BodyType typeB = bB.BodyType;
|
|
//Debug.Assert(typeA == BodyType.Dynamic || typeB == BodyType.Dynamic);
|
|
|
|
bool awakeA = bA.Awake && typeA != BodyType.Static;
|
|
bool awakeB = bB.Awake && typeB != BodyType.Static;
|
|
|
|
// Is at least one body awake?
|
|
if (awakeA == false && awakeB == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool collideA = (bA.IsBullet || typeA != BodyType.Dynamic) && !bA.IgnoreCCD;
|
|
bool collideB = (bB.IsBullet || typeB != BodyType.Dynamic) && !bB.IgnoreCCD;
|
|
|
|
// Are these two non-bullet dynamic bodies?
|
|
if (collideA == false && collideB == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Compute the TOI for this contact.
|
|
// Put the sweeps onto the same time interval.
|
|
float alpha0 = bA.Sweep.Alpha0;
|
|
|
|
if (bA.Sweep.Alpha0 < bB.Sweep.Alpha0)
|
|
{
|
|
alpha0 = bB.Sweep.Alpha0;
|
|
bA.Sweep.Advance(alpha0);
|
|
}
|
|
else if (bB.Sweep.Alpha0 < bA.Sweep.Alpha0)
|
|
{
|
|
alpha0 = bA.Sweep.Alpha0;
|
|
bB.Sweep.Advance(alpha0);
|
|
}
|
|
|
|
Debug.Assert(alpha0 < 1.0f);
|
|
|
|
// Compute the time of impact in interval [0, minTOI]
|
|
_input.ProxyA.Set(fA.Shape, c.ChildIndexA);
|
|
_input.ProxyB.Set(fB.Shape, c.ChildIndexB);
|
|
_input.SweepA = bA.Sweep;
|
|
_input.SweepB = bB.Sweep;
|
|
_input.TMax = 1.0f;
|
|
|
|
TOIOutput output;
|
|
TimeOfImpact.CalculateTimeOfImpact(out output, _input);
|
|
|
|
// Beta is the fraction of the remaining portion of the .
|
|
float beta = output.T;
|
|
if (output.State == TOIOutputState.Touching)
|
|
{
|
|
alpha = Math.Min(alpha0 + (1.0f - alpha0) * beta, 1.0f);
|
|
}
|
|
else
|
|
{
|
|
alpha = 1.0f;
|
|
}
|
|
|
|
c.TOI = alpha;
|
|
c.Flags |= ContactFlags.TOI;
|
|
}
|
|
|
|
if (alpha < minAlpha)
|
|
{
|
|
// This is the minimum TOI found so far.
|
|
minContact = c;
|
|
minAlpha = alpha;
|
|
}
|
|
}
|
|
|
|
if (minContact == null || 1.0f - 10.0f * Settings.Epsilon < minAlpha)
|
|
{
|
|
// No more TOI events. Done!
|
|
_stepComplete = true;
|
|
break;
|
|
}
|
|
|
|
// Advance the bodies to the TOI.
|
|
Fixture fA1 = minContact.FixtureA;
|
|
Fixture fB1 = minContact.FixtureB;
|
|
Body bA1 = fA1.Body;
|
|
Body bB1 = fB1.Body;
|
|
|
|
Sweep backup1 = bA1.Sweep;
|
|
Sweep backup2 = bB1.Sweep;
|
|
|
|
bA1.Advance(minAlpha);
|
|
bB1.Advance(minAlpha);
|
|
|
|
// The TOI contact likely has some new contact points.
|
|
minContact.Update(ContactManager);
|
|
minContact.Flags &= ~ContactFlags.TOI;
|
|
++minContact.TOICount;
|
|
|
|
// Is the contact solid?
|
|
if (minContact.Enabled == false || minContact.IsTouching() == false)
|
|
{
|
|
// Restore the sweeps.
|
|
minContact.Enabled = false;
|
|
bA1.Sweep = backup1;
|
|
bB1.Sweep = backup2;
|
|
bA1.SynchronizeTransform();
|
|
bB1.SynchronizeTransform();
|
|
continue;
|
|
}
|
|
|
|
bA1.Awake = true;
|
|
bB1.Awake = true;
|
|
|
|
// Build the island
|
|
Island.Clear();
|
|
Island.Add(bA1);
|
|
Island.Add(bB1);
|
|
Island.Add(minContact);
|
|
|
|
bA1.Flags |= BodyFlags.Island;
|
|
bB1.Flags |= BodyFlags.Island;
|
|
minContact.Flags |= ContactFlags.Island;
|
|
|
|
// Get contacts on bodyA and bodyB.
|
|
Body[] bodies = { bA1, bB1 };
|
|
for (int i = 0; i < 2; ++i)
|
|
{
|
|
Body body = bodies[i];
|
|
if (body.BodyType == BodyType.Dynamic)
|
|
{
|
|
// for (ContactEdge ce = body.ContactList; ce && Island.BodyCount < Settings.MaxTOIContacts; ce = ce.Next)
|
|
for (ContactEdge ce = body.ContactList; ce != null; ce = ce.Next)
|
|
{
|
|
Contact contact = ce.Contact;
|
|
|
|
// Has this contact already been added to the island?
|
|
if ((contact.Flags & ContactFlags.Island) == ContactFlags.Island)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Only add static, kinematic, or bullet bodies.
|
|
Body other = ce.Other;
|
|
if (other.BodyType == BodyType.Dynamic &&
|
|
body.IsBullet == false && other.IsBullet == false)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Skip sensors.
|
|
if (contact.FixtureA.IsSensor || contact.FixtureB.IsSensor)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Tentatively advance the body to the TOI.
|
|
Sweep backup = other.Sweep;
|
|
if ((other.Flags & BodyFlags.Island) == 0)
|
|
{
|
|
other.Advance(minAlpha);
|
|
}
|
|
|
|
// Update the contact points
|
|
contact.Update(ContactManager);
|
|
|
|
// Was the contact disabled by the user?
|
|
if (contact.Enabled == false)
|
|
{
|
|
other.Sweep = backup;
|
|
other.SynchronizeTransform();
|
|
continue;
|
|
}
|
|
|
|
// Are there contact points?
|
|
if (contact.IsTouching() == false)
|
|
{
|
|
other.Sweep = backup;
|
|
other.SynchronizeTransform();
|
|
continue;
|
|
}
|
|
|
|
// Add the contact to the island
|
|
contact.Flags |= ContactFlags.Island;
|
|
Island.Add(contact);
|
|
|
|
// Has the other body already been added to the island?
|
|
if ((other.Flags & BodyFlags.Island) == BodyFlags.Island)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Add the other body to the island.
|
|
other.Flags |= BodyFlags.Island;
|
|
|
|
if (other.BodyType != BodyType.Static)
|
|
{
|
|
other.Awake = true;
|
|
}
|
|
|
|
Island.Add(other);
|
|
}
|
|
}
|
|
}
|
|
|
|
TimeStep subStep;
|
|
subStep.dt = (1.0f - minAlpha) * step.dt;
|
|
subStep.inv_dt = 1.0f / subStep.dt;
|
|
subStep.dtRatio = 1.0f;
|
|
//subStep.positionIterations = 20;
|
|
//subStep.velocityIterations = step.velocityIterations;
|
|
//subStep.warmStarting = false;
|
|
Island.SolveTOI(ref subStep);
|
|
|
|
// Reset island flags and synchronize broad-phase proxies.
|
|
for (int i = 0; i < Island.BodyCount; ++i)
|
|
{
|
|
Body body = Island.Bodies[i];
|
|
body.Flags &= ~BodyFlags.Island;
|
|
|
|
if (body.BodyType != BodyType.Dynamic)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
body.SynchronizeFixtures();
|
|
|
|
// Invalidate all contact TOIs on this displaced body.
|
|
for (ContactEdge ce = body.ContactList; ce != null; ce = ce.Next)
|
|
{
|
|
ce.Contact.Flags &= ~(ContactFlags.TOI | ContactFlags.Island);
|
|
}
|
|
}
|
|
|
|
// Commit fixture proxy movements to the broad-phase so that new contacts are created.
|
|
// Also, some contacts can be destroyed.
|
|
ContactManager.FindNewContacts();
|
|
|
|
if (EnableSubStepping)
|
|
{
|
|
_stepComplete = false;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void AddController(Controller controller)
|
|
{
|
|
Debug.Assert(!ControllerList.Contains(controller), "You are adding the same controller more than once.");
|
|
|
|
controller.World = this;
|
|
ControllerList.Add(controller);
|
|
|
|
if (ControllerAdded != null)
|
|
ControllerAdded(controller);
|
|
}
|
|
|
|
public void RemoveController(Controller controller)
|
|
{
|
|
Debug.Assert(ControllerList.Contains(controller),
|
|
"You are removing a controller that is not in the simulation.");
|
|
|
|
if (ControllerList.Contains(controller))
|
|
{
|
|
ControllerList.Remove(controller);
|
|
|
|
if (ControllerRemoved != null)
|
|
ControllerRemoved(controller);
|
|
}
|
|
}
|
|
|
|
public void AddBreakableBody(BreakableBody breakableBody)
|
|
{
|
|
BreakableBodyList.Add(breakableBody);
|
|
}
|
|
|
|
public void RemoveBreakableBody(BreakableBody breakableBody)
|
|
{
|
|
//The breakable body list does not contain the body you tried to remove.
|
|
Debug.Assert(BreakableBodyList.Contains(breakableBody));
|
|
|
|
BreakableBodyList.Remove(breakableBody);
|
|
}
|
|
|
|
public Fixture TestPoint(Vector2 point)
|
|
{
|
|
AABB aabb;
|
|
Vector2 d = new Vector2(Settings.Epsilon, Settings.Epsilon);
|
|
aabb.LowerBound = point - d;
|
|
aabb.UpperBound = point + d;
|
|
|
|
Fixture myFixture = null;
|
|
|
|
// Query the world for overlapping shapes.
|
|
QueryAABB(
|
|
fixture =>
|
|
{
|
|
bool inside = fixture.TestPoint(ref point);
|
|
if (inside)
|
|
{
|
|
myFixture = fixture;
|
|
return false;
|
|
}
|
|
|
|
// Continue the query.
|
|
return true;
|
|
}, ref aabb);
|
|
|
|
return myFixture;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns a list of fixtures that are at the specified point.
|
|
/// </summary>
|
|
/// <param name="point">The point.</param>
|
|
/// <returns></returns>
|
|
public List<Fixture> TestPointAll(Vector2 point)
|
|
{
|
|
AABB aabb;
|
|
Vector2 d = new Vector2(Settings.Epsilon, Settings.Epsilon);
|
|
aabb.LowerBound = point - d;
|
|
aabb.UpperBound = point + d;
|
|
|
|
List<Fixture> fixtures = new List<Fixture>();
|
|
|
|
// Query the world for overlapping shapes.
|
|
QueryAABB(
|
|
fixture =>
|
|
{
|
|
bool inside = fixture.TestPoint(ref point);
|
|
if (inside)
|
|
fixtures.Add(fixture);
|
|
|
|
// Continue the query.
|
|
return true;
|
|
}, ref aabb);
|
|
|
|
return fixtures;
|
|
}
|
|
|
|
public void Clear()
|
|
{
|
|
ProcessChanges();
|
|
|
|
for (int i = BodyList.Count - 1; i >= 0; i--)
|
|
{
|
|
RemoveBody(BodyList[i]);
|
|
}
|
|
|
|
for (int i = ControllerList.Count - 1; i >= 0; i--)
|
|
{
|
|
RemoveController(ControllerList[i]);
|
|
}
|
|
|
|
for (int i = BreakableBodyList.Count - 1; i >= 0; i--)
|
|
{
|
|
RemoveBreakableBody(BreakableBodyList[i]);
|
|
}
|
|
|
|
ProcessChanges();
|
|
}
|
|
}
|
|
} |