WPF and Box2D. How I did physics with WPF
- From the sandbox
- Tutorial
Good time Habr. I am a big fan of physics in games, I worked with some interesting physics engines, but today I will talk about Box2D. It is as simple and straightforward as possible and is great for two-dimensional physics. I noticed that there are very few Box2D tutorials on C # on the Internet, there are almost none. I was repeatedly asked to write an article on this subject. Well, the time has come. There will be a lot of code, letters and a few comments. To display graphics, WPF and the Viewport3D element are used. Who cares, welcome tackle.
Box2d- computer program, free open physics engine. Box2D is a real-time physics engine and is designed to work with two-dimensional physical objects. The engine was developed by Erin Catto, written in the C ++ programming language and distributed under the terms of the zlib license.
The engine is used in two-dimensional computer games, including Angry Birds, Limbo, Crayon Physics Deluxe, Rolando, Fantastic Contraption, Incredibots, Transformice, Happy Wheels, Color Infection, Shovel Knight, King of Thieves.
You can download the link Box2D.dll
WPF and Viewport3D
For rendering the world, I decided for some reason to take WPF, of course, you can draw on anything, even on the usual Grphics and PictureBox, but this is not desirable since Graphics displays graphics through the CPU and will consume a lot of processor time.
Let's write a small environment for working with graphics. In the default project window, add the following XAML:
The code
ClipToBounds - says that the invisible edges will be clipped, although this is not useful here. there will be a 2D projection, I’ll turn it on anyway.
After the perspective camera is installed. FarPlaneDistance - the maximum distance that the camera captures, NearPlaneDistance - the minimum distance, and then the position where the camera looks and how it looks. Next, we create a Model3DGroup element into which we will throw the geometry through its name “models”, and add 3 lights to it.
Well, with XAML sorted out, now you can start writing a class to create geometry:
The code
public class MyModel3D
{
public Vector3D Position { get; set; } // Позиция квадрата
public Size Size { get; set; } // Размер квадрата
private TranslateTransform3D translateTransform; // Матрица перемещения
private RotateTransform3D rotationTransform; // Матрица вращения
public MyModel3D(Model3DGroup models, double x, double y, double z, string path, Size size, float axis_x = 0, double angle = 0, float axis_y = 0, float axis_z = 1)
{
this.Size = size;
this.Position = new Vector3D(x, y, z);
MeshGeometry3D mesh = new MeshGeometry3D();
// Проставляем вершины квадрату
mesh.Positions = new Point3DCollection(new List
{
new Point3D(-size.Width/2, -size.Height/2, 0),
new Point3D(size.Width/2, -size.Height/2, 0),
new Point3D(size.Width/2, size.Height/2, 0),
new Point3D(-size.Width/2, size.Height/2, 0)
});
// Указываем индексы для квадрата
mesh.TriangleIndices = new Int32Collection(new List { 0, 1, 2, 0, 2, 3 });
mesh.TextureCoordinates = new PointCollection();
// Устанавливаем текстурные координаты чтоб потом могли натянуть текстуру
mesh.TextureCoordinates.Add(new Point(0, 1));
mesh.TextureCoordinates.Add(new Point(1, 1));
mesh.TextureCoordinates.Add(new Point(1, 0));
mesh.TextureCoordinates.Add(new Point(0, 0));
// Натягиваем текстуру
ImageBrush brush = new ImageBrush(new BitmapImage(new Uri(path)));
Material material = new DiffuseMaterial(brush);
GeometryModel3D geometryModel = new GeometryModel3D(mesh, material);
models.Children.Add(geometryModel);
translateTransform = new TranslateTransform3D(x, y, z);
rotationTransform = new RotateTransform3D(new AxisAngleRotation3D(new Vector3D(axis_x, axis_y, axis_z), angle), 0.5, 0.5, 0.5);
Transform3DGroup tgroup = new Transform3DGroup();
tgroup.Children.Add(translateTransform);
tgroup.Children.Add(rotationTransform);
geometryModel.Transform = tgroup;
}
// Утсанавливает позицию объекта
public void SetPosition(Vector3D v3)
{
translateTransform.OffsetX = v3.X;
translateTransform.OffsetY = v3.Y;
translateTransform.OffsetZ = v3.Z;
}
public Vector3D GetPosition()
{
return new Vector3D(translateTransform.OffsetX, translateTransform.OffsetY, translateTransform.OffsetZ);
}
// Поворачивает объект
public void Rotation(Vector3D axis, double angle, double centerX = 0.5, double centerY = 0.5, double centerZ = 0.5)
{
rotationTransform.CenterX = translateTransform.OffsetX;
rotationTransform.CenterY = translateTransform.OffsetY;
rotationTransform.CenterZ = translateTransform.OffsetZ;
rotationTransform.Rotation = new AxisAngleRotation3D(axis, angle);
}
public Size GetSize()
{
return Size;
}
}
This class creates a square and draws a texture on it. I think by the names of the methods it is clear which method is responsible for what. To draw geometric shapes, I will use a texture and overlay it on a square with an alpha channel.
Box2D - Hello world
Let's start with the most basic, with the creation of the world. The world in Box2D has certain parameters, these are the boundaries (square) in which physical bodies are processed.
The parameters of the world also have a gravity vector and the ability of objects to "fall asleep" if their inertia is zero, this is well suited to save processor resources. Of course there are more parameters, but so far we only need these. Create a Physics class and add the following constructor:
The code
public class Physics
{
private World world;
public Physics(float x, float y, float w, float h, float g_x, float g_y, bool doSleep)
{
AABB aabb = new AABB();
aabb.LowerBound.Set(x, y); // Указываем левый верхний угол начала границ
aabb.UpperBound.Set(w, h); // Указываем нижний правый угол конца границ
Vec2 g = new Vec2(g_x, g_y); // Устанавливаеи вектор гравитации
world = new World(aabb, g, doSleep); // Создаем мир
}
}
Next, you need to write methods to add various physical bodies, I will add 3 methods to create a circle and a square and a polygon. I immediately add two constants to the Physics class:
private const string PATH_CIRCLE = @"Assets\circle.png"; // Изображение круга
private const string PATH_RECT = @"Assets\rect.png"; // Изображение квадрата
I describe a method for creating a square body:
The code
public MyModel3D AddBox(float x, float y, float w, float h, float density, float friction, float restetution)
{
// Создается наша графическая модель
MyModel3D model = new MyModel3D(models, x, -y, 0, PATH_RECT, new System.Windows.Size(w, h));
// Необходим для установи позиции, поворота, различных состояний и т.д. Советую поюзать свойства этих объектов
BodyDef bDef = new BodyDef();
bDef.Position.Set(x, y);
bDef.Angle = 0;
// Наш полигон который описывает вершины
PolygonDef pDef = new PolygonDef();
pDef.Restitution = restetution;
pDef.Friction = friction;
pDef.Density = density;
pDef.SetAsBox(w / 2, h / 2);
// Создание самого тела
Body body = world.CreateBody(bDef);
body.CreateShape(pDef);
body.SetMassFromShapes();
body.SetUserData(model); // Это отличная функция, она на вход принемает объекты типа object, я ее использовал для того чтобы запихнуть и хранить в ней нашу графическую модель, и в методе step ее доставать и обновлять
return model;
}
And to create a round body:
The code
public MyModel3D AddCircle(float x, float y, float radius, float angle, float density,
float friction, float restetution)
{
MyModel3D model = new MyModel3D(models, x, -y, 0, PATH_CIRCLE, new System.Windows.Size(radius * 2, radius * 2));
BodyDef bDef = new BodyDef();
bDef.Position.Set(x, y);
bDef.Angle = angle;
CircleDef pDef = new CircleDef();
pDef.Restitution = restetution;
pDef.Friction = friction;
pDef.Density = density;
pDef.Radius = radius;
Body body = world.CreateBody(bDef);
body.CreateShape(pDef);
body.SetMassFromShapes();
body.SetUserData(model);
return model;
}
I will not do this here, but you can create polygons in approximately this way:
The code
public MyModel3D AddVert(float x, float y, Vec2[] vert, float angle, float density,
float friction, float restetution)
{
MyModel3D model = new MyModel3D(models, x, y, 0, Environment.CurrentDirectory + "\\" + PATH_RECT, new System.Windows.Size(w, h)); // Данный метод нужно заменить на рисование многоугольников
BodyDef bDef = new BodyDef();
bDef.Position.Set(x, y);
bDef.Angle = angle;
PolygonDef pDef = new PolygonDef();
pDef.Restitution = restetution;
pDef.Friction = friction;
pDef.Density = density;
pDef.SetAsBox(model.Size.Width / 2, model.Size.Height / 2);
pDef.Vertices = vert;
Body body = world.CreateBody(bDef);
body.CreateShape(pDef);
body.SetMassFromShapes();
body.SetUserData(model);
return info;
}
It is very important to draw convex polygons so that collisions are handled correctly.
Everything is quite simple here if you know English. Next, you need to create a method for processing logic:
The code
public void Step(float dt, int iterat)
{
// Параметры этого метода управляют временем мира и точностью обработки коллизий тел
world.Step(dt / 1000.0f, iterat, iterat);
for (Body list = world.GetBodyList(); list != null; list = list.GetNext())
{
if (list.GetUserData() != null)
{
System.Windows.Media.Media3D.Vector3D position = new System.Windows.Media.Media3D.Vector3D(
list.GetPosition().X, list.GetPosition().Y, 0);
float angle = list.GetAngle() * 180.0f / (float)System.Math.PI; // Выполняем конвертацию из градусов в радианы
MyModel3D model = (MyModel3D)list.GetUserData();
model.SetPosition(position); // Перемещаем нашу графическую модель по x,y
model.Rotation(new System.Windows.Media.Media3D.Vector3D(0, 0, 1), angle); // Вращаем по координате x
}
}
}
Remember the model in the AddCircle and AddBox methods that we stuffed into body.SetUserDate () ? So, here we get it MyModel3D model = (MyModel3D) list.GetUserData (); and twirl as Box2D tells us.
Now all this can be tested, here is my code in the default window class:
The code
public partial class MainWindow : Window
{
private Game.Physics px;
public MainWindow()
{
InitializeComponent();
px = new Game.Physics(-1000, -1000, 1000, 1000, 0, -0.005f, false);
px.SetModelsGroup(models);
px.AddBox(0.6f, -2, 1, 1, 0, 0.3f, 0.2f);
px.AddBox(0, 0, 1, 1, 0.5f, 0.3f, 0.2f);
this.LayoutUpdated += MainWindow_LayoutUpdated;
}
private void MainWindow_LayoutUpdated(object sender, EventArgs e)
{
px.Step(1.0f, 20); // тут по хорошему нужно вычислять дельту времени, но лень :)
this.InvalidateArrange();
}
}
Yes, I forgot to mention that I added the px.SetModelsGroup () method to the Physics class; for convenience, passing a reference to the Model3DGroup object. If you use any other graphics engine, then you can do without it.
You probably noticed that the coordinate values of the cubes are too small, because we are used to working with the pixel. This is due to the fact that in Box2D all metrics are calculated in meters, so if you want everything to be calculated in pixels, you need to divide the pixels by 30. For example, bDef.SetPosition (x / 30.0f, y / 30.0f); and everything will be buzzing.
Already with this knowledge, you can successfully write a simple game, but Box2D has a few more chips, for example, collision tracking. For example, to know that a bullet hit a character, or to simulate different soil, etc. Create the Solver class:
The code
public class Solver : ContactListener
{
public delegate void EventSolver(MyModel3D body1, MyModel3D body2);
public event EventSolver OnAdd;
public event EventSolver OnPersist;
public event EventSolver OnResult;
public event EventSolver OnRemove;
public override void Add(ContactPoint point)
{
base.Add(point);
OnAdd?.Invoke((MyModel3D)point.Shape1.GetBody().GetUserData(), (MyModel3D)point.Shape2.GetBody().GetUserData());
}
public override void Persist(ContactPoint point)
{
base.Persist(point);
OnPersist?.Invoke((MyModel3D)point.Shape1.GetBody().GetUserData(), (MyModel3D)point.Shape2.GetBody().GetUserData());
}
public override void Result(ContactResult point)
{
base.Result(point);
OnResult?.Invoke((MyModel3D)point.Shape1.GetBody().GetUserData(), (MyModel3D)point.Shape2.GetBody().GetUserData());
}
public override void Remove(ContactPoint point)
{
base.Remove(point);
OnRemove?.Invoke((MyModel3D)point.Shape1.GetBody().GetUserData(), (MyModel3D)point.Shape2.GetBody().GetUserData());
}
}
Please note that we inherit a class from ContactListener. Used in Box2D to track collisions. Next, we just pass the object of this class to the world object in the Physics class, for this we write a function:
public void SetSolver(ContactListener listener)
{
world.SetContactListener(listener);
}
Create an object and pass it:
Game.Solver solver = new Game.Solver();
px.SetSolver(solver);
In the Solver class there are several callbacks that are called in turn according to the names, we will hang one for listening:
solver.OnAdd += (model1, model2) =>
{
// Произошло столкновение тел model1 и model2
};
Also, you can fasten a string name property to the MyModel3D class, set a value for it, and already in the OnAdd callback to check specifically which body the body encountered.
Box2D also allows you to make connections between bodies. They can be of different types, consider a couple:
The code
public Joint AddJoint(Body b1, Body b2, float x, float y)
{
RevoluteJointDef jd = new RevoluteJointDef();
jd.Initialize(b1, b2, new Vec2(x, y));
Joint joint = world.CreateJoint(jd);
return joint;
}
This is a simple rigid connection of the body b1 and b2 at the point x, y. You can see the properties of the RevoluteJointDef class. There you can make the object rotate, suitable for creating a machine, or mill. Move on:
The code
public Joint AddDistanceJoint(Body b1, Body b2, float x1, float y1, float x2, float y2,
bool collideConnected = true, float hz = 1f)
{
DistanceJointDef jd = new DistanceJointDef();
jd.Initialize(b1, b2, new Vec2(x1, y1), new Vec2(x2, y2));
jd.CollideConnected = collideConnected;
jd.FrequencyHz = hz;
Joint joint = world.CreateJoint(jd);
return joint;
}
This is a more interesting connection, it emits a spring, the value of hz is the spring tension. With such a connection, it is good to make a suspension for a car, or a catapult.
Conclusion
This is not all that Box2D can do. The cool thing about this engine is that it is free and there is a port for it on any platform, and the syntax is practically the same. By the way, I tried to use it in Xamarin on 4.1.1 android, the garbage collector constantly slowed down applications due to the fact that Box2D generated a lot of garbage. They say starting with the fifth android thanks to ART, everything is not so bad, although I did not check.
Link to the GitHub project: github.com/Winster332/Habrahabr
Port on dotnet core: github.com/Winster332/box2d-dotnet-core-1.0