Lennard Jones interaction between particles. Particles moving to one point - python

import numpy as np
import random
import pygame
background_colour = (255,255,255)
width, height = 300, 325
eps = 1
sigma = 1
dt = 0.05
class Particle():
def __init__(self):
self.x = random.uniform(0,400)
self.y = random.uniform(0,500)
self.vx = random.uniform(-.1,.1)
self.vy = random.uniform(-.1,.1)
self.fx = 0
self.fy = 0
self.m = 1
self.size = 10
self.colour = (0, 0, 255)
self.thickness = 0
def bounce(self):
if self.x > width - self.size:
self.x = 2*(width - self.size) - self.x
elif self.x < self.size:
self.x = 2*self.size - self.x
if self.y > height - self.size:
self.y = 2*(height - self.size) - self.y
elif self.y < self.size:
self.y = 2*self.size - self.y
def getForce(self, p2):
dx = self.x - p2.x
dy = self.y - p2.y
self.fx = 500*(-8*eps*((3*sigma**6*dx/(dx**2+dy**2)**4 - 6*sigma**12*dx/(dx**2+dy**2)**7)))
self.fy = 500*(-8*eps*((3*sigma**6*dy/(dx**2+dy**2)**4 - 6*sigma**12*dy/(dx**2+dy**2)**7)))
return self.fx, self.fy
def verletUpdate(self,dt):
self.x = self.x + dt*self.vx+0.5*dt**2*self.fx/self.m
self.y = self.y + dt*self.vy+0.5*dt**2*self.fy/self.m
def display(self):
pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)
screen = pygame.display.set_mode((width, height))
screen.fill(background_colour)
partList = []
for k in range(10):
partList.append(Particle())
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen.fill(background_colour)
for k, particle in enumerate(partList):
for p2 in partList[k+1:]:
particle.getForce(p2)
particle.verletUpdate(dt)
particle.bounce()
particle.display()
pygame.display.flip()
pygame.quit()
Is my code correct? I tried to simulate particles in 2D move with Lennard Jones forces. I think calculating forces works okay but why my particles are moving to one point? Ocasionally I also get error OverflowError: Python int too large to convert to C long Any advice would be useful.

I can not comment on the physics of the simulation, but as far as the display is concerned following are my observations:
Your particles move to one point because the update condition for the x and y parameter in your code in verletUpdate are slowly moving to values beyond the display area. Also to values out of the range of the int() function which is causing your error. You can see this with the statement:
def verletUpdate(self,dt):
self.x = self.x + dt*self.vx+0.5*dt**2*self.fx/self.m
self.y = self.y + dt*self.vy+0.5*dt**2*self.fy/self.m
print self.x
print self.y
Sample Output:
290.034892392
9.98686293664
290.028208837
9.99352484332
-2.55451579742e+19
1.12437640586e+19
Also they saturate and with iterations, the update gets smaller and smaller:
def display(self):
print ' %s + %s '%(self.x,self.y)
pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)
Output:
10.0009120033 + 10.0042647307
10.0009163718 + 10.0000322065
10.0009120033 + 10.0042647307
10.0009163718 + 10.0000322065
...
10.0009163718 + 10.0000322065
10.0009120033 + 10.0042647307
10.0009163718 + 10.0000322065
This is also why your bounce functions and your limit checking is not working. And after a lot of iterations on occasion your self.x and self.y are far exceeding the limits of int().
The code seems fine, but you can get rid of the overflow error by adding some checks above the draw line. For instance I initialized them randomly again to simulate a particle going off screen and us tracking a new one. Feel free to change it.
def display(self):
if(self.x<0 or self.x>height):
self.__init__()
print "reset"
if(self.y<0 or self.y>width):
self.__init__()
print "reset"
print ' %s + %s '%(self.x,self.y)
pygame.draw.circle(screen, self.colour, (int(self.x), int(self.y)), self.size, self.thickness)
Also at one point you adress the array as [k+1:], and addressing the zero element caused a divide by zero error. You might want to look at that.

Related

How to calculate distance from a player to a dynamic collision point

