PyMunk平台#

../../_images/title_animated_gif.gif

本教程介绍如何使用Arcade及其Pymunk API编写平台。本教程假定您对Python、Arcade和 Tiled Map Editor

  • 如果您不熟悉使用Python语言进行编程,请查看https://learn.arcade.academy

  • 如果您不熟悉Arcade库,请通过 简单平台

  • 如果您不熟悉切片地图编辑器,请参阅 简单平台 还介绍了如何使用平铺地图编辑器创建地图。

常见问题#

在开始之前,有几个关于PyMunk物理引擎的问题需要指出:

  • 物体重叠-允许快速移动的物体与其碰撞的物体重叠,稍后PyMunk会将它们分开。看见 collision bias 了解更多信息。

  • 通过-快速移动的对象可以通过另一个对象,如果它的速度如此之快,以至于不会在帧之间与另一个对象重叠。看见 object tunneling

  • 当物理引擎在时间上向前移动时,默认设置是向前移动1/60秒。无论选择什么增量,增量都应该始终保持不变。不要使用变量Delta_time update 方法作为一个单元,否则结果将不稳定和不可预测。为了获得更准确的模拟,您可以每帧前进1/120秒两次。这会增加所需的时间,但需要更多时间进行计算。

  • 一个精灵在由许多矩形组成的地板上移动时,可能会被边缘“抓住”。球员精灵的角落可能会被地板精灵的角落抓住。要解决此问题,请确保球员精灵底部的命中框是圆形的。此外,还要考虑合并水平的精灵行的可能性。

打开一扇窗#

首先,让我们从一个使用Arcade打开空白窗口的程序开始。它还包含我们稍后将填充的方法的存根。尝试此代码,并确保您可以运行它。它应该会弹出一扇黑色的窗户。

启动程序#
 1"""
 2Example of Pymunk Physics Engine Platformer
 3"""
 4import arcade
 5
 6SCREEN_TITLE = "PyMunk Platformer"
 7
 8# Size of screen to show, in pixels
 9SCREEN_WIDTH = 800
10SCREEN_HEIGHT = 600
11
12
13class GameWindow(arcade.Window):
14    """ Main Window """
15
16    def __init__(self, width, height, title):
17        """ Create the variables """
18
19        # Init the parent class
20        super().__init__(width, height, title)
21
22    def setup(self):
23        """ Set up everything with the game """
24        pass
25
26    def on_key_press(self, key, modifiers):
27        """Called whenever a key is pressed. """
28        pass
29
30    def on_key_release(self, key, modifiers):
31        """Called when the user releases a key. """
32        pass
33
34    def on_update(self, delta_time):
35        """ Movement and game logic """
36        pass
37
38    def on_draw(self):
39        """ Draw everything """
40        self.clear()
41
42
43def main():
44    """ Main function """
45    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
46    window.setup()
47    arcade.run()
48
49
50if __name__ == "__main__":
51    main()

创建常量#

现在,让我们设置 import 语句,并定义我们要使用的常量。在本例中,我们得到了128x128像素的精灵瓷砖。它们被缩小到50%的宽度和50%的高度(比例为0.5)。屏幕大小设置为25x15网格。

为简单起见,本例不使用播放器滚动屏幕。看见 简单平台以滚动屏幕为中心移动

当你运行这个程序时,屏幕应该更大一些。

添加一些常量#
 1"""
 2Example of Pymunk Physics Engine Platformer
 3"""
 4import math
 5from typing import Optional
 6import arcade
 7
 8SCREEN_TITLE = "PyMunk Platformer"
 9
10# How big are our image tiles?
11SPRITE_IMAGE_SIZE = 128
12
13# Scale sprites up or down
14SPRITE_SCALING_PLAYER = 0.5
15SPRITE_SCALING_TILES = 0.5
16
17# Scaled sprite size for tiles
18SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)
19
20# Size of grid to show on screen, in number of tiles
21SCREEN_GRID_WIDTH = 25
22SCREEN_GRID_HEIGHT = 15
23
24# Size of screen to show, in pixels
25SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
26SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT
27
28
29class GameWindow(arcade.Window):

创建实例变量#

接下来,让我们创建要使用的实例变量,并将背景颜色设置为绿色: arcade.color.AMAZON

如果您不熟悉Python上的类型转换,那么您可能不熟悉这样的代码行:

self.player_list: Optional[arcade.SpriteList] = None

这意味着 player_list 属性将是 SpriteListNone 。如果您不想打字,那么下面的代码也同样有效:

self.player_list = None

运行此程序应该会显示相同的窗口,但背景为绿色。

创建实例变量#
 1class GameWindow(arcade.Window):
 2    """ Main Window """
 3
 4    def __init__(self, width, height, title):
 5        """ Create the variables """
 6
 7        # Init the parent class
 8        super().__init__(width, height, title)
 9
10        # Player sprite
11        self.player_sprite: Optional[arcade.Sprite] = None
12
13        # Sprite lists we need
14        self.player_list: Optional[arcade.SpriteList] = None
15        self.wall_list: Optional[arcade.SpriteList] = None
16        self.bullet_list: Optional[arcade.SpriteList] = None
17        self.item_list: Optional[arcade.SpriteList] = None
18
19        # Track the current state of what key is pressed
20        self.left_pressed: bool = False
21        self.right_pressed: bool = False
22
23        # Set background color
24        self.background_color = arcade.color.AMAZON

