Building Bare-Bones Game Physics in Rust with Python Integration

Python’s simplicity makes it the go-to choice for scripting, while Rust excels in performance-critical tasks like game physics. This talk demonstrates how to build a minimalist physics engine in Rust, focusing on core concepts like collision detection, basic rigid body dynamics, and force application, while providing seamless Python integration using PyO3.

We’ll explore how PyO3 allows developers to expose Rust functionality as native Python modules, enabling Python developers to easily script and interact with the physics engine. Through practical examples, attendees will see how Python can be used for rapid prototyping and gameplay scripting, while Rust handles the heavy lifting of physics calculations.

By the end of this session, participants will not only understand the basics of implementing physics in Rust but also how to use PyO3 to bridge the gap between Rust’s performance and Python’s flexibility. This talk is perfect for Python enthusiasts curious about Rust or Rustaceans looking to make their libraries accessible to the Python ecosystem.

This session took place in track Rust and was classified suitable for intermediate python by the speaker.

Transcript (auto)

Auto-generated from the recording utilizing Open-Source AI. Speaker labels (Speaker 1, Speaker 2) reflect diarization, not identity. Timestamps refer to the recording.

Speaker 1 [00:07]

So, just because I think that the game physics term is a bit confusing, so I thought, like, to show what I meant. So, it's about making a 2D physics engine, the engine behind finding out how objects collide with one another. And you might see something like that in Angry Birds, for example, where you collide things, and they behave as expected, and humans expect that. So, as I said, I'm Sam. I work at Blue Yonder. My day job is about doing things with AI, but this talk is not about AI. And this talk is mostly a personal project, but why I did it in the first place is that, first, things like Angry Birds are cool. The second thing is that I really wanted to do a ROS project and then integrate it in Python. That is not just some very simple project. I wanted to get some complexity. And when you want to do something that you learn really what things are, how things work, and you get into troubles here and there, then you need to do something a little bit more complex. That's why I chose to do something with game physics or rigid body physics. And also that the problem is quite solved in terms of the physics aspects, and you get a lot of sample code. So I used, for example, box2delight library a lot that, you know, already has all the C code, for example, and you can get information from that. So going a little bit about why would you use Rust with Python, right? I mean, I should not promote Python in a Python conference. Python conference, well, Python is amazing, you get a lot of rapid prototyping, you get all of the things that you love Python for. But there are moments that you get to use cases, for example, you have some performance critical aspects, some really CPU-intensive thing that you want to do differently, or you're wondering, okay, should I use something else? And I personally think that it's a good idea sometimes to consider other programming languages. You know, you do a UI and you go to JavaScript or TypeScript. If you do, like, an iOS app, you would go to Swift maybe, you know, because a lot of people have figured out how to solve the problems that you have with those languages, and there are libraries and there are, like, help there. But when we go to performance-intensive applications and we want to integrate with Python, maybe we first think about C and C++, I mean nowadays maybe everyone thinks about Rust as it's hyped, but why to choose Rust? I thought that two plots from Google security blog is actually a good idea to show. So they started adopting Rust for some of their performance intensive parts of the code code and they saw that the memory safety vulnerabilities dropped down as they had more adoption. But also one other interesting thing you see in the left plot is that the amount of memory on safe code that they had did not reduce, it actually increased a bit. And that's why because if you have some code around that's been around for five, six years and you really battle tested it, so maybe it's not really needed to be ported because you already know it works well and you already found all the bugs in the code. So that's a little bit of motivation of why to use ROS plus Python. But going to the physics part is that what are the, by the way, all the things I pushed upwards because I saw like everyone has a problem in the back seeing things, if it looks a bit weird, that's why. So every step of the, every frame of animation or every frame of the game, you have to do all of these operations that I put here. So the first operation or broad phase is where we want to kind of find which objects are actually in our world are in, have the possibility to collide with one another. And then if you find those, we want to know the contact points. So we go to what we call the narrow phase, where we would actually find what are the points that these two objects, these two objects that follow the rules of our game physics, have with one another. When we find the contact points, then we can start applying forces and change the speeds and other things and give some action. On top of it, we would like to have some constraints to create some more complex behavior. And for the complex, if you have a pendulum or something that's a bit more fun to have around like a chain, you would need to have the constraints. And then you can solve them and correct what the application, the speeds of the objects that were in your simulation step or the frame of animation and make objects follow those rules. And you have to do this many, many times. So if you have, for example, a 30 frame per second, you have to do this 30 times per one second, for example, if you have more frame rate. That's why it's performance intensive. I want to tell a bit how, for example, collision detection algorithm works. I chose a very, very simple algorithm. It's just clipping points of object B with edges of object A. And what you do is that you select every edge of object A and ask, are the points of B on the right side of it or on the left side of it or on the line? And you go around and then check all the points that remain. And then reiterate and check the points that remain. And if they are on the edge, you select the ones that are on the edge, and you end up with these red dots. And also, you need, on top of this, a direction so that you would apply forces or update the speeds in those directions. So when you have those directions, we have two types of speed updates we want to do, or velocity updates we want to do. One is translational, like you drive in your car, you move forward, backward, or if you drive left and right. And the rotational is something that probably you don't want to do when you're in your car, right? So each step, you want to update that with the forces that you consider in the world that you have, or when the objects collide with one another. Third Newton law, probably you remember from school. You get updates from one another. So they push and update each other's velocity. Then you find the new positions and new rotations with this information. A little bit more complex thing is constraints. Now you want, for example, these objects to keep a relative position, for example, with one another. Then you can also formulate that with, for example, considering an anchor point and then say, OK, compared to this anchor point, I don't want these objects to change their distances. And that you can express with the relative velocity of that anchor point to remain 0. And then with mathematical magic that was found in, I think, 19th century, you can translate that information to updates in the velocities of the objects. So the green, which is the ground, and the pink here would be get the corrections. So all of that was the kind of not so much programming parts of things, but understanding parts of things about what goes into a such system. But when I started this, I was presented with this big decision. So should I keep all the code as a PyO3 project and then have the Python interface and Rust all together? And that seemed to be the way to go. But then I thought, oh, I want to port this C code that I like and it's easy. But then if I want to think about all the restrictions that PyO3 brings and it's not like ROS native, then I have to think about other things. Then I thought, oh, OK, maybe I separate these two. So I have a PyO3 project that keeps the Python part, Python integration part and the ROS project that can work standalone. And because it was easier, I thought, OK, maybe I go first with this. And I went with the left option of keeping these two as two separate projects. But then I understood later that, OK, whatever decision you make in the ROS will later come to bite you in principle. But also I got for free a lot of other possibilities for integration. So as you saw, I started the presentation with showing a little bit of a demo. And that one was on a browser deployed in GitHub Pages because I could compile this to WebAssembly, basically. And I used a package called nano for the graphics. And then you could create a very simple application and already have it there. And if you want to do it with Python, might be not as easy because not all of the graphics packages are supporting WebAssembly as good and are probably also not so fast. So I want to just show a little bit of what is the structure of the Rust library. I'm not going to show the code, but more of a schematic representation of how many objects do we need to present all of those things that I talked about. It's actually quite simple. You don't need a lot of things. You want a global context. We call it world, which is the world of objects that we have. And we add to this world the bodies that we have, the objects that want to collide. So I keep a vector of references to these body objects. And I want to have the constraints. So I present constraints as joints between these two objects. I have object one and object two, and I want to join them and that they keep their distance relative to one another. So I need another object called joints, and then also I keep a vector of those in the world. And then I also want to have an understanding of, okay, which ones are colliding with one another, then I want an object that I call a collision manager or an arbiter between the objects that would know which things are colliding with one another and what are the contact points for those. And on the world objects, so very understandable, I need to add the have a function to add these bodies, add joints, and then also, like, as I mentioned, we have a step, which is the frame of animation goes forward, and I have a function called a step. So very intuitive and keeps everything together. When you go to something that is very production-ready, all of this becomes very complicated. The mental image becomes very not as easy to kind of keep it in one slide. And that's also one of the ways that you can always find a project that is well-contained enough and you can end up implementing it and have the whole image and then you can understand all the production code that goes around. On all of these objects, I have a function that's called apply impulse, which means applying forces and updating the speed. So I do that in the step function of the world, basically do call all of these apply impulses. And as I said, I have the last object to the right, because it did not fit in the previous slide, which is the body, which has the attributes of the object. It's position, it's velocity, it's rotation, and things like that. So all of this is basically the Rust library. It doesn't have a lot of objects that you can, in principle, create a very, as you saw in the demo, functioning library with that not so much code. to say, okay, now I thought about, now that I want to come to the main task that I wanted, to implement it inside, expose it to Python, basically, is it going to be very difficult now? Well, it turned out not so much, because I can just, you know, take that library that I made and wrap it around new structs with the same names, just adding a pi before them, So if I had a body object, I would call it a PyBodyObject, and, you know, I can use this macro called PyClass, and it's already there, and with magic of PyO3, I would say. And then you can have also the constructor with using the new macro of also the you need to implement this struct and then annotate it with the PyMethods macro, and then you would get the constructor of the class. We would also need some getters and setters for the object to change the attributes. So that's I hid that information. The other part was I wanted to have a loop on all the objects that are inside the world. And this seemed to I thought, oh, maybe that would be very difficult, but it's very intuitive for someone who develops Python that you have the Donder methods now, Donder methods in in PyO3 that you can actually create a loop by just implementing this ether and next methods. And you get also a loop inside your Python code when you expose the module. So this is like the world object. Again, I define equivalently the new add body, either bodies like objects. And those are just calling the world objects add functions each step. And I want first your focus to go to the last part, which is the step. And then I can also take and expose all of the errors I get inside the Rust code with the PyRuntime error and exceptions inside PyO3. So anything that I create, if I do good error handling inside Rust, then I can get it also in Python and see those errors in Python. You can also do nice things with the errors that do not look like Rust errors. One thing that I had the problem with was, as I said, our earlier decisions in the Rust library comes to bite us later. And I wanted to do the ad joint. The ad body worked very well, because the body object was copy and clone, and I could do it. But when I tried to put the adjoint, the PyO3 didn't like it because, oh, you know, Python wants everything to be, you know, we don't have this internal mutation concept. So we want everything to be copy and clone. And this is not. And then I had to come up with some things that might not look so elegant, you know. Put it in an option, then take it, and, you know, doing a bit of a memory switcheroo there that might not look so nice, but you end up getting some challenges as, you know, I thought in the beginning the promise of making something more complex would give also more challenges. Then, you know, you can expose it with the PyModule and then import it, import this new module in a Python script, for example, with PyGame or Esquia, and then, you know, define the world object, give the gravity, for example, a number of iterations in the calculation. I mean, the constructors can be nicer looking, but that's for some updates or later. And then you would have some animation loop, and then you would call your world step similar to the intuitive image that we had in the beginning. You can, in principle, use the library. So I want to show a quick demo, if I manage to do that. So I hope that these things work as expected. So this is basically the lines of maybe it's a bit not so visible. I don't know. Thank you. Yeah. So in principle, for example, I'm using here SQL for the, as I said, for drawing part. So a lot of things are about just creating a window, for example, not part of the library, but you need to have these things. If you use Pygame, it's easier, I think. And then all of the SQL objects, and then you get to the part of, you know, like I wanted to start the world, so I give it a world, and gravity, like gravity. Then I have a few simple body objects, and then I add the bodies to the world, and define a joint between them. Lights don't collaborate. And this is basically an animation loop of this simple simulation. And I can just even run it because I think that the code doesn't matter that much. Hope this also works. And you get magical. I would say that the simulations on the Rust part was more exciting because, well, I spent more time there. But I can tell also one thing about debugging a library that does things that are not very easily testable when you want to try to test things by writing like, oh, I want this object to have this and that coordinates when it collides, doesn't end up being that easy. So you have to end up always try to make some debugging frameworks. So I did, for example, this little simulation to check if things actually collide with one another. Well, I maybe need to increase this size for Excel. And to just check, OK, does this make sense, for example, these objects function as the way I like them or not? And then also, for example, you would need even these samples from the, I think this does not update anymore. Because for example, I think I had this one. And in this specific demo, I understood that I'm handling rotations, for example, wrongly. And then I could update for that. Sorry about the things a bit. And then when you have this, it's a lot easier, because I was trying to find everything through the code. And I did write a lot of tests after doing these things and finding about the right coordinates to put in the tests for later on when I change something. But in the beginning, it's really nice to have some demos. And that's basically everything that I think I have to say. I can go back to, I can play all day with these things. There is the links to all of the projects. So if you want to kind of do something similar, you want to play around, for example, how to create a WebAssembly project, the code samples are all there. Yeah, and it's quite expandable as well, I was thinking for example to do a bit of, you know, storing the state of animation, the simulation, for example, as expansion or, you know, try to port it to WebGPU of the operations as another, you know, programming, you know, task or another expansion. That's all I have.

