Hit Points and Health Bars#

Screenshot of an enemy shooting at a player with an health indicator bar

This example demonstrates a reasonably efficient way of drawing a health bar above a character.

The enemy at the center of the screen shoots bullets at the player, while the player attempts to dodge the bullets by moving the mouse. Each bullet that hits the player reduces the player’s health, which is shown by the bar above the player’s head. When the player’s health bar is empty (zero), the game ends.

sprite_health.py#
  1"""
  2Sprite Health Bars
  3
  4Artwork from https://kenney.nl
  5
  6If Python and Arcade are installed, this example can be run from the command line with:
  7python -m arcade.examples.sprite_health
  8"""
  9import math
 10from typing import Tuple
 11
 12import arcade
 13from arcade.resources import (
 14    image_female_person_idle,
 15    image_laser_blue01,
 16    image_zombie_idle,
 17)
 18
 19SPRITE_SCALING_PLAYER = 0.5
 20SPRITE_SCALING_ENEMY = 0.5
 21SPRITE_SCALING_BULLET = 1
 22INDICATOR_BAR_OFFSET = 32
 23ENEMY_ATTACK_COOLDOWN = 1
 24BULLET_SPEED = 150
 25BULLET_DAMAGE = 1
 26PLAYER_HEALTH = 5
 27
 28SCREEN_WIDTH = 800
 29SCREEN_HEIGHT = 600
 30SCREEN_TITLE = "Sprite Health Bars"
 31
 32
 33def sprite_off_screen(
 34    sprite: arcade.Sprite,
 35    screen_height: int = SCREEN_HEIGHT,
 36    screen_width: int = SCREEN_WIDTH,
 37) -> bool:
 38    """Checks if a sprite is off-screen or not."""
 39    return (
 40        sprite.top < 0
 41        or sprite.bottom > screen_height
 42        or sprite.right < 0
 43        or sprite.left > screen_width
 44    )
 45
 46
 47class Player(arcade.Sprite):
 48    def __init__(self, bar_list: arcade.SpriteList) -> None:
 49        super().__init__(
 50            filename=image_female_person_idle,
 51            scale=SPRITE_SCALING_PLAYER,
 52        )
 53        self.indicator_bar: IndicatorBar = IndicatorBar(
 54            self, bar_list, (self.center_x, self.center_y)
 55        )
 56        self.health: int = PLAYER_HEALTH
 57
 58
 59class Bullet(arcade.Sprite):
 60    def __init__(self) -> None:
 61        super().__init__(
 62            filename=image_laser_blue01,
 63            scale=SPRITE_SCALING_BULLET,
 64        )
 65
 66    def on_update(self, delta_time: float = 1 / 60) -> None:
 67        """Updates the bullet's position."""
 68        self.position = (
 69            self.center_x + self.change_x * delta_time,
 70            self.center_y + self.change_y * delta_time,
 71        )
 72
 73
 74class IndicatorBar:
 75    """
 76    Represents a bar which can display information about a sprite.
 77
 78    :param Player owner: The owner of this indicator bar.
 79    :param arcade.SpriteList sprite_list: The sprite list used to draw the indicator
 80    bar components.
 81    :param Tuple[float, float] position: The initial position of the bar.
 82    :param arcade.Color full_color: The color of the bar.
 83    :param arcade.Color background_color: The background color of the bar.
 84    :param int width: The width of the bar.
 85    :param int height: The height of the bar.
 86    :param int border_size: The size of the bar's border.
 87    """
 88
 89    def __init__(
 90        self,
 91        owner: Player,
 92        sprite_list: arcade.SpriteList,
 93        position: Tuple[float, float] = (0, 0),
 94        full_color: arcade.Color = arcade.color.GREEN,
 95        background_color: arcade.Color = arcade.color.BLACK,
 96        width: int = 100,
 97        height: int = 4,
 98        border_size: int = 4,
 99    ) -> None:
