【日常点滴016】python游戏库arcade结合Tiled map editor地图编辑器和Pymunk物理引擎制作游戏过程记录,并最终打包为exe文件

独此一家,建议收藏

前言

一、创建一个空白窗口

窗口打开后 暂时是黑色背景 后续我们一步步完善

step001.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"

# 屏幕尺寸 大小以像素为单位 此处是800宽 600高
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

    def setup(self):
        """ Set up everything with the game 设置游戏的各种起始数据"""
        pass

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        pass

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        pass

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        pass

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()

def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()
    

效果示例 是一个黑色的界面
在这里插入图片描述

二、创建很多全局能用的常量

常量定义很多固定的属性,例如子弹的速度,大小等,在全局都可以直接使用
这里新引入的math 数学库 和用于确定数据类型的 typing的 Optional方法

在这里插入图片描述

step002.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 128

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

    def setup(self):
        """ Set up everything with the game 设置游戏的各种起始数据"""
        pass

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        pass

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        pass

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        pass

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()

def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

运行效果同上一步 只不过屏幕更大了 掌握方法也可以自由调整
接下来学习背景颜色的设置

三、创建实例变量即代表各种精灵等的变量

step003.py代码和效果示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 128

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        self.player_sprite: Optional[arcade.Sprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)


    def setup(self):
        """ Set up everything with the game 设置游戏的各种起始数据"""
        pass

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        pass

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        pass

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        pass

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()

def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

屏幕颜色变成了绿色
在这里插入图片描述

四、加载并显示地图编辑器设计的地图

这里暂时无法放置地图文件链接,以后补百度网盘的

1.打开地图编辑器 并新建地图文件

左上角 文件> 新建 > 创建新地图> 之后会让设置组成地图的格子的大小和个数
我们还是用32*32像素的 可以相对
在这里插入图片描述

2.设置地图大小并命名保存

在这里插入图片描述
下面这个图片请忽略
在这里插入图片描述
命名并保存
在这里插入图片描述

初始地图界面 这是我二次创建的 原来100*100格子太多了
在这里插入图片描述

3.加载图片素材用于绘制地图

图片素材:这里称为图块集 或者图块 就是把一张具有各种界面元素的图片分解为许多小块后的集合
地图文件命名:pymunk_test_map
不同地图块层命名:
地形层 Platforms 创建墙体和地面
可被移动物体层 Dynamic Items 例如一些小箱子之类的物体
在这里插入图片描述

选择另存为,然后保存到当前代码所在文件夹

在这里插入图片描述

保存到当前代码所在文件夹

在这里插入图片描述

预览图块
重新加载图块时 还要重新命名 可以用同样的名字覆盖掉之前文件就行
在这里插入图片描述

重新创建了地图和图块集 看起来更加舒适
修改当前图层名字为 Platforms
在这里插入图片描述

4.在Platforms图层绘制墙体和地面等元素

绘制完记得ctrl+s保存
在这里插入图片描述
右键空白区域 再创建一个可被移动物体层 Dynamic Items
在这里插入图片描述

5.在代码中显示地图

在这里插入图片描述
在这里插入图片描述

运行发现界面空白部分太大
我们稍微调一下画面尺寸就好 让一个基础物体大小等于一个格子大小 下面有讲
在这里插入图片描述
在这里插入图片描述
然后效果如下 玩家的初始位置似乎还要再上移一格 那我们再调整初始位置
在这里插入图片描述
稍作优化 即可更改玩家位置
在这里插入图片描述

step004.py代码和效果示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        self.player_sprite: Optional[arcade.Sprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png",
                                           SPRITE_SCALING_PLAYER)
        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        pass

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        pass

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        pass

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()

def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

五、添加物理引擎,使物体具有物理属性

物理属性主要为:

属性名介绍
质量Mass自定义质量影响其他属性,质量大的可以推动质量小的物体
阻尼damping0~1之间数字越大,阻力越大,失速越快,1没力施加时瞬间时速,0不失速。
重力gravity一个加速度常数跳起来的时候可以落下
摩擦力0~1之间0是冰,1是胶水

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
效果展示
在这里插入图片描述

step005.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)
        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        self.player_sprite: Optional[arcade.Sprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png",
                                           SPRITE_SCALING_PLAYER)
        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        pass

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        pass

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()

def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

六、添加玩家移动功能

补充常量都在程序一开始的地方定义就好 大写更容易明白是常量
在这里插入图片描述
监听按键状态
在这里插入图片描述
在刷新方法里 持续监听按键事件 并给物理以移动需要的力量
在这里插入图片描述

效果示例 可以使用键盘的左右键移动了
在这里插入图片描述

step006.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)
        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        self.player_sprite: Optional[arcade.Sprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png",
                                           SPRITE_SCALING_PLAYER)
        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行
            force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个左或右按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

七、让玩家跳跃

再次定义新的常量 控制空中受力 和起跳力
在这里插入图片描述
引擎的 is_on_ground()方法 检测到在地面上时返回true 否则返回false
在这里插入图片描述

设置不同状态时 左右移动的力 也就是速度
在这里插入图片描述

step007.py效果示例

在这里插入图片描述

step007.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1800

class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)
        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        self.player_sprite: Optional[arcade.Sprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.png",
                                           SPRITE_SCALING_PLAYER)
        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            if is_on_ground:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个左或右按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

八、添加角色移动动画

让角色移动时候可以有连续的动作 看起来就像在跑步或者走路 而不是静止飘移
还要考虑角色的左右朝向问题 因此 我们再加些新的常量
在这里插入图片描述
添加新的类 单独定义玩家的一些属性和方法
在这里插入图片描述
定义了新的玩家类 那个在主程序类中 创建玩家的方法也相应要做改变
init 和 setup 方法里都要改 改完后运行 精灵就可以在移动时切换造型了
在这里插入图片描述
在这里插入图片描述

step008.py效果示例

在这里插入图片描述

step008.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1800

# Close enough to not-moving to have the animation go to idle.
# 设置这个常量 当玩家角色移动的距离小于0.1时 不让他有动画的走路表现 而是静止 比如在挨着墙的时候按方向键 不再有走路动画
DEAD_ZONE = 0.1

# Constants used to track if the player is facing left or right
# 设置面向右侧 为 0  面向左 为 1
RIGHT_FACING = 0
LEFT_FACING = 1

# How many pixels to move before we change the texture in the walking animation
# 动作切换是很快的事情,那怎么控制切换频率或者速度呢,我们设置这个变量,使角色移动量每大于20像素时再进行动画 切换
DISTANCE_TO_CHANGE_TEXTURE = 20