Speaker 2 [22:55]

Again, thank you so much, Sam. So I think one of few demos which I've seen successfully getting executed live because usually, you know, demos, they always, yeah. Yeah, so I've got a couple of questions here. The first one, how complete does PyO3 feel to you? Was there anything missing you needed for your project in terms of Python REST interop?

Speaker 1 [23:21]

I'm not a PyO3 expert. There are talks about PyO3. It looked quite complete to me. I mean, it functioned quite well for me. But I guess as you expand and you have more complex projects, you end up hitting the limits of a library. I'm also not an expert in Rust.

Speaker 2 [23:44]

Thank you, and the next one can next and iter for the python class be automatically derived if the respective rest structure implements the iterator trait

Speaker 1 [23:54]

I'm not sure if I understand, this is the way that you have to implement it so that it would function in the Python side, so that's the requirement from PyO3 and that's how they ask you to do it. If you want to implement an iterator on the Rust side, it looks different and I mean also in the code if you look, there is also a body iterator inside in the Rust side, it looks different, you have a different type of defining the next.

Speaker 2 [24:25]

Okay. Thank you. And the next one, do you have a hint how a game with Rust slash Py could be made easy to install? Do I have a hint? Do you have a hint how a game with Rust slash Py could be made easy to install?

Speaker 1 [24:41]

