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