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