I'm trying to create sensors for a car to keep track of the distances from the car to the borders of the track. My goal is to have 5 sensors (see image below) and use them to train a machine learning algorithm.
But I can't figure out a way to calculate these distances. For now, I just need a sample of code and a logical explanation of how to implement this with PyGame. But a mathematical and geometrical explanation would be really nice as well for further reading. I'm using this code from a YouTuber tutorial series.
My biggest issue is how to get the points in blue. (last picture) I need them to create the red lines from the car to the points and to calculate the length of these lines. These points are taking the car's position and rotation into account and they have a specific angle at which they get out of the car. I've managed to create the lines, but could not get the point the line would collide with the track.
What I want to accomplish:
I've tried different approaches to this problem, but for now, my biggest problem is how to get the position of the blue dots:
--- Edit from the feedback ------
I added a new paragraph to better explain the problem. This way I hope it is clearer why this problem is different from those said to be related to it. The other problem we have the desired final position (mouse or enemy) in this one we have to figure out which point is the one we are going to use to create the line, and this is my issue.
My GitHub repo of the project
https://github.com/pedromello/ml-pygame/blob/main/main.py
The part of the code where I'm trying to implement this:
class AbstractCar:
def __init__(self, max_vel, rotation_vel):
self.img = self.IMG
self.max_vel = max_vel
self.vel = 0
self.rotation_vel = rotation_vel
self.angle = 0
self.x, self.y = self.START_POS
self.acceleration = 0.1
def rotate(self, left=False, right=False):
if left:
self.angle += self.rotation_vel
elif right:
self.angle -= self.rotation_vel
def draw(self, win):
blit_rotate_center(win, self.img, (self.x, self.y), self.angle)
def move_forward(self):
self.vel = min(self.vel + self.acceleration, self.max_vel)
self.move()
def move_backward(self):
self.vel = max(self.vel - self.acceleration, -self.max_vel/2)
self.move()
def move(self):
radians = math.radians(self.angle)
vertical = math.cos(radians) * self.vel
horizontal = math.sin(radians) * self.vel
self.y -= vertical
self.x -= horizontal
def collide(self, mask, x=0, y=0):
car_mask = pygame.mask.from_surface(self.img)
offset = (int(self.x - x), int(self.y - y))
poi = mask.overlap(car_mask, offset)
return poi
def reset(self):
self.x, self.y = self.START_POS
self.angle = 0
self.vel = 0
class PlayerCar(AbstractCar):
IMG = RED_CAR
START_POS = (180, 200)
def reduce_speed(self):
self.vel = max(self.vel - self.acceleration / 2, 0)
self.move()
def bounce(self):
self.vel = -self.vel
self.move()
def drawSensors(self):
radians = math.radians(self.angle)
vertical = -math.cos(radians)
horizontal = math.sin(radians)
car_center = pygame.math.Vector2(self.x + CAR_WIDTH/2, self.y + CAR_HEIGHT/2)
pivot_sensor = pygame.math.Vector2(car_center.x + horizontal * -100, car_center.y - vertical * -100)
#sensor1 = Vector(30, 0).rotate(self.angle) #+ self.pos # atualiza a posição do sensor 1
#sensor2 = Vector(30, 0).rotate((self.angle+30)%360) #+ self.pos # atualiza a posição do sensor 2
#sensor3 = Vector(30, 0).rotate((self.angle-30)%360) #+ self.pos # atualiza a posição do sensor 3
#rotate pivot sensor around car center
sensor_2 = pivot_sensor.rotate((self.angle+30)%360)
# Sensor 1
pygame.draw.line(WIN, (255, 0, 0), car_center, pivot_sensor, 2)
# Sensor 2
pygame.draw.line(WIN, (255, 0, 0), car_center, sensor_2, 2)
# Sensor 3
#pygame.draw.line(WIN, (255, 0, 0), (self.x, self.y), (self.x + horizontal * 100, self.y - vertical * 100), 2)
Thank you for the comments, I solved my problem using the idea of firing sensors so I can get the point on the wall when the "bullet" hits it.
As we can see when the bullet hits the wall we can create a line that connects the point to the car. This is not the best solution, as it takes time for the bullet to hit the wall and in the meantime, the car is "blind".
As Rabbid76 commented, using raycasting may be the solution I was looking for.
Code for reference:
Sensor Bullet class
class SensorBullet:
def __init__(self, car, base_angle, vel, color):
self.x = car.x + CAR_WIDTH/2
self.y = car.y + CAR_HEIGHT/2
self.angle = car.angle
self.base_angle = base_angle
self.vel = vel
self.color = color
self.img = pygame.Surface((4, 4))
self.fired = False
self.hit = False
self.last_poi = None
def draw(self, win):
pygame.draw.circle(win, self.color, (self.x, self.y), 2)
def fire(self, car):
self.angle = car.angle + self.base_angle
self.x = car.x + CAR_WIDTH/2
self.y = car.y + CAR_HEIGHT/2
self.fired = True
self.hit = False
def move(self):
if(self.fired):
radians = math.radians(self.angle)
vertical = math.cos(radians) * self.vel
horizontal = math.sin(radians) * self.vel
self.y -= vertical
self.x -= horizontal
def collide(self, x=0, y=0):
bullet_mask = pygame.mask.from_surface(self.img)
offset = (int(self.x - x), int(self.y - y))
poi = TRACK_BORDER_MASK.overlap(bullet_mask, offset)
if poi:
self.fired = False
self.hit = True
self.last_poi = poi
return poi
def draw_line(self, win, car):
if self.hit:
pygame.draw.line(win, self.color, (car.x + CAR_WIDTH/2, car.y + CAR_HEIGHT/2), (self.x, self.y), 1)
pygame.display.update()
def get_distance_from_poi(self, car):
if self.last_poi is None:
return -1
return math.sqrt((car.x - self.last_poi[0])**2 + (car.y - self.last_poi[1])**2)
Methods the car must perform to use the sensor
# Inside car's __init__ method
self.sensors = [SensorBullet(self, 25, 12, (100, 0, 255)), SensorBullet(self, 10, 12, (200, 0, 255)), SensorBullet(self, 0, 12, (0, 255, 0)), SensorBullet(self, -10, 12, (0, 0, 255)), SensorBullet(self, -25, 12, (0, 0, 255))]
# ------
# Cars methods
def fireSensors(self):
for bullet in self.sensors:
bullet.fire(self)
def sensorControl(self):
#print(contains(self.sensors, lambda x: x.hit))
for bullet in self.sensors:
if not bullet.fired:
bullet.fire(self)
for bullet in self.sensors:
bullet.move()
def get_distance_array(self):
return [bullet.get_distance_from_poi(self) for bullet in self.sensors]

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

