PauseablePhysicsGameState

Topics: Developer Forum, User Forum
Jul 16, 2011 at 1:50 AM
Edited Jul 16, 2011 at 2:06 AM

I put together a game state that allows body dynamics to be paused and body positions to be manipulated with the mouse cursor without using a FixedMouseJoint.  I thought I'd share it for some feedback and also for those who may be looking for similar functionality.

There are a couple of notes:

1.  For a manipulated body to keep up with the mouse cursor, the constant MaxTranslation value in the Farseer settings MUST be increased.  I've raised it to 4.0f for now.  I have not uncovered any information regarding the risks of increasing this value, but doubling it seems to overcome any lag that was affecting a manipulated body, previously.

2.  The HandleCursor method inside of the Samples project's InputHelper (which this uses) must be made virtual so that it can be overridden when dynamics are paused and mouse movement handled when a body is clicked on.  A separate method is used in a Farseer extensions class that moves the body to a point by settings its LinearVelocity.  This isn't included in the post, but if someone mentions it, I can include that too in a subsequent post.  The function that is referred to is "PhysicsExtensions.MoveBodyToPoint".

3.  I'm using a custom container to store body properties while dynamics are paused.  Because having dynamics disabled is meant to allow for scene manipulation without other physics actors being inadvertently affected due to incidental collision, the basic scalar properties of each body in the World (Friction, Mass, Restitution, LinearDamping, and AngularDamping) are set to zero.  When dynamics are unpaused, these properties are restored to their original values for each body.

 

using FarseerPhysics.Dynamics;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Input;

namespace FarseerPhysics.SamplesFramework
{
	public class PauseablePhysicsGameState : PhysicsGameState
	{
		/// <summary>
		/// The selected body when dynamics have been paused
		/// </summary>
		private Body _selectedBody;

		/// <summary>
		/// The position of the cursor relative to that of the selected body
		/// </summary>
		private Vector2 _selectedBodyOffset;

		/// <summary>
		/// Dictionary for storing body types when dynamics are paused
		/// </summary>
		private BodyPropertiesCollection _bodyPropertiesCollection;

		/// <summary>
		/// The gravity of the world before dynamics were paused that will be restored
		/// when they are resumed
		/// </summary>
		private Vector2 _storedGravity;

		/// <summary>
		/// Initializes a new instance of the PausedPhysicsState class.
		/// </summary>
		public PauseablePhysicsGameState(GameScreenManager manager)
			: base(manager)
		{
			DynamicsEnabled = true;
		}

		/// <summary>
		/// Flag identifying if dynamics are currently paused
		/// </summary>
		public bool DynamicsEnabled { get; private set; }

		/// <summary>
		/// Draws content to the screen.
		/// </summary>
		/// <param name = "gameTime">
		/// Snapshot of the game's timing values
		/// </param>
		public override void Draw(GameTime gameTime)
		{
			// Draw a string identifying if body dynamics are currently paused
			Color color = Color.White;
			string dynamicsStatus = "Body Dynamics: ";

			if (DynamicsEnabled)
			{
				dynamicsStatus += "ACTIVE";
			}
			else
			{
				color = Color.Red;
				dynamicsStatus += "PAUSED";
			}

			GameScreenManager.SpriteBatch.Begin();
			GameScreenManager.SpriteBatch.DrawString(GameScreenManager.Fonts.DetailsFont,
								 dynamicsStatus, new Vector2(1050, 30), color);
			GameScreenManager.SpriteBatch.End();

			base.Draw(gameTime);
		}

		/// <summary>
		/// Allows the screen to handle user input. Unlike Update, this method
		/// is only called when the screen is active, and not when some other
		/// screen has taken the focus.
		/// </summary>
		public override void HandleInput(InputManager input, GameTime gameTime)
		{
			if (input.IsNewKeyPress(Keys.P))
			{
				if (DynamicsEnabled)
				{
					PauseDyanmics();
				}
				else
				{
					ResumeDynamics();
				}
			}

			base.HandleInput(input, gameTime);
		}

