[Pygame] collision detection

[Pygame] collision detection

Postby metulburr » Sat Apr 13, 2013 9:27 pm

So basically the 3 main ways i guess of doing this is: rects, sprite, or per pixel check? IS that correct?

The method i used was rects. Have a rect per image and have the image follow the rect, and use the rect for collision detection. So i have been peicing together example codes to try to come up with a character that can go left, right, jumpvertical, jump horiztal based on right/left movement, and collision detection with one platform. I got the the movement down good, but then when i started the collision dection things started tweaking out, and i started getting confused on things.

So the problems:
The rect freaks out and jumps up to the top of the screen when i jump on the platform, but yet the image does not follow the rect. While also doing this it pauses for a short moment.
I cant quite seem to figure out gravity. During a jump is fine, but after jumping on the platform, the character continues walking in air as if the platform is there, when its not.

There are short examples online of the ones i found, but i havent really found them helpful. I havent gotten any closer to figuring out what i need to proceed. One example will use pygame.sprite.Sprite while another will just used rects, but both do so without explanation of why they chose that for each. Some people say just learn the sprite method and it will help you in the long run, while others say ignore that and use rects for collision detection and forget about the sprite.Sprite. Very confusing.

Mainly my problem is the fact that normally pygame examples are written so badly. They use globals a lot. They flood the namespace. Which in turns makes me think why am i even using that example if they do those in the first place. The irc for pygame doesnt respond much, the pygame site has apparently been abandoned and blocked from accepting more user registrations. Whereas i could ask a question about lists on here and get an answer in 5 minutes from a few of you, its harder to get answers from others about pygame. Even the pygame tut books piss me off on how they program python

So this is the mashed up code i ended up with. I think i have to jsut redo the thing because its so bad though:
Code: Select all
import pygame

class Player:
   def __init__(self):
      self.platformY = 0      # pixels
      self.jumping_duration = 750     # milliseconds
      self.horz_move_speed = 4    # pixels
      self.time_at_peak = self.jumping_duration / 2
      self.jump_height = 200         # pixels 
      
      self.player_image = pygame.image.load('/home/metulburr/Downloads/man.jpeg').convert_alpha()
      
      
      self.x = screen.get_width() / 2
      self.y = self.floorY()
      #self.player_rect = pygame.Rect((self.manX,self.manY), self.player_image.get_size())
      self.player_rect = self.player_image.get_rect()
      self.jumping = False
      self.jumpingHorz = 0
      self.jumpingStart = 0
      
      
   def floorY(self):
      '''for character jumping:  The Y coordinate of the floor, where the man is placed '''
      return screen.get_height() - self.player_image.get_height() - self.platformY
   def jumpHeightAtTime(self, elapsed_time):
      '''for character jumping:  The height of the jump at the given elapsed time (milliseconds) '''
      return ((-1.0 / self.time_at_peak ** 2) * ((elapsed_time - self.time_at_peak) ** 2) + 1) * self.jump_height
   #def horzMoveAmt(self, keys):
   #   '''for character jumping:  Amount of horizontal movement based on left/right arrow keys '''
   #   return (keys[pygame.K_RIGHT] - keys[pygame.K_LEFT]) * self.horz_move_speed
      
   def horz_move(self,x,y):
      self.backup_x = self.x
      self.backup_x_rect = self.player_rect[0]
      self.x += x
      
      #self.y = self.floorY() - y
      self.player_rect[0] = self.x
      self.player_rect[1] = self.y
   def check_for_jump(self, keys, jump_key_pressed):
      self.backup_y = self.y
      self.backup_y_rect = self.player_rect[1]
      if not self.jumping:
         
         #self.x += self.horzMoveAmt(keys)
         if jump_key_pressed:
            self.jumping = True
            #self.jumpingHorz = self.horzMoveAmt(keys)
            self.jumpingStart = pygame.time.get_ticks()

      if self.jumping:
         #self.jumping_duration -= .3 #possibly  mimic gravity with acceleration?
         t = pygame.time.get_ticks() - self.jumpingStart
         if t > self.jumping_duration:
            self.jumping = False
            self.jumpHeight = 0
         else:
            self.jumpHeight = self.jumpHeightAtTime(t)

         self.y = self.floorY() - self.jumpHeight
         #self.x += self.jumpingHorz
         
      self.player_rect[0] = self.x
      self.player_rect[1] = self.y
      
   def player_collision(self, world_objects_rects):
      player_collisions = self.player_rect.collidelistall(world_objects_rects)
      if player_collisions:
         print('collision detected')
         self.x = self.backup_x
         self.y = self.backup_y
         self.player_rect[0] = self.backup_x_rect
         self.player_rect[1] = self.backup_y_rect
         return True
      else:
         print('nothing')
         return False
         
   def update(self,keys, jump_key_pressed, world_objects_rects):
      '''player update upon every loop of mainloop'''

      
      self.check_for_jump(keys, jump_key_pressed)
      if not self.player_collision(world_objects_rects):
         #self.y = platform_rect[1]
         if keys[pygame.K_LEFT]:
            self.horz_move(-player.horz_move_speed,0)
         if keys[pygame.K_RIGHT]:
            self.horz_move(player.horz_move_speed,0)
      '''
      else:
         if keys[pygame.K_LEFT]:
            self.horz_move(player.horz_move_speed,0)
         if keys[pygame.K_RIGHT]:
            self.horz_move(-player.horz_move_speed,0)
      '''
      self.backup_y = self.y
      self.backup_x = self.x


pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((1000, 500))

player = Player()

world_objects_rects = []
platform_rect = pygame.Rect(250,450,150,50)
world_objects_rects.append(platform_rect)

