Decreasing distance between objects using pygame - python

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.

Related

Python pygame sprites collision detection. How to define which sprite withing group collided and effect it's attributes by reducing a point

What am trying to do is to create an Arkanoid game, where the bricks have 3 points of strength each and then they die. The issue is that instead of just the particular brick that gets hit, to lose the points, the whole brick_sprite group is loosing the points. And when one suppose to die, all the previous within the list up to that one dies. I think the issue is that it loops considering the update on line #240. Please check line 65 at def collision(self): under Brick Class. The issue is somewhere there.
"""This is a simple version of arkanoid game"""
import sys
import pygame
import random
# Set colors R G B
white = (255, 255, 255)
black = (0, 0, 0)
orange = (255, 100, 10)
light_blue = (0, 144, 255)
shadow = (192, 192, 192)
purple = (152, 0, 152)
# Display
display_height = 999
display_width = 444
pygame.display.set_caption = ("Arkanoid 1.0")
FPS = 60
# Movement speed
speed = display_width // 60
# Movements
left = (-speed, 0)
right = (speed, 0)
up = (0, speed)
diagonal_left = [-speed, -speed]
diagonal_right = [speed, -speed]
# Game objects dimentions
base_dimentions = (display_width // 5, display_height // 100)
[brick_width, brick_height] = [display_width // 20 * 2, display_height // 100]
brick_dimentions = [brick_width, brick_height]
ball_dimentions = (display_height // 100, display_height // 100)
# Initializing text font
pygame.font.init()
txt_font = pygame.font.SysFont("Score: ", display_height//44)
# Initializing sprite lists
all_sprites = pygame.sprite.Group()
brick_sprites = pygame.sprite.Group()
class Brick(pygame.sprite.Sprite):
def __init__(self, point_value, center):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface(brick_dimentions)
self.image.fill(purple)
self.rect = self.image.get_rect()
self.rect.center = center
self.point_value = point_value
def update(self):
self.collision()
def collision1(self): #This works no issue.
# If brick is hit, loses a point
collision = pygame.sprite.spritecollide(ball, brick_sprites, True)
return collision
def collision(self): #Here is the issue.
# If brick is hit, loses a point
collision = pygame.sprite.spritecollide(ball, brick_sprites, False)
if collision:
self.point_value -= 1
if self.point_value == 0:
self.kill() ## BUGGISH ##"""
class Ball(pygame.sprite.Sprite):
"""Initiates a moving ball and its' attributes"""
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface(ball_dimentions)
self.image.fill(light_blue)
self.rect = self.image.get_rect()
self.rect.center = self.init_position()
self.direction = random.choice([diagonal_left, diagonal_right])
self.score = 0
def update(self):
self.movement()
def init_position(self):
# Initialize position of the ball
init_position = (board.rect.center[0],
(board.rect.center[1] - (base_dimentions[1] / 2)
- (ball_dimentions[1] / 2)))
return init_position
def collision(self):
# If hit bricks
collision = pygame.sprite.spritecollideany(ball, brick_sprites)
if collision:
self.direction[1] *= -1
self.score += 1
enter code here
def movement(self):
self.containment()
self.rect[1] += self.direction[1]
self.rect[0] += self.direction[0]
self.deflect()
self.ball_loss()
self.collision()
def containment(self):
if self.rect.right >= display_width or self.rect.left <= 0:
self.direction[0] *= -1
if self.rect.top <= 0:
self.direction[1] *= -1
def ball_loss(self):
if self.rect.bottom >= display_height:
self.reset()
bricks_reset()
def reset(self):
self.rect.center = self.init_position()
self.direction[1] *= -1
self.score = 0
def deflect(self):
# If hit base_board, deflect
if (self.rect.bottom == board.rect.top and
(board.rect.left <= self.rect.left <= board.rect.right or
board.rect.left <= self.rect.right <= board.rect.right)):
self.direction[1] *= -1
self.board_ball_interaction()
def board_ball_interaction(self):
# When board is moving, effects balls direction/speed
keystate = pygame.key.get_pressed()
if keystate[pygame.K_LEFT] and board.rect.left > 0:
self.direction[0] -= speed // 2
elif keystate[pygame.K_RIGHT] and board.rect.right < display_width:
self.direction[0] += speed // 2
class Base_board(pygame.sprite.Sprite):
"""Initiates base_board class and it's attributes"""
def __init__(self):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface(base_dimentions)
self.image.fill(orange)
self.rect = self.image.get_rect()
self.rect.center = (display_width // 2,
display_height - 2 * base_dimentions[1])
self.x_direction = 0
def update(self):
# Up-dates classes' position according to user's imput
self.x_direction = 0
self.movement()
self.rect.x += self.x_direction
def movement(self):
# Creates movement and constrains object within screen dimentions
keystate = pygame.key.get_pressed()
if keystate[pygame.K_LEFT]:
if not self.rect.left <= 0:
self.x_direction = -speed
elif keystate[pygame.K_RIGHT]:
if not self.rect.right >= display_width:
self.x_direction = speed
def shoot(self):
pass
def enlogate(self):
pass
def control():
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
# and adding all sprites on lists
board = Base_board()
ball = Ball()
all_sprites.add(board)
all_sprites.add(ball)
def bricks_list_creator():
# Creates and adds bricks into a list
i = 9
point_value = 2 ####
coordinates = [display_width // 20 + brick_width / 6, display_height // 20]
while i > 0:
brick = Brick(point_value, (coordinates)) ####
coordinates[0] += brick_width * 1.1
brick_sprites.add(brick)
i -= 1
return brick_sprites
def bricks_reset():
# Reset brick list
brick_sprites.empty()
bricks_list_creator()
return brick_sprites
def render_text(screen):
text = txt_font.render("Score: {0}".format(ball.score), 1, (0, 0, 0))
return screen.blit(text, (5, 10))
def render_main(screen):
all_sprites.draw(screen)
brick_sprites.draw(screen)
render_text(screen)
# Game main
def main():
pygame.init()
clock = pygame.time.Clock()
screen = pygame.display.set_mode((display_width, display_height))
bricks_list_creator()
while True:
# Events
clock.tick(FPS)
control()
# Update
brick_sprites.update()
all_sprites.update()
# Render
screen.fill(shadow)
render_main(screen)
pygame.display.flip()
pygame.display.update()
main()
I think the issue is in the update() of your Brick class calling the collision.
The sprite update function is typically used for changing the position or look of your sprite, and is called every frame. So it's not a good place to check for collisions.
A Brick only needs to know its point_value, it doesn't move (AFAIK).
class Brick(pygame.sprite.Sprite):
def __init__(self, point_value, center):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface(brick_dimentions)
self.image.fill(purple)
self.rect = self.image.get_rect()
self.rect.center = center
self.point_value = point_value
def takeHit( self, ball_sprite ):
# the ball has collided with *this* brick
self.point_value -= 1
if self.point_value == 0:
self.kill()
Then in Ball.collision() use the pygame.sprite.spritecollide() to get the list of Bricks the Ball has collided with, and reduce their hit points:
class Ball:
# [...]
def collision(self):
# calculate the list of bricks hit
hit_list = pygame.sprite.spritecollide( self, brick_sprites, False )
for brick in hit_list:
brick.takeHit() # may (or may not) kill the brick
Most of the time the hit_list is going to be a single Brick, but depending on the size of the ball, perhaps occasionally it's two bricks.

How to make collision detection work when you have simulated gravity for your game?

I am creating a platform game using pygame, in my game I have simulated gravity and platforms that can be jumped on but I'm having trouble getting the collision detection to work correctly. By collision detection I mean when my character sprite jumps I want him to bounce off of the bottom and sides of the platform that will be above him. Now my character sprite jumps through the platform and lands on top of it.
My code is as follows:
My main class:
import pygame
import random
from settings import *
from sprites import *
from camera import *
from os import path
class Game:
def __init__(self):
pygame.init() # initialises pygame
pygame.mixer.init()
self.screen = pygame.display.set_mode((WIDTH, HEIGHT)) # sets the width and height of the pygame window
pygame.display.set_caption(TITLE)
self.clock = pygame.time.Clock()
self.running = True
self.font_name = pygame.font.match_font(FONT_NAME)
self.load_data()
def load_data(self):
pass
def new(self):
self.all_sprites = pygame.sprite.Group()
self.platforms = pygame.sprite.Group()
self.player = Player(self)
self.all_sprites.add(self.player)
for plat in PLATFORM_LIST:
p = Platform(*plat)
self.all_sprites.add(p)
self.platforms.add(p)
self.camera = Camera(WIDTH, HEIGHT) # creates the camera with WIDTH and HEIGHT of the screen
self.run()
def run(self): # Game Loop - runs the game
self.playing = True
while self.playing:
self.clock.tick(FPS)
self.events()
self.update()
self.draw()
def update(self): # Game loop - update
self.all_sprites.update()
# collision with a platform
if self.player.vel.y > 0:
hits = pygame.sprite.spritecollide(self.player, self.platforms, False) # returns a list of platform sprites that hit the player
if hits:
self.player.pos.y = hits[0].rect.top
self.player.vel.y = 0
# screen moves with player
self.camera.update(self.player) # is the camera that tracks players movement
def events(self): # Game loop - events
for event in pygame.event.get():
if event.type == pygame.QUIT:
if self.playing:
self.playing = False
self.running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
self.player.jump()
def draw(self): # Game loop - draw
self.screen.fill(RED)
#self.all_sprites.draw(self.screen)
for sprite in self.all_sprites:
self.screen.blit(sprite.image, self.camera.apply(sprite)) # loops through the all_sprites group and blit's each sprite onto the screen
pygame.display.flip()
def start_screen(self):
pass
def game_over_screen(self):
pass
def wait_for_key(self):
pass
def draw_text(self,text, size, colour, x, y):
pass
g = Game()
g.start_screen()
while g.running:
g.new()
g.game_over_screen()
pygame.quit()
my sprite classes:
# will hold the sprite classes
import pygame
from settings import *
import random
vec = pygame.math.Vector2
class Player(pygame.sprite.Sprite):
def __init__(self, game):
pygame.sprite.Sprite.__init__(self)
self.game = game
self.image = pygame.Surface((30, 40))
self.image.fill(BLUE)
self.rect = self.image.get_rect()
self.rect.center = (WIDTH / 2, HEIGHT / 2)
self.pos = vec(WIDTH / 2, HEIGHT / 2)
self.vel = vec(0, 0)
self.acc = vec(0, 0)
def jump(self):
# jump only if on a platform
self.rect.x += 1
hits = pygame.sprite.spritecollide(self, self.game.platforms, False)
self.rect.x -= 1
if hits:
self.vel.y = -20
def update(self):
self.acc = vec(0, PLAYER_GRAV)
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT]:
self.acc.x = -PLAYER_ACC
if keys[pygame.K_RIGHT]:
self.acc.x = PLAYER_ACC
# apply friction
self.acc.x += self.vel.x * PLAYER_FRICTION
# equations of motion
self.vel += self.acc
self.pos += self.vel + 0.5 * self.acc
# stop from running of the left side of the screen
if self.pos.x < 0:
self.pos.x = 0
self.rect.midbottom = self.pos
class Platform(pygame.sprite.Sprite):
def __init__(self, x, y, width, height):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((width, height))
self.image.fill(GREEN)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
camera class
import pygame
from settings import *
# A camera that keeps track of an offset that will be, how far we want to draw the screen which will include all objects on the screen. We are just shifting the drawing of our screen according to the offset. Camera needs to do two things, apply the offset and then update the movement of where the player is on the screen.
class Camera:
def __init__(self, width, height): # we will need to tell the camera how wide and high we want it to be
self.camera = pygame.Rect(0, 0, width, height) # is the rectangle we set to keep track of the screen/be the camera
self.width = width
self.height = height
def apply(self, entity): # method to apply the offset to the screen, by shifting the screen according to the movement of the entity within the camera screen
return entity.rect.move(self.camera.topleft)
def update(self, target): # method to update where the player/target has moved to, updates are done according to last known position of the target
# as the target moves the camera moves in the opposite direction of the target and stays within the center of the screen
x = -target.rect.x + int(WIDTH/2) # left to right
y = -target.rect.y + int(HEIGHT/2) # up and down
# limit scrolling to map size, keeps the 'camera' from going over the edges
x = min(0, x) # left
y = min(0, y) # top
y = max(-(self.height - HEIGHT), y) # bottom
self.camera = pygame.Rect(x, y, self.width, self.height) # adjusts the camera's rectangle with the new x and y
settings module:
# Game options/settings
TITLE = 'Platformer'
WIDTH = 900
HEIGHT = 500
FPS = 60
FONT_NAME = 'arial'
HS_FILE = 'highscore.txt'
SPRITESHEET = 'spritesheet_jumper.png'
# Game colours
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)
YELLOW = (255, 255, 0)
# Starting Platforms:
PLATFORM_LIST = [(0, HEIGHT - 50, WIDTH, 50), (WIDTH / 2, HEIGHT * 1 / 2, 200, 30), (WIDTH + 150, HEIGHT - 50, WIDTH, 50)]
# player properties
PLAYER_ACC = 0.5
PLAYER_FRICTION = -0.12
PLAYER_GRAV = 0.8
P.S. added all my code for anyone that may need to see it.
Your question says "when you have simulated gravity" but i don't understand how that would affect the bouncing. The answer below should work with your applied gravity so i don't think gravity will be an issue. So first, to bounce, we will need to know which side of the player is colliding with a particular side of the platform. For this the following functions can be used.
#Checks if right side of the player is colliding with left side of platform
def rightCheck(rect1, rect2):
if rect1.x + rect1.width > rect2.x:
if (rect1.y > rect2.y and rect1.y < rect2.y + rect2.height) or (rect1.y + rect1.height < rect2.y + rect2.height and rect1.y + rect1.height> rect2.y):
if rect1.x < rect2.x:
return True
return False
#Checks if bottom side of the player is colliding with top side of platform
def botCheck(rect1, rect2):
if rect1.y + rect1.height > rect2.y:
if (rect1.x > rect2.x and rect1.x < rect2.x + rect2.width) or (rect1.x + rect1.width > rect2.x and rect1.x + rect1.width < rect2.x + rect2.width):
if rect1.y < rect2.y:
return True
return False
# NOTICE they take pygame.Rect as arguments
For your game you are probably going to need one for left of the player and right side of the platform well, but i don't have it since i copied these from my game. I am sure you can write one yourself :). So, moving on. The code below shows how right side of the player colliding with left side of the platform can be implemented. So you can do this with all the sides. Also, bounce function is probably the only one you are interested in, others i has to put there to make it work.
import pygame
win = pygame.display
D = win.set_mode((1200, 600))
def rightCheck(rect1, rect2):
if rect1.x + rect1.width > rect2.x:
if (rect1.y > rect2.y and rect1.y < rect2.y + rect2.height) or (rect1.y + rect1.height < rect2.y + rect2.height and rect1.y + rect1.height> rect2.y):
if rect1.x < rect2.x:
return True
return False
class Player:
def __init__(self, ):
self.pos = [10, 10]
self.surf = pygame.Surface((50, 50)).convert()
self.energy = 10 #how much the player bounces to the left by
self.trigger = False #Triggers the left push if it is true
self.rightCheck = rightCheck # this is the function to check collision
self.rvel = 0.5 # This is the movement speed
def move(self):
key = pygame.key.get_pressed()
if key[pygame.K_RIGHT]:
self.pos[0] += self.rvel
if key[pygame.K_LEFT]:
self.pos[0] -= 0.5
if key[pygame.K_UP]:
self.pos[1] -= 0.5
if key[pygame.K_DOWN]:
self.pos[1] += 0.5
def draw(self):
D.blit(self.surf, (self.pos[0], self.pos[1]))
def bounce(self, platRect):
selfRect = pygame.Rect(self.pos[0], self.pos[1], 50, 50)
if self.rightCheck(selfRect, platRect):
self.trigger = True
self.rvel = 0
else:
self.rvel = 0.5
if self.trigger:
self.pos[0] -= self.energy*0.1
self.rvel = 0.1
self.energy -= 0.05
if self.energy <= 0:
self.trigger = False
else:
self.energy = 10
p = Player()
while True:
pygame.event.get()
D.fill((255, 255, 255))
platformRect = pygame.Rect(300, 150, 600, 200)
pygame.draw.rect(D, (0, 0, 0), platformRect)
p.bounce(platformRect)
p.move()
p.draw()
p.bounce(platformRect)
win.flip()
It basically just checks if they are colliding and if they are pushes the player a little to the left.

pygame - problem with killing a sprite in a group

I am trying to create a game using pygame for a school project. I would like obstacles(in this case boxes) which get in the way of the player. The player is able to destroy the boxes which would result in another box to be spawned in a random location at the same height.
I've split the code into 3 seperate modules seperating the sprites, the main code and the settings(game variables).
main:
import pygame as pg
import random
from sprites import *
from settings import *
import os
import sys
import time
class Game:
def __init__(init):#initialising the games properties(window,sound,speed,etc).
pg.init()
pg.mixer.init()
init.clock = pg.time.Clock()
init.screen = pg.display.set_mode((WIDTH,HEIGHT))
pg.display.set_caption(TITLE)
init.clock = pg.time.Clock()
init.running = True
init.font_name = pg.font.match_font(FONT_NAME)
init.data()
def data(load):
load.dir = os.path.dirname(__file__)
def new(new):#starts the game again.
new.score = 0
new.obstacles = pg.sprite.Group()
new.platforms = pg.sprite.Group()
new.bullets = pg.sprite.Group()
new.all_sprites = pg.sprite.Group()
new.player = Player(new)
new.all_sprites.add(new.player)
for plat in PLATFORM_LIST:
p = Platform(*plat)
new.all_sprites.add(p)
new.platforms.add(p)
for obs in OBSTACLE_LIST:
new.obstacle = Obstacle(*obs)
new.all_sprites.add(new.obstacle)
new.obstacles.add(new.obstacle)
new.run()
def run(run):
run.playing = True
while run.playing:
run.cooldown = 0
run.clock.tick(FPS)
run.events()
run.update()
run.draw()
def update(update):
bullet = Bullet
#game update.
update.all_sprites.update()
#spawning obstacles lower half
while len(update.obstacles) < 3:
width = random.randrange(50, 100)
update.obstacle = Obstacle(random.randrange(0, WIDTH - width),HEIGHT-100,100,50)
update.obstacles.add(update.obstacle)
update.all_sprites.add(update.obstacle)
#spawning obstacles randomly throughout the middle half
#spawning obstacles randomly throughout the map upper half
#check if bullet collides with an obstacles.
collide = pg.sprite.groupcollide(update.bullets,update.obstacles,True,False)
if collide:
update.obstacle.obs_health = update.obstacle.obs_health - bullet.damage
if update.obstacle.obs_health == 0:
update.obstacle.kill()
#check if player hits the sides of an obstacle.
if update.player.velocity.x >0:#when moving right.
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y >= collide[0].rect.centery+20:#if the player is above the platform.
update.player.pos.x = collide[0].rect.left - (PLAYER_WIDTH/2)
update.player.velocity.x = 0
update.player.acceleration.y = 0
if update.player.velocity.x <0:#when moving left.
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y >= collide[0].rect.centery:
update.player.pos.x = collide[0].rect.right + (PLAYER_WIDTH/2)
update.player.velocity.x = 0
#check if player hits side of platforms
if update.player.velocity.x >0 and (update.player.velocity.y < 0):#when moving right.
collide = pg.sprite.spritecollide(update.player,update.platforms,False)
if collide:
if update.player.pos.y < collide[0].rect.centery+50:#if the player is below the obstacle.
update.player.pos.x = collide[0].rect.left - (PLAYER_WIDTH/2)
update.player.velocity.x = 0
if update.player.velocity.x <0:#when moving left.
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y > collide[0].rect.centery:
update.player.pos.x = collide[0].rect.right + (PLAYER_WIDTH/2)
update.player.velocity.x = 0
#check if player hits a platform while ascending:
if update.player.velocity.y <0:#only when moving up.
collide = pg.sprite.spritecollide(update.player,update.platforms,False)
if collide:
if update.player.pos.y > collide[0].rect.bottom:
update.player.pos.y = collide[0].rect.bottom + (PLAYER_HEIGHT/2) + PLAYER_JUMP
update.player.velocity.y = 0
#check if a player hits a platform while falling.
if update.player.velocity.y >0:#only while falling will this apply.
collide = pg.sprite.spritecollide(update.player,update.platforms,False)#false allows you to avoid deleting the object you jump into.
if collide:
if update.player.pos.y < collide[0].rect.centery:#if the player is above the center of the platform.
update.player.pos.y = collide[0].rect.top +1
update.player.velocity.y = 0
collide = pg.sprite.spritecollide(update.player,update.obstacles,False)
if collide:
if update.player.pos.y < collide[0].rect.centery:
update.player.pos.y = collide[0].rect.top +1
update.player.velocity.y = 0
#spawning obstacles randomly throughout the map upper half
#spawning obstacles randomly throughout the middle half
def events(events):
events.cooldown += events.clock.get_time()
#processes inputs.
for event in pg.event.get():
#check for window closing.
if event.type == pg.QUIT:#if the 'x' button is clicked
if events.playing:
events.playing = False#stop the game loop.
events.running = False#stop the main loop.
if event.type == pg.KEYDOWN:
if event.key == pg.K_UP:
events.player.jump()
if event.key == pg.K_SPACE:
events.player.bullet_list.append(events.player.shoot())
#print(len(events.player.bullet_list))
def draw(draw):
draw.screen.fill(GREY)# creates a black screen.
draw.draw_text(str(draw.player.PLAYER_HEALTH),24,BLACK,WIDTH/32,HEIGHT /32)
draw.all_sprites.draw(draw.screen)#draws the sprites in the group all_sprites.
#after drawing the screen is flipped.
pg.display.flip()
def start_screen(start):#screen displayed when the game is started.
start.screen.fill(BGCOLOR)
start.draw_text(TITLE,48,WHITE,WIDTH/2,HEIGHT /4)
start.draw_text("Arrows to move,UP to jump", 22,WHITE,WIDTH/2,HEIGHT/2)
start.draw_text("Press a key to play",22,WHITE,WIDTH/2,HEIGHT*3/4)
pg.display.flip()
start.any_key()#temporary key to start system.
def any_key(wait):
waiting = True
while waiting:#a loop is used for the start screen until an action is done.
wait.clock.tick(FPS)#allows animations to
for event in pg.event.get():
if event.type == pg.QUIT:#if the 'x' button is pressed during the start screen.
waiting = False
wait.running = False#stops the main loop.
if event.type == pg.KEYUP:#if any key is released.
waiting = False
def over_screen(over):#displayed when the game ends.
if not over.running:
return#skips the over screen when 'x' button is pressed.
over.screen.fill(BGCOLOR)
over.draw_text('GAME OVER',48,WHITE,WIDTH/2,HEIGHT /4)
def draw_text(self, text, size, color, x, y):
font = pg.font.Font(self.font_name, size)#selects the chosen font.
text_surface = font.render(text, True, color)#creates the text with anti aliasing and the color chosen.
text_rect = text_surface.get_rect()
text_rect.midtop = (x, y)#position of text.
self.screen.blit(text_surface, text_rect)#renders text on screen.
g = Game()
g.start_screen()
while g.running:#the main loop.
g.new()
g.over_screen()
pg.quit()#closes the window.
sprites:
#Sprite class
import random
import pygame as pg
from settings import *
vec = pg.math.Vector2 #creates a 2D Vector which stores the x an y cordinates for the sprites.
class Player(pg.sprite.Sprite):
def __init__(self, game):#create initialise the properties of the sprite.
self.game = game#reference to variable in game class.
pg.sprite.Sprite.__init__(self)#provides functions for the sprite in other functions.
self.image = pg.Surface((PLAYER_WIDTH,PLAYER_HEIGHT))#creates a square for the Player to be used as a hitbox.
self.image.fill(GREEN)#place holder for the player.
self.rect = self.image.get_rect()
self.rect.center = (WIDTH/2),(HEIGHT/4)# allows you to move the character.
self.pos = vec(WIDTH/2,HEIGHT/2)#the center of the sprite.
self.velocity = vec(0,0)#the speed of the player.
self.acceleration = vec(0,0)#allows for the change in speed.
self.facing = 0 #direction the player is looking.
self.current = 0#current direction facing.
self.PLAYER_HEALTH = 1000
self.bullet_list = []
def jump(self):
hits = pg.sprite.spritecollide(self, self.game.platforms, False)
if hits:#only able to jump when colliding with platform.
self.velocity.y += -PLAYER_JUMP
collide = hits = pg.sprite.spritecollide(self, self.game.obstacles, False)
if collide:
self.velocity.y += -PLAYER_JUMP
def shoot(self):
#if game.cooldown > 400:
#cooldown = 0
self.bullet = Bullet(self,self.current,self.rect.centerx, self.rect.top)
self.bullet_list.append(self.bullet)
for bullet in self.bullet_list:
#self.bullet = Bullet(self.current,self.rect.centerx, self.rect.top)#creates bullet postioned in center.
self.game.all_sprites.add(self.bullet)
self.game.bullets.add(self.bullet)
#self.bullet = Bullet(self.current,self.rect.centerx, self.rect.top)#creates bullet postioned in center.
#self.game.all_sprites.add(self.bullet)
#self.game.bullets.add(self.bullet)
def update(self):
self.acceleration = vec(0,PLAYER_GRAV)#resets the position of player when not moving.
keys = pg.key.get_pressed()#inputs a pressed key.
if keys[pg.K_LEFT]:
self.acceleration.x = -PLAYER_ACC
self.facing = -1
self.current = self.facing
if keys[pg.K_RIGHT]:
self.acceleration.x = PLAYER_ACC
self.facing = 1
self.current = self.facing
if self.acceleration.x == 0:#if standing, the previous direction is saved
self.facing = self.current
#print(self.current)
#friction.
self.acceleration.x += self.velocity.x * PLAYER_FRICTION
#equation for displacment.
self.velocity += self.acceleration
self.pos += self.velocity + 0.5 * self.acceleration#moves thes players position to the new x,y co-ordinate.
#boundaries of screen.
if self.rect.right > WIDTH:
self.pos.x = WIDTH -(PLAYER_WIDTH * 0.5)#avoids overlapping the boundary.
self.velocity.x = 0 #avoids player sticking to walls by capping speed at boundaries.
self.acceleration.x = 0 #reduces the amount of 'jitter' when trying to move past boundaries.
if self.rect.left < 0 :
self.pos.x = (PLAYER_WIDTH * 0.5)#avoids overlapping the boundary.
# have to add half the player width to avoid player going into walls.
self.velocity.x = 0 #avoids player sticking to walls by stopping player at boundaries.
self.rect.midbottom = self.pos#tracks the position of the players center.
class Platform(pg.sprite.Sprite,):
def __init__(self, x, y, w, h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w,h))
self.image.fill(BLACK)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
class Obstacle(pg.sprite.Sprite):
def __init__(self,x,y,w,h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w,h))
self.image.fill(BLUE)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.obs_health = 100
class Bullet(pg.sprite.Sprite):
damage = 25
def __init__(self,player,current, x, y):
self.player = player#allows the bullet class to use player variables.
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((20,10))
self.image.fill(LBLUE)
self.rect = self.image.get_rect()
self.rect.right = x
self.rect.centery = y + (PLAYER_HEIGHT/2)
#self.damage = 25
self.velocity = vec(0,0)
if current == -1:#when looking left.
self.velocity = vec(-10,0)
if current == 1:#when looking right.
self.velocity = vec(10,0)
def update(self):
self.rect.right += self.velocity.x
#remove when moves of off screen.
if self.rect.right > WIDTH:
self.kill()
for bullet_amount in self.player.bullet_list:
self.player.bullet_list.pop(self.player.bullet_list.index(bullet_amount))
if self.rect.left <0:
self.kill()
for bullet_amount in self.player.bullet_list:
self.player.bullet_list.pop(self.player.bullet_list.index(bullet_amount))
#print(self.rect.x)
settings:
#settings
TITLE = "downpour"
WIDTH = 900
HEIGHT = 500
FPS = 60
FONT_NAME = 'Ariel'
#platforms
PLATFORM_LIST = [(0, HEIGHT - 50, WIDTH, 50),
(WIDTH -225 ,HEIGHT * 3/4 -50,200, 40),#(x,y,width,height)of the platforms.
(0 +25 ,HEIGHT * 3/4 -50,200, 40),
(0 +350,HEIGHT * 3/4 -150,200, 40)]
OBSTACLE_LIST = [(WIDTH/2,HEIGHT -50-50,100,50),(WIDTH/3,HEIGHT -50-50,100,50),(WIDTH/2,HEIGHT -50-50,100,50)]
#player properties
PLAYER_WIDTH = 50
PLAYER_HEIGHT = 50
PLAYER_ACC = 0.55
PLAYER_FRICTION = -0.05
PLAYER_GRAV = 0.8
PLAYER_JUMP = 15
#colors defines
WHITE = (255,255,255)
BLACK = (0,0,0)
GREY = (211,211,211)
RED = (255,0,0)
GREEN = (0,255,0)
BLUE = (0,0,255)
LBLUE = (132,112,255)
BGCOLOR = LBLUE
The problem I have encountered is with spawning a new box after destroying one of the multiple boxes. A box can be destroyed by depleting its health through shooting at it.
Lets say I have 3 boxes: A,B and C. when I try to destroy B or C, box A is the one that is destroyed and respawned.
I feel like it's an obvious answer...
code relating to the obstacle:
creating the class:
class Obstacle(pg.sprite.Sprite):
def __init__(self,x,y,w,h):
pg.sprite.Sprite.__init__(self)
self.image = pg.Surface((w,h))
self.image.fill(BLUE)
self.rect = self.image.get_rect()
self.rect.x = x
self.rect.y = y
self.obs_health = 100
adding it to a Sprite group:
for obs in OBSTACLE_LIST:
new.obstacle = Obstacle(*obs)
new.all_sprites.add(new.obstacle)
new.obstacles.add(new.obstacle)
collisions:
collide = pg.sprite.groupcollide(update.bullets,update.obstacles,True,False)
if collide:
update.obstacle.obs_health = update.obstacle.obs_health - bullet.damage
if update.obstacle.obs_health == 0:
update.obstacle.kill()
spawning a new obstacle:
while len(update.obstacles) < 3:
width = random.randrange(50, 100)
update.obstacle = Obstacle(random.randrange(0, WIDTH - width),HEIGHT-100,100,50)
update.obstacles.add(update.obstacle)
update.all_sprites.add(update.obstacle)
First of all, for all instance methods, it would help the reader if you used the name self instead of all the custom names you're using such as new or update for the first argument.
After that rewrite, you code will look like follows:
collide = pg.sprite.groupcollide(self.bullets,self.obstacles,True,False)
if collide:
self.obstacle.obs_health = self.obstacle.obs_health - bullet.damage
if self.obstacle.obs_health == 0:
self.obstacle.kill()
Now ask yourself, why does the program know that the self.obstacle is the one that collided? Should self.obstacle even exist? It looks like self.obstacle was just used a temporary local variable upon creation of the Game class to add Obstacle's to self.obstacles.
If so just use a local variable as follows:
for obs in OBSTACLE_LIST:
obstacle = Obstacle(*obs)
self.all_sprites.add(obstacle)
self.obstacles.add(obstacle)
At this point, hopefully the error message will make it clear that referencing self.obstacle isn't going to work. pg.sprite.groupcollide returns you a Sprite_dict, so you need to extract the obstacle from collide to figure out what has collided.
#KentShikama Thanks for pointing that out.
I have fixed the issue by using the dictionary.
for obstacles, bullets in collide.items():
obstacles.obs_health = obstacles.obs_health - bullet.damage
if obstacles.obs_health == 0:
obstacles.kill()

Balls are catched by bounds and they stick together (incorrect algorithm in draw method)

Let's consider following code (balls with random centers and velocities collide, with screen surface bounds, and with each other):
import pygame,sys,math
from pygame.locals import *
from random import randrange
WIDTH = 500
HEIGHT = 500
WHITE = (255,255,255)
BLUE = (0, 0, 255)
RADIUS = 10
FPS = 30
DISPLAYSURF = pygame.display.set_mode((WIDTH,HEIGHT),0,32)
TAB = []
fpsClock = pygame.time.Clock()
pygame.display.set_caption('Animation')
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self, A):
return math.sqrt((A.x-self.x)**2 + (A.y-self.y)**2)
def getTouple(self):
return (self.x,self.y)
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def norm(self):
return math.sqrt(self.x**2 + self.y**2)
class Ball:
def __init__(self, center, radius, velocity):
self.center = center
self.radius = radius
self.velocity = velocity
def __init__(self):
self.radius = RADIUS
self.center = Point(randrange(RADIUS,WIDTH-RADIUS), randrange(RADIUS,HEIGHT-RADIUS))
vx = randrange(-5,5)
vy = randrange(-5,5)
while vx == 0 or vy == 0:
vx = randrange(-5,5)
vy = randrange(-5,5)
self.velocity = Vector(vx,vy)
def draw(self):
self.center.x += self.velocity.x
self.center.y += self.velocity.y
for ball in TAB:
if ball != self:
if ball.center.distance(self.center) <= 2*RADIUS:
tmp = self.velocity
self.velocity = ball.velocity
ball.velocity = tmp
if self.center.x - self.radius <= 0:
self.velocity.x = -self.velocity.x
if self.center.x + self.radius >= WIDTH:
self.velocity.x = -self.velocity.x
if self.center.y - self.radius <= 0:
self.velocity.y = -self.velocity.y
if self.center.y + self.radius >= HEIGHT:
self.velocity.y = -self.velocity.y
pygame.draw.circle(DISPLAYSURF, BLUE, self.center.getTouple(), self.radius, 0)
BallNum = 30
for i in range(BallNum):
k = Ball()
TAB.append(k)
while True:
DISPLAYSURF.fill(WHITE)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
for ball in TAB:
ball.draw()
pygame.display.update()
fpsClock.tick(FPS)
This code works but not corectly: some balls are 'catched' by the bounds and others 'stick together'. I think the problem is in Ball class method draw. I will be grateful for any ideas on how to improve that code.
The main problem with the code you have posted which causes some of the balls to stick together, is that after they bounce off of each other, they still are in contact, so they bounce off of each other indefinitely which causes them to 'stick together'.
To fix this problem:
Add in an attribute/variable which holds a reference to the ball that it last hit. I called this attribute ball_last_hit and I initialized it to None.
Then, in the collision detection code, simply check to make sure the ball is not in contact with the ball it last hit, with an if statement like this: if ball != self.ball_last_hit:.
Then, if the balls should bounce off of each other, after swapping their velocities, set self.ball_last_hit to ball.
This will solve the 'sticking together' problem between the balls, but the ball could still get 'stuck' to the wall, although it is less common. To solve this you could add in an attribute called something like self.ignore_walls and after each collision with a wall, set it to true, then after a certain amount of time, set it back to false, so it can collide with walls again.
Here is the fixed code:
import pygame,sys,math
from pygame.locals import *
from random import randrange
WIDTH = 500
HEIGHT = 500
WHITE = (255,255,255)
BLUE = (0, 0, 255)
RADIUS = 10
FPS = 30
DISPLAYSURF = pygame.display.set_mode((WIDTH,HEIGHT),0,32)
TAB = []
fpsClock = pygame.time.Clock()
pygame.display.set_caption('Animation')
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def distance(self, A):
return math.sqrt((A.x-self.x)**2 + (A.y-self.y)**2)
def getTouple(self):
return (self.x,self.y)
class Vector:
def __init__(self, x, y):
self.x = x
self.y = y
def norm(self):
return math.sqrt(self.x**2 + self.y**2)
class Ball:
def __init__(self):
self.radius = RADIUS
self.center = Point(randrange(RADIUS,WIDTH-RADIUS), randrange(RADIUS,HEIGHT-RADIUS))
vx = randrange(-5,5)
vy = randrange(-5,5)
while vx == 0 or vy == 0:
vx = randrange(-5,5)
vy = randrange(-5,5)
self.velocity = Vector(vx,vy)
self.ball_last_hit = None
def draw(self):
self.center.x += self.velocity.x
self.center.y += self.velocity.y
for ball in TAB:
if ball != self:
if ball.center.distance(self.center) <= 2*RADIUS:
if ball != self.ball_last_hit:
tmp = self.velocity
self.velocity = ball.velocity
ball.velocity = tmp
self.ball_last_hit = ball
if self.center.x - self.radius <= 0:
self.velocity.x = -self.velocity.x
if self.center.x + self.radius >= WIDTH:
self.velocity.x = -self.velocity.x
if self.center.y - self.radius <= 0:
self.velocity.y = -self.velocity.y
if self.center.y + self.radius >= HEIGHT:
self.velocity.y = -self.velocity.y
pygame.draw.circle(DISPLAYSURF, BLUE, self.center.getTouple(), self.radius, 0)
BallNum = 30
for i in range(BallNum):
k = Ball()
TAB.append(k)
while True:
DISPLAYSURF.fill(WHITE)
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
for ball in TAB:
ball.draw()
pygame.display.update()
fpsClock.tick(FPS)
Note: instead of using a temp variable, and executing three lines of code, you can swap the balls' velocities with just one this line of code: self.velocity, ball.velocity = ball.velocity, self.velocity.
I hope this answer helped you, and if you have any further questions, please feel free to leave a comment below!
Edit: to get rid of the sticking to the walls, all we need to do is make sure that after a collision with the wall, the ball is not still in contact with the wall, because if it is, it will bounce against the wall indefinitely, and get 'stuck'
After changing the ball's velocity, when it collides with the wall, I just set the center of the ball to one pixel away from the wall, so it is definitely not touching the wall, and this fixed the problem for me.
Here is the new collision detection code, which goes at the end of the draw() method. There are just 4 new lines needed.
if self.center.x - self.radius <= 0:
self.velocity.x = -self.velocity.x
self.center.x = self.radius + 1
if self.center.x + self.radius >= WIDTH:
self.velocity.x = -self.velocity.x
self.center.x = WIDTH - self.radius - 1
if self.center.y - self.radius <= 0:
self.velocity.y = -self.velocity.y
self.center.y = self.radius + 1
if self.center.y + self.radius >= HEIGHT:
self.velocity.y = -self.velocity.y
self.center.y = HEIGHT - self.radius - 1
Note: this simulation gets very tricky if you increase the ball number even higher, because groups of 3 balls start to collide with each other at a time, and your current code is only capable of handling collision between two balls.

Make a sprite move to the mouse click position step by step

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()

Categories