pygame particle: issues removing from list - python

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!

Related

How to delete a single pygame drawing from the screen?

When the big circle touches the little circles I want the little circle that it touched to disappear from the screen. However, I can't figure out how exactly you delete an individual drawing in pygame. How do I fix this issue? does pygame have this feature built-in?
from pygame import *
import random as rd
import math as m
init()
screen = display.set_mode((800, 600))
p_1_x = 200
p_1_y = 200
p_1_change_x = 0
p_1_change_y = 0
def p_1(x, y):
player_1 = draw.circle(screen, (0, 0, 0), (x, y), 15)
def pick_up(x, y, xx, yy):
distance = m.sqrt(m.pow(xx - x, 2) + m.pow(yy - y, 2))
if distance < 19:
# I think the code to delete should go here
pass
dots = []
locations = []
for i in range(5):
x = rd.randint(100, 700)
y = rd.randint(100, 500)
locations.append((x, y))
while True:
screen.fill((255, 255, 255))
for events in event.get():
if events.type == QUIT:
quit()
if events.type == KEYDOWN:
if events.key == K_RIGHT:
p_1_change_x = 1
if events.key == K_LEFT:
p_1_change_x = -1
if events.key == K_UP:
p_1_change_y += 1
if events.key == K_DOWN:
p_1_change_y -= 1
if events.type == KEYUP:
if events.key == K_RIGHT or K_LEFT or K_UP or K_DOWN:
p_1_change_x = 0
p_1_change_y = 0
p_1_x += p_1_change_x
p_1_y -= p_1_change_y
for i, locate in enumerate(locations):
dot = draw.circle(screen, (0, 0, 0), locate, 5)
dots.append(dot)
for l in enumerate(locate):
pick_up(p_1_x, p_1_y, locate[0], locate[1])
p_1(p_1_x, p_1_y)
display.update()
Your code was so messy and hard to maintain, first I made 2 classes for Balls & Dots.
I detect collision by pygame.Rect.colliderect, first I make 2 rectangle then I check the collision like this:
def pick_up(ball, dot):
ball_rect = Rect( ball.x - ball.SIZE , ball.y - ball.SIZE , ball.SIZE*2, ball.SIZE*2)
dot_rect = Rect( dot.x - dot.SIZE , dot.y - dot.SIZE , dot.SIZE*2, dot.SIZE*2)
if ball_rect.colliderect(dot_rect):
return True
return False
If collision detects I remove it from dots array in the while loop:
for dot in dots:
if pick_up(ball, dot): # if dot in range ball
dots.remove(dot)
dot.draw()
Here is the whole source:
from pygame import *
import random as rd
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
NUMBER_OF_DOTS = 5
class Ball():
SIZE = 15
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self):
draw.circle(screen, (0, 0, 0), (self.x, self.y), Ball.SIZE)
def move(self, vx, vy):
self.x += vx
self.y += vy
class Dot():
SIZE = 5
def __init__(self, x, y):
self.x = x
self.y = y
def draw(self):
draw.circle(screen, (0, 0, 0), (self.x, self.y), Dot.SIZE)
def pick_up(ball, dot):
ball_rect = Rect( ball.x - ball.SIZE , ball.y - ball.SIZE , ball.SIZE*2, ball.SIZE*2)
dot_rect = Rect( dot.x - dot.SIZE , dot.y - dot.SIZE , dot.SIZE*2, dot.SIZE*2)
if ball_rect.colliderect(dot_rect):
return True
return False
init()
screen = display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
dots = []
ball = Ball(200,200)
# generate dots
for i in range(NUMBER_OF_DOTS):
x = rd.randint(100, 700)
y = rd.randint(100, 500)
dots.append(Dot(x,y))
# the main game loop
while True:
screen.fill((255, 255, 255))
keys=key.get_pressed()
for events in event.get():
keys=key.get_pressed()
if events.type == QUIT:
quit()
if keys[K_RIGHT]:
ball.move(+1,0)
if keys[K_LEFT]:
ball.move(-1,0)
if keys[K_UP]:
ball.move(0,-1)
if keys[K_DOWN]:
ball.move(0,+1)
for dot in dots:
dot.draw()
if pick_up(ball, dot):
dots.remove(dot)
ball.draw()
display.update()
time.delay(1) # Speed down
Update1:
PyGame Rectangle Collision
http://www.pygame.org/docs/ref/rect.html#pygame.Rect.colliderect
Update2:
I make a repo in the github and did some changes,
Dots are colorful, new dot gets random color and the ball gets bigger whenever eats a dot.
https://github.com/peymanmajidi/Ball-And-Dots-Game__Pygame
The code should delete it from the locations list so that it's not re-drawn in the future. You clear the screen each frame, so clearing + not-redrawing is "deleting".
Say you modified pick_up() to simply return True or False:
def pick_up(x, y, xx, yy):
result = False
distance = m.sqrt(m.pow(xx - x, 2) + m.pow(yy - y, 2))
if distance < 19:
result = True # It was picked
return result
Then as you iterate through the locations list drawing & checking for being picked, save the index of the picked circles, then remove them from the locations in a second step. Using the 2-step form means you don't have to worry about accidentally skipping items if you delete from the list as you iterate over it.
p_1_x += p_1_change_x
p_1_y -= p_1_change_y
picked_up = [] # empty list to hold "picked" items
for i, locate in enumerate(locations):
dot = draw.circle(screen, (0, 0, 0), locate, 5)
dots.append(dot)
for l in enumerate(locate):
if ( pick_up(p_1_x, p_1_y, locate[0], locate[1]) ):
picked_up.append( i ) # save the index of anything "picked"
# remove any picked-up circles from the list
for index in sorted( picked_up, reverse=True ): # start with the highest index first
print( "Removing circle from location[%d]" % ( index ) ) # DEBUG
del( locations[ index ] )

How to add snake body in pygame [duplicate]