加载和显示地图#

要开始,请使用平铺贴图编辑器创建一张地图。放置您不想移动的项目,并在名为“Platures”的层中充当平台。将你想要推来推去的物品放在一个叫做“动态物品”的层中。将文件命名为“pymunk_test_map.tmx”,并放置在与您的代码完全相同的目录中。

../../_images/tiled_map.png

如果您不确定如何使用切片地图编辑器,请参阅 第八步--收集硬币

现在,在 setup 函数,我们将向以下位置添加代码:

  • 创建以下对象的实例 SpriteList 对于我们要处理的每一组精灵。

  • 创建玩家精灵。

  • 在平铺地图上阅读。

  • 从平铺地图中的层创建精灵。

备注

从切片地图图层制作精灵时,加载的图层名称必须匹配 一点儿没错 使用在平铺地图编辑器中创建的层。它区分大小写。

创造我们的精灵#
 1    def setup(self):
 2        """ Set up everything with the game """
 3
 4        # Create the sprite lists
 5        self.player_list = arcade.SpriteList()
 6        self.bullet_list = arcade.SpriteList()
 7
 8        # Map name
 9        map_name = ":resources:/tiled_maps/pymunk_test_map.json"
10
11        # Load in TileMap
12        tile_map = arcade.load_tilemap(map_name, SPRITE_SCALING_TILES)
13
14        # Pull the sprite layers out of the tile map
15        self.wall_list = tile_map.sprite_lists["Platforms"]
16        self.item_list = tile_map.sprite_lists["Dynamic Items"]
17
18        # Create player sprite
19        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png",
20                                           SPRITE_SCALING_PLAYER)
21        # Set player location
22        grid_x = 1
23        grid_y = 1
24        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
25        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
26        # Add to player sprite list
27        self.player_list.append(self.player_sprite)

如果我们不画精灵,那么精灵就没有意义了,所以在 on_draw 方法,让我们画出精灵列表。

画出我们的精灵#
1    def on_draw(self):
2        """ Draw everything """
3        self.clear()
4        self.wall_list.draw()
5        self.bullet_list.draw()
6        self.item_list.draw()
7        self.player_list.draw()

使用以下程序中的新增内容,运行您的程序应该会显示您创建的平铺地图:

../../_images/pymunk_demo_platformer_04.png

添加物理引擎#

下一步是添加物理引擎。

首先,为我们的物理添加一些常量。我们在这里设置:

  • 表示重力的常量。

  • “减震”的值。如果阻尼值为1.0,则一旦力不再作用于某一项目,该项目将失去其所有速度。阻尼值为0.5会导致50%的速度在1秒内丢失。值为0表示自由落体。

  • 摩擦力值。0.0表示冰,1.0表示橡胶。

  • 弥撒。物品默认为1。我们将玩家设置为2,这样她就可以更容易地推物品了。

  • 极限是运动员的水平和垂直速度。如果玩家被限制在恒定的速度,那么玩起来会更容易。而且更现实,因为它们不是在轮子上。

为物理学添加常量#
 1# --- Physics forces. Higher number, faster accelerating.
 2
 3# Gravity
 4GRAVITY = 1500
 5
 6# Damping - Amount of speed lost per second
 7DEFAULT_DAMPING = 1.0
 8PLAYER_DAMPING = 0.4
 9
10# Friction between objects
11PLAYER_FRICTION = 1.0
12WALL_FRICTION = 0.7
13DYNAMIC_ITEM_FRICTION = 0.6
14
15# Mass (defaults to 1)
16PLAYER_MASS = 2.0
17
18# Keep player from going too fast
19PLAYER_MAX_HORIZONTAL_SPEED = 450
20PLAYER_MAX_VERTICAL_SPEED = 1600

第二,在 __init__ 保持我们的物理引擎的方法:

添加物理引擎属性#
1        # Physics engine
2        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

第三,在 setup 方法我们创建物理引擎并添加精灵。播放器、墙和动态项都有不同的属性,因此它们是单独添加的。

用“Setup”方法将精灵添加到物理引擎#
 1        # Add to player sprite list
 2        self.player_list.append(self.player_sprite)
 3
 4        # --- Pymunk Physics Engine Setup ---
 5
 6        # The default damping for every object controls the percent of velocity
 7        # the object will keep each second. A value of 1.0 is no speed loss,
 8        # 0.9 is 10% per second, 0.1 is 90% per second.
 9        # For top-down games, this is basically the friction for moving objects.