loop = True
while loop:
   
   #eventloop resets
   player.player_rect = player.player_image.get_rect()
   jump_key_pressed = False
   keys = pygame.key.get_pressed()
   mouseY, mouseX = pygame.mouse.get_pos()
   
   
   for event in pygame.event.get():
      if event.type == pygame.QUIT:
         loop = False
      elif event.type == pygame.KEYDOWN:
         if event.key == pygame.K_SPACE:
            jump_key_pressed = True #stop auto jumping while holding down jump key

      
   player.update(keys, jump_key_pressed, world_objects_rects)
            




   screen.fill((255,255,255))
   screen.blit(player.player_image, (player.x, player.y))
   pygame.draw.rect(screen, (0,0,155), platform_rect, 1)
   pygame.draw.rect(screen, (100,0,155), player.player_rect, 1)
   pygame.display.flip()
   clock.tick(60)

only dependant is the one image
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby Mekire » Sun Apr 14, 2013 3:57 am

Take a look at these two examples (I wrote these for another forum originally but I think they are instructive).

Here I use rects:
Code: Select all
import os,sys
from random import randint
import pygame as pg

class _Physics:
    def __init__(self):
        self.x_vel = self.y_vel = self.y_vel_i = 0
        self.grav = 20
        self.fall = False
        self.time = None
    def phys_update(self):
        if self.fall:
            if not self.time:
                self.time = pg.time.get_ticks()
            self.y_vel = self.grav*((pg.time.get_ticks()-self.time)/1000.0) + self.y_vel_i
        else:
            self.time = None
            self.y_vel = 0

class Player(_Physics):
    """Class representing our player."""
    def __init__(self, location,speed):
        _Physics.__init__(self)
        self.image = PLAY_IMG
        self.speed = speed
        self.jump_power = 10
        self.rect = self.image.get_rect()
        self.rect.topleft = location

        self.collide_ls = [] #what obstacles does the player collide with

    def get_pos(self,Obstacles):
        """Calculate where our player will end up this frame including collissions."""
        #Has the player walked off an edge?
        if not self.fall and not self.collide_with(Obstacles,[0,1]):
            self.fall = True
        #Has the player landed from a fall or jumped into an object above them?
        elif self.fall and self.collide_with(Obstacles,[0,int(self.y_vel)]):
            self.y_vel = self.adjust_pos(self.collide_ls,[0,int(self.y_vel)],1)
            self.y_vel_i = 0
            self.fall = False
        self.rect.y += int(self.y_vel) #Update y position before testing x.
        #Is the player running into a wall?.
        if self.collide_with(Obstacles,(int(self.x_vel),0)):
            self.x_vel = self.adjust_pos(self.collide_ls,[int(self.x_vel),0],0)
        self.rect.x += int(self.x_vel)
    def adjust_pos(self,Obstacles,offset,off_ind):
        offset[off_ind] += (1 if offset[off_ind]<0 else -1)
        while 1:
            if any(self.collide_with(self.collide_ls,offset)):
                offset[off_ind] += (1 if offset[off_ind]<0 else -1)
            else:
                return offset[off_ind]

    def collide_with(self,Obstacles,offset):
        test = ((self.rect.x+offset[0],self.rect.y+offset[1]),self.rect.size)
        self.collide_ls = []
        for Obs in Obstacles:
            if pg.Rect(test).colliderect(Obs.rect):
                self.collide_ls.append(Obs)
        return self.collide_ls

    def update(self,Surf,Obstacles):
        self.get_pos(Obstacles)
        self.phys_update()
        Surf.blit(self.image,self.rect)

class Block:
    """Class representing obstacles."""
    def __init__(self,location):
        self.make_image()
        self.rect = pg.Rect(location,(50,50))
    def make_image(self):
        self.image = pg.Surface((50,50)).convert()
        self.image.fill([randint(0,255) for i in range(3)])
        self.image.blit(SHADE_IMG,(0,0))
    def update(self,Surf):
        Surf.blit(self.image,self.rect)

class Control:
    """Class for managing event loop and game states."""
    def __init__(self):
        self.state = "GAME"
        self.Player = Player((50,-25), 4)
        self.make_obstacles()
        self.Clock = pg.time.Clock()
    def event_loop(self):
        keys = pg.key.get_pressed()
        self.Player.x_vel = 0
        if keys[pg.K_LEFT] or keys[pg.K_a]:
            self.Player.x_vel -= self.Player.speed
        if keys[pg.K_RIGHT] or keys[pg.K_d]:
            self.Player.x_vel += self.Player.speed
        for event in pg.event.get():
            if event.type == pg.QUIT or keys[pg.K_ESCAPE]:
                self.state = "QUIT"
            elif event.type == pg.KEYDOWN: #Key down events.
                if event.key == pg.K_SPACE and not self.Player.fall: #Jump
                    self.Player.y_vel_i = -self.Player.jump_power
                    self.Player.fall = True

    def update(self,Surf):
        Surf.fill((50,50,50))
        [obs.update(Surf) for obs in self.Obstacles]
        self.Player.update(Surf,self.Obstacles)
    def main(self,Surf):
        """Our games infinite loop"""
        while True:
            if self.state == "GAME":
                self.event_loop()
                self.update(Surf)
            elif self.state == "QUIT":
                break
            pg.display.update()
            self.Clock.tick(65)
    def make_obstacles(self):
        self.Obstacles = [Block((400,400)),Block((300,270)),Block((150,170))]
        self.Obstacles += [Block((500+50*i,220)) for i in range(3)]
        for i in range(12):
            self.Obstacles.append(Block((50+i*50,450)))
            self.Obstacles.append(Block((100+i*50,0)))
            self.Obstacles.append(Block((0,50*i)))
            self.Obstacles.append(Block((650,50*i)))

