Pygame - Return collision location - python

How can I get the exact location of a collision between two sprites?
And if there are multiple pixels overlapping, maybe getting a list of the pixels location.
I want to spawn particles at that point and would need it for other functions too.
Pseudo code:
obj1.rect..x += obj1.velocity
collision = collide(obj1, obj2):
if collision:
?
return collision_location
My code
def _bullet_collision(self):
for bullet in self.bullets:
hits = pygame.sprite.spritecollide(bullet, self.aliens, False, pygame.sprite.collide_mask)
if hits:
hit_str = 11
for alien in hits:
x, y = alien.x, alien.y
spawn_pos = (x, y)
self._hit_enemy(alien, x, y, hit_str)
bullet.bullet_hit(spawn_pos, hit_str)

Based on the answer: https://stackoverflow.com/a/57023183/1730895
It's fairly easy to calculate the intersection of the rectangles:
def rectIntersection( rect1, rect2 ):
""" Given two pygame.Rect intersecting rectangles, return the overlap rect """
left = max( rect1.left, rect2.left )
width = min( rect1.right, rect2.right ) - left
top = max( rect1.top, rect2.top )
height= min( rect1.bottom, rect2.bottom ) - top
return pygame.Rect( left, top, width, height )
So use pygame.rect.colliderect() (doco) first to determine if there's actually any overlap. Obviously this gives you an overlapping region, so to answer your question, it could be every pixel within this rectangular area. But perhaps you could just take the centroid for use as a "happening at" point.
import pygame
# Window size
WINDOW_WIDTH = 400
WINDOW_HEIGHT = 400
WINDOW_SURFACE = pygame.HWSURFACE|pygame.DOUBLEBUF|pygame.RESIZABLE
WINDOW_MAXFPS = 60
# Colours
DARK_BLUE = ( 3, 5, 54)
RED = (255, 0, 0)
YELLOW = (200, 200, 20)
GREEN = ( 20, 200, 20)
def rectIntersection( rect1, rect2 ):
""" Given two pygame.Rect intersecting rectangles, calculate the rectangle of intersection """
left = max( rect1.left, rect2.left )
width = min( rect1.right, rect2.right ) - left
top = max( rect1.top, rect2.top )
height= min( rect1.bottom, rect2.bottom ) - top
return pygame.Rect( left, top, width, height )
### Initialisation
pygame.init()
pygame.mixer.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), WINDOW_SURFACE )
pygame.display.set_caption("Rect-Rect Intersection")
central_rect = pygame.Rect( 175, 175, 50, 50 )
player_rect = pygame.Rect( 0, 0, 30, 30 )
### Main Loop
clock = pygame.time.Clock()
done = False
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.MOUSEMOTION ): # move the player's rectangle with the mouse
mouse_pos = pygame.mouse.get_pos()
player_rect.center = mouse_pos
# Update the window
window.fill( DARK_BLUE )
pygame.draw.rect( window, RED, central_rect, 1 ) # centre rect
pygame.draw.rect( window, YELLOW, player_rect, 1 ) # player's mouse rect
# IFF the rectangles overlap, paint the overlap
if ( player_rect.colliderect( central_rect ) ):
overlap_rect = rectIntersection( player_rect, central_rect )
pygame.draw.rect( window, GREEN, overlap_rect )
pygame.display.flip()
# Clamp FPS
clock.tick_busy_loop( WINDOW_MAXFPS )
pygame.quit()
If pixel-perfect collision is desired, use Surface.subsurface() with the overlap rectangle's co-ordinates on the bitmap mask of both sprites. This gives you two bit-masks (maskA and maskB) which you know overlap somehow.
I can't think of a fast way to find the exact pixels of overlap between these two sub-sections. However it's quite easy to iterate through each of the mask-pixels, finding maskA & maskB pairs that are both "on". Obviously you need to convert from screen co-ordinates to sprite co-ordinates during this step, but this is just a subtraction of the sprite's current screen x and y.
What you really want is the "edge" of the bitmap-intersection, but that's another question.

Related

Creating a rect for each tile in Pygame to make collisions

