1

So I'm using pygame to create a simple top-down shooter game, and I'm doing lots of angle calculations from the top-down perspective. Let's use a simple arrow and a ball as an example, I want the red arrow to keep pointing at the blue ball regardless of where the blue ball moves:

enter image description here

And it seemed easy enough, I just needed atan2:

angle = math.atan2(blue.y - red.y, blue.x - red.x)

But the problem is, atan2 works for a mathematical coordinate grid like this:

enter image description here

Where alpha = math.atan2(blue.y - red.y, blue.x - red.x)

But the thing with pygame (on Windows at least) is that the coordinate grid doesn't work like a mathematical coordinate grid, it's actually upside down starting from the left top corner of the game window:

enter image description here

So while it looks like the blue ball is higher up and thus mathematically blue.y should be larger than red.y, this is actually not the case due to the upside down coordinate grid, which Python's math.atan2() doesn't know of, and the original calculation I had:

angle = math.atan2(blue.y - red.y, blue.x - red.x)

Actually yields the correct angle's negation.

Now the obvious first solution that I came up with was to just flip the sign, and fair enough it worked with this:

angle = -math.atan2(blue.y - red.y, blue.x - red.x)

But the issues started again once I needed to do further calculations based on the previously calculated angle, which technically is now upside down.

What countermeasures could I take to "permanently" get rid of this issue?

Here's an actual example of where I need this, I have a "zombie" entity which does nothing but follows the target it has been given:

class Zombie(Entity):

    def __init__(self, *args, target=None, **kwargs):
        super().__init__(*args, **kwargs)
        self.target = target

    def update(self, dt, app):
        if self.target:
            # Face towards target
            dx = self.target.x - self.x
            dy = self.target.y - self.y
            self.angle = math.atan2(dy, dx)
            # Change velocity towards target
            speed = self.get_max_speed()
            vel_x = math.cos(angle) * speed
            vel_y = math.sin(angle) * speed
            self.velocity = (vel_x, vel_y)
        else:
            self.velocity = (0, 0)
        # Moves the zombie based on velocity
        super().update(dt, app)

For this particular case I managed to solve it by storing the angle into a separate variable for later use, and negating it separately upon setting the self.angle:

# Face towards target
dx = self.target.x - self.x
dy = self.target.y - self.y
angle = math.atan2(dy, dx)
self.angle = -angle
# Change velocity towards target
speed = self.get_max_speed()
vel_x = math.cos(angle) * speed
vel_y = math.sin(angle) * speed
self.velocity = (vel_x, vel_y)

But this is just begging for more bugs, and I'm looking for a more generic solution to the issue.

1
  • 3
    This is a classic model view problem. You want to model your world in a cartesian coordinate system. Fine do it. But when you present it you'll need some logic in the view to present the cartesian coordinate system in the style of the built in one. Fine. Just don't mix the two together. That's what's giving you a headache. While working in the model you should be able to pretend that the cartesian coordinate system is the only one and pretend the other doesn't exist. So long as you always use your view logic to present this will work fine. Dec 10, 2017 at 6:16

1 Answer 1

7

Just flip your shape's y at render time (the final part of processing) if the coordinate system is causing you headaches instead of trying to deal with it in all the intermediary stages beforehand.

draw_shape(shape.x, screen.h - shape.y - shape.h, ...)

That'll basically get the coordinate system the way it you like with +y going upwards and a y value of 0 being at the bottom of the screen, not the top.

If that's unwieldy with what you did so far, you might just write yourself some trig functions that work intuitively given the coordinate system you're dealing with, like a sine function that returns a negation of the normal y value or one that inputs the values flipped (like in the above example using screen.h-shape.y). You can solve it either at input or output level. I think easiest way is input.

Negating arc tangent makes my brain hurt a bit, trying to visualize in my brain the transformation between coordinate systems (doesn't help that I'm really sleepy). I think you could just negate the vel_y instead.

Another solution that works regardless of coordinate system is to use vector math instead of working with angles with trig. You can get the direction by just subtracting the enemy position from projectile position and then normalizing the resulting vector, and then multiply by speed to get velocity.

vec.x = projectile.x - enemy.x
vec.y = projectile.y - enemy.y
vec_len_squared = vec.x*vec.x + vec.y*vec.y
if vec_len2 > 0:
    vec_rlen = 1.0 / sqrt(vec_len_squared)
    vec.x = (vec.x * vec_rlen) * speed
    vec.y = (vec.y * vec_rlen) * speed

I haven't used trig in years so this at least makes it easier for me.

4
  • 1
    I think you mean "You can get the direction by just subtracting the enemy position from projectile position and then normalizing the resulting vector". It will be a velocity after you multiply it by the speed. Dec 10, 2017 at 6:59
  • Ah yes, my bad... also I just realized a goof -- need to check the vector length for 0 against length squared prior to sqrt.
    – user204677
    Dec 10, 2017 at 10:01
  • 1
    I first ended up flipping everything at render time, but later realized pygame has a built-in Vector2 that I should use. Dec 10, 2017 at 13:40
  • Doh sorry, ideal answer should have taken that into account! I don't know pygame at all.
    – user204677
    Dec 10, 2017 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.