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