Jacob
Jacob

Reputation: 207

atan2 isn't providing me with the angle I want

I'm trying to write a game in pygame, involving a moving object with a "turret" that swivels to follow a mouse. As of now, I'm mostly trying to expand upon examples, so the code's not entirely mine (credit to Sean J. McKiernan for his sample programs); however, this portion is. Below is my code; I use the center of rect (the "base" shape and the point around which the "turret" swivels) as the base point, and the position of the mouse as the other point. By subtracting the mouse's displacement from the displacement of the "center," I effectively get a vector between the two points and find the angle between that vector and the x-axis with atan2. Below is the code I use to do that:

def get_angle(self, mouse):
    x_off = (mouse[0]-self.rect.centerx)
    y_off = (mouse[1]-self.rect.centery)
    self.angle = math.degrees(math.atan2(-y_off, x_off) % 2*math.pi)
    self.hand = pg.transform.rotate(self.original_hand, self.angle)
    self.hand_rect = self.hand.get_rect(center=self.hand_rect.center)

According to multiple tutorials I've reviewed, this SHOULD be the correct code; however, I discovered later that those tutorials (and, in fact, this tutorial) were all for Python 2.7, while I am trying to write in Python 3.6. I don't think that should make a difference in this scenario, though. As it stands, the view appears to depend entirely upon the "character's" position on the screen. If the "character" is in one corner, the reaction of the "turret" is different than the reaction if the "character" is in the middle of the screen. However, this shouldn't matter; the position of the "character" relative to the mouse is the exact same no matter where on the screen they are. Any ideas, or do I need to supply more code?

Edit: Apparently, more code is required. Rather than attempt to extricate only the entirely necessary parts, I've provided the entire code sample, so everyone can run it. As a side note, the "Bolt" things (intended to fire simple yellow blocks) don't work either, but I'm just trying to get the arm working before I start in on debugging that.

Edit the second: I have discovered that the "Bolt" system works within a certain distance of the origin (0,0 in the window coordinate system), and that the arm also works within a much lesser distance. I added the line Block(pg.Color("chocolate"), (0,0,100,100)) under the "walls" grouping as a decision point, and the block was positioned in the top left corner. I've corrected Bolt by changing screen_rect to viewport in the control loop; however, I don't know why the "arm" swinging is dependent on adjacency to the origin. The positions of the mouse and "character" SHOULD be absolute. Am I missing something?

"""
Basic moving platforms using only rectangle collision.

-Written by Sean J. McKiernan 'Mekire'
Edited for a test of "arms"
"""

import os
import sys
import math
import pygame as pg


CAPTION = "Moving Platforms"
SCREEN_SIZE = (700,700)
BACKGROUND_COLOR = (50, 50, 50)
COLOR_KEY = (255, 255, 255)


class _Physics(object):
    """A simplified physics class. Psuedo-gravity is often good enough."""
    def __init__(self):
        """You can experiment with different gravity here."""
        self.x_vel = self.y_vel = 0
        self.grav = 0.4
        self.fall = False

    def physics_update(self):
        """If the player is falling, add gravity to the current y velocity."""
        if self.fall:
            self.y_vel += self.grav
        else:
            self.y_vel = 0


