How to make a collision detector in pygame - python

I am making a networked game that involves generating random blocks. Players should not be able to move inside the blocks so I created a collision detection function to stop a player moving inside a block. The function is able to stop a player moving into a block from the left and the top but players can still move in from the right and bottom. I also have one more problem that ever since I generated the block the player moves extremely slow.
This is the code for the Player class:
class player:
def __init__(self, x, y, width, height, colour):
self.width = width # dimensions of player
self.height = height # dimensions of player
self.x = x # position on the screen
self.y = y # position on the screen
self.colour = colour # players colour
self.rect = (x, y, width, height) # all the players properties in one
self.speed = 2 # how far/fast you move with each key press
self.direction = ""
def draw(self, win):
pygame.draw.rect(win, self.colour, self.rect)
def move(self, platforms):
keys = pygame.key.get_pressed() # dictionary of keys - values of 0/1
if keys[pygame.K_LEFT]: # move left: minus from x position value
if self.x <= 5:
pass
else:
self.x -= self.speed
self.direction = "Left"
elif keys[pygame.K_RIGHT]: # move right: add to x position value
if self.x == 785:
pass
else:
self.x += self.speed
self.direction = "right"
elif keys[pygame.K_UP]: # move up: minus from y position value
if self.y <= 5:
pass
else:
self.y -= self.speed
self.direction = "up"
elif keys[pygame.K_DOWN]: # move down from
if self.y >= 785:
pass
else:
self.y += self.speed
self.direction = "down"
def update(self, platforms, direction):
self.collision_with_platform(platforms, direction)
self.rect = (self.x, self.y, self.width, self.height) # redefine where the player is
self.update(platforms, self.direction)
This is the collision detector:
def collision_with_platform(self, platforms, direction):
# evaluates if the player is inside a platform, moving the player to it's edge if so
for platform in platforms:
# checks if player's coordinates are inside the platform, for each platform
if platform.rect.left - 20 < self.x < platform.rect.left + 50:
if platform.rect.top - 20 < self.y < platform.rect.top + 50:
# if moving horizontally
if direction == "left" or direction == "right": # evaluates which side of the platform to move the player to
if direction == "left":
self.x = platform.rect.left + 10 # move to the right side
if direction == "right":
self.x = platform.rect.left - 10 # move to the left side
# if moving vertically
elif direction == "down" or direction == "up": # evaluates which side of the platform to move the player to
if direction == "down":
self.y = platform.rect.top - 10 # move to the top side
elif direction == "up":
self.y_pos = platform.rect.top + 50 # move to the bottom side
This is how I generate platforms:
class Platform:
# represents a platform which the player can stand on
def __init__(self, left, top, width, height, colour):
self.rect = pygame.Rect(left, top, width, height) # square representing a single platform
self.colour = colour # platform's colour
def generate_platforms(probability):
# generates platforms randomly
points, platforms = [], []
# generates a list of all possible platform coordinates
# format of points : [(0,0), (0,1), (0,2), ..., (1,0), (1,1), (1,2), ...]
for x in range(0, 600):
for y in range(0, 600):
points.append((x, y))
# randomly selects platforms to create out of list of all possible platforms
for point in points:
if random.randint(1, 100) <= probability: # probability of each individual platform being created
platforms.append(Platform(point[0] * 50, point[1] * 50, 50, 50, (0, 0, 225)))
return platforms # returns the list of generated platforms
This is the main game loop:
def main(): # asking server for updates, checking for events
run = True
n = Network()
startPos = read_pos(n.getPos())
p = player(startPos[0], startPos[1], 10, 10, (255, 0, 0)) # connect, get starting position
p2 = player(0, 0, 10, 10, (0, 0, 255))
platforms = generate_platforms(25)
clock = pygame.time.Clock()
while run:
clock.tick(60)
p2Pos = read_pos(n.send(make_pos((p.x, p.y))))
p2.x = p2Pos[0]
p2.y = p2Pos[1]
p2.update(platforms, p.direction)
for event in pygame.event.get():
if event.type == pygame.QUIT: # quitting condition
run = False
pygame.quit()
p.move(platforms) # move character based off what keys are being pressed
redrawWindow(win, p, p2, platforms)

