17 KiB
Chapter 12. A Ship that Fires Bullets
Starting the Game Project
import sys
import pygame
class AlienInvasion:
"""Overall class to manage game assets and behaviour"""
def __init__(self):
"""Initialize the game, and create game resources."""
pygame.init()
self.screen = pygame.display.set_mode((1200, 800))
pygame.display.set_caption("Alien Invasion")
def run_game(self):
"""Start the main loop for the game"""
while True:
# event loop, watch for mouse/keyboard events
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
# Make the most recently draw screen visible
pygame.display.flip()
if __name__ == '__main__':
ai = AlienInvasion()
ai.run_game()
pygame.display.set_mode()
assigned a surface toself.screen
, which is a part of the screen where a game element can be displayed.- When we activate the game's animation loop, this surface will be redrawn every time.
- Game is contolled by
run_game()
method. containing- event loop block
- code that manages screen updates.
- To access the events that Pygame detects, use
pygame.event.get()
function. In each loop, any keyboard/mouse event will cause thefor
loop to run.
Creating a Settings Class
In order to manage the setting more efficiently, we move setting into a separate module settings.py
class Settings:
"""A class to store all settings for Alien Invasion"""
def __init__(self):
"""Initialize the game's settings."""
# Screen settings
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (230,230,230)
alien_invasion.py
will also be modified to fit it.
Adding The Ship Image
- Verify it's free licensed image
Creating the Ship Class
Create this ship.py
module
import pygame
class Ship:
"""A class to manage the ship."""
def __init__(self, ai_game):
"""Initialize the ship and set its starting position"""
self.screen = ai_game.screen
self.screen_rect = ai_game.screen.get_rect()
# Load the ship image and get its rect
self.image = pygame.image.load('ship.bmp')
self.rect = self.image.get_rect()
# Start each new ship at the bottom center of the screen
self.rect.midbottom = self.screen_rect.midbottom
def blitme(self):
"""Draw the ship at its current location"""
self.screen.blit(self.image,self.rect)
- Pygame treat all game elements like rectangles (rects). So, collision of objects can be calculated via
get_rect()
method pygame.image.load()
is called to load the image- When the image is loaded, we call
get_rect()
to access ship surface'srect
attribute
How to work with a rect
object:
- Use x,y coord of top, bottom, left and right edges of the rect.
- We can set any of these values to establish the current position of the rect.
- When working center of a game element, use
center
,centerx
, orcentery
attribute of a rect. - When working with edge of the screen: work with
top
,bottom
,left
orright
attributes. - When adjust horizontal/vertical placement of rect, just use
x
&y
attributes. (0,0)
is the top-left corner of the screen.
Drawing the Ship to the Screen
Modify alien_invasion.py
by:
- Add ship object in
__init__
- Add
self.ship.blitme()
in screen redraw code block
Refactoring: The _check_events() and _update_screen() methods
Refactor code:
- simplifies the structure
- Make it easier to build on.
Here we refactor the code by break the run_game()
:
- Move code for managing events to
_check_events()
- Move code for updating the screen to
_update_screen()
helper method:
- is a method in class but isn't meant to be called through an instance.
- syntax: a single leading underscore
_check_events()
Method
Create function in AlienInvasion
and replace original code snippet in while loop
def _check_events(self):
"""Respond to keypresses and mouse events."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
_update_screen()
Method
Create function _update_screen()
and replace original code snippet
def _update_screen(self):
"""Update images on the screen, and flip to the new screen"""
self.screen.fill(self.settings.bg_color)
self.ship.blitme()
def run_game(self):
"""Start the main loop for the game"""
while True:
self._check_events()
self._update_screen()
After replacing lengthy codes with helper function, run_game
now become simpler. This is a way of refactoring the code
Piloting the Ship
Now write code that responds when player presses the right or left arrow key.
Responding to a Keypress
- Whenever the player presses a key, the keypress is registered in Pygame as an event.
- Each event is picked by
pygame.event.get()
method. - We can specify in
_check_events()
method what kind of events we want the game to check for. - Each keypress is registered as a
KEYDOWN
event - If a
K_RIGHT
event is found, move the ship to right by increasing the valueself.ship.rect.x
by 1
def _check_events(self):
"""Respond to keypresses and mouse events."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
self.ship.rect.x += 1
Allowing Continuous Movement
- We can detect whether the key is released using
KEYUP
event. - We can use
KEYDOWN
&KEYUP
events together with a flag calledmoving_right
to implement continuous motion- When
moving_right
flag isFalse
, the ship will be motionless. - Create an attribute called
moving_right
andupdate()
method to check the status of themoving_right
flag.
- When
Change alien_invasion.py
:
def run_game(self):
"""Start the main loop for the game"""
while True:
self._check_events()
self.ship.update()
self._update_screen()
def _check_events(self):
"""Respond to keypresses and mouse events."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.type == pygame.KEYUP:
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
Change ship.py
:
def __init__(self, ai_game):
"""Initialize the ship and set its starting position"""
self.screen = ai_game.screen
self.screen_rect = ai_game.screen.get_rect()
# Load the ship image and get its rect
self.image = pygame.image.load('ship.bmp')
self.rect = self.image.get_rect()
# Start each new ship at the bottom center of the screen
self.rect.midbottom = self.screen_rect.midbottom
# Movement flag
self.moving_right = False
def update(self):
"""Update the ship's position based on movement flag"""
if self.moving_right:
self.rect.x += 1
Moving Both Left and Right
Apply the same logic on left movement
Adjusting the Ship's Speed
- Add
ship_speed
attribute to theSettings
class - Modify
update()
&__init__
method ofShip
, so it move by defined speed
class Settings:
"""A class to store all settings for Alien Invasion"""
def __init__(self):
"""Initialize the game's settings."""
# Screen settings
self.screen_width = 1200
self.screen_height = 800
self.bg_color = (230,230,230)
# Ship settings
self.ship_speed = 1.5
Modify ship.py
def __init__(self, ai_game):
"""Initialize the ship and set its starting position"""
self.screen = ai_game.screen
self.screen_rect = ai_game.screen.get_rect()
self.settings = ai_game.settings
# Load the ship image and get its rect
self.image = pygame.image.load('ship.bmp')
self.rect = self.image.get_rect()
# Start each new ship at the bottom center of the screen
self.rect.midbottom = self.screen_rect.midbottom
# Store a decimal value for the ship's horizontal position
self.x = float(self.rect.x)
# Movement flag
self.moving_right = False
self.moving_left = False
def update(self):
"""Update the ship's position based on movement flag"""
# Update the ship's x value, not the rect
if self.moving_right:
self.rect.x += self.settings.ship_speed
if self.moving_left:
self.rect.x -= self.settings.ship_speed
Limiting the Ship's Range
Add limit of ship's range
def update(self):
"""Update the ship's position based on movement flag"""
# Update the ship's x value, not the rect
if self.moving_right and self.rect.right < self.screen_rect.right:
self.rect.x += self.settings.ship_speed
if self.moving_left and self.rect.left > 0:
self.rect.x -= self.settings.ship_speed
Refactoring _check_events()
As _check_events()
is becoming lengthy, we should break/refactor it.
def _check_events(self):
"""Respond to keypresses and mouse events."""
for event in pygame.event.get():
if event.type == pygame.QUIT:
sys.exit()
elif event.type == pygame.KEYDOWN:
self._check_keydown_events(event)
elif event.type == pygame.KEYUP:
self._check_keyup_events(event)
def _check_keydown_events(self, event):
"""Respond to keypresses"""
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
def _check_keyup_events(self, event):
"""Respond to key releases"""
if event.key == pygame.K_RIGHT:
self.ship.moving_right = False
elif event.key == pygame.K_LEFT:
self.ship.moving_left = False
Pressing Q to Quit
- use
pygame.K_q
key event (q) to exit program
def _check_keydown_events(self, event):
"""Respond to keypresses"""
if event.key == pygame.K_RIGHT:
self.ship.moving_right = True
elif event.key == pygame.K_LEFT:
self.ship.moving_left = True
elif event.key == pygame.K_q:
sys.exit()
Running the Game in Fullscreen Mode
To run the game in fullscreen mode
class AlienInvasion:
"""Overall class to manage game assets and behaviour"""
def __init__(self):
"""Initialize the game, and create game resources."""
pygame.init()
self.settings = Settings()
# self.screen = pygame.display.set_mode((self.settings.screen_width, self.settings.screen_height))
self.screen = pygame.display.set_mode((0,0), pygame.FULLSCREEN)
self.settings.screen_width = self.screen.get_rect().width
self.settings.screen_height = self.screen.get_rect().height
pygame.display.set_caption("Alien Invasion")
self.ship = Ship(self)
Shooting Bullets
Write code that fires a bullet, which is represented by a small rectangle, when the player presses the spacebar.
Adding the Bullet Settings
def __init__(self):
"""Initialize the game's settings."""
# Screen settings
...
# Ship settings
...
# Bullet settings
self.bullet_speed = 1.0
self.bullet_width = 3
self.bullet_height = 15
self.bullet_color = (60,60,60)
bullet.py
module
import pygame
from pygame.sprite import Sprite
class Bullet(Sprite):
"""
A class to manage bullets fired from the ship
"""
def __init__(self, ai_game):
"""Create a bullet object at the ship's current position."""
super().__init__()
self.screen = ai_game.screen
self.settings = ai_game.settings
self.color = self.settings.bullet_color
# Create a bullet rect at (0,0) and then set correct position
self.rect = pygame.Rect(0,0,self.settings.bullet_width, self.settings.bullet_height)
self.rect.midtop = ai_game.ship.rect.midtop
# Store the bullet's position as a decimal value
self.y = float(self.rect.y)
self.rect = ...
created bullet'srect
attribute.- First, initialize at
(0,0)
, with width/height stored inself.settings
- Then, move it to the correct location via
self.rect.midtop
, as the bullet's position depends on the ship's position
- First, initialize at
Then add update()
& draw_bullet()
for Bullet class
def update(self):
"""Move the bullet up the screen"""
# Update the decimal position of the bullet
self.y -= self.settings.bullet_speed
# Update the rect position
self.rect.y = self.y # When a bullet is fired, it moves up the screen
def draw_bullet(self):
"""Draw the bullet to the screen"""
pygame.draw.rect(self.screen, self.color, self.rect)
Storing Bullets in a Group
After defining the Bullet
class and its necessary settings, we can write code to fire a bullet each time the player presses the spacebar.
First, we'll create a group in AlienInvasion
to store all the live bullets, so we can manage the bullets that have already been fired.
pygame.sprite.Group
class behave like a list with some extra functionality that's helpful when building games. So we directly draw the group (instead of individual bullets) to the screen.
alien_invasion.py
def __init__(self):
...
self.ship = Ship(self)
self.bullets = pygame.sprite.Group()
...
def run_game(self):
"""Start the main loop for the game"""
while True:
self._check_events()
self.ship.update()
self.bullets.update() # initiate the bullets group
self._update_screen()
Firing Bullets
- Add key press event in
_check_keydown_events(self,event)
, which will trigger_fire_bullet()
byK_SPACE
- Create function
_fire_bullet(self)
- Add bullets group's drawing action in
_update_screen()
from bullet import Bullet
class AlienInvasion:
def _check_keydown_events(self, event):
"""Respond to keypresses"""
...
elif event.key == pygame.K_q:
sys.exit()
elif event.key == pygame.K_SPACE:
self._fire_bullet()
def _check_keyup_events(self, event):
...
def _fire_bullet(self):
"""Create a new bullet and add it to the bullets group"""
new_bullet = Bullet(self)
self.bullets.add(new_bullet)
def _update_screen(self):
"""Update images on the screen, and flip to the new screen"""
...
for bullet in self.bullets.sprites():
bullet.draw_bullet()
pygame.display.flip()
Deleting Old Bullets
Currently, bullets don't delete themselves after moving out of frame. Only their y-coordinate values grow increasingly negative.
- Con: bullets continue to consume memory and processing power.
Solution: need recycle these objects during running the game
def run_game(self):
"""Start the main loop for the game"""
while True:
...
# Delete bullets out of screen
for bullet in self.bullets.copy():
if bullet.rect.bottom <= 0:
self.bullets.remove(bullet)
print(len(self.bullets))
Limiting the Number of Bullets
To limit the number of bullets.
class Settings:
"""A class to store all settings for Alien Invasion"""
def __init__(self):
"""Initialize the game's settings."""
...
self.bullet_allowed = 3
Modify AlienInvasion
class as shown below
class AlienInvasion:
"""Overall class to manage game assets and behaviour"""
...
def _fire_bullet(self):
"""Create a new bullet and add it to the bullets group"""
if len(self.bullets) < self.settings.bullets_allowed:
new_bullet = Bullet(self)
self.bullets.add(new_bullet)
Creating the _update_bullets() Method
Need refactor run_game
function in AlienInvasion
class again, to make bullets update and deletion independent of run_game()
...
class AlienInvasion:
"""Overall class to manage game assets and behaviour"""
def run_game(self):
"""Start the main loop for the game"""
while True:
self._check_events()
self.ship.update()
self._update_bullets()
self._update_screen()
def _update_bullets(self):
"""Update position of bullets and get rid of old bullets"""
# Update bullet positions.
self.bullets.update()
# Delete bullets out of screen
for bullet in self.bullets.copy():
if bullet.rect.bottom <= 0:
self.bullets.remove(bullet)
print(len(self.bullets))
...