class Player(_Physics, object):
    def __init__(self,location,speed):
        _Physics.__init__(self)
        HAND = pg.image.load("playertst2.png").convert()
        HAND.set_colorkey(COLOR_KEY)
        self.image = pg.image.load('playertst.png').convert()
        self.rect = self.image.get_rect(topleft=location)
        self.speed = speed
        self.jump_power = -9.0
        self.jump_cut_magnitude = -3.0
        self.on_moving = False
        self.collide_below = False

        self.original_hand = HAND
        self.fake_hand = self.original_hand.copy()
        self.hand = self.original_hand.copy()
        self.hand_rect = self.hand.get_rect(topleft=location)
        self.angle = self.get_angle(pg.mouse.get_pos())

    def check_keys(self, keys):
        """Find the player's self.x_vel based on currently held keys."""
        self.x_vel = 0
        if keys[pg.K_LEFT] or keys[pg.K_a]:
            self.x_vel -= self.speed
        if keys[pg.K_RIGHT] or keys[pg.K_d]:
            self.x_vel += self.speed

    def get_position(self, obstacles):
        """Calculate the player's position this frame, including collisions."""
        if not self.fall:
            self.check_falling(obstacles)
        else:
            self.fall = self.check_collisions((0,self.y_vel), 1, obstacles)
        if self.x_vel:
            self.check_collisions((self.x_vel,0), 0, obstacles)

    def check_falling(self, obstacles):
        """If player is not contacting the ground, enter fall state."""
        if not self.collide_below:
            self.fall = True
            self.on_moving = False

    def check_moving(self,obstacles):
        """
        Check if the player is standing on a moving platform.
        If the player is in contact with multiple platforms, the prevously
        detected platform will take presidence.
        """
        if not self.fall:
            now_moving = self.on_moving
            any_moving, any_non_moving = [], []
            for collide in self.collide_below:
                if collide.type == "moving":
                    self.on_moving = collide
                    any_moving.append(collide)
                else:
                    any_non_moving.append(collide)
            if not any_moving:
                self.on_moving = False
            elif any_non_moving or now_moving in any_moving:
                self.on_moving = now_moving

    def check_collisions(self, offset, index, obstacles):
        """
        This function checks if a collision would occur after moving offset
        pixels. If a collision is detected, the position is decremented by one
        pixel and retested. This continues until we find exactly how far we can
        safely move, or we decide we can't move.
        """
        unaltered = True
        self.rect[index] += offset[index]
        self.hand_rect[index] += offset[index]
        while pg.sprite.spritecollideany(self, obstacles):
            self.rect[index] += (1 if offset[index]<0 else -1)
            self.hand_rect[index] += (1 if offset[index]<0 else -1)
            unaltered = False
        return unaltered

    def check_above(self, obstacles):
        """When jumping, don't enter fall state if there is no room to jump."""
        self.rect.move_ip(0, -1)
        collide = pg.sprite.spritecollideany(self, obstacles)
        self.rect.move_ip(0, 1)
        return collide

    def check_below(self, obstacles):
        """Check to see if the player is contacting the ground."""
        self.rect.move_ip((0,1))
        collide = pg.sprite.spritecollide(self, obstacles, False)
        self.rect.move_ip((0,-1))
        return collide

    def jump(self, obstacles):
        """Called when the user presses the jump button."""
        if not self.fall and not self.check_above(obstacles):
            self.y_vel = self.jump_power
            self.fall = True
            self.on_moving = False

    def jump_cut(self):
        """Called if player releases the jump key before maximum height."""
        if self.fall:
            if self.y_vel < self.jump_cut_magnitude:
                self.y_vel = self.jump_cut_magnitude

    def get_angle(self, mouse):
        x_off = (mouse[0]-self.rect.centerx)
        y_off = (mouse[1]-self.rect.centery)
        self.angle = math.degrees(math.atan2(-y_off, x_off) % (2*math.pi))
        self.hand = pg.transform.rotate(self.original_hand, self.angle)
        self.hand_rect = self.hand.get_rect(center=self.hand_rect.center)

        """
        offset = (mouse[1]-self.hand_rect.centery, mouse[0]-self.hand_rect.centerx)
        self.angle = math.atan2(-offset[0], offset[1]) % (2 * math.pi)
        self.angle = math.degrees(self.angle)
        self.hand = pg.transform.rotate(self.original_hand, self.angle)
        self.hand_rect = self.hand.get_rect(center=self.rect.center)


        self.angle = 135-math.degrees(math.atan2(*offset))
        self.hand = pg.transform.rotate(self.original_hand, self.angle)
        self.hand_rect = self.hand.get_rect(topleft=self.rect.topleft)
        """

    def pre_update(self, obstacles):
        """Ran before platforms are updated."""
        self.collide_below = self.check_below(obstacles)
        self.check_moving(obstacles)

    def update(self, obstacles, keys):
        """Everything we need to stay updated; ran after platforms update."""
        self.check_keys(keys)
        self.get_position(obstacles)
        self.physics_update()

    def get_event(self, event, bolts):
        if event.type == pg.MOUSEBUTTONDOWN and event.button == 1:
            bolts.add(Bolt(self.rect.center))
        elif event.type == pg.MOUSEMOTION:
            self.get_angle(event.pos)

    def draw(self, surface):
        """Blit the player to the target surface."""
        surface.blit(self.image, self.rect)
        surface.blit(self.hand, self.hand_rect)