I am trying to create a rect for each tile in Pygame in order to make collisions- so enabling me to walk on blocks when I jump and not being able to run through a block. My map is a 2D list and I render it on the screen using for loops. I need help with collisions and creating a rect for each tile- would I need to create a rect for the 2D list, I'm not sure.
each_block and next_row are passed in the parameter and both have a value of 0,0 in the while loop e.g. so player1.level1(0,0)
def level1(self,each_block,next_row): #ROWS #MOVE DOWN TO THE NEXT ROW
self.game_level = [['0','0','0','0','0','0','0','0','0','0'], #LOOKING AT THIS 2D LIST, THE DIMENSIONS ARE 10X10
['0','0','0','0','0','0','0','0','0','0'],
['0','0','0','0','2','2','0','0','0','0'],
['0','0','0','0','0','0','0','0','0','0'],
['0','0','0','0','0','0','0','0','0','0'],
['2','2','2','2','2','2','2','2','2','2'],
['1','1','1','1','1','1','1','1','1','1'],
['1','1','1','1','1','1','1','1','1','1'],
['1','1','1','1','1','1','1','1','1','1'],
['1','1','1','1','1','1','1','1','1','1']]
next_row = 0
for row in self.game_level: #LOOPS THROUGH EACH ROW
each_block = 0
for each_tile in row: #LOOPS THROUGH EVERY STRING IN THE ROW
if each_tile == '1':
WINDOW.blit(self.dirt_image,(each_block * 70, next_row * 70))
if each_tile == '2':
WINDOW.blit(self.grass_image,(each_block * 70, next_row * 70))
each_block += 1
next_row += 1
self.area1 = pygame.Rect(x,y,scale_width,scale_height) #This is my player rect which is in the constructor.
I was thinking of doing something like this, but I don't know if it is correct or complete. I think this is only for the jumping?
pr = self.area1
for t in tiles:
# assume each tile has a rect associated
# check is the player is within the bounds of tile.
if pr.right > t.rect.left and pr.left < t.rect.right:
# check if the player is above the tile and below the tile is velocity is applied
if pr.bottom < t.rect.top and pr.bottom + player.velocity >= t.rect.top:
player.velocity = 0
pr.bottom = t.rect.top
One way of doing this, is to make a rectangle for each tile as the level1 like function iterates over all the tiles. You know the cell-position of the tile in the map, so multiplying this by the tile dimensions gives the rectangle parameters:
`tile_rect = pygame.Rect( map_x * TILE_SIZE, map_y * TILE_SIZE, TILE_SIZE, TILE_SIZE )`
The problem is that gives you a lot of rectangles. It's better to only make rectangles for the types that can collide. There's no point testing collisions with a tile that's part of the background art.
So define a set of "platform" tile types:
`PLATFORM_TYPES = '21'`
And then check the tile-type code is a member of of that before making the tile:
tile_rect_list = []
if ( each_tile in PLATFORM_TYPES ):
# valid platform-making tile, create a rect
tile_rect_list.append( pygame.Rect( map_x * TILE_SIZE, ... ) )
If the code has a rectangle for the player, a collision calculation is as simple as calling the rectangle collision function pygame.Rect.colliderect(). Which would look something like this:
tile_rect = pygame.Rect( ... )
player_rect = pygame.Rect( player_x, player_y, player_width, player_height )
collides = player_rect.collide_rect( tile_rect )
Or if you have a list of tiles:
for tile_rect in tile_rect_list:
if ( player_rect.collide_rect( tile_rect ) ):
# Player has collided, do something about it here
PyGame Rectangles are a really useful module. You don't need to write code to check all the coordinates and lengths yourself.
One further optimisation is to create rectangles that span the rows of same-type tiles. So a block of 2-typed tiles 22222 creates a single rectangle that's 5 * tile_width long.
One way of doing this, is that when iterating through the rows of tiles, remember the previous tile-type. Then only output a rectangle when the tile-type changes to a new type.
import pygame
# Window size
WINDOW_WIDTH = 320
WINDOW_HEIGHT = 320
BLACK = ( 0, 0, 0)
GREEN = ( 0, 255, 0)
RED = (255, 0, 0)
class Level():
TILE_SIZE = 32 # size of tiles in pixels
PLATFORM_TILES = '12' # tile types the player can walk on
def __init__( self ):
self.game_level = ['0000000000', #DIMENSIONS 10X10
'1000000001',
'0000220000',
'0000000011',
'0000000000',
'2022220220',
'1111111111',
'1111111111',
'1111111111',
'1111111110']
self.tile_rects = [] # holds all the tile rectangles
# Create a 2D list of tile-rects to match the map.
# Rects are horizontal blocks of same-tiles
for row_y, row in enumerate( self.game_level ):
prev_tile_type = '*' # no match
platform_start = -1 # start column of platform
rect_y = row_y * Level.TILE_SIZE
last_column = len( row ) -1
for col_x, tile_type in enumerate( row ):
# Match same tiles to make a longer rect
if ( platform_start != -1 and ( prev_tile_type != tile_type or col_x == last_column ) ): # tile changed
# The tile-type has changed, or it's the row-end, make the rectangle
# Handle the special case of the last cell in the row
if ( col_x == last_column and prev_tile_type == tile_type ):
col_x += 1
width = ( col_x - platform_start ) * Level.TILE_SIZE
rect_x = platform_start * Level.TILE_SIZE
if ( prev_tile_type in Level.PLATFORM_TILES ):
self.tile_rects.append( pygame.Rect( rect_x, rect_y, width, Level.TILE_SIZE ) )
prev_tile_type = '*'
platform_start = -1
elif ( tile_type in Level.PLATFORM_TILES ):
# If it's the last block in the row, make a rectangle, as there's no "next" block
if ( col_x == len( row ) - 1 ):
rect_x = col_x * Level.TILE_SIZE
self.tile_rects.append( pygame.Rect( rect_x, rect_y, Level.TILE_SIZE, Level.TILE_SIZE ) )
elif ( prev_tile_type != tile_type ):
# start a new row-block for the found tile-type
platform_start = col_x
prev_tile_type = tile_type
def getPlatforms( self ):
""" Get the list of platform-collision rects """
return self.tile_rects
###
### MAIN
###
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.HWSURFACE )
pygame.display.set_caption("Tile Rects")
pygame.mixer.init()
level = Level()
# Main loop
clock = pygame.time.Clock()
running = True
while running:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
running = False
window.fill( BLACK )
for platform_rect in level.getPlatforms():
pygame.draw.rect( window, GREEN, platform_rect, 5 )
#print( "RECT: %d,%d w=%d h=%d" % ( platform_rect.x, platform_rect.y, platform_rect.width, platform_rect.height ) )
# Draw a grid for debugging sizes
for y in range( 10 ):
for x in range( 10 ):
pygame.draw.rect( window, RED, [ x*Level.TILE_SIZE, y*Level.TILE_SIZE, Level.TILE_SIZE, Level.TILE_SIZE ], 1 )
pygame.display.flip()
# Clamp FPS
clock.tick(3) # slow update
pygame.quit()

How to adjust content to changing screen-size?