10        # For platformers with gravity, this should probably be set to 1.0.
11        # Default value is 1.0 if not specified.
12        damping = DEFAULT_DAMPING
13
14        # Set the gravity. (0, 0) is good for outer space and top-down.
15        gravity = (0, -GRAVITY)
16
17        # Create the physics engine
18        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
19                                                         gravity=gravity)
20
21        # Add the player.
22        # For the player, we set the damping to a lower value, which increases
23        # the damping rate. This prevents the character from traveling too far
24        # after the player lets off the movement keys.
25        # Setting the moment of inertia to PymunkPhysicsEngine.MOMENT_INF prevents it from
26        # rotating.
27        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
28        # Friction is between two objects in contact. It is important to remember
29        # in top-down games that friction moving along the 'floor' is controlled
30        # by damping.
31        self.physics_engine.add_sprite(self.player_sprite,
32                                       friction=PLAYER_FRICTION,
33                                       mass=PLAYER_MASS,
34                                       moment_of_inertia=arcade.PymunkPhysicsEngine.MOMENT_INF,
35                                       collision_type="player",
36                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
37                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)
38
39        # Create the walls.
40        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
41        # move.
42        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
43        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
44        # repositioned by code and don't respond to physics forces.
45        # Dynamic is default.
46        self.physics_engine.add_sprite_list(self.wall_list,
47                                            friction=WALL_FRICTION,
48                                            collision_type="wall",
49                                            body_type=arcade.PymunkPhysicsEngine.STATIC)
50
51        # Create the items

第四,在 on_update 方法,我们将物理引擎的 step 方法。

用“Setup”方法将精灵添加到物理引擎#
1    def on_update(self, delta_time):
2        """ Movement and game logic """
3        self.physics_engine.step()

如果您运行该程序,并且您有悬而未决的动态物品,您应该会在游戏开始时看到它们坠落。

添加球员移动#

下一步是让球员动起来。在本节中,我们将介绍如何向左和向右移动。在下一节中,我们将演示如何跳跃。

我们将移动玩家的力定义为 PLAYER_MOVE_FORCE_ON_GROUND 。如果玩家碰巧在空中,我们稍后会施加不同的力。

添加玩家移动-常量和属性#
 1# Force applied while on the ground
 2PLAYER_MOVE_FORCE_ON_GROUND = 8000
 3
 4class GameWindow(arcade.Window):
 5    """ Main Window """
 6
 7    def __init__(self, width, height, title):
 8        """ Create the variables """
 9
10        # Init the parent class
11        super().__init__(width, height, title)
12
13        # Player sprite
14        self.player_sprite: Optional[arcade.Sprite] = None
15
16        # Sprite lists we need
17        self.player_list: Optional[arcade.SpriteList] = None
18        self.wall_list: Optional[arcade.SpriteList] = None
19        self.bullet_list: Optional[arcade.SpriteList] = None
20        self.item_list: Optional[arcade.SpriteList] = None
21
22        # Track the current state of what key is pressed
23        self.left_pressed: bool = False
24        self.right_pressed: bool = False

我们需要跟踪是否按下了左/右键。为此,我们定义了实例变量 left_pressedright_pressed 。在按键和释放处理程序中将这些设置为适当的值。

处理按键向上和向下事件#
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = True
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = True
 8
 9    def on_key_release(self, key, modifiers):
10        """Called when the user releases a key. """
11
12        if key == arcade.key.LEFT:
13            self.left_pressed = False
14        elif key == arcade.key.RIGHT:
15            self.right_pressed = False

最后,我们需要将正确的力量应用于 on_update 。力在元组中指定力,首先是水平力,其次是垂直力。

当我们移动到0和不移动到1时,我们也设置摩擦力。这对于获得逼真的移动很重要。

对移动玩家应用力#
 1    def on_update(self, delta_time):
 2        """ Movement and game logic """
 3
 4        # Update player forces based on keys pressed
 5        if self.left_pressed and not self.right_pressed:
 6            # Create a force to the left. Apply it.
 7            force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
 8            self.physics_engine.apply_force(self.player_sprite, force)
 9            # Set friction to zero for the player while moving
10            self.physics_engine.set_friction(self.player_sprite, 0)
11        elif self.right_pressed and not self.left_pressed:
12            # Create a force to the right. Apply it.
13            force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
14            self.physics_engine.apply_force(self.player_sprite, force)
15            # Set friction to zero for the player while moving
16            self.physics_engine.set_friction(self.player_sprite, 0)
17        else:
18            # Player's feet are not moving. Therefore up the friction so we stop.
19            self.physics_engine.set_friction(self.player_sprite, 1.0)
20
21        # Move items in the physics engine
22        self.physics_engine.step()

添加运动员跳跃#

要让玩家跳起来,我们需要:

  • 确保球员躺在地上。

  • 向上施加一种冲动力给玩家。

  • 当球员在空中时,改变他们的左/右力量。

我们可以查看精灵下面是否有精灵 is_on_ground 功能。否则,我们将能够在空中跳跃。(两次跳跃将允许这种情况发生一次。)

如果我们不允许球员在空中左右移动,他们将很难控制。如果我们允许他们以与地面相同的力量向左/向右移动,那通常就太多了。所以我们得到了不同的左/右作用力,这取决于我们是否在空中。

对于代码更改,首先我们将定义一些常量:

添加玩家跳跃常量#
1# Force applied when moving left/right in the air
2PLAYER_MOVE_FORCE_IN_AIR = 900
3
4# Strength of a jump
5PLAYER_JUMP_IMPULSE = 1800

我们将添加在我们跳跃时施加冲力的逻辑:

添加玩家跳跃-跳跃力#
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = True
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = True
 8        elif key == arcade.key.UP:
 9            # find out if player is standing on ground