class Bolt(pg.sprite.Sprite):
    def __init__(self, location):
        pg.sprite.Sprite.__init__(self)
        """self.original_bolt = pg.image.load('bolt.png')"""
        """self.angle = -math.radians(angle-135)"""
        """self.image = pg.transform.rotate(self.original_bolt, angle)"""
        """self.image = self.original_bolt"""
        self.image=pg.Surface((5,10)).convert()
        self.image.fill(pg.Color("yellow"))
        self.rect = self.image.get_rect(center=location)
        self.move = [self.rect.x, self.rect.y]
        self.speed_magnitude = 5
        """self.speed = (self.speed_magnitude*math.cos(self.angle), self.speed_magnitude*math.sin(self.angle))"""
        """self.speed = (5,0)"""
        self.done = False

    def update(self, screen_rect, obstacles):
        self.move[0] += self.speed_magnitude
        """self.move[1] += self.speed[1]"""
        self.rect.topleft = self.move
        self.remove(screen_rect, obstacles)

    def remove(self, screen_rect, obstacles):
        if not self.rect.colliderect(screen_rect):
            self.kill()


class Block(pg.sprite.Sprite):
    """A class representing solid obstacles."""
    def __init__(self, color, rect):
        """The color is an (r,g,b) tuple; rect is a rect-style argument."""
        pg.sprite.Sprite.__init__(self)
        self.rect = pg.Rect(rect)
        self.image = pg.Surface(self.rect.size).convert()
        self.image.fill(color)
        self.type = "normal"


class MovingBlock(Block):
    """A class to represent horizontally and vertically moving blocks."""
    def __init__(self, color, rect, end, axis, delay=500, speed=2, start=None):
        """
        The moving block will travel in the direction of axis (0 or 1)
        between rect.topleft and end. The delay argument is the amount of time
        (in miliseconds) to pause when reaching an endpoint; speed is the
        platforms speed in pixels/frame; if specified start is the place
        within the blocks path to start (defaulting to rect.topleft).
        """
        Block.__init__(self, color, rect)
        self.start = self.rect[axis]
        if start:
            self.rect[axis] = start
        self.axis = axis
        self.end = end
        self.timer = 0.0
        self.delay = delay
        self.speed = speed
        self.waiting = False
        self.type = "moving"

    def update(self, player, obstacles):
        """Update position. This should be done before moving any actors."""
        obstacles = obstacles.copy()
        obstacles.remove(self)
        now = pg.time.get_ticks()
        if not self.waiting:
            speed = self.speed
            start_passed = self.start >= self.rect[self.axis]+speed
            end_passed = self.end <= self.rect[self.axis]+speed
            if start_passed or end_passed:
                if start_passed:
                    speed = self.start-self.rect[self.axis]
                else:
                    speed = self.end-self.rect[self.axis]
                self.change_direction(now)
            self.rect[self.axis] += speed
            self.move_player(now, player, obstacles, speed)
        elif now-self.timer > self.delay:
            self.waiting = False

    def move_player(self, now, player, obstacles, speed):
        """
        Moves the player both when on top of, or bumped by the platform.
        Collision checks are in place to prevent the block pushing the player
        through a wall.
        """
        if player.on_moving is self or pg.sprite.collide_rect(self,player):
            axis = self.axis
            offset = (speed, speed)
            player.check_collisions(offset, axis, obstacles)
            if pg.sprite.collide_rect(self, player):
                if self.speed > 0:
                    self.rect[axis] = player.rect[axis]-self.rect.size[axis]
                else:
                    self.rect[axis] = player.rect[axis]+player.rect.size[axis]
                self.change_direction(now)

    def change_direction(self, now):
        """Called when the platform reaches an endpoint or has no more room."""
        self.waiting = True
        self.timer = now
        self.speed *= -1

"""class Spell(pg.sprite.Sprite):
    def __init__(self, location, angle)"""

