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.resources import (
14 image_female_person_idle,
15 image_laser_blue01,
16 image_zombie_idle,
17)
18
19SPRITE_SCALING_PLAYER = 0.5
20SPRITE_SCALING_ENEMY = 0.5
21SPRITE_SCALING_BULLET = 1
22INDICATOR_BAR_OFFSET = 32
23ENEMY_ATTACK_COOLDOWN = 1
24BULLET_SPEED = 150
25BULLET_DAMAGE = 1
26PLAYER_HEALTH = 5
27
28SCREEN_WIDTH = 800
29SCREEN_HEIGHT = 600
30SCREEN_TITLE = "Sprite Health Bars"
31
32
33def sprite_off_screen(
34 sprite: arcade.Sprite,
35 screen_height: int = SCREEN_HEIGHT,
36 screen_width: int = SCREEN_WIDTH,
37) -> bool:
38 """Checks if a sprite is off-screen or not."""
39 return (
40 sprite.top < 0
41 or sprite.bottom > screen_height
42 or sprite.right < 0
43 or sprite.left > screen_width
44 )
45
46
47class Player(arcade.Sprite):
48 def __init__(self, bar_list: arcade.SpriteList) -> None:
49 super().__init__(
50 filename=image_female_person_idle,
51 scale=SPRITE_SCALING_PLAYER,
52 )
53 self.indicator_bar: IndicatorBar = IndicatorBar(
54 self, bar_list, (self.center_x, self.center_y)
55 )
56 self.health: int = PLAYER_HEALTH
57
58
59class Bullet(arcade.Sprite):
60 def __init__(self) -> None:
61 super().__init__(
62 filename=image_laser_blue01,
63 scale=SPRITE_SCALING_BULLET,
64 )
65
66 def on_update(self, delta_time: float = 1 / 60) -> None:
67 """Updates the bullet's position."""
68 self.position = (
69 self.center_x + self.change_x * delta_time,
70 self.center_y + self.change_y * delta_time,
71 )
72
73
74class IndicatorBar:
75 """
76 Represents a bar which can display information about a sprite.
77
78 :param Player owner: The owner of this indicator bar.
79 :param arcade.SpriteList sprite_list: The sprite list used to draw the indicator
80 bar components.
81 :param Tuple[float, float] position: The initial position of the bar.
82 :param arcade.Color full_color: The color of the bar.
83 :param arcade.Color background_color: The background color of the bar.
84 :param int width: The width of the bar.
85 :param int height: The height of the bar.
86 :param int border_size: The size of the bar's border.
87 """
88
89 def __init__(
90 self,
91 owner: Player,
92 sprite_list: arcade.SpriteList,
93 position: Tuple[float, float] = (0, 0),
94 full_color: arcade.Color = arcade.color.GREEN,
95 background_color: arcade.Color = arcade.color.BLACK,
96 width: int = 100,
97 height: int = 4,
98 border_size: int = 4,
99 ) -> None:
100 # Store the reference to the owner and the sprite list
101 self.owner: Player = owner
102 self.sprite_list: arcade.SpriteList = sprite_list
103
104 # Set the needed size variables
105 self._box_width: int = width
106 self._box_height: int = height
107 self._half_box_width: int = self._box_width // 2
108 self._center_x: float = 0.0
109 self._center_y: float = 0.0
110 self._fullness: float = 0.0
111
112 # Create the boxes needed to represent the indicator bar
113 self._background_box: arcade.SpriteSolidColor = arcade.SpriteSolidColor(
114 self._box_width + border_size,
115 self._box_height + border_size,
116 background_color,
117 )
118 self._full_box: arcade.SpriteSolidColor = arcade.SpriteSolidColor(
119 self._box_width,
120 self._box_height,
121 full_color,
122 )
123 self.sprite_list.append(self._background_box)
124 self.sprite_list.append(self._full_box)
125
126 # Set the fullness and position of the bar
127 self.fullness: float = 1.0
128 self.position: Tuple[float, float] = position
129
130 def __repr__(self) -> str:
131 return f"<IndicatorBar (Owner={self.owner})>"
132
133 @property
134 def background_box(self) -> arcade.SpriteSolidColor:
135 """Returns the background box of the indicator bar."""
136 return self._background_box
137
138 @property
139 def full_box(self) -> arcade.SpriteSolidColor:
140 """Returns the full box of the indicator bar."""
141 return self._full_box
142
143 @property
144 def fullness(self) -> float:
145 """Returns the fullness of the bar."""
146 return self._fullness
147
148 @fullness.setter
149 def fullness(self, new_fullness: float) -> None:
150 """Sets the fullness of the bar."""
151 # Check if new_fullness if valid
152 if not (0.0 <= new_fullness <= 1.0):
153 raise ValueError(
154 f"Got {new_fullness}, but fullness must be between 0.0 and 1.0."
155 )
156
157 # Set the size of the bar
158 self._fullness = new_fullness
159 if new_fullness == 0.0:
160 # Set the full_box to not be visible since it is not full anymore
161 self.full_box.visible = False
162 else:
163 # Set the full_box to be visible incase it wasn't then update the bar
164 self.full_box.visible = True
165 self.full_box.width = self._box_width * new_fullness
166 self.full_box.left = self._center_x - (self._box_width // 2)
167
168 @property
169 def position(self) -> Tuple[float, float]:
170 """Returns the current position of the bar."""
171 return self._center_x, self._center_y
172
173 @position.setter
174 def position(self, new_position: Tuple[float, float]) -> None:
175 """Sets the new position of the bar."""
176 # Check if the position has changed. If so, change the bar's position
177 if new_position != self.position:
178 self._center_x, self._center_y = new_position
179 self.background_box.position = new_position
180 self.full_box.position = new_position
181
182 # Make sure full_box is to the left of the bar instead of the middle
183 self.full_box.left = self._center_x - (self._box_width // 2)
184
185
186class MyGame(arcade.Window):
187 def __init__(self) -> None:
188 super().__init__(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
189
190 # Create sprite lists
191 self.bullet_list: arcade.SpriteList = arcade.SpriteList()
192 self.bar_list: arcade.SpriteList = arcade.SpriteList()
193 self.player_sprite_list: arcade.SpriteList = arcade.SpriteList()
194 self.enemy_sprite_list: arcade.SpriteList = arcade.SpriteList()
195
196 # Create player sprite
197 self.player_sprite = Player(self.bar_list)
198 self.player_sprite_list.append(self.player_sprite)
199
200 # Create enemy Sprite
201 self.enemy_sprite = arcade.Sprite(image_zombie_idle, SPRITE_SCALING_ENEMY)
202 self.enemy_sprite_list.append(self.enemy_sprite)
203
204 # Create text objects
205 self.top_text: arcade.Text = arcade.Text(
206 "Dodge the bullets by moving the mouse!",
207 self.width // 2,
208 self.height - 50,
209 anchor_x="center",
210 )
211 self.bottom_text: arcade.Text = arcade.Text(
212 "When your health bar reaches zero, you lose!",
213 self.width // 2,
214 50,
215 anchor_x="center",
216 )
217 self.enemy_timer = 0
218
219 def setup(self) -> None:
220 """Set up the game and initialize the variables."""
221 # Setup player and enemy positions
222 self.player_sprite.position = self.width // 2, self.height // 4
223 self.enemy_sprite.position = self.width // 2, self.height // 2
224
225 # Set the background color
226 self.background_color = arcade.color.AMAZON
227
228 def on_draw(self) -> None:
229 """Render the screen."""
230 # Clear the screen. This command has to happen before we start drawing
231 self.clear()
232
233 # Draw all the sprites
234 self.player_sprite_list.draw()
235 self.enemy_sprite_list.draw()
236 self.bullet_list.draw()
237 self.bar_list.draw()
238
239 # Draw the text objects
240 self.top_text.draw()
241 self.bottom_text.draw()
242
243 def on_mouse_motion(self, x: float, y: float, dx: float, dy: float) -> None:
244 """Called whenever the mouse moves."""
245 self.player_sprite.position = x, y
246
247 def on_update(self, delta_time) -> None:
248 """Movement and game logic."""
249 # Check if the player is dead. If so, exit the game
250 if self.player_sprite.health <= 0:
251 arcade.exit()
252
253 # Increase the enemy's timer
254 self.enemy_timer += delta_time
255
256 # Update the player's indicator bar position
257 self.player_sprite.indicator_bar.position = (
258 self.player_sprite.center_x,
259 self.player_sprite.center_y + INDICATOR_BAR_OFFSET,
260 )
261
262 # Call updates on bullet sprites
263 self.bullet_list.on_update(delta_time)
264
265 # Check if the enemy can attack. If so, shoot a bullet from the
266 # enemy towards the player
267 if self.enemy_timer >= ENEMY_ATTACK_COOLDOWN:
268 self.enemy_timer = 0
269
270 # Create the bullet
271 bullet = Bullet()
272
273 # Set the bullet's position
274 bullet.position = self.enemy_sprite.position
275
276 # Set the bullet's angle to face the player
277 diff_x = self.player_sprite.center_x - self.enemy_sprite.center_x
278 diff_y = self.player_sprite.center_y - self.enemy_sprite.center_y
279 angle = math.atan2(diff_y, diff_x)
280 angle_deg = math.degrees(angle)
281 if angle_deg < 0:
282 angle_deg += 360
283 bullet.angle = angle_deg
284
285 # Give the bullet a velocity towards the player
286 bullet.change_x = math.cos(angle) * BULLET_SPEED
287 bullet.change_y = math.sin(angle) * BULLET_SPEED
288
289 # Add the bullet to the bullet list
290 self.bullet_list.append(bullet)
291
292 # Loop through each bullet
293 for existing_bullet in self.bullet_list:
294 # Check if the bullet has gone off-screen. If so, delete the bullet
295 if sprite_off_screen(existing_bullet):
296 existing_bullet.remove_from_sprite_lists()
297 continue
298
299 # Check if the bullet has hit the player
300 if arcade.check_for_collision(existing_bullet, self.player_sprite):
301 # Damage the player and remove the bullet
302 self.player_sprite.health -= BULLET_DAMAGE
303 existing_bullet.remove_from_sprite_lists()
304
305 # Set the player's indicator bar fullness
306 self.player_sprite.indicator_bar.fullness = (
307 self.player_sprite.health / PLAYER_HEALTH
308 )
309
310
311def main() -> None:
312 """Main Program."""
313 window = MyGame()
314 window.setup()
315 arcade.run()
316
317
318if __name__ == "__main__":
319 main()