# Chapter 12. A Ship that Fires Bullets ## Starting the Game Project ```python 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** to `self.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 the `for` loop to run. ### Creating a Settings Class In order to manage the setting more efficiently, we move setting into a separate module `settings.py` ```python 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 ```python 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's `rect` 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`, or `centery` attribute of a rect. * When working with edge of the screen: work with `top`, `bottom`, `left` or `right` 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: 1. Add ship object in `__init__` 2. 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 ```python 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 ```python 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() ``` ```python 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 value `self.ship.rect.x` by 1 ```python 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 called `moving_right` to implement continuous motion * When `moving_right` flag is `False`, the ship will be motionless. * Create an attribute called `moving_right` and `update()` method to check the status of the `moving_right` flag. Change `alien_invasion.py`: ```python 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`: ```python 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 1. Add `ship_speed` attribute to the `Settings` class 2. Modify `update()` & `__init__` method of `Ship`, so it move by defined speed ```python 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` ```python 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 ```python 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. ```python 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 ```python 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 ```python 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 ```python 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 ```python 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's `rect` attribute. * First, initialize at `(0,0)`, with width/height stored in `self.settings` * Then, move it to the correct location via `self.rect.midtop`, as the bullet's position depends on the ship's position Then add `update()` & `draw_bullet()` for Bullet class ```python 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` ```python 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 1. Add key press event in `_check_keydown_events(self,event)`, which will trigger `_fire_bullet()` by `K_SPACE` 2. Create function `_fire_bullet(self)` 3. Add bullets group's drawing action in `_update_screen()` ```python 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 ```python 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. ```python 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 ```python 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()` ```python ... 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)) ... ```