		/// <summary>
		/// Handles the mouse cursor if dynamics are currently paused.
		/// </summary>
		protected override void HandleCursor(InputManager input)
		{
			if (DynamicsEnabled)
			{
				base.HandleCursor(input);
				return;
			}

			if (input.MouseState.LeftButton == ButtonState.Pressed)
			{
				Vector2 cursorPosition = Camera.ConvertScreenToWorld(input.CursorPosition);

				if (input.IsNewMouseButtonPress(MouseButtons.Left))
				{
					// Get fixture at click point
					Fixture clickedFixture = World.TestPoint(cursorPosition);

					// Check that the fixture is valid
					if (clickedFixture != null)
					{
						// Store the selected fixture's body
						_selectedBody = clickedFixture.Body;

						// Convert a selected static body to kinematic so that it can be manipulated
						_selectedBody.BodyType = _bodyPropertiesCollection.GetBodyType(_selectedBody.BodyId)
						                         == BodyType.Static ? BodyType.Kinematic
						                         : _bodyPropertiesCollection.GetBodyType(_selectedBody.BodyId);

						// Get offset of cursor from body's center
						_selectedBodyOffset = cursorPosition - _selectedBody.Position;
					}
				}

				if (_selectedBody != null)
				{
					// Update selected body's position
					Vector2 targetPosition = cursorPosition - _selectedBodyOffset;
					PhysicsExtensions.MoveBodyToPoint(_selectedBody, targetPosition);

					// Stop any persisting rotation during manipulation
					_selectedBody.AngularVelocity = 0.0f;
				}
			}
			else if (input.IsNewMouseButtonRelease(MouseButtons.Left) && _selectedBody != null)
			{
				// Halt any motion of the selected body when released
				_selectedBody.BodyType = BodyType.Static;

				// Reset selected body value
				_selectedBody = null;
			}
		}

		/// <summary>
		/// Pause dynamics so the non-static physics actors can be manipulated manually.
		/// </summary>
		public void PauseDyanmics()
		{
			if (!DynamicsEnabled)
			{
				return;
			}

			_storedGravity = World.Gravity;

			World.Gravity = Vector2.Zero;

			if (_bodyPropertiesCollection == null)
			{
				_bodyPropertiesCollection = new BodyPropertiesCollection(World.BodyList.Count);
			}

			for (int i = World.BodyList.Count - 1; i >= 0; i--)
			{
				Body body = World.BodyList[i];
				_bodyPropertiesCollection.StoreAndResetBodyProperties(ref body);
				body.BodyType = BodyType.Static;
			}

			DynamicsEnabled = false;
		}

		/// <summary>
		/// Resume dynamics by restoring gravity and allowing the world to handle physics.
		/// </summary>
		public void ResumeDynamics()
		{
			if (DynamicsEnabled)
			{
				return;
			}

			World.Gravity = _storedGravity;

			for (int i = World.BodyList.Count - 1; i >= 0; i--)
			{
				Body body = World.BodyList[i];
				_bodyPropertiesCollection.RestoreBodyProperties(ref body);
			}

			DynamicsEnabled = true;
		}
	}
}

 

The code for the custom container/collection that also handles the reset/restoration of the body property values is below.

 

using System;
using System.Collections.Generic;
using FarseerPhysics.Dynamics;

namespace FarseerPhysics.SamplesFramework
{
	public class BodyPropertiesCollection
	{
		private readonly Dictionary<int, BodyProperties> _storedBodyProperties;

		public BodyPropertiesCollection(int capacity)
		{
			_storedBodyProperties = new Dictionary<int, BodyProperties>(capacity);
		}

		public void StoreAndResetBodyProperties(ref Body body)
		{
			if (_storedBodyProperties.ContainsKey(body.BodyId))
			{
				return;
			}

			_storedBodyProperties.Add(body.BodyId, new BodyProperties(body));

			body.AngularDamping = 0f;
			body.Friction = 0f;
			body.LinearDamping = 0f;
			body.Mass = 0f;
			body.Restitution = 0f;
		}

		public void RestoreBodyProperties(ref Body body)
		{
			if (!_storedBodyProperties.ContainsKey(body.BodyId))
			{
				throw new InvalidOperationException("Specified body's properties have not been stored.");
			}

			BodyProperties properties = _storedBodyProperties[body.BodyId];

			body.AngularDamping = properties.AngularDamping;
			body.BodyType = properties.BodyType;
			body.Friction = properties.Friction;
			body.LinearDamping = properties.LinearDamping;
			body.Mass = properties.Mass;
			body.Restitution = properties.Restitution;

			_storedBodyProperties.Remove(body.BodyId);
		}

		public BodyType GetBodyType(int bodyId)
		{
			if (!_storedBodyProperties.ContainsKey(bodyId))
			{
				throw new InvalidOperationException("Specified body's properties have not been stored.");
			}

			return _storedBodyProperties[bodyId].BodyType;
		}

		#region Nested type: BodyProperties

		public struct BodyProperties
		{
			public readonly float AngularDamping;

			public readonly BodyType BodyType;

			public readonly float Friction;

			public readonly float LinearDamping;

			public readonly float Mass;

			public readonly float Restitution;

			public BodyProperties(Body body)
				: this()
			{
				AngularDamping = body.AngularDamping;
				BodyType = body.BodyType;
				Friction = body.Friction;
				LinearDamping = body.LinearDamping;
				Mass = body.Mass;
				Restitution = body.Restitution;
			}
		}

		#endregion Nested type: BodyProperties
	}
}