pygame particle: issues removing from list

I have Particle and Explosion classes I am drawing using pygame.
An Explosion represents a bunch of flying Particles.
A Particle fades eventually; when its ttl property becomes 0 it should not be visible and should be removed from the Explosion.particles.
I want the program to delete dead particles so that they are not updated.
The issue I am having is with the Explosion.update() method. It seems it's not removing any Particles. I am experiencing a bug where supposedly 'dead' Particles are still drawn but their movement is not updated.I've experimented on lists in python and verified that the premise of iterating through a 'dead' list to remove from the other list works.Any suggestions on where the fault lies in this code would be greatly appreciated.
Edit: I've attached and shortened both source files for better context.
explosion.py
import sys
import pygame
from particleac import *
class App:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode(SCREEN_SIZE, pygame.HWSURFACE|pygame.DOUBLEBUF)
self.clock = pygame.time.Clock()
self.running = True
self.explosions = []
self.frame_no = 0
def check_input(self):
for event in pygame.event.get():
if event.type == pygame.QUIT:
self.running = False
if event.type == pygame.MOUSEBUTTONDOWN or event.type == pygame.MOUSEMOTION:
x, y = pygame.mouse.get_pos()
self.explosions.append(Explosion(x, y))
def update_screen(self):
self.screen.fill(BLACK)
dead_explosions = []
# remove explosion if particles all gone
for e in self.explosions:
if len(e.particles) == 0:
dead_explosions.append(e)
else:
for p in e.particles:
pygame.draw.circle(self.screen, p.colour, [p.x, p.y], p.size)
p.update()
for e in dead_explosions:
self.explosions.remove(e)
pygame.display.flip()
self.frame_no += 1
self.frame_no %= 60
self.clock.tick(FRAMERATE)
def run(self):
while self.running:
self.check_input()
self.update_screen()
pygame.quit()
app = App()
app.run()
particleac.py
import random
import sys
BLACK = [ 0, 0, 0]
WHITE = [255, 255, 255]
BLUE = [0, 0, 255]
SCREEN_WIDTH = 600
SCREEN_HEIGHT = 400
SCREEN_CENTRE = [SCREEN_WIDTH/2, SCREEN_HEIGHT/2]
FRAMERATE = 40
SCREEN_SIZE = [SCREEN_WIDTH, SCREEN_HEIGHT]
GRAVITY = 1
TERMINAL_VELOCITY = 10
class Particle:
def __init__(self, x=SCREEN_CENTRE[0], y=SCREEN_CENTRE[1], colour=WHITE):
self.x = x
self.y = y
self.colour = colour
self.brightness = 255
self.size = 2
self.x_velocity = random.randrange(-4, 4)
self.y_velocity = random.randrange(-8, 3)
self.ttl = random.randrange(50, 200)
self.decay = (self.ttl / FRAMERATE) * 2
def update(self):
if self.ttl > 0:
self.ttl -= 1
self.brightness -= self.decay
if self.brightness < 0:
self.brightness = 0
self.colour[0] = self.colour[1] = self.colour[2] = self.brightness
self.y_velocity += GRAVITY
self.y += self.y_velocity
self.x += self.x_velocity
if self.y > SCREEN_HEIGHT:
self.y -= SCREEN_HEIGHT
class Explosion:
MAX_NUM_PARTICLES = 2
def __init__(self, x=0, y=0, colour=WHITE):
self.x = x
self.y = y
self.colour = colour
self.x_velocity = random.randrange(-4, 4)
self.y_velocity = random.randrange(-6, -1)
self.num_particles = random.randrange(1, self.MAX_NUM_PARTICLES)
self.particles = []
for i in range(self.MAX_NUM_PARTICLES):
p = Particle(self.x, self.y)
p.colour = self.colour
p.x_velocity += self.x_velocity
p.y_velocity += self.y_velocity
self.particles.append(p)
def update(self):
for p in self.particles:
p.update()
self.particles = [p for p in self.particles if p.ttl > 0]
sys.stdout.write("len(self.particles) == {}".format(len(self.particles)))
sys.stdout.flush()
Your code works for me, once sys.out.write() is changed to sys.stdout.write(). Since there is that error in your code, are you sure that the code in your post the same as the code that is failing?
You can simplify Explosion.update() to this:
def update(self):
for p in self.particles:
p.update()
self.particles = [p for p in self.particles if p.ttl > 0]
Possibly this change might fix the problem because you are now dealing with only one list, but I believe that your code should work.
The problem may come from the fact that self.particles is defined at the class level. This may cause some problems elsewhere in your code of you create more than one instance of the Explosion class. Try moving the self.particle in the constructor, and see what happens.
(As a suggestion) Since you build a list anyways, why not copy the ones that are alive?
def update(self):
particles_alive = []
for p in self.particles:
p.update()
sys.out.write("p.ttl == {}\n".format(p.ttl))
if p.ttl == 0:
sys.out.write("Particle died.\n")
else:
particles_alive.append(p)
self.particles = particles_alive
Solution: I was never calling e.update() !Therefore Explosions weren't updating while iterating through them, which meant the dead Particles weren't being removed. Aaargh!I added e.update() to app.update_screen() and removed the incorrect p.update() (which is called for each Particle in the e.update()).
def update_screen(self):
self.screen.fill(BLACK)
dead_explosions = []
for e in self.explosions:
if len(e.particles) == 0:
dead_explosions.append(e)
else:
e.update()
for p in e.particles:
pygame.draw.circle(self.screen, p.colour, [p.x, p.y], p.size)
for e in dead_explosions:
self.explosions.remove(e)
pygame.display.flip()
self.clock.tick(FRAMERATE)
I now know to give better context when posting up code. Thank you kindly for your trouble guys!

