diff --git a/chap12_a_ship_that_fires_bullets.md b/chap12_a_ship_that_fires_bullets.md new file mode 100644 index 0000000..52e407c --- /dev/null +++ b/chap12_a_ship_that_fires_bullets.md @@ -0,0 +1,589 @@ +# 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)) +... +``` \ No newline at end of file diff --git a/src/project_i_alien_invasion/alien_invasion.py b/src/project_i_alien_invasion/alien_invasion.py new file mode 100644 index 0000000..a026276 --- /dev/null +++ b/src/project_i_alien_invasion/alien_invasion.py @@ -0,0 +1,92 @@ +import sys +import pygame +from settings import Settings +from ship import Ship +from bullet import Bullet + +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)) # Use default screen width/height + # self.screen = pygame.display.set_mode((0,0), pygame.FULLSCREEN) # Use full screen + + 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) + 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._update_bullets() + + 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: + 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 + elif event.key == pygame.K_q: + sys.exit() + elif event.key == pygame.K_SPACE: + self._fire_bullet() + + 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 + + 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) + + 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)) + + 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() + for bullet in self.bullets.sprites(): + bullet.draw_bullet() + + pygame.display.flip() + +if __name__ == '__main__': + ai = AlienInvasion() + ai.run_game() \ No newline at end of file diff --git a/src/project_i_alien_invasion/bullet.py b/src/project_i_alien_invasion/bullet.py new file mode 100644 index 0000000..42377ff --- /dev/null +++ b/src/project_i_alien_invasion/bullet.py @@ -0,0 +1,34 @@ +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) + + 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) \ No newline at end of file diff --git a/src/project_i_alien_invasion/settings.py b/src/project_i_alien_invasion/settings.py new file mode 100644 index 0000000..9f1e1fc --- /dev/null +++ b/src/project_i_alien_invasion/settings.py @@ -0,0 +1,20 @@ +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 + + # Bullet settings + self.bullet_speed = 1.0 + self.bullet_width = 3 + self.bullet_height = 15 + self.bullet_color = (60,60,60) + self.bullets_allowed = 3 \ No newline at end of file diff --git a/src/project_i_alien_invasion/ship.bmp b/src/project_i_alien_invasion/ship.bmp new file mode 100644 index 0000000..9a94402 Binary files /dev/null and b/src/project_i_alien_invasion/ship.bmp differ diff --git a/src/project_i_alien_invasion/ship.py b/src/project_i_alien_invasion/ship.py new file mode 100644 index 0000000..9d27b08 --- /dev/null +++ b/src/project_i_alien_invasion/ship.py @@ -0,0 +1,36 @@ +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() + 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 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 + + def blitme(self): + """Draw the ship at its current location""" + self.screen.blit(self.image,self.rect) \ No newline at end of file