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