How can I make my Sprite launch an object towards the mouse position?
Let's discard everything and start from scratch and make use of pygame features like sprites and vector math.
We begin with a basic skeleton of a pygame game, a simple window:
import pygame
def main():
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
screen.fill(pygame.Color('grey'))
pygame.display.flip()
clock.tick(60)
if __name__ == '__main__':
main()
Our game will have different scenes (title scene, game scene, game-over scene), so let's implement them now:
import pygame
import pygame.freetype
pygame.init()
FONT = pygame.freetype.SysFont(None, 32)
class SimpleScene:
def __init__(self, text, background, next_scene):
if background:
self.background = pygame.image.load(background).convert()
else:
self.background = pygame.Surface((640, 480))
self.background.fill(pygame.Color('white'))
if text:
FONT.render_to(self.background, (100, 200), text, pygame.Color('black'))
FONT.render_to(self.background, ( 99, 199), text, pygame.Color('red'))
self.next_scene = next_scene
def start(self):
pass
def draw(self, screen):
screen.blit(self.background, (0, 0))
def update(self, events, dt):
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
return self.next_scene
class Game:
def __init__(self):
self.background = pygame.image.load('background.png').convert()
def start(self):
pass
def draw(self, screen):
screen.blit(self.background, (0, 0))
def update(self, events, dt):
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
return 'VICTORY'
def main():
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
scenes = {
'TITLE': SimpleScene('PRESS SPACE TO START', 'background.png', 'GAME'),
'GAME': Game(),
'VICTORY': SimpleScene('YOU WIN!', None, 'TITLE'),
'DEFEAT': SimpleScene('YOU LOSE!', None, 'TITLE'),
}
scene = scenes['TITLE']
dt = 0
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
next_scene = scene.update(events, dt)
if next_scene:
scene = scenes[next_scene]
scene.start()
scene.draw(screen)
pygame.display.flip()
dt = clock.tick(60)
if __name__ == '__main__':
main()
This allows us to cycle through the game scenes by pressing space.
Now let's implement the core game. First, we need some sprites, so let's create an Actor
class and prepare the game scene to display and reset our sprites. We use some of pygame's basic stuff, like the Sprite
class.
...
class Actor(pygame.sprite.Sprite):
def __init__(self, image, pos, direction):
super().__init__()
self.image = image
self.rect = self.image.get_rect(center=pos)
self.pos = pygame.Vector2(*pos)
self.direction = pygame.Vector2(*direction)
class Game:
def __init__(self):
self.background = pygame.image.load('background.png').convert()
self.images = {
'tobi': pygame.image.load('tobi.png').convert_alpha(),
'naruto': pygame.image.load('naruto.png').convert_alpha(),
'kunai': pygame.image.load('kunai.png').convert_alpha()
}
self.sprites = pygame.sprite.Group()
def start(self):
self.sprites.empty()
self.sprites.add(Actor(self.images['naruto'], (50, 150), (0, 0)))
self.sprites.add(Actor(self.images['tobi'], (450, 300), (0, 0)))
def draw(self, screen):
screen.blit(self.background, (0, 0))
self.sprites.draw(screen)
def update(self, events, dt):
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
return 'VICTORY'
self.sprites.update()
...
Time for some action. Let's implement some behaviour in our actors. We create function of each different kind of behaviour we want. One for Tobi to move along the screen, one for Naruto to be controlled by the keyboard, and one for the kunais.
Since we use the Vector
class for the position and direction of our sprites, is simply a matter of substraction to make the kunai move towards the mouse position.
Here's the full code:
import pygame
import pygame.freetype
pygame.init()
FONT = pygame.freetype.SysFont(None, 32)
class SimpleScene:
def __init__(self, text, background, next_scene):
if background:
self.background = pygame.image.load(background).convert()
else:
self.background = pygame.Surface((640, 480))
self.background.fill(pygame.Color('white'))
if text:
FONT.render_to(self.background, (100, 200), text, pygame.Color('black'))
FONT.render_to(self.background, ( 99, 199), text, pygame.Color('red'))
self.next_scene = next_scene
def start(self):
pass
def draw(self, screen):
screen.blit(self.background, (0, 0))
def update(self, events, dt):
for event in events:
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
return self.next_scene
def tobi_ai(self, dt):
self._update_pos(dt)
# change direction when hitting the edge of the screen
display = pygame.display.get_surface().get_rect()
if self.rect.bottom > display.bottom or self.rect.top < 0: self.direction.y *= -1
if self.rect.right > display.right or self.rect.left < 0: self.direction.x *= -1
self._keep_on_screen()
def player_ai(self, dt):
# alter direction if arrow keys are pressed
self.direction = pygame.Vector2()
pressed = pygame.key.get_pressed()
if pressed[pygame.K_UP] or pressed[pygame.K_w]: self.direction += (0, -1)
if pressed[pygame.K_DOWN] or pressed[pygame.K_s]: self.direction += (0, 1)
if pressed[pygame.K_LEFT] or pressed[pygame.K_a]: self.direction += (-1, 0)
if pressed[pygame.K_RIGHT] or pressed[pygame.K_d]: self.direction += (1, 0)
self._update_pos(dt)
self._keep_on_screen()
def kunai_ai(self, dt):
self._update_pos(dt)
display = pygame.display.get_surface().get_rect()
# just fly around and die if out of screen
if not display.contains(self.rect):
self.kill()
class Actor(pygame.sprite.Sprite):
def __init__(self, image, pos, direction=(0, 0), speed=20, behaviour=None, rotate=False):
super().__init__()
self.image = image
self.rect = self.image.get_rect(center=pos)
# using vectors for position and direction makes it easy to calculate movement and rotation
self.pos = pygame.Vector2(*pos)
self.direction = pygame.Vector2(*direction)
if rotate:
self.image = pygame.transform.rotate(self.image, self.direction.angle_to(pygame.Vector2(1,0)))
self.speed = speed
self.behaviour = behaviour
def update(self, dt):
if self.behaviour:
self.behaviour(self, dt)
def _update_pos(self, dt):
if self.direction.length() > 0:
self.pos = self.pos + (self.direction.normalize() * self.speed * dt/100)
self.rect.center = int(self.pos.x), int(self.pos.y)
def _keep_on_screen(self):
self.rect.center = self.pos
display = pygame.display.get_surface().get_rect()
self.rect.clamp_ip(display)
self.pos.x, self.pos.y = self.rect.center
class Game:
def __init__(self):
self.background = pygame.image.load('background.png').convert()
self.images = {
'tobi': pygame.image.load('tobi.png').convert_alpha(),
'naruto': pygame.image.load('naruto.png').convert_alpha(),
'kunai': pygame.image.load('kunai.png').convert_alpha()
}
self.sprites = pygame.sprite.Group()
self.kunais = pygame.sprite.Group()
def start(self):
self.sprites.empty()
self.kunais.empty()
self.naruto = Actor(self.images['naruto'], (50, 150), behaviour=player_ai)
self.tobi = Actor(self.images['tobi'], (450, 300), (1, 1), behaviour=tobi_ai)
self.sprites.add(self.naruto)
self.sprites.add(self.tobi)
self.player_lives = 10
self.enemy_lives = 3
def draw(self, screen):
screen.blit(self.background, (0, 0))
self.sprites.draw(screen)
FONT.render_to(screen, (430, 10), 'Naruto:' , pygame.Color('red'))
FONT.render_to(screen, (550, 10), str(self.player_lives), pygame.Color('red'))
FONT.render_to(screen, (430, 50), 'Tobi:', pygame.Color('red'))
FONT.render_to(screen, (550, 50), str(self.enemy_lives), pygame.Color('red'))
def update(self, events, dt):
for event in events:
if event.type == pygame.MOUSEBUTTONDOWN:
kunai = Actor(self.images['kunai'],
self.naruto.pos,
event.pos-self.naruto.pos,
speed=35,
behaviour=kunai_ai,
rotate=True)
self.sprites.add(kunai)
self.kunais.add(kunai)
self.sprites.update(dt)
# kunai hits tobi
# we use https://www.pygame.org/docs/ref/sprite.html#pygame.sprite.spritecollide
# for collition detection
for sprite in pygame.sprite.spritecollide(self.tobi, self.kunais, True):
self.enemy_lives -= 1
if self.enemy_lives <= 0:
return 'VICTORY'
# tobi hits naruto
if self.tobi.rect.colliderect(self.naruto.rect):
self.player_lives -= 1
if self.player_lives <= 0:
return 'DEFEAT'
self.naruto.pos = pygame.Vector2(50, 150)
self.tobi.pos.x = max(self.tobi.pos.x, 400)
def main():
screen = pygame.display.set_mode((640, 480))
clock = pygame.time.Clock()
scenes = {
'TITLE': SimpleScene('PRESS SPACE TO START', 'background.png', 'GAME'),
'GAME': Game(),
'VICTORY': SimpleScene('YOU WIN!', None, 'TITLE'),
'DEFEAT': SimpleScene('YOU LOSE!', None, 'TITLE'),
}
scene = scenes['TITLE']
dt = 0
while True:
events = pygame.event.get()
for e in events:
if e.type == pygame.QUIT:
return
next_scene = scene.update(events, dt)
if next_scene:
scene = scenes[next_scene]
scene.start()
scene.draw(screen)
pygame.display.flip()
dt = clock.tick(60)
if __name__ == '__main__':
main()
Now we have a simple game that's replayable and easily extendable. Feel free to use this code for whatever you like.