Related
In my pygame-code, I have a drone that is supposed to follow a flight path.
I used pygame.draw.lines to draw lines between specified points. Now, I have a flight path with 10 points where after each point the path angle changes (a bit like a zigzag). The player can move the drone by pressing the keys.
My goal is to print a warning once the drone deviates from the path, e.g. by +/-30. I have been racking my brain for two days but can't come up with a condition to detect a deviation. I just can't figure out how to approach this.
I can determine the drone's x-coordinate at any time but how do I determine the offset from the path? I have attached an image to visualize my problem.
Edit:
As I am a beginner my code is a mess but when copy-pasting it, I guess only the lines 35-91 are interesting. Thank you for any kind of advice in advance!!
import pygame
import pygame.gfxdraw
import random
import sys
import math
pygame.init()
# Define some colors
black = (0,0,0)
white = (255,255,255)
red = (255,0,0)
red_transp = (255,0,0, 150)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
BLUE = (0, 0, 255)
GREEN = (0, 255, 0)
RED = (255, 0, 0)
X = 0
Y = 250
#Display
display_width, display_height = 1200, 700
h_width, h_height = display_width/2, display_height/2
gameDisplay = pygame.display.set_mode((display_width,display_height))
pygame.display.set_caption('Game Display')
#Drone Sprite Image Load Function
droneImg_interim = pygame.image.load('drone.png')
droneImg = pygame.transform.scale(droneImg_interim, [50,50])
drone_width, drone_height = droneImg.get_rect().size
#Create 11 Waypoints with the same coordinates
p1=[X, Y]
p2=[X, Y]
p3=[X, Y]
p4=[X, Y]
p5=[X, Y]
p6=[X, Y]
p7=[X, Y]
p8=[X, Y]
p9=[X, Y]
p10=[X, Y]
p11=[X, Y]
pointlist = [p1, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11]
x_min=drone_width
x_max=100
#Setting new x-coordinate for each point
for i in pointlist:
i[0] = random.randrange(x_min, x_max)
x_min+=250
x_max+=250
#Setting new y-coordinate for each point
for i in range(len(pointlist)):
if i == 0:
pointlist[i][1] = random.randrange(200, 400)
else:
prev = pointlist[i-1][1]
pointlist[i][1] = random.randrange(200, prev+100)
#Plotting pointlist on gameDisplay and connecting dots
def flightpath(pointlist):
pygame.draw.lines(gameDisplay, (255, 0, 0), False, pointlist, 2)
def margin(x):
for i in range(len(pointlist)-1):
p1_x = pointlist[i][0]
p2_x = pointlist[i+1][0]
p1_y = pointlist[i][1]
p2_y = pointlist[i+1][1]
distance_x = p2_x - p1_x
distance = math.sqrt((p2_x-p1_x)**2+(p2_y-p1_y)**2)
halfwaypoint_x = math.sqrt((p2_x - p1_x)**2)/2 + p1_x
halfwaypoint_y = math.sqrt((p2_y - p1_y)**2)/2 + p1_y
if p2_y < p1_y:
angle_rad = math.acos(distance_x/distance)
elif p2_y > p1_y:
angle_rad = 0 - math.acos(distance_x/distance)
angle_deg = math.degrees(angle_rad)
rect_width = distance
rect_height = 60
"""
This part of the code is meant for displaying the margins (the rectangles) around the flight path on the display.
marginSize = (rect_width, rect_height)
surface = pygame.Surface(marginSize, pygame.SRCALPHA)
surface.fill((255,0,0,25))
rotated_surface = pygame.transform.rotate(surface, angle_deg)
#new_rect = rotated_surface.get_rect(center = surface.get_rect(center = ((pointlist[i][0], pointlist[i][1]))).center)
new_rect = rotated_surface.get_rect(center = surface.get_rect(center = ((halfwaypoint_x, halfwaypoint_y))).center)
#gameDisplay.blit(rotated_surface, new_rect)
"""
#Placing drone on the screen
def drone(x,y):
rect = droneImg.get_rect ()
rect.center=(x, y)
gameDisplay.blit(droneImg,rect)
def displayMSG(value,ttext,posx,posy):
myFont = pygame.font.SysFont("Verdana", 12)
Label = myFont.render(ttext, 1, black)
Value = myFont.render(str(value), 1, black)
gameDisplay.blit(Label, (posx, posy))
gameDisplay.blit(Value, (posx + 100, posy))
#Main Loop Object
def game_loop():
global X, Y, FThrustX, FThrustY, FDragY, Time
FThrustY = 0
gameExit = False
while not gameExit:
#Event Checker (Keyboard, Mouse, etc.)
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
sys.exit()
keys = pygame.key.get_pressed() #checking pressed keys
if keys[pygame.K_LEFT]:
X -= 1
if keys[pygame.K_RIGHT]:
X +=1
if keys[pygame.K_DOWN]:
Y += 1
if keys[pygame.K_UP]:
Y -=1
#Display Background Fill
gameDisplay.fill(white)
#Plot flightpath
flightpath(pointlist)
#YS: Determine the position of the mouse
current_pos_x, current_pos_y = pygame.mouse.get_pos()
displayMSG(current_pos_x,'x:',20,665)
displayMSG(current_pos_y,'y:',20,680)
#Plot margin
margin(5)
#Move Drone Object
drone(X,Y)
#Determine the position of the mouse
current_pos_x, current_pos_y = pygame.mouse.get_pos()
#No exceeding of display edge
if X > display_width - drone_width: X = display_width - drone_width
if Y > display_height - drone_height: Y = display_height - drone_height
if X < drone_width: X = drone_width
if Y < drone_height: Y = drone_height
pygame.display.update()
#MAIN
game_loop()
pygame.quit()
sys.exit()
One approach is to find the minimum distance between the center of the drone and the line.
Write the function that calculates the minimum distance from a point to a line segment. To do this, use pygame.math.Vector2 and the Dot product:
def distance_point_linesegment(pt, l1, l2):
LV = pygame.math.Vector2(l2[0] - l1[0], l2[1] - l1[1])
PV = pygame.math.Vector2(pt[0] - l1[0], pt[1]- l1[1])
dotLP = LV.dot(PV)
if dotLP < 0:
return PV.length()
if dotLP > LV.length_squared():
return pygame.math.Vector2(pt[0] - l2[0], pt[1]- l2[1]).length()
NV = pygame.math.Vector2(l1[1] - l2[1], l2[0] - l1[0])
return abs(NV.normalize().dot(PV))
Find the line segment with the shortest distance in a loop:
def minimum_distance(pt, pointlist):
min_dist = -1
for i in range(len(pointlist)-1):
dist = distance_point_linesegment(pt, pointlist[i], pointlist[i+1])
if i == 0 or dist < min_dist:
min_dist = dist
return min_dist
Create an alert when the distance exceeds a certain threshold:
def game_loop():
# [...]
while not gameExit:
# [...]
dist_to_path = minimum_distance((X, Y), pointlist)
if dist_to_path > 25:
pygame.draw.circle(gameDisplay, (255, 0, 0), (X, Y), 25, 4)
drone(X,Y
# [...]
Another possible solution is to use pygame.Rect.clipline and detect the collision of the line segments and the rectangle surrounding the drone:
def intersect_rect(rect, pointlist):
for i in range(len(pointlist)-1):
if rect.clipline(pointlist[i], pointlist[i+1]):
return True
return False
def game_loop():
# [...]
while not gameExit:
# [...]
if not intersect_rect(droneImg.get_rect(center = (X, Y)), pointlist):
pygame.draw.circle(gameDisplay, (255, 0, 0), (X, Y), 25, 4)
drone(X,Y
# [...]
The interesting part of the question is of course finding the nearest point on the desired path to the actual position; distance is easy. The hard part of that is in turn identifying the nearest element (line segment) of the path; projecting onto it is also straightforward.
If the path is simple enough (in particular, if it doesn’t branch and it’s impossible/disallowed to skip sections at a self-intersection), you can finesse that part by just maintaining that current element in a variable and updating it to the previous or next element when the projection onto one of them is closer than the projection onto the current one. This is a typical algorithm used by racing games to determine the instantaneous order of racers.
I try to implement beam collision detection with a predefined track mask in Pygame. My final goal is to give an AI car model vision to see a track it's riding on:
This is my current code where I fire beams to mask and try to find an overlap:
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
# compute beam final point
x_dest = 250 + 500 * math.cos(math.radians(angle))
y_dest = 250 + 500 * math.sin(math.radians(angle))
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
hit = mask.overlap(beam_mask, (pos[0] - 250, pos[1] - 250))
if hit is not None:
pg.draw.line(surface, BLUE, mouse_pos, hit)
pg.draw.circle(surface, GREEN, hit, 3)
surface = pg.display.set_mode((500, 500))
mask_surface = pg.image.load("../assets/mask.png")
mask = pg.mask.from_surface(mask_surface)
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 120, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Let's describe what happens in the code snippet. One by one, I draw beams to beam_surface, make masks from them, and find overlap with background mask defined by one rectangle and a circle (black color in gifs). If there is a "hit point" (overlap point between both masks), I draw it with a line connecting hit point and mouse position.
It works fine for angles <0,90>:
But it's not working for angles in range <90,360>:
Pygame's overlap() documentation tells this:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
This means that this approach will work only if the beam hits the mask approximately from the top left corner. Do you have any advice on how to make it work for all of the situations? Is this generally a good approach to solve this problem?
Your approach works fine, if the x and y component of the ray axis points in the positive direction, but it fails if it points in the negative direction. As you pointed out, that is caused by the way pygame.mask.Mask.overlap works:
Starting at the top left corner it checks bits 0 to W - 1 of the first row ((0, 0) to (W - 1, 0)) then continues to the next row ((0, 1) to (W - 1, 1)). Once this entire column block is checked, it continues to the next one (W to 2 * W - 1).
To make the algorithm work, you have to ensure that the rays point always in the positive direction. Hence if the ray points in the negative x direction, then flip the mask and the ray vertical and if the ray points in the negative y direction than flip the ray horizontal.
Use pygame.transform.flip() top create 4 masks. Not flipped, flipped horizontal, flipped vertical and flipped vertical and horizontal:
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
Determine if the direction of the ray:
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
Get the flipped mask dependent on the direction of the ray:
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
Compute the flipped target point:
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
Compute the flipped offset:
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
Get the nearest intersection point of the flipped ray and mask and unflip the intersection point:
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 500 - hit[0] if flip_x else hit[0]
hy = 500 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, mouse_pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
See the example: repl.it/#Rabbid76/PyGame-PyGame-SurfaceLineMaskIntersect-2
import math
import sys
import pygame as pg
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
pg.init()
beam_surface = pg.Surface((500, 500), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 250 + 500 * abs(c)
y_dest = 250 + 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (250, 250), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 250 - pos[0] if flip_x else pos[0] - 250
offset_y = 250 - pos[1] if flip_y else pos[1] - 250
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
#pg.draw.circle(surface, (255, 255, 0), mouse_pos, 3)
surface = pg.display.set_mode((500, 500))
#mask_surface = pg.image.load("../assets/mask.png")
mask_surface = pg.Surface((500, 500), pg.SRCALPHA)
mask_surface.fill((255, 0, 0))
pg.draw.circle(mask_surface, (0, 0, 0, 0), (250, 250), 100)
pg.draw.rect(mask_surface, (0, 0, 0, 0), (170, 170, 160, 160))
mask = pg.mask.from_surface(mask_surface)
mask_fx = pg.mask.from_surface(pg.transform.flip(mask_surface, True, False))
mask_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, False, True))
mask_fx_fy = pg.mask.from_surface(pg.transform.flip(mask_surface, True, True))
flipped_masks = [[mask, mask_fy], [mask_fx, mask_fx_fy]]
clock = pg.time.Clock()
while True:
for e in pg.event.get():
if e.type == pg.QUIT:
pg.quit()
sys.exit()
mouse_pos = pg.mouse.get_pos()
surface.fill((0, 0, 0))
surface.blit(mask_surface, mask_surface.get_rect())
for angle in range(0, 359, 30):
draw_beam(surface, angle, mouse_pos)
pg.display.update()
clock.tick(30)
Not,the algorithm can be further improved. The ray is always drawn on the bottom right quadrant of the beam_surface. Hence the other 3 quadrants are no longer needed and the size of beam_surface can be reduced to 250x250. The start of the ray is at (0, 0) rather than (250, 250) and the computation of the offsets hast to be slightly adapted:
beam_surface = pg.Surface((250, 250), pg.SRCALPHA)
def draw_beam(surface, angle, pos):
c = math.cos(math.radians(angle))
s = math.sin(math.radians(angle))
flip_x = c < 0
flip_y = s < 0
filpped_mask = flipped_masks[flip_x][flip_y]
# compute beam final point
x_dest = 500 * abs(c)
y_dest = 500 * abs(s)
beam_surface.fill((0, 0, 0, 0))
# draw a single beam to the beam surface based on computed final point
pg.draw.line(beam_surface, BLUE, (0, 0), (x_dest, y_dest))
beam_mask = pg.mask.from_surface(beam_surface)
# find overlap between "global mask" and current beam mask
offset_x = 499-pos[0] if flip_x else pos[0]
offset_y = 499-pos[1] if flip_y else pos[1]
hit = filpped_mask.overlap(beam_mask, (offset_x, offset_y))
if hit is not None and (hit[0] != pos[0] or hit[1] != pos[1]):
hx = 499 - hit[0] if flip_x else hit[0]
hy = 499 - hit[1] if flip_y else hit[1]
hit_pos = (hx, hy)
pg.draw.line(surface, BLUE, pos, hit_pos)
pg.draw.circle(surface, GREEN, hit_pos, 3)
I am creating a randomly generated map in with PyGame. However, I've run into an issue, where, if the user scrolls away from the top-left corner of the map and changes the PyGame surface that's displayed, an issue happens.
The problem is, PyGame still starts them on the upper-left of the surface, and will then allow them to scroll off the edges of the surface because the list that keeps track of that, camera_pos, now has incorrect values.
All of the surfaces are the same dimensions and I want to make it so the user is in the same position when they change the displayed surface. However, I'm not sure how to set the position of the user's view when pygame switches surfaces.
How can I switch the position of the user's view back to what it used to be when the surface is switched?
I have made a MCV Example below I hope will help. Instead of displaying maps, it just draws a border around a solid color. I apologize for how long it is. I'm not sure how to make it much shorter.
In this example, scrolling is done with the arrow keys. You can press r, g, or b on the keyboard to display the different colored surfaces.
import pygame
import numpy as np
import sys
def scroll_y(display_surface, offset):
"""
Handles vertical scrolling.
:param display_surface: A pyGame surface object.
:param offset: The speed of the scroll
"""
width, height = display_surface.get_size()
map_copy = display_surface.copy()
display_surface.blit(map_copy, (0, offset))
# handle scrolling down
if offset < 0:
display_surface.blit(map_copy,
(0, height + offset),
(0, 0, width, -offset))
# handle scrolling up
else:
display_surface.blit(map_copy,
(0, 0),
(0, height - offset, width, offset))
def scroll_x(display_surface, offset):
"""
Handles horizontal scrolling.
:param display_surface: A pyGame surface object.
:param offset: The speed of the scroll
"""
width, height = display_surface.get_size()
map_copy = display_surface.copy()
display_surface.blit(map_copy, (offset, 0))
# handle scrolling right
if offset < 0:
display_surface.blit(map_copy,
(width + offset, 0),
(0, 0, -offset, height))
# handle scrolling left
else:
display_surface.blit(map_copy,
(0, 0),
(width - offset, 0, offset, height))
def main():
"""
This function displays the three surfaces.
Press r to show the red surface (which is displayed by default).
Press g to show the green surface.
Press b to show the blue surface.
"""
pygame.init()
window = pygame.display.set_mode((1600, 900))
red_surface = pygame.Surface([3200, 1800]).convert(window)
green_surface = pygame.Surface([3200, 1800]).convert(window)
blue_surface = pygame.Surface([3200, 1800]).convert(window)
red_surface.fill((255, 145, 145))
green_surface.fill((145, 255, 145))
blue_surface.fill((145, 145, 255))
# draw thick black lines on surface borders
pygame.draw.rect(red_surface, (0, 0, 0), (0, 0, 3200, 1800), 40)
pygame.draw.rect(green_surface, (0, 0, 0), (0, 0, 3200, 1800), 40)
pygame.draw.rect(blue_surface, (0, 0, 0), (0, 0, 3200, 1800), 40)
display_surface = red_surface.copy()
camera_pos = np.array([0, 0])
while True: # <-- the pyGame loop
event = pygame.event.poll()
pressed = pygame.key.get_pressed()
# handle closing the window
if event.type == pygame.QUIT:
break
window.blit(display_surface, (0, 0))
# handle switching display modes
if pressed[pygame.K_g]:
display_surface = green_surface
elif pressed[pygame.K_b]:
display_surface = blue_surface
elif pressed[pygame.K_r]:
display_surface = red_surface
# handle scrolling, make sure you can't scroll past the borders
if pressed[pygame.K_UP] and camera_pos[1] > 0:
scroll_y(display_surface, 5)
camera_pos[1] -= 5
elif pressed[pygame.K_DOWN] and camera_pos[1] < (1800 / 2):
scroll_y(display_surface, -5)
camera_pos[1] += 5
elif pressed[pygame.K_LEFT] and camera_pos[0] > 0:
scroll_x(display_surface, 5)
camera_pos[0] -= 5
elif pressed[pygame.K_RIGHT] and camera_pos[0] < (3200 / 2):
scroll_x(display_surface, -5)
camera_pos[0] += 5
# updates what the window displays
pygame.display.update()
pygame.quit()
sys.exit(0)
if __name__ == "__main__":
# runs the pyGame loop
main()
Here's what I think is a fairly elegant solution that doesn't require the two scrolling functions, scroll_x() and scroll_y() you have. Because it was so fast not using them, the main loop was detecting the same scrolling key as being pressed multiple times — necessitating the addition of a pygame.time.Clock to slow the frame-rate down to something reasonable.
Instead of scrolling the display surfaces themselves via those scrolling functions, as your code was doing, this version just updates the current "camera" position, then blits the corresponding region of the current display_surface to the window whenever it's modified. The camera's position is constrained by making sure its x and y components stay within some boundary limit constants — MINX,MINY and MAXX,MAXY — which get computed based the values of some other previously defined constants.
The use of symbolic constants rather than hardcoding literal values multiple places in the code is considered a very good programming practice because it makes changing them easier, since doing so only requires a source code change to be done one place.
import pygame
import sys
def main():
"""
This function displays the three surfaces.
Press r to show the red surface (which is displayed by default).
Press g to show the green surface.
Press b to show the blue surface.
"""
FPS = 60 # Frames per second
SURF_WIDTH, SURF_HEIGHT = 3200, 1800
WIN_WIDTH, WIN_HEIGHT = 1600, 900
DX, DY = 5, 5 # Scroll amounts.
MINX, MAXX = DX, SURF_WIDTH - WIN_WIDTH + DX - 1
MINY, MAXY = DY, SURF_HEIGHT - WIN_HEIGHT + DY - 1
pygame.init()
pygame.font.init()
fonts = pygame.font.get_fonts()
clock = pygame.time.Clock()
window = pygame.display.set_mode((WIN_WIDTH, WIN_HEIGHT))
red_surface = pygame.Surface([SURF_WIDTH, SURF_HEIGHT]).convert(window)
green_surface = pygame.Surface([SURF_WIDTH, SURF_HEIGHT]).convert(window)
blue_surface = pygame.Surface([SURF_WIDTH, SURF_HEIGHT]).convert(window)
red_surface.fill((255, 145, 145))
green_surface.fill((145, 255, 145))
blue_surface.fill((145, 145, 255))
# Draw thick black lines on surface borders
pygame.draw.rect(red_surface, (0, 0, 0), (0, 0, SURF_WIDTH, SURF_HEIGHT), 40)
pygame.draw.rect(green_surface, (0, 0, 0), (0, 0, SURF_WIDTH, SURF_HEIGHT), 40)
pygame.draw.rect(blue_surface, (0, 0, 0), (0, 0, SURF_WIDTH, SURF_HEIGHT), 40)
# Draw label on each of the surfaces for testing. (ADDED)
font = pygame.font.SysFont(None, 35)
rtext = font.render('red surface', True, (255, 0, 0))
textpos = rtext.get_rect(centerx=300, centery=200) # Reused.
red_surface.blit(rtext, textpos)
rtext = font.render('green surface', True, (0, 192, 0))
green_surface.blit(rtext, textpos)
rtext = font.render('blue surface', True, (0, 0, 255))
blue_surface.blit(rtext, textpos)
display_surface = red_surface
camera_pos = pygame.math.Vector2(0, 0)
update_surface = True
while True: # Game loop
if update_surface:
window.blit(display_surface, (0, 0), (camera_pos[0], camera_pos[1],
WIN_WIDTH, WIN_HEIGHT))
update_surface = False
event = pygame.event.poll()
pressed = pygame.key.get_pressed()
# Close window?
if event.type == pygame.QUIT or pressed[pygame.K_ESCAPE]:
break
# Switch display surface?
if pressed[pygame.K_g]:
display_surface = green_surface
update_surface = True
elif pressed[pygame.K_b]:
display_surface = blue_surface
update_surface = True
elif pressed[pygame.K_r]:
display_surface = red_surface
update_surface = True
# Constrain scrolling to within borders
if pressed[pygame.K_LEFT] and camera_pos[0] >= MINX:
camera_pos[0] -= DX
update_surface = True
elif pressed[pygame.K_RIGHT] and camera_pos[0] <= MAXX:
camera_pos[0] += DX
update_surface = True
elif pressed[pygame.K_UP] and camera_pos[1] >= MINY:
camera_pos[1] -= DY
update_surface = True
elif pressed[pygame.K_DOWN] and camera_pos[1] <= MAXY:
camera_pos[1] += DY
update_surface = True
# updates what the window displays
pygame.display.update()
clock.tick(FPS)
pygame.quit()
sys.exit(0)
if __name__ == "__main__":
main() # runs the pyGame loop
Hello I'm a pretty new programmer and I'm trying to make a ball bounce off a 45 degree triangle. Here is my code:
This program makes the ball bounce when it hits the sides of the window, but I don't know how to make it bounce off a triangle.
import pygame # importing the pygame
import sys # importing the system libraries
import time # importing timer
import random
from pygame.locals import * # importing the locals functions from the pygame library set
pygame.init() # the function from pygame that initializes all relevant variable
# setting length and width
width = 500
length = 300
# colour variables
WHITE = (255,255,255)
BLUE = (0,0,255)
# importing ball image
ball = pygame.image.load('ball.png')
ballRect = ball.get_rect()
ballRect.left = 300
ballRect.right = 300
# setting speed
x_speed = 2
y_speed = 2
# setting window size
WINDOW = pygame.display.set_mode((width, length))# setting the size of the window
pygame.display.update()
# loop
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
ballRect = ballRect.move(x_speed,y_speed)
WINDOW.fill(WHITE) # changing screen colour to white
WINDOW.blit(ball,ballRect) # printing the ball to screen
pygame.display.update()
pygame.display.flip()
time.sleep(0.002) # to slow down the speed of bouncing
pygame.display.update()
# if the left side of ballRect is in a position less than 0, or the right side of ballRect is greater than 500
if ballRect.left < 0 or ballRect.right > (width):
x_speed = x_speed * -1
# if the top of ballRect is in a position less than 0, or the bottom of ballRect is greater than the length
elif ballRect.top < 0 or ballRect.bottom > (length):
y_speed = y_speed * -1
pygame.display.update()
I haven't drawn in the triangle because I don't know where to, but I expect the ball to bounce off the triangle like it does when it hits the sides of the window. Any help would be great!
Interesting task. A triangle can be defined by a simple list:
triangle = [(250, 220), (400, 300), (100, 300)]
The triangle can be drawn by pygame.draw.polygon()
pygame.draw.polygon(WINDOW, RED, triangle, 0)
Use pygame.math.Vector2 to define the position and the motion vector of the ball:
ballvec = pygame.math.Vector2(1, 1)
ballpos = pygame.math.Vector2(150, 250)
balldiameter = 64
Create a function, which does the collision detection. The function has to detect if the ball hits a line. If the line is hit, then the motion vector of the ball is reflected on the line.
The line is represented by 2 points (lp0, lp1), which are pygame.math.Vector2 objects.
The position of the ball (pt) and the motion vector (dir) are pygame.math.Vector2 objects, too:
def isect(lp0, lp1, pt, dir, radius):
# direction vector of the line
l_dir = (lp1 - lp0).normalize()
# normal vector to the line
nv = pygame.math.Vector2(-l_dir[1], l_dir[0])
# distance to line
d = (lp0-pt).dot(nv)
# intersection point on endless line
ptX = pt + nv * d
# test if the ball hits the line
if abs(d) > radius or dir.dot(ptX-pt) <= 0:
return dir
if (ptX-lp0).dot(l_dir) < 0 or (ptX-lp1).dot(l_dir) > 0:
return dir
# reflect the direction vector on the line (like a billiard ball)
r_dir = dir.reflect(nv)
return r_dir
Append the window rectangle and the triangle to a list of lines. Ech line is represented by a tuple of 2 pygame.math.Vector2 objects:
# add screen rect
screen_rect = [(0, 0), (0, 300), (500, 300), (500, 0)]
for i in range(len(screen_rect)):
p0, p1 = screen_rect[i], screen_rect[(i+1) % len(screen_rect)]
line_list.append((pygame.math.Vector2(p0[0], p0[1]), pygame.math.Vector2(p1[0], p1[1])))
# add red trianlge
triangle = [(250, 220), (400, 300), (100, 300)]
for i in range(len(triangle)):
p0, p1 = triangle[i], triangle[(i+1) % len(triangle)]
line_list.append((pygame.math.Vector2(p0[0], p0[1]), pygame.math.Vector2(p1[0], p1[1])))
Do the collision detection in a loop, which traverse the lines. If the ball hits a line, then the motion vector is replaced by the reflected motion vector:
for line in line_list:
ballvec = isect(*line, ballpos, ballvec, balldiameter/2)
Finally update the position of the ball an the ball rectangle:
ballpos = ballpos + ballvec
ballRect.x, ballRect.y = ballpos[0]-ballRect.width/2, ballpos[1]-ballRect.height/2
See the example code, where I applied the suggested changes to your original code. My ball image has a size of 64x64. The ball diameter has to be set to this size (balldiameter = 64):
Minimal example
import pygame
pygame.init()
window = pygame.display.set_mode((500, 300))
try:
ball = pygame.image.load("Ball64.png")
except:
ball = pygame.Surface((64, 64), pygame.SRCALPHA)
pygame.draw.circle(ball, (255, 255, 0), (32, 32), 32)
ballvec = pygame.math.Vector2(1.5, 1.5)
ballpos = pygame.math.Vector2(150, 250)
balldiameter = ball.get_width()
def reflect_circle_on_line(lp0, lp1, pt, dir, radius):
l_dir = (lp1 - lp0).normalize() # direction vector of the line
nv = pygame.math.Vector2(-l_dir[1], l_dir[0]) # normal vector to the line
d = (lp0-pt).dot(nv) # distance to line
ptX = pt + nv * d # intersection point on endless line
if (abs(d) > radius or dir.dot(ptX-pt) <= 0 or # test if the ball hits the line
(ptX-lp0).dot(l_dir) < 0 or (ptX-lp1).dot(l_dir) > 0):
return dir
r_dir = dir.reflect(nv) # reflect the direction vector on the line (like a billiard ball)
return r_dir
triangle1 = [(250, 220), (400, 300), (100, 300)]
triangle2 = [(250, 80), (400, 0), (100, 0)]
screen_rect = [(0, 0), (0, window.get_height()), window.get_size(), (window.get_width(), 0)]
line_list = []
for p0, p1 in zip(triangle1, triangle1[1:] + triangle1[:1]):
line_list.append((pygame.math.Vector2(p0), pygame.math.Vector2(p1)))
for p0, p1 in zip(triangle2, triangle2[1:] + triangle2[:1]):
line_list.append((pygame.math.Vector2(p0), pygame.math.Vector2(p1)))
for p0, p1 in zip(screen_rect, screen_rect[1:] + screen_rect[:1]):
line_list.append((pygame.math.Vector2(p0), pygame.math.Vector2(p1)))
clock = pygame.time.Clock()
run = True
while run:
clock.tick(250)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
for line in line_list:
ballvec = reflect_circle_on_line(*line, ballpos, ballvec, balldiameter/2)
ballpos = ballpos + ballvec
window.fill((64, 64, 64))
pygame.draw.polygon(window, (255, 0, 0), triangle1, 0)
pygame.draw.polygon(window, (0, 0, 255), triangle2, 0)
window.blit(ball, (round(ballpos[0]-balldiameter/2), round(ballpos[1]-balldiameter/2)))
pygame.display.flip()
pygame.quit()
Below is the code I wrote.
The object moves along a circular path when I constantly calculate its position and give the coordinates to the obj.rect.x and object.rect.y.
What I need to know is how to rotate the object by something like below.
obj.rect.x += incrementx
obj.rect.y += incrementy
I implemented this in my code bu then the motion becomes anything but circluar.
Please help.
The two images used are here.
http://s5.postimg.org/fs4adqqib/crate_B.png
http://s5.postimg.org/vevjr44ab/plt0.png
import sys, os, pygame
from math import sin,cos,pi, radians
from pygame.locals import *
from standard_object_creator import *
SCREENW = 800
SCREENH = 700
BLUE = (0, 50, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
PURPLE = (145, 0, 100)
YELLOW = (220,220, 0)
pygame.init()
FPSCLOCK = pygame.time.Clock()
FONT1= "data\Cookie-Regular.ttf"
if sys.platform == 'win32' or sys.platform == 'win64':
#os.environ['SDL_VIDEO_CENTERED'] = '2'# center of screen
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (10,30)#top left corner
SCREEN = pygame.display.set_mode((SCREENW, SCREENH))
## self, imagelist, posx, posy, speedx = 0, speedy = 0, value = 0
plat = pygame.image.load("grfx\plt0.png").convert_alpha()
box = pygame.image.load("grfx\crateB.png").convert_alpha()
FPS = 160 # frames per second
platforms = pygame.sprite.Group()
boxes = pygame.sprite.Group()
def maketext(msg,fontsize, colour = YELLOW, font = FONT1):
mafont = pygame.font.Font(font, fontsize)
matext = mafont.render(msg, True, colour)
matext = matext.convert_alpha()
return matext
box = object_factory ([box], 340, 50, 0, 1)
boxes.add(box)
center_x = 450 # x pos in relation to screen width
center_y = 400 # y pos in relation to screen height
radius = 200
angle = -90 #pi / 4 # starting angle 45 degrees
omega = .001 #Angular velocity
for x in xrange(6):
xpos = radius * cos(angle) #+ center_x #Starting position x
ypos = radius * sin(angle) #+ center_x #Startinh position y
obj = object_factory([plat], xpos, ypos)
obj.angle = angle
obj.omega = omega #angula velocity
obj.radius = radius
platforms.add(obj)
angle += 60
mouseposlist = []
all2gether = [platforms, boxes]
while True:
SCREEN.fill(BLACK)
## MOVE THE SPRITE IN A CIRCLE. Each object is placed by varying the step)
for obj in platforms:
obj.angle = obj.angle + obj.omega
## THE CODE BELOW WORKS
obj.rect.x = center_x + (cos(obj.angle) * obj.radius)
obj.rect.y = center_y + (sin(obj.angle) * obj.radius)
## How can I get the same thing to work in this way? by adding the rate of change to the box objects rect.x and rec.t? Why does this not work?
#obj.rect.x += obj.radius * obj.omega * cos(obj.angle)
#obj.rect.y -= obj.radius * obj.omega * sin(obj.angle)
pygame.draw.line(SCREEN, BLUE, (center_x, center_y), (obj.rect.x, obj.rect.y), 2)
for hp in boxes:
hp.rect.x += hp.speedx
hp.rect.y += hp.speedy
hp.move()
hp.collide(platforms)
for thing in all2gether:
thing.update()
thing.draw(SCREEN)
pygame.draw.line(SCREEN, BLUE, (0, SCREENH / 2), (SCREENW, SCREENH / 2), 2)
pygame.draw.line(SCREEN, BLUE, (SCREENW / 2, 0), (SCREENW / 2, SCREENH), 2)
pygame.display.update()
FPSCLOCK.tick(FPS)
##--------------------------------------------------------------
pygame.event.pump()
keys = pygame.key.get_pressed()
for event in pygame.event.get():
if event.type == MOUSEBUTTONDOWN:
if event.button == 1:
pos = pygame.mouse.get_pos()
val = [pos[0], pos[1], 0, 0]
print val
mouseposlist.append(val)
elif event.button == 3 and mouseposlist != []:
mouseposlist.pop(-1)
if event.type == KEYDOWN and event.key == K_ESCAPE:
print mouseposlist
pygame.quit()
sys.exit()
pygame.time.wait(0)
Your solution for moving the sprite in a circle is the time evaluation of the positional equation. You need to calculate the angle as a function of time. x = r * cos (omega * time). your first solution is a loop on time, incrementing omega by the fractional angle that is provided by the angular velocity. To evaluate a position take the amount of time multiplied by the angular velocity....
I manged to solve my problem and would like to share it. The new code is given below.
This works with Python / Pygame
center_of_rotation_x = SCREENW/2
center_of_rotation_y = SCREENH/2
radius = 200
angle = radians(45) #pi/4 # starting angle 45 degrees
omega = 0.1 #Angular velocity
x = center_of_rotation_x + radius * cos(angle) #Starting position x
y = center_of_rotation_y - radius * sin(angle) #Starting position y
SCREEN.blit(star, (x, y)) # Draw current x,y
angle = angle + omega # New angle, we add angular velocity
x = x + radius * omega * cos(angle + pi / 2) # New x
y = y - radius * omega * sin(angle + pi / 2) # New y
The above code works as it is. But when applied as a class it works differently. I will ask that in another question