Hit Points and Health Bars#
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.
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()