生命点数和生命值栏#

敌人用健康指示条向球员射击的屏幕截图

此示例演示了在角色上方绘制健康条的一种相当有效的方法。

屏幕中央的敌人向玩家发射子弹,而玩家试图通过移动鼠标来躲避子弹。每一颗击中球员的子弹都会降低球员的健康,这一点由球员头顶上的横条显示。当玩家的生命条为空(零)时,游戏结束。

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