I made my pygame window resizeable by using pygame.RESIZEABLE in pygame.display.set_mode() but now the content (buttons) I put on the screen earlier haven't shifted with the resizing which is expected but I cannot figure out a way to successfully implement this. I don't get any error messages thankfully. The only problem is that I am not getting the output I want. If the start button is at 100, 100 out of a screen of 600,600 I want it to be at 200,200 out of the resized screen which for this example I'm taking 1200,1200 for the sake of simplicity. I tried using percentages of the total screen size but that completely messed up the positioning.
import pygame
#Initializing pygane
pygame.init()
#Setting Screen Size
SCREENSIZE = (1000,700)
SCREEN = pygame.display.set_mode(SCREENSIZE, pygame.RESIZABLE)
class Button():
def __init__(self, x, y, image, scale):
...
def draw(self, surface):
...
#create button instances
start_button = Button(100,100,start_img,0.8)
exit_button = Button(250,250,exit_img,0.8)
setting_button = Button(450,75,setting_img, 0.35)
As suggested, it's pretty easy to do this with percentage size and position.
When the Button is first initialised, there is a given position and size. But there is also a window size and position at startup too. So instead of storing an exact x co-ordinate, instead we also store the relative x co-ordinate as a factor of the window width.
So say the window is 1000 pixels wide. If your button is positioned in the exact middle, at pixel 500 - the relative position is:
initial_x (500) / initial_width (1000) => 0.5
So we're keeping that relative_x of 0.5. Any time we need to re-position the button, the new x will always be relative_x * window_width for any window size. Bigger or smaller, it still works.
So say now the window is stretched to 1500 pixels wide. The new x co-ordinate would be computer as:
relative_x (0.5) * window_width (1500) => 750
Which is obviously still right in the middle. Say the window then shrunk to 300 pixels ~
relative_x (0.5) * window_width (300) => 150
Obviously you need to extend this further for the y, width and height. But that is just more of the exact same thing.
Working example:
Code:
import pygame
WINDOW_WIDTH = 600 # initial size only
WINDOW_HEIGHT= 300
MAX_FPS = 60
WHITE = (255,255,255)
BLACK = ( 0, 0, 0)
RED = (255, 0, 0)
class StretchyButton( pygame.sprite.Sprite ):
""" A Button Sprite that maintains its relative position and size
despite size changes to the underlying window.
(Based on a pyGame Sprite, just to make drawing simpler) """
def __init__( self, window, x, y, image ):
super().__init__()
self.original_image = image.convert_alpha() # re-scaling the same image gets messed-up
self.image = image
self.rect = self.image.get_rect()
self.rect.topleft = ( x, y )
# Calculate the relative size and position, so we can maintain our
# position & proportions when the window size changes
# So "x_percent" is the relative position of the original x co-ord at the original size
window_rect = window.get_rect()
image_rect = image.get_rect()
# The given (x,y) of the button is relative to the initial window size.
# Keep the ratios of the position against window size
self.x_percent = x / window_rect.width
self.y_percent = y / window_rect.height
self.width_percent = image_rect.width / window_rect.width
self.height_percent = image_rect.height / window_rect.height
def reScale( self, window ):
""" When the window size changes, call this to re-scale the button to match """
window_rect = window.get_rect()
# re-calculate the button's position and size to match the new window size
# we calcualted the percentage (relative) positions at startup, so now
# they can just be used to re-position & re-size
self.rect.x = self.x_percent * window_rect.width
self.rect.width = self.width_percent * window_rect.width
self.rect.y = self.y_percent * window_rect.height
self.rect.height = self.height_percent * window_rect.height
# re-scale the button's image too
# always re-scale off an original image, otherwise the image degrades
self.image = pygame.transform.smoothscale( self.original_image, ( self.rect.width, self.rect.height ) )
### I don't like the look of text without some surrounding space
def renderWithBorder( font, text, border=3, foreground=WHITE, background=BLACK ):
""" Render the given text with the given font, but surrounded by a border of pixels """
text = font.render( text, True, foreground, background )
width,height = text.get_rect().size
bigger_surf = pygame.Surface( ( width + border + border, height + border + border ), pygame.SRCALPHA )
bigger_surf.fill( background )
bigger_surf.blit( text, ( border, border ) )
return bigger_surf
###
### MAIN
###
pygame.init()
window = pygame.display.set_mode( ( WINDOW_WIDTH, WINDOW_HEIGHT ), pygame.RESIZABLE )
pygame.display.set_caption( "Stretchy Buttons" )
font = pygame.font.Font( None, 30 )
# Just a re-square image
red_box = pygame.Surface( ( 64, 64 ), pygame.SRCALPHA )
red_box.fill( RED )
# create a set of buttons
butt0 = StretchyButton( window, 10, 10, red_box )
butt1 = StretchyButton( window, 200, 100, renderWithBorder( font, "The Owl" ) )
butt2 = StretchyButton( window, 200, 150, renderWithBorder( font, "And The Pussycat" ) )
butt3 = StretchyButton( window, 200, 200, renderWithBorder( font, "Went to Sea" ) )
# use a sprite group to hold the buttons, because it's quicker
buttons = pygame.sprite.Group()
buttons.add( [ butt0, butt1, butt2, butt3 ] )
running = True
while running:
clock = pygame.time.Clock() # used to govern the max MAX_FPS
# Handle events
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
running = False
elif ( event.type == pygame.VIDEORESIZE ):
# The window size has changed, re-scale our buttons
#new_width, new_height = event.size
for butt in buttons:
butt.reScale( window )
# paint the window
window.fill( WHITE )
buttons.draw( window )
pygame.display.flip()
clock.tick( MAX_FPS ) # keep the frame-rate sensible, don't waste electricity

How do I make images appear/disappear in pygame?

