How to program a collision - python

I want to code a collision. I have 2 classes and if they collide one of them should undraw for 1 second
#Laden der Pygame Bibliothek
import pygame
import time
import random
#Initialisierung der Pygame Bibliothek
pygame.init()
# Spiel-Fenster erstellen
size = [700, 500]
screen = pygame.display.set_mode(size)
screen.fill((255,255,255))
# Noetig um die fps zu begrenzen
clock = pygame.time.Clock()
# Speichert ob das Spiel-Fenster geschlossen wurde
done = False
First class that spawn an object that only can move left and right
class Schlitten():
def __init__(self, px, py, pscreen):
self.FARBE1 = (139,87,66)
self.FARBE2 = (139,90,43)
self.braun = (104,73,71)
self.x = px
self.grau = (118,122,121)
self.y = py
self.red = (255,0,0)
self.screen = pscreen
self.hit = False
def draw(self):
if self.hit == False:
pygame.draw.rect(self.screen, self.FARBE2, [self.x,self.y,5,75])
pygame.draw.rect(self.screen, self.FARBE2, [self.x+29,self.y,5,75])
pygame.draw.rect(self.screen, self.braun, [self.x+5,self.y+20,24,3])
pygame.draw.rect(self.screen, self.braun, [self.x+5,self.y+55,24,3])
pygame.draw.rect(self.screen, self.FARBE1, [self.x+6,self.y+15,3,50])
pygame.draw.rect(self.screen, self.FARBE1, [self.x+12,self.y+15,3,50])
pygame.draw.rect(self.screen, self.FARBE1, [self.x+18,self.y+15,3,50])
pygame.draw.rect(self.screen, self.FARBE1, [self.x+24,self.y+15,3,50])
pygame.draw.rect(self.screen, self.grau, [self.x+5,self.y+10,24,2])
def kollision(self):
self.hit = True
def movemint(self):
keys = pygame.key.get_pressed()
if keys [pygame.K_LEFT] :
self.x -= 4
if keys [pygame.K_RIGHT] :
self.x += 4
if self.x < 0:
self.x += 4
if self.x > 665:
self.x -= 4
def left(self):
return self.x
def right(self):
return self.x+34
def up(self):
return self.y
def down(self):
return self.y+75
Second class that spawn trees that are coming from above
class Baum():
def __init__(self ,pos_x , pos_y ,pscreen ,pschlitten):
self.green = (0,100,0)
self.braun = (139,69,19)
self.red = (255,0,0)
self.x = pos_x
self.y = pos_y
self.screen = pscreen
self.Schlitten = pschlitten
def draw(self):
pygame.draw.polygon(self.screen ,self.green , [(self.x+50 ,self.y-95),(self.x+0 , self.y-10),
(self.x+100,self.y-10)])
pygame.draw.rect(self.screen , self.braun , [self.x+43,self.y-10,15,30])
pygame.draw.polygon(self.screen , self.green , [(self.x+50 , self.y-95), (self.x+5 , self.y-
25), (self.x+95,self.y-25)])
pygame.draw.polygon(self.screen , self.green , [(self.x+50 , self.y-95), (self.x+10 , self.y-
40), (self.x+90,self.y-40)])
pygame.draw.polygon(self.screen , self.green , [(self.x+50 , self.y-95), (self.x+15, self.y-
53), (self.x+85,self.y-53)])
def bewegung(self):
self.y += 5
def spawn(self):
if self.y > 600:
self.y = -50
self.x = random.randrange(0,700)
This is the collision but its unfinished
def collision(self):
if self.y > 385:
self.Schlitten.hit()
#Objekt der Klasse Schlitten erzeugen
spieler1 = Schlitten(350,400,screen)
Score = score(Baum)
#Objekt der Klasse Baum erzeugen
Baum1 = Baum(500,0 ,screen , spieler1)
Baum2 = Baum(300,-525 , screen , spieler1)
Baum3 = Baum(100,-1050 , screen, spieler1)
schrift = pygame.font.SysFont("comicsans" , 30 , True )
# -------- Haupt-Schleife -----------
while not done:
# Ändert den Wert von done auf True, falls Spiel-Fenster geschlossen wird
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
# --- hier Zeichenbefehle ergänzen---
# Screen mit weiß fuellen
screen.fill((255,255,255))
pygame.mixer.music.set_volume(0.1)
Score.anzeigen()
# Schlitten zeichnen
spieler1.draw()
spieler1.movemint()
# Baeume zeichnen
Baum1.draw()
Baum1.bewegung()
Baum1.spawn()
Baum1.collision()
Baum2.draw()
Baum2.bewegung()
Baum2.spawn()
Baum2.collision()
Baum3.draw()
Baum3.bewegung()
Baum3.spawn()
Baum3.collision()