The problem is you have way more platforms than you think, there is a 25% chance to have a platform on every pixel that is 50 x 50. you have a lot of overlapping platforms. I ran it and got a length of 62516 platforms. I think what you want it
for x in range(0, screen_Width//50):
for y in range(0, screen_Height//50):
if random.randint(1, 100) <= probability: # probability of each individual platform being created
platforms.append(Platform(x * 50, y * 50, 50, 50, (0, 0, 225)))
This only creates platforms every 50 pixels, so none overlap. Running this i got a length of 22, a lot lower and still looks about the same. This will speed up your program considerably. I was getting 1.5 fps but now maxed out fps at 60.
To fix your collisions.
Change your player rect to a pygame rect object
self.rect = pygame.Rect(x, y, width, height) # all the players properties in one
and change the rect when you move
if keys[pygame.K_LEFT]: # move left: minus from x position value
if self.x > 5:
self.x -= self.speed
self.rect.x -= self.speed
self.direction = "left" # lowercase l as everything else was lowercase
then for the collisions:
if self.rect.colliderect(platform.rect):
if direction == "left":
self.rect.left = platform.rect.right # move to the right side
elif direction == "right":
self.rect.right = platform.rect.left # move to the left side
# if moving vertically
elif direction == "down":
self.rect.bottom = platform.rect.top # move to the top side
elif direction == "up":
self.rect.top = platform.rect.bottom # move to the bottom side
also In player.update you dont need to call it again inside it. So just this will do
def update(self, platforms):
self.collision_with_platform(platforms, self.direction)

Related

How to make collision detection work when you have simulated gravity for your game?

I am creating a platform game using pygame, in my game I have simulated gravity and platforms that can be jumped on but I'm having trouble getting the collision detection to work correctly. By collision detection I mean when my character sprite jumps I want him to bounce off of the bottom and sides of the platform that will be above him. Now my character sprite jumps through the platform and lands on top of it.
My code is as follows:
My main class:
import pygame
import random
from settings import *
from sprites import *
from camera import *
from os import path
class Game:
def __init__(self):
pygame.init() # initialises pygame
pygame.mixer.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) # sets the width and height of the pygame window
pygame.display.set_caption(TITLE)
self.clock = pygame.time.Clock()
self.running = True
self.font_name = pygame.font.match_font(FONT_NAME)
self.load_data()
def load_data(self):
pass
def new(self):
self.all_sprites = pygame.sprite.Group()
self.platforms = pygame.sprite.Group()
self.player = Player(self)
self.all_sprites.add(self.player)
for plat in PLATFORM_LIST:
p = Platform(*plat)
self.all_sprites.add(p)
self.platforms.add(p)
self.camera = Camera(WIDTH, HEIGHT) # creates the camera with WIDTH and HEIGHT of the screen
self.run()
def run(self): # Game Loop - runs the game
self.playing = True
while self.playing:
self.clock.tick(FPS)
self.events()
self.update()
self.draw()
def update(self): # Game loop - update
self.all_sprites.update()
# collision with a platform
if self.player.vel.y > 0:
hits = pygame.sprite.spritecollide(self.player, self.platforms, False) # returns a list of platform sprites that hit the player
if hits:
self.player.pos.y = hits[0].rect.top
self.player.vel.y = 0
# screen moves with player
self.camera.update(self.player) # is the camera that tracks players movement
def events(self): # Game loop - events
for event in pygame.event.get():
if event.type == pygame.QUIT:
if self.playing:
self.playing = False
self.running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
self.player.jump()
def draw(self): # Game loop - draw
self.screen.fill(RED)
#self.all_sprites.draw(self.screen)
for sprite in self.all_sprites:
self.screen.blit(sprite.image, self.camera.apply(sprite)) # loops through the all_sprites group and blit's each sprite onto the screen
pygame.display.flip()
def start_screen(self):
pass
def game_over_screen(self):
pass
def wait_for_key(self):
pass
def draw_text(self,text, size, colour, x, y):
pass
g = Game()
g.start_screen()
while g.running:
g.new()
g.game_over_screen()
pygame.quit()
my sprite classes:
# will hold the sprite classes
import pygame
from settings import *
import random
vec = pygame.math.Vector2
class Player(pygame.sprite.Sprite):
def __init__(self, game):
pygame.sprite.Sprite.__init__(self)
self.game = game
self.image = pygame.Surface((30, 40))
self.image.fill(BLUE)
self.rect = self.image.get_rect()
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.pos = vec(WIDTH / 2, HEIGHT / 2)
self.vel = vec(0, 0)
self.acc = vec(0, 0)
def jump(self):
# jump only if on a platform
self.rect.x += 1
hits = pygame.sprite.spritecollide(self, self.game.platforms, False)
self.rect.x -= 1
if hits:
self.vel.y = -20
def update(self):
self.acc = vec(0, PLAYER_GRAV)
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.acc.x = -PLAYER_ACC
if keys[pygame.K_RIGHT]:
self.acc.x = PLAYER_ACC
# apply friction
self.acc.x += self.vel.x * PLAYER_FRICTION
# equations of motion
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
# stop from running of the left side of the screen
if self.pos.x < 0:
self.pos.x = 0
self.rect.midbottom = self.pos
class Platform(pygame.sprite.Sprite):
def __init__(self, x, y, width, height):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((width, height))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
camera class
import pygame
from settings import *
# A camera that keeps track of an offset that will be, how far we want to draw the screen which will include all objects on the screen. We are just shifting the drawing of our screen according to the offset. Camera needs to do two things, apply the offset and then update the movement of where the player is on the screen.
class Camera:
def __init__(self, width, height): # we will need to tell the camera how wide and high we want it to be
self.camera = pygame.Rect(0, 0, width, height) # is the rectangle we set to keep track of the screen/be the camera
self.width = width
self.height = height
def apply(self, entity): # method to apply the offset to the screen, by shifting the screen according to the movement of the entity within the camera screen
return entity.rect.move(self.camera.topleft)
def update(self, target): # method to update where the player/target has moved to, updates are done according to last known position of the target
# as the target moves the camera moves in the opposite direction of the target and stays within the center of the screen
x = -target.rect.x + int(WIDTH/2) # left to right
y = -target.rect.y + int(HEIGHT/2) # up and down
# limit scrolling to map size, keeps the 'camera' from going over the edges
x = min(0, x) # left
y = min(0, y) # top
y = max(-(self.height - HEIGHT), y) # bottom
self.camera = pygame.Rect(x, y, self.width, self.height) # adjusts the camera's rectangle with the new x and y
settings module:
# Game options/settings
TITLE = 'Platformer'
WIDTH = 900
HEIGHT = 500
FPS = 60
FONT_NAME = 'arial'
HS_FILE = 'highscore.txt'
SPRITESHEET = 'spritesheet_jumper.png'
# Game colours
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
# Starting Platforms:
PLATFORM_LIST = [(0, HEIGHT - 50, WIDTH, 50), (WIDTH / 2, HEIGHT * 1 / 2, 200, 30), (WIDTH + 150, HEIGHT - 50, WIDTH, 50)]
# player properties
PLAYER_ACC = 0.5
PLAYER_FRICTION = -0.12
PLAYER_GRAV = 0.8
P.S. added all my code for anyone that may need to see it.
Your question says "when you have simulated gravity" but i don't understand how that would affect the bouncing. The answer below should work with your applied gravity so i don't think gravity will be an issue. So first, to bounce, we will need to know which side of the player is colliding with a particular side of the platform. For this the following functions can be used.
#Checks if right side of the player is colliding with left side of platform
def rightCheck(rect1, rect2):
if rect1.x + rect1.width > rect2.x:
if (rect1.y > rect2.y and rect1.y < rect2.y + rect2.height) or (rect1.y + rect1.height < rect2.y + rect2.height and rect1.y + rect1.height> rect2.y):
if rect1.x < rect2.x:
return True
return False
#Checks if bottom side of the player is colliding with top side of platform
def botCheck(rect1, rect2):
if rect1.y + rect1.height > rect2.y:
if (rect1.x > rect2.x and rect1.x < rect2.x + rect2.width) or (rect1.x + rect1.width > rect2.x and rect1.x + rect1.width < rect2.x + rect2.width):
if rect1.y < rect2.y:
return True
return False
# NOTICE they take pygame.Rect as arguments
For your game you are probably going to need one for left of the player and right side of the platform well, but i don't have it since i copied these from my game. I am sure you can write one yourself :). So, moving on. The code below shows how right side of the player colliding with left side of the platform can be implemented. So you can do this with all the sides. Also, bounce function is probably the only one you are interested in, others i has to put there to make it work.
import pygame
win = pygame.display
D = win.set_mode((1200, 600))
def rightCheck(rect1, rect2):
if rect1.x + rect1.width > rect2.x:
if (rect1.y > rect2.y and rect1.y < rect2.y + rect2.height) or (rect1.y + rect1.height < rect2.y + rect2.height and rect1.y + rect1.height> rect2.y):
if rect1.x < rect2.x:
return True
return False
class Player:
def __init__(self, ):
self.pos = [10, 10]
self.surf = pygame.Surface((50, 50)).convert()
self.energy = 10 #how much the player bounces to the left by
self.trigger = False #Triggers the left push if it is true
self.rightCheck = rightCheck # this is the function to check collision
self.rvel = 0.5 # This is the movement speed
def move(self):
key = pygame.key.get_pressed()
if key[pygame.K_RIGHT]:
self.pos[0] += self.rvel
if key[pygame.K_LEFT]:
self.pos[0] -= 0.5
if key[pygame.K_UP]:
self.pos[1] -= 0.5
if key[pygame.K_DOWN]:
self.pos[1] += 0.5
def draw(self):
D.blit(self.surf, (self.pos[0], self.pos[1]))
def bounce(self, platRect):
selfRect = pygame.Rect(self.pos[0], self.pos[1], 50, 50)
if self.rightCheck(selfRect, platRect):
self.trigger = True
self.rvel = 0
else:
self.rvel = 0.5
if self.trigger:
self.pos[0] -= self.energy*0.1
self.rvel = 0.1
self.energy -= 0.05
if self.energy <= 0:
self.trigger = False
else:
self.energy = 10
p = Player()
while True:
pygame.event.get()
D.fill((255, 255, 255))
platformRect = pygame.Rect(300, 150, 600, 200)
pygame.draw.rect(D, (0, 0, 0), platformRect)
p.bounce(platformRect)
p.move()
p.draw()
p.bounce(platformRect)
win.flip()
It basically just checks if they are colliding and if they are pushes the player a little to the left.

pygame - problem with killing a sprite in a group

I am trying to create a game using pygame for a school project. I would like obstacles(in this case boxes) which get in the way of the player. The player is able to destroy the boxes which would result in another box to be spawned in a random location at the same height.
I've split the code into 3 seperate modules seperating the sprites, the main code and the settings(game variables).
main:
import pygame as pg
import random
from sprites import *
from settings import *
import os
import sys
import time
class Game:
def __init__(init):#initialising the games properties(window,sound,speed,etc).
pg.init()
pg.mixer.init()
init.clock = pg.time.Clock()
init.screen = pg.display.set_mode((WIDTH,HEIGHT))
pg.display.set_caption(TITLE)
init.clock = pg.time.Clock()
init.running = True
init.font_name = pg.font.match_font(FONT_NAME)
init.data()
def data(load):
load.dir = os.path.dirname(__file__)
def new(new):#starts the game again.
new.score = 0
new.obstacles = pg.sprite.Group()
new.platforms = pg.sprite.Group()
new.bullets = pg.sprite.Group()
new.all_sprites = pg.sprite.Group()
new.player = Player(new)
new.all_sprites.add(new.player)
for plat in PLATFORM_LIST:
p = Platform(*plat)
new.all_sprites.add(p)
new.platforms.add(p)
for obs in OBSTACLE_LIST:
new.obstacle = Obstacle(*obs)
new.all_sprites.add(new.obstacle)
new.obstacles.add(new.obstacle)
new.run()
def run(run):
run.playing = True
while run.playing:
run.cooldown = 0
run.clock.tick(FPS)
run.events()
run.update()
run.draw()
def update(update):
bullet = Bullet
#game update.
update.all_sprites.update()
#spawning obstacles lower half
while len(update.obstacles) < 3:
width = random.randrange(50, 100)
update.obstacle = Obstacle(random.randrange(0, WIDTH - width),HEIGHT-100,100,50)
update.obstacles.add(update.obstacle)
update.all_sprites.add(update.obstacle)
#spawning obstacles randomly throughout the middle half
#spawning obstacles randomly throughout the map upper half
#check if bullet collides with an obstacles.
collide = pg.sprite.groupcollide(update.bullets,update.obstacles,True,False)
if collide:
update.obstacle.obs_health = update.obstacle.obs_health - bullet.damage
if update.obstacle.obs_health == 0:
update.obstacle.kill()
#check if player hits the sides of an obstacle.
if update.player.velocity.x >0:#when moving right.
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y >= collide[0].rect.centery+20:#if the player is above the platform.
update.player.pos.x = collide[0].rect.left - (PLAYER_WIDTH/2)
update.player.velocity.x = 0
update.player.acceleration.y = 0
if update.player.velocity.x <0:#when moving left.
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y >= collide[0].rect.centery:
update.player.pos.x = collide[0].rect.right + (PLAYER_WIDTH/2)
update.player.velocity.x = 0
#check if player hits side of platforms
if update.player.velocity.x >0 and (update.player.velocity.y < 0):#when moving right.
collide = pg.sprite.spritecollide(update.player,update.platforms,False)
if collide:
if update.player.pos.y < collide[0].rect.centery+50:#if the player is below the obstacle.
update.player.pos.x = collide[0].rect.left - (PLAYER_WIDTH/2)
update.player.velocity.x = 0
if update.player.velocity.x <0:#when moving left.
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y > collide[0].rect.centery:
update.player.pos.x = collide[0].rect.right + (PLAYER_WIDTH/2)
update.player.velocity.x = 0
#check if player hits a platform while ascending:
if update.player.velocity.y <0:#only when moving up.
collide = pg.sprite.spritecollide(update.player,update.platforms,False)
if collide:
if update.player.pos.y > collide[0].rect.bottom:
update.player.pos.y = collide[0].rect.bottom + (PLAYER_HEIGHT/2) + PLAYER_JUMP
update.player.velocity.y = 0
#check if a player hits a platform while falling.
if update.player.velocity.y >0:#only while falling will this apply.
collide = pg.sprite.spritecollide(update.player,update.platforms,False)#false allows you to avoid deleting the object you jump into.
if collide:
if update.player.pos.y < collide[0].rect.centery:#if the player is above the center of the platform.
update.player.pos.y = collide[0].rect.top +1
update.player.velocity.y = 0
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y < collide[0].rect.centery:
update.player.pos.y = collide[0].rect.top +1
update.player.velocity.y = 0
#spawning obstacles randomly throughout the map upper half
#spawning obstacles randomly throughout the middle half
def events(events):
events.cooldown += events.clock.get_time()
#processes inputs.
for event in pg.event.get():
#check for window closing.
if event.type == pg.QUIT:#if the 'x' button is clicked
if events.playing:
events.playing = False#stop the game loop.
events.running = False#stop the main loop.
if event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
events.player.jump()
if event.key == pg.K_SPACE:
events.player.bullet_list.append(events.player.shoot())
#print(len(events.player.bullet_list))
def draw(draw):
draw.screen.fill(GREY)# creates a black screen.
draw.draw_text(str(draw.player.PLAYER_HEALTH),24,BLACK,WIDTH/32,HEIGHT /32)
draw.all_sprites.draw(draw.screen)#draws the sprites in the group all_sprites.
#after drawing the screen is flipped.
pg.display.flip()
def start_screen(start):#screen displayed when the game is started.
start.screen.fill(BGCOLOR)
start.draw_text(TITLE,48,WHITE,WIDTH/2,HEIGHT /4)
start.draw_text("Arrows to move,UP to jump", 22,WHITE,WIDTH/2,HEIGHT/2)
start.draw_text("Press a key to play",22,WHITE,WIDTH/2,HEIGHT*3/4)
pg.display.flip()
start.any_key()#temporary key to start system.
def any_key(wait):
waiting = True
while waiting:#a loop is used for the start screen until an action is done.
wait.clock.tick(FPS)#allows animations to
for event in pg.event.get():
if event.type == pg.QUIT:#if the 'x' button is pressed during the start screen.
waiting = False
wait.running = False#stops the main loop.
if event.type == pg.KEYUP:#if any key is released.
waiting = False
def over_screen(over):#displayed when the game ends.
if not over.running:
return#skips the over screen when 'x' button is pressed.
over.screen.fill(BGCOLOR)
over.draw_text('GAME OVER',48,WHITE,WIDTH/2,HEIGHT /4)
def draw_text(self, text, size, color, x, y):
font = pg.font.Font(self.font_name, size)#selects the chosen font.
text_surface = font.render(text, True, color)#creates the text with anti aliasing and the color chosen.
text_rect = text_surface.get_rect()
text_rect.midtop = (x, y)#position of text.
self.screen.blit(text_surface, text_rect)#renders text on screen.
g = Game()
g.start_screen()
while g.running:#the main loop.
g.new()
g.over_screen()
pg.quit()#closes the window.
sprites:
#Sprite class
import random
import pygame as pg
from settings import *
vec = pg.math.Vector2 #creates a 2D Vector which stores the x an y cordinates for the sprites.
class Player(pg.sprite.Sprite):
def __init__(self, game):#create initialise the properties of the sprite.
self.game = game#reference to variable in game class.
pg.sprite.Sprite.__init__(self)#provides functions for the sprite in other functions.
self.image = pg.Surface((PLAYER_WIDTH,PLAYER_HEIGHT))#creates a square for the Player to be used as a hitbox.
self.image.fill(GREEN)#place holder for the player.
self.rect = self.image.get_rect()
self.rect.center = (WIDTH/2),(HEIGHT/4)# allows you to move the character.
self.pos = vec(WIDTH/2,HEIGHT/2)#the center of the sprite.
self.velocity = vec(0,0)#the speed of the player.
self.acceleration = vec(0,0)#allows for the change in speed.
self.facing = 0 #direction the player is looking.
self.current = 0#current direction facing.
self.PLAYER_HEALTH = 1000
self.bullet_list = []
def jump(self):
hits = pg.sprite.spritecollide(self, self.game.platforms, False)
if hits:#only able to jump when colliding with platform.
self.velocity.y += -PLAYER_JUMP
collide = hits = pg.sprite.spritecollide(self, self.game.obstacles, False)
if collide:
self.velocity.y += -PLAYER_JUMP
def shoot(self):
#if game.cooldown > 400:
#cooldown = 0
self.bullet = Bullet(self,self.current,self.rect.centerx, self.rect.top)
self.bullet_list.append(self.bullet)
for bullet in self.bullet_list:
#self.bullet = Bullet(self.current,self.rect.centerx, self.rect.top)#creates bullet postioned in center.
self.game.all_sprites.add(self.bullet)
self.game.bullets.add(self.bullet)
#self.bullet = Bullet(self.current,self.rect.centerx, self.rect.top)#creates bullet postioned in center.
#self.game.all_sprites.add(self.bullet)
#self.game.bullets.add(self.bullet)
def update(self):
self.acceleration = vec(0,PLAYER_GRAV)#resets the position of player when not moving.
keys = pg.key.get_pressed()#inputs a pressed key.
if keys[pg.K_LEFT]:
self.acceleration.x = -PLAYER_ACC
self.facing = -1
self.current = self.facing
if keys[pg.K_RIGHT]:
self.acceleration.x = PLAYER_ACC
self.facing = 1
self.current = self.facing
if self.acceleration.x == 0:#if standing, the previous direction is saved
self.facing = self.current
#print(self.current)
#friction.
self.acceleration.x += self.velocity.x * PLAYER_FRICTION
#equation for displacment.
self.velocity += self.acceleration
self.pos += self.velocity + 0.5 * self.acceleration#moves thes players position to the new x,y co-ordinate.
#boundaries of screen.
if self.rect.right > WIDTH:
self.pos.x = WIDTH -(PLAYER_WIDTH * 0.5)#avoids overlapping the boundary.
self.velocity.x = 0 #avoids player sticking to walls by capping speed at boundaries.
self.acceleration.x = 0 #reduces the amount of 'jitter' when trying to move past boundaries.
if self.rect.left < 0 :
self.pos.x = (PLAYER_WIDTH * 0.5)#avoids overlapping the boundary.
# have to add half the player width to avoid player going into walls.
self.velocity.x = 0 #avoids player sticking to walls by stopping player at boundaries.
self.rect.midbottom = self.pos#tracks the position of the players center.
class Platform(pg.sprite.Sprite,):
def __init__(self, x, y, w, h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w,h))
self.image.fill(BLACK)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
class Obstacle(pg.sprite.Sprite):
def __init__(self,x,y,w,h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w,h))
self.image.fill(BLUE)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.obs_health = 100
class Bullet(pg.sprite.Sprite):
damage = 25
def __init__(self,player,current, x, y):
self.player = player#allows the bullet class to use player variables.
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((20,10))
self.image.fill(LBLUE)
self.rect = self.image.get_rect()
self.rect.right = x
self.rect.centery = y + (PLAYER_HEIGHT/2)
#self.damage = 25
self.velocity = vec(0,0)
if current == -1:#when looking left.
self.velocity = vec(-10,0)
if current == 1:#when looking right.
self.velocity = vec(10,0)
def update(self):
self.rect.right += self.velocity.x
#remove when moves of off screen.
if self.rect.right > WIDTH:
self.kill()
for bullet_amount in self.player.bullet_list:
self.player.bullet_list.pop(self.player.bullet_list.index(bullet_amount))
if self.rect.left <0:
self.kill()
for bullet_amount in self.player.bullet_list:
self.player.bullet_list.pop(self.player.bullet_list.index(bullet_amount))
#print(self.rect.x)
settings:
#settings
TITLE = "downpour"
WIDTH = 900
HEIGHT = 500
FPS = 60
FONT_NAME = 'Ariel'
#platforms
PLATFORM_LIST = [(0, HEIGHT - 50, WIDTH, 50),
(WIDTH -225 ,HEIGHT * 3/4 -50,200, 40),#(x,y,width,height)of the platforms.
(0 +25 ,HEIGHT * 3/4 -50,200, 40),
(0 +350,HEIGHT * 3/4 -150,200, 40)]
OBSTACLE_LIST = [(WIDTH/2,HEIGHT -50-50,100,50),(WIDTH/3,HEIGHT -50-50,100,50),(WIDTH/2,HEIGHT -50-50,100,50)]
#player properties
PLAYER_WIDTH = 50
PLAYER_HEIGHT = 50
PLAYER_ACC = 0.55
PLAYER_FRICTION = -0.05
PLAYER_GRAV = 0.8
PLAYER_JUMP = 15
#colors defines
WHITE = (255,255,255)
BLACK = (0,0,0)
GREY = (211,211,211)
RED = (255,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
LBLUE = (132,112,255)
BGCOLOR = LBLUE
The problem I have encountered is with spawning a new box after destroying one of the multiple boxes. A box can be destroyed by depleting its health through shooting at it.
Lets say I have 3 boxes: A,B and C. when I try to destroy B or C, box A is the one that is destroyed and respawned.
I feel like it's an obvious answer...
code relating to the obstacle:
creating the class:
class Obstacle(pg.sprite.Sprite):
def __init__(self,x,y,w,h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w,h))
self.image.fill(BLUE)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.obs_health = 100
adding it to a Sprite group:
for obs in OBSTACLE_LIST:
new.obstacle = Obstacle(*obs)
new.all_sprites.add(new.obstacle)
new.obstacles.add(new.obstacle)
collisions:
collide = pg.sprite.groupcollide(update.bullets,update.obstacles,True,False)
if collide:
update.obstacle.obs_health = update.obstacle.obs_health - bullet.damage
if update.obstacle.obs_health == 0:
update.obstacle.kill()
spawning a new obstacle:
while len(update.obstacles) < 3:
width = random.randrange(50, 100)
update.obstacle = Obstacle(random.randrange(0, WIDTH - width),HEIGHT-100,100,50)
update.obstacles.add(update.obstacle)
update.all_sprites.add(update.obstacle)
First of all, for all instance methods, it would help the reader if you used the name self instead of all the custom names you're using such as new or update for the first argument.
After that rewrite, you code will look like follows:
collide = pg.sprite.groupcollide(self.bullets,self.obstacles,True,False)
if collide:
self.obstacle.obs_health = self.obstacle.obs_health - bullet.damage
if self.obstacle.obs_health == 0:
self.obstacle.kill()
Now ask yourself, why does the program know that the self.obstacle is the one that collided? Should self.obstacle even exist? It looks like self.obstacle was just used a temporary local variable upon creation of the Game class to add Obstacle's to self.obstacles.
If so just use a local variable as follows:
for obs in OBSTACLE_LIST:
obstacle = Obstacle(*obs)
self.all_sprites.add(obstacle)
self.obstacles.add(obstacle)
At this point, hopefully the error message will make it clear that referencing self.obstacle isn't going to work. pg.sprite.groupcollide returns you a Sprite_dict, so you need to extract the obstacle from collide to figure out what has collided.
#KentShikama Thanks for pointing that out.
I have fixed the issue by using the dictionary.
for obstacles, bullets in collide.items():
obstacles.obs_health = obstacles.obs_health - bullet.damage
if obstacles.obs_health == 0:
obstacles.kill()