This question already has an answer here:
How do I get the snake to grow and chain the movement of the snake's body?
(1 answer)
Closed 2 years ago.
I'm a fairly newe programmer and this is the first time I develop a game and I wanted to start with something pretty simple, so I chose the snake game. I have coded everything apart from adding the body part when the food is eaten.
import random
import pygame
from pygame import *
import sys
import os
import time
###objects
class snake:
def __init__(self, win):
self.score = 1
self.length = 25
self.width = 25
self.win = win
self.r = random.randint(0,500)
self.vel = 25
self.update = pygame.display.update()
self.right = True
self.left = False
self.up = False
self.down = False
# 0 = right 1 = left 2 = up 3 = down
self.can = [True, False, True, True]
self.keys = pygame.key.get_pressed()
while True:
if self.r % 25 == 0:
break
else:
self.r = random.randint(0,500)
continue
self.x = self.r
self.y = self.r
self.r = random.randint(0,500)
while True:
if self.r % 25 == 0:
break
else:
self.r = random.randint(0,500)
continue
self.a = self.r
self.b = self.r
def move(self, win):
win.fill((0,0,0))
self.keys = pygame.key.get_pressed()
if self.right == True:
self.x += self.vel
if self.left == True:
self.x -= self.vel
if self.up == True:
self.y -= self.vel
if self.down == True:
self.y += self.vel
if self.x > 475:
self.x = 0
if self.x < 0:
self.x = 500
if self.y > 475:
self.y = 0
if self.y < 0:
self.y = 500
if self.keys[pygame.K_RIGHT] and self.can[0] == True:
self.right = True
self.left= False
self.up = False
self.down = False
self.can[1] = False
self.can[0] = True
self.can[2] = True
self.can[3] = True
if self.keys[pygame.K_LEFT] and self.can[1] == True:
self.right = False
self.left = True
self.up = False
self.down = False
self.can[0] = False
self.can[1] = True
self.can[2] = True
self.can[3] = True
if self.keys[pygame.K_UP] and self.can[2] == True:
self.right = False
self.left = False
self.up = True
self.down = False
self.can[3] = False
self.can[0] = True
self.can[1] = True
self.can[2] = True
if self.keys[pygame.K_DOWN] and self.can[3] == True:
self.right = False
self.left = False
self.up = False
self.down = True
self.can[2] = False
self.can[0] = True
self.can[1] = True
self.can[3] = True
self.length = 25 * self.score
self.snake = pygame.draw.rect(self.win, (0,255,0), (self.x, self.y, self.length, self.width))
def food(self, win):
pygame.draw.rect(self.win, (255,0,0), (self.a, self.b,25,25))
if self.a == self.x and self.b == self.y:
self.r = random.randint(0,500)
while True:
if self.r % 25 == 0:
break
else:
self.r = random.randint(0,500)
continue
self.a = self.r
self.b = self.r
self.score += 1
###functions
###main game
##variables
screen = (500,500)
W = 25
L = 25
WHITE = 255,255,255
clock = pygame.time.Clock()
##game
pygame.init()
win = pygame.display.set_mode(screen)
title = pygame.display.set_caption("snake game")
update = pygame.display.update()
snake = snake(win)
run = True
while run:
clock.tick(10)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
snake.move(win)
snake.food(win)
pygame.display.update()
pygame.quit()
I know the code is a bit messy because I wanted to try to implement OOP, since I never used it.
This is also my first time using pygame, so I be doing something wrong.
So far I have made it so that the snake and food spawn in a random location in an invible grid, and when the head of the snake has the same coordinates of the food, the snake becomes longer (I'm just adding 25 pixels to the snake's body, but when it turns, the whole rectangular shaped snake turns). Also, if the snake reaches the edge of the display, the appears from the opposite side.
The comments below might sound harsh, and I've tried to write them in a neutral way simply pointing out facts and state them as they are. If you are truly a new programmer, this is a pretty good project to learn from and you've done quite good to come this far. So keep an mind that these comments are not meant to be mean, but objective and always comes with a proposed solution to make you an even better programmer, not to bash you.
I also won't go into detail in the whole list as a body thing, others have covered it but I'll use it also in this code.
Here's the result, and blow is the code and a bunch of pointers and tips.
Never re-use variables
First of all, never re-use variable names, as you've overwritten and got lucky with snake = snake() which replaces the whole snake class, and can thus never be re-used again, defeating the whole purpose of OOP and classes. But since you only use it once, it accidentally worked out ok this time. Just keep that in mind for future projects.
Single letter variables
Secondly, I would strongly avoid using single-letter variables unless you really know what you're doing and often that's tied to a math equation or something. I'm quite allergic to the whole concept of self.a and self.b as they don't say anything meaningful, and in a few iterations you probably won't have an idea of what they do either. This is common tho when you're moving quickly and you currently have a grasp on your code - but will bite you in the ass sooner or later (will/should give you bad grades in school or won't land you that dream job you're applying for).
Never mix logic in one function
You've also bundled the food into the player object, which is a big no-no. As well as render logic in the movement logic. So I propose a re-work in the shape of even more OOP where food and player are two separate entities and a function for each logical operation (render, move, eat, etc..).
So I restructured it into this logic:
While I'm at it, I also re-worked the movement mechanics a bit, to use less lines and logic to produce the same thing. I also removed all this logic:
self.r = random.randint(0,500)
while True:
if self.r % 25 == 0:
break
else:
self.r = random.randint(0,500)
continue
And replaced it with this, which does the exact same thing, but uses built-ins to produce it. And hopefully the functions/variables are more descriptive than a rogue while loop.
self.r = random.choice(range(0, 500, 25))
And the final result would look something like this:
import random
import pygame
from pygame import *
import sys
import os
import time
# Constants (Used for bitwise operations - https://www.tutorialspoint.com/python/bitwise_operators_example.htm)
UP = 0b0001
DOWN = 0b0010
LEFT = 0b0100
RIGHT = 0b1000
###objects
class Food:
def __init__(self, window, x=None, y=None):
self.window = window
self.width = 25
self.height = 25
self.x, self.y = x, y
if not x or not y: self.new_position()
def draw(self):
pygame.draw.rect(self.window, (255,0,0), (self.x, self.y, 25, 25))
def new_position(self):
self.x, self.y = random.choice(range(0, 500, 25)), random.choice(range(0, 500, 25))
class Snake:
def __init__(self, window):
self.width = 25
self.width = 25
self.height = 25
self.window = window
self.vel = 25
self.update = pygame.display.update()
start_position = random.choice(range(0, 500, 25)), random.choice(range(0, 500, 25))
self.body = [start_position]
self.direction = RIGHT
def move(self, window):
self.keys = pygame.key.get_pressed()
# since key-presses are always 1 or 0, we can multiply each key with their respective value from the
# static map above, LEFT = 4 in binary, so if we multiply 4*1|0 we'll get binary 0100 if it's pressed.
# We can always safely combine 1, 2, 4 and 8 as they will never collide and thus always create a truth map of
# which direction in bitwise friendly representation.
if any((self.keys[pygame.K_UP], self.keys[pygame.K_DOWN], self.keys[pygame.K_LEFT], self.keys[pygame.K_RIGHT])):
self.direction = self.keys[pygame.K_UP]*1 + self.keys[pygame.K_DOWN]*2 + self.keys[pygame.K_LEFT]*4 + self.keys[pygame.K_RIGHT]*8
x, y = self.body[0] # Get the head position, which is always the first in the "history" aka body.
self.body.pop() # Remove the last object from history
# Use modolus to "loop around" when you hit 500 (or the max width/height desired)
# as it will wrap around to 0, try for instance 502 % 500 and it should return "2".
if self.direction & UP:
y = (y - self.vel)%500
elif self.direction & DOWN:
y = (y + self.vel)%500
elif self.direction & LEFT:
x = (x - self.vel)%500
elif self.direction & RIGHT:
x = (x + self.vel)%500 # window.width
self.body.insert(0, (x, y))
def eat(self, food):
x, y = self.body[0] # The head
if x >= food.x and x+self.width <= food.x+food.width:
if y >= food.y and y+self.height <= food.y+food.height:
self.body.append(self.body[-1])
return True
return False
def draw(self):
for x, y in self.body:
pygame.draw.rect(self.window, (0,255,0), (x, y, self.width, self.width))
##variables
clock = pygame.time.Clock()
##game
pygame.init()
window = pygame.display.set_mode((500,500))
pygame.display.set_caption("snake game")
snake = Snake(window)
food = Food(window)
food.new_position()
score = 0
run = True
while run:
clock.tick(10)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.fill((0,0,0)) # Move the render logic OUTSIDE of the player object
snake.move(window)
if snake.eat(food):
score += 1
food.new_position()
snake.draw()
food.draw()
pygame.display.update()
pygame.quit()
draw() now handles all rendering logic within the objects themselves, instead of being entangled in the move().
snake.eat() is now a function that returns True or False based on the snake head (first position in history, aka body) being inside a food object. This function also adds to the body if a eat was successful, perhaps this code should be moved outside as well, but it's one line of code so I skipped on my own rule a bit to keep the code simple.
food.new_position() is a function that simply moves the food to a new position, called when eat() was successful for instance, or if you want to randomly move the food around at a given interval.
move() and finally the move function, which only has one purpose now, and that is to move the snake in a certain direction. It does so by first getting the current head position, then remove the last history item (tail moves with the head) and then adds a new position at the front of the body that is equal to velocity.
The "is inside" logic might look like porridge, but it's quite simple, and the logic is this:
If the snakes head body[0] has it's x greater or equal to the foods x, it means the heads lower upper left corner was at least past or equal to the foods upper left corner. If the heads width (x+width) is less or equal to the foods width, we're at least inside on the X axis. And then we just repeat for the Y axis and that will tell you if the head is inside or outside the boundary of the food.
The movement logic is reworked to make it fractionally faster but also less code and hopefully easier to use once you have a understanding of how it works. I switched to something called bitwise operations. The basic concept is that you can on a "machine level" (bits) do quick operations to determinate if something is true or not with AND operations for instance. To do this, you can compare to bit-sequences and see if at any point two 1 overlap each other, if not, it's False. Here's an overview of the logic used and all the possible combinations of UP, DOWN, LEFT and RIGHT in binary representation:
On a bit level, 1 is simply 0001, 2 would be 0010 and 4 being 0100 and finally 8 being 1000. Knowing this, if we press → (right) we want to convert this into the bit representation that is the static variable RIGHT (1000 in binary). To achieve this, we simply multiply the value pygame gives us when a key is pressed, which is 1. We multiply it by the decimal version of 1000 (RIGHT), which is 8.
So if → is pressed we do 8*1. Which gives us 1000. And we simply repeat this process for all the keys. If we pressed ↑ + → it would result in 1001 because 8*1 + 1*1 and since ← and ↓ weren't pressed, they will become 4*0 and 2*0 resulting in two zeroes at binary positions.
We can then use these binary representations by doing the AND operator shown in the picture above, to determinate if a certain direction was pressed or not, as DOWN will only be True if there's a 1 on the DOWN position, being the second number from the right in this case. Any other binary positional number will result in False in the AND comparitor.
This is quite efficient, and once you get the hang of it - it's pretty useful for other things as well. So it's a good time to learn it in a controlled environment where it hopefully makes sense.
The main thing to take away here (other than what other people have already pointed out, keep the tail in a array/list as a sort of history of positions) is that game objects should be individual objects, and main rendering logic shouldn't be in player objects, only player render specifics should be in the player object (as an example).
And actions such as eat() should be a thing rather than being checked inside the function that handles move(), render() and other things.
And my suggestions are just suggestions. I'm not a game developer by trade, just optimizing things where I can. Hope the concepts come to use or spark an idea or two. Best of luck.
Yo have to mange the body of the snake in a list. Add the current position of the the head at the head of the body list and remove an element at the tail of the list in ever frame.
Add an attribute self.body:
class snake:
def __init__(self, win):
# [...]
self.body = [] # list of body elements
Add the current head to the bode before the head is moved:
class snake:
# [...]
def move(self, win):
# [...]
# move snake
self.body.insert(0, (self.x, self.y))
Remove elements a the end of self.body, as long the length of the snake exceeds the score:
class snake:
# [...]
def move(self, win):
# [...]
# remove element at end
while len(self.body) >= self.score:
del self.body[-1]
Draw the bode of the snake in a loop:
class snake:
# [...]
def move(self, win):
# [...]
# draw smake and body
self.snake = pygame.draw.rect(self.win, (0,255,0), (self.x, self.y, 25, self.width))
for pos in self.body:
pygame.draw.rect(self.win, (0,255,0), (pos[0], pos[1], 25, self.width))
class snake:
class snake:
def __init__(self, win):
self.score = 1
self.length = 25
self.width = 25
self.win = win
self.r = random.randint(0,500)
self.vel = 25
self.update = pygame.display.update()
self.right = True
self.left = False
self.up = False
self.down = False
# 0 = right 1 = left 2 = up 3 = down
self.can = [True, False, True, True]
self.keys = pygame.key.get_pressed()
while True:
if self.r % 25 == 0:
break
else:
self.r = random.randint(0,500)
continue
self.x = self.r
self.y = self.r
self.body = [] # list of body elements
self.r = random.randint(0,500)
while True:
if self.r % 25 == 0:
break
else:
self.r = random.randint(0,500)
continue
self.a = self.r
self.b = self.r
def move(self, win):
win.fill((0,0,0))
self.keys = pygame.key.get_pressed()
# move snake
self.body.insert(0, (self.x, self.y))
if self.right == True:
self.x += self.vel
if self.left == True:
self.x -= self.vel
if self.up == True:
self.y -= self.vel
if self.down == True:
self.y += self.vel
if self.x > 475:
self.x = 0
if self.x < 0:
self.x = 500
if self.y > 475:
self.y = 0
if self.y < 0:
self.y = 500
# remove element at end
while len(self.body) >= self.score:
del self.body[-1]
if self.keys[pygame.K_RIGHT] and self.can[0] == True:
self.right = True
self.left= False
self.up = False
self.down = False
self.can[1] = False
self.can[0] = True
self.can[2] = True
self.can[3] = True
if self.keys[pygame.K_LEFT] and self.can[1] == True:
self.right = False
self.left = True
self.up = False
self.down = False
self.can[0] = False
self.can[1] = True
self.can[2] = True
self.can[3] = True
if self.keys[pygame.K_UP] and self.can[2] == True:
self.right = False
self.left = False
self.up = True
self.down = False
self.can[3] = False
self.can[0] = True
self.can[1] = True
self.can[2] = True
if self.keys[pygame.K_DOWN] and self.can[3] == True:
self.right = False
self.left = False
self.up = False
self.down = True
self.can[2] = False
self.can[0] = True
self.can[1] = True
self.can[3] = True
# draw smake and body
self.snake = pygame.draw.rect(self.win, (0,255,0), (self.x, self.y, 25, self.width))
for pos in self.body:
pygame.draw.rect(self.win, (0,255,0), (pos[0], pos[1], 25, self.width))
def food(self, win):
pygame.draw.rect(self.win, (255,0,0), (self.a, self.b,25,25))
if self.a == self.x and self.b == self.y:
self.r = random.randint(0,500)
while True:
if self.r % 25 == 0:
break
else:
self.r = random.randint(0,500)
continue
self.a = self.r
self.b = self.r
self.score += 1
I would make a body part object and when the snake gets longer you add a body part. The head does the movement and the body parts follow the head.
Each game turn you just move the head then go over all of the body parts starting from the one closest to the head and move them to their parents location. So head moves 1 block, next part moves the previous head location, third part moves to second parts previous location, ...