Ok, firstly is the standard response to these sort of questions: If you use the PyGame Sprite functions, after some extra work initially, your program will be easier to write and maintain.
To make good collisions on arbitrary objects, first you need a "bounding box". This is a rectangle which surrounds your object.
Looking at the code for the Schlitten/Sleigh I have to calculate this from the various drawing co-ordinates (but I'm only doing a quick/rough job). It looks like from Schlitten.x and Schlitten.y the rendering extends another 31 pixels in x and 75 pixels in y. You may want to temporarily add some code to draw the bounding-box to check it.
So to define a collision function, we need a PyGame Rect.
class Schlitten:
def __init__( self ):
self.x = px
self.y = py
self.width = 31
self.height = 75
self.rect = pygame.Rect( px, py, self.width, self.height )
As you can see from the code, a PyGame Rect just needs the co-ordinates. The .rect will need to be updated as the object moves, but we can do that right before the collision test. We added the self.width and self.height so our code is not peppered with meaningless numbers all over the place. Also if the drawing of the sleigh changes, these numbers need be adjusted only in one place.
Anyway, so now for the collision function:
class Schlitten:
...
def collideWith( self, other_rect ):
""" Has the sleigh collided with the other object """
self.rect.x = self.x # update the rect position
self.rect.y = self.y
collision = self.rect.colliderect( other_rect )
return collision
Make a similar change for your Baum class - at least to the point where the code has a baum.rect.
class Baum():
def __init__( self, pos_x, pos_y, pscreen, pschlitten ):
self.x = pos_x
self.y = pos_y
self.width = 50
self.height = 95
self.rect = pygame.Rect( pos_x, pos_y, self.width, self.height )
def bewegung( self ):
self.y += 5
self.rect.y += 5
def spawn( self ):
if self.y > 600:
self.y = -50
self.x = random.randrange(0,700)
self.rect.x = x
self.rect.y = y
This then allows the code to quickly and easily check for collisions:
alles_baumen = [ Baum1, Baum2, Baum3 ] # all tree objects
# main loop
while not exiting:
...
for baum in alles_baumen: # for each tree
baum.draw()
baum.bewegung()
baum.spawn()
if ( speiler1.collideWith( baum.rect ) ): # player hits tree?
speiler1.kollision() # Oh-noes!
Note: The tree-drawing function paints the trees as-if the y co-ordinate is the bottom-left corner. The changes I made do not account for this, so either change the drawing code, or change the position of the Baum.rect to suit this negative-y layout.

Related

python kivy collision detection isn't working

Im trying to create a game with Python's Kivy but my collision detection system isnt working i've tried many different methods on youtube but still no success it either does detect anything or just gives me error messages
def collides(self, player, ball2):
r1x = player.pos[0]
r1y = player.pos[1]
r2x = ball2.pos[0]
r2y = ball2.pos[1]
r1w = player.size[0]
r1h = player.size[1]
r2w = ball2.size[0]
r2h = ball2.size[1]
if r1x < r2x + r2w and r1x + r1w > r2x and r1y < r2y + r2h and r1y + r1h > r2y:
print("True")
return True
else:
return False
print('False')
Your code in collides seems OK but rest of code (in repo) doesn't look good.
I took code from repo and first I made many changes to make it cleaner - I made class Sprite similar to pygame.Sprite
And next I tried use collisions and they work for me.
I keep all balls on list so I can use for-loop to work with all ball. And I can add more balls and it will still works the same. And I can remove ball from list when it is "killed".
I also run all with one schedule_interval. When I click button then I only change speed vx without running another schedule_interval. And this way in update() I can first I make calculation, next I can check collisions and at the end I can move rect on canvas - and this way rect doesn't blik when I have to move it back to previous position (ie. when I detect collision with border).
from kivy.app import App
from kivy.graphics import Ellipse, Rectangle, Color
from kivy.metrics import dp
from kivy.properties import Clock, ObjectProperty, NumericProperty
from kivy.uix.boxlayout import BoxLayout
from kivy.uix.widget import Widget
# How to play the game: Click left and right to move along the 'x' axis to stop click button to move in the opposite
# direction once, to go faster just repeatedly click the direction you want to go BUT there's a catch the faster
# you are going the harder it is to stop sp be carefull. you can teleport to the other side of the screen but only from
# the right side to the left side
# Objective: Dodge all incoming enemies until you reach the next level
# Level layout: Lvl 1: Space invaders type mode Lvl 2: Platform runner type mode Lvl 3: undecided...
# Goal: Make this game playable both on mobile and pc
class Sprite():
def __init__(self, x, y, size, color, vx, vy):
'''Set all values.'''
self.start_x = x
self.start_y = y
self.x = x
self.y = y
self.size = size
self.color = color
self.vx = vx
self.vy = vy
self.rect = None
#self.alive = True
def create_rect(self):
'''Execute it in `with canvas:` in `on_size()`.'''
Color(*self.color)
self.rect = Rectangle(pos=(self.x, self.y), size=(self.size, self.size))
def set_start_pos(self, center_x, center_y):
'''Move to start position.'''
self.x = center_x + self.start_x
self.y = center_y + self.start_y
def move(self):
'''Calculate new position without moving object on `canvas`.'''
self.x += self.vx
self.y += self.vy
def draw(self):
'''Move object on canvas.'''
self.rect.pos = (self.x, self.y)
def check_collision_circle(self, other):
distance = (((self.x-other.x)**2) + ((self.y-other.y)**2)) ** 0.5
#if distance < (self.size + other.size)/2:
# print(True)
# return True
#else:
# return False
return distance < (self.size + other.size)/2:
def check_collision_rect(self, other):
# code `... and ...` gives `True` or `False`
# and it doesn't need `if ...: return True else: return False`
return (
(other.x <= self.x + self.size) and
(self.x <= other.x + other.size) and
(other.y <= self.y + self.size) and
(self.y <= other.y + other.size)
)
class MainCanvas(Widget):
rec_x = NumericProperty(0)
inc = dp(3)
ball_size = dp(35)
my_player = ObjectProperty(Rectangle)
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.player = Sprite(x=-self.ball_size/2, y=145, size=dp(15), vx=dp(0), vy=dp(0), color=(1, .3, .5))
self.balls = [
Sprite(x=0, y=-2000, size=dp(15), vx=dp(0), vy=dp(8), color=(1, 0, 0)),
Sprite(x=100, y=-1000, size=dp(15), vx=dp(0), vy=dp(5), color=(1, 1, 0)),
Sprite(x=-200, y=-1000, size=dp(30), vx=dp(0), vy=dp(5), color=(1, 1, 1)),
Sprite(x=300, y=-600, size=dp(15), vx=dp(0), vy=dp(5), color=(1, 1, 1)),
]
with self.canvas:
for ball in self.balls:
ball.create_rect()
self.player.create_rect()
Clock.schedule_interval(self.update, 1/60)
def on_size(self, *args):
print(f'on_size : {self.width}x{self.height}')
for ball in self.balls:
ball.set_start_pos(self.center_x, self.center_y)
self.player.set_start_pos(self.center_x, self.center_y)
self.rec_x = self.player.x
def update(self, dt):
# all in one function to control if it check collision after move, and draw only after all calculations
# --- moves (without draws) ---
self.player_move(dt)
# move green rectangle below player
self.rec_x = self.player.x
self.ball_move(dt)
# --- collisions (without draws) ---
live_balls = []
for ball in self.balls:
if self.player.check_collision_rect(ball):
#if self.player.check_collision_circle(ball):
print('killed')
# hide
#ball.set_start_pos(self.center_x, self.center_y)
#ball.draw()
# or remove from canvas
self.canvas.remove(ball.rect)
else:
live_balls.append(ball)
self.balls = live_balls
# --- draws ---
self.player.draw()
for ball in self.balls:
ball.draw()
def on_left_click(self):
print('Left Clicked')
self.player.vx -= self.inc
def on_right_click(self):
print('Right Clicked')
self.player.vx += self.inc
def ball_move(self, dt):
for ball in self.balls:
ball.move()
if ball.y + ball.size > self.height:
ball.set_start_pos(self.center_x, self.center_y)
def player_move(self, dt):
self.player.move()
# moving left and stop on screen border
if self.player.vx < 0 and self.player.x < 0:
self.player.x = 0
self.player.vx = 0
# moving right and jump to left side when leave screen
if self.player.vx > 0 and self.width < self.player.x:
self.player.x = 0
class TheFalling(App):
pass
app = TheFalling()
app.run()
#app.stop()
app.root_window.close()

Is there a way to move an object in pygame in random directions smoothly? [duplicate]

This question already has an answer here:
Pygame game help: Easing/Acceleration
(1 answer)
Closed 2 years ago.
I'm trying to make blobs move in a random direction for several frames rather than just once so that it appears less jerky and more smooth, but have been unable to do so. Is there any way to make each object move in the same direction for several ticks before choosing another random direction and doing the same?
My code (most is irrelevant):
import pygame
import random
import numpy as np
WIDTH = 1800
HEIGHT = 1000
BLUE = (15,15,180)
RED = (150,0,0)
class Blob:
def __init__(self, colour, x_boundary, y_boundary, size):
self.colour = colour
self.size = size
self.x_boundary = x_boundary
self.y_boundary = y_boundary
self.x = random.randrange(0, self.x_boundary)
self.y = random.randrange(0, self.y_boundary)
def move(self):
self.x += random.randrange(-6,7)
self.y += random.randrange(-6,7)
def limits(self):
if self.x < 0:
self.x = 0
elif self.x > self.x_boundary:
self.x = self.x_boundary
if self.y < 0:
self.y = 0
elif self.y > self.y_boundary:
self.y = self.y_boundary
def __add__(self, other_blob):
if other_blob.size > self.size:
other_blob.size += int(self.size * 0.5)
self.size = 0
class FastBlob(Blob):
def __init__(self, colour, x_boundary, y_boundary, size):
super().__init__(colour, x_boundary, y_boundary, size)
def move(self):
self.x += random.randrange(-20,21)
self.y += random.randrange(-20,21)
pygame.init()
game_display = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption('Blob world')
clock = pygame.time.Clock()
def is_touching(b1,b2):
return np.linalg.norm(np.array([b1.x,b1.y])-np.array([b2.x,b2.y])) < (b1.size + b2.size)
def handle_collisions(blob_list):
blues, reds, slow_reds = blob_list
for first_blobs in blues, reds, slow_reds:
for first_blob_id, first_blob in first_blobs.copy().items():
for other_blobs in blues, reds, slow_reds:
for other_blob_id, other_blob in other_blobs.copy().items():
if first_blob == other_blob:
pass
else:
if is_touching(first_blob, other_blob):
first_blob + other_blob
return blues, reds, slow_reds
def draw_environment(blob_list):
game_display.fill((210,210,210))
handle_collisions(blob_list)
for blob_dict in blob_list:
for blob_id in blob_dict:
blob = blob_dict[blob_id]
pygame.draw.circle(game_display, blob.colour, [blob.x, blob.y], blob.size)
blob.move()
blob.limits()
pygame.display.update()
def main():
blue_blobs = dict(enumerate([FastBlob(BLUE, WIDTH, HEIGHT, random.randrange(10,15)) for i in range(20)]))
red_blobs = dict(enumerate([FastBlob(RED, WIDTH, HEIGHT, random.randrange(5,10)) for i in range(30)]))
slow_red_blobs = dict(enumerate([Blob(RED, WIDTH, HEIGHT, random.randrange(20,30)) for i in range(5)]))
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
draw_environment([blue_blobs, red_blobs, slow_red_blobs])
clock.tick(7)
if __name__ == '__main__':
main()
Here, I have similar problem in my game, when enemy has to randomly change directions so it is unpredictable to the player.
def goblin_move(): #Goblin auto (random) movement
if goblin.x < 0:
goblin.go_right()
elif goblin.x > 500:
goblin.go_left()
else:
if goblin.x > (rnd_1 * win_width) and goblin.move_flag == -1:
goblin.go_left()
goblin.move_flag = -1
else:
goblin.go_right()
goblin.move_flag = 1
if goblin.x > (rnd_2 * win_width):
goblin.move_flag = -1
def set_random(rnd_1, rnd_2): #Random function generator
rnd_1 = round(random.uniform(0, 0.45), 2)
rnd_2 = round(random.uniform(0.65, 0.95), 2)
return rnd_1, rnd_2
And this is how I set it in the main loop:
if round(pg.time.get_ticks()/1000) % 3 == 0: #Calling random function
(rnd_1, rnd_2) = set_random(rnd_1, rnd_2)
Hope you will find it useful.
Use pygame.math.Vector2 to do the computations. Store the coordinates of the blob to a Vector2 and define a maximum distance (self.maxdist), a velocity (self.speed), a random distance (self.dist) a nd a random direction (self.dir). The random direction is a vector with length 1 (Unit vector) and a random angel (rotate()):
class Blob:
def __init__(self, colour, x_boundary, y_boundary, size):
self.colour = colour
self.size = size
self.x_boundary = x_boundary
self.y_boundary = y_boundary
self.x = random.randrange(0, self.x_boundary)
self.y = random.randrange(0, self.y_boundary)
self.pos = pygame.math.Vector2(self.x, self.y)
self.maxdist = 7
self.speed = 1
self.dist = random.randrange(self.maxdist)
self.dir = pygame.math.Vector2(1, 0).rotate(random.randrange(360))
When the blob moves, then scale the direction by the speed and add it to the position (self.pos += self.dir * self.speed). Decrement the distance (self.dist -= self.speed) and update self.x, self.y by the rounded (round) position. If self.dist falls below 0, the create a new random direction and distance:
class Blob:
# [...]
def move(self):
self.pos += self.dir * self.speed
self.dist -= self.speed
self.x, self.y = round(self.pos.x), round(self.pos.y)
if self.dist <= 0:
self.dist = random.randrange(self.maxdist)
self.dir = pygame.math.Vector2(1, 0).rotate(random.randrange(360))
In the method limit you have to ensure that self.pos is in bounds. Finally you have to update self.x, self.y:
class Blob:
# [...]
def limits(self):
if self.pos.x < 0:
self.pos.x = 0
elif self.pos.x > self.x_boundary:
self.pos.x = self.x_boundary
if self.pos.y < 0:
self.pos.y = 0
elif self.pos.y > self.y_boundary:
self.pos.y = self.y_boundary
self.x, self.y = round(self.pos.x), round(self.pos.y)
The class FastBlob does not need its own move method. It is sufficient do define its own self.maxdist and self.speed:
class FastBlob(Blob):
def __init__(self, colour, x_boundary, y_boundary, size):
super().__init__(colour, x_boundary, y_boundary, size)
self.maxdist = 35
self.speed = 5

Generate enemies around the player from all directions randomly

The enemy are being generated from above the screen and then move toward player in the middle, I want to generate enemies randomly around the screen from all directions but not inside the screen directly and proceed to move towards the player and also enemy sprites are sometimes joining combining and moving together how to repel the enemy sprites.
I have tried changing x,y coordinates of enemy objects using a random range but sometimes they generate objects inside the play screen, I want enemies to generate outside the playing window.
class Mob(pg.sprite.Sprite):
def __init__(self):
pg.sprite.Sprite.__init__(self)
self.image = pg.image.load('enemy.png').convert_alpha()
self.image = pg.transform.smoothscale(pg.image.load('enemy.png'), (33, 33))
self.image_orig = self.image.copy()
self.radius = int(29 * .80 / 2)
self.rect = self.image.get_rect()
self.rect.x = random.randrange(width - self.rect.width)
self.rect.y = random.randrange(-100, -40)
self.speed = 4
self.rot = 0
self.rot_speed = 5
self.last_update = pg.time.get_ticks()
def rotate(self):
now = pg.time.get_ticks()
if now - self.last_update > 50:
self.last_update = now
self.rot = (self.rot + self.rot_speed) % 360
new_image = pg.transform.rotozoom(self.image_orig, self.rot, 1)
old_center = self.rect.center
self.image = new_image
self.rect = self.image.get_rect()
self.rect.center = old_center
def update(self):
self.rotate()
dirvect = pg.math.Vector2(rotator.rect.x - self.rect.x,
rotator.rect.y- self.rect.y)
if dirvect.length_squared() > 0:
dirvect = dirvect.normalize()
# Move along this normalized vector towards the player at current speed.
if dirvect.length_squared() > 0:
dirvect.scale_to_length(self.speed)
self.rect.move_ip(dirvect)
if self.rect.top > height + 10 or self.rect.left < -25 or self.rect.right > width + 20:
self.rect.x = random.randrange(width - self.rect.width)
self.rect.y = random.randrange(-100, -40)
self.speed = random.randrange(1, 4)
[UPDATE]
This the remaining code:
import math
import random
import os
import pygame as pg
import sys
pg.init()
height = 650
width = 1200
os_x = 100
os_y = 45
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (os_x, os_y)
screen = pg.display.set_mode((width, height), pg.NOFRAME)
screen_rect = screen.get_rect()
background = pg.image.load('background.png').convert()
background = pg.transform.smoothscale(pg.image.load('background.png'), (width, height))
clock = pg.time.Clock()
running = True
font_name = pg.font.match_font('Bahnschrift', bold=True)
def draw_text(surf, text, size, x, y, color):
[...]
class Mob(pg.sprite.Sprite):
[...]
class Rotator(pg.sprite.Sprite):
def __init__(self, screen_rect):
pg.sprite.Sprite.__init__(self)
self.screen_rect = screen_rect
self.master_image = pg.image.load('spaceship.png').convert_alpha()
self.master_image = pg.transform.smoothscale(pg.image.load('spaceship.png'), (33, 33))
self.radius = 12
self.image = self.master_image.copy()
self.rect = self.image.get_rect(center=[width / 2, height / 2])
self.delay = 10
self.timer = 0.0
self.angle = 0
self.distance = 0
self.angle_offset = 0
def get_angle(self):
mouse = pg.mouse.get_pos()
offset = (self.rect.centerx - mouse[0], self.rect.centery - mouse[1])
self.angle = math.degrees(math.atan2(*offset)) - self.angle_offset
old_center = self.rect.center
self.image = pg.transform.rotozoom(self.master_image, self.angle, 1)
self.rect = self.image.get_rect(center=old_center)
self.distance = math.sqrt((offset[0] * offset[0]) + (offset[1] * offset[1]))
def update(self):
self.get_angle()
self.display = 'angle:{:.2f} distance:{:.2f}'.format(self.angle, self.distance)
self.dx = 1
self.dy = 1
self.rect.clamp_ip(self.screen_rect)
def draw(self, surf):
surf.blit(self.image, self.rect)
def shoot(self, mousepos):
dx = mousepos[0] - self.rect.centerx
dy = mousepos[1] - self.rect.centery
if abs(dx) > 0 or abs(dy) > 0:
bullet = Bullet(self.rect.centerx, self.rect.centery, dx, dy)
all_sprites.add(bullet)
bullets.add(bullet)
There's not much informations to go by here, but you probably need to check the x and y range your play window has and make sure the random spawn coordinates you generate are outside of it:
In your init:
# These are just example min/max values. Maybe pass these as arguments to your __init__ method.
min_x = min_y = -1000
max_x = max_y = 1000
min_playwindow_x = min_playwindow_y = 500
max_playwindow_x = max_playwindow_y = 600
self.x = (random.randrange(min_x, min_playwindow_x), random.randrange(max_playwindow_x, max_x))[random.randrange(0,2)]
self.y = (random.randrange(min_y, min_playwindow_y), random.randrange(max_playwindow_y, max_y))[random.randrange(0,2)]
This solution should work in basically any setup. For x and y it generates a tuple of values outside the playing window. Then a coinflip decides on the value. This will only spawn mobs that are diagonally outside the playing field, but it will always generate valid random coordinates.
Another approach would be just generating as many random variables as needed to get a valid pair like this:
while min_playingwindow_x <= self.x <= max_playingwindow_x and
min_playingwindow_y <= self.y <= max_playingwindow_y:
# While within screen(undesired) calculate new random positions
self.x = random.randrange(min_x, max_x)
self.y = random.randrange(min_y, max_y)
This can be really slow however if your valid amount of positions is (for example) only 1% of the total positions.
IF you need something really fleshed out, you need to know the corners of both your map and the rectangle that is actually displayed, which is I assume smaller than the entire map(otherwise you cannot spawn enemies outside your view.
(0,0)
+----------------------+
| A |
|-----+-----------+----|
| D | W | B |
|-----+-----------+----|
| C |
+----------------------+(max_x, max_y)
In this diagram W is the window that is acutally visible to the player, and A,B,C,D together are the part of your map that is not currently visible. Since you only want to spawn mobs outside the player's view, you'll need to make sure that the coordinates you generate are inside your map and outside your view:
def generate_coordinates_outside_of_view(map_width=1000, map_height=1000, view_window_top_left=(100, 100),
view_width=600, view_height=400):
"""
A very over the top way to generate coordinates outside surrounding a rectangle within a map almost without bias
:param map_width: width of map in pixels (note that 0,0 on the map is top left)
:param map_height: height of map in pixels
:param view_window_top_left: top left point(2-tuple of ints) of visible part of map
:param view_width: width of view in pixels
:param view_height: height of view in pixels
"""
from random import randrange
# generate 2 samples for each x and y, one guaranteed to be random, and one outside the view for sure.
x = (randrange(0, map_width), (randrange(0, view_window_top_left[0]),
randrange(view_window_top_left[0] + view_width, map_width))[randrange(0, 2)])
y = (randrange(0, map_height), (randrange(0, view_window_top_left[1]),
randrange(view_window_top_left[1] + view_height, map_height))[randrange(0, 2)])
# now we have 4 values. To get a point outside our view we have to return a point where at least 1 of the
# values x/y is guaranteed to be outside the view.
if randrange(0, 2) == 1: # to be almost completely unbiased we randomize the check
selection_x = randrange(0, 2)
selection_y = randrange(0, 2) if selection_x == 1 else 1
else:
selection_y = randrange(0, 2)
selection_x = randrange(0, 2) if selection_y == 1 else 1
return x[selection_x], y[selection_y]
HTH

Sprite moving faster left than right pygame

I think I'm having a rounding problem causing my sprite to move faster/jump farther while moving left.
My sprites update method is calling move, which calls move_single_axis for each axis. Inside this I'm doing some collision detection where I rely on pygame's rect class to both detect the collision, and set the new position.
I think this is the problem but I'm uncertain how to get around the rounding issue because pygame's rect uses integers under the hood.
Here's the update code:
def update(self, dt, game):
self.calc_grav(game, dt)
self.animate(dt, game)
self._old_position = self._position[:]
self.move(dt, game)
self.rect.topleft = self._position
def move(self, dt, game):
# Move each axis separately. Note that this checks for collisions both times.
dx = self.velocity[0]
dy = self.velocity[1]
if dx != 0:
self.move_single_axis(dx, 0, dt)
if dy != 0:
self.move_single_axis(0, dy, dt)
def move_single_axis(self, dx, dy, dt):
#print("hero_destination: ({}, {})".format(dx *dt, dy *dt))
self._position[0] += dx * dt
self._position[1] += dy * dt
#print("Game walls: {}".format(game.walls))
self.rect.topleft = self._position
body_sensor = self.get_body_sensor()
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
#print(" -- Moving right; Hit the left side of the wall")
self.rect.right = wall.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
#print(" -- Moving left; Hit the right side of the wall")
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
if dy > 0: # Moving down; Hit the top side of the wall
#print(" -- Moving down; Hit the top side of the wall")
self.rect.bottom = wall.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
#print(" -- Moving up; Hit the bottom side of the wall")
self.rect.top = wall.rect.bottom
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
Here is the whole source(https://github.com/davidahines/python_sidescroller):
import os.path
import pygame
from pygame.locals import *
from pytmx.util_pygame import load_pygame
import pyscroll
import pyscroll.data
from pyscroll.group import PyscrollGroup
# define configuration variables here
RESOURCES_DIR = 'data'
HERO_JUMP_HEIGHT = 180
HERO_MOVE_SPEED = 200 # pixels per second
GRAVITY = 1000
MAP_FILENAME = 'maps/dungeon_0.tmx'
# simple wrapper to keep the screen resizeable
def init_screen(width, height):
screen = pygame.display.set_mode((width, height), pygame.RESIZABLE)
return screen
# make loading maps a little easier
def get_map(filename):
return os.path.join(RESOURCES_DIR, filename)
# make loading images a little easier
def load_image(filename):
return pygame.image.load(os.path.join(RESOURCES_DIR, filename))
class Hero(pygame.sprite.Sprite):
""" Our Hero
The Hero has three collision rects, one for the whole sprite "rect" and
"old_rect", and another to check collisions with walls, called "feet".
The position list is used because pygame rects are inaccurate for
positioning sprites; because the values they get are 'rounded down'
as integers, the sprite would move faster moving left or up.
Feet is 1/2 as wide as the normal rect, and 8 pixels tall. This size size
allows the top of the sprite to overlap walls. The feet rect is used for
collisions, while the 'rect' rect is used for drawing.
There is also an old_rect that is used to reposition the sprite if it
collides with level walls.
"""
def __init__(self, map_data_object):
pygame.sprite.Sprite.__init__(self)
self.STATE_STANDING = 0
self.STATE_WALKING = 1
self.STATE_JUMPING = 2
self.FRAME_DELAY_STANDING =1
self.FRAME_DELAY_WALKING = 1
self.FRAME_DELAY_JUMPING = 1
self.FACING_RIGHT = 0
self.FACING_LEFT = 1
self.MILLISECONDS_TO_SECONDS = 1000.0
self.COLLISION_BOX_OFFSET = 8
self.time_in_state = 0.0
self.current_walking_frame = 0
self.current_standing_frame = 0
self.current_jumping_frame = 0
self.load_sprites()
self.velocity = [0, 0]
self.state = self.STATE_STANDING
self.facing = self.FACING_RIGHT
self._position = [map_data_object.x, map_data_object.y]
self._old_position = self.position
self.rect = pygame.Rect(8, 0, self.image.get_rect().width - 8, self.image.get_rect().height)
def set_state(self, state):
if self.state != state:
self.state = state
self.time_in_state = 0.0
def load_sprites(self):
self.spritesheet = Spritesheet('data/art/platformer_template_g.png')
standing_images = self.spritesheet.images_at((
pygame.Rect(0, 0, 32, 32),
), colorkey= (0,255,81))
self.standing_images = []
for standing_image in standing_images:
self.standing_images.append(standing_image.convert_alpha())
self.image = self.standing_images[self.current_standing_frame]
#property
def position(self):
return list(self._position)
#position.setter
def position(self, value):
self._position = list(value)
def get_floor_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1]+2, self.rect.width -self.COLLISION_BOX_OFFSET, self.rect.height)
def get_ceiling_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1]-self.rect.height, self.rect.width, 2)
def get_body_sensor(self):
return pygame.Rect(self.position[0]+self.COLLISION_BOX_OFFSET, self.position[1], self.rect.width -self.COLLISION_BOX_OFFSET, self.rect.height)
def calc_grav(self, game, dt):
""" Calculate effect of gravity. """
floor_sensor = self.get_floor_sensor()
collidelist = floor_sensor.collidelist(game.walls)
hero_is_airborne = collidelist == -1
if hero_is_airborne:
if self.velocity[1] == 0:
self.velocity[1] = GRAVITY * dt
else:
self.velocity[1] += GRAVITY * dt
def update(self, dt, game):
self.calc_grav(game, dt)
self._old_position = self._position[:]
self.move(dt, game)
def move(self, dt, game):
# Move each axis separately. Note that this checks for collisions both times.
dx = self.velocity[0]
dy = self.velocity[1]
if dx != 0:
self.move_single_axis(dx, 0, dt)
if dy != 0:
self.move_single_axis(0, dy, dt)
self.rect.topleft = self._position
def move_single_axis(self, dx, dy, dt):
#print("hero_destination: ({}, {})".format(dx *dt, dy *dt))
self._position[0] += dx * dt
self._position[1] += dy * dt
#print("Game walls: {}".format(game.walls))
self.rect.topleft = self._position
body_sensor = self.get_body_sensor()
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
self.rect.right = wall.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
if dy > 0: # Moving down; Hit the top side of the wall
self.rect.bottom = wall.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
self.rect.top = wall.rect.bottom
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
class Wall(pygame.sprite.Sprite):
"""
A sprite extension for all the walls in the game
"""
def __init__(self, map_data_object):
pygame.sprite.Sprite.__init__(self)
self._position = [map_data_object.x, map_data_object.y]
self.rect = pygame.Rect(
map_data_object.x, map_data_object.y,
map_data_object.width, map_data_object.height)
#property
def position(self):
return list(self._position)
#position.setter
def position(self, value):
self._position = list(value)
class Spritesheet(object):
def __init__(self, filename):
try:
self.sheet = pygame.image.load(filename).convert()
except pygame.error:
print ('Unable to load spritesheet image: {}').format(filename)
raise SystemExit
# Load a specific image from a specific rectangle
def image_at(self, rectangle, colorkey = None):
"Loads image from x,y,x+offset,y+offset"
rect = pygame.Rect(rectangle)
image = pygame.Surface(rect.size).convert()
image.blit(self.sheet, (0, 0), rect)
if colorkey is not None:
if colorkey is -1:
colorkey = image.get_at((0,0))
image.set_colorkey(colorkey, pygame.RLEACCEL)
return image
# Load a whole bunch of images and return them as a list
def images_at(self, rects, colorkey = None):
"Loads multiple images, supply a list of coordinates"
return [self.image_at(rect, colorkey) for rect in rects]
class QuestGame(object):
""" This class is a basic game.
It also reads input and moves the Hero around the map.
Finally, it uses a pyscroll group to render the map and Hero.
This class will load data, create a pyscroll group, a hero object.
"""
filename = get_map(MAP_FILENAME)
def __init__(self):
# true while running
self.running = False
self.debug = False
# load data from pytmx
self.tmx_data = load_pygame(self.filename)
# setup level geometry with simple pygame rects, loaded from pytmx
self.walls = list()
self.npcs = list()
for map_object in self.tmx_data.objects:
if map_object.type == "wall":
self.walls.append(Wall(map_object))
elif map_object.type == "guard":
print("npc load failed: reimplement npc")
#self.npcs.append(Npc(map_object))
elif map_object.type == "hero":
self.hero = Hero(map_object)
# create new data source for pyscroll
map_data = pyscroll.data.TiledMapData(self.tmx_data)
# create new renderer (camera)
self.map_layer = pyscroll.BufferedRenderer(map_data, screen.get_size(), clamp_camera=True, tall_sprites=1)
self.map_layer.zoom = 2
self.group = PyscrollGroup(map_layer=self.map_layer, default_layer=3)
# add our hero to the group
self.group.add(self.hero)
def draw(self, surface):
# center the map/screen on our Hero
self.group.center(self.hero.rect.center)
# draw the map and all sprites
self.group.draw(surface)
if(self.debug):
floor_sensor_rect = self.hero.get_floor_sensor()
ox, oy = self.map_layer.get_center_offset()
new_rect = floor_sensor_rect.move(ox * 2, oy * 2)
pygame.draw.rect(surface, (255,0,0), new_rect)
def handle_input(self, dt):
""" Handle pygame input events
"""
poll = pygame.event.poll
event = poll()
while event:
if event.type == QUIT:
self.running = False
break
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
self.running = False
break
# this will be handled if the window is resized
elif event.type == VIDEORESIZE:
init_screen(event.w, event.h)
self.map_layer.set_size((event.w, event.h))
event = poll()
# using get_pressed is slightly less accurate than testing for events
# but is much easier to use.
pressed = pygame.key.get_pressed()
floor_sensor = self.hero.get_floor_sensor()
floor_collidelist = floor_sensor.collidelist(self.walls)
hero_is_airborne = floor_collidelist == -1
ceiling_sensor = self.hero.get_ceiling_sensor()
ceiling_collidelist = ceiling_sensor.collidelist(self.walls)
hero_touches_ceiling = ceiling_collidelist != -1
if pressed[K_l]:
print("airborne: {}".format(hero_is_airborne))
print("hero position: {}, {}".format(self.hero.position[0], self.hero.position[1]))
print("hero_touches_ceiling: {}".format(hero_touches_ceiling))
print("hero_is_airborne: {}".format(hero_is_airborne))
if hero_is_airborne == False:
#JUMP
if pressed[K_SPACE]:
self.hero.set_state(self.hero.STATE_JUMPING)
# stop the player animation
if pressed[K_LEFT] and pressed[K_RIGHT] == False:
# play the jump left animations
self.hero.velocity[0] = -HERO_MOVE_SPEED
elif pressed[K_RIGHT] and pressed[K_LEFT] == False:
self.hero.velocity[0] = HERO_MOVE_SPEED
self.hero.velocity[1]= -HERO_JUMP_HEIGHT
elif pressed[K_LEFT] and pressed[K_RIGHT] == False:
self.hero.set_state(self.hero.STATE_WALKING)
self.hero.velocity[0] = -HERO_MOVE_SPEED
elif pressed[K_RIGHT] and pressed[K_LEFT] == False:
self.hero.set_state(self.hero.STATE_WALKING)
self.hero.velocity[0] = HERO_MOVE_SPEED
else:
self.hero.state = self.hero.STATE_STANDING
self.hero.velocity[0] = 0
def update(self, dt):
""" Tasks that occur over time should be handled here
"""
self.group.update(dt, self)
def run(self):
""" Run the game loop
"""
clock = pygame.time.Clock()
self.running = True
from collections import deque
times = deque(maxlen=30)
try:
while self.running:
dt = clock.tick(60) / 1000.
times.append(clock.get_fps())
self.handle_input(dt)
self.update(dt)
self.draw(screen)
pygame.display.flip()
except KeyboardInterrupt:
self.running = False
if __name__ == "__main__":
pygame.init()
pygame.font.init()
screen = init_screen(800, 600)
pygame.display.set_caption('Test Game.')
try:
game = QuestGame()
game.run()
except:
pygame.quit()
raise
I ripped out everything except for the hero and the QuestGame class and could see the incorrect movement, so the problem was not caused by pyscroll (unless there are more issues).
The reason for the movement problems is that you set the self._position in the update method of the hero to the topleft coords of the rect.
self._position[0] = self.rect.topleft[0]
self._position[1] = self.rect.topleft[1]
pygame.Rects can only store integers and truncate floats that you assign to them, so you shouldn't use them to update the actual position of the hero. Here's a little demonstration:
>>> pos = 10
>>> rect = pygame.Rect(10, 0, 5, 5)
>>> pos -= 1.4 # Move left.
>>> rect.x = pos
>>> rect
<rect(8, 0, 5, 5)> # Truncated the actual position.
>>> pos = rect.x # Pos is now 8 so we moved 2 pixels.
>>> pos += 1.4 # Move right.
>>> rect.x = pos
>>> rect
<rect(9, 0, 5, 5)> # Truncated.
>>> pos = rect.x
>>> pos # Oops, we only moved 1 pixel to the right.
9
The self._position is the exact position and should only be set to one of the rect's coords if the hero collides with a wall or another obstacle (because the rect is used for the collision detection).
Move the two mentioned lines into the if body_sensor.colliderect(wall.rect): clause in the wall collision for loop and it should work correctly.
for wall in game.walls:
if body_sensor.colliderect(wall.rect):
if dx > 0: # Moving right; Hit the left side of the wall
self.rect.right = wall.rect.left
self._position[0] = self.rect.left
if dx < 0: # Moving left; Hit the right side of the wall
self.rect.left = wall.rect.right - self.COLLISION_BOX_OFFSET
self._position[0] = self.rect.left
if dy > 0: # Moving down; Hit the top side of the wall
self.rect.bottom = wall.rect.top
self._position[1] = self.rect.top
if dy < 0: # Moving up; Hit the bottom side of the wall
self.rect.top = wall.rect.bottom
self._position[1] = self.rect.top