#################################
def image_from_url(url):
    try:
        from urllib2 import urlopen
        from cStringIO import StringIO as inout
    except ImportError:
        from urllib.request import urlopen
        from io import BytesIO as inout
    myurl = urlopen(url)
    return inout(myurl.read()) #Can be loaded by pygame.image.load

if __name__ == "__main__":
    os.environ['SDL_VIDEO_CENTERED'] = '1'
    pg.init()
    SCREENSIZE = (700,500)
    SCREEN = pg.display.set_mode(SCREENSIZE)
    face_url  = "http://i1192.photobucket.com/albums/aa340/Mekire/smallface.png"
    shade_url = "http://i1192.photobucket.com/albums/aa340/Mekire/shader.png"
    PLAY_IMG  = pg.image.load(image_from_url(face_url)).convert_alpha()
    SHADE_IMG = pg.image.load(image_from_url(shade_url)).convert_alpha()
##    PLAY_IMG  = pg.image.load("smallface.png").convert_alpha() ###from computer
##    SHADE_IMG = pg.image.load("shader.png").convert_alpha()    ###from computer
    RunIt = Control()
    RunIt.main(SCREEN)
    pg.quit();sys.exit()

And in this one I use bitmasks (which is my recommendation):
Code: Select all
import os,sys
from random import randint
import pygame as pg

class _Physics:
    def __init__(self):
        self.x_vel = self.y_vel = self.y_vel_i = 0
        self.grav = 20
        self.fall = False
        self.time = None
    def phys_update(self):
        if self.fall:
            if not self.time:
                self.time = pg.time.get_ticks()
            self.y_vel = self.grav*((pg.time.get_ticks()-self.time)/1000.0) + self.y_vel_i
        else:
            self.time = None
            self.y_vel = 0

class Player(_Physics):
    """Class representing our player."""
    def __init__(self, location,speed):
        _Physics.__init__(self)
        self.image = PLAY_IMG
        self.mask  = pg.mask.from_surface(self.image)
        self.speed = speed
        self.jump_power = 10
        self.rect = self.image.get_rect(topleft=location)

    def get_pos(self,Mask):
        """Calculate where our player will end up this frame including collissions."""
        #Has the player walked off an edge?
        if not self.fall and not self.simple_overlap(Mask,[0,1]):
            self.fall = True
        #Has the player landed from a fall or jumped into an object above them?
        elif self.fall and self.simple_overlap(Mask,[0,int(self.y_vel)]):
            self.y_vel = self.adjust_pos(Mask,[0,int(self.y_vel)],1)
            self.y_vel_i = 0
            self.fall = False
        self.rect.y += int(self.y_vel) #Update y position before testing x.
        #Is the player running into a wall?.
        if self.simple_overlap(Mask,(int(self.x_vel),0)):
            self.x_vel = self.adjust_pos(Mask,[int(self.x_vel),0],0)
        self.rect.x += int(self.x_vel)
    def adjust_pos(self,Mask,offset,off_ind):
        while self.simple_overlap(Mask,offset):
            offset[off_ind] += (1 if offset[off_ind]<0 else -1)
        return offset[off_ind]
    def simple_overlap(self,Mask,offset):
        off = (self.rect.x+offset[0],self.rect.y+offset[1])
        return Mask.overlap_area(self.mask,off)

    def update(self,Surf,Mask):
        self.get_pos(Mask)
        self.phys_update()
        Surf.blit(self.image,self.rect)

class Block:
    """Class representing obstacles."""
    def __init__(self,location):
        self.make_image()
        self.rect = pg.Rect(location,(50,50))
    def make_image(self):
        self.image = pg.Surface((50,50)).convert()
        self.image.fill([randint(0,255) for i in range(3)])
        self.image.blit(SHADE_IMG,(0,0))
    def update(self,Surf):
        Surf.blit(self.image,self.rect)

class Control:
    """Class for managing event loop and game states."""
    def __init__(self):
        self.state = "GAME"
        self.Player = Player((50,-25), 4)
        self.make_obstacles()
        self.bg_mask,self.bg_image = self.make_bg_mask()
        self.Clock = pg.time.Clock()
    def event_loop(self):
        keys = pg.key.get_pressed()
        self.Player.x_vel = 0
        if keys[pg.K_LEFT] or keys[pg.K_a]:
            self.Player.x_vel -= self.Player.speed
        if keys[pg.K_RIGHT] or keys[pg.K_d]:
            self.Player.x_vel += self.Player.speed
        for event in pg.event.get():
            if event.type == pg.QUIT or keys[pg.K_ESCAPE]:
                self.state = "QUIT"
            elif event.type == pg.KEYDOWN: #Key down events.
                if event.key == pg.K_SPACE and not self.Player.fall: #Jump
                    self.Player.y_vel_i = -self.Player.jump_power
                    self.Player.fall = True

    def update(self,Surf):
        Surf.fill((50,50,50))
        Surf.blit(self.bg_image,(0,0))
        self.Player.update(Surf,self.bg_mask)
    def main(self,Surf):
        """Our games infinite loop"""
        while True:
            if self.state == "GAME":
                self.event_loop()
                self.update(Surf)
            elif self.state == "QUIT":
                break
            pg.display.update()
            self.Clock.tick(65)
    def make_obstacles(self):
        self.Obstacles = [Block((400,400)),Block((300,270)),Block((150,170))]
        self.Obstacles += [Block((500+50*i,220)) for i in range(3)]
        for i in range(12):
            self.Obstacles.append(Block((50+i*50,450)))
            self.Obstacles.append(Block((100+i*50,0)))
            self.Obstacles.append(Block((0,50*i)))
            self.Obstacles.append(Block((650,50*i)))
    def make_bg_mask(self):
        temp = pg.Surface(SCREENSIZE).convert_alpha()
        temp.fill((0,0,0,0))
        for obs in self.Obstacles:
            obs.update(temp)
        return pg.mask.from_surface(temp),temp