Putting the scrolling camera in a mini-window in pygame

I'm trying to create a game where the action is shown in a little box within the main screen object, freeing up the surrounding space for text and menus and what-not. Since the map is larger than the allotted window, I coded a basic "camera" that follows the player around. It mostly works, but I'm having trouble "trimming off" the area outside of this window.
Here's the relevant bits of code (EDITED to provide Working Example):
import pygame, os, sys
from pygame.locals import *
pygame.init()
RIGHT = 'RIGHT'
LEFT = 'LEFT'
UP = 'UP'
DOWN = 'DOWN'
class Camera():
def __init__(self, screen, x_ratio = 1, y_ratio = 1, x_offset = 0, y_offset = 0):
self.screen = screen.copy()
self.rec = self.screen.get_rect()
self.rec.width *= x_ratio
self.rec.height *= y_ratio
self.x_offset = x_offset
self.y_offset = y_offset
def get_pos(self):
return (self.x_offset - self.rec.x, self.y_offset - self.rec.y)
def get_window(self):
w = pygame.Rect(self.rec)
w.topleft = (0 - self.rec.x, 0 - self.rec.y)
return w
def move(self, x, y):
"""Move camera into new position"""
self.rec.x = x
self.rec.y = y
def track(self, obj):
while obj.rec.left < self.rec.left:
self.rec.x -= 1
while obj.rec.right > self.rec.right:
self.rec.x += 1
while obj.rec.top < self.rec.top:
self.rec.y -= 1
while obj.rec.bottom > self.rec.bottom:
self.rec.y += 1
class Map:
def __init__(self, width, height):
self.width = width
self.height = height
self.rec = pygame.Rect(0,0,self.width,self.height)
def draw(self, screen):
pygame.draw.rect(screen, (200,200,200), self.rec)
class Obj:
def __init__(self, char, x = 0, y = 0, width = 0, height = 0):
self.width = width
self.height = height
self.rec = pygame.Rect(x, y, width, height)
self.cur_map = None
self.timers = {}
#Dummying in chars for sprites
self.char = char
self.x_dir = 1
self.y_dir = 1
self.speed = 1
self.moving = False
def move(self):
if self.x_dir != 0 or self.y_dir != 0:
new_x = self.rec.x + (self.x_dir*self.speed)
new_y = self.rec.y + (self.y_dir*self.speed)
new_rec = pygame.Rect(new_x, new_y, self.width, self.height)
#Keep movement within bounds of map
while new_rec.left < self.cur_map.rec.left:
new_rec.x += 1
while new_rec.right > self.cur_map.rec.right:
new_rec.x -= 1
while new_rec.top < self.cur_map.rec.top:
new_rec.y += 1
while new_rec.bottom > self.cur_map.rec.bottom:
new_rec.y -= 1
self.rec = new_rec
def set_dir(self, d):
self.x_dir = 0
self.y_dir = 0
if d == LEFT:
self.x_dir = -1
elif d == RIGHT:
self.x_dir = 1
elif d == UP:
self.y_dir = -1
elif d == DOWN:
self.y_dir = 1
def set_moving(self, val = True):
self.moving = val
class Game:
def __init__(self):
self.screen_size = (800, 600)
self.screen = pygame.display.set_mode(self.screen_size)
self.map_screen = self.screen.copy()
self.title = 'RPG'
pygame.display.set_caption(self.title)
self.camera = Camera(self.screen, 0.75, 0.75)#, 10, 75)
self.fps = 80
self.clock = pygame.time.Clock()
self.debug = False
self.bg_color = (255,255,255)
self.text_size = 18
self.text_font = 'Arial'
self.text_style = pygame.font.SysFont(self.text_font, self.text_size)
self.key_binds = {LEFT : [K_LEFT, K_a], RIGHT : [K_RIGHT, K_d], UP : [K_UP, K_w], DOWN : [K_DOWN, K_s],
'interact' : [K_RETURN, K_z], 'inventory' : [K_i, K_SPACE], 'quit' : [K_ESCAPE]}
self.player = Obj('p', 0, 0, 10, self.text_size)
def draw(self, obj):
char = obj.char
self.draw_text(char, obj.rec.x, obj.rec.y, screen = self.map_screen)
def draw_text(self, text, x, y, color = (0,0,0), screen = None):
textobj = self.text_style.render(text, 1, color)
textrect = textobj.get_rect()
textrect.x = x
textrect.y = y
if screen == None:
"""Use default screen"""
self.screen.blit(textobj, textrect)
else:
screen.blit(textobj, textrect)
def play(self):
done = False
cur_map = Map(800, 800)
self.map_screen = pygame.Surface((cur_map.width, cur_map.height))
self.map_screen.fill(self.bg_color)
bg = pygame.Surface((cur_map.width, cur_map.height))
cur_map.draw(bg)
self.player.cur_map = cur_map
while not done:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYDOWN:
if event.key in self.key_binds[LEFT]:
self.player.set_dir(LEFT)
self.player.set_moving()
elif event.key in self.key_binds[RIGHT]:
self.player.set_dir(RIGHT)
self.player.set_moving()
elif event.key in self.key_binds[UP]:
self.player.set_dir(UP)
self.player.set_moving()
elif event.key in self.key_binds[DOWN]:
self.player.set_dir(DOWN)
self.player.set_moving()
elif event.type == KEYUP:
self.player.set_moving(False)
if self.player.moving:
self.player.move()
self.camera.track(self.player)
self.clock.tick()
self.screen.fill(self.bg_color)
self.map_screen.blit(bg, (0,0))
self.draw(self.player)
pygame.draw.rect(self.map_screen, (0,0,0), self.camera.rec, 1)
#self.screen.blit(self.map_screen, (0,0), [0 - self.camera.rec.x, 0 - self.camera.rec.y, self.camera.rec.width, self.camera.rec.height])
self.screen.blit(self.map_screen, self.camera.get_pos(), self.camera.get_window())
pygame.display.flip()
game = Game()
game.play()
Moving the player past past the bounds of the camera's window causes the window to roll up completely and disappear. I tried adjusting the blitting coordinates, as advised earlier, but it seems to only change the direction in which the window rolls up.
From your updated code, the blitting coordinates for self.screen.blit(...) are still changing: self.camera.get_window() changes value because rec.x and rec.y are values referring to the player position within the map. Hence you should define a constant minimap coordinate, this should be the same as the camera offset.
self.screen.blit(self.map_screen, (self.camera.x_offset,self.camera.y_offset), (*self.camera.get_pos(), self.camera.rec.width, self.camera.rec.height))
Change the Camera().get_pos() to:
def get_pos(self):
return (self.rec.x, self.rec.y)
I believe I only changed the self.screen.blit(...) and stopped using or rewrote your Camera functions as you're confusing yourself with all the rec variables.
To illustrate it working amend the Map().draw(screen) to:
def draw(self, screen):
pygame.draw.rect(screen, (200,200,200), self.rec)
pygame.draw.circle(screen, (255, 255, 255), (50, 50), 20, 2)
One tip as well don't draw the entire map at each loop, just the part that will be visible.

