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