Oh, for example, I use for, maybe I cannot show it, but I try to put everything in, if I show the readme, no, it's very difficult with the, so I put everything in a Pixie project, so I, you know, like, I just do Pixie install, Pixie run this and that, and it keeps all the, you know, everything is contained. So I don't have to worry about a lot of the interdependencies. Also, I think Esquia didn't like a lot of things when I wanted to do it just with the requirement.txt. And then I saw that's even more effort. You know, Pixie is the way to go.

Speaker 2 [25:26]

Yeah. Thank you. That was quite a visual explanation. How long did it take you to create the game?

Speaker 1 [25:33]

I do this on my free time, so as I said, because all of these are very solved problems, you get a lot of sample code, you can use ChatGPT or any of your favorite GPTs. So it took, I think, all of this together maybe less than two months. But if I was doing it full-time, probably in a week you can do it.

Speaker 2 [26:02]

OK, thank you so much. We've got like two more minutes. Do you guys have any more burning questions? OK.

Speaker 3 [26:10]

So I haven't played with Rust and Python integration, so I don't really know how it goes, but I saw that you had to go to a smart pointer or something like that. Is that mandatory? I'm thinking especially here in a physics engine, you want to avoid smart pointer because you want to avoid cache misses and things like that when iterating.

Speaker 1 [26:33]

