Let me start by introducing my project. I have been developing a universal game engine, kind of similar to what IDEs like Game Maker, Game Editor, and Construct use, called the "SGE", which stands for "Stellar Game Engine" and is pronounced like "sage". More information can be found at the SGE's web page.[1]

I noticed recently that I never made the rotation mechanism used in the SGE rotate images about what is called the origin: the point where an image is positioned from, so I decided to implement this recently. Unfortunately, the way I have done it seems to be completely wrong. Without the new code I added (albeit with some fixes to the rotation, which was slightly off previously), images rotate perfectly about the center. With the new code which is intended to make the image rotate around the origin, however, the rotation is completely different, and what exactly happens is hard to describe.

To see the problem, checkout or download the Git repository from one of the Git repos[2][3] (they are currently the same; the GitHub repo will be dropped at some point in the future) and run examples/rotation.py. You will need Python 2 and Pygame 1.9 or later. You will see a few needles rotating; the point they should be rotating around is the head (the fatter parts of the needles), but instead they rotate in a completely different way which I don't really understand.

I have found that the problem is somewhere between lines 1651 and 1697 of sge-pygame/StellarClass.py. However, I ran through a test case using the method on paper with origin_x=18, origin_y=4, rotation=112, image_width=32, image_height=32 and got a result that seems right: an x offset of about -6.5 and a y offset of about 51.6, resulting in the origin being at about (11.5, 55.6). (In fact, I did find some problems with some calculations, but I fixed those, and the rotation is still wrong.)

Here is the code where the problem lies (a little bit more than the problem lines; the problem lines start at "if rotation % 360"):

The part that gave me the biggest headache was "x_mag" and "y_mag", because I couldn't for the life of me figure out what "mag" stood for and couldn't figure out what they meant until I considered the usage of math.atan2 to find the "start angle"; of course, it's the origin relative to the center instead of the top-left.

In order to save processing you shouldn't recalculate the radius and start angle every frame (unless you need to). This is actually why I moved them out into a callable class. Also, not sure if you have it planned or not but I recommend you build rotation caching into your engine. Rotation can be an incredibly expensive process when done on mass.

Rotation caching is already in there (every transformation ever made to each image, and that includes rotation and scaling, is cached). Radius has to be recalculated each time unless it's cached; the position of a sprite's origin can change at any time. If the performance hit is significant, I guess I can cache radius and angle values for each origin position. Are square root and arc tangent calculations really that expensive?

Neither the radius nor the start angle need to change actually unless you are dynamically changing the radius. The location of the origin may change, but that doesn't effect the radius or the start angle. And no, the big cost is definitely the rotation/scaling itself; but if you can avoid recalling trig functions and powers without loss it is still worth it in my opinion. That kind of stuff can still add up.

Anyway, few minor edits to my version, including making one of the images move with the arrow keys. This demonstrates how even with a moving origin the radius and start angle are not recalced.

@mekiremight i suggest keep going with the pygame tutorials snippets on your github? It is quite a good pygame refresher or general tutorial, and you know how hard those are to find. I enjoy also skimming them too when i have been away from pygame/python awhile.

Mekire wrote:Neither the radius nor the start angle need to change actually unless you are dynamically changing the radius. The location of the origin may change, but that doesn't effect the radius or the start angle.

How can that be the case? What you called "x_mag" and "y_mag", and what I renamed to "xorig" and "yorig", is used to calculate both of these, and these two variables are calculated based on the location of the origin and the size of the image (where the center is, specifically). In short, the start angle is the angle on the unit circle if you assume that the center is the origin, and the radius is the distance of that point from (0, 0). So if the origin is at (0, 0) with an image that's 32x32, you'll have a start angle of 125 degrees and a radius of sqrt(512), but if the origin is at (18, 16) with that same image, the start angle will be 0 degrees and the radius will be 2.

Even if I misunderstood something, though, the radius can dynamically change. Images can be scaled and resized at any time.

Mekire wrote:And no, the big cost is definitely the rotation/scaling itself; but if you can avoid recalling trig functions and powers without loss it is still worth it in my opinion. That kind of stuff can still add up.

I'd have to highly disagree with that. It's not Pythonic to worry about performance hits if they're not a real problem.

I did some quick tests, and I found that these operations do take about 3 times longer than a few "normal" math operations, but I don't think this is inefficient enough to justify making a big deal about it; on my laptop, at 10 million repetitions, it took 6 seconds to do both a math.hypot call and a math.atan2 call as opposed to 2 seconds for one each of addition, subtraction, multiplication, and division. I took a look at checking a dictionary (the way I would cache results), too, and that took about 1 second. If I'm doing so many repetitions that a little basic arithmetic is taking up two seconds of time in a single frame, I clearly need to reduce the number of repetitions, so I don't really see any sense in caching these results in an attempt to make the function itself more efficient.

Yes, resizing would require recalcing, especially if it actually changed the aspect ratio of the sprite; but this is another matter. My point is it doesn't need to be recalculated 60 times a frame (or whatever your target frame rate). I do agree that caching them would be going a bit far though. As for unpythonic; yes mindlessly focusing on tiny optimizations can be unpythonic, but that logic is not justification for writing inefficient algorithms (Edit: Harsher sounding than intended. I don't mean that yours is inefficient; just that simplicity in general shouldn't be used as an argument against efficiency). If you have numerous bodies on screen all making 60 calls to those functions a second though, it can easily cause frame loss. If you only have one or two, yeah it shouldn't matter much. I actually originally wrote mine as a single function that took all these arguments too until I realized that they didn't need to change.

I also wasn't sure if you were just talking about moving the origin point around but keeping the rotation axis the same (as in my example), or if you are trying to allow the user to change the sprites rotation axis at any time. The latter will get a little tricky and certainly require recalculation.

Anyway, good luck to you. Either way sounds like you have essentially solved your problem.-Mek