📜 ⬆️ ⬇️

WPF and Box2D. How I did physics with WPF

image

Good time Habr. I'm a big fan of physics in games, I worked with some interesting physics engines, but today I’ll tell you about Box2D. It is as simple and straightforward as possible and is perfect for two-dimensional physics. I noticed that there are very few Box2D tutorials on C # on the Internet, there are almost none. I have been repeatedly asked to write an article about this. Well, the time has come. There will be a lot of code, letters and some comments. To render the graphics, use WPF and the Viewport3D element. Who cares, welcome tackle.

Box2D is a 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 is designed by Erin Catto (born Erin Catto), written in the C ++ programming language and distributed under the license zlib.

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 drawing 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 because Graphics displays graphics through the CPU and will take a lot of CPU time.

Let's write a small environment for working with graphics. In the default project window, add the following XAML:

Code
<Grid> <Viewport3D x:Name="viewport" ClipToBounds="True"> <Viewport3D.Camera> <PerspectiveCamera FarPlaneDistance="1000" NearPlaneDistance="0.1" Position="0, 0, 10" LookDirection="0, 0, -1" UpDirection="0, 1, 0"/> </Viewport3D.Camera> <ModelVisual3D> <ModelVisual3D.Content> <Model3DGroup x:Name="models"> <AmbientLight Color="#333"/> <DirectionalLight Color="#FFF" Direction="-1, -1, -1"/> <DirectionalLight Color="#FFF" Direction="1, -1, -1"/> </Model3DGroup> </ModelVisual3D.Content> </ModelVisual3D> </Viewport3D> </Grid> 


ClipToBounds - says that invisible faces will be cut off, although here it is not useful because there will be a 2D projection, I will 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 is looking and how it looks. Next we create the Model3DGroup element in which we will throw the geometry through its name “models”, and add 3 lights to it.

Well, with XAML figured out, now you can start writing a class to create geometry:

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<Point3D> { 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<int> { 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 the names of the methods understand what method is responsible for what. To draw geometric shapes, I will use a texture and lay it on a square with an alpha channel.

imageimage

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 borders (square) in which physical bodies are processed.

image

In the parameters of the world, there is also a gravity vector and the ability of objects to “fall asleep” if their inertia is zero, this is well suited for saving processor resources. Of course there are more porametrov, but so far we only need these. Create the Physics class and add the following constructor:

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 different physical bodies, I will add 3 methods creating a circle and a square and a polygon. 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:

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:

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 a similar way:

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 processed correctly.

It's all simple enough if you know English. Next you need to create a method for processing logic:

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 that model in the AddCircle and AddBox methods that we crammed into body.SetUserDate () ? So, here we get it MyModel3D model = (MyModel3D) list.GetUserData (); and we are turning as Box2D tells us.

Now all this can be done, here is my code in the default window class:

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(); } } 


image

Yes, I forgot to mention that I added the px.SetModelsGroup () method to the Physics class; for the convenience of passing the reference to the Model3DGroup object. If you use any other graphics engine, then you can do without it.

You probably noticed that the coordinates of the cubes are too small, because we are used to working with a 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 good.

Already with this knowledge, you can successfully write a simple game, but Box2D has a few more chips, for example, tracking of columns. For example, to know that the bullet hit the character, or to simulate a different ground, etc. Create a Solver class:

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. Then we simply pass the object of this class to the world object in the Physics class, for this we write the 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 in accordance with the names, hang one to audition:

 solver.OnAdd += (model1, model2) => { //    model1  model2 }; 

You can also attach a property of type string name to the class MyModel3D, give it a value and, in the OnAdd callback, check specifically which body has collided with which body.

Also Box2D allows you to make connections between the bodies. They can be of different types, consider a couple:

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 view the properties of the RevoluteJointDef class. There you can make the object rotate, suitable for creating a machine, or a mill. Go ahead:

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 tension of the spring. With such a connection, it is good to make a suspension for the car, or a catapult.

Conclusion


This is not all that Box2D can do. What's great about this engine is that it is free and has a port for any platform, and the syntax is almost 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 produced a lot of garbage. They say since 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

Source: https://habr.com/ru/post/333040/


All Articles