pygame: Random direction of a ball at the start of the game

I'm doing the Pong game. However instead of 2 players, I'm doing it for 4 (one on each side of the screen). I need to have the ball randomly "choose" the direction it has to go.
import pygame
import random
class Ball(object):
#classmethod
def init(cls, SCREEN_WIDTH, SCREEN_HEIGHT):
cls.radius = 20
cls.centerx = SCREEN_WIDTH*0.5
cls.centery = SCREEN_HEIGHT*0.5
cls.rect = pygame.Rect(cls.centerx - cls.radius,
cls.centery - cls.radius,
cls.radius * 2,
cls.radius * 2)
# x , y
cls.direction = [random.choice([1, -1]), random.choice([1, -1])]
cls.speed = [5, 8] # x, y
# left, right, top, bottom
cls.hit_edge = [False, False, False, False]
#classmethod
def update(cls, player1, player2, player3, player4, SCREEN_WIDTH,
SCREEN_HEIGHT):
cls.centerx += cls.direction[0] * cls.speed[0]
cls.centery += cls.direction[1] * cls.speed[1]
cls.rect.center = (cls.centerx, cls.centery)
#detects if someone losses
if cls.rect.left <= 0:
cls.hit_edge[0] = True
elif cls.rect.right >= SCREEN_WIDTH-1:
cls.hit_edge[1] = True
elif cls.rect.top <= 0:
cls.hit_edge[2] = True
elif cls.rect.bottom >= SCREEN_HEIGHT-1:
cls.hit_edge[3] = True
#detects collision between players & the ball
if cls.rect.colliderect(player1.rect):
cls.direction[0] = 1
cls.up_speed()
elif cls.rect.colliderect(player2.rect):
cls.direction[0] = -1
cls.up_speed()
elif cls.rect.colliderect(player3.rect):
cls.direction[1] = 1
cls.up_speed()
elif cls.rect.colliderect(player4.rect):
cls.direction[1] = -1
cls.up_speed()
#classmethod
def up_speed(cls):
cls.speed[0] += random.uniform(0, 0.25)
cls.speed[1] += random.uniform(0, 0.25)
#classmethod
def render(cls, SCREEN, color):
pygame.draw.circle(SCREEN, color, cls.rect.center, cls.radius, 0)
To take into account: I had the idea to add a "0" in every random.choice(), although if I do this only function at the beginning, then it will not be able to move in the axis where the "0" . Also I have two types of speeds in X and Y, could be solved by putting a "0.1" in random.choice () but this would make when the game starts the ball goes very slow. As you would do for the ball to start in a random direction (taking into account that the speed of the ball at the start must be the same for all players. If the ball goes at the beginning to the left,and later (in another game) when it starts but the ball goes up has to go at the same speed)
This may be a little over-complicating things, but if you know the speed you want the ball to start with overall, you could use something like this:
Generate random number between 0-1
angle = 360 * random number
xSpeed = startSpeed * sin(angle)
ySpeed = startSpeed * cos(angle)
This will mean that your ball will always travel at the same speed. The only thing that is random is the direction it travels in.
I recommend to use vectors. For the velocity you can just pick an arbitrary start speed like (8, 0) and then rotate the vector by a random angle.
position = pg.math.Vector2(100, 200)
velocity = pg.math.Vector2(8, 0).rotate(random.randrange(360))
To update the position:
position += velocity
Here's an example program that spawns balls with random color and velocity.
import sys
import math
from random import randrange
import pygame as pg
class Ball(pg.sprite.Sprite):
def __init__(self, pos, *groups):
super().__init__(groups)
self.image = pg.Surface((30, 30), pg.SRCALPHA)
col = randrange(256), randrange(256), randrange(256)
pg.draw.circle(self.image, col, (15, 15), 15)
self.rect = self.image.get_rect(center=pos)
self.vel = pg.math.Vector2(8, 0).rotate(randrange(360))
self.pos = pg.math.Vector2(pos)
def update(self):
self.pos += self.vel
self.rect.center = self.pos
if self.rect.left < 0 or self.rect.right > 640:
self.vel.x *= -1
if self.rect.top < 0 or self.rect.bottom > 480:
self.vel.y *= -1
def main():
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
sprite_group = pg.sprite.Group()
ball = Ball((320, 240), sprite_group)
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
elif event.type == pg.MOUSEBUTTONDOWN:
sprite_group.add(Ball((320, 240)))
sprite_group.update()
screen.fill((30, 30, 30))
sprite_group.draw(screen)
pg.display.flip()
clock.tick(30)
if __name__ == '__main__':
pg.init()
main()
pg.quit()
sys.exit()