Why are my balls sticking together? [closed]

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 4 years ago.
Improve this question
I'm making a clone of Ballz, a mobile game where you have to shoot a whole bunch of balls at blocks that break after multiple hits. It's like BrickBreaker on steroids. I've got it working mostly, but I can't figure out how to shoot the balls one after another. I know from testing that at the time of shooting, the balls are at different places, but immediately after that they occupy the same space.
Oh btw, the way that I'm keeping them separate is by making the balls go further back outside of the screen. So you can imagine it like setting them all up one behind the other, off screen, below the bottom of the player.
Here's my code:
import pygame
import math
import random
from vector import *
backgroundColor = (0, 0, 0)
ballColor = (255, 255, 255)
sizeOfOneBlock = 50.0
realDimension = 600.0
blockNumberInLine = int(realDimension/sizeOfOneBlock)
size = [int(realDimension), int(realDimension)]
# eg. probability(1/3)
def probability(chance):
return random.random() <= chance
def abs(x):
if x>=0:
return x
else:
return -x
# the classes used:
# Block, BlockHandler, Ball, Player
class Block():
def __init__(self, strength, i, j):
self.strength = strength
# i and j are numbers between 0 and blockNumberInLine-1
self.i, self.j = i, j
self.refreshStats()
def refreshStats(self):
self.color = (100, 224, 89)
def display(self, Surface):
pygame.draw.rect(Surface, (0, 0, 255), (self.i*sizeOfOneBlock, self.j*sizeOfOneBlock, sizeOfOneBlock, sizeOfOneBlock), 0)
class BlockHandler():
def __init__(self):
self.blockList = []
self.blockPositions = []
def resetPositionArray(self):
self.blockPositions = []
for block in self.blockList:
self.blockPositions.append([block.i*sizeOfOneBlock, block.j*sizeOfOneBlock])
def addNewLayer(self, gameLevel):
# move every existing block down
for block in self.blockList:
block.j += 1
# add new layer
for i in range(blockNumberInLine):
if probability(1/3):
# gameLevel determines the strength of the block
self.blockList.append(Block(gameLevel, i, 0))
# after all blocks are loaded, do this
self.resetPositionArray()
def displayBlocks(self, Surface):
for block in self.blockList:
block.display(Surface)
class Ball():
def __init__(self, posVector, moveVector):
self.posVector = posVector
self.moveVector = moveVector
self.radius = 2
self.x = int(self.posVector.x)
self.y = int(self.posVector.y)
def move(self):
self.posVector.add(self.moveVector)
self.x = int(self.posVector.x)
self.y = int(self.posVector.y)
def display(self, Surface):
pygame.draw.circle(Surface, ballColor, (self.x, self.y), self.radius)
def changeDirection(self, tuple):
# east
if tuple[0]>0:
self.moveVector.x = abs(self.moveVector.x)
# west
if tuple[0]<0:
self.moveVector.x = -abs(self.moveVector.x)
# south
if tuple[1]>0:
self.moveVector.y = abs(self.moveVector.y)
# north
if tuple[1]<0:
self.moveVector.y = -abs(self.moveVector.y)
def collisionDetect(self, blockX, blockY, blockSize, circleX, circleY, circleRadius):
xDeflect, yDeflect = 0, 0
# if in the same column
if (circleX>=blockX) and (circleX<=(blockX+blockSize)):
# if touching block from above or below
distance = circleY-(blockY+0.5*blockSize)
if abs(distance)<=(0.5*blockSize+circleRadius):
# either 1 or -1
if distance!=0:
yDeflect = distance/abs(distance)
# if in the same row
if (circleY>=blockY) and (circleY<=(blockY+blockSize)):
# if touching block from left or right
distance = circleX-(blockX+0.5*blockSize)
if abs(distance)<=(0.5*blockSize+circleRadius):
if distance!=0:
xDeflect = distance/abs(distance)
return [xDeflect, yDeflect]
def checkForCollisions(self, blockPositions):
# walls
if (self.x<=(0+self.radius)):
# east
self.changeDirection([1,0])
if (self.x>=(realDimension-self.radius)):
# west
self.changeDirection([-1,0])
if (self.y<=(0+self.radius)):
# south
self.changeDirection([0,1])
# blocks
for pos in blockPositions:
collision = self.collisionDetect(pos[0], pos[1], sizeOfOneBlock, self.x, self.y, self.radius)
self.changeDirection(collision)
class Player():
def __init__(self, posVector):
self.posVector = posVector
self.x = int(self.posVector.x)
self.y = int(self.posVector.y)
self.level = 1
self.numberOfBalls = 3
self.balls = []
def resetBalls(self):
self.balls = []
for j in range(self.numberOfBalls):
self.balls.append(Ball(self.posVector, moveVector=Vector(0.0, 0.0)))
# print(ball)
def placeBalls(self, separateVector):
# self.resetBalls()
for j in range(len(self.balls)):
ball = self.balls[j]
for i in range(j):
ball.posVector.subtract(separateVector)
def display(self, Surface):
# possibly change color
pygame.draw.circle(Surface, ballColor, (self.x, self.y), 20)
def displayBalls(self, Surface):
for ball in self.balls:
ball.display(Surface)
def updateBalls(self, blockHandler):
for ball in self.balls:
ball.move()
ball.checkForCollisions(blockPositions=blockHandler.blockPositions)
def main():
pygame.init()
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Ballz")
done = False
clock = pygame.time.Clock()
blockHandler = BlockHandler()
blockHandler.addNewLayer(1)
playerPosition = Vector(realDimension/2, realDimension-10)
player = Player(posVector=playerPosition)
player.resetBalls()
# -------- Main Program Loop -----------
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.KEYDOWN:
# JFF
if event.key == pygame.K_w:
blockHandler.addNewLayer(1)
# for debugging
if event.key == pygame.K_d:
for ball in player.balls:
print(ball.posVector.x, ball.posVector.y)
print(ball.moveVector.x, ball.moveVector.y)
print("")
if event.key == pygame.K_r:
player.resetBalls()
if event.type == pygame.MOUSEBUTTONUP:
mousePos = pygame.mouse.get_pos()
player.shootVector = Vector(mousePos[0]-player.x, mousePos[1]-player.y).shortenTo(1)
for ball in player.balls:
for i in range(player.balls.index(ball)*10):
ball.posVector.subtract(player.shootVector)
ball.moveVector = player.shootVector
# test
print(ball.posVector.x, ball.posVector.y)
print(ball.moveVector.x, ball.moveVector.y)
print("")
# LOGIC
player.updateBalls(blockHandler)
# DRAW
screen.fill(backgroundColor)
blockHandler.displayBlocks(screen)
player.displayBalls(screen)
player.display(screen)
pygame.display.flip()
# 60 frames per second
clock.tick(60)
pygame.quit()
if __name__ == "__main__":
main()
Edit: Forgot to add the vector class.
class Vector():
def __init__(self, x=0, y=0):
self.x, self.y = x, y
def magnitude(self):
return ((self.x)**2 + (self.y)**2)**0.5
def shortenTo(self, radius):
magnitude = self.magnitude()
unitX = self.x/magnitude
unitY = self.y/magnitude
return Vector(unitX*radius, unitY*radius)
def add(self, addedVector):
self.x += addedVector.x
self.y += addedVector.y
def subtract(self, subtractedVector):
self.x -= subtractedVector.x
self.y -= subtractedVector.y
def printCoordinates(self):
print(self.x, self.y)
Sorry, no reproduction, your balls are fine:
No, but the problem you have is with mutable objects.
When you set
ball.moveVector = player.shootVector
you set all moveVector's to the same object, so every collision detection will change the direction of all balls simultaneosly. Simplest fix:
ball.moveVector = player.shootVector + Vector(x=0, y=0)
EDIT
I used a different vector module, in your case you can either use copy.copy or create a custom __add__ method:
def __add__(self, other):
if not isinstance(other, Vector)
raise ValueError
return Vector(self.x+other.x, self.y+other.y)
(This comes inside the Vector class, likewise for subtraction and mult.)
END EDIT
Also there are some problems with the way you reset when the balls leave the image and you should prevent the player from clicking again until the balls are reset, but I guess that comes in later development.
Appendix
Note: I'm working in Python 3 and either I installed a different vector module, or they changed a lot, so had to change some syntax there as well. Hope it helps :)
import pygame
import math
import random
from vector import *
backgroundColor = (0, 0, 0)
ballColor = (255, 255, 255)
sizeOfOneBlock = 50.0
realDimension = 600.0
blockNumberInLine = int(realDimension/sizeOfOneBlock)
size = [int(realDimension), int(realDimension)]
# eg. probability(1/3)
def probability(chance):
return random.random() <= chance
def abs(x):
if x>=0:
return x
else:
return -x
# the classes used:
# Block, BlockHandler, Ball, Player
class Block():
def __init__(self, strength, i, j):
self.strength = strength
# i and j are numbers between 0 and blockNumberInLine-1
self.i, self.j = i, j
self.refreshStats()
def refreshStats(self):
self.color = (100, 224, 89)
def display(self, Surface):
pygame.draw.rect(Surface, (0, 0, 255), (self.i*sizeOfOneBlock, self.j*sizeOfOneBlock, sizeOfOneBlock, sizeOfOneBlock), 0)
class BlockHandler():
def __init__(self):
self.blockList = []
self.blockPositions = []
def resetPositionArray(self):
self.blockPositions = []
for block in self.blockList:
self.blockPositions.append([block.i*sizeOfOneBlock, block.j*sizeOfOneBlock])
def addNewLayer(self, gameLevel):
# move every existing block down
for block in self.blockList:
block.j += 1
# add new layer
for i in range(blockNumberInLine):
if probability(1/3):
# gameLevel determines the strength of the block
self.blockList.append(Block(gameLevel, i, 0))
# after all blocks are loaded, do this
self.resetPositionArray()
def displayBlocks(self, Surface):
for block in self.blockList:
block.display(Surface)
class Ball():
def __init__(self, posVector, moveVector):
self.posVector = posVector
self.moveVector = moveVector
self.radius = 2
self.x = int(self.posVector['x'])
self.y = int(self.posVector['y'])
def move(self):
self.posVector += self.moveVector
self.x = int(self.posVector['x'])
self.y = int(self.posVector['y'])
def display(self, Surface):
pygame.draw.circle(Surface, ballColor, (self.x, self.y), self.radius)
def changeDirection(self, tuple):
# east
if tuple[0]>0:
self.moveVector['x'] = abs(self.moveVector['x'])
# west
if tuple[0]<0:
self.moveVector['x'] = -abs(self.moveVector['x'])
# south
if tuple[1]>0:
self.moveVector['y'] = abs(self.moveVector['y'])
# north
if tuple[1]<0:
self.moveVector['y'] = -abs(self.moveVector['y'])
def collisionDetect(self, blockX, blockY, blockSize, circleX, circleY, circleRadius):
xDeflect, yDeflect = 0, 0
# if in the same column
if (circleX>=blockX) and (circleX<=(blockX+blockSize)):
# if touching block from above or below
distance = circleY-(blockY+0.5*blockSize)
if abs(distance)<=(0.5*blockSize+circleRadius):
# either 1 or -1
if distance!=0:
yDeflect = distance/abs(distance)
# if in the same row
if (circleY>=blockY) and (circleY<=(blockY+blockSize)):
# if touching block from left or right
distance = circleX-(blockX+0.5*blockSize)
if abs(distance)<=(0.5*blockSize+circleRadius):
if distance!=0:
xDeflect = distance/abs(distance)
return [xDeflect, yDeflect]
def checkForCollisions(self, blockPositions):
# walls
if (self.x<=(0+self.radius)):
# east
self.changeDirection([1,0])
if (self.x>=(realDimension-self.radius)):
# west
self.changeDirection([-1,0])
if (self.y<=(0+self.radius)):
# south
self.changeDirection([0,1])
# blocks
for pos in blockPositions:
collision = self.collisionDetect(pos[0], pos[1], sizeOfOneBlock, self.x, self.y, self.radius)
self.changeDirection(collision)
class Player():
def __init__(self, posVector):
self.posVector = posVector
self.x = int(self.posVector['x'])
self.y = int(self.posVector['y'])
self.level = 1
self.numberOfBalls = 3
self.balls = []
def resetBalls(self):
self.balls = []
for j in range(self.numberOfBalls):
x = Vector(x=j, y=j) - Vector(x=j, y=j)
self.balls.append(Ball(self.posVector, x))
# print(ball)
def placeBalls(self, separateVector):
# self.resetBalls()
for j in range(len(self.balls)):
ball = self.balls[j]
for i in range(j):
ball.posVector -= separateVector
def display(self, Surface):
# possibly change color
pygame.draw.circle(Surface, ballColor, (self.x, self.y), 20)
def displayBalls(self, Surface):
for ball in self.balls:
ball.display(Surface)
def updateBalls(self, blockHandler):
for ball in self.balls:
ball.move()
ball.checkForCollisions(blockPositions=blockHandler.blockPositions)
def main():
pygame.init()
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Ballz")
done = False
clock = pygame.time.Clock()
blockHandler = BlockHandler()
blockHandler.addNewLayer(1)
playerPosition = Vector(x=realDimension/2, y=realDimension-10)
player = Player(posVector=playerPosition)
player.resetBalls()
# -------- Main Program Loop -----------
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
if event.type == pygame.KEYDOWN:
# JFF
if event.rrrr == pygame.K_w:
blockHandler.addNewLayer(1)
# for debugging
if event.key == pygame.K_d:
for ball in player.balls:
print(ball.posVector['x'], ball.posVector['y'])
print(ball.moveVector['x'], ball.moveVector['y'])
print("")
if event.key == pygame.K_r:
player.resetBalls()
if event.type == pygame.MOUSEBUTTONUP:
mousePos = pygame.mouse.get_pos()
player.shootVector = Vector(x=mousePos[0]-player.x, y=mousePos[1]-player.y) / ((mousePos[0]-player.x)**2 + (mousePos[1]-player.y))**.5
for ball in player.balls:
for i in range(player.balls.index(ball)*10):
ball.posVector -= player.shootVector
ball.moveVector = player.shootVector + Vector(x=0, y=0)
# test
print(ball.posVector['x'], ball.posVector['y'])
print(ball.moveVector['x'], ball.moveVector['y'])
print("")
# LOGIC
player.updateBalls(blockHandler)
# DRAW
screen.fill(backgroundColor)
blockHandler.displayBlocks(screen)
player.displayBalls(screen)
player.display(screen)
pygame.display.flip()
# 60 frames per second
clock.tick(60)
main()
Since you're passing the player's self.posVector to the ball instances and then just assign it to their posVector attributes, the positions of the player and the balls all refer to the same Vector object in the memory. You can instead make copies of the vector, for example with the copy module, so that every ball.posVector refers to a separate vector object.
First import the copy function (it creates shallow copies).
from copy import copy
Then copy the vector objects before you pass them.
def resetBalls(self):
self.balls = []
for j in range(self.numberOfBalls):
self.balls.append(
Ball(copy(self.posVector), moveVector=Vector(0.0, 0.0)))
# ...
for ball in player.balls:
for i in range(player.balls.index(ball)*10):
ball.posVector.subtract(player.shootVector)
ball.moveVector = copy(player.shootVector)
I also recommend using pygame's Vector2 class instead of your own Vector, because it is more feature-rich and efficient.
And abs is a built-in function.