10            if self.physics_engine.is_on_ground(self.player_sprite):
11                # She is! Go ahead and jump
12                impulse = (0, PLAYER_JUMP_IMPULSE)
13                self.physics_engine.apply_impulse(self.player_sprite, impulse)

然后,我们将根据是否接地来调整左/右力:

添加运动员跳跃-左/右力量选择#
 1    def on_update(self, delta_time):
 2        """ Movement and game logic """
 3
 4        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
 5        # Update player forces based on keys pressed
 6        if self.left_pressed and not self.right_pressed:
 7            # Create a force to the left. Apply it.
 8            if is_on_ground:
 9                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
10            else:
11                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)
12            self.physics_engine.apply_force(self.player_sprite, force)
13            # Set friction to zero for the player while moving
14            self.physics_engine.set_friction(self.player_sprite, 0)
15        elif self.right_pressed and not self.left_pressed:
16            # Create a force to the right. Apply it.
17            if is_on_ground:
18                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
19            else:
20                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)
21            self.physics_engine.apply_force(self.player_sprite, force)
22            # Set friction to zero for the player while moving
23            self.physics_engine.set_friction(self.player_sprite, 0)
24        else:
25            # Player's feet are not moving. Therefore up the friction so we stop.
26            self.physics_engine.set_friction(self.player_sprite, 1.0)
27

添加播放器动画#

为了创建播放器动画,我们创建了一个自定义子类 Sprite 。我们加载所需的每一帧动画,包括它的镜像。

我们将把玩家翻转到左边或右边。如果玩家在空中,我们还将在跳跃和下降图形之间进行切换。

因为物理引擎使用的是小的浮点数,所以它经常会以很小的幅度在零上和零下翻转。这是个好主意 not 若要在x和y在零附近浮动时更改动画。因此,在这段代码中,我们有一个“死区”。我们不会更改动画,直到它超出该区域。

在更改行走动画之前,我们还需要控制球员移动的距离,以使脚看起来与地面同步。

添加播放器动画-常量#
1DEAD_ZONE = 0.1
2
3# Constants used to track if the player is facing left or right
4RIGHT_FACING = 0
5LEFT_FACING = 1
6
7# How many pixels to move before we change the texture in the walking animation
8DISTANCE_TO_CHANGE_TEXTURE = 20
9

接下来,我们创建一个 Player 类的子级。 arcade.Sprite 。这个类将更新播放器动画。

这个 __init__ 方法加载所有纹理。在这里,我们使用的是Kennedy y.nl Toon Characters 1 打包。它有六个不同的字符,您可以使用相同的布局进行选择,因此它使更改变得与更改启用哪一行一样简单。有八种用于行走的纹理,以及用于空闲、跳跃和坠落的纹理。

由于角色可以面向左侧或右侧,因此我们使用 arcade.load_texture_pair 它将同时加载常规图像和镜像图像。

对于多帧行走动画,我们使用的是“里程表”。在更改动画之前,我们需要移动一定数量的像素。如果该值太小,我们的角色就会像弗雷德·弗林斯通一样移动她的腿,如果太大,看起来就像是在滑冰。我们跟踪当前纹理的索引,0-7,因为有8个。

任何被PyMunk引擎移动的精灵都会有它的 pymunk_moved 方法已调用。这可用于更新动画。

添加播放器动画-播放器类#
 1class PlayerSprite(arcade.Sprite):
 2    """ Player Sprite """
 3    def __init__(self):
 4        """ Init """
 5        # Let parent initialize
 6        super().__init__()
 7
 8        # Set our scale
 9        self.scale = SPRITE_SCALING_PLAYER
10
11        # Images from Kenney.nl's Character pack
12        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
13        main_path = ":resources:images/animated_characters/female_person/femalePerson"
14        # main_path = ":resources:images/animated_characters/male_person/malePerson"
15        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
16        # main_path = ":resources:images/animated_characters/zombie/zombie"
17        # main_path = ":resources:images/animated_characters/robot/robot"
18
19        # Load textures for idle standing
20        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")
21        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")
22        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")
23
24        # Load textures for walking
25        self.walk_textures = []
26        for i in range(8):
27            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
28            self.walk_textures.append(texture)
29
30        # Set the initial texture
31        self.texture = self.idle_texture_pair[0]
32
33        # Default to face-right
34        self.character_face_direction = RIGHT_FACING
35
36        # Index of our current texture
37        self.cur_texture = 0
38
39        # How far have we traveled horizontally since changing the texture
40        self.x_odometer = 0
41
42    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
43        """ Handle being moved by the pymunk engine """
44        # Figure out if we need to face left or right
45        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
46            self.character_face_direction = LEFT_FACING
47        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
48            self.character_face_direction = RIGHT_FACING
49
50        # Are we on the ground?
51        is_on_ground = physics_engine.is_on_ground(self)
52
53        # Add to the odometer how far we've moved
54        self.x_odometer += dx
55
56        # Jumping animation
57        if not is_on_ground:
58            if dy > DEAD_ZONE:
59                self.texture = self.jump_texture_pair[self.character_face_direction]
60                return
61            elif dy < -DEAD_ZONE:
62                self.texture = self.fall_texture_pair[self.character_face_direction]
63                return
64
65        # Idle animation
66        if abs(dx) <= DEAD_ZONE:
67            self.texture = self.idle_texture_pair[self.character_face_direction]
68            return
69
70        # Have we moved far enough to change the texture?
71        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
72
73            # Reset the odometer
74            self.x_odometer = 0
75
76            # Advance the walking animation
77            self.cur_texture += 1
78            if self.cur_texture > 7:
79                self.cur_texture = 0
80            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]