Why pygame.draw.circle doesn't work in this code?

I have a ball object that waits one second in the middle of the screen before moving. This is the update method:
def update(self, dt):
now = pygame.time.get_ticks() / 1000
if now - self._spawn_time >= BALL_WAIT_TIME:
self.rect = self.calcnewpos(dt)
self.handle_collision()
else:
step = 255 / FPS
value = int(self._frame * step)
rgb = (value, value, value)
self._draw_ball(rgb)
self._frame += 1
That one second happens below the else clause. My goal is to have the ball image go from black (8, 8, 8) to white (255, 255, 255) in that time but as it is _draw_ball doesn't do anything.
def _draw_ball(self, rgb):
pygame.draw.circle(self.image, rgb, self.rect.center, BALL_RADIUS)
The funny things is, it works the first time when it's called in __init__. I've tried taking lines out of update and testing this code on its own in another module but can't figure out what's the problem. Why is pygame.draw.circle not drawing the the circles in the colors passed by the update method?
Here is the whole class:
#!python3
class Ball(pygame.sprite.Sprite):
def __init__(self, game, velocity):
super(Ball, self).__init__()
self.image = pygame.Surface((BALL_RADIUS*2, BALL_RADIUS*2))
self.image.fill(BLACK)
self.image.set_colorkey(BLACK, RLEACCEL)
self.rect = self.image.get_rect()
screen = pygame.display.get_surface()
self.area = screen.get_rect().inflate(-GAP*2, 0)
self.velocity = velocity
self.game = game
self.start_to_the = random.choice(['left', 'right'])
self._draw_ball(BALL_COLOR)
self.reinit()
def _draw_ball(self, rgb):
pygame.draw.circle(self.image, rgb, self.rect.center, BALL_RADIUS)
def _hit_topbottom(self):
return self.rect.top < self.area.top or self.rect.bottom > self.area.bottom
def _hit_leftright(self):
if self.rect.left < self.area.left: return 'left'
elif self.rect.right > self.area.right: return 'right'
else: return 0
def reinit(self):
self._spawn_time = pygame.time.get_ticks() / 1000
self._frame = 1
if self.start_to_the == 'left':
self.velocity = Vec2D(-BALL_SPEED, 0)
else:
self.velocity = Vec2D(BALL_SPEED, 0)
self.rect.center = self.area.center
def update(self, dt):
now = pygame.time.get_ticks() / 1000
if now - self._spawn_time >= BALL_WAIT_TIME:
self.rect = self.calcnewpos(dt)
self.handle_collision()
else:
step = 255 / FPS
value = int(self._frame * step)
rgb = (value, value, value)
self.image.fill(rgb)
self._frame += 1
def calcnewpos(self, dt):
(dx, dy) = self.velocity.x, self.velocity.y
return self.rect.move(dx, dy)
def handle_collision(self):
(dx, dy) = self.velocity.x, self.velocity.y
if self._hit_topbottom():
dy = -dy
elif self._hit_leftright():
side = self._hit_leftright()
self.game.enemy.update_hitpos()
self.game.increase_score(side)
if side == 'left': self.start_to_the = 'right'
elif side == 'right': self.start_to_the = 'left'
self.reinit()
return
else:
if self.hit_paddle():
paddle = self.hit_paddle()
paddle.handle_collision()
if paddle == self.game.paddles['left']:
self.rect.left = GAP + PADDLE_WIDTH
elif paddle == self.game.paddles['right']:
self.rect.right = SCREEN_WIDTH - (GAP + PADDLE_WIDTH)
dx = -dx
dy = (self.rect.centery - paddle.rect.centery)
dy = (math.copysign(min(abs(dy) // 16 * 16, 32), dy)) / 4
paddle.handle_collision()
self.velocity = Vec2D(dx, dy)
def hit_paddle(self):
paddles = self.game.paddles.values()
for paddle in paddles:
if self.rect.colliderect(paddle.rect): return paddle
I don't see any calls to pygame.display.flip. This is the function responsible for updating the screen with the current state of your display surface. It also doesn't look like you are redrawing your ball on the display surface. Somewhere, probably in update or _draw_ball there should be calls like the following:
self.screen.draw(self.image, self.rect)
pygame.display.flip()
The first line draws the image of the ball to the surface representing the screen, and the second call updates the screen to reflect the new surface.
My second theory is that you are drawing new frames of the ball outside of the bounds of self.image. This theory comes from seeing that are moving the ball's rect according to velocity, but always drawing a circle on self.image at self.rect's center. The size of self.image is only BALL_RADIUS*2, which makes it easy to draw outside of it if self.rect's topleft becomes something that's not (0,0). Even if this isn't your problem now, it will be later.
in pygame the draw circle statement is :
pygame.draw.circle (SURFACE, COLOUR, (X, Y), SIZE, 0)
if you put your screen.fill statement after the circle statement then it will draw the circle and immediately cover it up with the colour of the screen, making your circle disappear a 10000th of a second after its drawn.

Categories