Flawed collision detection between rectangles

I am creating a physics-based game with Pygame in which the player controls a ball. As you control the ball, it accelerates in the specified direction (holding the left arrow adds x pixels per frame to its movement speed). Since the ball is... well... a ball, and Pygame doesn't support ball collision detection, I created a new class with its own collision method. The method has two parts: if the ball runs into the corner of a rectangle, or if it runs into the side of the rectangle. The problem concerns the circle-to-side collision.
The ball is based on a rect object, and therefore has those pesky corners. I cannot use the simple colliderect method, otherwise the situation above would detect a collision where there should be none, and it would overlap the first part of my collision detection method. Instead, I opted to use collidepoint between the rectangle and the midpoints on each side of the ball's rectangle.
Finally, the heart of the issue. I mentioned earlier that the ball accelerates. When the ball accelerates to the point that (even though it appears to be standing still) it moves far enough into the rectangle for another midpoint on the circle to detect a "collision." This problem likely stems from the fact that (for a collision on the left side of the ball) my code sets the ball's left equal to the rectangle's right, so that when the ball accelerates enough to be inside the rectangle, it gets moved to another face of the rectangle.
Thank you so much for bearing with me, any and all suggestions are welcome. I would either be looking for a fix to my specific problem, or a cleaner way to handle the collision detection. My full code is below:
import pygame, sys, math
global Color
Color = {}
Color['white'] = (255,255,255)
Color['black'] = ( 0, 0, 0)
Color['red'] = (255, 0, 0)
Color['green'] = ( 0,255, 0)
Color['blue'] = ( 0, 0,255)
global WINDOWWIDTH, WINDOWHEIGHT
WINDOWWIDTH, WINDOWHEIGHT = 500, 500
class Ball():
def __init__(self, x, y, r):
self.rect = pygame.Rect(x, y, r, r)
self.radius = r/2
self.speed = [0, 0]
self.b_fact = 1
self.move = {'left':False, 'right':False, 'up':False, 'down':False}
self.new_dir = {'left':False, 'right':False, 'up':False, 'down':False}
def move_self(self):
if self.move['left']:
self.speed[0] -= 2
if self.move['up']:
self.speed[1] -= 2
if self.move['right']:
self.speed[0] += 2
if self.move['down']:
self.speed[1] += 2
if self.speed[0] < 0:
self.speed[0] += 1
if self.speed[1] < 0:
self.speed[1] += 1
if self.speed[0] > 0:
self.speed[0] -= 1
if self.speed[1] > 0:
self.speed[1] -= 1
self.rect.left += self.speed[0]
self.rect.top += self.speed[1]
def bounce(self, rectList):
for rect in rectList:
self.collide_rect(rect)
if self.rect.left <= 0:
self.rect.left = 0
self.new_dir['right'] = True
if self.rect.right >= WINDOWWIDTH:
self.rect.right = WINDOWWIDTH
self.new_dir['left'] = True
if self.rect.top <= 0:
self.rect.top = 0
self.new_dir['down'] = True
if self.rect.bottom >= WINDOWHEIGHT:
self.rect.bottom = WINDOWHEIGHT
self.new_dir['up'] = True
for key in self.new_dir:
if self.new_dir[key] and key=='left':
self.speed[0] *= (-1)*self.b_fact
if self.new_dir[key] and key=='right':
self.speed[0] *= (-1)*self.b_fact
if self.new_dir[key] and key=='up':
self.speed[1] *= (-1)*self.b_fact
if self.new_dir[key] and key=='down':
self.speed[1] *= (-1)*self.b_fact
self.new_dir[key] = False
def collide_rect(self, rect):
x1, y1, r = self.rect.centerx, self.rect.centery, self.radius
foundSide = 0
foundCorner = 0
side_list = ['left', 'right', 'bottom', 'top']
corner_list = ['topleft', 'topright', 'bottomleft', 'bottomright']
collision_list = []
for side in side_list:
if rect.collidepoint(eval('self.rect.mid'+side)):
collision_list.append(side)
for corner in corner_list:
x2, y2 = eval('rect.'+corner)[0], eval('rect.'+corner)[1]
dist = math.sqrt((x2 - x1)**2 + (y2 - y1)**2)
if dist < r:
if corner.find('left') > -1:
corner = corner.replace('left','right')
else:
corner = corner.replace('right','left')
if corner.find('top') > -1:
corner = corner.replace('top','bottom')
else:
corner = corner.replace('bottom','top')
collision_list.append(corner)
for direction in collision_list:
if direction.find('left') > -1:
self.rect.left = rect.right
self.new_dir['left'] = True
if direction.find('top') > -1:
self.rect.top = rect.bottom
self.new_dir['top'] = True
if direction.find('right') > -1:
self.rect.right = rect.left
self.new_dir['right'] = True
if direction.find('bottom') > -1:
self.rect.bottom = rect.top
self.new_dir['bottom'] = True
class BallGame():
def __init__(self):
pygame.display.set_caption("Ball is life")
pygame.init()
self.ball = Ball(0, 0, 30)
self.allRects = []
rect = pygame.Rect(60,60,50,50)
self.allRects.append(rect)
self.mainClock = pygame.time.Clock()
self.screen = pygame.display.set_mode((WINDOWWIDTH, WINDOWHEIGHT))
self.basicFont = pygame.font.SysFont(None, 50)
def drawScreen(self):
self.screen.fill(Color['green'])
pygame.draw.ellipse(self.screen, Color['white'], self.ball.rect)
for rect in self.allRects:
pygame.draw.rect(self.screen, Color['black'], rect)
def mainloop(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
for i in range(2):
k = (pygame.KEYUP, pygame.KEYDOWN)
if event.type == k[i]:
if event.key == pygame.K_LEFT:
self.ball.move['left'] = i
elif event.key == pygame.K_UP:
self.ball.move['up'] = i
elif event.key == pygame.K_RIGHT:
self.ball.move['right'] = i
elif event.key == pygame.K_DOWN:
self.ball.move['down'] = i
self.ball.move_self()
self.ball.bounce(self.allRects)
self.drawScreen()
pygame.display.update()
self.mainClock.tick(20)
Game = BallGame()
while True:
Game.mainloop()
Another way to think about the collision is to consider an enlarged version of the black rectangle. This would be a rounded rectangle with corner radius r. The collision between the ball and black rectangle is equivalent to the collision between the center of the ball and the rounded rectangle. This can help make the analysis of the situation easier.
When it bounces a more accurate way of determining the new position is to consider the line from the previous position to the current position. You can calculate where this line crosses the boundary and where a prefect reflection should be.

Make a sprite move to the mouse click position step by step

I'm writing a little pirate game in Pygame. If you played sea battles in Empires Total War, you have an idea of what I would like to achieve:
The ship's sprite is at position (x1|y1). The player now clicks at position (x2|y2) on the screen. The sprite is now supposed to take (x2|y2) as its new position - by going there step by step, not by beaming there instantly.
I figured out that it has something to do with the diagonal of the rectangle (x1|y1),(x1|y2),(x2|y2),(x2|y1) but I just can't figure it out, especially not with keeping the speed the same no matter what angle that diagonal has and considering that the x and y values of either (ship or click) might be bigger or smaller than the respective other.
This little snippet is my last try to write a working function:
def update(self, new_x, new_y, speed, screen, clicked):
if clicked:
self.xshift = (self.x - new_x)
self.yshift = ((self.y - new_y) / (self.x - new_x))
if self.x > (new_x + 10):
self.x -= 1
self.y -= self.yshift
elif self.x > new_x and self.x < (new_x + 10):
self.x -= 1
self.y -= self.yshift
elif self.x < (new_x - 10):
self.x += 1
self.y += self.yshift
elif self.x < new_x and self.x < (new_x - 10):
self.x += 1
self.y += self.yshift
else:
self.x += 0
self.y += 0
screen.set_at((self.x, self.y), (255, 0, 255))
The "ship" is just a pink pixel here. The reaction it shows upon my clicks onto the screen is to move roughly towards my click but to stop at a seemingly random distance of the point I clicked.
The variables are:
new_x, new_y = position of mouseclick
speed = constant speed depending on ship types
clicked = set true by the MOUSEBUTTONDOWN event, to ensure that the xshift and yshift of self are only defined when the player clicked and not each frame again.
How can I make the ship move smoothly from its current position to the point the player clicked?
Say the current position is pos, and the point the player clicked is target_pos, then take the vector between pos and target_pos.
Now you know how to get from pos to target_pos, but to move in constant speed (and not the entire distance at once), you have to normalize the vector, and apply a speed constant by scalar multiplication.
That's it.
Complete example: (the relevant code is in the Ship.update method)
import pygame
class Ship(pygame.sprite.Sprite):
def __init__(self, speed, color):
super().__init__()
self.image = pygame.Surface((10, 10))
self.image.set_colorkey((12,34,56))
self.image.fill((12,34,56))
pygame.draw.circle(self.image, color, (5, 5), 3)
self.rect = self.image.get_rect()
self.pos = pygame.Vector2(0, 0)
self.set_target((0, 0))
self.speed = speed
def set_target(self, pos):
self.target = pygame.Vector2(pos)
def update(self):
move = self.target - self.pos
move_length = move.length()
if move_length < self.speed:
self.pos = self.target
elif move_length != 0:
move.normalize_ip()
move = move * self.speed
self.pos += move
self.rect.topleft = list(int(v) for v in self.pos)
def main():
pygame.init()
quit = False
screen = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
group = pygame.sprite.Group(
Ship(1.5, pygame.Color('white')),
Ship(3.0, pygame.Color('orange')),
Ship(4.5, pygame.Color('dodgerblue')))
while not quit:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
if event.type == pygame.MOUSEBUTTONDOWN:
for ship in group.sprites():
ship.set_target(pygame.mouse.get_pos())
group.update()
screen.fill((20, 20, 20))
group.draw(screen)
pygame.display.flip()
clock.tick(60)
if __name__ == '__main__':
main()

Categories