#################################
def image_from_url(url):
    try:
        from urllib2 import urlopen
        from cStringIO import StringIO as inout
    except ImportError:
        from urllib.request import urlopen
        from io import BytesIO as inout
    myurl = urlopen(url)
    return inout(myurl.read()) #Can be loaded by pygame.image.load

if __name__ == "__main__":
    os.environ['SDL_VIDEO_CENTERED'] = '1'
    pg.init()
    SCREENSIZE = (700,500)
    SCREEN = pg.display.set_mode(SCREENSIZE)
    face_url  = "http://i1192.photobucket.com/albums/aa340/Mekire/smallface.png"
    shade_url = "http://i1192.photobucket.com/albums/aa340/Mekire/shader.png"
    PLAY_IMG  = pg.image.load(image_from_url(face_url)).convert_alpha()
    SHADE_IMG = pg.image.load(image_from_url(shade_url)).convert_alpha()
    RunIt = Control()
    RunIt.main(SCREEN)
    pg.quit();sys.exit()


Also in terms of jumping I recommend you write a simple gravity function and find out where the ground is via collision detection. If you tell the player where the ground is without actually looking for it you can walk off ledges and your player won't fall.

Let me know if you need any clarification in the above codes.
-Mek
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: collision detection

Postby metulburr » Sun Apr 14, 2013 5:20 am

ohj wow yeah, the bitmasks one, you definately can tell the difference. Ill take some time to review the code you gave. Thanks Mekire
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby Mekire » Sun Apr 14, 2013 12:10 pm

metulburr wrote:So basically the 3 main ways i guess of doing this is: rects, sprite, or per pixel check? IS that correct?

Regarding this, rectangle collision and bitmask collision (pixel perfect) are the only two methods. Using sprites is not a separate method. Pygame provides a class called Sprite. Usage of this class is completely voluntary. It is extremely light weight; so much so that I never use it. I always write my own sprite classes. Collision detection with sprites still uses the same two methods. Basically all the sprite class does is provide you a way to make groups of sprites that will all update together with a single group update call. Some people swear by using it; but most don't bother.

In more complex code, where the cost of always doing bitmask collision tests gets too high, the solution is to move to a hybrid method. One should use rectangle collision tests to find out if objects are close enough to each other to warrant doing the more expensive bitmask tests. Admittedly my own programs are rarely complicated enough to require this, but it is something worth being aware of.

metulburr wrote:Mainly my problem is the fact that normally pygame examples are written so badly. They use globals a lot. They flood the namespace. Which in turns makes me think why am i even using that example if they do those in the first place. The irc for pygame doesnt respond much, the pygame site has apparently been abandoned and blocked from accepting more user registrations.
I agree there is a ton of bad code around, but that is to be expected. You are at least experienced enough to know that when you see namespace flooding or global variables, they are doing something wrong. Unfortunately this isn't the case with a lot of people.

As to the state of the pygame.org website these days, I agree. It makes me pretty sad. The site regularly gets filled with spam from Indonesian motorcycle tire sellers (who knows why). This is probably why they closed registration, but a lot of good that does the people that honestly want to share their code. Also the highlights and news get updated only about twice a year.

As I said in another thread, in my current attempt to learn pyOpenGL I have been having much the same problems as you. There are tons of people claiming to have sample code, but when you actually look at it, it almost all without exception demonstrates the same ignorance (globals, namespace flooding, and in the case of OpenGL the ubiquitous use of the antiquated fixed-function pipeline).

-Mek
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: collision detection

Postby metulburr » Sun Apr 14, 2013 2:38 pm

actually your code snippet makes sense. Kind of a "duh" moment for me, as i say to myself, dumbass that is how you would do it. lol.

So far the onlything i am confused on is the pygame.mask. Probably because i have never used it.
pygame.mask.from_surface()
Returns a Mask from the given surface.
from_surface(Surface, threshold = 127) -> Mask

Makes the transparent parts of the Surface not set, and the opaque parts set.

The alpha of each pixel is checked to see if it is greater than the given threshold.

If the Surface is color keyed, then threshold is not used.


This also might be my inexperience towards images. Maybe i need to delve into how images are made, and what makes them tick? I mean so inexpeirenced that if an image gives me a problem, i just use a different one until i get one to work. of course i havent made anything elaborate which includes images yet. So i am confused on what exactly in your code is player.mask

A few other image mysteries for me:
From what i seen from others code snippets, you can chop up sprite sheets and categorized them (assuming each sprite sheet has the same stype image in each loccation on each one). In your code, i see the filling the block image with a random color, and the shades of color are based on base pic? To me i would of thought that it would of filled the entire image with solid. Maybe i need to work on color shading simple pygame shapes? ON some others code, the pic has a background, but pygame makes it tranparent. To me that is like magic, lol. I didnt know you could do any of that that. Nor would i know how.

Hopefully one day i can just start from an empty script and just start typing up something like your example, like i do with text based programs.
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby metulburr » Sun Apr 14, 2013 2:54 pm

actually now researching it al little more. Maybe it looks like maybe i should not of skipped the tutorials on bitwise operations? AS it looks like the mask is refering to using these operations on the images? I dont know, lol
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby Mekire » Sun Apr 14, 2013 3:02 pm

In creating the blocks I fill an alpha enabled surface with a solid color; then blit the block image on top (which also uses alpha).

