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