pygame bullet physics messed up by scrolling

the code below is the bullet class for my shooter game in pygame. as you can see if you run the full game (https://github.com/hailfire006/economy_game/blob/master/shooter_game.py) the code works great to fire bullets at the cursor as long as the player isn't moving. However, I recently added scrolling, where I change a global offsetx and offsety every time the player gets close to an edge. These offsets are then used to draw each object in their respective draw functions.
unfortunately, my bullet physics in the bullet's init function no longer work as soon as the player scrolls and the offsets are added. Why are the offsets messing up my math and how can I change the code to get the bullets to fire in the right direction?
class Bullet:
def __init__(self,mouse,player):
self.exists = True
centerx = (player.x + player.width/2)
centery = (player.y + player.height/2)
self.x = centerx
self.y = centery
self.launch_point = (self.x,self.y)
self.width = 20
self.height = 20
self.name = "bullet"
self.speed = 5
self.rect = None
self.mouse = mouse
self.dx,self.dy = self.mouse
distance = [self.dx - self.x, self.dy - self.y]
norm = math.sqrt(distance[0] ** 2 + distance[1] ** 2)
direction = [distance[0] / norm, distance[1] / norm]
self.bullet_vector = [direction[0] * self.speed, direction[1] * self.speed]
def move(self):
self.x += self.bullet_vector[0]
self.y += self.bullet_vector[1]
def draw(self):
make_bullet_trail(self,self.launch_point)
self.rect = pygame.Rect((self.x + offsetx,self.y + offsety),(self.width,self.height))
pygame.draw.rect(screen,(255,0,40),self.rect)
You don't take the offset into account when calculating the angle between the player and the mouse. You can fix this by changing the distance like this:
distance = [self.dx - self.x - offsetx, self.dy - self.y - offsety]

Categories