You can have two types of surfaces. Alpha enabled or non-alpha (depending on whether you use convert_alpha() or convert()). If you have a non-alpha enabled surface with a solid color in the background you can choose that color to be transparent with Surface.set_colorkey(). The usual convention when doing this is to use magenta as the transparent color i.e. rgb (255,0,255). Using a non alpha surface with a color key should be faster than using actual alpha (24 bit surface vs 32 bit surface) but for certain things you have no choice. I'm blitting a semi-transparent gradient on my blocks so they have to use alpha. If you look at my 4-direction movement sample here you will see an example where I use color keys and also get the images all from one sheet.

As for masks... they are a bit tricky. They are just arrays that correspond to what pixels are "set". mask.from_surface will make a mask from the given image and if the alpha of the given pixel is higher than the threshold it will set it. In color_keyed surfaces, all non-color key pixels would be set. You can also just make rectangular masks by saying:
Code: Select all
mymask = pygame.Mask((50,50))
mymask.fill()
The actual collision with the masks is the tricky bit. Masks aren't connected to the rect of the sprite they are associated with. They have no clue where they are. The offset in the mask collision functions has to be a function of the rect coordinates of the two objects represented by the masks.

You really won't need to worry about much of those bitwise things yet. Just learn to create masks from surfaces; to create rectangular ones manually; and how to properly check if two masks, corresponding to two separate sprites, collide.

(Edit: just realized I actually only used a regular converted surface for my blocks but used convert_alpha when I loaded the image itself, but mostly what I said still stands. You can use a 24 bit surface and set a color to be transparent, or you can use a 32 bit surface and have "actual" transparency.)
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: collision detection

Postby metulburr » Mon Apr 15, 2013 12:35 pm

ok so i have been messing around with your code snippet. Changing a little here and there and formatting the way i like. Also inserting the images from local dir instead of urllib. I manuelly typed out the whole thing to try to get a better understanding of it and to see each part in of its own to see what exactly its doing and to what. I thought i had gotten it correct. But when i execute the code i get a black screen in pygame and nothing else. Not sure if i typo'd to get an unexpected error, major error or not.

Code: Select all
import os
import sys
import pygame
import random

class WorldPhysics:
   def __init__(self):
      self.x_vel = 0
      self.y_vel = 0
      self.y_vel_inc = 0
      self.gravity = 20
      self.fall = False
      self.time = None
   
   def physics_update(self):
      if self.fall:
         if not self.time:
            self.time = pygame.time.get_ticks()
         self.y_vel = self.gravity * ((pygame.time.get_ticks() - self.time) / 1000.0) + self.y_vel_inc
      else:
         self.time = None
         self.y_vel = 0
         
class Player(WorldPhysics):
   def __init__(self, location, speed):
      WorldPhysics.__init__(self)
      self.image = pygame.image.load('/home/metulburr/Pictures/spoonbombsmall.png').convert_alpha()
      self.mask = pygame.mask.from_surface(self.image)
      self.speed = speed
      self.jump_power = 9
      self.rect = self.image.get_rect(topleft=location)
      
   def get_pos(self, mask):
      if not self.fall and not self.simple_overlap(mask, [0,1]):
         self.fall = True
      elif self.fall and self.simple_overlap(mask, [0, int(self.y_vel)] ):
         self.y_vel = self.adjust_pos(mask, [0, int(self.y_vel)], 1)
         self.y_vel_inc = 0
         self.fall = None
      self.rect.y += int(self.y_vel)
      if self.simple_overlap(mask, (int(self.x_vel), 0)):
         self.x_vel = self.adjust_pos(mask, [int(self.x_vel), 0], 0)
      self.rect.x += int(self.x_vel)
      
   def adjust_pos(self, mask, offset, off_ind):
      while self.simple_overlap(mask, offset):
         if offset[off_ind] < 0:
            offset[off_ind] += 1
         else:
            offset[off_ind] += -1
      return offset[off_ind]
      
   def simple_overlap(self, mask, offset):
      off = (self.rect.x + offset[0], self.rect.y + offset[1])
      return mask.overlap_area(self.mask, off)
      
   def update(self, surf, mask):
      self.get_pos(mask)
      self.physics_update()
      surf.blit(self.image, self.rect)
      
class Block:
   def __init__(self, location):
      self.make_image()
      self.rect = pygame.Rect(location, (50,50))
   def make_image(self):
      self.image = pygame.Surface((50,50)).convert()
      self.image_pic = pygame.image.load('/home/metulburr/Pictures/shader.png').convert_alpha()
      #self.image.fill( [random.randint(0,255) for i in range(3)] )
      self.image.fill((155,155,155))
      self.image.blit(self.image_pic, (0,0))
   def update(self, surf):
      surf.blit(self.image, self.rect)
      
class Control:
   def __init__(self, screensize):
      self.screensize = screensize
      self.state = 'GAME'
      self.player = Player((50, -25), 4)
      self.make_obstacles()
      self.bg_mask, self.bg_image = self.make_bg_mask()
      self.clock = pygame.time.Clock()
   def event_loop(self):
      keys = pygame.key.get_pressed()
      self.player.x_vel = 0
      if keys[pygame.K_LEFT] or keys[pygame.K_a]:
         self.player.x_vel -= self.player.speed
      if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
         self.player.x_vel += self.player.speed
      for event in pygame.event.get():
         if event.type == pygame.QUIT:
            self.state = 'QUIT'
         elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_SPACE and not self.player.fall:
               self.player.y_vel_inc = -self.player.jump_power
               self.player.fall = True
               
   def update(self, surf):
      surf.fill((50,50,50))
      surf.blit(self.bg_image, (0,0))
      self.player.update(surf, self.bg_mask)
      
   def main(self, surf):
      while True:
         if self.state == 'GAME':
            self.event_loop()
            self.update(surf)
         elif self.state == 'QUIT':
            break
         pygame.display.update()
         self.clock.tick(60)
   
   def make_obstacles(self):
      self.Obstacles = [Block((400,400)), Block((300,270)), Block((150, 170))]
      self.Obstacles += [Block((500 + 50 * i, 220)) for i in range(3)]
      for i in range(12):
         self.Obstacles.append(Block((50 + i * 50, 450)))
         self.Obstacles.append(Block((100 + i * 50, 0)))
         self.Obstacles.append(Block((0, 50 * i)))
         self.Obstacles.append(Block((650, 50 * i)))
   
   def make_bg_mask(self):
      temp = pygame.Surface(self.screensize).convert_alpha()
      temp.fill((255,0,0))
      for obs in self.Obstacles:
         obs.update(temp)
      return pygame.mask.from_surface(temp), temp
      
      