I am creating a battleship-type game. I am using .blit to display images that I load using the function pygame.image.load. I was wondering, is it possible to make images like this appear/disappear at different points?
My code is as follows:
import random, sys, pygame
from pygame.locals import *
# Set variables, like screen width and height
# globals
FPS = 60 #Determines the number of frames per second
REVEALSPEED = 2 #Determines the speed at which the squares reveals after being clicked
WINDOWWIDTH = 800 #Width of game window
WINDOWHEIGHT = 600 #Height of game window
TILESIZE = 40 #Size of the squares in each grid(tile)
MARKERSIZE = 40 #Size of the box which contatins the number that indicates how many ships in this row/col
BUTTONHEIGHT = 20 #Height of a standard button
BUTTONWIDTH = 40 #Width of a standard button
TEXT_HEIGHT = 25 #Size of the text
TEXT_LEFT_POSN = 10 #Where the text will be positioned
BOARDWIDTH = 6 #Number of grids horizontally
BOARDHEIGHT = 6 #Number of grids vertically
DISPLAYWIDTH = 200 #Width of the game board
EXPLOSIONSPEED = 10 #How fast the explosion graphics will play
XMARGIN = int((WINDOWWIDTH - (BOARDWIDTH * TILESIZE) - DISPLAYWIDTH - MARKERSIZE) / 2) #x-position of the top left corner of board
YMARGIN = int((WINDOWHEIGHT - (BOARDHEIGHT * TILESIZE) - MARKERSIZE) / 2) #y-position of the top left corner of board
#Colours which will be used by the game
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
GREEN = ( 0, 204, 0)
GRAY = ( 60, 60, 60)
BLUE = ( 0, 50, 255)
YELLOW = (255, 255, 0)
DARKGRAY =( 40, 40, 40)
transparent = (0, 0, 0, 0)
#Determine what to colour each element of the game
BGCOLOR = GRAY
BUTTONCOLOR = GREEN
TEXTCOLOR = WHITE
TILECOLOR = GREEN
BORDERCOLOR = BLUE
TEXTSHADOWCOLOR = BLUE
SHIPCOLOR = YELLOW
HIGHLIGHTCOLOR = BLUE
def main():
"""
The main function intializes the variables which will be used by the game.
"""
global DISPLAYSURF, FPSCLOCK, BASICFONT, HELP_SURF, HELP_RECT, NEW_SURF, \
NEW_RECT, SHOTS_SURF, SHOTS_RECT, BIGFONT, COUNTER_SURF, \
COUNTER_RECT, HBUTTON_SURF, EXPLOSION_IMAGES
pygame.init()
FPSCLOCK = pygame.time.Clock()
#Fonts used by the game
DISPLAYSURF = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
BASICFONT = pygame.font.Font('freesansbold.ttf', 20)
BIGFONT = pygame.font.Font('freesansbold.ttf', 50)
# Create and label the buttons
HELP_SURF = BASICFONT.render("HELP", True, WHITE)
HELP_RECT = HELP_SURF.get_rect()
HELP_RECT.topleft = (WINDOWWIDTH - 180, WINDOWHEIGHT - 350)
NEW_SURF = BASICFONT.render("NEW GAME", True, WHITE)
NEW_RECT = NEW_SURF.get_rect()
NEW_RECT.topleft = (WINDOWWIDTH - 200, WINDOWHEIGHT - 200)
# The 'Shots:' label at the top
SHOTS_SURF = BASICFONT.render("Shots: ", True, WHITE)
SHOTS_RECT = SHOTS_SURF.get_rect()
SHOTS_RECT.topleft = (WINDOWWIDTH - 750, WINDOWHEIGHT - 570)
# Load the explosion graphics from the /img folder
EXPLOSION_IMAGES = [
pygame.image.load("blowup1.png"), pygame.image.load("blowup2.png"),
pygame.image.load("blowup3.png"),pygame.image.load("blowup4.png"),
pygame.image.load("blowup5.png"),pygame.image.load("blowup6.png")]
# Set the title in the menu bar to 'Battleship'
pygame.display.set_caption('Battleship')
# Keep the game running at all times
while True:
shots_taken = run_game() #Run the game until it stops and save the result in shots_taken
show_gameover_screen(shots_taken) #Display a gameover screen by passing in shots_taken
def run_game():
greenButton = pygame.image.load('green-button-icon-png-13.png')
greenButton = pygame.transform.scale(greenButton, (75,75))
rect = greenButton.get_rect()
rect = rect.move((150, 475))
redButton = pygame.image.load('red-button-1426817_960_720.png')
redButton = pygame.transform.scale(redButton, (85,85))
rect2 = redButton.get_rect()
rect2 = rect2.move((400, 475))
"""
Function is executed while a game is running.
returns the amount of shots taken
"""
revealed_tiles = generate_default_tiles(False) #Contains the list of the tiles revealed by user
# main board object,
main_board = generate_default_tiles(None) #Contains the list of the ships which exists on board
ship_objs = ['raft'] # List of the ships available
main_board = add_ships_to_board(main_board, ship_objs) #call add_ships_to_board to add the list of ships to the main_board
mousex, mousey = 0, 0 #location of mouse
counter = [] #counter to track number of shots fired
while True:
# counter display (it needs to be here in order to refresh it)
COUNTER_SURF = BASICFONT.render(str(len(counter)), True, WHITE)
COUNTER_RECT = SHOTS_SURF.get_rect()
COUNTER_RECT.topleft = (WINDOWWIDTH - 680, WINDOWHEIGHT - 570)
# Fill background
DISPLAYSURF.fill(BGCOLOR)
# draw the buttons
DISPLAYSURF.blit(HELP_SURF, HELP_RECT)
DISPLAYSURF.blit(NEW_SURF, NEW_RECT)
DISPLAYSURF.blit(SHOTS_SURF, SHOTS_RECT)
DISPLAYSURF.blit(COUNTER_SURF, COUNTER_RECT)
DISPLAYSURF.blit(greenButton, rect)
DISPLAYSURF.blit(redButton, rect2)
greenButton.fill(transparent)
DISPLAYSURF.blit(greenButton, rect)
# Draw the tiles onto the board and their respective markers
draw_board(main_board, revealed_tiles)
mouse_clicked = False
check_for_quit()
#Check for pygame events
for event in pygame.event.get():
if event.type == MOUSEBUTTONUP:
if HELP_RECT.collidepoint(event.pos): #if the help button is clicked on
DISPLAYSURF.fill(BGCOLOR)
show_help_screen() #Show the help screen
elif NEW_RECT.collidepoint(event.pos): #if the new game button is clicked on
main() #goto main, which resets the game
else: #otherwise
mousex, mousey = event.pos #set mouse positions to the new position
mouse_clicked = True #mouse is clicked but not on a button
elif event.type == MOUSEMOTION: #Detected mouse motion
mousex, mousey = event.pos #set mouse positions to the new position
#Check if the mouse is clicked at a position with a ship piece
tilex, tiley = get_tile_at_pixel(mousex, mousey)
if tilex != None and tiley != None:
if not revealed_tiles[tilex][tiley]: #if the tile the mouse is on is not revealed
draw_highlight_tile(tilex, tiley) # draws the hovering highlight over the tile
if not revealed_tiles[tilex][tiley] and mouse_clicked: #if the mouse is clicked on the not revealed tile
reveal_tile_animation(main_board, [(tilex, tiley)])
revealed_tiles[tilex][tiley] = True #set the tile to now be revealed
if check_revealed_tile(main_board, [(tilex, tiley)]): # if the clicked position contains a ship piece
left, top = left_top_coords_tile(tilex, tiley)
blowup_animation((left, top))
if check_for_win(main_board, revealed_tiles): # check for a win
counter.append((tilex, tiley))
return len(counter) # return the amount of shots taken
counter.append((tilex, tiley))
pygame.display.update()
FPSCLOCK.tick(FPS)
def generate_default_tiles(default_value):
"""
Function generates a list of 10 x 10 tiles. The list will contain tuples
('shipName', boolShot) set to their (default_value).
default_value -> boolean which tells what the value to set to
returns the list of tuples
"""
default_tiles = [[default_value]*BOARDHEIGHT for i in range(BOARDWIDTH)]
return default_tiles
def blowup_animation(coord):
"""
Function creates the explosition played if a ship is shot.
coord -> tuple of tile coords to apply the blowup animation
"""
for image in EXPLOSION_IMAGES: # go through the list of images in the list of pictures and play them in sequence
#Determine the location and size to display the image
image = pygame.transform.scale(image, (TILESIZE+10, TILESIZE+10))
DISPLAYSURF.blit(image, coord)
pygame.display.flip()
FPSCLOCK.tick(EXPLOSIONSPEED) #Determine the delay to play the image with
def check_revealed_tile(board, tile):
"""
Function checks if a tile location contains a ship piece.
board -> the tiled board either a ship piece or none
tile -> location of tile
returns True if ship piece exists at tile location
"""
return board[tile[0][0]][tile[0][1]] != None
def reveal_tile_animation(board, tile_to_reveal):
"""
Function creates an animation which plays when the mouse is clicked on a tile, and whatever is
behind the tile needs to be revealed.
board -> list of board tile tuples ('shipName', boolShot)
tile_to_reveal -> tuple of tile coords to apply the reveal animation to
"""
for coverage in range(TILESIZE, (-REVEALSPEED) - 1, -REVEALSPEED): #Plays animation based on reveal speed
draw_tile_covers(board, tile_to_reveal, coverage)
def draw_tile_covers(board, tile, coverage):
"""
Function draws the tiles according to a set of variables.
board -> list; of board tiles
tile -> tuple; of tile coords to reveal
coverage -> int; amount of the tile that is covered
"""
left, top = left_top_coords_tile(tile[0][0], tile[0][1])
if check_revealed_tile(board, tile):
pygame.draw.rect(DISPLAYSURF, SHIPCOLOR, (left, top, TILESIZE,
TILESIZE))
else:
pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top, TILESIZE,
TILESIZE))
if coverage > 0:
pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left, top, coverage,
TILESIZE))
pygame.display.update()
FPSCLOCK.tick(FPS)
def check_for_quit():
"""
Function checks if the user has attempted to quit the game.
"""
for event in pygame.event.get(QUIT):
pygame.quit()
sys.exit()
def check_for_win(board, revealed):
"""
Function checks if the current board state is a winning state.
board -> the board which contains the ship pieces
revealed -> list of revealed tiles
returns True if all the ships are revealed
"""
for tilex in range(BOARDWIDTH):
for tiley in range(BOARDHEIGHT):
if board[tilex][tiley] != None and not revealed[tilex][tiley]: # check if every board with a ship is revealed, return false if not
return False
return True
def draw_board(board, revealed):
"""
Function draws the game board.
board -> list of board tiles
revealed -> list of revealed tiles
"""
#draws the grids depending on its state
for tilex in range(BOARDWIDTH):
for tiley in range(BOARDHEIGHT):
left, top = left_top_coords_tile(tilex, tiley)
if not revealed[tilex][tiley]:
pygame.draw.rect(DISPLAYSURF, TILECOLOR, (left, top, TILESIZE,
TILESIZE))
else:
if board[tilex][tiley] != None:
pygame.draw.rect(DISPLAYSURF, SHIPCOLOR, (left, top,
TILESIZE, TILESIZE))
else:
pygame.draw.rect(DISPLAYSURF, BGCOLOR, (left, top,
TILESIZE, TILESIZE))
#draws the horizontal lines
for x in range(0, (BOARDWIDTH + 1) * TILESIZE, TILESIZE):
pygame.draw.line(DISPLAYSURF, DARKGRAY, (x + XMARGIN + MARKERSIZE,
YMARGIN + MARKERSIZE), (x + XMARGIN + MARKERSIZE,
WINDOWHEIGHT - YMARGIN))
#draws the vertical lines
for y in range(0, (BOARDHEIGHT + 1) * TILESIZE, TILESIZE):
pygame.draw.line(DISPLAYSURF, DARKGRAY, (XMARGIN + MARKERSIZE, y +
YMARGIN + MARKERSIZE), (WINDOWWIDTH - (DISPLAYWIDTH + MARKERSIZE *
2), y + YMARGIN + MARKERSIZE))
def add_ships_to_board(board, ships):
"""
Function goes through a list of ships and add them randomly into a board.
board -> list of board tiles
ships -> list of ships to place on board
returns list of board tiles with ships placed on certain tiles
"""
new_board = board[:]
ship_length = 0
for ship in ships: #go through each ship declared in the list
#Randomly find a valid position that fits the ship
valid_ship_position = False
while not valid_ship_position:
xStartpos = random.randint(0, (BOARDHEIGHT-1))
yStartpos = random.randint(0, (BOARDHEIGHT-1))
isHorizontal = random.randint(0, 1) #vertical or horizontal positioning
#Type of ship and their respective length
if 'battleship' in ship:
ship_length = 5
elif 'destroyer' in ship:
ship_length = 4
elif 'cruiser'in ship:
ship_length = 3
elif 'submarine' in ship:
ship_length = 2
elif 'raft' in ship:
ship_length = 1
#check if position is valid
valid_ship_position, ship_coords = make_ship_position(new_board,
xStartpos, yStartpos, isHorizontal, ship_length, ship)
#add the ship if it is valid
if valid_ship_position:
for coord in ship_coords:
new_board[coord[0]][coord[1]] = ship
return new_board
def make_ship_position(board, xPos, yPos, isHorizontal, length, ship):
"""
Function makes a ship on a board given a set of variables
board -> list of board tiles
xPos -> x-coordinate of first ship piece
yPos -> y-coordinate of first ship piece
isHorizontal -> True if ship is horizontal
length -> length of ship
returns tuple: True if ship position is valid and list ship coordinates
"""
ship_coordinates = [] #the coordinates the ship will occupy
if isHorizontal:
for i in range(length):
if (i+xPos > (BOARDHEIGHT-1)) or (board[i+xPos][yPos] != None) or \
hasAdjacent(board, i+xPos, yPos, ship): #if the ship goes out of bound, hits another ship, or is adjacent to another ship
return (False, ship_coordinates) #then return false
else:
ship_coordinates.append((i+xPos, yPos))
else:
for i in range(length):
if (i+yPos > (BOARDHEIGHT-1)) or (board[xPos][i+yPos] != None) or \
hasAdjacent(board, xPos, i+yPos, ship): #if the ship goes out of bound, hits another ship, or is adjacent to another ship
return (False, ship_coordinates) #then return false
else:
ship_coordinates.append((xPos, i+yPos))
return (True, ship_coordinates) #ship is successfully added
def hasAdjacent(board, xPos, yPos, ship):
"""
Funtion checks if a ship has adjacent ships
board -> list of board tiles
xPos -> x-coordinate of first ship piece
yPos -> y-coordinate of first ship piece
ship -> the ship being checked for adjacency
returns true if there are adjacent ships and false if there are no adjacent ships
"""
for x in range(xPos-1,xPos+2):
for y in range(yPos-1,yPos+2):
if (x in range (BOARDHEIGHT)) and (y in range (BOARDHEIGHT)) and \
(board[x][y] not in (ship, None)):
return True
return False
def left_top_coords_tile(tilex, tiley):
"""
Function calculates and returns the pixel of the tile in the top left corner
tilex -> int; x position of tile
tiley -> int; y position of tile
returns tuple (int, int) which indicates top-left pixel coordinates of tile
"""
left = tilex * TILESIZE + XMARGIN + MARKERSIZE
top = tiley * TILESIZE + YMARGIN + MARKERSIZE
return (left, top)
def get_tile_at_pixel(x, y):
"""
Function finds the corresponding tile coordinates of pixel at top left, defaults to (None, None) given a coordinate.
x -> int; x position of pixel
y -> int; y position of pixel
returns tuple (tilex, tiley)
"""
for tilex in range(BOARDWIDTH):
for tiley in range(BOARDHEIGHT):
left, top = left_top_coords_tile(tilex, tiley)
tile_rect = pygame.Rect(left, top, TILESIZE, TILESIZE)
if tile_rect.collidepoint(x, y):
return (tilex, tiley)
return (None, None)
def draw_highlight_tile(tilex, tiley):
"""
Function draws the hovering highlight over the tile.
tilex -> int; x position of tile
tiley -> int; y position of tile
"""
left, top = left_top_coords_tile(tilex, tiley)
pygame.draw.rect(DISPLAYSURF, HIGHLIGHTCOLOR,
(left, top, TILESIZE, TILESIZE), 4)
def show_help_screen():
"""
Function display a help screen until any button is pressed.
"""
line1_surf, line1_rect = make_text_objs('Press a key to return to the game',
BASICFONT, TEXTCOLOR)
line1_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT)
DISPLAYSURF.blit(line1_surf, line1_rect)
line2_surf, line2_rect = make_text_objs(
'This is a battleship puzzle game. Your objective is ' \
'to sink all the ships in as few', BASICFONT, TEXTCOLOR)
line2_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 3)
DISPLAYSURF.blit(line2_surf, line2_rect)
line3_surf, line3_rect = make_text_objs('shots as possible. The markers on'\
' the edges of the game board tell you how', BASICFONT, TEXTCOLOR)
line3_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 4)
DISPLAYSURF.blit(line3_surf, line3_rect)
line4_surf, line4_rect = make_text_objs('many ship pieces are in each'\
' column and row. To reset your game click on', BASICFONT, TEXTCOLOR)
line4_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 5)
DISPLAYSURF.blit(line4_surf, line4_rect)
line5_surf, line5_rect = make_text_objs('the "New Game" button.',
BASICFONT, TEXTCOLOR)
line5_rect.topleft = (TEXT_LEFT_POSN, TEXT_HEIGHT * 6)
DISPLAYSURF.blit(line5_surf, line5_rect)
while check_for_keypress() == None: #Check if the user has pressed keys, if so go back to the game
pygame.display.update()
FPSCLOCK.tick()
def check_for_keypress():
"""
Function checks for any key presses by pulling out all KEYDOWN and KEYUP events from queue.
returns any KEYUP events, otherwise return None
"""
for event in pygame.event.get([KEYDOWN, KEYUP, MOUSEBUTTONDOWN, MOUSEBUTTONUP, MOUSEMOTION]):
if event.type in (KEYDOWN, MOUSEBUTTONUP, MOUSEBUTTONDOWN, MOUSEMOTION):
continue
return event.key
return None
def make_text_objs(text, font, color):
"""
Function creates a text.
text -> string; content of text
font -> Font object; face of font
color -> tuple of color (red, green blue); colour of text
returns the surface object, rectangle object
"""
surf = font.render(text, True, color)
return surf, surf.get_rect()
def show_gameover_screen(shots_fired):
"""
Function display a gameover screen when the user has successfully shot at every ship pieces.
shots_fired -> the number of shots taken before game is over
"""
DISPLAYSURF.fill(BGCOLOR)
titleSurf, titleRect = make_text_objs('Congrats! Puzzle solved in:',
BIGFONT, TEXTSHADOWCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2))
DISPLAYSURF.blit(titleSurf, titleRect)
titleSurf, titleRect = make_text_objs('Congrats! Puzzle solved in:',
BIGFONT, TEXTCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2) - 3)
DISPLAYSURF.blit(titleSurf, titleRect)
titleSurf, titleRect = make_text_objs(str(shots_fired) + ' shots',
BIGFONT, TEXTSHADOWCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2 + 50))
DISPLAYSURF.blit(titleSurf, titleRect)
titleSurf, titleRect = make_text_objs(str(shots_fired) + ' shots',
BIGFONT, TEXTCOLOR)
titleRect.center = (int(WINDOWWIDTH / 2) - 3, int(WINDOWHEIGHT / 2 + 50) - 3)
DISPLAYSURF.blit(titleSurf, titleRect)
pressKeySurf, pressKeyRect = make_text_objs(
'Press a key to try to beat that score.', BASICFONT, TEXTCOLOR)
pressKeyRect.center = (int(WINDOWWIDTH / 2), int(WINDOWHEIGHT / 2) + 100)
DISPLAYSURF.blit(pressKeySurf, pressKeyRect)
while check_for_keypress() == None: #Check if the user has pressed keys, if so start a new game
pygame.display.update()
FPSCLOCK.tick()
if __name__ == "__main__": #This calls the game loop
main()
Generally there's two ways of doing this.
The more common way is to re-paint the entire screen on each iteration of the main loop.
For example:
### Main Loop
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.MOUSEBUTTONUP ):
mouse_position = pygame.mouse.get_pos() # Location of mouse-click
player_moves.append ( PlayerMove( mouse_position ) ) # Make a new move
# Re-paint the screen
window.fill( OCEAN_BLUE_COLOUR ) # clear the screen
# Paint each of the players turns
for m in player_moves:
m.draw( window ) # paints a hit or miss icon
pygame.display.flip()
Alternatively, instead of re-painting everything, only change the items that have updated, or when events happen. This is close to the "dirty-rectangles" method of updating.
# Initially paint the screen
window.fill( OCEAN_BLUE_COLOUR ) # clear the screen
### Main Loop
while not done:
# Handle user-input
for event in pygame.event.get():
if ( event.type == pygame.QUIT ):
done = True
elif ( event.type == pygame.MOUSEBUTTONUP ):
mouse_position = pygame.mouse.get_pos() # Location of mouse-click
move = playerMove( mouse_position )
move.draw( window )
pygame.display.flip()
The difficulty of the second method, is that the program needs to clean up after the movement of on-screen images (otherwise they will leave a trail). Obviously in a battleship game, no on-screen elements move - but things like re-drawing scores or starting a new game will need to somehow erase the background. I'm not sure if this will also re-paint the window after it has been occluded by another window.
If you are a beginner programmer, I would use the first method. It's much simpler, and a lot of games are written this way.