很重要!此时,我们仍在创建 arcade.Spritenot PlayerSprite 。我们需要回到 setup 方法,并替换创建 player 实例具有:

添加玩家动画-创建玩家类#
        # Create player sprite
        self.player_sprite = PlayerSprite()

我见过程序员犯的一个非常常见的错误(我自己也犯了这个错误)是忘记了最后一个部分。然后,当错误出现在设置中时,您可以花费大量时间查看Player类。

我们还需要返回并更改 __init__ 方法:

添加玩家动画-创建玩家类#
        # Player sprite
        self.player_sprite: Optional[PlayerSprite] = None

射出子弹#

让玩家投篮可以给我们的游戏增加很多东西。首先,我们将定义几个要使用的常量。射击子弹的力量有多大,子弹的质量,以及子弹的重力。

如果我们对子弹使用与其他物体相同的重力,它往往会下降得太快。如果我们希望它根本不下降,我们可以将其设置为零。

射弹.常量#
1# How much force to put on the bullet
2BULLET_MOVE_FORCE = 4500
3
4# Mass of the bullet
5BULLET_MASS = 0.1
6
7# Make bullet less affected by gravity
8BULLET_GRAVITY = 300

接下来,我们将添加一个鼠标按压处理程序来输入子弹射击代码。

我们需要:

  • 创建子弹精灵

  • 我们需要计算从玩家到鼠标点击的角度

  • 在远离玩家的正确方向上创建子弹,因为在玩家内部产生子弹会混淆物理引擎

  • 将子弹添加到物理引擎

  • 如果移动,则对子弹施加作用力。请注意,当我们调整子弹的角度时,我们不需要调整力的角度。

警告

你的平台会滚动吗?

如果您的窗口滚动,您需要添加坐标偏移量,否则角度计算将不正确。

警告

子弹还没消失呢!

如果子弹飞出屏幕,它不会消失,物理引擎仍然必须跟踪它。

射击子弹-鼠标按下#
 1    def on_mouse_press(self, x, y, button, modifiers):
 2        """ Called whenever the mouse button is clicked. """
 3
 4        bullet = arcade.SpriteSolidColor(width=20, height=5, color=arcade.color.DARK_YELLOW)
 5        self.bullet_list.append(bullet)
 6
 7        # Position the bullet at the player's current location
 8        start_x = self.player_sprite.center_x
 9        start_y = self.player_sprite.center_y
10        bullet.position = self.player_sprite.position
11
12        # Get from the mouse the destination location for the bullet
13        # IMPORTANT! If you have a scrolling screen, you will also need
14        # to add in self.view_bottom and self.view_left.
15        dest_x = x
16        dest_y = y
17
18        # Do math to calculate how to get the bullet to the destination.
19        # Calculation the angle in radians between the start points
20        # and end points. This is the angle the bullet will travel.
21        x_diff = dest_x - start_x
22        y_diff = dest_y - start_y
23        angle = math.atan2(y_diff, x_diff)
24
25        # What is the 1/2 size of this sprite, so we can figure out how far
26        # away to spawn the bullet
27        size = max(self.player_sprite.width, self.player_sprite.height) / 2
28
29        # Use angle to to spawn bullet away from player in proper direction
30        bullet.center_x += size * math.cos(angle)
31        bullet.center_y += size * math.sin(angle)
32
33        # Set angle of bullet
34        bullet.angle = math.degrees(angle)
35
36        # Gravity to use for the bullet
37        # If we don't use custom gravity, bullet drops too fast, or we have
38        # to make it go too fast.
39        # Force is in relation to bullet's angle.
40        bullet_gravity = (0, -BULLET_GRAVITY)
41
42        # Add the sprite. This needs to be done AFTER setting the fields above.
43        self.physics_engine.add_sprite(bullet,
44                                       mass=BULLET_MASS,
45                                       damping=1.0,
46                                       friction=0.6,
47                                       collision_type="bullet",
48                                       gravity=bullet_gravity,
49                                       elasticity=0.9)
50
51        # Add force to bullet
52        force = (BULLET_MOVE_FORCE, 0)
53        self.physics_engine.apply_force(bullet, force)

销毁子弹和物品#

本节有两个目标:

  • 如果子弹飞出屏幕,就把它扔掉

  • 处理子弹和其他物品的碰撞

如果子弹不在屏幕上,就把它销毁#

首先,我们将创建自定义项目符号类。这个类将定义 pymunk_moved 方法,每次子弹移动时检查我们的位置。如果我们的y值太低,我们将删除项目符号。

销毁子弹--子弹Sprite#
1class BulletSprite(arcade.SpriteSolidColor):
2    """ Bullet Sprite """
3    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
4        """ Handle when the sprite is moved by the physics engine. """
5        # If the bullet falls below the screen, remove it
6        if self.center_y < -100:
7            self.remove_from_sprite_lists()

当然,一旦我们创建了项目符号,我们必须更新我们的代码以使用它而不是普通的 arcade.Sprite 班级。

