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.
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()