Trying to delete sprite with kill(), but sprite isn't disappearing

This is my first time asking here, so sorry if I don't ask very well.
Lately I've been trying to make a Terraria-type game where you can break/place blocks in a randomly generated landscape. While trying to implement the breaking-block mechanic (which is triggered by clicking on a block), I ran into an issue. The block no longer became solid (that's a good thing), but the block's image is still there, and I can walk right through it.
A visual example:
Before breaking block vs.
After breaking block
Here's the code
Main file (hopefully only the relevant bits):
# Imports
import pygame as pg
import json
import sys
import random
import os
from settings import *
from world_handler import *
# Initialize pygame
pg.mixer.pre_init()
pg.init()
# Fonts
# NOTE: put fonts in here
# Helper functions
def load_image(file_path):
# Loads an image
img = pg.image.load(file_path)
return img
def play_sound(sound, loops=0, maxtime=0, fade_ms=0):
# Plays some audio
if sound_on:
sound.play(loops, maxtime, fade_ms)
def play_music():
# Plays background music
if sound_on:
pg.mixer.music.play(-1)
# File paths
current_path = os.path.dirname(__file__)
assets_path = os.path.join(current_path, "assets")
image_path = os.path.join(assets_path, "img")
# Images
player_standing = load_image((os.path.join(image_path, "player", "standing", "player-standing.png")))
player_walking1 = load_image((os.path.join(image_path, "player", "walking", "player-walking1.png")))
player_walking2 = load_image((os.path.join(image_path, "player", "walking", "player-walking2.png")))
player_walking3 = load_image((os.path.join(image_path, "player", "walking", "player-walking3.png")))
player_walking4 = load_image((os.path.join(image_path, "player", "walking", "player-walking4.png")))
player_jumping = load_image((os.path.join(image_path, "player", "jumping", "player-jumping.png")))
player_images = {"walking": [player_walking1, player_walking2, player_walking3, player_walking4],
"jumping": player_jumping,
"standing": player_standing}
block_images = {"Grass": load_image((os.path.join(image_path, "blocks", "grass.png"))),
"Dirt": load_image((os.path.join(image_path, "blocks", "dirt.png"))),
"Stone": load_image((os.path.join(image_path, "blocks", "stone.png")))}
cursor_tracker = load_image((os.path.join(image_path, "misc", "clear-single-pixel.png")))
class Entity(pg.sprite.Sprite):
def __init__(self, x, y, image):
# Initialize an entity
super().__init__()
self.image = image
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.vx = 0
self.vy = 0
def apply_gravity(self, world):
# Let the enemy be affected by gravity
self.vy += world.gravity
self.vy = min(self.vy, world.terminal_velocity)
class Block(Entity):
def __init__(self, x, y, image):
# Initialize the block
super().__init__(x, y, image)
class Cursor(Entity):
def __init__(self, x, y, image):
# Initialize the invisible mouse cursor object
# This will be used to track where the mouse goes and if the mouse is on a block
super().__init__(x, y, image)
self.on_block = False
def follow_mouse(self):
# Make object follow the mouse
self.mouse_x, self.mouse_y = pg.mouse.get_pos()
self.rect.x = self.mouse_x
self.rect.y = self.mouse_y
def detect_block_collision(self, world):
# Detects collsion between cursor tracker and a block
hit_list = pg.sprite.spritecollide(self, world.blocks, True)
if len(hit_list) > 0:
pass
def update(self, world):
# Update the cursor object
self.follow_mouse()
world.active_sprites.add(self)
class Player(Entity):
def __init__(self, images):
# Initialize the player
super().__init__(0, 0, images["standing"])
# Images in each direction
self.image_standing_right = images["standing"]
self.image_standing_left = pg.transform.flip(self.image_standing_right, 1, 0)
self.images_walking_right = images["walking"]
self.images_walking_left = [pg.transform.flip(img, 1, 0) for img in self.images_walking_right]
self.image_jumping_right = images["jumping"]
self.image_jumping_left = pg.transform.flip(self.image_jumping_right, 1, 0)
# Player variables
self.running_images = self.images_walking_right
self.image_index = 0
self.steps = 0
self.speed = 3.5
self.jump_power = 12
self.vx = 0
self.vy = 0
self.direction = "right"
self.on_ground = True
self.score = 0
self.health = 100
self.max_health = 100
self.invincibility = 0
def move_left(self):
# Move to the left
self.vx = -self.speed + 0.9
self.direction = "left"
def move_right(self):
# Move to the rightS
self.vx = self.speed
self.direction = "right"
def stop(self):
# Stop it right there
self.vx = 0
def jump(self, blocks):
# Jump up, jump up, and get down
self.rect.y += 1
hit_list = pg.sprite.spritecollide(self, blocks, False)
if len(hit_list) > 0:
self.vy = -1 * self.jump_power
self.rect.y -= 1
def check_world_boundaries(self, world):
# Make sure the player doesn"t walk off the world
if self.rect.left < 0:
self.rect.left = 0
elif self.rect.right > world.width:
self.rect.right = world.width
def move_and_process_blocks(self, blocks):
# Detect block collisions
# Block side collisions
self.rect.x += self.vx
hit_list = pg.sprite.spritecollide(self, blocks, False)
for block in hit_list:
if self.vx > 0:
self.rect.right = block.rect.left
self.vx = 0
elif self.vx < 0:
self.rect.left = block.rect.right
self.vx = 0
self.on_ground = False
# Block top and bottom collisions
self.rect.y += self.vy + 1 # The +1 isn"t necessary, but it helps
hit_list = pg.sprite.spritecollide(self, blocks, False)
for block in hit_list:
if self.vy > 0:
self.rect.bottom = block.rect.top
self.on_ground = True
self.vy = 0
elif self.vy < 0:
self.rect.top = block.rect.bottom
self.on_ground = True
self.vy = 0
def set_image(self):
# Set images and animate
if self.on_ground:
if self.vx != 0:
if self.direction == "right":
self.walking_images = self.images_walking_right
elif self.direction == "left":
self.walking_images = self.images_walking_left
self.steps = (self.steps + 1) % self.speed
if self.steps == 0:
self.image_index = (self.image_index + 1) % len(self.walking_images)
self.image = self.walking_images[self.image_index]
else:
if self.direction == "right":
self.image = self.image_standing_right
elif self.direction == "left":
self.image = self.image_standing_left
else:
if self.direction == "right":
self.image = self.image_jumping_right
elif self.direction == "left":
self.image = self.image_jumping_left
def die(self):
# D E D
pass
def check_block_breaks(self, blocks):
# Break a block
# mouse_pos = pg.mouse.get_pos()
# for block in blocks:
# if block.rect.collidepoint(mouse_pos):
# print("hi")
pass
def respawn(self, world):
# Hey, you"re back!
self.rect.x = world.start_x
self.rect.y = world.start_y
self.health = self.max_health
self.invincibility = 0
self.direction = "right"
def update(self, world):
# Constantly update the player
self.apply_gravity(world)
self.move_and_process_blocks(world.blocks)
self.check_world_boundaries(world)
self.set_image()
self.check_block_breaks(world.blocks)
if self.health > 0:
if self.invincibility > 0:
self.invincibility -= 1
else:
self.die()
class Game():
def __init__(self):
# Initialize the game itself
self.window = pg.display.set_mode([WINDOWWIDTH, WINDOWHEIGHT])
pg.display.set_caption(TITLE)
self.clock = pg.time.Clock()
self.done = False
self.reset()
def start(self):
# Start the whole thing up
self.world = World(worlds[self.current_world])
self.cursor = Cursor(0, 0, cursor_tracker)
self.world.reset()
self.player.respawn(self.world)
def reset(self):
# Reset the game
self.player = Player(player_images)
self.current_world = 0
self.start()
def update(self):
# Update things in the game
self.player.update(self.world)
self.cursor.update(self.world)
if self.player.health <= 0:
self.player.respawn(self.world)
def calculate_offset(self):
# Calculate x/y coordinates after screen scrolls
x = -1 * self.player.rect.centerx + WINDOWWIDTH / 2
if self.player.rect.centerx < WINDOWWIDTH / 2:
x = 0
elif self.player.rect.centerx > self.world.width - WINDOWWIDTH / 2:
x = -1 * self.world.width + WINDOWWIDTH
y = -1 * self.player.rect.centery + WINDOWHEIGHT / 2
if self.player.rect.centery < WINDOWHEIGHT / 2:
y = 0
elif self.player.rect.centery > self.world.height - WINDOWHEIGHT / 2:
y = -1 * self.world.height + WINDOWHEIGHT
return x, y
def draw(self):
# Draw sprites to the screen
self.offset_x, self.offset_y = self.calculate_offset()
self.world.active_layer.fill(TRANSPARENT)
self.world.active_sprites.draw(self.world.active_layer)
if self.player.invincibility % 3 < 2:
self.world.active_layer.blit(self.player.image, [self.player.rect.x, self.player.rect.y])
self.window.blit(self.world.background_layer, [self.offset_x / 3, self.offset_y])
self.window.blit(self.world.inactive_layer, [self.offset_x, self.offset_y])
self.window.blit(self.world.active_layer, [self.offset_x, self.offset_y])
self.offset_cursor_x = self.cursor.rect.x - self.offset_x
self.offset_cursor_y = self.cursor.rect.y - self.offset_y
self.cursor.rect.x = self.offset_cursor_x
self.cursor.rect.y = self.offset_cursor_y
pg.display.update(0, 0, WINDOWWIDTH, WINDOWHEIGHT)
def process_events(self):
# Handle events (key presses, mouse clicks, etc)
for event in pg.event.get():
if event.type == pg.QUIT:
self.done = True
elif event.type == pg.KEYDOWN:
# Jump
if event.key == JUMP:
self.player.jump(self.world.blocks)
# Debug reset
elif event.key == pg.K_r:
self.reset()
# Debug close
elif event.key == pg.K_q:
self.done = True
# Break a block if you click on it
if event.type == pg.MOUSEBUTTONDOWN:
# for block in self.world.blocks:
# if block.rect.collidepoint(self.offset_cursor_x, self.offset_cursor_y):
# block.kill()
self.cursor.detect_block_collision(self.world)
pressed = pg.key.get_pressed()
if pressed[LEFT]:
self.player.move_left()
elif pressed[RIGHT]:
self.player.move_right()
else:
self.player.stop()
def loop(self):
# Loop through essential functions
while not self.done:
self.process_events()
self.update()
self.draw()
self.clock.tick(FPS)
if __name__ == "__main__":
# Begin the loop and pre-initialize the game
game = Game()
game.start()
game.loop()
pg.quit()
sys.exit()
World Handler (generated blocks and adds them to groups):
Some parts are commented out because they serve no purpose yet, but will soon.
# Imports
import pygame as pg
import json
import random
import os
from settings import *
from main import *
# Initialize pygame
pg.init()
class World():
def __init__(self, file_path):
# Initialize the world
# Starting entities
self.starting_blocks = []
# Entity groups
self.blocks = pg.sprite.Group()
# Sprite groups (active/inactive)
self.active_sprites = pg.sprite.Group()
self.inactive_sprites = pg.sprite.Group()
# Read the world json file
with open(file_path, "r") as f:
data = f.read()
map_data = json.loads(data)
# World width and height
self.width = map_data["width"] * GRID_SIZE
self.height = map_data["height"] * GRID_SIZE
# Player start position
self.start_x = map_data["start"][0] * GRID_SIZE
self.start_y = map_data["start"][1] * GRID_SIZE
# Load blocks
for item in map_data["blocks"]:
x, y = item[0] * GRID_SIZE, item[1] * GRID_SIZE
img = block_images[item[2]]
self.starting_blocks.append(Block(x, y, img))
# Layers
self.background_layer = pg.Surface([self.width, self.height], pg.SRCALPHA, 32)
self.inactive_layer = pg.Surface([self.width, self.height], pg.SRCALPHA, 32)
self.active_layer = pg.Surface([self.width, self.height], pg.SRCALPHA, 32)
# Load background color
if map_data["bg-color"] != "":
self.background_layer.fill(map_data["bg-color"])
# Load background image
# if map_data["bg-image"] != "":
# bg_image = pg.image.load(map_data["bg-image"]).convert_alpha()
#
# if map_data["bg-fill-y"]:
# h = bg_image.get_height()
# w = int(bg_image.get_width() * WINDOWHEIGHT / h)
# bg_image = pg.transform.scale(bg_image, (w, WINDOWHEIGHT))
#
# if "top" in map_data["bg-position"]:
# start_y = 0
# elif "bottom" in map_data["bg-postion"]:
# start_y = self.height = bg_image.get_height()
#
# if map_data["bg-repeat-x"]:
# for x in range(0, self.width, bg_image.get_width()):
# self.background_layer.blit(bg_image, [x, start_y])
# else:
# self.background_layer.blit(bg_image, [0, start_y])
# Load background music
# pg.mixer.music.load(map_data["music"])
# Set the world's gravity strength and terminal velocity
self.gravity = map_data["gravity"]
self.terminal_velocity = map_data["terminal-velocity"]
# Grass generator
if map_data["gen-type"] == "earth":
gen_loop = map_data["width"]
x = 0
y = 56 * GRID_SIZE # The general y coordinate for block placing
y_gen = 56 * GRID_SIZE # Stored to be referenced in order to make a smoother landscape
for i in range (0, map_data["width"]):
# Generate grass
img = block_images["Grass"]
self.starting_blocks.append(Block(x, y, img))
y += GRID_SIZE
# Generate dirt
for i in range(0, 6):
img = block_images["Dirt"]
self.starting_blocks.append(Block(x, y, img))
y += GRID_SIZE
# Generate stone
for i in range(1, int((self.height / GRID_SIZE))):
img = block_images["Stone"]
self.starting_blocks.append(Block(x, y, img))
y += GRID_SIZE
y = y_gen
x += GRID_SIZE
gen_loop -= 1
# Randomly decide what the next grass' y will be in relation to the previous one
random_grass = random.randint(0, 5)
# The lowest point you'll find a block of grass
lowest_grass_y = 53 * GRID_SIZE
# How extreme the changes in block heights will be
# 0 is flat, 1 will have pretty smooth terrain, while something like 10 would be super steep
gen_extremity = 1
# Keep the grass at the same y
if random_grass == 0 or random_grass == 1 or random_grass == 2 or random_grass == 3:
gen_loop -= 1
if y <= lowest_grass_y:
y += GRID_SIZE
# Increase y
elif random_grass == 4:
y_gen += GRID_SIZE * gen_extremity
if y <= lowest_grass_y:
y += GRID_SIZE
# Decrease y
elif random_grass == 5:
y_gen -= GRID_SIZE * gen_extremity
if y <= lowest_grass_y:
y += GRID_SIZE
else:
raise ValueError("How did we get here? Grass generator somehow generated an invalid number.")
# Add starting entities to their groups
self.blocks.add(self.starting_blocks)
# Add sprites to inactive/active sprite groups
self.inactive_sprites.add(self.blocks)
# Does... something?
for s in self.active_sprites:
s.image.convert()
for s in self.inactive_sprites:
s.image.convert()
# Draw inactive sprites to the inactive layer
self.inactive_sprites.draw(self.inactive_layer)
# Convert layers
self.background_layer.convert()
self.inactive_layer.convert()
self.active_layer.convert()
Setting file (to help you recreate the issue and find out what's going on
# Imports
import pygame as pg
import os
# File paths
current_path = os.path.dirname(__file__)
assets_path = os.path.join(current_path, 'assets')
image_path = os.path.join(assets_path, 'img')
# Window settings
TITLE = "Mooncraft"
WINDOWWIDTH = 960
WINDOWHEIGHT = 640
FPS = 60
GRID_SIZE = 32
# Options
sound_on = True
# Controls
LEFT = pg.K_a
RIGHT = pg.K_d
JUMP = pg.K_SPACE
# Colors
TRANSPARENT = (0, 0, 0, 0)
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
# World files
worlds = [(os.path.join(assets_path, 'worlds', 'earth.json'))]
If someone could explain what I'm doing wrong, it would be much appreciated.
Update 2: Added player class and settings file to help anyone willing to assist me find the issue.
From just a glance, it looks like you might not be clearing the screen at any point in the update loop, but rather drawing over what was already there. This would result in the block still being visible, but not actually there. Try adding screen.fill(#Color here) before flipping the display. Also try using pygame.display.update() instead of pygame.display.flip()
You can do this:
all_sprites = pg.sprite.Group()
all_sprites.add(your_sprite)
#your event:
all_sprites.remove(your_sprite)

Categories