if __name__ == '__main__':
   pygame.init()
   screensize = (800,700)
   screen = pygame.display.set_mode(screensize)
   controller = Control(screensize)
   controller.main(screen)
   pygame.quit()
   sys.exit()


EDIT: Ok so the display gets drawn if you change the character's starting point to something rediculous like (1000,1000). Why would it not get drawn unless the characters is way over out in space?

EDIT2:GRRRR this drives me nuts when programs dont work the way expected. The most problems i get are with GUI. Maybe i need a break or something
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby Mekire » Mon Apr 15, 2013 2:49 pm

Quite a few typos; nothing major. One of the bigger problems was your change to the make_bg_mask function. mask.from_surface works by making all non-transparent (or non-color-keyed) areas of a surface set. For some reason you filled the temp surface with red. Everything seems to be working now. I changed all your images to filled blocks for testing purposes but switching back to images shouldn't be hard.

One note on paths though. Best practice is to always use os.path.join when inputting a path. I'm not sure if you were just using full paths for your own testing purposes or not, but it is a good rule of thumb.

Anyway I tried to place ## on any lines where I fixed typos or changed stuff:
Code: Select all
import os
import sys
import pygame
import random

class WorldPhysics:
    def __init__(self):
        self.x_vel = 0
        self.y_vel = 0
        self.y_vel_inc = 0
        self.gravity = 20
        self.fall = False
        self.time = None

    def physics_update(self):
        if self.fall:
            if not self.time:
                self.time = pygame.time.get_ticks()
            self.y_vel = self.gravity * ((pygame.time.get_ticks() - self.time) / 1000.0) + self.y_vel_inc
        else:
            self.time = None
            self.y_vel = 0

class Player(WorldPhysics):
    def __init__(self, location, speed):
        WorldPhysics.__init__(self)
##      self.image = pygame.image.load('/home/metulburr/Pictures/spoonbombsmall.png').convert_alpha()
        self.image = pygame.Surface((50,50)).convert_alpha() ##
        self.image.fill((255,255,0)) ##
        self.mask = pygame.mask.from_surface(self.image)
        self.speed = speed
        self.jump_power = 10 ##
        self.rect = self.image.get_rect(topleft=location)

    def get_pos(self, mask):
        if not self.fall and not self.simple_overlap(mask, [0,1]):
            self.fall = True
        elif self.fall and self.simple_overlap(mask, [0, int(self.y_vel)] ):
            self.y_vel = self.adjust_pos(mask, [0, int(self.y_vel)], 1)
            self.y_vel_inc = 0
            self.fall = None
        self.rect.y += int(self.y_vel)
        if self.simple_overlap(mask, (int(self.x_vel), 0)):
            self.x_vel = self.adjust_pos(mask, [int(self.x_vel), 0], 0)
        self.rect.x += int(self.x_vel)

    def adjust_pos(self, mask, offset, off_ind):
        while self.simple_overlap(mask, offset):
            if offset[off_ind] < 0:
                offset[off_ind] += 1
            else:
                offset[off_ind] += -1
        return offset[off_ind]

    def simple_overlap(self, mask, offset):
        off = (self.rect.x + offset[0], self.rect.y + offset[1])
        return mask.overlap_area(self.mask, off)

    def update(self, surf, mask):
        self.get_pos(mask)
        self.physics_update()
        surf.blit(self.image, self.rect)

class Block:
    def __init__(self, location):
        self.make_image()
        self.rect = pygame.Rect(location, (50,50))
    def make_image(self):
        self.image = pygame.Surface((50,50)).convert()
##      self.image_pic = pygame.image.load('/home/metulburr/Pictures/shader.png').convert_alpha()
        self.image.fill((255,0,0)) ##
##      self.image.blit(self.image_pic, (0,0))
    def update(self, surf):
        surf.blit(self.image, self.rect)