销毁子弹--子弹Sprite#
1        bullet = BulletSprite(width=20, height=5, color=arcade.color.DARK_YELLOW)
2        self.bullet_list.append(bullet)
3
4        # Position the bullet at the player's current location
5        start_x = self.player_sprite.center_x
6        start_y = self.player_sprite.center_y

处理冲突#

要处理冲突,我们可以添加自定义冲突处理程序回调。如果您还记得我们在物理引擎中添加项目时,我们为每个项目指定了一个碰撞类型,例如“墙”、“子弹”或“项目”。我们可以编写一个函数并注册它来处理所有的子弹/墙碰撞。

在这种情况下,击中墙壁的子弹就会消失。击中物品的子弹会使物品和子弹都消失。我们也可以添加代码来跟踪精灵的伤害,只有在施加了如此多的伤害后才能删除它。甚至根据它的健康状况改变质地。

销毁子弹-碰撞操纵者#
 1        def wall_hit_handler(bullet_sprite, _wall_sprite, _arbiter, _space, _data):
 2            """ Called for bullet/wall collision """
 3            bullet_sprite.remove_from_sprite_lists()
 4
 5        self.physics_engine.add_collision_handler("bullet", "wall", post_handler=wall_hit_handler)
 6
 7        def item_hit_handler(bullet_sprite, item_sprite, _arbiter, _space, _data):
 8            """ Called for bullet/wall collision """
 9            bullet_sprite.remove_from_sprite_lists()
10            item_sprite.remove_from_sprite_lists()
11
12        self.physics_engine.add_collision_handler("bullet", "item", post_handler=item_hit_handler)

添加移动平台#

我们可以添加对移动平台的支持。可以在对象层中添加平台。对象层允许将平台放置在任何位置,而不仅仅是精确的网格位置。对象层还允许我们为放置的每个瓷砖添加自定义特性。

../../_images/add_object_layer.png

添加对象层。#

一旦我们放置了瓷砖,我们就可以为它添加自定义属性。单击‘+’图标并添加以下全部或部分属性:

  • change_x

  • change_y

  • left_boundary

  • right_boundary

  • top_boundary

  • bottom_boundary

如果它们被命名为精确匹配,它们会自动将它们的值复制到同名的精灵属性中。

../../_images/add_custom_properties.png

添加自定义属性。#

现在我们需要更新代码。在……里面 GameWindow.__init__ 添加一行以为其创建属性 moving_sprites_list

移动平台-添加精灵列表#
        self.moving_sprites_list: Optional[arcade.SpriteList] = None

setup 方法,从TMX层加载精灵列表。

移动平台-添加精灵列表#
        self.moving_sprites_list = tile_map.sprite_lists['Moving Platforms']

也在 setup 方法,我们需要将这些精灵添加到物理引擎中。在本例中,我们将精灵添加为 KINEMATIC 。静止的精灵不会移动。动力学精灵可以移动,并且可以由其他对象对其施加力。运动学精灵确实会移动,但不受其他对象的影响。

移动平台-装载精灵#
        # Add kinematic sprites
        self.physics_engine.add_sprite_list(self.moving_sprites_list,
                                            body_type=arcade.PymunkPhysicsEngine.KINEMATIC)

我们需要画出移动平台的精灵。添加这一行后,您应该能够运行程序并看到来自该层的精灵,即使它们还没有移动。

移动平台-画出精灵#
1    def on_draw(self):
2        """ Draw everything """
3        self.clear()
4        self.wall_list.draw()
5        self.moving_sprites_list.draw()
6        self.bullet_list.draw()
7        self.item_list.draw()
8        self.player_list.draw()

接下来,我们需要让精灵们行动起来。首先,我们将检查是否设置了任何边界,以及是否需要反转方向。

之后,我们将创建一个速度矢量。速度以每秒像素为单位。在本例中,我假设用户以平铺为单位设置了每帧的速度,因此我们将进行转换。

警告

更改center_x和center_y不会移动精灵。如果要更改精灵的位置,请使用物理引擎的 set_position 方法。

此外,设置物品的位置也会将其“传送”到那里。物理引擎将很高兴地将该对象移动到另一个对象中。相反,设置项目的速度将导致物理引擎移动项目,从而将任何动态项目推到一边。

移动平台-移动精灵#
        # For each moving sprite, see if we've reached a boundary and need to
        # reverse course.
        for moving_sprite in self.moving_sprites_list:
            if moving_sprite.boundary_right and \
                    moving_sprite.change_x > 0 and \
                    moving_sprite.right > moving_sprite.boundary_right:
                moving_sprite.change_x *= -1
            elif moving_sprite.boundary_left and \
                    moving_sprite.change_x < 0 and \
                    moving_sprite.left > moving_sprite.boundary_left:
                moving_sprite.change_x *= -1
            if moving_sprite.boundary_top and \
                    moving_sprite.change_y > 0 and \
                    moving_sprite.top > moving_sprite.boundary_top:
                moving_sprite.change_y *= -1
            elif moving_sprite.boundary_bottom and \
                    moving_sprite.change_y < 0 and \
                    moving_sprite.bottom < moving_sprite.boundary_bottom:
                moving_sprite.change_y *= -1

            # Figure out and set our moving platform velocity.
            # Pymunk uses velocity is in pixels per second. If we instead have
            # pixels per frame, we need to convert.
            velocity = (moving_sprite.change_x * 1 / delta_time, moving_sprite.change_y * 1 / delta_time)
            self.physics_engine.set_velocity(moving_sprite, velocity)