Object oriented circular button in pygame?

I want to make a circular button in pygame.
I had made a class for the rectangle button. (below)
But I want to make it as a circle (and also other polygon).
I know there is no get_cir in Surface and it even doesn't work with it.
So how could I make it?
import pygame
class Button():
def __init__(self, text, x, y, width, height, normal_color, hovered_color,
label_color, font_type, font_size, command = None):
self.text = text
self.normal_color = normal_color
self.hovered_color = hovered_color
self.label_color = label_color
self.font_type = font_type
self.font_size = font_size
self.command = command
self.image_normal = pygame.Surface((width, height))
self.image_normal.fill(self.normal_color)
self.image_hovered = pygame.Surface((width, height))
self.image_hovered.fill(self.hovered_color)
self.image = self.image_normal
self.rect = self.image.get_rect()
font = pygame.font.SysFont(self.font_type, self.font_size)
text_image = font.render(text, True, self.label_color)
text_rect = text_image.get_rect(center = self.rect.center)
self.image_normal.blit(text_image, text_rect)
self.image_hovered.blit(text_image, text_rect)
self.rect.topleft = (x, y)
self.hovered = False
def update(self):
if self.hovered:
self.image = self.image_hovered
else:
self.image = self.image_normal
def draw(self, surface):
surface.blit(self.image, self.rect)
def handle_event(self, event):
if event.type == pygame.MOUSEMOTION:
self.hovered = self.rect.collidepoint(event.pos)
elif event.type == pygame.MOUSEBUTTONDOWN:
if self.hovered == True:
self.command()
You can keep the (fast and efficient) rectangular collision detection, and when that indicates a click, then check the event's (x,y) position distance from the centre-point of the circle.
The distance formula gives us a function:
def lineLength( pos1, pos2 ):
"""Return the distance between the points (pos1) and (pos2)"""
x1,y1 = pos1
x2,y2 = pos2
# length = sqrt((x2 - x1)^2 + (y2 - y1)^2)
x_squared = ( x2 - x1 ) * ( x2 - x1 )
y_squared = ( y2 - y1 ) * ( y2 - y1 )
length = math.sqrt( x_squared + y_squared )
return length
So then add an extra check to work out the distance between the centre of your button, and the mouse-click-point. This should be less than the button's radius for the hover/click to be on the button:
def handle_event( self, event ):
if event.type == pygame.MOUSEMOTION:
self.hovered = False
if ( self.rect.collidepoint( event.pos ) ):
# check click is over the circular button ~
radius = self.width / 2
click_distance = lineLength( self.rect.center, event.pos )
if ( click_distance <= radius ):
self.hovered = True
There is no problem to draw circle or use image with circle. The only problem is to check mouse collision with circle object.
There is sprite.collide_circle which uses circle areas to check collision between two sprites so you would need to create sprite with mouse position and check collision with button's sprite.
OR you can use circle definition
x**2 + y**2 =< R
so calculate distance between mouse and circle's center and it has to be equal or smaller then radius.
You can use even pygame.math.Vector2.distance_to() for this
For other figures the only idea is to use sprite.collide_mask which uses black&white bitmaps to check collisions between sprites. But it would need to create bitmap with filled figure and it can make problem if you want to draw polygon instead of use bitmap with polygon.

