I'm writing a little pirate game in Pygame. If you played sea battles in Empires Total War, you have an idea of what I would like to achieve:
The ship's sprite is at position (x1|y1). The player now clicks at position (x2|y2) on the screen. The sprite is now supposed to take (x2|y2) as its new position - by going there step by step, not by beaming there instantly.
I figured out that it has something to do with the diagonal of the rectangle (x1|y1),(x1|y2),(x2|y2),(x2|y1) but I just can't figure it out, especially not with keeping the speed the same no matter what angle that diagonal has and considering that the x and y values of either (ship or click) might be bigger or smaller than the respective other.
This little snippet is my last try to write a working function:
def update(self, new_x, new_y, speed, screen, clicked):
if clicked:
self.xshift = (self.x - new_x)
self.yshift = ((self.y - new_y) / (self.x - new_x))
if self.x > (new_x + 10):
self.x -= 1
self.y -= self.yshift
elif self.x > new_x and self.x < (new_x + 10):
self.x -= 1
self.y -= self.yshift
elif self.x < (new_x - 10):
self.x += 1
self.y += self.yshift
elif self.x < new_x and self.x < (new_x - 10):
self.x += 1
self.y += self.yshift
else:
self.x += 0
self.y += 0
screen.set_at((self.x, self.y), (255, 0, 255))
The "ship" is just a pink pixel here. The reaction it shows upon my clicks onto the screen is to move roughly towards my click but to stop at a seemingly random distance of the point I clicked.
The variables are:
new_x, new_y = position of mouseclick
speed = constant speed depending on ship types
clicked = set true by the MOUSEBUTTONDOWN event, to ensure that the xshift and yshift of self are only defined when the player clicked and not each frame again.
How can I make the ship move smoothly from its current position to the point the player clicked?
Say the current position is pos, and the point the player clicked is target_pos, then take the vector between pos and target_pos.
Now you know how to get from pos to target_pos, but to move in constant speed (and not the entire distance at once), you have to normalize the vector, and apply a speed constant by scalar multiplication.
That's it.
Complete example: (the relevant code is in the Ship.update method)
import pygame
class Ship(pygame.sprite.Sprite):
def __init__(self, speed, color):
super().__init__()
self.image = pygame.Surface((10, 10))
self.image.set_colorkey((12,34,56))
self.image.fill((12,34,56))
pygame.draw.circle(self.image, color, (5, 5), 3)
self.rect = self.image.get_rect()
self.pos = pygame.Vector2(0, 0)
self.set_target((0, 0))
self.speed = speed
def set_target(self, pos):
self.target = pygame.Vector2(pos)
def update(self):
move = self.target - self.pos
move_length = move.length()
if move_length < self.speed:
self.pos = self.target
elif move_length != 0:
move.normalize_ip()
move = move * self.speed
self.pos += move
self.rect.topleft = list(int(v) for v in self.pos)
def main():
pygame.init()
quit = False
screen = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()
group = pygame.sprite.Group(
Ship(1.5, pygame.Color('white')),
Ship(3.0, pygame.Color('orange')),
Ship(4.5, pygame.Color('dodgerblue')))
while not quit:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
if event.type == pygame.MOUSEBUTTONDOWN:
for ship in group.sprites():
ship.set_target(pygame.mouse.get_pos())
group.update()
screen.fill((20, 20, 20))
group.draw(screen)
pygame.display.flip()
clock.tick(60)
if __name__ == '__main__':
main()
Related
I'm working on Space Invaders variation. Right now all Enemies are moving horizontal and when first or last Enemy in column hits window border all Enemies start moving in opposite direction.
My problem is when Enemy hits few times in window right border distance between last and before last Enemy is decreasing. Below gif is showing an issue.
https://imgur.com/a/cmswhvA
I think that problem is in move method in Enemy class but I'm not sure about that.
My code:
import pygame
import os
import random
# CONST
WIDTH, HEIGHT = 800, 800
WIN = pygame.display.set_mode((WIDTH, HEIGHT))
FPS = 60
BACKGROUND = pygame.image.load(os.path.join("assets", "background.png"))
PLAYER_IMG = pygame.image.load(os.path.join("assets", "player-space-ship.png"))
PLAYER_VELOCITY = 7
PLAYER_LIVES = 3
ENEMY_IMG = pygame.image.load(os.path.join("assets", "Enemy1.png"))
ENEMY_VELOCITY = 1
ENEMY_ROWS = 3
ENEMY_COOLDOWN_MIN, ENEMY_COOLDOWN_MAX = 60, 60 * 4
ENEMIES_NUM = 10
LASER_IMG = pygame.image.load(os.path.join("assets", "laser-bullet.png"))
LASER_VELOCITY = 10
pygame.init()
pygame.display.set_caption("Galaxian")
font = pygame.font.SysFont("Comic Sans MS", 20)
class Ship:
def __init__(self, x, y):
self.x = x
self.y = y
self.ship_img = None
self.laser = None
def draw(self, surface):
if self.laser:
self.laser.draw(surface)
surface.blit(self.ship_img, (self.x, self.y))
def get_width(self):
return self.ship_img.get_width()
def get_height(self):
return self.ship_img.get_height()
def get_rect(self):
return self.ship_img.get_rect(topleft=(self.x, self.y))
def move_laser(self, vel, obj):
if self.laser:
self.laser.move(vel)
if self.laser.is_offscreen():
self.laser = None
elif self.laser.collision(obj):
self.laser = None
obj.kill()
def fire(self):
if not self.laser:
x = int(self.get_width() / 2 + self.x - 4)
y = self.y - 9
self.laser = Laser(x, y)
class Player(Ship):
def __init__(self, x, y):
super().__init__(x, y)
self.ship_img = PLAYER_IMG
self.mask = pygame.mask.from_surface(self.ship_img)
self.score = 0
self.lives = PLAYER_LIVES
def move_laser(self, vel, objs):
if self.laser:
self.laser.move(vel)
if self.laser.is_offscreen():
self.laser = None
else:
for obj in objs:
if self.laser and self.laser.collision(obj):
self.score += 1
objs.remove(obj)
self.laser = None
def get_lives(self):
return self.lives
def get_score(self):
return self.score
def kill(self):
self.lives -= 1
def collision(self, obj):
return is_collide(self, obj)
class Enemy(Ship):
y_offset = 0
rect = None
def __init__(self, x, y):
super().__init__(x, y)
self.ship_img = ENEMY_IMG
self.mask = pygame.mask.from_surface(self.ship_img)
self.vel = ENEMY_VELOCITY
self.cooldown = random.randint(ENEMY_COOLDOWN_MIN, ENEMY_COOLDOWN_MAX)
def move(self, objs):
if self.x + self.vel + self.get_width() > WIDTH or self.x + self.vel < 0:
for obj in objs:
obj.vel *= -1
self.x += self.vel
def can_fire(self, enemies):
if self.cooldown > 0:
self.cooldown -= 1
return False
self.cooldown = random.randint(ENEMY_COOLDOWN_MIN, ENEMY_COOLDOWN_MAX)
self.rect = pygame.Rect((self.x, self.y), (self.get_width(), HEIGHT))
for enemy in enemies:
if self.rect.contains(enemy.get_rect()) and enemy != self:
return False
return True
def collision(self, obj):
return is_collide(self, obj)
class Laser:
def __init__(self, x, y):
self.x = x
self.y = y
self.img = LASER_IMG
self.mask = pygame.mask.from_surface(self.img)
def draw(self, surface):
surface.blit(self.img, (self.x, self.y))
def move(self, vel):
self.y += vel
def is_offscreen(self):
return self.y + self.get_height() < 0 or self.y > HEIGHT
def get_width(self):
return self.img.get_width()
def get_height(self):
return self.img.get_height()
def collision(self, obj):
return is_collide(self, obj)
def is_collide(obj1, obj2):
offset_x = obj2.x - obj1.x
offset_y = obj2.y - obj1.y
return obj1.mask.overlap(obj2.mask, (offset_x, offset_y)) != None
def spawn_enemies(count):
distance = int(WIDTH / (count + 1))
enemies = []
enemy_width = ENEMY_IMG.get_width()
enemy_height = ENEMY_IMG.get_height()
if distance >= enemy_width * 2:
for i in range(count):
x = distance * (i + 1) - enemy_width
for row in range(ENEMY_ROWS):
enemies.append(Enemy(x, (row + 1) * enemy_height * 2))
return enemies
def game():
is_running = True
clock = pygame.time.Clock()
player = Player(int(WIDTH / 2), int(HEIGHT - PLAYER_IMG.get_height() - 20))
enemies = spawn_enemies(ENEMIES_NUM)
background = pygame.transform.scale(BACKGROUND, (WIDTH, HEIGHT))
bg_y = 0
def redraw():
WIN.blits(((background, (0, bg_y - HEIGHT)), (background, (0, bg_y)),))
score = font.render(f"Punktacja: {player.get_score()}", True, (255, 255, 255))
lives_text = font.render(f"Życia: {player.get_lives()}", True, (255, 255, 255))
WIN.blit(score, (20, 20))
WIN.blit(lives_text, (WIDTH - lives_text.get_width() - 20, 20))
player.draw(WIN)
for enemy in enemies:
enemy.draw(WIN)
while is_running:
clock.tick(FPS)
redraw()
# Move backgrounds
if bg_y < HEIGHT:
bg_y += 1
else:
bg_y = 0
for event in pygame.event.get():
if event.type == pygame.QUIT:
is_running = False
break
key = pygame.key.get_pressed()
if key[pygame.K_LEFT] and (player.x - PLAYER_VELOCITY) >= 0:
player.x -= PLAYER_VELOCITY
if (
key[pygame.K_RIGHT]
and (player.x + player.get_width() + PLAYER_VELOCITY) <= WIDTH
):
player.x += PLAYER_VELOCITY
if key[pygame.K_SPACE]:
player.fire()
player.move_laser(-LASER_VELOCITY, enemies)
for enemy in enemies:
if enemy.can_fire(enemies):
enemy.fire()
enemy.move(enemies)
enemy.move_laser(LASER_VELOCITY, player)
pygame.display.update()
pygame.quit()
game()
-- EDIT --
Below I'm posting working solution according to Glenn's advice.
class Enemy(Ship):
# ...
def move(self, objs):
last_obj = objs[-1]
if (
last_obj.x + last_obj.vel + last_obj.get_width() > WIDTH
or self.x + self.vel < 0
):
for obj in objs:
obj.vel *= -1
self.x += self.vel
I have not run the code, so this is from an analysis of the code logic.
You spawn your enemies and add them to the list from left to right (on all three rows), which means the three on the far left are the first three to be added to the enemies list and the three on the far right are the last three to be added. Since the list order is maintained when you iterate through the enemies list you also iterate through them in that order (left to right).
In your Enemy.move() method, you check to see if that enemy would go off the screen on either side, and if it would you change the direction of all of the enemies by reversing the velocity of them all. However when the enemies on the far right hit the edge of the screen, you have already moved all the enemies that are to the left of them. That means all the enemies to the left have moved right and only the last three don't and in fact they move to the left instead. This closes the gap on the right side each time they hit the right edge.
This does not happen when moving left because you start the iteration with those enemies and so the velocity direction is changed first before any of them are moved. (If you had created the enemies from right to left, you would see the opposite behavior).
There are two easy approaches to fixing this.
Before moving any enemies you can look to see if any would go off the screen and switch the direction before moving any of them. This actually is not that bad because you can use the ordering that you created them in to your advantage here. the enemies list will always be in left to right order even after some are killed and removed. The first entry in enemies should always be as far the the left as any of them , and the last entry will be as far to the right as the farthest to that side. That means that you only have to check the first (enemies[0]) against the left edge and the last (enemies[-1]) against the right edge before doing the move.
The other option is to always move them all. but have the move() method return whether it went off the edge. If any did you remember that, then after moving them all you switch the direction of them all and on the next pass they all move the other way. This means they could go off the screen slightly on either side, but you could prevent that by padding the edge check by an extra velocity amount and that would keep them from actually going off the screen at all.
I would probably go with option 1) myself. They are both pretty straightforward to implement.
I am making a networked game that involves generating random blocks. Players should not be able to move inside the blocks so I created a collision detection function to stop a player moving inside a block. The function is able to stop a player moving into a block from the left and the top but players can still move in from the right and bottom. I also have one more problem that ever since I generated the block the player moves extremely slow.
This is the code for the Player class:
class player:
def __init__(self, x, y, width, height, colour):
self.width = width # dimensions of player
self.height = height # dimensions of player
self.x = x # position on the screen
self.y = y # position on the screen
self.colour = colour # players colour
self.rect = (x, y, width, height) # all the players properties in one
self.speed = 2 # how far/fast you move with each key press
self.direction = ""
def draw(self, win):
pygame.draw.rect(win, self.colour, self.rect)
def move(self, platforms):
keys = pygame.key.get_pressed() # dictionary of keys - values of 0/1
if keys[pygame.K_LEFT]: # move left: minus from x position value
if self.x <= 5:
pass
else:
self.x -= self.speed
self.direction = "Left"
elif keys[pygame.K_RIGHT]: # move right: add to x position value
if self.x == 785:
pass
else:
self.x += self.speed
self.direction = "right"
elif keys[pygame.K_UP]: # move up: minus from y position value
if self.y <= 5:
pass
else:
self.y -= self.speed
self.direction = "up"
elif keys[pygame.K_DOWN]: # move down from
if self.y >= 785:
pass
else:
self.y += self.speed
self.direction = "down"
def update(self, platforms, direction):
self.collision_with_platform(platforms, direction)
self.rect = (self.x, self.y, self.width, self.height) # redefine where the player is
self.update(platforms, self.direction)
This is the collision detector:
def collision_with_platform(self, platforms, direction):
# evaluates if the player is inside a platform, moving the player to it's edge if so
for platform in platforms:
# checks if player's coordinates are inside the platform, for each platform
if platform.rect.left - 20 < self.x < platform.rect.left + 50:
if platform.rect.top - 20 < self.y < platform.rect.top + 50:
# if moving horizontally
if direction == "left" or direction == "right": # evaluates which side of the platform to move the player to
if direction == "left":
self.x = platform.rect.left + 10 # move to the right side
if direction == "right":
self.x = platform.rect.left - 10 # move to the left side
# if moving vertically
elif direction == "down" or direction == "up": # evaluates which side of the platform to move the player to
if direction == "down":
self.y = platform.rect.top - 10 # move to the top side
elif direction == "up":
self.y_pos = platform.rect.top + 50 # move to the bottom side
This is how I generate platforms:
class Platform:
# represents a platform which the player can stand on
def __init__(self, left, top, width, height, colour):
self.rect = pygame.Rect(left, top, width, height) # square representing a single platform
self.colour = colour # platform's colour
def generate_platforms(probability):
# generates platforms randomly
points, platforms = [], []
# generates a list of all possible platform coordinates
# format of points : [(0,0), (0,1), (0,2), ..., (1,0), (1,1), (1,2), ...]
for x in range(0, 600):
for y in range(0, 600):
points.append((x, y))
# randomly selects platforms to create out of list of all possible platforms
for point in points:
if random.randint(1, 100) <= probability: # probability of each individual platform being created
platforms.append(Platform(point[0] * 50, point[1] * 50, 50, 50, (0, 0, 225)))
return platforms # returns the list of generated platforms
This is the main game loop:
def main(): # asking server for updates, checking for events
run = True
n = Network()
startPos = read_pos(n.getPos())
p = player(startPos[0], startPos[1], 10, 10, (255, 0, 0)) # connect, get starting position
p2 = player(0, 0, 10, 10, (0, 0, 255))
platforms = generate_platforms(25)
clock = pygame.time.Clock()
while run:
clock.tick(60)
p2Pos = read_pos(n.send(make_pos((p.x, p.y))))
p2.x = p2Pos[0]
p2.y = p2Pos[1]
p2.update(platforms, p.direction)
for event in pygame.event.get():
if event.type == pygame.QUIT: # quitting condition
run = False
pygame.quit()
p.move(platforms) # move character based off what keys are being pressed
redrawWindow(win, p, p2, platforms)
The problem is you have way more platforms than you think, there is a 25% chance to have a platform on every pixel that is 50 x 50. you have a lot of overlapping platforms. I ran it and got a length of 62516 platforms. I think what you want it
for x in range(0, screen_Width//50):
for y in range(0, screen_Height//50):
if random.randint(1, 100) <= probability: # probability of each individual platform being created
platforms.append(Platform(x * 50, y * 50, 50, 50, (0, 0, 225)))
This only creates platforms every 50 pixels, so none overlap. Running this i got a length of 22, a lot lower and still looks about the same. This will speed up your program considerably. I was getting 1.5 fps but now maxed out fps at 60.
To fix your collisions.
Change your player rect to a pygame rect object
self.rect = pygame.Rect(x, y, width, height) # all the players properties in one
and change the rect when you move
if keys[pygame.K_LEFT]: # move left: minus from x position value
if self.x > 5:
self.x -= self.speed
self.rect.x -= self.speed
self.direction = "left" # lowercase l as everything else was lowercase
then for the collisions:
if self.rect.colliderect(platform.rect):
if direction == "left":
self.rect.left = platform.rect.right # move to the right side
elif direction == "right":
self.rect.right = platform.rect.left # move to the left side
# if moving vertically
elif direction == "down":
self.rect.bottom = platform.rect.top # move to the top side
elif direction == "up":
self.rect.top = platform.rect.bottom # move to the bottom side
also In player.update you dont need to call it again inside it. So just this will do
def update(self, platforms):
self.collision_with_platform(platforms, self.direction)
I just cant figure out why my bullet is not working. I made a bullet class and here it is:
class Bullet:
def __init__(self):
self.x = player.x
self.y = player.y
self.height = 7
self.width = 2
self.bullet = pygame.Surface((self.width, self.height))
self.bullet.fill((255, 255, 255))
Now I added several functions in my game class and here is the new code:
class Game:
def __init__(self):
self.bullets = []
def shoot_bullet(self):
if self.bullets:
for bullet in self.bullets:
rise = mouse.y - player.y
run = mouse.x - player.x
angle = math.atan2(rise, run)
bullet.x += math.cos(angle)
bullet.y += math.sin(angle)
pygame.transform.rotate(bullet.bullet, -math.degrees(angle))
D.blit(bullet.bullet, (bullet.x, bullet.y))
def generate_bullet(self):
if mouse.is_pressed():
self.bullets.append(Bullet())
What I was expecting the code to do was a Bullet() would get added to game.bullets every time I pressed the mouse button, then game.shoot_bullet would calculate the angle between the player and the mouse and shoot the bullet accordingly in the direction of the mouse. However, the result is a complete mess and the bullets actually don't rotate and don't move. They get generated and move weirdly to the left of the screen. I am not sure if I have messed up something or the method I have used is completely wrong.
First of all pygame.transform.rotate does not transform the object itself, but creates a new rotated surface and returns it.
If you want to fire a bullet in a certain direction, the direction is defined the moment the bullet is fired, but it does not change continuously.
When the bullet is fired, set the starting position of the bullet and calculate the direction vector to the mouse position:
self.pos = (x, y)
mx, my = pygame.mouse.get_pos()
self.dir = (mx - x, my - y)
The direction vector should not depend on the distance to the mouse, but it has to be a Unit vector.
Normalize the vector by dividing by the Euclidean distance
length = math.hypot(*self.dir)
if length == 0.0:
self.dir = (0, -1)
else:
self.dir = (self.dir[0]/length, self.dir[1]/length)
Compute the angle of the vector and rotate the bullet. In general, the angle of a vector can be computed by atan2(y, x). The y-axis needs to be reversed (atan2(-y, x)) as the y-axis generally points up, but in the PyGame coordinate system the y-axis points down (see How to know the angle between two points?):
angle = math.degrees(math.atan2(-self.dir[1], self.dir[0]))
self.bullet = pygame.Surface((7, 2)).convert_alpha()
self.bullet.fill((255, 255, 255))
self.bullet = pygame.transform.rotate(self.bullet, angle)
To update the position of the bullet, it is sufficient to scale the direction (by a velocity) and add it to the position of the bullet:
self.pos = (self.pos[0]+self.dir[0]*self.speed,
self.pos[1]+self.dir[1]*self.speed)
To draw the rotated bullet in the correct position, take the bounding rectangle of the rotated bullet and set the center point with self.pos (see How do I rotate an image around its center using PyGame?):
bullet_rect = self.bullet.get_rect(center = self.pos)
surf.blit(self.bullet, bullet_rect)
See also Shoot bullets towards target or mouse
Minimal example: repl.it/#Rabbid76/PyGame-FireBulletInDirectionOfMouse
import pygame
import math
pygame.init()
window = pygame.display.set_mode((500, 500))
clock = pygame.time.Clock()
class Bullet:
def __init__(self, x, y):
self.pos = (x, y)
mx, my = pygame.mouse.get_pos()
self.dir = (mx - x, my - y)
length = math.hypot(*self.dir)
if length == 0.0:
self.dir = (0, -1)
else:
self.dir = (self.dir[0]/length, self.dir[1]/length)
angle = math.degrees(math.atan2(-self.dir[1], self.dir[0]))
self.bullet = pygame.Surface((7, 2)).convert_alpha()
self.bullet.fill((255, 255, 255))
self.bullet = pygame.transform.rotate(self.bullet, angle)
self.speed = 2
def update(self):
self.pos = (self.pos[0]+self.dir[0]*self.speed,
self.pos[1]+self.dir[1]*self.speed)
def draw(self, surf):
bullet_rect = self.bullet.get_rect(center = self.pos)
surf.blit(self.bullet, bullet_rect)
bullets = []
pos = (250, 250)
run = True
while run:
clock.tick(60)
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
if event.type == pygame.MOUSEBUTTONDOWN:
bullets.append(Bullet(*pos))
for bullet in bullets[:]:
bullet.update()
if not window.get_rect().collidepoint(bullet.pos):
bullets.remove(bullet)
window.fill(0)
pygame.draw.circle(window, (0, 255, 0), pos, 10)
for bullet in bullets:
bullet.draw(window)
pygame.display.flip()
I noticed that the cycle where you checking the condition if
lenghts is equal to 0 it doesn't really make any sense.
Of-course the value of sqrt(0) is always 0, but how is this even possible in reality to get? 0% (i guess)
So, while its looking for a path by calculate a hypotenuse
if length == 0.0:
self.dir = (0, -1)
In which scenario where it returns 0 is possible ?
P.S. if i am missing any thing feel free to suggest
I'm making a simple pong game and I've written the majority of the code.
The problem is, once the ball falls, if you continue to move the paddle, it will bounce back up into the screen from the bottom. I need it to stay off the screen permanently once it misses the paddle.
Any feedback is appreciated! Thanks in advance!
L1_base.py (my base code):
import math
import random
import sys, pygame
from pygame.locals import *
import ball
import colors
import paddle
# draw the scene
def draw(screen, ball1, paddle1) :
screen.fill((128, 128, 128))
ball1.draw_ball(screen)
paddle1.draw_paddle(screen)
#function to start up the main drawing
def main():
pygame.init()
width = 600
height = 600
screen = pygame.display.set_mode((width, height))
ball1 = ball.Ball(300, 1, 40, colors.YELLOW, 0, 4)
paddle1 = paddle.Paddle(200, 575, colors.GREEN, 100, 20)
while 1:
for event in pygame.event.get():
if event.type == QUIT: sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
paddle1.update_paddle('right', 35)
if event.key == pygame.K_LEFT:
paddle1.update_paddle('left', 35)
ball1.test_collide_top_ball(0)
ball1.test_collide_bottom_ball(600, paddle1)
ball1.update_ball()
draw(screen, ball1, paddle1)
pygame.display.flip()
if __name__ == '__main__':
main()
ball.py (contains ball class/methods):
import pygame
class Ball:
def __init__(self, x, y, radius, color, dx, dy):
self.x = x
self.y = y
self.radius = radius
self.color = color
self.dx = dx
self.dy = dy
def draw_ball(self, screen):
pygame.draw.ellipse(screen, self.color,
pygame.Rect(self.x, self.y, self.radius, self.radius))
def update_ball(self):
self.x += self.dx
self.y += self.dy
def test_collide_top_ball(self, top_height):
if (self.y <= top_height) and (self.dy < 0):
self.dy *= -1
def test_collide_bottom_ball(self, coll_height, paddle):
if (self.y >= coll_height - self.radius - (600 - paddle.y)) and (self.x >= paddle.x) and (self.x <= paddle.x + paddle.width) and (self.dy > 0):
self.dy *= -1
paddle.py (contains paddle class/methods):
import pygame
class Paddle:
def __init__(self, x, y, c, w, h):
self.x = x
self.y = y
self.color = c
self.width = w
self.height = h
def draw_paddle(self, screen):
pygame.draw.rect(screen, self.color,
pygame.Rect(self.x, self.y, self.width, self.height), 0)
def update_paddle(self, dir, dx):
if (dir == 'left'):
self.x = max(self.x - dx, 0)
else:
self.x = min(self.x + dx, 600 - self.width)
def get_left(self):
if (self.x < 300):
return self.x
def get_right(self):
if (self.x >= 300):
return self.x
You tried to cram too much stuff into one if statement in test_collide_bottom_ball and that made debugging much harder. Its also easier in this case to consider all the reasons not to self.dy *= -1 over the reasons you should. That switching of directions is a privilege paddle and should only happen on special occasions.
I have left my debugging code in to clarify the way i thought about the problem, also there is still another bug which can get the ball stuck in the paddle, but you should be able to fix that given a similar approach. Please see the following code:
def test_collide_bottom_ball(self, coll_height, paddle):
print paddle.x, paddle.width + paddle.x, self.x
# shoot ball back to top when it has gone off the bottom
if self.y > 600:
self.y = 300
return
# ignore when the ball is far from the y access of the paddle
if not self.y >= coll_height - self.radius - (600 - paddle.y):
return
# check that the current x plus raidus to find "too small" condition
if self.x + self.radius <= paddle.x:
print "self.x too small"
return
# for too big we have to consider the paddle width
if self.x >= paddle.x + paddle.width:
print "self.x too big"
return
# ok, looks like its time to switch directions!
self.dy *= -1
P.S. use the pep8 coding standard utility. Its not any harder to code following pep8 and will save you headache.
So I have this mini particle effect that produces circles that move up. I want to make it look like smoke. I'm having a lot of problems though.
First off I want to make it recursive so that when the particle reaches the top it
returns to its original spot and begins again, what I have works a little but not exactly.
The other thing is I don't feel like it really looks like smoke can anyone suggest changes to make it look better?
Also I want to add this into my game, how would I make this callable for my game so I could just call particle and give it a location and it appears there. Can anyone help with any of this?
My Code
import pygame,random
from pygame.locals import *
xmax = 1000 #width of window
ymax = 600 #height of window
class Particle():
def __init__(self, x, y, dx, dy, col):
self.x = x
self.y = y
self.col = col
self.ry = y
self.rx = x
self.dx = dx
self.dy = dy
def move(self):
if self.y >= 10:
if self.dy < 0:
self.dy = -self.dy
self.ry -= self.dy
self.y = int(self.ry + 0.5)
self.dy -= .1
if self.y < 1:
self.y += 500
def main():
pygame.init()
screen = pygame.display.set_mode((xmax,ymax))
white = (255, 255, 255)
black = (0,0,0)
grey = (128,128,128)
particles = []
for part in range(25):
if part % 2 > 0: col = black
else: col = grey
particles.append( Particle(random.randint(500, 530), random.randint(0, 500), 0, 0, col))
exitflag = False
while not exitflag:
for event in pygame.event.get():
if event.type == QUIT:
exitflag = True
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
exitflag = True
screen.fill(white)
for p in particles:
p.move()
pygame.draw.circle(screen, p.col, (p.x, p.y), 8)
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
main()
I have made some major edits to your code. For starters, I cleaned up your class a great deal. Let's start with the arguments and the __init__ function.
First of all, instead of going down 500 to reset, particles go to the location that was set as their starting point. It's location that it starts out in is now chosen randomly in the __init__ function rather than in the game. I got rid of some of your unnecessary arguments as well.
In the move function of your class, I simplified quite a bit. In order for the particle to detect if it should reset, it simply sees if it's above 0. The going up is just a simple decrease of the y by 1. A change I added in is that the x changes randomly and goes to the right and left. This will make the smoke look a lot better / more realistic.
I didn't make many changes to the rest of your code. I changed your calling of the Particle class to fit the new arguments. I made a ton more particles, once again for visual effect. I also massively decreased the size of the circles drawn for (can you guess it?) visual effect. I added in a clock as well to keep the particles from going at supersonic speed.
Here is the final code. I hope you like it.
import pygame,random
from pygame.locals import *
xmax = 1000 #width of window
ymax = 600 #height of window
class Particle():
def __init__(self, startx, starty, col):
self.x = startx
self.y = random.randint(0, starty)
self.col = col
self.sx = startx
self.sy = starty
def move(self):
if self.y < 0:
self.x=self.sx
self.y=self.sy
else:
self.y-=1
self.x+=random.randint(-2, 2)
def main():
pygame.init()
screen = pygame.display.set_mode((xmax,ymax))
white = (255, 255, 255)
black = (0,0,0)
grey = (128,128,128)
clock=pygame.time.Clock()
particles = []
for part in range(300):
if part % 2 > 0: col = black
else: col = grey
particles.append( Particle(515, 500, col) )
exitflag = False
while not exitflag:
for event in pygame.event.get():
if event.type == QUIT:
exitflag = True
elif event.type == KEYDOWN:
if event.key == K_ESCAPE:
exitflag = True
screen.fill(white)
for p in particles:
p.move()
pygame.draw.circle(screen, p.col, (p.x, p.y), 2)
pygame.display.flip()
clock.tick(50)
pygame.quit()
if __name__ == "__main__":
main()
update
In order to add particles into your code, just do what you did in the code above. It works fine. If you wanted to do something to show the smoke starting, just put a pause time into your arguments and inhibit the movement of the smoke until that amount of time has passed. New class with that added in:
class Particle():
def __init__(self, startx, starty, col, pause):
self.x = startx
self.y = starty
self.col = col
self.sx = startx
self.sy = starty
self.pause = pause
def move(self):
if self.pause==0:
if self.y < 0:
self.x=self.sx
self.y=self.sy
else:
self.y-=1
self.x+=random.randint(-2, 2)
else:
self.pause-=1
The code you will need to create new particles:
for part in range(1, A):
if part % 2 > 0: col = black
else: col = grey
particles.append( Particle(515, B, col, round(B*part/A)) )
A and B are variables (I reccomend around 300 for A, B will be the Y value)
The new code will make particles spawn at the start location, and rise continously with no breaks. Hope you enjoy.
I have made a lot of changes to your code, especially in the Particle class.
Although, there are rather puzzling things in this code, it will be more flexible than your current code.
Here,
I have quite literately rewritten you Particle class.
Other than changing the __init__, to take many arguments (7 to be exact),
I have used trigonometry and the math module to move the particle, making it easier to manage (if you are good at math!). And I have also added bounce and draw methods to the Particle, and made the code more readable.
Just like #PygameNerd, I have added a clock, for restricting the max fps.
I haven't changed any event handling, but have used the bounce and draw funcs in the for p in particles: loop.
import pygame, random, math
def radians(degrees):
return degrees*math.pi/180
class Particle:
def __init__(self, (x, y), radius, speed, angle, colour, surface):
self.x = x
self.y = y
self.speed = speed
self.angle = angle
self.radius = 3
self.surface = surface
self.colour = colour
self.rect = pygame.draw.circle(surface,(255,255,0),
(int(round(x,0)),
int(round(y,0))),
self.radius)
def move(self):
""" Update speed and position based on speed, angle """
# for constant change in position values.
self.x += math.sin(self.angle) * self.speed
self.y -= math.cos(self.angle) * self.speed
# pygame.rect likes int arguments for x and y
self.rect.x = int(round(self.x))
self.rect.y = int(round(self.y))
def draw(self):
""" Draw the particle on screen"""
pygame.draw.circle(self.surface,self.colour,self.rect.center,self.radius)
def bounce(self):
""" Tests whether a particle has hit the boundary of the environment """
if self.x > self.surface.get_width() - self.radius: # right
self.x = 2*(self.surface.get_width() - self.radius) - self.x
self.angle = - self.angle
elif self.x < self.radius: # left
self.x = 2*self.radius - self.x
self.angle = - self.angle
if self.y > self.surface.get_height() - self.radius: # bottom
self.y = 2*(self.surface.get_height() - self.radius) - self.y
self.angle = math.pi - self.angle
elif self.y < self.radius: # top
self.y = 2*self.radius - self.y
self.angle = math.pi - self.angle
def main():
xmax = 640 #width of window
ymax = 480 #height of window
white = (255, 255, 255)
black = (0,0,0)
grey = (128,128,128)
pygame.init()
screen = pygame.display.set_mode((xmax,ymax))
clock = pygame.time.Clock()
particles = []
for i in range(1000):
if i % 2:
colour = black
else:
colour = grey
# for readability
x = random.randint(0, xmax)
y = random.randint(0, ymax)
speed = random.randint(0,20)*0.1
angle = random.randint(0,360)
radius = 3
particles.append( Particle((x, y), radius, speed, angle, colour, screen) )
done = False
while not done:
for event in pygame.event.get():
if event.type == pygame.QUIT:
done = True
break
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
done = True
break
if done:
break
screen.fill(white)
for p in particles:
p.move()
p.bounce()
p.draw()
clock.tick(40)
pygame.display.flip()
pygame.quit()
if __name__ == "__main__":
main()