A couple of years after my first posting, I am releasing an upgrade where I did the following:
- Changes and cleanups to the original article to improve ease of use (as a coder and as a user)
- Added a physics engine (gravity, momentum, and drag)
- Bound objects to a grid to improve debugging
- Added a
In this tip, I explain these improvements and some of the caveats I ran into. The tip itself is not a recipe of how to recreate this, for that, you can read the code. Nothing here is so complicated that it cannot be followed.
When you run this, you will be able to grab many objects in a flat world and throw them around. You'll see how gravity, mass, friction all interact with each other. As part of this, you will learn how to model all this yourself.
Using the Code
Similar to last time, I rely on an interface called
IShape. However, I broke it up a little into
IShape is an
IDrawable. The basic difference is that an
IDrawable is something that has no independent interactivity within a canvas, whereas an
Shapes have evolved very little since the last article. The biggest change I made is that I made more of an OO hierarchy to pull out more common code to the base class
Shape and be able to better reuse it within the concrete shapes, such as selection and drawing. I guess this pretty much follows a template pattern. I also added some fields that allow it to interact with the physics engine described below.
This is the main drawing area, it is very similar as in the previous article, but I'll explain some concepts that I changed. I added a field of view (
fov) which defines the location and size of the view. As opposed to the original article where you had to define a zoom and angle, now I define a field of view (http://en.wikipedia.org/wiki/Field_of_view) which makes it much easier to define where the canvas is located and what its size is, as shown below. In my application, most units are in terms of metres.
zoomPanel.Initialize(universe, new RectangleF(0, 0, 10, 10));
largePanel.Initialize(universe, new RectangleF(-50, 0, 110, 55));
Here, the zoom panel has a size of 10x10m whilst the large panel measures 110, 55 and includes the zoom panel somewhat centered. The canvas takes its own set of universe which means that they could show you different things, not sure how realistic that is. I removed the rotation aspect because I wanted to deal more with gravity (later) so down should always be down.
The canvas can be dragged around using the right button. The canvas is an
IDrawable since I want to make sure it gets drawn to let the user where it is located. It's not an
IShape since it does not interact with Shapes.
I had previously added a little log area at the bottom here. Nothing fancy, just a text area that you can easily get to. However, my app was becoming too verbose so I decided to display position information of each object on a grid. Of course, I wanted to learn a little more, so after a little searching, I ended up binding the list of shapes directly to the grid which enabled me to do more fancy stuff like adding interactivity and of course "auto-update" as the object changes.
Binding the universe to the grid was as simple as this:
private void BindShapesToGrid(ShapeGroup universe)
BindingSource bs = new BindingSource();
bs.DataSource = new BindingList<IShape>(universe.Shapes);
dgModel.DataSource = bs;
Fields that have a "setter" defined on them also become automatically settable through the grid, so I can easily add a specific height or velocity to any object without having to estimate using mouse movements.
I actually spent a lot of time here even though it was clearly taught here. At the end, it turned out that I had to make
IShape to inherit
INotifyPropertyChanged as opposed to
Shape (which actually contains the
PropertyChangedEventHandler event). I spent several hours because I also assumed that the .NET classes would be looking at the actual objects that were passed for the event, as opposed to the declaration of the type of the
BindingList. Kind of an embarrassment, I guess it won't happen again. Notifying changes were easily done as advertised:
private void NotifyPropertyChanged(string name)
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(name));
Another cool thing that I learned here was the
TypeConverter framework. I wanted to enter new velocities to any object through the grid. In order to parse the
strings and convert them to
Vector3, I added
Vector3Converter, this is all well explained in the Microsoft documentation, but here are the basics. The class that needs to be converted gets tagged with an attribute that tells it which class provides the conversion.
public struct Vector3...
Then, if you look at the class definition [of
Vector3Converter], it implements
TypeConverter and overrides some simple methods. Take a look at the code for the details.
Finally, we get to the physics engine. I wanted to be able to:
- Throw things around
- Have things fall to the ground
- Have an atmosphere that exerts some level of drag
There are two basic parts to this part: a modeler that figures out how forces act on an object, and a simulator that simulates the passage of time. Given forces and masses, it is possible to deterministically figure out how far things move during an amount of time (delta T, or elapsed time). The passage of time needs to be simulated.
This is actually so simple that is is simply
Timer with an interval set at 10ms. That means that ever 10ms or so, an event is fired which executes the physics modeler. As you can see, I said 10ms "or so", which at this resolution is usually more "or so" than not. To get things really accurate, the simulator needs to keep its own time and use that as the actual time elapsed between two events. For this, I used a
StopWatch which easily tells me the actual time between two events. I was happy when I was able to simulate an object falling from 1000m (assuming a drag of 0) and have the time match what the math would say. If you don't have your own way of keeping time, your objects will fall too slow.
I think I'll create a simple class that includes the
StopWatch to implement the simulator on my next iteration.
First some physics basics - not that I remember much from school, I used Wikipedia quite a bit to learn some equations, especially regarding drag.
- a (acceleration) =gravity = 9.8 m/s^2
- v (velocity) = u + at (t = time elapsed, u = start velocity, this is really the integral of a)
- s (displacement) = ut + 0.5at^2
- FDrag (Drag Force) - See http://en.wikipedia.org/wiki/Drag_equation
- f = ma (this enables me to account for FDrag when applying gravity)
I am not overly confident with my application of drag (and you can see that by the divisor of 500. However, the basics seem to work. A bigger object will fall slower due to its increased area dragging. Again though, I took a simplification and assumed that everything weighs 1 kg. I'll probably change this in a future article to play with other concepts. If you can tell me what the proper way to apply drag is, let me know!
Once you are comfortable with the equations, you just plug and play. It's important to remember that forces and velocities are vectors. I suppose it is possible to create a single vector that describes net forces from the environment, but instead I decided to work on the components of these vectors individually. I'm not so advanced...
The other part when working with
doubles is to check values with some level of allowable error, and in my case, I also wanted to have pieces stop moving otherwise they would continue bouncing quite a bit which was just annoying. For that, I added this simple code:
if (Utilities.CheckWithAbsTolerance((float)vx, 0f, 0.1f)) vx = 0f;
if (Utilities.CheckWithAbsTolerance((float)vy, 0f, 0.1f)) vy = 0f;
That's it! Try the code and play with it... let me know what you think!