class PlayerSprite(arcade.Sprite):
    """ Player Sprite """
    def __init__(self):
        """ Init """
        # Let parent initialize 初始化父类方法
        super().__init__()

        # Set our scale 设置缩放系数
        self.scale = SPRITE_SCALING_PLAYER

        # Images from Kenney.nl's Character pack 图片来自 Kenney网站的素材 aracade 以默认内置
        # 以下 图片都可以用 选一个喜欢的造型集即可 名字是一样的 后缀不一样则造型不一样 所以main_path 只定义名字
        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
        main_path = ":resources:images/animated_characters/female_person/femalePerson"
        # main_path = ":resources:images/animated_characters/male_person/malePerson"
        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
        # main_path = ":resources:images/animated_characters/zombie/zombie"
        # main_path = ":resources:images/animated_characters/robot/robot"

        # Load textures for idle standing 使用load_texture_pair加载不同状态下的造型图片
        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")  # 站立造型
        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")  # 跳跃造型
        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")  # 下落造型

        # Load textures for walking 由于走路的造型较多 这里用循环去加载 然后放入列表中备用
        self.walk_textures = []
        for i in range(8):
            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
            self.walk_textures.append(texture)

        # Set the initial texture 默认的站立造型 (0,代表造型里的第一张图片,因为以后还会有左右不同方向的造型)
        self.texture = self.idle_texture_pair[0]

        # Hit box will be set based on the first image used. 角色边框由第一张造型图片定义 以便后续碰撞检测
        self.hit_box = self.texture.hit_box_points

        # Default to face-right 默认觉得是脸朝右的
        self.character_face_direction = RIGHT_FACING

        # Index of our current texture  当前造型图片在图片组中的索引值
        self.cur_texture = 0

        # How far have we traveled horizontally since changing the texture
        # 定义一个 记录上次变换造型以后 角色的移动距离  后续用于比较当他大于我们上面常量设置的值时 执行造型切换
        self.x_odometer = 0

    #  这是物理引擎一个处理移动的方法,只要这个精灵是由物理引擎创建的,那么物理引擎自己就会去调用这个固定方法的
    #  dx 代表每一帧之间 物体横向变化的距离  dy 代表每一帧之间 物体纵向变化的距离
    #  d_angle则代表每一帧之间 物体角度朝向的变化量
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle being moved by the pymunk engine 这是物理引擎一个处理移动的方法"""
        # Figure out if we need to face left or right  判断我们是否要改变角色的朝向
        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
            self.character_face_direction = LEFT_FACING
        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
            self.character_face_direction = RIGHT_FACING

        # Are we on the ground? 判断当前对象是否在地面上 self代表当前对象 谁用这个类创建的 那么self就是谁
        is_on_ground = physics_engine.is_on_ground(self)

        # Add to the odometer how far we've moved  将横向的变化量累加起来
        self.x_odometer += dx

        # Jumping animation
        if not is_on_ground:
            if dy > DEAD_ZONE:
                self.texture = self.jump_texture_pair[self.character_face_direction]
                return
            elif dy < -DEAD_ZONE:
                self.texture = self.fall_texture_pair[self.character_face_direction]
                return

        # Idle animation
        if abs(dx) <= DEAD_ZONE:
            self.texture = self.idle_texture_pair[self.character_face_direction]
            return

        # Have we moved far enough to change the texture?
        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:

            # Reset the odometer
            self.x_odometer = 0

            # Advance the walking animation
            self.cur_texture += 1
            if self.cur_texture > 7:
                self.cur_texture = 0
            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)
        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        # self.player_sprite: Optional[arcade.Sprite] = None
        self.player_sprite: Optional[PlayerSprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        # self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.p
        # ng",SPRITE_SCALING_PLAYER)
        self.player_sprite = PlayerSprite()

        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            if is_on_ground:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个左或右按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

九、增加射击功能

老规矩 在增加新的物体的时候 我们一般都会去创建一些关于这个物体的常量
在这里插入图片描述
创建一个鼠标监听事件 (arcade库默认会调用他)子弹暂时使用创建的矩形来表示
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

step009.py效果示例

点击鼠标时 子弹可以朝鼠标位置发射出来 并且碰到别的物体时候 可以反弹 具备正常物理属性
在这里插入图片描述

step009.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity 重力
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1800

# Close enough to not-moving to have the animation go to idle.
# 设置这个常量 当玩家角色移动的距离小于0.1时 不让他有动画的走路表现 而是静止 比如在挨着墙的时候按方向键 不再有走路动画
DEAD_ZONE = 0.1

# Constants used to track if the player is facing left or right
# 设置面向右侧 为 0  面向左 为 1
RIGHT_FACING = 0
LEFT_FACING = 1

# How many pixels to move before we change the texture in the walking animation
# 动作切换是很快的事情,那怎么控制切换频率或者速度呢,我们设置这个变量,使角色移动量每大于20像素时再进行动画 切换
DISTANCE_TO_CHANGE_TEXTURE = 20

# step009.py 添加
# How much force to put on the bullet  给子弹施加的初始动力
BULLET_MOVE_FORCE = 4500

# Mass of the bullet  子弹的质量
BULLET_MASS = 0.1

# Make bullet less affected by gravity 给子弹一个更小的重力值
BULLET_GRAVITY = 300


class PlayerSprite(arcade.Sprite):
    """ Player Sprite """
    def __init__(self):
        """ Init """
        # Let parent initialize 初始化父类方法
        super().__init__()

        # Set our scale 设置缩放系数
        self.scale = SPRITE_SCALING_PLAYER

        # Images from Kenney.nl's Character pack 图片来自 Kenney网站的素材 aracade 以默认内置
        # 以下 图片都可以用 选一个喜欢的造型集即可 名字是一样的 后缀不一样则造型不一样 所以main_path 只定义名字
        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
        main_path = ":resources:images/animated_characters/female_person/femalePerson"
        # main_path = ":resources:images/animated_characters/male_person/malePerson"
        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
        # main_path = ":resources:images/animated_characters/zombie/zombie"
        # main_path = ":resources:images/animated_characters/robot/robot"

        # Load textures for idle standing 使用load_texture_pair加载不同状态下的造型图片
        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")  # 站立造型
        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")  # 跳跃造型
        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")  # 下落造型

        # Load textures for walking 由于走路的造型较多 这里用循环去加载 然后放入列表中备用
        self.walk_textures = []
        for i in range(8):
            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
            self.walk_textures.append(texture)

        # Set the initial texture 默认的站立造型 (0,代表造型里的第一张图片,因为以后还会有左右不同方向的造型)
        self.texture = self.idle_texture_pair[0]

        # Hit box will be set based on the first image used. 角色边框由第一张造型图片定义 以便后续碰撞检测
        self.hit_box = self.texture.hit_box_points

        # Default to face-right 默认觉得是脸朝右的
        self.character_face_direction = RIGHT_FACING

        # Index of our current texture  当前造型图片在图片组中的索引值
        self.cur_texture = 0

        # How far have we traveled horizontally since changing the texture
        # 定义一个 记录上次变换造型以后 角色的移动距离  后续用于比较当他大于我们上面常量设置的值时 执行造型切换
        self.x_odometer = 0

    #  这是物理引擎一个处理移动的方法,只要这个精灵是由物理引擎创建的,那么物理引擎自己就会去调用这个固定方法的
    #  dx 代表每一帧之间 物体横向变化的距离  dy 代表每一帧之间 物体纵向变化的距离
    #  d_angle则代表每一帧之间 物体角度朝向的变化量
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle being moved by the pymunk engine 这是物理引擎一个处理移动的方法"""
        # Figure out if we need to face left or right  判断我们是否要改变角色的朝向
        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
            self.character_face_direction = LEFT_FACING
        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
            self.character_face_direction = RIGHT_FACING

        # Are we on the ground? 判断当前对象是否在地面上 self代表当前对象 谁用这个类创建的 那么self就是谁
        is_on_ground = physics_engine.is_on_ground(self)

        # Add to the odometer how far we've moved  将横向的变化量累加起来
        self.x_odometer += dx

        # Jumping animation
        if not is_on_ground:
            if dy > DEAD_ZONE:
                self.texture = self.jump_texture_pair[self.character_face_direction]
                return
            elif dy < -DEAD_ZONE:
                self.texture = self.fall_texture_pair[self.character_face_direction]
                return

        # Idle animation
        if abs(dx) <= DEAD_ZONE:
            self.texture = self.idle_texture_pair[self.character_face_direction]
            return

        # Have we moved far enough to change the texture?
        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:

            # Reset the odometer
            self.x_odometer = 0

            # Advance the walking animation
            self.cur_texture += 1
            if self.cur_texture > 7:
                self.cur_texture = 0
            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)
        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        # self.player_sprite: Optional[arcade.Sprite] = None
        self.player_sprite: Optional[PlayerSprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        # self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.p
        # ng",SPRITE_SCALING_PLAYER)
        self.player_sprite = PlayerSprite()

        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            if is_on_ground:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个左或右按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()

    # 当鼠标点击屏幕时 会给该方法传入以下参数 x :横坐标 y:纵坐标 button:鼠标左中右按的哪个键 modifiers:修饰键 alt shift 等这些
    def on_mouse_press(self, x, y, button, modifiers):
        """ Called whenever the mouse button is clicked.
        当鼠标点击时 本方法被调用"""

        # 使用创建据矩形的方法 创建一个子弹
        bullet = arcade.SpriteSolidColor(20, 5, arcade.color.DARK_YELLOW)
        self.bullet_list.append(bullet)  # 把子弹加入上面就定义过的子弹精灵组

        # Position the bullet at the player's current location
        # 定义子弹的初始位置 也就是精灵的位置
        start_x = self.player_sprite.center_x
        start_y = self.player_sprite.center_y
        bullet.position = self.player_sprite.position

        # Get from the mouse the destination location for the bullet
        # IMPORTANT! If you have a scrolling screen, you will also need
        # to add in self.view_bottom and self.view_left.
        # 记录点击位置的坐标
        dest_x = x
        dest_y = y

        # Do math to calculate how to get the bullet to the destination.
        # Calculation the angle in radians between the start points
        # and end points. This is the angle the bullet will travel.
        # 使用高一会学习的三角函数知识 计算 玩家相对于平面和鼠标点击位置之间的角度
        x_diff = dest_x - start_x
        y_diff = dest_y - start_y
        angle = math.atan2(y_diff, x_diff)

        # What is the 1/2 size of this sprite, so we can figure out how far
        # away to spawn the bullet
        size = max(self.player_sprite.width, self.player_sprite.height) / 2

        # Use angle to to spawn bullet away from player in proper direction
        # 加上size的好处是 让子弹从玩家图片外部发射出去,避免内部发射导致物理引擎误判
        bullet.center_x += size * math.cos(angle)
        bullet.center_y += size * math.sin(angle)

        # Set angle of bullet 设置子弹的朝向
        bullet.angle = math.degrees(angle)

        # Gravity to use for the bullet
        # If we don't use custom gravity, bullet drops too fast, or we have
        # to make it go too fast.
        # Force is in relation to bullet's angle.
        # 专门定义以下子弹垂直方向受到的重力 而不是默认的
        bullet_gravity = (0, -BULLET_GRAVITY)

        # Add the sprite. This needs to be done AFTER setting the fields above.
        # 使用物理引擎管理子弹 这个子弹是具有弹性的
        self.physics_engine.add_sprite(bullet,
                                       mass=BULLET_MASS,
                                       damping=1.0,
                                       friction=0.6,
                                       collision_type="bullet",
                                       gravity=bullet_gravity,
                                       elasticity=0.9)

        # Add force to bullet
        # 最后 让物理引擎给子弹施加一个力量,让他飞走
        force = (BULLET_MOVE_FORCE, 0)
        self.physics_engine.apply_force(bullet, force)

def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

十、让子弹消失

为了减轻程序的压力 我们规定子弹在小于某一个高度时即可消失
或者在碰到某物体时消失
首先 专门再定一个子弹类 让物理引擎来时时监测他
在这里插入图片描述
然后
在这里插入图片描述

step010.py效果示例(1)

在这里插入图片描述
当撞击某规定物体时候,和物体一起消失
在这里插入图片描述

step010.py效果示例(2)

在这里插入图片描述

step010.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity 重力
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1800

# Close enough to not-moving to have the animation go to idle.
# 设置这个常量 当玩家角色移动的距离小于0.1时 不让他有动画的走路表现 而是静止 比如在挨着墙的时候按方向键 不再有走路动画
DEAD_ZONE = 0.1

# Constants used to track if the player is facing left or right
# 设置面向右侧 为 0  面向左 为 1
RIGHT_FACING = 0
LEFT_FACING = 1

# How many pixels to move before we change the texture in the walking animation
# 动作切换是很快的事情,那怎么控制切换频率或者速度呢,我们设置这个变量,使角色移动量每大于20像素时再进行动画 切换
DISTANCE_TO_CHANGE_TEXTURE = 20

# step009.py 添加
# How much force to put on the bullet  给子弹施加的初始动力
BULLET_MOVE_FORCE = 4500

# Mass of the bullet  子弹的质量
BULLET_MASS = 0.1

# Make bullet less affected by gravity 给子弹一个更小的重力值
BULLET_GRAVITY = 300


class PlayerSprite(arcade.Sprite):
    """ Player Sprite """
    def __init__(self):
        """ Init """
        # Let parent initialize 初始化父类方法
        super().__init__()

        # Set our scale 设置缩放系数
        self.scale = SPRITE_SCALING_PLAYER

        # Images from Kenney.nl's Character pack 图片来自 Kenney网站的素材 aracade 以默认内置
        # 以下 图片都可以用 选一个喜欢的造型集即可 名字是一样的 后缀不一样则造型不一样 所以main_path 只定义名字
        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
        main_path = ":resources:images/animated_characters/female_person/femalePerson"
        # main_path = ":resources:images/animated_characters/male_person/malePerson"
        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
        # main_path = ":resources:images/animated_characters/zombie/zombie"
        # main_path = ":resources:images/animated_characters/robot/robot"

        # Load textures for idle standing 使用load_texture_pair加载不同状态下的造型图片
        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")  # 站立造型
        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")  # 跳跃造型
        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")  # 下落造型

        # Load textures for walking 由于走路的造型较多 这里用循环去加载 然后放入列表中备用
        self.walk_textures = []
        for i in range(8):
            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
            self.walk_textures.append(texture)

        # Set the initial texture 默认的站立造型 (0,代表造型里的第一张图片,因为以后还会有左右不同方向的造型)
        self.texture = self.idle_texture_pair[0]

        # Hit box will be set based on the first image used. 角色边框由第一张造型图片定义 以便后续碰撞检测
        self.hit_box = self.texture.hit_box_points

        # Default to face-right 默认觉得是脸朝右的
        self.character_face_direction = RIGHT_FACING

        # Index of our current texture  当前造型图片在图片组中的索引值
        self.cur_texture = 0

        # How far have we traveled horizontally since changing the texture
        # 定义一个 记录上次变换造型以后 角色的移动距离  后续用于比较当他大于我们上面常量设置的值时 执行造型切换
        self.x_odometer = 0

    #  这是物理引擎一个处理移动的方法,只要这个精灵是由物理引擎创建的,那么物理引擎自己就会去调用这个固定方法的
    #  dx 代表每一帧之间 物体横向变化的距离  dy 代表每一帧之间 物体纵向变化的距离
    #  d_angle则代表每一帧之间 物体角度朝向的变化量
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle being moved by the pymunk engine 这是物理引擎一个处理移动的方法"""
        # Figure out if we need to face left or right  判断我们是否要改变角色的朝向
        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
            self.character_face_direction = LEFT_FACING
        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
            self.character_face_direction = RIGHT_FACING

        # Are we on the ground? 判断当前对象是否在地面上 self代表当前对象 谁用这个类创建的 那么self就是谁
        is_on_ground = physics_engine.is_on_ground(self)

        # Add to the odometer how far we've moved  将横向的变化量累加起来
        self.x_odometer += dx

        # Jumping animation
        if not is_on_ground:
            if dy > DEAD_ZONE:
                self.texture = self.jump_texture_pair[self.character_face_direction]
                return
            elif dy < -DEAD_ZONE:
                self.texture = self.fall_texture_pair[self.character_face_direction]
                return

        # Idle animation
        if abs(dx) <= DEAD_ZONE:
            self.texture = self.idle_texture_pair[self.character_face_direction]
            return

        # Have we moved far enough to change the texture?
        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:

            # Reset the odometer
            self.x_odometer = 0

            # Advance the walking animation
            self.cur_texture += 1
            if self.cur_texture > 7:
                self.cur_texture = 0
            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]


class BulletSprite(arcade.SpriteSolidColor):
    """ Bullet Sprite """
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle when the sprite is moved by the physics engine. """
        # If the bullet falls below the screen, remove it
        # 当子弹下落并小于某一个值时 令其消失 这里设置为50 容易观察 负数则在掉出屏幕底部时消失(一般也就设置为负数)
        if self.center_y < 50:
            self.remove_from_sprite_lists()


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)
        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        # self.player_sprite: Optional[arcade.Sprite] = None
        self.player_sprite: Optional[PlayerSprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        # self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.p
        # ng",SPRITE_SCALING_PLAYER)
        self.player_sprite = PlayerSprite()

        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2
        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

        # step010.py添加 在setup 方法里面定义两个方法并调用

        def wall_hit_handler(bullet_sprite, _wall_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞墙时消失"""
            bullet_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "wall", post_handler=wall_hit_handler)

        def item_hit_handler(bullet_sprite, item_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞到物体时消失 """
            bullet_sprite.remove_from_sprite_lists()
            item_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "item", post_handler=item_hit_handler)

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            if is_on_ground:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个左或右按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()

    # 当鼠标点击屏幕时 会给该方法传入以下参数 x :横坐标 y:纵坐标 button:鼠标左中右按的哪个键 modifiers:修饰键 alt shift 等这些
    def on_mouse_press(self, x, y, button, modifiers):
        """ Called whenever the mouse button is clicked.
        当鼠标点击时 本方法被调用"""

        # 使用创建据矩形的方法 创建一个子弹
        # bullet = arcade.SpriteSolidColor(20, 5, arcade.color.DARK_YELLOW)
        bullet = BulletSprite(20, 5, arcade.color.DARK_YELLOW)
        self.bullet_list.append(bullet)   #  把子弹加入上面就定义过的子弹精灵组

        # Position the bullet at the player's current location
        # 定义子弹的初始位置 也就是精灵的位置
        start_x = self.player_sprite.center_x
        start_y = self.player_sprite.center_y
        bullet.position = self.player_sprite.position

        # Get from the mouse the destination location for the bullet
        # IMPORTANT! If you have a scrolling screen, you will also need
        # to add in self.view_bottom and self.view_left.
        # 记录点击位置的坐标
        dest_x = x
        dest_y = y

        # Do math to calculate how to get the bullet to the destination.
        # Calculation the angle in radians between the start points
        # and end points. This is the angle the bullet will travel.
        # 使用高一会学习的三角函数知识 计算 玩家相对于平面和鼠标点击位置之间的角度
        x_diff = dest_x - start_x
        y_diff = dest_y - start_y
        angle = math.atan2(y_diff, x_diff)

        # What is the 1/2 size of this sprite, so we can figure out how far
        # away to spawn the bullet
        size = max(self.player_sprite.width, self.player_sprite.height) / 2

        # Use angle to to spawn bullet away from player in proper direction
        # 加上size的好处是 让子弹从玩家图片外部发射出去,避免内部发射导致物理引擎误判
        bullet.center_x += size * math.cos(angle)
        bullet.center_y += size * math.sin(angle)

        # Set angle of bullet 设置子弹的朝向
        bullet.angle = math.degrees(angle)

        # Gravity to use for the bullet
        # If we don't use custom gravity, bullet drops too fast, or we have
        # to make it go too fast.
        # Force is in relation to bullet's angle.
        # 专门定义以下子弹垂直方向受到的重力 而不是默认的
        bullet_gravity = (0, -BULLET_GRAVITY)

        # Add the sprite. This needs to be done AFTER setting the fields above.
        # 使用物理引擎管理子弹 这个子弹是具有弹性的
        self.physics_engine.add_sprite(bullet,
                                       mass=BULLET_MASS,
                                       damping=1.0,
                                       friction=0.6,
                                       collision_type="bullet",
                                       gravity=bullet_gravity,
                                       elasticity=0.9)

        # Add force to bullet
        # 最后 让物理引擎给子弹施加一个力量,让他飞走
        force = (BULLET_MOVE_FORCE, 0)
        self.physics_engine.apply_force(bullet, force)


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

十一、添加自动移动的物体

1.在对象层加入物体图块并显示

回到地图编辑器 添加对象层 命名为 Moving Platforms
在这里插入图片描述
添加完 记得ctrl+s保存文件
在这里插入图片描述
先初始化这个层 然后再在setup()方法里加载
在这里插入图片描述
加载并放入引擎
在这里插入图片描述
x`

此时运行,发现界面已经成功展示,不过这个可移动物体还不能移动,我们需要额外对他添加属性。而这个可以在 Tiled 地图编辑器中设置

2.给对象层物体添加属性

先来说下精灵可以设置的基本属性
在这里插入图片描述
添加完 依旧记得先保存文件哦
在这里插入图片描述
在这里插入图片描述

然后我们继续补全代码,通过检测图块到达边界时的状态,进而调整改变量的正负来达到 让图块可以来回移动的目的
在这里插入图片描述

step011.py效果示例

在这里插入图片描述

step011.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity 重力
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1800

# Close enough to not-moving to have the animation go to idle.
# 设置这个常量 当玩家角色移动的距离小于0.1时 不让他有动画的走路表现 而是静止 比如在挨着墙的时候按方向键 不再有走路动画
DEAD_ZONE = 0.1

# Constants used to track if the player is facing left or right
# 设置面向右侧 为 0  面向左 为 1
RIGHT_FACING = 0
LEFT_FACING = 1

# How many pixels to move before we change the texture in the walking animation
# 动作切换是很快的事情,那怎么控制切换频率或者速度呢,我们设置这个变量,使角色移动量每大于20像素时再进行动画 切换
DISTANCE_TO_CHANGE_TEXTURE = 20

# step009.py 添加
# How much force to put on the bullet  给子弹施加的初始动力
BULLET_MOVE_FORCE = 4500

# Mass of the bullet  子弹的质量
BULLET_MASS = 0.1

# Make bullet less affected by gravity 给子弹一个更小的重力值
BULLET_GRAVITY = 300

class PlayerSprite(arcade.Sprite):
    """ Player Sprite """
    def __init__(self):
        """ Init """
        # Let parent initialize 初始化父类方法
        super().__init__()

        # Set our scale 设置缩放系数
        self.scale = SPRITE_SCALING_PLAYER

        # Images from Kenney.nl's Character pack 图片来自 Kenney网站的素材 aracade 以默认内置
        # 以下 图片都可以用 选一个喜欢的造型集即可 名字是一样的 后缀不一样则造型不一样 所以main_path 只定义名字
        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
        main_path = ":resources:images/animated_characters/female_person/femalePerson"
        # main_path = ":resources:images/animated_characters/male_person/malePerson"
        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
        # main_path = ":resources:images/animated_characters/zombie/zombie"
        # main_path = ":resources:images/animated_characters/robot/robot"

        # Load textures for idle standing 使用load_texture_pair加载不同状态下的造型图片
        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")  # 站立造型
        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")  # 跳跃造型
        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")  # 下落造型

        # Load textures for walking 由于走路的造型较多 这里用循环去加载 然后放入列表中备用
        self.walk_textures = []
        for i in range(8):
            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
            self.walk_textures.append(texture)

        # Set the initial texture 默认的站立造型 (0,代表造型里的第一张图片,因为以后还会有左右不同方向的造型)
        self.texture = self.idle_texture_pair[0]

        # Hit box will be set based on the first image used. 角色边框由第一张造型图片定义 以便后续碰撞检测
        self.hit_box = self.texture.hit_box_points

        # Default to face-right 默认觉得是脸朝右的
        self.character_face_direction = RIGHT_FACING

        # Index of our current texture  当前造型图片在图片组中的索引值
        self.cur_texture = 0

        # How far have we traveled horizontally since changing the texture
        # 定义一个 记录上次变换造型以后 角色的移动距离  后续用于比较当他大于我们上面常量设置的值时 执行造型切换
        self.x_odometer = 0

    #  这是物理引擎一个处理移动的方法,只要这个精灵是由物理引擎创建的,那么物理引擎自己就会去调用这个固定方法的
    #  dx 代表每一帧之间 物体横向变化的距离  dy 代表每一帧之间 物体纵向变化的距离
    #  d_angle则代表每一帧之间 物体角度朝向的变化量
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle being moved by the pymunk engine 这是物理引擎一个处理移动的方法"""
        # Figure out if we need to face left or right  判断我们是否要改变角色的朝向
        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
            self.character_face_direction = LEFT_FACING
        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
            self.character_face_direction = RIGHT_FACING

        # Are we on the ground? 判断当前对象是否在地面上 self代表当前对象 谁用这个类创建的 那么self就是谁
        is_on_ground = physics_engine.is_on_ground(self)

        # Add to the odometer how far we've moved  将横向的变化量累加起来
        self.x_odometer += dx

        # Jumping animation
        if not is_on_ground:
            if dy > DEAD_ZONE:
                self.texture = self.jump_texture_pair[self.character_face_direction]
                return
            elif dy < -DEAD_ZONE:
                self.texture = self.fall_texture_pair[self.character_face_direction]
                return

        # Idle animation
        if abs(dx) <= DEAD_ZONE:
            self.texture = self.idle_texture_pair[self.character_face_direction]
            return

        # Have we moved far enough to change the texture?
        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:

            # Reset the odometer
            self.x_odometer = 0

            # Advance the walking animation
            self.cur_texture += 1
            if self.cur_texture > 7:
                self.cur_texture = 0
            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]


class BulletSprite(arcade.SpriteSolidColor):
    """ Bullet Sprite """
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle when the sprite is moved by the physics engine. """
        # If the bullet falls below the screen, remove it
        # 当子弹下落并小于某一个值时 令其消失 这里设置为50 容易观察 负数则在掉出屏幕底部时消失(一般也就设置为负数)
        if self.center_y < 50:
            self.remove_from_sprite_lists()


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        # self.player_sprite: Optional[arcade.Sprite] = None
        self.player_sprite: Optional[PlayerSprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None

        # step011.py添加 可移动物体层 而不是动态物体层
        self.moving_sprites_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""
        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)
        # step011.py添加 移动物体层
        self.moving_sprites_list = arcade.tilemap.process_layer(my_map,
                                                                'Moving Platforms',
                                                                SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        # self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.p
        # ng",SPRITE_SCALING_PLAYER)
        self.player_sprite = PlayerSprite()

        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2

        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)
        # Add kinematic sprites
        self.physics_engine.add_sprite_list(self.moving_sprites_list,
                                            body_type=arcade.PymunkPhysicsEngine.KINEMATIC)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

        # step010.py添加 在setup 方法里面定义两个方法并调用

        def wall_hit_handler(bullet_sprite, _wall_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞墙时消失"""
            bullet_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "wall", post_handler=wall_hit_handler)

        def item_hit_handler(bullet_sprite, item_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞到物体时消失 """
            bullet_sprite.remove_from_sprite_lists()
            item_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "item", post_handler=item_hit_handler)

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            if is_on_ground:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个左或右按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # step011.py添加 当移动物体层的物体移动到给他定义的边缘时 让他再原路返回
        # For each moving sprite, see if we've reached a boundary and need to
        # reverse course.
        for moving_sprite in self.moving_sprites_list:
            # print(moving_sprite.top,moving_sprite.bottom,moving_sprite.boundary_top,moving_sprite.boundary_bottom,moving_sprite.change_y)
            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
            # velocity 是一个速度列表,第一个时横向速度,第二个是纵向速度(是每秒的改变量,因此下面我们要变成每帧的变化量再用)
            # 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)
        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面"""
        arcade.start_render()
        self.wall_list.draw()
        self.moving_sprites_list.draw()  # step011.py添加
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()

    # 当鼠标点击屏幕时 会给该方法传入以下参数 x :横坐标 y:纵坐标 button:鼠标左中右按的哪个键 modifiers:修饰键 alt shift 等这些
    def on_mouse_press(self, x, y, button, modifiers):
        print(x,y)
        """ Called whenever the mouse button is clicked.
        当鼠标点击时 本方法被调用"""
        # 使用创建据矩形的方法 创建一个子弹
        # bullet = arcade.SpriteSolidColor(20, 5, arcade.color.DARK_YELLOW)
        bullet = BulletSprite(20, 5, arcade.color.DARK_YELLOW)
        self.bullet_list.append(bullet)   #  把子弹加入上面就定义过的子弹精灵组

        # Position the bullet at the player's current location
        # 定义子弹的初始位置 也就是精灵的位置
        start_x = self.player_sprite.center_x
        start_y = self.player_sprite.center_y
        bullet.position = self.player_sprite.position

        # Get from the mouse the destination location for the bullet
        # IMPORTANT! If you have a scrolling screen, you will also need
        # to add in self.view_bottom and self.view_left.
        # 记录点击位置的坐标
        dest_x = x
        dest_y = y

        # Do math to calculate how to get the bullet to the destination.
        # Calculation the angle in radians between the start points
        # and end points. This is the angle the bullet will travel.
        # 使用高一会学习的三角函数知识 计算 玩家相对于平面和鼠标点击位置之间的角度
        x_diff = dest_x - start_x
        y_diff = dest_y - start_y
        angle = math.atan2(y_diff, x_diff)

        # What is the 1/2 size of this sprite, so we can figure out how far
        # away to spawn the bullet
        size = max(self.player_sprite.width, self.player_sprite.height) / 2

        # Use angle to to spawn bullet away from player in proper direction
        # 加上size的好处是 让子弹从玩家图片外部发射出去,避免内部发射导致物理引擎误判
        bullet.center_x += size * math.cos(angle)
        bullet.center_y += size * math.sin(angle)

        # Set angle of bullet 设置子弹的朝向
        bullet.angle = math.degrees(angle)

        # Gravity to use for the bullet
        # If we don't use custom gravity, bullet drops too fast, or we have
        # to make it go too fast.
        # Force is in relation to bullet's angle.
        # 专门定义以下子弹垂直方向受到的重力 而不是默认的
        bullet_gravity = (0, -BULLET_GRAVITY)

        # Add the sprite. This needs to be done AFTER setting the fields above.
        # 使用物理引擎管理子弹 这个子弹是具有弹性的
        self.physics_engine.add_sprite(bullet,
                                       mass=BULLET_MASS,
                                       damping=1.0,
                                       friction=0.6,
                                       collision_type="bullet",
                                       gravity=bullet_gravity,
                                       elasticity=0.9)

        # Add force to bullet
        # 最后 让物理引擎给子弹施加一个力量,让他飞走
        force = (BULLET_MOVE_FORCE, 0)
        self.physics_engine.apply_force(bullet, force)


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

十二、创建可以上下爬动的梯子

这是目前官方文档的最后一步了,看到这里恭喜你,很快就要结束本文档的折磨了,哈哈哈,加油

1.在地图编辑器创建梯子的图层

画完依旧要记得保存哦
在这里插入图片描述

2.在代码中加载图块层并显示

初始化是先创建一个变量
在这里插入图片描述
之后在setup()方法里把梯子图层加载进来
在这里插入图片描述

然后在屏幕绘制时调用以下draw()方法,把梯子在界面上显示
在这里插入图片描述
运行程序 已显示
在这里插入图片描述

3.让玩家能够爬上梯子并相应移动

那首先要加载玩家 爬梯子时的动作图 待会看起来会更真实一些
在这里插入图片描述

然后玩家可以爬梯子是因为玩家碰到了梯子,这种情况不像玩家落在地面或者碰到墙体一样,可以由物理引擎自动控制,需要我们自己手动去检测玩家和梯子的碰撞情况,而我们自己去检测后,还可以更新更多的属性值,让玩家在梯子上时,和在地面上或者空中时拥有不同的位移速度和重力等。
因此,我们在创建玩家类时,通过参数,把梯子精灵列表也一起传给玩家类,以便于后续玩家类中pymunk_moved()刷新精灵的方法进行使用。

在主程序setup()方法中更改玩家精灵的创建方式 第二个参数是传递的,物理引擎模式,Detailed是代表我们当前用的pymunk物理引擎

在这里插入图片描述
在玩家类里接收参数
在这里插入图片描述
接收参数后 设置相应的数据 包括纵向的变化量
在这里插入图片描述
根据玩家和梯子的碰撞状态 更改阻尼和重力等属性 这样玩家在梯子上和不在梯子上将有不同的属性
在这里插入图片描述
紧接着 设置在梯子上时动作的变化情况
在这里插入图片描述
添加相应的上下方向的按键事件 主要针对在梯子上时
在这里插入图片描述
在这里插入图片描述
在on_update()方法 里 添加针对上下按键按下后的响应逻辑
在这里插入图片描述

step012.py效果示例

在这里插入图片描述

step012.py代码示例
"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity 重力
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1800

# Close enough to not-moving to have the animation go to idle.
# 设置这个常量 当玩家角色移动的距离小于0.1时 不让他有动画的走路表现 而是静止 比如在挨着墙的时候按方向键 不再有走路动画
DEAD_ZONE = 0.1

# Constants used to track if the player is facing left or right
# 设置面向右侧 为 0  面向左 为 1
RIGHT_FACING = 0
LEFT_FACING = 1

# How many pixels to move before we change the texture in the walking animation
# 动作切换是很快的事情,那怎么控制切换频率或者速度呢,我们设置这个变量,使角色移动量每大于20像素时再进行动画 切换
DISTANCE_TO_CHANGE_TEXTURE = 20

# step009.py 添加
# How much force to put on the bullet  给子弹施加的初始动力
BULLET_MOVE_FORCE = 4500

# Mass of the bullet  子弹的质量
BULLET_MASS = 0.1

# Make bullet less affected by gravity 给子弹一个更小的重力值
BULLET_GRAVITY = 300


class PlayerSprite(arcade.Sprite):
    """ Player Sprite """
    def __init__(self, ladder_list: arcade.SpriteList,
                 hit_box_algorithm):
        """ Init """
        # Let parent initialize 初始化父类方法
        super().__init__()

        # Set our scale 设置缩放系数
        self.scale = SPRITE_SCALING_PLAYER

        # Images from Kenney.nl's Character pack 图片来自 Kenney网站的素材 aracade 以默认内置
        # 以下 图片都可以用 选一个喜欢的造型集即可 名字是一样的 后缀不一样则造型不一样 所以main_path 只定义名字
        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
        main_path = ":resources:images/animated_characters/female_person/femalePerson"
        # main_path = ":resources:images/animated_characters/male_person/malePerson"
        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
        # main_path = ":resources:images/animated_characters/zombie/zombie"
        # main_path = ":resources:images/animated_characters/robot/robot"

        # Load textures for idle standing 使用load_texture_pair加载不同状态下的造型图片
        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")  # 站立造型
        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")  # 跳跃造型
        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")  # 下落造型

        # Load textures for walking 由于走路的造型较多 这里用循环去加载 然后放入列表中备用
        self.walk_textures = []
        for i in range(8):
            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
            self.walk_textures.append(texture)
        # step012.py添加 爬梯子的造型加载
        # Load textures for climbing
        self.climbing_textures = []
        texture = arcade.load_texture(f"{main_path}_climb0.png")
        self.climbing_textures.append(texture)
        texture = arcade.load_texture(f"{main_path}_climb1.png")
        self.climbing_textures.append(texture)

        # Set the initial texture 默认的站立造型 (0,代表造型里的第一张图片,因为以后还会有左右不同方向的造型)
        self.texture = self.idle_texture_pair[0]

        # Hit box will be set based on the first image used. 角色边框由第一张造型图片定义 以便后续碰撞检测
        self.hit_box = self.texture.hit_box_points

        # Default to face-right 默认觉得是脸朝右的
        self.character_face_direction = RIGHT_FACING

        # Index of our current texture  当前造型图片在图片组中的索引值
        self.cur_texture = 0

        # How far have we traveled horizontally since changing the texture
        # 定义一个 记录上次变换造型以后 角色的移动距离  后续用于比较当他大于我们上面常量设置的值时 执行造型切换
        self.x_odometer = 0
        # step012.py添加
        self.y_odometer = 0
        self.ladder_list = ladder_list

        self.is_on_ladder = False

    #  这是物理引擎一个处理移动的方法,只要这个精灵是由物理引擎创建的,那么物理引擎自己就会去调用这个固定方法的
    #  dx 代表每一帧之间 物体横向变化的距离  dy 代表每一帧之间 物体纵向变化的距离
    #  d_angle则代表每一帧之间 物体角度朝向的变化量
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle being moved by the pymunk engine 这是物理引擎一个处理移动的方法"""
        # Figure out if we need to face left or right  判断我们是否要改变角色的朝向
        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
            self.character_face_direction = LEFT_FACING
        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
            self.character_face_direction = RIGHT_FACING
        # Are we on the ground? 判断当前对象是否在地面上 self代表当前对象 谁用这个类创建的 那么self就是谁
        is_on_ground = physics_engine.is_on_ground(self)
        # Are we on a ladder? 根据玩家精灵组和梯子精灵组的碰撞结果,设置不同的属性
        # 碰撞检测结果是个列表,被碰撞的精灵都会存进去,所以一旦碰撞,列表的长度就会大于0
        if len(arcade.check_for_collision_with_list(self, self.ladder_list)) > 0:
            # 碰撞以后 检查玩家是否在梯子上
            # 如果碰撞了 但是玩家状态显示不在梯子上 一定是刚刚上到梯子上来  则更改状态为 在梯子上,然后重力大小设置为0
            if not self.is_on_ladder:
                self.is_on_ladder = True
                self.pymunk.gravity = (0, 0)
                self.pymunk.damping = 0.0001  # 刚到梯子上时,玩家的速度还使用之前的速度,只不过会稍微衰减
                self.pymunk.max_vertical_velocity = PLAYER_MAX_HORIZONTAL_SPEED  # 在梯子上时最大的垂直速度
        # 如果玩家并没有碰撞到梯子
        else:
            # 但是玩家状态却还是在梯子上 那么 肯定是玩家刚从梯子上掉下来了
            if self.is_on_ladder:
                self.pymunk.damping = 1.0  # 让玩家瞬间失去原来在梯子上时的速度
                self.pymunk.max_vertical_velocity = PLAYER_MAX_VERTICAL_SPEED
                self.is_on_ladder = False
                self.pymunk.gravity = None  # 把原来的重力(0,0)清除 开始自由落体

        # Add to the odometer how far we've moved  将横向的变化量累加起来
        self.x_odometer += dx
        # step012.py添加 垂直方向的变化量
        self.y_odometer += dy

        # 设置在梯子上时的动作变化
        if self.is_on_ladder and not is_on_ground:
            # Have we moved far enough to change the texture?
            # 当在梯子上的垂直的变化量大于我们设定的可以进行动作切换的值时 改变动作 并重置变化量
            if abs(self.y_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
                # Reset the odometer
                self.y_odometer = 0
                # Advance the walking animation
                # 切换下一张动作图的索引
                self.cur_texture += 1
            # 有两张图就可以
            if self.cur_texture > 1:
                self.cur_texture = 0
            # 更改梯子上的动作
            self.texture = self.climbing_textures[self.cur_texture]
            return

        # Jumping animation
        if not is_on_ground:
            if dy > DEAD_ZONE:
                self.texture = self.jump_texture_pair[self.character_face_direction]
                return
            elif dy < -DEAD_ZONE:
                self.texture = self.fall_texture_pair[self.character_face_direction]
                return

        # Idle animation
        if abs(dx) <= DEAD_ZONE:
            self.texture = self.idle_texture_pair[self.character_face_direction]
            return

        # Have we moved far enough to change the texture?
        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:

            # Reset the odometer
            self.x_odometer = 0

            # Advance the walking animation
            self.cur_texture += 1
            if self.cur_texture > 7:
                self.cur_texture = 0
            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]


class BulletSprite(arcade.SpriteSolidColor):
    """ Bullet Sprite """
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle when the sprite is moved by the physics engine. """
        # If the bullet falls below the screen, remove it
        # 当子弹下落并小于某一个值时 令其消失 这里设置为50 容易观察 负数则在掉出屏幕底部时消失(一般也就设置为负数)
        if self.center_y < 50:
            self.remove_from_sprite_lists()


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        # self.player_sprite: Optional[arcade.Sprite] = None
        self.player_sprite: Optional[PlayerSprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None
        # step012.py添加
        self.ladder_list: Optional[arcade.SpriteList] = None

        # step011.py添加 可移动物体层(不受其他物体力量的影响) 而不是动态物体层(会被其他物体的力量影响)
        self.moving_sprites_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False
        # step012.py 添加
        self.up_pressed: bool = False
        self.down_pressed: bool = False

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""
        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        my_map = arcade.tilemap.read_tmx(map_name)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(my_map, 'Platforms', SPRITE_SCALING_TILES)
        self.item_list = arcade.tilemap.process_layer(my_map, 'Dynamic Items', SPRITE_SCALING_TILES)
        # step011.py添加 移动物体层
        self.moving_sprites_list = arcade.tilemap.process_layer(my_map,
                                                                'Moving Platforms',
                                                                SPRITE_SCALING_TILES)
        # step012.py添加
        self.ladder_list = arcade.tilemap.process_layer(my_map,
                                                        'Ladders',
                                                        SPRITE_SCALING_TILES,
                                                        use_spatial_hash=True,
                                                        hit_box_algorithm="Detailed")

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        # self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.p
        # ng",SPRITE_SCALING_PLAYER)
        self.player_sprite = PlayerSprite(self.ladder_list, hit_box_algorithm="Detailed")

        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2

        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)
        # Add kinematic sprites
        self.physics_engine.add_sprite_list(self.moving_sprites_list,
                                            body_type=arcade.PymunkPhysicsEngine.KINEMATIC)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

        # step010.py添加 在setup 方法里面定义两个方法并调用

        def wall_hit_handler(bullet_sprite, _wall_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞墙时消失"""
            bullet_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "wall", post_handler=wall_hit_handler)

        def item_hit_handler(bullet_sprite, item_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞到物体时消失 """
            bullet_sprite.remove_from_sprite_lists()
            item_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "item", post_handler=item_hit_handler)

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            self.up_pressed = True
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)
        elif key == arcade.key.DOWN:
            self.down_pressed = True

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False
        elif key == arcade.key.UP:
            self.up_pressed = False
        elif key == arcade.key.DOWN:
            self.down_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            # step012.py 加入的 新条件时 or self.player_sprite.is_on_ladder
            if is_on_ground or self.player_sprite.is_on_ladder:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground or self.player_sprite.is_on_ladder:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)
            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        # step012.py添加
        elif self.up_pressed and not self.down_pressed:
            # Create a force to the up. Apply it.
            if self.player_sprite.is_on_ladder:
                force = (0, PLAYER_MOVE_FORCE_ON_GROUND)
                self.physics_engine.apply_force(self.player_sprite, force)
                # Set friction to zero for the player while moving
                self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.down_pressed and not self.up_pressed:
            # Create a force to the down. Apply it.
            if self.player_sprite.is_on_ladder:
                force = (0, -PLAYER_MOVE_FORCE_ON_GROUND)
                self.physics_engine.apply_force(self.player_sprite, force)
                # Set friction to zero for the player while moving
                self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # step011.py添加 当移动物体层的物体移动到给他定义的边缘时 让他再原路返回
        # For each moving sprite, see if we've reached a boundary and need to
        # reverse course.
        for moving_sprite in self.moving_sprites_list:
            # print(moving_sprite.top,moving_sprite.bottom,moving_sprite.boundary_top,moving_sprite.boundary_bottom,moving_sprite.change_y)
            # 下面这行写的很长 主要时为了观看时便于理解代码含义 为了规范书写 也可以换行
            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
            # velocity 是一个速度列表,第一个时横向速度,第二个是纵向速度(是每秒的改变量,因此下面我们要变成每帧的变化量再用)
            # 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)
        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面 后执行的绘制 会覆盖先绘制的图案"""
        arcade.start_render()
        self.wall_list.draw()
        self.ladder_list.draw()  # step012.py添加
        self.moving_sprites_list.draw()  # step011.py添加
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()

    # 当鼠标点击屏幕时 会给该方法传入以下参数 x :横坐标 y:纵坐标 button:鼠标左中右按的哪个键 modifiers:修饰键 alt shift 等这些
    def on_mouse_press(self, x, y, button, modifiers):
        print(x,y)
        """ Called whenever the mouse button is clicked.
        当鼠标点击时 本方法被调用"""
        # 使用创建据矩形的方法 创建一个子弹
        # bullet = arcade.SpriteSolidColor(20, 5, arcade.color.DARK_YELLOW)
        bullet = BulletSprite(20, 5, arcade.color.DARK_YELLOW)
        self.bullet_list.append(bullet)   #  把子弹加入上面就定义过的子弹精灵组

        # Position the bullet at the player's current location
        # 定义子弹的初始位置 也就是精灵的位置
        start_x = self.player_sprite.center_x
        start_y = self.player_sprite.center_y
        bullet.position = self.player_sprite.position

        # Get from the mouse the destination location for the bullet
        # IMPORTANT! If you have a scrolling screen, you will also need
        # to add in self.view_bottom and self.view_left.
        # 记录点击位置的坐标
        dest_x = x
        dest_y = y

        # Do math to calculate how to get the bullet to the destination.
        # Calculation the angle in radians between the start points
        # and end points. This is the angle the bullet will travel.
        # 使用高一会学习的三角函数知识 计算 玩家相对于平面和鼠标点击位置之间的角度
        x_diff = dest_x - start_x
        y_diff = dest_y - start_y
        angle = math.atan2(y_diff, x_diff)

        # What is the 1/2 size of this sprite, so we can figure out how far
        # away to spawn the bullet
        size = max(self.player_sprite.width, self.player_sprite.height) / 2

        # Use angle to to spawn bullet away from player in proper direction
        # 加上size的好处是 让子弹从玩家图片外部发射出去,避免内部发射导致物理引擎误判
        bullet.center_x += size * math.cos(angle)
        bullet.center_y += size * math.sin(angle)

        # Set angle of bullet 设置子弹的朝向
        bullet.angle = math.degrees(angle)

        # Gravity to use for the bullet
        # If we don't use custom gravity, bullet drops too fast, or we have
        # to make it go too fast.
        # Force is in relation to bullet's angle.
        # 专门定义以下子弹垂直方向受到的重力 而不是默认的
        bullet_gravity = (0, -BULLET_GRAVITY)

        # Add the sprite. This needs to be done AFTER setting the fields above.
        # 使用物理引擎管理子弹 这个子弹是具有弹性的
        self.physics_engine.add_sprite(bullet,
                                       mass=BULLET_MASS,
                                       damping=1.0,
                                       friction=0.6,
                                       collision_type="bullet",
                                       gravity=bullet_gravity,
                                       elasticity=0.9)

        # Add force to bullet
        # 最后 让物理引擎给子弹施加一个力量,让他飞走
        force = (BULLET_MOVE_FORCE, 0)
        self.physics_engine.apply_force(bullet, force)


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

十三、 打包成exe软件包

1.首先在入口文件 的代码里加上三行代码

这些代码是为了后续 让程序自动去找素材文件的。不然会出现,代码文件找不到素材文件的错误
不需要素材文件的可以不加

import os, sys

if getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS'):
    os.chdir(sys._MEIPASS)

这里我是又用了一个新的文件存储代码 名字自己起就行。这里叫arcade_game.py
在这里插入图片描述

2.安装打包工具库pyinstaller

直接在pycharm里 或者项目所在python环境的终端
安装 pyinstaller

用国内镜像源安装 多少会快一些 这是豆瓣的

pip install pyinstaller -i https://pypi.doubanio.com/simple
3.修改arcade库源文件的一行代码

为什么修改 参看这里报错情况
在这里插入图片描述

4.进行打包

在项目所在终端 输入
这样打包的软件 运行时会默认打开一个终端窗口 输出程序里可能输出的信息

pyinstaller arcade_game.py

如果想让软件在 运行时 只显示游戏或者主要程序相关的窗体,而不默认打开终端窗口,则使用下面的代码

pyinstaller -w arcade_game.py

打包时可能会触发安全软件的检测 选择允许程序的操作即可 总之就是各种开绿灯

在这里插入图片描述

打包完后 会看到在项目目录下 有两个新的文件夹 disk 和 buld 我们在disk目录里找到

在这里插入图片描述
下面的exe既可以双击运行。
也可以拖进cmd终端窗口运行(这样有报错的话,可以直接看到报错信息),程序运行异常,也这样去查看异常原因

(如果我们的代码文件本身不需要别的 文件【如图片 音乐】,那么到这里软件就打包完成了,如果像我们这个游戏项目一样,还需要地图,图块集等别的文件,那么我们就要再执行别的代码,把文件也打包进去,具体看下面)

在这里插入图片描述

5.把素材文件加入软件包

同样是在原来的项目终端执行以下代码

如果素材是和代码文件并列单独存放的 执行下面的语句

pyinstaller arcade_game.py --add-data "tmw_desert_spacing.tsx;."  --add-data "pymunk_test_map.tmx;."

如过素材是放在某个文件夹里的 执行下面代码 文件多时建议存到一个统一的文件夹
例如有一个名为images的文件夹

pyinstaller myscript.py --add-data "images;images"

在这里插入图片描述

6.再次双击exe程序 搞定

在这里插入图片描述

十四、拓展(1):屏幕移动(随玩家)

原理解释:
我们给屏幕创建一个内边框,当玩家移动到内边框时,就让屏幕进行移动跟踪。这样可以体验更大的地图

1.在原来地图基础上加入更多元素

地图一旦开始创建成功,便没有了重新设置大小的属性,不过我们可以把地图属性改为无无限大,待创建完成后,再取消无限大,即可自动调整成我们当前设计的大小。
操作步骤
打开原来的地图编辑器,点击工具栏上方地图选项,选择地图属性,给无限那个方框打上对勾。一般需要双击才能勾选好。然后添加元素,添加完毕后记得取消对勾 和保存文件。
之后重新运行程序,页面可以正常展示和使用
在这里插入图片描述

2.定义内边框的范围 和 当前边框数值

在这里插入图片描述
在窗口大小固定的情况下,我们只要知道左侧和底部边框的值,就可以知道右侧和顶部,所以之定义左 下 两个即可
在这里插入图片描述

3.改保存地图的变量 为 类属性(也就是加个self)

参照上图,先把__init__里的加上
然后在setup()方法里 把我们原来写的my_map 都用self.my_map替代
在这里插入图片描述

4.在update()方法里计算屏幕的滑动量,并让屏幕滑动

在这里插入图片描述
简单的代码 往往需要花更多的时间理解 加油
在这里插入图片描述

step013.py效果示例

在这里插入图片描述

step013.py代码示例
# coding=<UTF-8>


"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity 重力
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 450
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 8000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1800

# Close enough to not-moving to have the animation go to idle.
# 设置这个常量 当玩家角色移动的距离小于0.1时 不让他有动画的走路表现 而是静止 比如在挨着墙的时候按方向键 不再有走路动画
DEAD_ZONE = 0.1

# Constants used to track if the player is facing left or right
# 设置面向右侧 为 0  面向左 为 1
RIGHT_FACING = 0
LEFT_FACING = 1

# How many pixels to move before we change the texture in the walking animation
# 动作切换是很快的事情,那怎么控制切换频率或者速度呢,我们设置这个变量,使角色移动量每大于20像素时再进行动画 切换
DISTANCE_TO_CHANGE_TEXTURE = 20

# step009.py 添加
# How much force to put on the bullet  给子弹施加的初始动力
BULLET_MOVE_FORCE = 4500

# Mass of the bullet  子弹的质量
BULLET_MASS = 0.1

# Make bullet less affected by gravity 给子弹一个更小的重力值
BULLET_GRAVITY = 300


# How many pixels to keep as a minimum margin between the character
# and the edge of the screen.
# 设置物体距离屏幕边缘的最小距离,当小于之这个距离时执行界面的滑动。
# 换言之就是根据 可视的界面,构建一个内部的内边框,当玩家移出边框时,进行屏幕滑动
# 这里我们使用上面定义过的屏幕大小的常量来定义内边框 到 外边框(可视界面的边缘)的距离 这样更通用些
LEFT_VIEWPORT_MARGIN = SCREEN_WIDTH/4
RIGHT_VIEWPORT_MARGIN = SCREEN_WIDTH/4
BOTTOM_VIEWPORT_MARGIN = SCREEN_HEIGHT/2
TOP_VIEWPORT_MARGIN = SCREEN_HEIGHT/4


class PlayerSprite(arcade.Sprite):
    """ Player Sprite """
    def __init__(self, ladder_list: arcade.SpriteList,
                 hit_box_algorithm):
        """ Init """
        # Let parent initialize 初始化父类方法
        super().__init__()

        # Set our scale 设置缩放系数
        self.scale = SPRITE_SCALING_PLAYER

        # Images from Kenney.nl's Character pack 图片来自 Kenney网站的素材 aracade 以默认内置
        # 以下 图片都可以用 选一个喜欢的造型集即可 名字是一样的 后缀不一样则造型不一样 所以main_path 只定义名字
        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
        main_path = ":resources:images/animated_characters/female_person/femalePerson"
        # main_path = ":resources:images/animated_characters/male_person/malePerson"
        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
        # main_path = ":resources:images/animated_characters/zombie/zombie"
        # main_path = ":resources:images/animated_characters/robot/robot"

        # Load textures for idle standing 使用load_texture_pair加载不同状态下的造型图片
        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")  # 站立造型
        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")  # 跳跃造型
        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")  # 下落造型

        # Load textures for walking 由于走路的造型较多 这里用循环去加载 然后放入列表中备用
        self.walk_textures = []
        for i in range(8):
            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
            self.walk_textures.append(texture)
        # step012.py添加 爬梯子的造型加载
        # Load textures for climbing
        self.climbing_textures = []
        texture = arcade.load_texture(f"{main_path}_climb0.png")
        self.climbing_textures.append(texture)
        texture = arcade.load_texture(f"{main_path}_climb1.png")
        self.climbing_textures.append(texture)

        # Set the initial texture 默认的站立造型 (0,代表造型里的第一张图片,因为以后还会有左右不同方向的造型)
        self.texture = self.idle_texture_pair[0]

        # Hit box will be set based on the first image used. 角色边框由第一张造型图片定义 以便后续碰撞检测
        self.hit_box = self.texture.hit_box_points

        # Default to face-right 默认觉得是脸朝右的
        self.character_face_direction = RIGHT_FACING

        # Index of our current texture  当前造型图片在图片组中的索引值
        self.cur_texture = 0

        # How far have we traveled horizontally since changing the texture
        # 定义一个 记录上次变换造型以后 角色的移动距离  后续用于比较当他大于我们上面常量设置的值时 执行造型切换
        self.x_odometer = 0
        # step012.py添加
        self.y_odometer = 0
        self.ladder_list = ladder_list

        self.is_on_ladder = False

    #  这是物理引擎一个处理移动的方法,只要这个精灵是由物理引擎创建的,那么物理引擎自己就会去调用这个固定方法的
    #  dx 代表每一帧之间 物体横向变化的距离  dy 代表每一帧之间 物体纵向变化的距离
    #  d_angle则代表每一帧之间 物体角度朝向的变化量
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle being moved by the pymunk engine 这是物理引擎一个处理移动的方法"""
        # Figure out if we need to face left or right  判断我们是否要改变角色的朝向
        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
            self.character_face_direction = LEFT_FACING
        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
            self.character_face_direction = RIGHT_FACING
        # Are we on the ground? 判断当前对象是否在地面上 self代表当前对象 谁用这个类创建的 那么self就是谁
        is_on_ground = physics_engine.is_on_ground(self)
        # Are we on a ladder? 根据玩家精灵组和梯子精灵组的碰撞结果,设置不同的属性
        # 碰撞检测结果是个列表,被碰撞的精灵都会存进去,所以一旦碰撞,列表的长度就会大于0
        if len(arcade.check_for_collision_with_list(self, self.ladder_list)) > 0:
            # 碰撞以后 检查玩家是否在梯子上
            # 如果碰撞了 但是玩家状态显示不在梯子上 一定是刚刚上到梯子上来  则更改状态为 在梯子上,然后重力大小设置为0
            if not self.is_on_ladder:
                self.is_on_ladder = True
                self.pymunk.gravity = (0, 0)
                self.pymunk.damping = 0.0001  # 刚到梯子上时,玩家的速度还使用之前的速度,只不过会稍微衰减
                self.pymunk.max_vertical_velocity = PLAYER_MAX_HORIZONTAL_SPEED  # 在梯子上时最大的垂直速度
        # 如果玩家并没有碰撞到梯子
        else:
            # 但是玩家状态却还是在梯子上 那么 肯定是玩家刚从梯子上掉下来了
            if self.is_on_ladder:
                self.pymunk.damping = 1.0  # 让玩家瞬间失去原来在梯子上时的速度
                self.pymunk.max_vertical_velocity = PLAYER_MAX_VERTICAL_SPEED
                self.is_on_ladder = False
                self.pymunk.gravity = None  # 把原来的重力(0,0)清除 开始自由落体

        # Add to the odometer how far we've moved  将横向的变化量累加起来
        self.x_odometer += dx
        # step012.py添加 垂直方向的变化量
        self.y_odometer += dy

        # 设置在梯子上时的动作变化
        if self.is_on_ladder and not is_on_ground:
            # Have we moved far enough to change the texture?
            # 当在梯子上的垂直的变化量大于我们设定的可以进行动作切换的值时 改变动作 并重置变化量
            if abs(self.y_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
                # Reset the odometer
                self.y_odometer = 0
                # Advance the walking animation
                # 切换下一张动作图的索引
                self.cur_texture += 1
            # 有两张图就可以
            if self.cur_texture > 1:
                self.cur_texture = 0
            # 更改梯子上的动作
            self.texture = self.climbing_textures[self.cur_texture]
            return

        # Jumping animation
        if not is_on_ground:
            if dy > DEAD_ZONE:
                self.texture = self.jump_texture_pair[self.character_face_direction]
                return
            elif dy < -DEAD_ZONE:
                self.texture = self.fall_texture_pair[self.character_face_direction]
                return

        # Idle animation
        if abs(dx) <= DEAD_ZONE:
            self.texture = self.idle_texture_pair[self.character_face_direction]
            return

        # Have we moved far enough to change the texture?
        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:

            # Reset the odometer
            self.x_odometer = 0

            # Advance the walking animation
            self.cur_texture += 1
            if self.cur_texture > 7:
                self.cur_texture = 0
            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]


class BulletSprite(arcade.SpriteSolidColor):
    """ Bullet Sprite """
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle when the sprite is moved by the physics engine. """
        # If the bullet falls below the screen, remove it
        # 当子弹下落并小于某一个值时 令其消失 这里设置为50 容易观察 负数则在掉出屏幕底部时消失(一般也就设置为负数)
        if self.center_y < 50:
            self.remove_from_sprite_lists()


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        # self.player_sprite: Optional[arcade.Sprite] = None
        self.player_sprite: Optional[PlayerSprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None
        # step012.py添加
        self.ladder_list: Optional[arcade.SpriteList] = None

        # step011.py添加 可移动物体层(不受其他物体力量的影响) 而不是动态物体层(会被其他物体的力量影响)
        self.moving_sprites_list: Optional[arcade.SpriteList] = None

        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False
        # step012.py 添加
        self.up_pressed: bool = False
        self.down_pressed: bool = False

        # 屏幕滚动相关添加
        # Used to keep track of our scrolling
        self.view_bottom = 0
        self.view_left = 0

        # 创建存储地图的变量
        self.my_map = None

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""
        # 屏幕滚动相关添加
        # Used to keep track of our scrolling
        self.view_bottom = 0
        self.view_left = 0

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        self.my_map = arcade.tilemap.read_tmx(map_name)
        # 使用类属性作为地图变量,方便后续在设置屏幕滚动时程序获取地图的宽高,地图快大小等信息
        # self.map_size = self.my_map .map_size
        # print('地图的宽高为:',self.map_size)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(self.my_map, 'Platforms', SPRITE_SCALING_TILES)

        self.item_list = arcade.tilemap.process_layer(self.my_map, 'Dynamic Items', SPRITE_SCALING_TILES)
        # step011.py添加 移动物体层
        self.moving_sprites_list = arcade.tilemap.process_layer(self.my_map,
                                                                'Moving Platforms',
                                                                SPRITE_SCALING_TILES)
        # step012.py添加
        self.ladder_list = arcade.tilemap.process_layer(self.my_map,
                                                        'Ladders',
                                                        SPRITE_SCALING_TILES,
                                                        use_spatial_hash=True,
                                                        hit_box_algorithm="Detailed")

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        # self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.p
        # ng",SPRITE_SCALING_PLAYER)
        self.player_sprite = PlayerSprite(self.ladder_list, hit_box_algorithm="Detailed")

        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2

        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)
        # Add kinematic sprites
        self.physics_engine.add_sprite_list(self.moving_sprites_list,
                                            body_type=arcade.PymunkPhysicsEngine.KINEMATIC)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

        # step010.py添加 在setup 方法里面定义两个方法并调用

        def wall_hit_handler(bullet_sprite, _wall_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞墙时消失"""
            bullet_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "wall", post_handler=wall_hit_handler)

        def item_hit_handler(bullet_sprite, item_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞到物体时消失 """
            bullet_sprite.remove_from_sprite_lists()
            item_sprite.remove_from_sprite_lists()

        self.physics_engine.add_collision_handler("bullet", "item", post_handler=item_hit_handler)

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            self.up_pressed = True
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)
        elif key == arcade.key.DOWN:
            self.down_pressed = True

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False
        elif key == arcade.key.UP:
            self.up_pressed = False
        elif key == arcade.key.DOWN:
            self.down_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            # step012.py 加入的 新条件时 or self.player_sprite.is_on_ladder
            if is_on_ground or self.player_sprite.is_on_ladder:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground or self.player_sprite.is_on_ladder:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)
            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        # step012.py添加
        elif self.up_pressed and not self.down_pressed:
            # Create a force to the up. Apply it.
            if self.player_sprite.is_on_ladder:
                force = (0, PLAYER_MOVE_FORCE_ON_GROUND)
                self.physics_engine.apply_force(self.player_sprite, force)
                # Set friction to zero for the player while moving
                self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.down_pressed and not self.up_pressed:
            # Create a force to the down. Apply it.
            if self.player_sprite.is_on_ladder:
                force = (0, -PLAYER_MOVE_FORCE_ON_GROUND)
                self.physics_engine.apply_force(self.player_sprite, force)
                # Set friction to zero for the player while moving
                self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # step011.py添加 当移动物体层的物体移动到给他定义的边缘时 让他再原路返回
        # For each moving sprite, see if we've reached a boundary and need to
        # reverse course.
        for moving_sprite in self.moving_sprites_list:
            # print(moving_sprite.top,moving_sprite.bottom,moving_sprite.boundary_top,moving_sprite.boundary_bottom,moving_sprite.change_y)
            # 下面这行写的很长 主要时为了观看时便于理解代码含义 为了规范书写 也可以换行
            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
            # velocity 是一个速度列表,第一个时横向速度,第二个是纵向速度(是每秒的改变量,因此下面我们要变成每帧的变化量再用)
            # 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)

            # 屏幕滚动相关添加
            # --- Manage Scrolling ---
            # Track if we need to change the viewport
            changed = False
            # Scroll left
            left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
            if self.player_sprite.left < left_boundary:
                self.view_left -= left_boundary - self.player_sprite.left
                changed = True
            # Scroll right
            right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
            if self.player_sprite.right > right_boundary:
                self.view_left += self.player_sprite.right - right_boundary
                changed = True
            # Scroll up
            top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
            if self.player_sprite.top > top_boundary:
                self.view_bottom += self.player_sprite.top - top_boundary
                changed = True
            # Scroll down
            bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
            if self.player_sprite.bottom < bottom_boundary:
                self.view_bottom -= bottom_boundary - self.player_sprite.bottom
                changed = True
            # print('地图块大小',self.my_map.tile_size)
            #  self.my_map.tile_size 代表组成地图的 每一个地图块的大小 和地图编辑器中创建时一样,是32*32 但是由于地图编辑器输出文件的时候
            #  设置输出成了16*16的,所以我们要再整除2 变成游戏中实际的大小
            # self.my_map.map_size.width 地图单行横向的格子数
            # self.my_map.map_size.height 地图单行纵向的格子数
            # 设置地图界面从左到右的宽度 结果是用 图块个数表示
            TILED_MAP_WIDTH = self.my_map.map_size.width * self.my_map.tile_size.width // 2
            # 设置地图界面从上到下的高度 结果是用 图块个数表示
            TILED_MAP_HEIGHT = self.my_map.map_size.height * self.my_map.tile_size.height // 2
            if changed:
                # Only scroll to integers. Otherwise we end up with pixels that
                # don't line up on the screen
                self.view_bottom = max(min(int(self.view_bottom), TILED_MAP_HEIGHT-SCREEN_HEIGHT), 0)
                self.view_left = max(min(int(self.view_left), TILED_MAP_WIDTH - SCREEN_WIDTH), 0)
                view_right = min(SCREEN_WIDTH + self.view_left, TILED_MAP_WIDTH)
                view_top = min(TILED_MAP_HEIGHT, self.view_bottom+SCREEN_HEIGHT)
                # view_top = min(TILED_MAP_HEIGHT+20, self.view_bottom+SCREEN_HEIGHT+20)

                # if SCREEN_WIDTH + self.view_left > TILED_MAP_WIDTH:
                #     self.view_right = TILED_MAP_WIDTH
                #     self.view_left =TILED_MAP_WIDTH -SCREEN_WIDTH
                # else:
                #     self.view_right = SCREEN_WIDTH + self.view_left

                # if self.view_bottom <= 0:
                #     self.view_bottom = 0
                # if self.view_left <= 0:
                #     self.view_left = 0

                # Do the scrolling  设置视口大小
                arcade.set_viewport(self.view_left,
                                    view_right,
                                    self.view_bottom,
                                    view_top)
        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面 后执行的绘制 会覆盖先绘制的图案"""
        arcade.start_render()
        self.wall_list.draw()
        self.ladder_list.draw()  # step012.py添加
        self.moving_sprites_list.draw()  # step011.py添加
        self.bullet_list.draw()
        self.item_list.draw()
        self.player_list.draw()

    # 当鼠标点击屏幕时 会给该方法传入以下参数 x :横坐标 y:纵坐标 button:鼠标左中右按的哪个键 modifiers:修饰键 alt shift 等这些
    def on_mouse_press(self, x, y, button, modifiers):
        print(x,y,'左侧',self.player_sprite.left,'底部',self.player_sprite.bottom)
        """ Called whenever the mouse button is clicked.
        当鼠标点击时 本方法被调用"""
        # 使用创建据矩形的方法 创建一个子弹
        # bullet = arcade.SpriteSolidColor(20, 5, arcade.color.DARK_YELLOW)
        bullet = BulletSprite(20, 5, arcade.color.DARK_YELLOW)
        self.bullet_list.append(bullet)   #  把子弹加入上面就定义过的子弹精灵组

        # Position the bullet at the player's current location
        # 定义子弹的初始位置 也就是精灵的位置
        start_x = self.player_sprite.center_x
        start_y = self.player_sprite.center_y
        bullet.position = self.player_sprite.position

        # Get from the mouse the destination location for the bullet
        # IMPORTANT! If you have a scrolling screen, you will also need
        # to add in self.view_bottom and self.view_left.
        # 记录点击位置的坐标
        dest_x = x
        dest_y = y

        # Do math to calculate how to get the bullet to the destination.
        # Calculation the angle in radians between the start points
        # and end points. This is the angle the bullet will travel.
        # 使用高一会学习的三角函数知识 计算 玩家相对于平面和鼠标点击位置之间的角度
        x_diff = dest_x - start_x
        y_diff = dest_y - start_y
        angle = math.atan2(y_diff, x_diff)

        # What is the 1/2 size of this sprite, so we can figure out how far
        # away to spawn the bullet
        size = max(self.player_sprite.width, self.player_sprite.height) / 2

        # Use angle to to spawn bullet away from player in proper direction
        # 加上size的好处是 让子弹从玩家图片外部发射出去,避免内部发射导致物理引擎误判
        bullet.center_x += size * math.cos(angle)
        bullet.center_y += size * math.sin(angle)

        # Set angle of bullet 设置子弹的朝向
        bullet.angle = math.degrees(angle)

        # Gravity to use for the bullet
        # If we don't use custom gravity, bullet drops too fast, or we have
        # to make it go too fast.
        # Force is in relation to bullet's angle.
        # 专门定义以下子弹垂直方向受到的重力 而不是默认的
        bullet_gravity = (0, -BULLET_GRAVITY)

        # Add the sprite. This needs to be done AFTER setting the fields above.
        # 使用物理引擎管理子弹 这个子弹是具有弹性的
        self.physics_engine.add_sprite(bullet,
                                       mass=BULLET_MASS,
                                       damping=1.0,
                                       friction=0.6,
                                       collision_type="bullet",
                                       gravity=bullet_gravity,
                                       elasticity=0.9)

        # Add force to bullet
        # 最后 让物理引擎给子弹施加一个力量,让他飞走
        force = (BULLET_MOVE_FORCE, 0)
        self.physics_engine.apply_force(bullet, force)


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

十五、拓展(2):优化玩家速度

1. 让玩家不再逆天弹跳

在这里插入图片描述

十六、拓展(3):添加背景层和前景层

前景:即可以挡住玩家的元素
背景:即可以被玩家挡住的元素
前景 背景 取决于图块层的加载先后,即on_draw()顺序

1.地图编辑器里创建新层并绘制相应图块

前景层 foreground_layer
在这里插入图片描述
背景层 background_layer
在这里插入图片描述

2、在代码中添加相关图层并显示

1、准备初始变量

在这里插入图片描述
2、给初始变量赋值确定类型
在这里插入图片描述
3、加载前景和背景图块层
在这里插入图片描述
4、绘制前景和背景层
在这里插入图片描述

十七、拓展(4):添加音效和背景音乐

1.添加音效

第一步加载音效
第二步 在合适的时机 或者代码位置 播放音乐
首先 还是初始化变量(属性)
在这里插入图片描述
其次 加载音乐文件
在这里插入图片描述
最后
在这里插入图片描述
在这里插入图片描述

2.添加背景音乐

背景音乐方法播放略有不同,因为我们要监听播放状态,并适时候切换音乐
在这里插入图片描述
在这里插入图片描述
播放
在这里插入图片描述
切换
在这里插入图片描述
控制台效果如下
在这里插入图片描述

代码示例
# coding=<UTF-8>


"""
Example of Pymunk Physics Engine Platformer
使用Pymunk物理引擎的示例游戏 该物理引擎可以为物体也就是说的精灵
添加 质量 弹性 摩擦力 弹力 动力等等 并且可以进行碰撞检测
"""

import math
from typing import Optional
import arcade

SCREEN_TITLE = "arcade结合PyMunk引擎示例"


# How big are our image tiles 我们的图片基础大小尺寸是多少,后续尺寸都在此基础上计算
SPRITE_IMAGE_SIZE = 64

# Scale sprites up or down  图片的缩放系数 0.5 是原来的一半大小
SPRITE_SCALING_PLAYER = 0.5
SPRITE_SCALING_TILES = 0.5

# Scaled sprite size for tiles  构成地图的单个基准网格的大小 是图片基础尺寸的倍数(0.5倍)
SPRITE_SIZE = int(SPRITE_IMAGE_SIZE * SPRITE_SCALING_PLAYER)

# Size of grid to show on screen, in number of tiles  以基准格子数来计算地图大小
SCREEN_GRID_WIDTH = 25
SCREEN_GRID_HEIGHT = 15

# Size of screen to show, in pixels  重新定义地图尺寸 这里以格子数量为计量单位 之前step001是直接设置的像素大小
SCREEN_WIDTH = SPRITE_SIZE * SCREEN_GRID_WIDTH
SCREEN_HEIGHT = SPRITE_SIZE * SCREEN_GRID_HEIGHT

# step005添加
# --- Physics forces. Higher number, faster accelerating.
# 数字越大 加速率越大
# Gravity 重力
GRAVITY = 1500

# Damping - Amount of speed lost per second
# 默认的阻尼 和 玩家的阻尼(每帧损失速度的比例)
DEFAULT_DAMPING = 1.0
PLAYER_DAMPING = 0.4

# Friction between objects 不同物体间的摩擦力
PLAYER_FRICTION = 1.0
WALL_FRICTION = 0.7
DYNAMIC_ITEM_FRICTION = 0.6

# Mass (defaults to 1) 玩家质量设置为2 可以推动默认1.0质量的其他物体
PLAYER_MASS = 2.0

# Keep player from going too fast  # 玩家角色最大的 水平 和 垂直 方向的速度 可以自由调整
PLAYER_MAX_HORIZONTAL_SPEED = 300
PLAYER_MAX_VERTICAL_SPEED = 1600

# Force applied while on the ground 玩家在地面移动时将受到的推力
PLAYER_MOVE_FORCE_ON_GROUND = 4000

# Force applied when moving left/right in the air 玩家在空中时受到的左右方向的推力 大小可以自己调节
PLAYER_MOVE_FORCE_IN_AIR = 900

# Strength of a jump  玩家起跳时给玩家施加的弹射力 力越大跳的越高 可以自己优化
PLAYER_JUMP_IMPULSE = 1400

# Close enough to not-moving to have the animation go to idle.
# 设置这个常量 当玩家角色移动的距离小于0.1时 不让他有动画的走路表现 而是静止 比如在挨着墙的时候按方向键 不再有走路动画
DEAD_ZONE = 0.1

# Constants used to track if the player is facing left or right
# 设置面向右侧 为 0  面向左 为 1
RIGHT_FACING = 0
LEFT_FACING = 1

# How many pixels to move before we change the texture in the walking animation
# 动作切换是很快的事情,那怎么控制切换频率或者速度呢,我们设置这个变量,使角色移动量每大于20像素时再进行动画 切换
DISTANCE_TO_CHANGE_TEXTURE = 20

# step009.py 添加
# How much force to put on the bullet  给子弹施加的初始动力
BULLET_MOVE_FORCE = 4500

# Mass of the bullet  子弹的质量
BULLET_MASS = 0.1

# Make bullet less affected by gravity 给子弹一个更小的重力值
BULLET_GRAVITY = 300


# How many pixels to keep as a minimum margin between the character
# and the edge of the screen.
# 设置物体距离屏幕边缘的最小距离,当小于之这个距离时执行界面的滑动。
# 换言之就是根据 可视的界面,构建一个内部的内边框,当玩家移出边框时,进行屏幕滑动
# 这里我们使用上面定义过的屏幕大小的常量来定义内边框 到 外边框(可视界面的边缘)的距离 这样更通用些
LEFT_VIEWPORT_MARGIN = SCREEN_WIDTH/4
RIGHT_VIEWPORT_MARGIN = SCREEN_WIDTH/4
BOTTOM_VIEWPORT_MARGIN = SCREEN_HEIGHT/2
TOP_VIEWPORT_MARGIN = SCREEN_HEIGHT/4


class PlayerSprite(arcade.Sprite):
    """ Player Sprite """
    def __init__(self, ladder_list: arcade.SpriteList,
                 hit_box_algorithm):
        """ Init """
        # Let parent initialize 初始化父类方法
        super().__init__()

        # Set our scale 设置缩放系数
        self.scale = SPRITE_SCALING_PLAYER

        # Images from Kenney.nl's Character pack 图片来自 Kenney网站的素材 aracade 以默认内置
        # 以下 图片都可以用 选一个喜欢的造型集即可 名字是一样的 后缀不一样则造型不一样 所以main_path 只定义名字
        # main_path = ":resources:images/animated_characters/female_adventurer/femaleAdventurer"
        main_path = ":resources:images/animated_characters/female_person/femalePerson"
        # main_path = ":resources:images/animated_characters/male_person/malePerson"
        # main_path = ":resources:images/animated_characters/male_adventurer/maleAdventurer"
        # main_path = ":resources:images/animated_characters/zombie/zombie"
        # main_path = ":resources:images/animated_characters/robot/robot"

        # Load textures for idle standing 使用load_texture_pair加载不同状态下的造型图片
        self.idle_texture_pair = arcade.load_texture_pair(f"{main_path}_idle.png")  # 站立造型
        self.jump_texture_pair = arcade.load_texture_pair(f"{main_path}_jump.png")  # 跳跃造型
        self.fall_texture_pair = arcade.load_texture_pair(f"{main_path}_fall.png")  # 下落造型

        # Load textures for walking 由于走路的造型较多 这里用循环去加载 然后放入列表中备用
        self.walk_textures = []
        for i in range(8):
            texture = arcade.load_texture_pair(f"{main_path}_walk{i}.png")
            self.walk_textures.append(texture)
        # step012.py添加 爬梯子的造型加载
        # Load textures for climbing
        self.climbing_textures = []
        texture = arcade.load_texture(f"{main_path}_climb0.png")
        self.climbing_textures.append(texture)
        texture = arcade.load_texture(f"{main_path}_climb1.png")
        self.climbing_textures.append(texture)

        # Set the initial texture 默认的站立造型 (0,代表造型里的第一张图片,因为以后还会有左右不同方向的造型)
        self.texture = self.idle_texture_pair[0]

        # Hit box will be set based on the first image used. 角色边框由第一张造型图片定义 以便后续碰撞检测
        self.hit_box = self.texture.hit_box_points

        # Default to face-right 默认觉得是脸朝右的
        self.character_face_direction = RIGHT_FACING

        # Index of our current texture  当前造型图片在图片组中的索引值
        self.cur_texture = 0

        # How far have we traveled horizontally since changing the texture
        # 定义一个 记录上次变换造型以后 角色的移动距离  后续用于比较当他大于我们上面常量设置的值时 执行造型切换
        self.x_odometer = 0
        # step012.py添加
        self.y_odometer = 0
        self.ladder_list = ladder_list

        self.is_on_ladder = False

    #  这是物理引擎一个处理移动的方法,只要这个精灵是由物理引擎创建的,那么物理引擎自己就会去调用这个固定方法的
    #  dx 代表每一帧之间 物体横向变化的距离  dy 代表每一帧之间 物体纵向变化的距离
    #  d_angle则代表每一帧之间 物体角度朝向的变化量
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle being moved by the pymunk engine 这是物理引擎一个处理移动的方法"""
        # Figure out if we need to face left or right  判断我们是否要改变角色的朝向
        if dx < -DEAD_ZONE and self.character_face_direction == RIGHT_FACING:
            self.character_face_direction = LEFT_FACING
        elif dx > DEAD_ZONE and self.character_face_direction == LEFT_FACING:
            self.character_face_direction = RIGHT_FACING
        # Are we on the ground? 判断当前对象是否在地面上 self代表当前对象 谁用这个类创建的 那么self就是谁
        is_on_ground = physics_engine.is_on_ground(self)
        # Are we on a ladder? 根据玩家精灵组和梯子精灵组的碰撞结果,设置不同的属性
        # 碰撞检测结果是个列表,被碰撞的精灵都会存进去,所以一旦碰撞,列表的长度就会大于0
        if len(arcade.check_for_collision_with_list(self, self.ladder_list)) > 0:
            # 碰撞以后 检查玩家是否在梯子上
            # 如果碰撞了 但是玩家状态显示不在梯子上 一定是刚刚上到梯子上来  则更改状态为 在梯子上,然后重力大小设置为0
            if not self.is_on_ladder:
                self.is_on_ladder = True
                self.pymunk.gravity = (0, 0)
                self.pymunk.damping = 0.0001  # 刚到梯子上时,玩家的速度还使用之前的速度,只不过会稍微衰减
                self.pymunk.max_vertical_velocity = PLAYER_MAX_HORIZONTAL_SPEED  # 在梯子上时最大的垂直速度
        # 如果玩家并没有碰撞到梯子
        else:
            # 但是玩家状态却还是在梯子上 那么 肯定是玩家刚从梯子上掉下来了
            if self.is_on_ladder:
                self.pymunk.damping = 1.0  # 让玩家瞬间失去原来在梯子上时的速度
                self.pymunk.max_vertical_velocity = PLAYER_MAX_VERTICAL_SPEED
                self.is_on_ladder = False
                self.pymunk.gravity = None  # 把原来的重力(0,0)清除 开始自由落体

        # Add to the odometer how far we've moved  将横向的变化量累加起来
        self.x_odometer += dx
        # step012.py添加 垂直方向的变化量
        self.y_odometer += dy

        # 设置在梯子上时的动作变化
        if self.is_on_ladder and not is_on_ground:
            # Have we moved far enough to change the texture?
            # 当在梯子上的垂直的变化量大于我们设定的可以进行动作切换的值时 改变动作 并重置变化量
            if abs(self.y_odometer) > DISTANCE_TO_CHANGE_TEXTURE:
                # Reset the odometer
                self.y_odometer = 0
                # Advance the walking animation
                # 切换下一张动作图的索引
                self.cur_texture += 1
            # 有两张图就可以
            if self.cur_texture > 1:
                self.cur_texture = 0
            # 更改梯子上的动作
            self.texture = self.climbing_textures[self.cur_texture]
            return

        # Jumping animation
        if not is_on_ground:
            if dy > DEAD_ZONE:
                self.texture = self.jump_texture_pair[self.character_face_direction]
                return
            elif dy < -DEAD_ZONE:
                self.texture = self.fall_texture_pair[self.character_face_direction]
                return

        # Idle animation
        if abs(dx) <= DEAD_ZONE:
            self.texture = self.idle_texture_pair[self.character_face_direction]
            return

        # Have we moved far enough to change the texture?
        if abs(self.x_odometer) > DISTANCE_TO_CHANGE_TEXTURE:

            # Reset the odometer
            self.x_odometer = 0

            # Advance the walking animation
            self.cur_texture += 1
            if self.cur_texture > 7:
                self.cur_texture = 0
            self.texture = self.walk_textures[self.cur_texture][self.character_face_direction]


class BulletSprite(arcade.SpriteSolidColor):
    """ Bullet Sprite """
    def pymunk_moved(self, physics_engine, dx, dy, d_angle):
        """ Handle when the sprite is moved by the physics engine. """
        # If the bullet falls below the screen, remove it
        # 当子弹下落并小于某一个值时 令其消失 这里设置为50 容易观察 负数则在掉出屏幕底部时消失(一般也就设置为负数)
        if self.center_y < 50:
            self.remove_from_sprite_lists()


class GameWindow(arcade.Window):
    """ Main Window 主窗体"""

    def __init__(self, width, height, title):
        """ Create the variables 创建变量"""

        # 继承父类的属性
        super().__init__(width, height, title)

        # Physics engine 初始化物理引擎
        self.physics_engine = Optional[arcade.PymunkPhysicsEngine]

        # Player sprite 定义玩家精灵变量 冒号后面是说明变量的类型
        # self.player_sprite: Optional[arcade.Sprite] = None
        self.player_sprite: Optional[PlayerSprite] = None

        # Sprite lists we need  精灵列表变量的创建
        self.player_list: Optional[arcade.SpriteList] = None
        self.wall_list: Optional[arcade.SpriteList] = None
        self.bullet_list: Optional[arcade.SpriteList] = None
        self.item_list: Optional[arcade.SpriteList] = None
        # step012.py添加
        self.ladder_list: Optional[arcade.SpriteList] = None

        # step011.py添加 可移动物体层(不受其他物体力量的影响) 而不是动态物体层(会被其他物体的力量影响)
        self.moving_sprites_list: Optional[arcade.SpriteList] = None

        # 前景修饰层添加
        self.foreground_list = None
        # 背景修饰层添加
        self.background_list = None

        # 声音相关的变量
        # Variables used to manage our music. See setup() for giving them
        # 发射子弹的声音
        self.bullet_fire_sound = None
        # 子弹和动态物体碰撞爆炸的声音
        self.boom_explode_sound = None
        # 子弹撞墙的声音
        self.bullet_hit_wall_sound = None
        # 玩家跳跃的声音
        self.jump_sound = None

        # 背景音乐列表
        self.music_list = []
        # 当前播放的背景音乐索引
        self.current_song_index = 0
        # 当前正在播放的音乐
        self.current_player = None
        # 当前音乐
        self.music = None


        # Track the current state of what key is pressed
        # 监听方向按键状态的变量 按下为 True 未按下 为False 这里暂时只设置了左右键的状态
        self.left_pressed: bool = False
        self.right_pressed: bool = False
        # step012.py 添加
        self.up_pressed: bool = False
        self.down_pressed: bool = False

        # 屏幕滚动相关添加
        # Used to keep track of our scrolling
        self.view_bottom = 0
        self.view_left = 0

        # 创建存储地图的变量
        self.my_map = None

        # Set background color  设置一个指定的窗口背景颜色,颜色单词都大写
        arcade.set_background_color(arcade.color.AMAZON)

    # 添加切换音乐的方法 默认下一首
    def advance_song(self):
        """ Advance our pointer to the next song. This does NOT start the song. """
        self.current_song_index += 1
        if self.current_song_index >= len(self.music_list):
            self.current_song_index = 0
        print(f"Advancing song to {self.current_song_index}.")

    # 控制音乐的暂停
    def play_song(self):
        """ Play the song. """
        # Stop what is currently playing.
        # if self.music:
        #     self.music.stop()

        # Play the next song
        print(f"Playing {self.music_list[self.current_song_index]}")
        self.music = arcade.Sound(self.music_list[self.current_song_index], streaming=True)
        self.current_player = self.music.play(0.5)  # 括号里数字设置音乐声音大小
        # This is a quick delay. If we don't do this, our elapsed time is 0.0
        # and on_update will think the music is over and advance us to the next
        # song before starting this one.
        import time
        time.sleep(0.03)

    def setup(self):
        """ Set up everything with the game 设置变量具体代表的物体"""
        # 音乐播放相关
        # List of music
        self.music_list = [":resources:music/funkyrobot.mp3", ":resources:music/1918.mp3"]
        # Array index of what to play
        self.current_song_index = 0
        # Play the song
        self.play_song()

        # 屏幕滚动相关添加
        # Used to keep track of our scrolling
        self.view_bottom = 0
        self.view_left = 0

        # Create the sprite lists 创建精灵列表(精灵组)
        # 玩家精灵组
        self.player_list = arcade.SpriteList()
        # 子弹精灵组 后续我们添加
        self.bullet_list = arcade.SpriteList()
        # 前景修饰层添加
        self.foreground_list = arcade.SpriteList()
        # 背景修饰层添加
        self.background_list = arcade.SpriteList()

        # 添加音效
        # Load sounds
        # 发射子弹的声音
        self.bullet_fire_sound = arcade.load_sound(":resources:sounds/hit2.wav")
        # 子弹和动态物体碰撞爆炸的声音
        self.boom_explode_sound = arcade.load_sound(":resources:sounds/explosion2.wav")
        # 子弹撞墙的声音
        self.bullet_hit_wall_sound = arcade.load_sound(":resources:sounds/hurt5.wav")
        # 玩家跳跃的声音
        self.jump_sound = arcade.load_sound(":resources:sounds/jump4.wav")

        # Read in the tiled map 设置tiled编辑的地图的名字 并读取
        map_name = "pymunk_test_map.tmx"  # 这个就是我们编辑地图时定义的名字 这里带了后缀名.tmx
        self.my_map = arcade.tilemap.read_tmx(map_name)
        # 使用类属性作为地图变量,方便后续在设置屏幕滚动时程序获取地图的宽高,地图快大小等信息
        # self.map_size = self.my_map .map_size
        # print('地图的宽高为:',self.map_size)

        # Read in the map layers  # 读取地图中图层的内容
        self.wall_list = arcade.tilemap.process_layer(self.my_map, 'Platforms', SPRITE_SCALING_TILES)

        self.item_list = arcade.tilemap.process_layer(self.my_map, 'Dynamic Items', SPRITE_SCALING_TILES)
        # step011.py添加 移动物体层
        self.moving_sprites_list = arcade.tilemap.process_layer(self.my_map,
                                                                'Moving Platforms',
                                                                SPRITE_SCALING_TILES)
        # step012.py添加
        self.ladder_list = arcade.tilemap.process_layer(self.my_map,
                                                        'Ladders',
                                                        SPRITE_SCALING_TILES,
                                                        use_spatial_hash=True,
                                                        hit_box_algorithm="Detailed")

        # 前景修饰层添加
        self.foreground_list = arcade.tilemap.process_layer(self.my_map,
                                                            'foreground_layer',
                                                            SPRITE_SCALING_TILES)
        # 前景修饰层添加
        self.background_list = arcade.tilemap.process_layer(self.my_map,
                                                            'background_layer',
                                                            SPRITE_SCALING_TILES)

        # Create player sprite 创建玩家精灵对象 图片地址是arcade库自带的,第二个参数是缩放系数
        # self.player_sprite = arcade.Sprite(":resources:images/animated_characters/female_person/femalePerson_idle.p
        # ng",SPRITE_SCALING_PLAYER)
        self.player_sprite = PlayerSprite(self.ladder_list, hit_box_algorithm="Detailed")



        # Set player location 定位玩家位置 加的那个SPRITE_SIZE / 2 是由于素材是由中心点位置定位的
        grid_x = 1
        grid_y = 1.5
        self.player_sprite.center_x = SPRITE_SIZE * grid_x + SPRITE_SIZE / 2
        self.player_sprite.center_y = SPRITE_SIZE * grid_y + SPRITE_SIZE / 2

        # Add to player sprite list 把玩家精灵 加入玩家精灵组
        self.player_list.append(self.player_sprite)

        # step005.py添加
        # --- Pymunk Physics Engine Setup ---

        # The default damping for every object controls the percent of velocity
        # the object will keep each second. A value of 1.0 is no speed loss,
        # 0.9 is 10% per second, 0.1 is 90% per second.
        # For top-down games, this is basically the friction for moving objects.
        # For platformers with gravity, this should probably be set to 1.0.
        # Default value is 1.0 if not specified.
        damping = DEFAULT_DAMPING

        # Set the gravity. (0, 0) is good for outer space and top-down.
        gravity = (0, -GRAVITY)

        # Create the physics engine
        self.physics_engine = arcade.PymunkPhysicsEngine(damping=damping,
                                                         gravity=gravity)
        # Add kinematic sprites
        self.physics_engine.add_sprite_list(self.moving_sprites_list,
                                            body_type=arcade.PymunkPhysicsEngine.KINEMATIC)

        # Add the player.
        # For the player, we set the damping to a lower value, which increases
        # the damping rate. This prevents the character from traveling too far
        # after the player lets off the movement keys.
        # Setting the moment to PymunkPhysicsEngine.MOMENT_INF prevents it from
        # rotating.
        # Friction normally goes between 0 (no friction) and 1.0 (high friction)
        # Friction is between two objects in contact. It is important to remember
        # in top-down games that friction moving along the 'floor' is controlled
        # by damping.
        self.physics_engine.add_sprite(self.player_sprite,
                                       friction=PLAYER_FRICTION,
                                       mass=PLAYER_MASS,
                                       moment=arcade.PymunkPhysicsEngine.MOMENT_INF,
                                       collision_type="player",
                                       max_horizontal_velocity=PLAYER_MAX_HORIZONTAL_SPEED,
                                       max_vertical_velocity=PLAYER_MAX_VERTICAL_SPEED)

        # Create the walls.
        # By setting the body type to PymunkPhysicsEngine.STATIC the walls can't
        # move.
        # Movable objects that respond to forces are PymunkPhysicsEngine.DYNAMIC
        # PymunkPhysicsEngine.KINEMATIC objects will move, but are assumed to be
        # repositioned by code and don't respond to physics forces.
        # Dynamic is default.
        self.physics_engine.add_sprite_list(self.wall_list,
                                            friction=WALL_FRICTION,
                                            collision_type="wall",
                                            body_type=arcade.PymunkPhysicsEngine.STATIC)

        # Create the items
        self.physics_engine.add_sprite_list(self.item_list,
                                            friction=DYNAMIC_ITEM_FRICTION,
                                            collision_type="item")

        # step010.py添加 在setup 方法里面定义两个方法并调用

        def wall_hit_handler(bullet_sprite, _wall_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞墙时消失"""
            bullet_sprite.remove_from_sprite_lists()
            # 播放子弹撞墙的声音
            self.bullet_hit_wall_sound.play()
        self.physics_engine.add_collision_handler("bullet", "wall", post_handler=wall_hit_handler)

        def item_hit_handler(bullet_sprite, item_sprite, _arbiter, _space, _data):
            """ Called for bullet/wall collision 子弹撞到物体时消失 """
            bullet_sprite.remove_from_sprite_lists()
            item_sprite.remove_from_sprite_lists()
            # 播放子弹射中物体后爆炸的声音
            self.boom_explode_sound.play()
        self.physics_engine.add_collision_handler("bullet", "item", post_handler=item_hit_handler)

    def on_key_press(self, key, modifiers):
        """Called whenever a key is pressed. 监听鼠标点击事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = True
        elif key == arcade.key.RIGHT:
            self.right_pressed = True
        elif key == arcade.key.UP:
            self.up_pressed = True
            # find out if player is standing on ground
            # 判断玩家按下上方向键时的位置是否在地面上 在则可以跳 不在则不能跳  这样避免了二连跳等操作
            if self.physics_engine.is_on_ground(self.player_sprite):
                # 播放玩家跳跃的音效
                self.jump_sound.play()
                # She is! Go ahead and jump  在地面上 那个则执行跳跃功能
                impulse = (0, PLAYER_JUMP_IMPULSE)  # 第一个参数代表左右的力 第二个代表上下的力
                # 由于物理有质量 受重力影响 自己还会落下来 所以 下方向键后续可以再补充(用于下蹲或者上下楼梯等)
                self.physics_engine.apply_impulse(self.player_sprite, impulse)
        elif key == arcade.key.DOWN:
            self.down_pressed = True

    def on_key_release(self, key, modifiers):
        """Called when the user releases a key. 监听鼠标释放事件"""
        if key == arcade.key.LEFT:
            self.left_pressed = False
        elif key == arcade.key.RIGHT:
            self.right_pressed = False
        elif key == arcade.key.UP:
            self.up_pressed = False
        elif key == arcade.key.DOWN:
            self.down_pressed = False

    def on_update(self, delta_time):
        """ Movement and game logic 控制屏幕画面内容的刷新和变化"""
        # 控制音乐的切换,因为一个音乐播放完后,会自动重置播放进度为0。这样无法切换下一首
        # 于是我们就去检测 当音乐播放进度为0时,切换音乐 或者重新播放
        position = self.music.get_stream_position(self.current_player)
        # The position pointer is reset to 0 right after we finish the song.
        # This makes it very difficult to figure out if we just started playing
        # or if we are doing playing.
        if position == 0.0:
            self.advance_song()
            self.play_song()

        # Update player forces based on keys pressed  在按键按下时更新玩家受到的力

        # 在刷新时再次检测物体当前是否在地面上 或者 空中 以便于设置不同的向左 或向右的力
        is_on_ground = self.physics_engine.is_on_ground(self.player_sprite)
        if self.left_pressed and not self.right_pressed:
            # Create a force to the left. Apply it. 创建一个向左的力 并开始执行
            # step012.py 加入的 新条件时 or self.player_sprite.is_on_ladder
            if is_on_ground or self.player_sprite.is_on_ladder:
                force = (-PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (-PLAYER_MOVE_FORCE_IN_AIR, 0)

            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为0的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.right_pressed and not self.left_pressed:
            # Create a force to the right. Apply it. 创建一个向右的力 并开始执行

            if is_on_ground or self.player_sprite.is_on_ladder:
                force = (PLAYER_MOVE_FORCE_ON_GROUND, 0)
            else:
                force = (PLAYER_MOVE_FORCE_IN_AIR, 0)
            self.physics_engine.apply_force(self.player_sprite, force)
            # Set friction to zero for the player while moving 当移动的时候,物理引擎给玩家精灵一个值为 0 的摩擦力
            self.physics_engine.set_friction(self.player_sprite, 0)
        # step012.py添加
        elif self.up_pressed and not self.down_pressed:
            # Create a force to the up. Apply it.
            if self.player_sprite.is_on_ladder:
                force = (0, PLAYER_MOVE_FORCE_ON_GROUND)
                self.physics_engine.apply_force(self.player_sprite, force)
                # Set friction to zero for the player while moving
                self.physics_engine.set_friction(self.player_sprite, 0)
        elif self.down_pressed and not self.up_pressed:
            # Create a force to the down. Apply it.
            if self.player_sprite.is_on_ladder:
                force = (0, -PLAYER_MOVE_FORCE_ON_GROUND)
                self.physics_engine.apply_force(self.player_sprite, force)
                # Set friction to zero for the player while moving
                self.physics_engine.set_friction(self.player_sprite, 0)
        else:
            # Player's feet are not moving. Therefore up the friction so we stop.
            # 当没有单个按键按下时,给玩家精灵一个最大的摩擦力 1 以快速停止移动
            self.physics_engine.set_friction(self.player_sprite, 1.0)

        # step011.py添加 当移动物体层的物体移动到给他定义的边缘时 让他再原路返回
        # For each moving sprite, see if we've reached a boundary and need to
        # reverse course.
        for moving_sprite in self.moving_sprites_list:
            # print(moving_sprite.top,moving_sprite.bottom,moving_sprite.boundary_top,moving_sprite.boundary_bottom,moving_sprite.change_y)
            # 下面这行写的很长 主要时为了观看时便于理解代码含义 为了规范书写 也可以换行
            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
            # velocity 是一个速度列表,第一个时横向速度,第二个是纵向速度(是每秒的改变量,因此下面我们要变成每帧的变化量再用)
            # 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)

            # 屏幕滚动相关添加
            # --- Manage Scrolling ---
            # Track if we need to change the viewport
            changed = False
            # Scroll left
            left_boundary = self.view_left + LEFT_VIEWPORT_MARGIN
            if self.player_sprite.left < left_boundary:
                self.view_left -= left_boundary - self.player_sprite.left
                changed = True
            # Scroll right
            right_boundary = self.view_left + SCREEN_WIDTH - RIGHT_VIEWPORT_MARGIN
            if self.player_sprite.right > right_boundary:
                self.view_left += self.player_sprite.right - right_boundary
                changed = True
            # Scroll up
            top_boundary = self.view_bottom + SCREEN_HEIGHT - TOP_VIEWPORT_MARGIN
            if self.player_sprite.top > top_boundary:
                self.view_bottom += self.player_sprite.top - top_boundary
                changed = True
            # Scroll down
            bottom_boundary = self.view_bottom + BOTTOM_VIEWPORT_MARGIN
            if self.player_sprite.bottom < bottom_boundary:
                self.view_bottom -= bottom_boundary - self.player_sprite.bottom
                changed = True
            # print('地图块大小',self.my_map.tile_size)
            #  self.my_map.tile_size 代表组成地图的 每一个地图块的大小 和地图编辑器中创建时一样,是32*32 但是由于地图编辑器输出文件的时候
            #  设置输出成了16*16的,所以我们要再整除2 变成游戏中实际的大小
            # self.my_map.map_size.width 地图单行横向的格子数
            # self.my_map.map_size.height 地图单行纵向的格子数
            # 设置地图界面从左到右的宽度 结果是用 图块个数表示
            TILED_MAP_WIDTH = self.my_map.map_size.width * self.my_map.tile_size.width // 2
            # 设置地图界面从上到下的高度 结果是用 图块个数表示
            TILED_MAP_HEIGHT = self.my_map.map_size.height * self.my_map.tile_size.height // 2
            if changed:
                # Only scroll to integers. Otherwise we end up with pixels that
                # don't line up on the screen
                self.view_bottom = max(min(int(self.view_bottom), TILED_MAP_HEIGHT-SCREEN_HEIGHT), 0)
                self.view_left = max(min(int(self.view_left), TILED_MAP_WIDTH - SCREEN_WIDTH), 0)
                view_right = min(SCREEN_WIDTH + self.view_left, TILED_MAP_WIDTH)
                view_top = min(TILED_MAP_HEIGHT, self.view_bottom+SCREEN_HEIGHT)
                # view_top = min(TILED_MAP_HEIGHT+20, self.view_bottom+SCREEN_HEIGHT+20)

                # if SCREEN_WIDTH + self.view_left > TILED_MAP_WIDTH:
                #     self.view_right = TILED_MAP_WIDTH
                #     self.view_left =TILED_MAP_WIDTH -SCREEN_WIDTH
                # else:
                #     self.view_right = SCREEN_WIDTH + self.view_left

                # if self.view_bottom <= 0:
                #     self.view_bottom = 0
                # if self.view_left <= 0:
                #     self.view_left = 0

                # Do the scrolling  设置视口大小
                arcade.set_viewport(self.view_left,
                                    view_right,
                                    self.view_bottom,
                                    view_top)
        # 物理引擎在帧率刷新时被调用 on_update方法里的代码默认每秒执行60遍
        self.physics_engine.step()

    def on_draw(self):
        """ Draw everything 绘制界面 后执行的绘制 会覆盖先绘制的图案"""
        arcade.start_render()
        self.wall_list.draw()
        self.ladder_list.draw()  # step012.py添加
        self.moving_sprites_list.draw()  # step011.py添加
        self.bullet_list.draw()  # 子弹
        self.item_list.draw()  # 动态物体(例如箱子)
        self.background_list.draw()  # 背景修饰层 先添加的会被后添加的挡住 放在玩家前添加 即为背景
        self.player_list.draw()  # 玩家
        self.foreground_list.draw()  # 前景修饰层 后添加的会挡住先添加的 放在玩家后添加 即为前景

    # 当鼠标点击屏幕时 会给该方法传入以下参数 x :横坐标 y:纵坐标 button:鼠标左中右按的哪个键 modifiers:修饰键 alt shift 等这些
    def on_mouse_press(self, x, y, button, modifiers):
        print(x,y,'左侧',self.player_sprite.left,'底部',self.player_sprite.bottom)
        """ Called whenever the mouse button is clicked.
        当鼠标点击时 本方法被调用"""
        # 使用创建据矩形的方法 创建一个子弹
        # bullet = arcade.SpriteSolidColor(20, 5, arcade.color.DARK_YELLOW)
        bullet = BulletSprite(20, 5, arcade.color.DARK_YELLOW)
        self.bullet_list.append(bullet)   #  把子弹加入上面就定义过的子弹精灵组

        # Position the bullet at the player's current location
        # 定义子弹的初始位置 也就是精灵的位置
        start_x = self.player_sprite.center_x
        start_y = self.player_sprite.center_y
        bullet.position = self.player_sprite.position

        # Get from the mouse the destination location for the bullet
        # IMPORTANT! If you have a scrolling screen, you will also need
        # to add in self.view_bottom and self.view_left.
        # 记录点击位置的坐标
        dest_x = x
        dest_y = y

        # Do math to calculate how to get the bullet to the destination.
        # Calculation the angle in radians between the start points
        # and end points. This is the angle the bullet will travel.
        # 使用高一会学习的三角函数知识 计算 玩家相对于平面和鼠标点击位置之间的角度
        x_diff = dest_x - start_x
        y_diff = dest_y - start_y
        angle = math.atan2(y_diff, x_diff)

        # What is the 1/2 size of this sprite, so we can figure out how far
        # away to spawn the bullet
        size = max(self.player_sprite.width, self.player_sprite.height) / 2

        # Use angle to to spawn bullet away from player in proper direction
        # 加上size的好处是 让子弹从玩家图片外部发射出去,避免内部发射导致物理引擎误判
        bullet.center_x += size * math.cos(angle)
        bullet.center_y += size * math.sin(angle)

        # Set angle of bullet 设置子弹的朝向
        bullet.angle = math.degrees(angle)

        # Gravity to use for the bullet
        # If we don't use custom gravity, bullet drops too fast, or we have
        # to make it go too fast.
        # Force is in relation to bullet's angle.
        # 专门定义以下子弹垂直方向受到的重力 而不是默认的
        bullet_gravity = (0, -BULLET_GRAVITY)

        # Add the sprite. This needs to be done AFTER setting the fields above.
        # 使用物理引擎管理子弹 这个子弹是具有弹性的
        self.physics_engine.add_sprite(bullet,
                                       mass=BULLET_MASS,
                                       damping=1.0,
                                       friction=0.6,
                                       collision_type="bullet",
                                       gravity=bullet_gravity,
                                       elasticity=0.9)


        # Add force to bullet
        # 最后 让物理引擎给子弹施加一个力量,让他飞走
        force = (BULLET_MOVE_FORCE, 0)
        self.physics_engine.apply_force(bullet, force)
        self.bullet_fire_sound.play()


def main():
    """ Main method 主程序 由最后一行调动 然后再调动游戏整体"""
    ''' 初始化传入屏幕的宽 高 标题'''
    window = GameWindow(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
    window.setup()
    arcade.run()


if __name__ == "__main__":
    main()

十八、拓展(5):添加分数文字等

老规矩,添加什么就要准备什么的变量
这里 我们先准备分数的变量即可 因为他真的是随全局程序会变化的
init
在这里插入图片描述
setup()
在这里插入图片描述
在这里插入图片描述
on_draw()
把分数写到屏幕上
在这里插入图片描述

十九、拓展(6):定时器功能

本拓展将使用定时器功能制作一个 倒计时,然后变成文本在界面显示

当你想在游戏中,想让某个功能代码像手表的指针一样有规律的循环执行时,你可以时用arcade自带的schedule()方法
在这里插入图片描述

代码中用法如下:
在这里插入图片描述
放到on_draw()方法里显示
在这里插入图片描述

终结

总体代码比看官网的更准确一些,至于思路和过程,也建议有能力的同学,结合官网的英文讲解进行理解(网页自动翻译成的汉语作为辅助就行,翻译不准时实在是影响理解)
祝大家都能做出自己心仪的小游戏,我也会不断努力的

  • 3
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值