class Control(object):
    """Class for managing event loop and game states."""
    def __init__(self):
        """Initalize the display and prepare game objects."""
        self.screen = pg.display.get_surface()
        self.screen_rect = self.screen.get_rect()
        self.clock = pg.time.Clock()
        self.fps = 60.0
        self.keys = pg.key.get_pressed()
        self.done = False
        self.player = Player((50,875), 4)
        self.viewport = self.screen.get_rect()
        self.level = pg.Surface((1000,1000)).convert()
        self.level_rect = self.level.get_rect()
        self.win_text,self.win_rect = self.make_text()
        self.obstacles = self.make_obstacles()
        self.bolts = pg.sprite.Group()

    def make_text(self):
        """Renders a text object. Text is only rendered once."""
        font = pg.font.Font(None, 100)
        message = "You win. Celebrate."
        text = font.render(message, True, (100,100,175))
        rect = text.get_rect(centerx=self.level_rect.centerx, y=100)
        return text, rect

    def make_obstacles(self):
        """Adds some arbitrarily placed obstacles to a sprite.Group."""
        walls = [Block(pg.Color("chocolate"), (0,980,1000,20)),
                 Block(pg.Color("chocolate"), (0,0,20,1000)),
                 Block(pg.Color("chocolate"), (980,0,20,1000))]
        static = [Block(pg.Color("darkgreen"), (250,780,200,100)),
                  Block(pg.Color("darkgreen"), (600,880,200,100)),
                  Block(pg.Color("darkgreen"), (20,360,880,40)),
                  Block(pg.Color("darkgreen"), (950,400,30,20)),
                  Block(pg.Color("darkgreen"), (20,630,50,20)),
                  Block(pg.Color("darkgreen"), (80,530,50,20)),
                  Block(pg.Color("darkgreen"), (130,470,200,215)),
                  Block(pg.Color("darkgreen"), (20,760,30,20)),
                  Block(pg.Color("darkgreen"), (400,740,30,40))]
        moving = [MovingBlock(pg.Color("olivedrab"), (20,740,75,20), 325, 0),
                  MovingBlock(pg.Color("olivedrab"), (600,500,100,20), 880, 0),
                  MovingBlock(pg.Color("olivedrab"),
                              (420,430,100,20), 550, 1, speed=3, delay=200),
                  MovingBlock(pg.Color("olivedrab"),
                              (450,700,50,20), 930, 1, start=930),
                  MovingBlock(pg.Color("olivedrab"),
                              (500,700,50,20), 730, 0, start=730),
                  MovingBlock(pg.Color("olivedrab"),
                              (780,700,50,20), 895, 0, speed=-1)]
        return pg.sprite.Group(walls, static, moving)

    def update_viewport(self):
        """
        The viewport will stay centered on the player unless the player
        approaches the edge of the map.
        """
        self.viewport.center = self.player.rect.center
        self.viewport.clamp_ip(self.level_rect)

    def event_loop(self):
        """We can always quit, and the player can sometimes jump."""
        for event in pg.event.get():
            if event.type == pg.QUIT or self.keys[pg.K_ESCAPE]:
                self.done = True
            elif event.type == pg.KEYDOWN:
                if event.key == pg.K_SPACE:
                    self.player.jump(self.obstacles)
            elif event.type == pg.KEYUP:
                if event.key == pg.K_SPACE:
                    self.player.jump_cut()
            elif event.type == pg.MOUSEMOTION or event.type == pg.MOUSEBUTTONDOWN:
                self.player.get_event(event, self.bolts)

    def update(self):
        """Update the player, obstacles, and current viewport."""
        self.keys = pg.key.get_pressed()
        self.player.pre_update(self.obstacles)
        self.obstacles.update(self.player, self.obstacles)
        self.player.update(self.obstacles, self.keys)
        self.update_viewport()
        self.bolts.update(self.screen_rect, self.obstacles)

    def draw(self):
        """
        Draw all necessary objects to the level surface, and then draw
        the viewport section of the level to the display surface.
        """
        self.level.fill(pg.Color("lightblue"))
        self.obstacles.draw(self.level)
        self.level.blit(self.win_text, self.win_rect)
        self.player.draw(self.level)
        self.bolts.draw(self.level)
        self.screen.blit(self.level, (0,0), self.viewport)

    def display_fps(self):
        """Show the programs FPS in the window handle."""
        caption = "{} - FPS: {:.2f}".format(CAPTION, self.clock.get_fps())
        pg.display.set_caption(caption)

    def main_loop(self):
        """As simple as it gets."""
        while not self.done:
            self.event_loop()
            self.update()
            self.draw()
            pg.display.update()
            self.clock.tick(self.fps)
            self.display_fps()


if __name__ == "__main__":
    os.environ['SDL_VIDEO_CENTERED'] = '1'
    pg.init()
    pg.display.set_caption(CAPTION)
    pg.display.set_mode(SCREEN_SIZE)
    PLAYERIMG = pg.image.load("playertst.png").convert()
    PLAYERIMG.set_colorkey(COLOR_KEY)
    run_it = Control()
    run_it.main_loop()
    pg.quit()
    sys.exit()

Upvotes: 0

Views: 563

Answers (1)

fbence
fbence

Reputation: 2153

The % 2*pi unnecessary, and your get_angle function has no return value, but you do an assignment to self.angle = self.get_angle, but that is not the issue. The issue is that the mouse position is relative to the screen (i.e. clicking in the top right area of your game screen will always yield (0,480) if your screen is 640x480), while the position of the (character) rectangle is given in your game play area, which is larger than the screen, ergo if you move the character and thus the view shifts, you are getting coordinates in two different coordinate systems. You will have to keep track of where the view is in your game play area and add the offset to the mouse coordinates.

Upvotes: 1

Related Questions