I mean, there is always, you know, I mean, if you're using Rust and you have some pointing to some, you know, like pointer, you want to use a smart pointer. But which one of them you want to use is based on what are you ready to pay, what price are you ready to pay. So if you use just a ref cell bounded in a reference counted, you know, reference, then, you know, it might not be that expensive. But then if you want it also to be multi-threaded, then you have to use atomic operations. So things get a bit more costly. But it's also not that, I would say, it doesn't go anything beyond how you would do it. I don't think that the performance hit is that bigger than if you would implement it in C. Because also there is a lot of optimization that happens when things get compiled. Yeah, but the reason I need the body objects to be always muted is because I want to update the position and velocities. And maybe if you're better at choosing how your objects go around, as I said in the production-ready libraries, they do spend time to try to separate everything out that they would use the less costly thing to implement. But you do need to use smart pointers, and I think that's a nice thing about Rust that gives you a lot of different ways to handle memory compared to C. And also, you know, if you don't like this way, maybe you can go to other programming languages that approach the same problem in a different way, right? yeah thanks for a great talk and a quite practical one did you also had some experience in regards siphon in comparison to the rust implementation for similar tasks no okay thank you

Speaker 2 [28:36]

Yeah, so I think we have time. So that was really amazing, Sam. And thank you for those great explanations. So then we give a round of applause to Sam.

Sam Kaveh

About — in the speaker's own words

Born in Iran, I have embraced diverse roles throughout my career, ranging from founding a startup and software development to consulting companies on cloud migrations and integrating machine learning technologies into their operations. My professional journey has been shaped by a passion for problem-solving and innovation across various domains.

Academically, I hold a Ph.D. in particle physics, specializing in Higgs boson precision measurements as part of the CMS experiment at CERN's Large Hadron Collider. This experience honed my analytical skills and gave me a deep appreciation for collaboration in high-stakes, cutting-edge environments.

Today, I draw on my multidisciplinary background to create solutions at the intersection of software, data science, and high-performance computing, continually seeking to bridge theory and practice in impactful ways.

Social card for talk: Building Bare-Bones Game Physics in Rust with Python Integration