class Control:
    def __init__(self, screensize):
        self.screensize = screensize
        self.state = 'GAME'
        self.player = Player((50, -25), 4)
        self.make_obstacles()
        self.bg_mask, self.bg_image = self.make_bg_mask()
        self.clock = pygame.time.Clock()
    def event_loop(self):
        keys = pygame.key.get_pressed()
        self.player.x_vel = 0
        if keys[pygame.K_LEFT] or keys[pygame.K_a]:
            self.player.x_vel -= self.player.speed
        if keys[pygame.K_RIGHT] or keys[pygame.K_d]:
            self.player.x_vel += self.player.speed ##
        for event in pygame.event.get():
            if event.type == pygame.QUIT or (event.type==pygame.KEYDOWN and event.key==pygame.K_ESCAPE):##
                self.state = 'QUIT'
            elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_SPACE and not self.player.fall:##
                    self.player.y_vel_inc = -self.player.jump_power
                    self.player.fall = True

    def update(self, surf):
        surf.fill(0)
        surf.blit(self.bg_image, (0,0))
        self.player.update(surf,self.bg_mask)

    def main(self, surf):
        while True:
            if self.state == 'GAME':
                self.event_loop()
                self.update(surf)
            elif self.state == 'QUIT':
                break
            pygame.display.update()
            self.clock.tick(60)

    def make_obstacles(self):
        self.Obstacles = [Block((400,400)), Block((300,270)), Block((150, 170))]
        self.Obstacles += [Block((500 + 50 * i, 220)) for i in range(3)]
        for i in range(12):
            self.Obstacles.append(Block((50 + i * 50, 450)))
            self.Obstacles.append(Block((100 + i * 50, 0)))
            self.Obstacles.append(Block((0, 50 * i)))
            self.Obstacles.append(Block((650, 50 * i)))

    def make_bg_mask(self):
        temp = pygame.Surface(self.screensize).convert_alpha()
        temp.fill((0,0,0,0)) ##Must fill surface with transparent to make mask here
        for obs in self.Obstacles:
            obs.update(temp)
        return pygame.mask.from_surface(temp), temp


if __name__ == '__main__':
    pygame.init()
    screensize = (700,500)
    screen = pygame.display.set_mode(screensize)
    controller = Control(screensize)
    controller.main(screen)
    pygame.quit()
    sys.exit()

Oh yeah... I changed all your tabs to four spaces >.> damn non-conformist.

Cheers,
-Mek

Edit: The reason your program broke was because of the aforementioned filling of the entire background mask with red. This made a solid mask the size of your whole screen such that your player always collided. The reason the image displayed when you gave the player a ridiculous offset was, the masks no longer collided when you did this. The behavior seemed arbitrary, but there was logic behind it.
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: collision detection

Postby metulburr » Mon Apr 15, 2013 4:35 pm

ah OK.That makes sense, and somehting that i dont think would of crossed my mind. I treid everything and anything but that, lol.

Yeah i was using full path for testing, since i already know quite well that aspect of it.

Thanks that gets rid of a lot of stress knowing what was wrong with my retype of your example. no im Serious! I was really pissed, lol.



I guess i am not understanding what is being returned on this funciton:
Code: Select all
   def simple_overlap(self, mask, offset):
      off = (self.rect.x + offset[0], self.rect.y + offset[1])
      a = mask.overlap_area(self.mask, off)
      print('mask.overlap_area(self.mask, off) returns: {}'.format(a))
      return a


is it the number of pixels of the mask of the player that is overlapping the mask of the blocks? How does that define the direction of collision?

for example with your filled in square:
that prints out 0 in free falling, 50 and 0 when just standing or moving around on horizontal movements. When moving right for example and collides with block, it prints out 50, 200, 200, 150, 50, 0 and repeats. What are these numbers?
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby Mekire » Mon Apr 15, 2013 10:51 pm

So If the player is just standing, in the function get_pos, I first check if there is ground beneath the player by doing a collision test one pixel below us.
Code: Select all
if not self.fall and not self.simple_overlap(mask, [0,1]):
    self.fall = True
This checks if the player would collide with the background mask if his rectangle was one pixel lower. Since he is standing on the ground this returns True. The players mask is 50x50 pixels and we are checking how many pixels overlap if it was in the ground a depth of 1 pixel, which equals 50. When moving horizontally on the other hand, our speed is 4 pixels per frame. The collision check occurs here:
Code: Select all
#Is the player running into a wall?.
if self.simple_overlap(Mask,(int(self.x_vel),0)):
    self.x_vel = self.adjust_pos(Mask,[int(self.x_vel),0],0)
And note that adjust_pos is a while loop:
Code: Select all
def adjust_pos(self,Mask,offset,off_ind):
    while self.simple_overlap(Mask,offset):
        offset[off_ind] += (1 if offset[off_ind]<0 else -1)
    return offset[off_ind]
First the player tries to move 4 pixels (ie the whole speed) into the wall. 4*50 = 200. The while loop comes back with a collision and rechecks if the player had moved 3 pixels instead of 4. 3*50 = 150. Then 2; 2*50 = 100; then checks 1; 50. Then finally decides the player can't move at all; the loop exits and we continue.

If we didn't use the while loop then sometimes the player wouldn't collide perfectly. I.e if the player is 3 pixels from a wall it would just say you can't get closer. Or worse the pixels from the ground are fewer than the next gravity adjustment and the player stops before the ground.

-Mek
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: collision detection

Postby metulburr » Tue Apr 16, 2013 7:30 am

ah ok. THanks for the clear up
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby metulburr » Tue Apr 16, 2013 8:59 am

I was trying to mimic a small portion of the program using mask. I was expected a variable integer based on its position, but its steady regardless whether the two images overlap each other or not.

Code: Select all
import pygame

pygame.init()

screensize = (400,400)
screen = pygame.display.set_mode(screensize)
fullpath = '/home/metulburr/Pictures/spoonbombsmall.png'
image = pygame.image.load(fullpath)
image2 = pygame.image.load(fullpath)
mask = pygame.mask.from_surface(image)
mask2 = pygame.mask.from_surface(image2)


rect = image.get_rect()
run = True
while run:
   keys = pygame.key.get_pressed()
   mouseY, mouseX = pygame.mouse.get_pos()
   
   
   rect[0] = mouseX
   rect[1] = mouseY
   print(rect)
   print('mask.get_size(): {}'.format(mask.get_size()))
   print('mask.overlap_area(): {}'.format(mask.overlap_area(mask2, (0,0))))



   
   for event in pygame.event.get():
      if event.type == pygame.QUIT:
         run = False

   screen.fill((0,0,0))
   screen.blit(image, (rect[1],rect[0]))
   screen.blit(image2, (0,0))
   pygame.display.flip()