添加梯子#

向我们的平台添加阶梯的第一步是修改 __init__ 要跟踪更多项目,请执行以下操作:

  • 有一个梯子精灵列表的参考

  • 为攀登动画添加纹理

  • 跟踪我们在y方向的移动

  • 添加一个布尔值来跟踪我们是否在梯子上/下

添加梯子-PlayerSprite类#
 1    def __init__(self,
 2                 ladder_list: arcade.SpriteList,
 3                 hit_box_algorithm: arcade.hitbox.HitBoxAlgorithm):
 4        """ Init """
 5        # Let parent initialize
 6        super().__init__()
 7
 8        # Set our scale
 9        self.scale = SPRITE_SCALING_PLAYER
10
11        # Images from Kenney.nl's Character pack
12        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
13        main_path = ":resources:images/animated_characters/female_person/femalePerson"
14        # main_path = ":resources:images/animated_characters/male_person/malePerson"
15        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
16        # main_path = ":resources:images/animated_characters/zombie/zombie"
17        # main_path = ":resources:images/animated_characters/robot/robot"
18
19        # Load textures for idle standing
20        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png",
21                                                          hit_box_algorithm=hit_box_algorithm)
22        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")
23        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")
24
25        # Load textures for walking
26        self.walk_textures = []
27        for i in range(8):
28            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
29            self.walk_textures.append(texture)
30
31        # Load textures for climbing
32        self.climbing_textures = []
33        texture = arcade.load_texture(f"{main_path}_climb0.png")
34        self.climbing_textures.append(texture)
35        texture = arcade.load_texture(f"{main_path}_climb1.png")
36        self.climbing_textures.append(texture)
37
38        # Set the initial texture
39        self.texture = self.idle_texture_pair[0]
40
41        # Default to face-right
42        self.character_face_direction = RIGHT_FACING
43
44        # Index of our current texture
45        self.cur_texture = 0
46
47        # How far have we traveled horizontally since changing the texture
48        self.x_odometer = 0
49        self.y_odometer = 0
50
51        self.ladder_list = ladder_list
52        self.is_on_ladder = False

接下来,在我们的 pymunk_moved 方法当我们在梯子上时,我们需要改变物理,并更新我们的玩家纹理。

当我们在梯子上时,我们将关闭重力,增加阻尼,并降低我们的最大垂直速度。如果我们脱离了阶梯,请重置这些属性。

当我们在梯子上,而不是在地面上时,我们将在几个攀登纹理之间交替。

添加梯子-PlayerSprite类#
 1    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
 2        """ Handle being moved by the pymunk engine """
 3        # Figure out if we need to face left or right
 4        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
 5            self.character_face_direction = LEFT_FACING
 6        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
 7            self.character_face_direction = RIGHT_FACING
 8
 9        # Are we on the ground?
10        is_on_ground = physics_engine.is_on_ground(self)
11
12        # Are we on a ladder?
13        if len(arcade.check_for_collision_with_list(self, self.ladder_list)) > 0:
14            if not self.is_on_ladder:
15                self.is_on_ladder = True
16                self.pymunk.gravity = (0, 0)
17                self.pymunk.damping = 0.0001
18                self.pymunk.max_vertical_velocity = PLAYER_MAX_HORIZONTAL_SPEED
19        else:
20            if self.is_on_ladder:
21                self.pymunk.damping = 1.0
22                self.pymunk.max_vertical_velocity = PLAYER_MAX_VERTICAL_SPEED
23                self.is_on_ladder = False
24                self.pymunk.gravity = None
25
26        # Add to the odometer how far we've moved
27        self.x_odometer += dx
28        self.y_odometer += dy
29
30        if self.is_on_ladder and not is_on_ground:
31            # Have we moved far enough to change the texture?
32            if abs(self.y_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
33
34                # Reset the odometer
35                self.y_odometer = 0
36
37                # Advance the walking animation
38                self.cur_texture += 1
39
40            if self.cur_texture > 1:
41                self.cur_texture = 0
42            self.texture = self.climbing_textures[self.cur_texture]
43            return
44
45        # Jumping animation
46        if not is_on_ground:
47            if dy > DEAD_ZONE:
48                self.texture = self.jump_texture_pair[self.character_face_direction]
49                return
50            elif dy < -DEAD_ZONE:
51                self.texture = self.fall_texture_pair[self.character_face_direction]
52                return
53
54        # Idle animation
55        if abs(dx) <= DEAD_ZONE:
56            self.texture = self.idle_texture_pair[self.character_face_direction]
57            return
58
59        # Have we moved far enough to change the texture?
60        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
61
62            # Reset the odometer
63            self.x_odometer = 0
64
65            # Advance the walking animation
66            self.cur_texture += 1
67            if self.cur_texture > 7:
68                self.cur_texture = 0
69            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]

然后我们只需要将一些变量添加到 __init__ 要追踪梯子,请执行以下操作:

添加梯子-游戏窗口初始化#
 1    def __init__(self, width, height, title):
 2        """ Create the variables """
 3
 4        # Init the parent class
 5        super().__init__(width, height, title)
 6
 7        # Player sprite
 8        self.player_sprite: Optional[PlayerSprite] = None
 9
10        # Sprite lists we need
11        self.player_list: Optional[arcade.SpriteList] = None
12        self.wall_list: Optional[arcade.SpriteList] = None
13        self.bullet_list: Optional[arcade.SpriteList] = None
14        self.item_list: Optional[arcade.SpriteList] = None
15        self.moving_sprites_list: Optional[arcade.SpriteList] = None
16        self.ladder_list: Optional[arcade.SpriteList] = None
17
18        # Track the current state of what key is pressed
19        self.left_pressed: bool = False
20        self.right_pressed: bool = False
21        self.up_pressed: bool = False
22        self.down_pressed: bool = False
23
24        # Physics engine
25        self.physics_engine: Optional[arcade.PymunkPhysicsEngine] = None
26
27        # Set background color
28        self.background_color = arcade.color.AMAZON

然后将梯形图层加载到 setup

添加梯子-游戏窗口设置#
        # Pull the sprite layers out of the tile map
        self.wall_list = tile_map.sprite_lists["Platforms"]
        self.item_list = tile_map.sprite_lists["Dynamic Items"]
        self.ladder_list = tile_map.sprite_lists["Ladders"]
        self.moving_sprites_list = tile_map.sprite_lists['Moving Platforms']

另外,将梯子列表传递给Player类:

添加梯子-游戏窗口设置#
        # Create player sprite
        self.player_sprite = PlayerSprite(self.ladder_list, hit_box_algorithm=arcade.hitbox.algo_detailed)

然后改变跳跃按钮,这样我们在梯子上就不会跳跃了。此外,我们还希望跟踪是否按下了向上键或向下键。

添加梯子-游戏窗口按下键#
 1    def on_key_press(self, key, modifiers):
 2        """Called whenever a key is pressed. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = True
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = True
 8        elif key == arcade.key.UP:
 9            self.up_pressed = True
10            # find out if player is standing on ground, and not on a ladder
11            if self.physics_engine.is_on_ground(self.player_sprite) \
12                    and not self.player_sprite.is_on_ladder:
13                # She is! Go ahead and jump
14                impulse = (0, PLAYER_JUMP_IMPULSE)
15                self.physics_engine.apply_impulse(self.player_sprite, impulse)
16        elif key == arcade.key.DOWN:
17            self.down_pressed = True

添加到Key Up处理程序跟踪哪个键被按下。

添加梯子-游戏窗口方向键打开#
 1    def on_key_release(self, key, modifiers):
 2        """Called when the user releases a key. """
 3
 4        if key == arcade.key.LEFT:
 5            self.left_pressed = False
 6        elif key == arcade.key.RIGHT:
 7            self.right_pressed = False
 8        elif key == arcade.key.UP:
 9            self.up_pressed = False
10        elif key == arcade.key.DOWN:
11            self.down_pressed = False

接下来,使用阶梯的新更新更改我们的更新。

添加梯子-更新时的游戏窗口#
 1    def on_update(self, delta_time):
 2        """ Movement and game logic """
 3
 4        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
 5        # Update player forces based on keys pressed
 6        if self.left_pressed and not self.right_pressed:
 7            # Create a force to the left. Apply it.
 8            if is_on_ground or self.player_sprite.is_on_ladder:
 9                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
10            else:
11                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)
12            self.physics_engine.apply_force(self.player_sprite, force)
13            # Set friction to zero for the player while moving
14            self.physics_engine.set_friction(self.player_sprite, 0)
15        elif self.right_pressed and not self.left_pressed:
16            # Create a force to the right. Apply it.
17            if is_on_ground or self.player_sprite.is_on_ladder:
18                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
19            else:
20                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)
21            self.physics_engine.apply_force(self.player_sprite, force)
22            # Set friction to zero for the player while moving
23            self.physics_engine.set_friction(self.player_sprite, 0)
24        elif self.up_pressed and not self.down_pressed:
25            # Create a force to the right. Apply it.
26            if self.player_sprite.is_on_ladder:
27                force = (0, PLAYER_MOVE_FORCE_ON_GROUND)
28                self.physics_engine.apply_force(self.player_sprite, force)
29                # Set friction to zero for the player while moving
30                self.physics_engine.set_friction(self.player_sprite, 0)
31        elif self.down_pressed and not self.up_pressed:
32            # Create a force to the right. Apply it.
33            if self.player_sprite.is_on_ladder:
34                force = (0, -PLAYER_MOVE_FORCE_ON_GROUND)
35                self.physics_engine.apply_force(self.player_sprite, force)
36                # Set friction to zero for the player while moving
37                self.physics_engine.set_friction(self.player_sprite, 0)

当然,别忘了画梯子:

添加梯子-游戏窗口按下键#
1    def on_draw(self):
2        """ Draw everything """
3        self.clear()
4        self.wall_list.draw()
5        self.ladder_list.draw()
6        self.moving_sprites_list.draw()
7        self.bullet_list.draw()
8        self.item_list.draw()
9        self.player_list.draw()