100        # Store the reference to the owner and the sprite list
101        self.owner: Player = owner
102        self.sprite_list: arcade.SpriteList = sprite_list
103
104        # Set the needed size variables
105        self._box_width: int = width
106        self._box_height: int = height
107        self._half_box_width: int = self._box_width // 2
108        self._center_x: float = 0.0
109        self._center_y: float = 0.0
110        self._fullness: float = 0.0
111
112        # Create the boxes needed to represent the indicator bar
113        self._background_box: arcade.SpriteSolidColor = arcade.SpriteSolidColor(
114            self._box_width + border_size,
115            self._box_height + border_size,
116            background_color,
117        )
118        self._full_box: arcade.SpriteSolidColor = arcade.SpriteSolidColor(
119            self._box_width,
120            self._box_height,
121            full_color,
122        )
123        self.sprite_list.append(self._background_box)
124        self.sprite_list.append(self._full_box)
125
126        # Set the fullness and position of the bar
127        self.fullness: float = 1.0
128        self.position: Tuple[float, float] = position
129
130    def __repr__(self) -> str:
131        return f"<IndicatorBar (Owner={self.owner})>"
132
133    @property
134    def background_box(self) -> arcade.SpriteSolidColor:
135        """Returns the background box of the indicator bar."""
136        return self._background_box
137
138    @property
139    def full_box(self) -> arcade.SpriteSolidColor:
140        """Returns the full box of the indicator bar."""
141        return self._full_box
142
143    @property
144    def fullness(self) -> float:
145        """Returns the fullness of the bar."""
146        return self._fullness
147
148    @fullness.setter
149    def fullness(self, new_fullness: float) -> None:
150        """Sets the fullness of the bar."""
151        # Check if new_fullness if valid
152        if not (0.0 <= new_fullness <= 1.0):
153            raise ValueError(
154                f"Got {new_fullness}, but fullness must be between 0.0 and 1.0."
155            )
156
157        # Set the size of the bar
158        self._fullness = new_fullness
159        if new_fullness == 0.0:
160            # Set the full_box to not be visible since it is not full anymore
161            self.full_box.visible = False
162        else:
163            # Set the full_box to be visible incase it wasn't then update the bar
164            self.full_box.visible = True
165            self.full_box.width = self._box_width * new_fullness
166            self.full_box.left = self._center_x - (self._box_width // 2)
167
168    @property
169    def position(self) -> Tuple[float, float]:
170        """Returns the current position of the bar."""
171        return self._center_x, self._center_y
172
173    @position.setter
174    def position(self, new_position: Tuple[float, float]) -> None:
175        """Sets the new position of the bar."""
176        # Check if the position has changed. If so, change the bar's position
177        if new_position != self.position:
178            self._center_x, self._center_y = new_position
179            self.background_box.position = new_position
180            self.full_box.position = new_position
181
182            # Make sure full_box is to the left of the bar instead of the middle
183            self.full_box.left = self._center_x - (self._box_width // 2)
184
185
186class MyGame(arcade.Window):
187    def __init__(self) -> None:
188        super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
189
190        # Create sprite lists
191        self.bullet_list: arcade.SpriteList = arcade.SpriteList()
192        self.bar_list: arcade.SpriteList = arcade.SpriteList()
193        self.player_sprite_list: arcade.SpriteList = arcade.SpriteList()
194        self.enemy_sprite_list: arcade.SpriteList = arcade.SpriteList()
195
196        # Create player sprite
197        self.player_sprite = Player(self.bar_list)
198        self.player_sprite_list.append(self.player_sprite)
199
200        # Create enemy Sprite
201        self.enemy_sprite = arcade.Sprite(image_zombie_idle, SPRITE_SCALING_ENEMY)
202        self.enemy_sprite_list.append(self.enemy_sprite)
203
204        # Create text objects
205        self.top_text: arcade.Text = arcade.Text(
206            "Dodge the bullets by moving the mouse!",
207            self.width // 2,
208            self.height - 50,
209            anchor_x="center",
210        )
211        self.bottom_text: arcade.Text = arcade.Text(
212            "When your health bar reaches zero, you lose!",
213            self.width // 2,
214            50,
215            anchor_x="center",
216        )
217        self.enemy_timer = 0
218
219    def setup(self) -> None:
220        """Set up the game and initialize the variables."""
221        # Setup player and enemy positions
222        self.player_sprite.position = self.width // 2, self.height // 4
223        self.enemy_sprite.position = self.width // 2, self.height // 2
224
225        # Set the background color
226        self.background_color = arcade.color.AMAZON
227
228    def on_draw(self) -> None:
229        """Render the screen."""
230        # Clear the screen. This command has to happen before we start drawing
231        self.clear()
232
233        # Draw all the sprites
234        self.player_sprite_list.draw()
235        self.enemy_sprite_list.draw()
236        self.bullet_list.draw()
237        self.bar_list.draw()
238
239        # Draw the text objects
240        self.top_text.draw()
241        self.bottom_text.draw()
242
243    def on_mouse_motion(self, x: float, y: float, dx: float, dy: float) -> None:
244        """Called whenever the mouse moves."""
245        self.player_sprite.position = x, y
246
247    def on_update(self, delta_time) -> None:
248        """Movement and game logic."""
249        # Check if the player is dead. If so, exit the game
250        if self.player_sprite.health <= 0:
251            arcade.exit()
252
253        # Increase the enemy's timer
254        self.enemy_timer += delta_time
255
256        # Update the player's indicator bar position
257        self.player_sprite.indicator_bar.position = (
258            self.player_sprite.center_x,
259            self.player_sprite.center_y + INDICATOR_BAR_OFFSET,
260        )
261
262        # Call updates on bullet sprites
263        self.bullet_list.on_update(delta_time)
264
265        # Check if the enemy can attack. If so, shoot a bullet from the
266        # enemy towards the player
267        if self.enemy_timer >= ENEMY_ATTACK_COOLDOWN:
268            self.enemy_timer = 0
269
270            # Create the bullet
271            bullet = Bullet()
272
273            # Set the bullet's position
274            bullet.position = self.enemy_sprite.position
275
276            # Set the bullet's angle to face the player
277            diff_x = self.player_sprite.center_x - self.enemy_sprite.center_x
278            diff_y = self.player_sprite.center_y - self.enemy_sprite.center_y
279            angle = math.atan2(diff_y, diff_x)
280            angle_deg = math.degrees(angle)
281            if angle_deg < 0:
282                angle_deg += 360
283            bullet.angle = angle_deg
284
285            # Give the bullet a velocity towards the player
286            bullet.change_x = math.cos(angle) * BULLET_SPEED
287            bullet.change_y = math.sin(angle) * BULLET_SPEED
288
289            # Add the bullet to the bullet list
290            self.bullet_list.append(bullet)
291
292        # Loop through each bullet
293        for existing_bullet in self.bullet_list:
294            # Check if the bullet has gone off-screen. If so, delete the bullet
295            if sprite_off_screen(existing_bullet):
296                existing_bullet.remove_from_sprite_lists()
297                continue
298
299            # Check if the bullet has hit the player
300            if arcade.check_for_collision(existing_bullet, self.player_sprite):
301                # Damage the player and remove the bullet
302                self.player_sprite.health -= BULLET_DAMAGE
303                existing_bullet.remove_from_sprite_lists()
304
305                # Set the player's indicator bar fullness
306                self.player_sprite.indicator_bar.fullness = (
307                    self.player_sprite.health / PLAYER_HEALTH
308                )
309
310
311def main() -> None:
312    """Main Program."""
313    window = MyGame()
314    window.setup()
315    arcade.run()
316
317
318if __name__ == "__main__":
319    main()