EDIT: oh the second arg is the the coords of the image you are comparing to from the first arg. I think i get it.
Code: Select all
   print('mask.overlap_area(): {}'.format(mask.overlap_area(mask2, (rect[1],rect[0]))))

which now i get the amount of pixels overlaps. Ok ignore the last code then.

EDIT2:
Ok so its effectively the same as comparing rects for collision detection except the mask allows pixel perfect collision detection, is that rihgt? Now io see why you were recommend that over rect collision.

EDIT3:
So the idea is to first check if the rects collide, if so, then check the masks for more in depth collision, otherwise dont bother with the masks. So this is the idea of making hte program speediest as possible? and this is done on every image in the game?

EDIT4:
so this is my mimic of first checking the rects then checking the masks, but now sure what is going as somehtimes the image doesnt get detected overlapping the other.
Code: Select all

import pygame

pygame.init()

screensize = (400,400)
screen = pygame.display.set_mode(screensize)
fullpath = '/home/metulburr/Pictures/spoonbombsmall.png'
image = pygame.image.load(fullpath)
image2 = pygame.image.load(fullpath)
mask = pygame.mask.from_surface(image)
mask2 = pygame.mask.from_surface(image2)


rect = image.get_rect()
rect2 = image2.get_rect()
run = True
while run:
   keys = pygame.key.get_pressed()
   mouseY, mouseX = pygame.mouse.get_pos()
   
   
   rect[0] = mouseX
   rect[1] = mouseY
   #print(rect)
   #print('mask.get_size(): {}'.format(mask.get_size()))
   #print('mask.overlap_area(): {}'.format(''))
   
   
   if rect.colliderect(rect2):
      if mask.overlap_area(mask2, (rect[1],rect[0])):
         print('images overlapping')
      else:
         print('image mask not overlapping other image mask')
   else:
      print('image rect no where near other image rect')



   
   for event in pygame.event.get():
      if event.type == pygame.QUIT:
         run = False

   screen.fill((0,0,0))
   screen.blit(image, (rect[1],rect[0]))
   screen.blit(image2, (0,0))
   pygame.display.flip()
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby Mekire » Tue Apr 16, 2013 9:53 am

The coordinates are exactly how I get the direction. If I want to check if we are falling the coordinates are (0,1); If I am checking horizontal movement the coordinates are (x_velocity,0) as well as an index for the while loop to know we are changing x. If checking vertical movement the coordinates are (0,y_velocity) and an index for the while loop top know we are changing y.

Take note that the offset argument for the actual mask.overlap function (not the simple overlap one I wrote) is the distance that the two masks are from each other. Here the background is always at (0,0) so the entire offset argument is just the location of the player. But when comparing two sprites at arbitrary locations the offsets full form would be more like:
Code: Select all
(self.rect.x-other.rect.x,self.rect.y-other.rect.y)


-Mek
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: collision detection

Postby Mekire » Tue Apr 16, 2013 10:15 am

Give this a shot (any key to switch which rectangle you are dragging):
Code: Select all
import pygame,sys

pygame.init()
screensize = (400,400)
screen = pygame.display.set_mode(screensize)

image = pygame.Surface((50,50)).convert_alpha()
image.fill((255,0,0))
rect = image.get_rect()
mask = pygame.mask.from_surface(image)

image2 = image.copy()
image2.fill((0,255,0))
rect2 = image2.get_rect(center = (250,250))
mask2 = pygame.mask.from_surface(image2)

red = True
run = True
while run:
   keys = pygame.key.get_pressed()

   if red:
      rect.center = pygame.mouse.get_pos()
   else:
      rect2.center = pygame.mouse.get_pos()

   if rect.colliderect(rect2):
      if mask.overlap_area(mask2,(rect2.x-rect.x,rect2.y-rect.y)):
         print('Images overlapping')
      else:
         print('Image mask not overlapping other image mask')
         raise Exception,"Somethings not right here."
   else:
      print('Image rect no where near other image rect')

   for event in pygame.event.get():
      if event.type == pygame.QUIT:
         run = False
      elif event.type == pygame.KEYDOWN:
         red = not red

   screen.fill(0)
   screen.blit(image,rect)
   screen.blit(image2,rect2)
   pygame.display.flip()

pygame.quit();sys.exit()


-Mek

Edit: Sigh... had a typo in exactly the line I was trying to demonstrate. Fixed now.
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan

Re: collision detection

Postby metulburr » Tue Apr 16, 2013 6:11 pm

Code: Select all
         raise Exception,"Somethings not right here."

are you implying that this block will never be executed? Becaue i thought it would be executed upon the the 2 rects colliding, but not the the actual pixels of the image. Of course that is meaning to the fact the image is all over the place and not a square.
New Users, Read This
version Python 3.3.2 and 2.7.5, tkinter 8.5, pyqt 4.8.4, pygame 1.9.2 pre
OS Ubuntu 14.04, Arch Linux, Gentoo, Windows 7/8
https://github.com/metulburr
User avatar
metulburr
 
Posts: 1130
Joined: Thu Feb 07, 2013 4:47 pm
Location: Elmira, NY

Re: collision detection

Postby Mekire » Tue Apr 16, 2013 11:37 pm

Sorry, you are absolutely correct for the general case. In this case my images were just blocks that filled the whole rectangle, so if the rectangles collided, the masks better have collided too. I just wanted the program to stop if this occurred rather than swallow the message up in other print statements.

-Mek
User avatar
Mekire
 
Posts: 830
Joined: Thu Feb 07, 2013 11:33 pm
Location: Amakusa, Japan


Return to Game Development

Who is online

Users browsing this forum: No registered users and 2 guests