Pygame - " display.set_mode() " creates a smaller window, than requested ( Surfaces are outside of the window )

I´m trying to draw a ship on the bottom-right of the screen, but it´s not appearing on the window! Coordinates seem to be off on X, Y by approximately 50 points. No matter what kind of resolution is set through pygame.display.set_mode(), the window is always smaller than the defined dimensions ( by 50 ).
External FULL HD screen is connected to the laptop through HDMI, but disconnecting it had no effect. Using Windows 10, Python 3.6.2 and Pygame 1.9.3.
Using "centerx", "bottom" to display the ship
Same as above, but substracting both "centerx" and "bottom" by 50.
import sys
import pygame
def main():
#Initialize the screen.
pygame.init()
screen = pygame.display.set_mode( ( 1024, 768 ) )
screen_rect = screen.get_rect()
bg_color = ( 235, 235, 235 )
# Load the ship surface, get its rect.
ship_image = pygame.image.load( "images/ship.bmp" )
ship_rect = ship_image.get_rect()
# TRYING TO POSITION THE SHIP TO THE BOTTOM-RIGHT OF THE SCREEN.
screen_bottom_right = screen_rect.centerx, screen_rect.bottom
while True:
for event in pygame.event.get():
if ( event == "QUIT" ):
sys.exit()
# Redraw the screen.
screen.fill( bg_color )
# Blit the ship´s image.
screen.blit( ship_image, ( screen_bottom_right ) )
pygame.display.flip()
main()
Tried searching for answers, but none of them had worked / explicitly mentioned this issue. Tutorials, which used the code didn´t substract the X/Y coordinates to obtain exactly positioned image. "0, 0" as rect drawing position works flawlessly. The bottom-right suffers from the above-mentioned issue.
Pygame blits the image so that its top left corner is positioned at the coordinates that you passed. So by passing the screen bottom as the y-coord you're telling pygame to draw the ship below the screen. To fix this you can assign the bottomright attribute of the screen_rect to the bottomright of the ship_rect and then just blit the image at the ship_rect.
import sys
import pygame
def main():
pygame.init()
screen = pygame.display.set_mode((1024, 768))
screen_rect = screen.get_rect()
bg_color = (235, 235, 235)
ship_image = pygame.Surface((40, 50))
ship_image.fill((20, 10, 100))
ship_rect = ship_image.get_rect()
ship_rect.bottomright = screen_rect.bottomright
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
screen.fill(bg_color)
screen.blit(ship_image, ship_rect)
pygame.display.flip()
main()
pygame.Rects have a lot of other attributes that you can use as well, but keep in mind that only the topleft coords are used as the blit position:
x,y
top, left, bottom, right
topleft, bottomleft, topright, bottomright
midtop, midleft, midbottom, midright
center, centerx, centery
size, width, height
w,h

Categories

Resources