Python-安卓应用构建教程-使用-Kivy-和-AndroidStudio-三-

72 阅读43分钟

Python 安卓应用构建教程:使用 Kivy 和 AndroidStudio(三)

原文:Building Android Apps in Python Using Kivy with Android Studio

协议:CC BY-NC-SA 4.0

六、完成并发布您的第一个游戏

在前一章中,我们开始开发一个多层次的跨平台游戏,其中一个角色根据触摸位置移动,使用动画。玩家的工作是在屏幕上收集一些均匀分布的硬币。一个怪物试图攻击玩家以杀死它。当玩家和怪物发生碰撞时,玩家死亡,关卡结束。直到上一章结束,游戏才有了单关。

在这一章中,我们继续通过增加更多的关卡来开发游戏。第四章介绍的屏幕将用于组织游戏界面。将会添加更多的怪物。火将被引入,通过预先定义的路径来来回回。随着游戏的开发,重要的问题将会被讨论和解决。在下一章的最后,这款游戏将在 Google Play 上发布,供 Android 设备上的任何用户下载和安装。

给游戏增加更多关卡

我们的游戏只有一个关卡,玩家必须收集五个硬币才能完成。任务完成后,"Level Completed"消息显示在Label小工具中。在这一部分,更多的关卡将被添加到游戏中,这样玩家就可以从一个关卡进入另一个。在构建这个多关卡游戏的时候,我们会尽量遵循不重复自己(干)的原则。

在 Kivy 中创建多关卡游戏的最佳方式是使用多个屏幕,每个屏幕包含一级的小部件树。因此,我们将使用ScreenScreenManager类来构建和管理多个屏幕。让我们从创建我们将在开发游戏时使用的模板开始。

创建多个屏幕

正如我们在第四章中讨论的,在 Python 文件中为每个要创建的屏幕创建了一个类。因此,为了创建一个具有两个屏幕的应用,在 Python 文件中创建了三个类。一个类用于应用,两个类用于两个屏幕。在我们的游戏中,我们将开始在前一关的基础上增加另一关。所以,一共有两个层次。每个级别都有一个屏幕,因此为每个屏幕创建了一个新的类。将有一个额外的屏幕被用作游戏主屏幕,玩家可以从这里进入任何级别。因此,清单 6-1 中显示的 Python 文件将有三个屏幕。为代表级别的两个屏幕创建的类被命名为Level1Level2。主屏幕类被命名为MainScreen。因为类是空的,所以pass关键字作为虚拟类体被添加。

import kivy.app
import kivy.uix.screenmanager

class TestApp(kivy.app.App):
    pass

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    pass

class Level2(kivy.uix.screenmanager.Screen):
    pass

app = TestApp()
app.run()

Listing 6-1Adding a Level and a Main Screen to the Game Using Screens

在清单 6-2 中的 KV 文件中,ScreenManager被用作应用小部件树中的根小部件。它有三个子代,分别是三个 screens 类的实例,第一个名为level1,第二个名为level2,主屏幕名为main。通过将屏幕类名括在<>之间,可以定义 KV 文件中每个屏幕的布局。

记住,我们之前创建的关卡有一个FloatLayout来保存所有的游戏部件。因为每个屏幕代表一个游戏关卡,它将有一个子级FloatLayout来保存关卡中的所有小部件。这个布局将有一个使用canvas.before内的Rectangle顶点指令关联的背景图像。第一级和第二级图像的名称分别为bg_lvl1.jpgbg_lvl2.jpg

ScreenManager:
    MainScreen:
    Level1:
    Level2:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"
<Level1>:
    name: "level1"
    FloatLayout:
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl1.jpg"

<Level2>:
    name: "level2"
    FloatLayout:
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos

                source: "bg_lvl2.jpg"

Listing 6-2Defining the Widget Tree of the Second Level of the Game

记住,ScreenManager类有一个名为current的属性,它接受要在窗口中显示的屏幕名称。如果没有明确指定,它默认为添加到窗口小部件树的第一个屏幕,也就是MainScreen屏幕。该屏幕有一个带有两个子按钮的BoxLayout。每个按钮负责转换到一个游戏级别。在我们运行应用后,我们会看到图 6-1 中的窗口。

img/481739_1_En_6_Fig1_HTML.jpg

图 6-1

游戏的主画面有两个关卡

通过准备这些 KV 和 Python 文件,我们设置了构建两级游戏所遵循的模板。建立更多的关卡将会重复我们后面将要讨论的步骤。

在 KV 文件中添加定制小部件

在之前的游戏中,单人关卡在FloatLayout里面有三个小部件。这些小部件是一个用于打印收集硬币数量的Label,一个用于怪物的Image小部件,以及另一个用于角色的Image小部件。为了构建这两个级别,我们不必为每个屏幕复制三个小部件。

我们可以只创建一次并多次使用,而不是为每个级别创建一个单独的Image小部件。这是通过为游戏中的每个元素创建一个自定义小部件,然后在每个屏幕中重用它。清单 6-3 中修改后的 KV 文件创建了每个级别所需的三个定制小部件。开始时,两个级别之间会有微小的差异。稍后,我们将在第二关中添加更多的怪物和硬币。

名为MonsterCharacter的两个定制小部件分别代表怪物和角色。它们是用来扩展Image小部件的。使用@字符以 KV 为单位进行扩展。记得用<>括起定制小部件的名称。

创建了另一个名为NumCoinsCollected的定制小部件,它扩展了Label小部件来打印收集的硬币数量。每个小部件中的代码与我们在上一个游戏中使用的完全相同。

ScreenManager:
    MainScreen:
    Level1:
    Level2:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"
<Level1>:
    name: "level1"
    FloatLayout:
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl1.jpg"
        NumCollectedCoins:
        Monster:
        Character:

<Level2>:
    name: "level2"
    FloatLayout:
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl2.jpg"
        NumCollectedCoins:
        Monster:
        Character:

<NumCollectedCoins@Label>:
    id: num_coins_collected

    size_hint: (0.1, 0.02)
    pos_hint: {'x': 0.0, 'y': 0.97}
    text: "Coins 0"
    font_size: 20

<Monster@Image>:
    id: monster_image
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.8, 'y': 0.8}
    source: "10.png"
    im_num: 10
    allow_stretch: True
    on_im_num: app.change_monst_im(self)
    on_pos_hint: app.monst_pos_hint(self)

<Character@Image>:
    id: character_image
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.2, 'y': 0.6}
    source: "0.png"
    im_num: 0
    allow_stretch: True
    on_im_num: app.change_char_im(self)
    on_pos_hint: app.char_pos_hint(self)

Listing 6-3Creating Custom Widgets for Game Elements

记住,on_im_numon_pos_hint事件被绑定到MonsterCharacter小部件。所有级别的所有Character小部件的事件都使用相同的回调函数来处理,它们是change_char_im()char_pos_hint(),存在于应用类TestApp中。同样的过程也适用于Monster小部件。因为同一个函数将处理不同小部件触发的事件,所以知道哪个小部件触发了事件非常重要。这就是函数接受self参数的原因,该参数指的是触发事件的小部件。

给小部件分配 id

为了在每个屏幕中使用这三个定制小部件,我们创建了一个实例。如果从任一自定义小部件中获取多个实例,就会出现问题。自定义小部件被赋予 id。因为这种小部件的每个实例都继承了它的所有属性,所以如果没有改变,所有实例都将具有相同的 ID。

在 Kivy 中,同一个小部件树中的两个小部件不能有相同的 ID。注意,这两个屏幕仍然在同一个小部件树中,因为它们分组在ScreenManager根小部件下。出于这个原因,我们应该从定制小部件模板中移除 ID,并将其添加到实例中,如清单 6-4 中的新 KV 文件所示。

为了便于操作,同一个小部件的 id 反映了它们所在屏幕的索引。此外,不同屏幕上的相同小部件被赋予相同的名称,除了其 ID 末尾的屏幕索引。例如,第一个屏幕中的Monster小部件被赋予一个 ID monster_image_lvl1,反映出它驻留在索引为 1 的屏幕中。这个小部件在索引为 2 的屏幕中被赋予一个 ID monster_image_lvl2。请注意,主屏幕的索引为 0。

因为我们需要向每个屏幕的FloatLayout小部件添加硬币,所以它们被赋予了 id,以便在 Python 代码中引用它们。他们被赋予了 idlayout_lvl1layout_lvl2

ScreenManager:
    MainScreen:
    Level1:
    Level2:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"
<Level1>:
    name: "level1"
    FloatLayout:
        id: layout_lvl1
        on_touch_down: app.touch_down_handler(1, args)
        canvas.before:
            Rectangle:
                size: self.size

                pos: self.pos
                source: "bg_lvl1.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl1
        Monster:
            id: monster_image_lvl1
        Character:
            id: character_image_lvl1

<Level2>:
    name: "level2"
    FloatLayout:
        id: layout_lvl2
        on_touch_down: app.touch_down_handler(2, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl2.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl2
        Monster:
            id: monster_image_lvl2
        Character:
            id: character_image_lvl2

<NumCollectedCoins@Label>:
    size_hint: (0.1, 0.02)
    pos_hint: {'x': 0.0, 'y': 0.97}
    text: "Coins 0"
    font_size: 20

<Monster@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.8, 'y': 0.8}

    source: "10.png"
    im_num: 10
    allow_stretch: True
    on_im_num: app.change_monst_im(self)
    on_pos_hint: app.monst_pos_hint(self)

<Character@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.2, 'y': 0.6}
    source: "0.png"
    im_num: 0
    allow_stretch: True
    on_im_num: app.change_char_im(self)
    on_pos_hint: app.char_pos_hint(self)

Listing 6-4Removing IDs from Custom Widgets and Adding Them Inside the Instances

注意,我们需要将on_touch_down事件绑定到FloatLayout。这是因为游戏中的主角根据触摸的位置移动。在之前的游戏中,记得我们创建了回调函数touch_down_handler()来处理接受args参数的事件。

请注意,所有屏幕都将使用相同的事件处理程序。因此,为了正确处理它,知道哪个屏幕触发了事件是非常重要的。这个概念适用于 Python 文件中的所有共享函数。因此,代表屏幕索引的附加参数被添加到函数中。通过这样做,我们可以引用触发事件的确切屏幕。

这是 KV 文件创建两个游戏关卡所需的大部分工作。后面会有简单的补充。让我们继续看 Python 文件。

游戏类别变量

在之前的单关游戏中,TestApp类中定义了四个变量,分别是character_killednum_coinsnum_coins_collectedcoins_ids。注意,如果一个变量被添加到应用类TestApp,它将在每个屏幕上共享。因此,两个屏幕将使用相同的变量。为了解决这个问题,将在每个屏幕类中定义这样的变量,如清单 6-5 中的 Python 文件所示。

import kivy.app
import kivy.uix.screenmanager

class TestApp(kivy.app.App):
    pass

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()
app.run()

Listing 6-5Adding Variables Inside the Level Classes

屏幕 on_pre_enter 事件

根据第五章的图 5-31 和图 5-32,它们总结了执行的游戏流程,build()方法是根据 Kivy 应用生命周期在我们的代码中执行的第一个函数。这个方法用于将硬币的部件添加到部件树中。

因为该功能在应用级执行,所以它不会区分不同的屏幕。因为我们需要在每个屏幕上添加硬币,所以我们应该使用一个名为 per screen 的方法。因此,build()方法不再适用。

Screen类提供了一个名为on_pre_enter的事件,当屏幕在显示之前将要被使用时,该事件被触发。它的回调功能将是添加硬币部件的好地方。该事件被绑定到清单 6-6 中 KV 文件的每个屏幕。名为screen_on_pre_enter()的回调函数为两个屏幕处理这样的事件。为了知道哪个屏幕触发了该事件,这个回调函数接受一个引用ScreenManager中屏幕索引的参数。

ScreenManager:
    MainScreen:
    Level1:
    Level2:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"

<Level1>:
    name: "level1"
    on_pre_enter: app.screen_on_pre_enter(1)
    FloatLayout:
        id: layout_lvl1
        on_touch_down: app.touch_down_handler(1, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl1.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl1
        Monster:
            id: monster_image_lvl1
        Character:
            id: character_image_lvl1

<Level2>:
    name: "level2"
    on_pre_enter: app.screen_on_pre_enter(2)
    FloatLayout:
        id: layout_lvl2
        on_touch_down: app.touch_down_handler(2, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl2.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl2
        Monster:
            id: monster_image_lvl2
        Character:
            id: character_image_lvl2

<NumCollectedCoins@Label>:
    size_hint: (0.1, 0.02)
    pos_hint: {'x': 0.0, 'y': 0.97}
    text: "Coins 0"
    font_size: 20

<Monster@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.8, 'y': 0.8}
    source: "10.png"
    im_num: 10
    allow_stretch: True
    on_im_num: app.change_monst_im(self)
    on_pos_hint: app.monst_pos_hint(self)

<Character@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.2, 'y': 0.6}
    source: "0.png"
    im_num: 0
    allow_stretch: True
    on_im_num: app.change_char_im(self)
    on_pos_hint: app.char_pos_hint(self)

Listing 6-6Binding the on_pre_enter Event to Each Screen

向游戏关卡添加硬币

screen_on_pre_enter()回调函数中,硬币被添加到每个屏幕的FloatLayout小部件中,如清单 6-7 中的 Python 文件所示。请注意如何检索num_coins屏幕类变量。它使用其 ID 返回。根据回调函数头中的screen_num参数,该数字被附加到layout_lvl字符串,以便返回触发on_pre_enter事件的屏幕内部的布局。

创建硬币Image小部件后,根据随机模块中使用uniform()函数随机生成的位置,小部件被添加到屏幕的FloatLayout小部件内。

最后,小部件引用被插入到 screen 类中定义的coins_ids字典中。它的访问方式类似于num_coins变量。

import kivy.app
import kivy.uix.screenmanager
import random

class TestApp(kivy.app.App):

    def screen_on_pre_enter(self, screen_num):
        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)
            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            curr_screen.coins_ids['coin'+str(k)] = coin

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()
app.run()

Listing 6-7Adding Coins to the Screen Inside the screen_on_pre_enter() Callback Function

使用索引引用 Python 中的屏幕

使用下一行在curr_screen变量中返回对当前屏幕类的引用。关键字self指的是TestApp类。root返回根小部件,也就是ScreenManager。为了访问管理器内的屏幕,返回ScreenManager内的screens列表。

curr_screen = self.root.screens[screen_num]

打印时,该列表的内容显示在显示屏幕名称的下一行。

[<Screen name="main">, <Screen name="level1">, <Screen name="level2">]

名为level1的屏幕是第二个元素,因此索引为 1。level2屏幕的索引为 2,主屏幕的索引为 0。返回类引用后,我们可以访问其中的任何变量。图 6-2 显示运行应用后的level2屏幕。

img/481739_1_En_6_Fig2_HTML.jpg

图 6-2

第二层屏幕

屏幕 on_enter 事件

我们用 screen 类的 on_pre_enter 事件的回调替换了build()方法。根据 Kivy 应用生命周期,我们之前游戏中要调用的下一个方法是on_start()方法。同样,这个方法在整个应用中只存在一次。我们需要使用一种方法来区分不同的屏幕。

Screen类有一个名为on_enter的事件,它正好在屏幕显示之前被触发。为了将该事件绑定到屏幕上,修改后的 KV 文件如清单 6-8 所示。类似于on_pre_enter事件,有一个名为screen_on_enter()的回调函数与所有屏幕相关联。它接受一个引用触发事件的屏幕索引的数字。

ScreenManager:
    MainScreen:
    Level1:
    Level2:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"

<Level1>:
    name: "level1"
    on_pre_enter: app.screen_on_pre_enter(1)
    on_enter: app.screen_on_enter(1)
    FloatLayout:
        id: layout_lvl1
        on_touch_down: app.touch_down_handler(1, args)
        canvas.before:
            Rectangle:
                size: self.size

                pos: self.pos
                source: "bg_lvl1.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl1
        Monster:
            id: monster_image_lvl1
        Character:
            id: character_image_lvl1

<Level2>:
    name: "level2"
    on_pre_enter: app.screen_on_pre_enter(2)
    on_enter: app.screen_on_enter(2)
    FloatLayout:
        id: layout_lvl2
        on_touch_down: app.touch_down_handler(2, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl2.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl2
        Monster:
            id: monster_image_lvl2
        Character:
            id: character_image_lvl2

<NumCollectedCoins@Label>:
    size_hint: (0.1, 0.02)
    pos_hint: {'x': 0.0, 'y': 0.97}

    text: "Coins 0"
    font_size: 20

<Monster@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.8, 'y': 0.8}
    source: "10.png"
    im_num: 10
    allow_stretch: True
    on_im_num: app.change_monst_im(self)
    on_pos_hint: app.monst_pos_hint(self)

<Character@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.2, 'y': 0.6}
    source: "0.png"
    im_num: 0
    allow_stretch: True
    on_im_num: app.change_char_im(self)
    on_pos_hint: app.char_pos_hint(self)

Listing 6-8Binding the on_enter Event to the Screens Inside the KV File

清单 6-9 展示了实现screen_on_enter()回调函数后的 Python 文件。在引用当前屏幕和使用其 ID 的Monster小部件(通过将屏幕号附加到monster_image_lvl字符串)后,一切都类似于上一个游戏的on_start()方法。

import kivy.app
import kivy.uix.screenmanager
import random

class TestApp(kivy.app.App):

    def screen_on_pre_enter(self, screen_num):
        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)

            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            self.root.screens[screen_num].ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            self.root.screens[screen_num].coins_ids['coin'+str(k)] = coin

    def screen_on_enter(self, screen_num):
        curr_screen = self.root.screens[screen_num]
        monster_image = curr_screen.ids['monster_image_lvl'+str(screen_num)]
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False

    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()
app.run()

Listing 6-9Implementing the screen_on_enter() Callback Function Inside the Python File

怪物动画

screen_on_enter()函数调用start_monst_animation()函数来启动怪物的动画。该功能是根据清单 6-10 中修改后的 Python 文件实现的。在之前的游戏中,动画开始时使用当前多关卡游戏中预定义的 ID 引用怪物Image小部件。因此,这个函数被修改,以便接受Monster小部件来运行动画。

import kivy.app
import kivy.uix.screenmanager
import random

class TestApp(kivy.app.App):

    def screen_on_pre_enter(self, screen_num):
        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)
            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            curr_screen.coins_ids['coin'+str(k)] = coin

    def screen_on_enter(self, screen_num):
        curr_screen = self.root.screens[screen_num]
        monster_image = curr_screen.ids['monster_image_lvl'+str(screen_num)]
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def start_monst_animation(self, monster_image, new_pos, anim_duration):
        monst_anim = kivy.animation.Animation(pos_hint={'x': new_pos[0], 'y': new_pos[1]}, im_num=17,duration=anim_duration)
        monst_anim.bind(on_complete=self.monst_animation_completed)
        monst_anim.start(monster_image)

def monst_animation_completed(self, *args):

    monster_image = args[1]
    monster_image.im_num = 10

    new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
    self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False

    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()
app.run()

Listing 6-10Starting the Monster Animation Inside the screen_on_enter() callback Function

怪物动画在完成后触发on_complete事件,并有一个名为monst_animaiton_completed()的回调函数,该函数在清单 6-10 中实现。

注意 Monster 小部件是如何被引用的。这个事件在args变量中传递参数。当它被打印出来时,看起来如下所示:

(<kivy.animation.Animation object at 0x7f9eeb0f2a70>, <WeakProxy to <kivy.factory.Monster object at 0x7f9eeb15f8d0
≫
)

它是一个元组,其中索引 0 处的第一个元素引用触发事件的动画,索引 1 处的第二个元素是与动画相关联的小部件。因此,我们可以通过用索引 1 索引args来直接引用小部件。

处理 Monster on_pos_hint 和 on_im_num 事件

根据清单 6-8 中的 KV 文件,Monster widget 触发on_pos_hinton_im_num events,使用monst_pos_hint()change_monst_im()回调函数处理。清单 6-11 中的新 KV 文件显示了它们的实现。

这两个函数都接受触发事件的小部件。在函数内部,我们不仅需要怪物小部件,还需要角色小部件。我们如何引用那个小部件?

import kivy.app
import kivy.uix.screenmanager
import random
import kivy.clock
import functools

class TestApp(kivy.app.App):

    def screen_on_pre_enter(self, screen_num):
        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)
            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            curr_screen.coins_ids['coin'+str(k)] = coin

    def screen_on_enter(self, screen_num):
        curr_screen = self.root.screens[screen_num]
        monster_image = curr_screen.ids['monster_image_lvl'+str(screen_num)]
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def start_monst_animation(self, monster_image, new_pos, anim_duration):
        monst_anim = kivy.animation.Animation(pos_hint={'x': new_pos[0], 'y': new_pos[1]}, im_num=17,duration=anim_duration)
        monst_anim.bind(on_complete=self.monst_animation_completed)
        monst_anim.start(monster_image)

    def monst_animation_completed(self, *args):
        monster_image = args[1]
        monster_image.im_num = 10

        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self, monster_image):
        screen_num = int(monster_image.parent.parent.name[5:])
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]

        character_center = character_image.center
        monster_center = monster_image.center

        gab_x = character_image.width / 2
        gab_y = character_image.height / 2
        if character_image.collide_widget(monster_image) and abs(character_center[0] - monster_center[0]) <= gab_x and abs(character_center[1] - monster_center[1]) <= gab_y and curr_screen.character_killed == False:
            curr_screen.character_killed = True

            kivy.animation.Animation.cancel_all(character_image)
            kivy.animation.Animation.cancel_all(monster_image)

            character_image.im_num = 91
            char_anim = kivy.animation.Animation(im_num=95)
            char_anim.start(character_image)
            kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)

    def change_monst_im(self, monster_image):
        screen_num = int(monster_image.parent.parent.name[5:])
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def back_to_main_screen(self, screenManager, *args):
        screenManager.current = "main"

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()
app.run()

Listing 6-11Handling the on_pos_hint and on_im_num Events of the Monster Widget

引用 Screen FloatLayout 使用其子级

我们可以使用parent属性引用Monster小部件的父部件FloatLayout。从Floatlayout中,我们可以再次使用父属性来引用它的parent屏幕。引用屏幕后,我们可以使用name属性访问它的名称。在 KV 文件中,两级屏幕的名称末尾有一个数字,表示屏幕索引。返回屏幕索引的行如下所示。指数从 5 开始。

在当前只有两级的游戏中,屏幕的指数是 1 和 2。因此,我们可以只使用-1而不是5:来返回屏幕名称末尾的数字。但是只有当屏幕的索引是从 0 到 9 的单个数字时,这才会起作用。如果屏幕的索引为 10,它将不起作用。

为了将索引转换成数字,使用了int()函数。

screen_num = int(monster_image.parent.parent.name[5:])

返回的索引保存在screen_num变量中。该索引可用于根据 ID 引用Character小部件,就像之前对Monster小部件所做的那样。只需将它附加到character_image_lvl字符串中。

返回主屏幕

在角色被杀死后,应用会转到主屏幕。这是通过调度使用kivy.clock.Clock.schedule_interval()回调函数在三秒钟后将ScreenManager的当前属性更改为main来完成的。名为back_to_main_screen()的函数被调用,它接受ScreenManager。为了将参数传递给schedule_interval()函数,我们使用了functools.partial()函数。

至此,我们完成了Monster小部件的所有必需工作。让我们继续看一下Character小部件。

使用 on_touch_down 事件处理角色运动

根据清单 6-8 中的 KV 文件,FloatLayout有一个名为on_touch_down的事件,它是使用touch_down_handler()回调函数处理的,如下一个 Python 文件所示。该函数接受screen_num参数中的屏幕索引。它使用该索引来引用在 screen 类中定义的character_killed属性。

为了启动动画,回调函数调用启动Character小部件动画的start_char_animation()函数。它在清单 6-12 中实现。这个函数接受代表屏幕索引的screen_num参数。它的实现与之前游戏中讨论的完全相同,除了使用screen_num参数引用Character小部件。

import kivy.app
import kivy.uix.screenmanager
import random
import kivy.clock
import functools

class TestApp(kivy.app.App):

    def screen_on_pre_enter(self, screen_num):
        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)
            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            curr_screen.coins_ids['coin'+str(k)] = coin

    def screen_on_enter(self, screen_num):
        curr_screen = self.root.screens[screen_num]
        monster_image = curr_screen.ids['monster_image_lvl'+str(screen_num)]
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def start_monst_animation(self, monster_image, new_pos, anim_duration):
        monst_anim = kivy.animation.Animation(pos_hint={'x': new_pos[0], 'y': new_pos[1]}, im_num=17,duration=anim_duration)
        monst_anim.bind(on_complete=self.monst_animation_completed)
        monst_anim.start(monster_image)

    def monst_animation_completed(self, *args):
        monster_image = args[1]
        monster_image.im_num = 10

        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self, monster_image):
        screen_num = int(monster_image.parent.parent.name[5:])
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]

        character_center = character_image.center
        monster_center = monster_image.center

        gab_x = character_image.width / 2
        gab_y = character_image.height / 2
        if character_image.collide_widget(monster_image) and abs(character_center[0] - monster_center[0]) <= gab_x and abs(character_center[1] - monster_center[1]) <= gab_y and curr_screen.character_killed == False:
            curr_screen.character_killed = True

            kivy.animation.Animation.cancel_all(character_image)
            kivy.animation.Animation.cancel_all(monster_image)

            character_image.im_num = 91
            char_anim = kivy.animation.Animation(im_num=95)
            char_anim.start(character_image)
            kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)

    def change_monst_im(self, monster_image):
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def touch_down_handler(self, screen_num, args):
        curr_screen = self.root.screens[screen_num]
        if curr_screen.character_killed == False:
            self.start_char_animation(screen_num, args[1].spos)

    def start_char_animation(self, screen_num, touch_pos):
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]
        character_image.im_num = 0
        char_anim = kivy.animation.Animation(pos_hint={'x': touch_pos[0] - character_image.size_hint[0] / 2,'y': touch_pos[1] - character_image.size_hint[1] / 2}, im_num=7)
        char_anim.bind(on_complete=self.char_animation_completed)
        char_anim.start(character_image)

    def char_animation_completed(self, *args):
        character_image = args[1]
        character_image.im_num = 0

    def back_to_main_screen(self, screenManager, *args):
        screenManager.current = "main"

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()
app.run()

Listing 6-12Implementing the start_char_animation() Callback Function to Start the Animation of the Character Widget

因为on_complete事件被绑定到动画,所以它的回调函数char_animation_completed()是在清单 6-12 中的 Python 文件中实现的。如前所述,它使用args参数引用Character小部件。

处理字符 on_pos_hint 和 on_im_num 事件

类似于Monster小部件,KV 文件中定义的Character小部件触发两个事件— on_pos_hinton_im_num。它们的回调函数如清单 6-13 中的 Python 文件所示实现,这与前面讨论的类似。

当所有的硬币被收集后,应用返回到主屏幕,就像之前角色被杀死时一样。

import kivy.app
import kivy.uix.screenmanager
import random
import kivy.clock
import functools

class TestApp(kivy.app.App):

    def screen_on_pre_enter(self, screen_num):
        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)
            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            curr_screen.coins_ids['coin'+str(k)] = coin

    def screen_on_enter(self, screen_num):
        curr_screen = self.root.screens[screen_num]
        monster_image = curr_screen.ids['monster_image_lvl'+str(screen_num)]
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def start_monst_animation(self, monster_image, new_pos, anim_duration):
        monst_anim = kivy.animation.Animation(pos_hint={'x': new_pos[0], 'y': new_pos[1]}, im_num=17,duration=anim_duration)
        monst_anim.bind(on_complete=self.monst_animation_completed)
        monst_anim.start(monster_image)

    def monst_animation_completed(self, *args):
        monster_image = args[1]

        monster_image.im_num = 10

        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self, monster_image):
        screen_num = int(monster_image.parent.parent.name[5:])
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]

        character_center = character_image.center
        monster_center = monster_image.center

        gab_x = character_image.width / 2
        gab_y = character_image.height / 2
        if character_image.collide_widget(monster_image) and abs(character_center[0] - monster_center[0]) <= gab_x and abs(character_center[1] - monster_center[1]) <= gab_y and curr_screen.character_killed == False:
            curr_screen.character_killed = True

            kivy.animation.Animation.cancel_all(character_image)
            kivy.animation.Animation.cancel_all(monster_image)

            character_image.im_num = 91
            char_anim = kivy.animation.Animation(im_num=95)
            char_anim.start(character_image)
            kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)

    def change_monst_im(self, monster_image):
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def touch_down_handler(self, screen_num, args):
        curr_screen = self.root.screens[screen_num]
        if curr_screen.character_killed == False:
            self.start_char_animation(screen_num, args[1].spos)

    def start_char_animation(self, screen_num, touch_pos):
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]
        character_image.im_num = 0
        char_anim = kivy.animation.Animation(pos_hint={'x': touch_pos[0] - character_image.size_hint[0] / 2,'y': touch_pos[1] - character_image.size_hint[1] / 2}, im_num=7)
        char_anim.bind(on_complete=self.char_animation_completed)
        char_anim.start(character_image)

    def char_animation_completed(self, *args):
        character_image = args[1]
        character_image.im_num = 0

    def char_pos_hint(self, character_image):
        screen_num = int(character_image.parent.parent.name[5:])
        character_center = character_image.center

        gab_x = character_image.width / 3
        gab_y = character_image.height / 3
        coins_to_delete = []
        curr_screen = self.root.screens[screen_num]

        for coin_key, curr_coin in curr_screen.coins_ids.items():
            curr_coin_center = curr_coin.center
            if character_image.collide_widget(curr_coin) and abs(character_center[0] - curr_coin_center[0]) <= gab_x and abs(character_center[1] - curr_coin_center[1]) <= gab_y:
                coins_to_delete.append(coin_key)
                curr_screen.ids['layout_lvl'+str(screen_num)].remove_widget(curr_coin)
                curr_screen.num_coins_collected = curr_screen.num_coins_collected + 1
                curr_screen.ids['num_coins_collected_lvl'+str(screen_num)].text = "Coins "+str(curr_screen.num_coins_collected)
                if curr_screen.num_coins_collected == curr_screen.num_coins:
                    kivy.animation.Animation.cancel_all(character_image)
                    kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)                    kivy.animation.Animation.cancel_all(curr_screen.ids['monster_image_lvl'+str(screen_num)])

                    curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(kivy.uix.label.Label(pos_hint={'x': 0.1, 'y': 0.1}, size_hint=(0.8, 0.8), font_size=90, text="Level Completed"))

        if len(coins_to_delete) > 0:
            for coin_key in coins_to_delete:
                del curr_screen.coins_ids[coin_key]

    def change_char_im(self, character_image):
        character_image.source = str(int(character_image.im_num)) + ".png"

    def back_to_main_screen(self, screenManager, *args):
        screenManager.current = "main"

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()

app.run()

Listing 6-13Handling the on_pos_hint and on_im_num Events of the Character Widget

到上一步为止,已经成功创建了一个两级游戏,它使用的代码几乎与之前的单级游戏相同。但是在玩的时候会出现一些问题。你发现什么问题了吗?不用担心;我们将在下一节讨论它们。

游戏的问题

之前的游戏中存在六个问题。大部分是由于Screen类中定义的变量初始化不当(即Level1Level2)。

问题 1:角色在关卡开始后立即死亡

第一个问题发生在玩一个关卡时角色被杀死,然后我们试图重新开始那个关卡。这个角色很容易在关卡开始时被杀死,这取决于怪物的动作。我们来讨论一下这个问题。

当怪物和角色的位置满足monst_pos_hint()回调函数中的条件时,这意味着角色和怪物发生了碰撞,因此角色被杀死。三秒钟后,应用将自动重定向到主屏幕。当玩家再次开始相同的关卡时,角色和怪物存在于角色被杀死的相同位置。这可能会导致在新关卡开始后立即杀死角色,因为玩家将无法足够快地移动角色。

在图 6-3 所示的情况下,角色和怪物正在向箭头所指的方向移动。因为他们朝不同的方向移动,怪物的新位置可能会远离当前角色的位置。因此,角色不会被杀死。不管怎样,我们必须解决这个问题。

img/481739_1_En_6_Fig3_HTML.jpg

图 6-3

角色和怪物部件正在相互远离

解决办法

在关卡结束后,我们必须重置角色和怪物的位置。事实上,我们必须重置从上一次游戏时间计算的所有内容,例如收集的硬币数量。

最好是在关卡结束后重置位置,而不是在再次开始关卡之前,以确保没有碰撞的机会。Screen类提供了一个名为on_pre_leave的事件,该事件在离开屏幕之前被触发。在它的回调函数中,我们可以改变角色和怪物的位置,以确保它们在下一关开始时彼此远离。

清单 6-14 中显示了用于将on_pre_leave绑定到两个屏幕的修改后的 KV 文件。有必要将此事件绑定到每个级别的屏幕。名为screen_on_pre_leave()的回调函数被附加到这个事件。它接受一个定义屏幕索引的参数,以便访问 Python 文件中的屏幕及其小部件。

ScreenManager:
    MainScreen:
    Level1:
    Level2:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"

<Level1>:
    name: "level1"
    on_pre_enter: app.screen_on_pre_enter(1)
    on_pre_leave: app.screen_on_pre_leave(1)
    on_enter: app.screen_on_enter(1)
    FloatLayout:
        id: layout_lvl1
        on_touch_down: app.touch_down_handler(1, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos

                source: "bg_lvl1.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl1
        Monster:
            id: monster_image_lvl1
        Character:
            id: character_image_lvl1

<Level2>:
    name: "level2"
    on_pre_enter: app.screen_on_pre_enter(2)
    on_pre_leave: app.screen_on_pre_leave(2)
    on_enter: app.screen_on_enter(2)
    FloatLayout:
        id: layout_lvl2
        on_touch_down: app.touch_down_handler(2, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl2.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl2
        Monster:
            id: monster_image_lvl2
        Character:
            id: character_image_lvl2

<NumCollectedCoins@Label>:
    size_hint: (0.1, 0.02)
    pos_hint: {'x': 0.0, 'y': 0.97}
    text: "Coins 0"
    font_size: 20

<Monster@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.8, 'y': 0.8}

    source: "10.png"
    im_num: 10
    allow_stretch: True
    on_im_num: app.change_monst_im(self)
    on_pos_hint: app.monst_pos_hint(self)

<Character@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.2, 'y': 0.6}
    source: "0.png"
    im_num: 0
    allow_stretch: True
    on_im_num: app.change_char_im(self)
    on_pos_hint: app.char_pos_hint(self)

Listing 6-14Binding the on_pre_leave to the Screens

该功能的实现如清单 6-15 所示。它将怪物和角色的位置设置为 KV 文件中使用的相同位置。

def screen_on_pre_leave(self, screen_num):
    curr_screen = self.root.screens[screen_num]

    curr_screen.ids['monster_image_lvl' + str(screen_num)].pos_hint = {'x': 0.8, 'y': 0.8}
    curr_screen.ids['character_image_lvl' + str(screen_num)].pos_hint = {'x': 0.2, 'y': 0.6}

Listing 6-15Implementing the screen_on_pre_leave() Callback Function

问题 2:角色在重新开始同一个关卡后没有移动

角色被杀死后,monst_pos_hint()回调函数将character_killed类变量设置为True。即使在重新启动同一级别后,该标志仍然有效。根据 Python 文件中的 touch _down_handler()回调函数,只有该标志为False时才会启动角色动画。

解决办法

为了解决这个问题,必须将character_killed标志重置为False。这允许touch_down_handler()功能激活角色移动到被触摸的位置。

因为这个值在屏幕启动之前必须是 false,所以我们可以在on_pre_enter事件的screen_on_pre_enter()回调函数内部重置它。修改后的功能如清单 6-16 所示。这允许角色在重新开始被杀死的关卡时移动。

def screen_on_pre_enter(self, screen_num):
    curr_screen = self.root.screens[screen_num]
    curr_screen.character_killed = False

    coin_width = 0.05
    coin_height = 0.05

    section_width = 1.0 / curr_screen.num_coins
    for k in range(curr_screen.num_coins):
        x = random.uniform(section_width * k, section_width * (k + 1) - coin_width)
        y = random.uniform(0, 1 - coin_height)
        coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y},
                                    allow_stretch=True)
        curr_screen.ids['layout_lvl' + str(screen_num)].add_widget(coin, index=-1)
        curr_screen.coins_ids['coin' + str(k)] = coin

Listing 6-16Allowing the Character to Move When Replaying a Level It Was Killed in Previously

问题 3:角色形象从一个死的形象开始

在角色被杀死后,在monst_pos_hint()回调函数的末尾有一个动画,在进入主屏幕之前,将它的im_num从 91 变为 95。动画结束后,im_num属性将被设置为 95。

如果重启关卡,im_num的值还是 95,也就是死角色形象。我们可以用同样的方法重置怪物的图像。

解决办法

解决方法是将角色的im_num属性值改为 0,表示一个活着的角色。怪物图像也被重置为 0。这可以在screen_on_pre_enter()回调函数中更改,在清单 6-17 中进行了修改。

def screen_on_pre_enter(self, screen_num):
    curr_screen = self.root.screens[screen_num]
    curr_screen.character_killed = False
    curr_screen.ids['character_image_lvl' + str(screen_num)].im_num = 0
    curr_screen.ids['monster_image_lvl' + str(screen_num)].im_num = 10

    coin_width = 0.05
    coin_height = 0.05

    section_width = 1.0 / curr_screen.num_coins
    for k in range(curr_screen.num_coins):
        x = random.uniform(section_width * k, section_width * (k + 1) - coin_width)
        y = random.uniform(0, 1 - coin_height)
        coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y},
                                    allow_stretch=True)
        curr_screen.ids['layout_lvl' + str(screen_num)].add_widget(coin, index=-1)
        curr_screen.coins_ids['coin' + str(k)] = coin

Listing 6-17Forcing the im_num property of the Character Widget to 0 Before Starting a Level

问题 4:未收集的硬币会留在下一次相同关卡的试玩中

当角色被杀死时,会有一些剩余的硬币没有被收集。不幸的是,当玩家试图重玩相同的关卡时,这些硬币将保持不被收集。我们来讨论一下为什么会出现这种情况。

假设我们从主屏幕中选择了第二关,根据在类中定义的num_coins有八个硬币。在屏幕开始之前,硬币是在on_pre_enter事件的回调函数中创建的,这个事件就是screen_on_pre_enter()。在该函数中,为每个硬币创建一个新的Image小部件,并作为子小部件添加到FloatLayout。为了引用添加的小部件,硬币也被添加到在Screen类中定义的coins_ids字典中。根据图 6-4 打印执行回调函数前后的布局内容和字典。注意,小部件中的子部件是使用children属性返回的。

img/481739_1_En_6_Fig4_HTML.jpg

图 6-4

在调用 on_pre_enter()回调函数之前和之后打印硬币字典和游戏布局

在触发on_pre_enter之前,字典是空的,正如在类中初始化的那样,并且布局只有三个子元素,它们是在 KV 文件中添加的(CharacterMonsterNumCollectedCoins)。

在事件在screen_on_pre_enter()函数中被处理后,八个硬币Image部件被创建并添加到布局和字典中。因此,布局总共有 11 个子节点,字典有 8 个条目。

图 6-5 显示随机分布在屏幕上的硬币。

img/481739_1_En_6_Fig5_HTML.jpg

图 6-5

在屏幕上随机分配硬币

假设人物在收集到八个硬币中的两个后被杀死,如图 6-6 。请注意,左上角标签中当前显示的文本反映了只收集了两枚硬币。结果,屏幕中剩下六个硬币。

img/481739_1_En_6_Fig6_HTML.jpg

图 6-6

人物在收集了 8 个硬币中的 2 个后被杀死

玩家希望在on_pre_enter被触发之前,当他们重新开始那个关卡时,在布局中有一个空字典和三个子部件。但是根据图 6-7 ,这并没有发生。在处理事件之前,布局有九个子节点而不是三个子节点,字典有六个子节点而不是零个子节点。

img/481739_1_En_6_Fig7_HTML.jpg

图 6-7

硬币字典不会在重玩一关后重置,它会储存先前游戏中的硬币信息

在第一次玩第二关时收集了两枚硬币后,剩余的六枚硬币不会从字典中删除。注意,字典中有键— coin2coin4—它们是前一次收集的,因为它们的键在字典中丢失了。

在关卡结束后,必须重置布局和词典。

注意,游戏不重置字典也能很好地运行,但最好将所有类变量重置为初始值。执行screen_on_pre_enter()回调函数后,未被重置的字典内容和布局如图 6-8 所示。有八个新的Image小部件代表添加到布局中的新硬币,因此布局中的孩子总数现在是 17。在这 17 个部件中,有三个部件分别代表CharacterMonsterNumCollectedCoins。剩下的六个小部件是上次玩 2 级游戏时没有收集的硬币Image小部件。

img/481739_1_En_6_Fig8_HTML.jpg

图 6-8

在上次关卡试玩中没有收集到的硬币可以在再次玩该关卡时获得

字典有八个条目,这意味着之前的六个硬币从字典中删除了。之前的六个小部件没有被删除,而是被新的小部件覆盖,因为字典对条目使用相同的键(coin0coin7)。注意,带有关键字coin2coin4的项目,即之前收集的硬币,被添加到字典的末尾。

当屏幕显示时,结果如图 6-9 所示。请注意,上一次玩第二关时的前六枚硬币会显示在屏幕上,因为它们存在于FloatLayout中。但是这些硬币不能被收集。那是因为它们的位置是从coins_ids字典中检索的。因为之前的六个硬币被新的八个小部件覆盖了,所以我们无法访问它们的位置。因此,你只能从屏幕上显示的 14 个硬币中选择 8 个。

img/481739_1_En_6_Fig9_HTML.jpg

图 6-9

当重新玩这个关卡时,会出现上一关没有收集到的硬币

解决办法

因为我们需要确保coins_ids字典是空的,并且布局没有以前的硬币Image部件,我们可以在screen_on_pre_enter()回调函数中重置它们。

修改后的功能在清单 6-18 中列出。为了从FloatLayout中移除一个小部件,我们必须引用这个小部件。请记住,上一次试验中对 coins 小部件的所有引用都存储在coins_ids字典中。我们可以获取每个字典条目,返回硬币小部件,然后将其传递给remove_widget()函数,该函数由使用其 ID 和函数的screen_num参数获取的FloatLayout调用。循环结束后,使用{}重置字典。

def screen_on_pre_enter(self, screen_num):
    curr_screen = self.root.screens[screen_num]
    curr_screen.character_killed = False
    curr_screen.ids['character_image_lvl' + str(screen_num)].im_num = 0
    curr_screen.ids['monster_image_lvl' + str(screen_num)].im_num = 10

    for key, coin in curr_screen.coins_ids.items():
        curr_screen.ids['layout_lvl' + str(screen_num)].remove_widget(coin)
    curr_screen.coins_ids = {}

    coin_width = 0.05
    coin_height = 0.05

    section_width = 1.0 / curr_screen.num_coins
    for k in range(curr_screen.num_coins):
        x = random.uniform(section_width * k, section_width * (k + 1) - coin_width)
        y = random.uniform(0, 1 - coin_height)
        coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y},
                                    allow_stretch=True)
        curr_screen.ids['layout_lvl' + str(screen_num)].add_widget(coin, index=-1)
        curr_screen.coins_ids['coin' + str(k)] = coin

Listing 6-18Resetting the Coins Dictionary and Layout Before Playing a Level

这样做的话,在当前的试玩中就不会出现先前试玩中给定关卡的硬币了。我们还可以打印子布局和字典条目,如图 6-10 所示,以确保一切按预期运行。第二次启动相同的级别后,布局只包含 KV 文件中定义的三个小部件。还有,字典是空的。

img/481739_1_En_6_Fig10_HTML.jpg

图 6-10

打印布局子项和硬币字典,以确保一次试验中的硬币不会出现在下一次试验中

问题 NumCollectedCoins 标签小部件不以“硬币 0”文本开始

图 6-11 显示了角色收集到两枚硬币后被杀死的结果。标签显示"Coins 2",确认收集了两枚硬币。

img/481739_1_En_6_Fig11_HTML.jpg

图 6-11

角色在收集了两枚硬币后被杀死

当级别重复时,我们期望看到标签上的文本"Coins 0",但是显示的是"Coins 2",如图 6-12 所示。

img/481739_1_En_6_Fig12_HTML.jpg

图 6-12

标签中的文本尚未重置

解决办法

为了解决这个问题,根据清单 6-19 中修改后的screen_on_pre_enter()回调函数,在屏幕进入之前,标签上显示的文本必须重置为"Coins 0"

def screen_on_pre_enter(self, screen_num):
    curr_screen = self.root.screens[screen_num]
    curr_screen.character_killed = False
    curr_screen.ids['character_image_lvl' + str(screen_num)].im_num = 0
    curr_screen.ids['monster_image_lvl' + str(screen_num)].im_num = 10
    curr_screen.ids['num_coins_collected_lvl' + str(screen_num)].text = "Coins 0"

    for key, coin in curr_screen.coins_ids.items():
        curr_screen.ids['layout_lvl' + str(screen_num)].remove_widget(coin)
    curr_screen.coins_ids = {}

    coin_width = 0.05
    coin_height = 0.05

    section_width = 1.0 / curr_screen.num_coins
    for k in range(curr_screen.num_coins):
        x = random.uniform(section_width * k, section_width * (k + 1) - coin_width)
        y = random.uniform(0, 1 - coin_height)
        coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y},
                                    allow_stretch=True)
        curr_screen.ids['layout_lvl' + str(screen_num)].add_widget(coin, index=-1)
        curr_screen.coins_ids['coin' + str(k)] = coin

Listing 6-19Resetting the Label Text to “Coins 0” Before Entering a Screen

问题 6:在接下来的试验中,收集的硬币数量不会从 0 开始

重置显示在Label小部件上的文本并不能完全解决之前的问题。我们的目标是当关卡再次开始时,将收集的硬币数量重置为 0。当收集到一枚硬币时,我们希望数字增加 1,文本从“硬币 0”变为“硬币 1”,但这并没有发生。我们确实重置了关卡开始时显示的文本,但这并没有完全解决问题。

标签上显示的数字取自类中定义的num_coins_collected变量。从图 6-12 可知,变量值为 2。因为它的值没有被重置,所以当级别再次开始时,它将保持相同的值。当下一次尝试收集到单个硬币时,该数字将增加 1 为 3,如图 6-13 所示。因此,我们需要在接下来的试验中将变量重置为从 0 开始。

img/481739_1_En_6_Fig13_HTML.jpg

图 6-13

收集的硬币数量从上次游戏停止的地方开始计算

解决办法

在清单 6-20 的screen_on_pre_enter()函数中,num_coins_collected类变量的值被修改为 0。

def screen_on_pre_enter(self, screen_num):
    curr_screen = self.root.screens[screen_num]
    curr_screen.character_killed = False
    curr_screen.num_coins_collected = 0
    curr_screen.ids['character_image_lvl' + str(screen_num)].im_num = 0
    curr_screen.ids['monster_image_lvl' + str(screen_num)].im_num = 10
    curr_screen.ids['num_coins_collected_lvl' + str(screen_num)].text = "Coins 0"

    for key, coin in curr_screen.coins_ids.items():
        curr_screen.ids['layout_lvl' + str(screen_num)].remove_widget(coin)
    curr_screen.coins_ids = {}

    coin_width = 0.05
    coin_height = 0.05

    curr_screen = self.root.screens[screen_num]

    section_width = 1.0 / curr_screen.num_coins
    for k in range(curr_screen.num_coins):
        x = random.uniform(section_width * k, section_width * (k + 1) - coin_width)
        y = random.uniform(0, 1 - coin_height)

        coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y},
                                    allow_stretch=True)
        curr_screen.ids['layout_lvl' + str(screen_num)].add_widget(coin, index=-1)
        curr_screen.coins_ids['coin' + str(k)] = coin

Listing 6-20Resetting the num_coins_collected to 0 Before Starting a Screen

完整的游戏实现

在解决了应用中的所有问题后,游戏的完整代码将在此部分列出。清单 6-21 显示了 KV 文件。Python 文件的完整代码如清单 6-22 所示。

KV 文件

ScreenManager:
    MainScreen:
    Level1:
    Level2:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"

<Level1>:
    name: "level1"
    on_pre_enter: app.screen_on_pre_enter(1)
    on_pre_leave: app.screen_on_pre_leave(1)
    on_enter: app.screen_on_enter(1)
    FloatLayout:
        id: layout_lvl1
        on_touch_down: app.touch_down_handler(1, args)
        canvas.before:
            Rectangle:
                size: self.size

                pos: self.pos
                source: "bg_lvl1.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl1
        Monster:
            id: monster_image_lvl1
        Character:
            id: character_image_lvl1

<Level2>:
    name: "level2"
    on_pre_enter: app.screen_on_pre_enter(2)
    on_pre_leave: app.screen_on_pre_leave(2)
    on_enter: app.screen_on_enter(2)
    FloatLayout:
        id: layout_lvl2
        on_touch_down: app.touch_down_handler(2, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl2.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl2
        Monster:
            id: monster_image_lvl2
        Character:
            id: character_image_lvl2

<NumCollectedCoins@Label>:
    size_hint: (0.1, 0.02)
    pos_hint: {'x': 0.0, 'y': 0.97}
    text: "Coins 0"
    font_size: 20

<Monster@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.8, 'y': 0.8}
    source: "10.png"
    im_num: 10
    allow_stretch: True
    on_im_num: app.change_monst_im(self)
    on_pos_hint: app.monst_pos_hint(self)

<Character@Image>:
    size_hint: (0.15, 0.15)

    pos_hint: {'x': 0.2, 'y': 0.6}
    source: "0.png"
    im_num: 0
    allow_stretch: True
    on_im_num: app.change_char_im(self)
    on_pos_hint: app.char_pos_hint(self)

Listing 6-21Complete KV File of the Game

Python 文件

清单 6-22 中的 Python 文件包含了播放音效的代码,就像我们在之前的单级游戏中所做的一样。

import kivy.app
import kivy.uix.screenmanager
import random
import kivy.core.audio
import os
import functools

class TestApp(kivy.app.App):

    def screen_on_pre_leave(self, screen_num):
        curr_screen = self.root.screens[screen_num]

        curr_screen.ids['monster_image_lvl'+str(screen_num)].pos_hint = {'x': 0.8, 'y': 0.8}
        curr_screen.ids['character_image_lvl'+str(screen_num)].pos_hint = {'x': 0.2, 'y': 0.6}

    def screen_on_pre_enter(self, screen_num):
        curr_screen = self.root.screens[screen_num]
        curr_screen.character_killed = False
        curr_screen.num_coins_collected = 0
        curr_screen.ids['character_image_lvl'+str(screen_num)].im_num = 0
        curr_screen.ids['monster_image_lvl'+str(screen_num)].im_num = 10
        curr_screen.ids['num_coins_collected_lvl'+str(screen_num)].text = "Coins 0"

        for key, coin in curr_screen.coins_ids.items():
            curr_screen.ids['layout_lvl'+str(screen_num)].remove_widget(coin)
        curr_screen.coins_ids = {}

        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)
            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            curr_screen.coins_ids['coin'+str(k)] = coin

    def screen_on_enter(self, screen_num):
        music_dir = os.getcwd()+"/music/"
        self.bg_music = kivy.core.audio.SoundLoader.load(music_dir+"bg_music_piano.wav")
        self.bg_music.loop = True

        self.coin_sound = kivy.core.audio.SoundLoader.load(music_dir+"coin.wav")
        self.level_completed_sound = kivy.core.audio.SoundLoader.load(music_dir+"level_completed_flaute.wav")
        self.char_death_sound = kivy.core.audio.SoundLoader.load(music_dir+"char_death_flaute.wav")

        self.bg_music.play()

        curr_screen = self.root.screens[screen_num]
        monster_image = curr_screen.ids['monster_image_lvl'+str(screen_num)]
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def start_monst_animation(self, monster_image, new_pos, anim_duration):
        monst_anim = kivy.animation.Animation(pos_hint={'x': new_pos[0], 'y': new_pos[1]}, im_num=17,duration=anim_duration)

        monst_anim.bind(on_complete=self.monst_animation_completed)
        monst_anim.start(monster_image)

    def monst_animation_completed(self, *args):
        monster_image = args[1]
        monster_image.im_num = 10

        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self, monster_image):
        screen_num = int(monster_image.parent.parent.name[5:])
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]

        character_center = character_image.center
        monster_center = monster_image.center

        gab_x = character_image.width / 2
        gab_y = character_image.height / 2
        if character_image.collide_widget(monster_image) and abs(character_center[0] - monster_center[0]) <= gab_x and abs(character_center[1] - monster_center[1]) <= gab_y and curr_screen.character_killed == False:
            self.bg_music.stop()
            self.char_death_sound.play()
            curr_screen.character_killed = True

            kivy.animation.Animation.cancel_all(character_image)
            kivy.animation.Animation.cancel_all(monster_image)

            character_image.im_num = 91
            char_anim = kivy.animation.Animation(im_num=95, duration=1.0)
            char_anim.start(character_image)
            kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)

    def change_monst_im(self, monster_image):
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def touch_down_handler(self, screen_num, args):
        curr_screen = self.root.screens[screen_num]

        if curr_screen.character_killed == False:
            self.start_char_animation(screen_num, args[1].spos)

    def start_char_animation(self, screen_num, touch_pos):
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]
        character_image.im_num = 0
        char_anim = kivy.animation.Animation(pos_hint={'x': touch_pos[0] - character_image.size_hint[0] / 2,'y': touch_pos[1] - character_image.size_hint[1] / 2}, im_num=7)
        char_anim.bind(on_complete=self.char_animation_completed)
        char_anim.start(character_image)

    def char_animation_completed(self, *args):
        character_image = args[1]
        character_image.im_num = 0

    def char_pos_hint(self, character_image):
        screen_num = int(character_image.parent.parent.name[5:])
        character_center = character_image.center

        gab_x = character_image.width / 3
        gab_y = character_image.height / 3
        coins_to_delete = []
        curr_screen = self.root.screens[screen_num]

        for coin_key, curr_coin in curr_screen.coins_ids.items():
            curr_coin_center = curr_coin.center
            if character_image.collide_widget(curr_coin) and abs(character_center[0] - curr_coin_center[0]) <= gab_x and abs(character_center[1] - curr_coin_center[1]) <= gab_y:
                self.coin_sound.play()
                coins_to_delete.append(coin_key)
                curr_screen.ids['layout_lvl'+str(screen_num)].remove_widget(curr_coin)
                curr_screen.num_coins_collected = curr_screen.num_coins_collected + 1
                curr_screen.ids['num_coins_collected_lvl'+str(screen_num)].text = "Coins "+str(curr_screen.num_coins_collected)
                if curr_screen.num_coins_collected == curr_screen.num_coins:
                    self.bg_music.stop()
                    self.level_completed_sound.play()

                    kivy.animation.Animation.cancel_all(character_image)
                    kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)
                    kivy.animation.Animation.cancel_all(curr_screen.ids['monster_image_lvl'+str(screen_num)])

        if len(coins_to_delete) > 0:
            for coin_key in coins_to_delete:
                del curr_screen.coins_ids[coin_key]

    def change_char_im(self, character_image):
        character_image.source = str(int(character_image.im_num)) + ".png"

    def back_to_main_screen(self, screenManager, *args):
        screenManager.current = "main"

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False

    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}

app = TestApp()
app.run()

Listing 6-22Complete Python File of the Game

添加更多关卡别

现在我们已经解决了所有的问题,我们已经成功地创建了一个两级游戏。我们可以通过以下步骤轻松地为游戏添加更多关卡:

  1. 为 Python 文件中的级别创建一个类,该类扩展了Screen类并初始化了前面的四个类变量。

  2. 创建一个自定义小部件,定义 KV 文件中的类的布局。记得更改添加到类中的三个小部件的 id(MonsterCharacterNumCoinsCollected),使屏幕索引位于末尾。

  3. 将自定义小部件的实例作为ScreenManager的子部件添加到应用小部件树中。

  4. 不要忘记在主屏幕内添加一个按钮,以便进入新的级别。

通过遵循这四个步骤,我们可以根据需要添加更多的级别。

添加更多的怪物

每个级别只能有一个怪物,这使得游戏很无聊,因为没有新的挑战。为了在代码改动最少的情况下增加更多的怪物,我们应该怎么做?让我想想。

如果我们想添加更多的怪物,每一个必须被唯一识别。这是因为每个怪物的位置必须与角色位置进行比较,以防发生碰撞。区分不同怪物的方法是在 KV 文件中给每个怪物一个不同的 ID。通过参考每个怪物的 ID,我们可以唯一地识别每个怪物。

monster ID 的一般形式如下:

monster<monst_index>_image_lvl<lvl_index>

其中monst_index指的是怪物索引lvl_index指的是它们各自开始的等级索引。例如,如果每个指数从 1 开始,第三级中第二个怪物的 ID 将是monster2_image_lvl3。三级的布局有两个怪物,如清单 6-23 所示。

<Level3>:
    name: "level3"
    on_pre_enter: app.screen_on_pre_enter(3)
    on_pre_leave: app.screen_on_pre_leave(3)
    on_enter: app.screen_on_enter(3)
    FloatLayout:
        id: layout_lvl3
        on_touch_down: app.touch_down_handler(3, args)
        canvas.before:
            Rectangle:
                pos: self.pos
                size: self.size
                source: "bg_lvl3.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl3
        Monster:
            id: monster1_image_lvl3
        Monster:
            id: monster2_image_lvl3
        Character:
            id: character_image_lvl3

Listing 6-23Defining the Layout for Level 3 in Which Two Monsters Exist

请注意,我们在以下每个回调函数的 Python 代码中使用了 monster ID 来指代 monster:

  • screen_on_pre_leave():重置怪物位置

  • screen_on_pre_enter():重置im_num属性

  • screen_on_enter():开始动画

  • monst_pos_hint():角色被杀死后,取消其动画

  • char_pos_hint():收集所有硬币完成关卡后,取消动画

当前指定 ID 的形式并不反映 monster 索引。因此,我们必须改变它。

例如在char_pos_hint()里面,怪物动画是按照这一行取消的:

kivy.animation.Animation.cancel_all(curr_screen.ids['monster_image_lvl' + str(screen_num)])

当有一个以上的怪物时,必须对所有的怪物重复上一行。我们可以使用一个for循环轻松地实现这一点,该循环遍历所有怪物,根据怪物索引和级别(屏幕)索引准备它们的 ID,然后使用这个 ID 作为ids字典的键。

为了创建一个for循环,我们需要定义关卡中怪物的数量。这就是我们添加一个新的类变量命名为num_monsters的原因。添加该变量后的 3 级类(Level3)的定义如清单 6-24 所示。

class Level3(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 12
    num_coins_collected = 0
    coins_ids = {}
    num_monsters = 2

Listing 6-24Adding the num_monsters Variable Inside the Class Header of Level 3

清单 6-25 显示了添加在char_pos_hint()中的for循环,以访问每个怪物并取消其动画。怪物索引由(i + 1)返回,因为循环变量i从 0 开始,而第一个怪物索引是 1。

for i in range(curr_screen.num_monsters): kivy.animation.Animation.cancel_all(curr_screen.ids['monster' + str(i + 1) + '_image_lvl' + str(screen_num)])

Listing 6-25Accessing the Monsters and Cancelling Their Animations

同样,我们可以编辑剩余的四个回调函数来访问每个怪物,然后对其应用所需的操作。

每关有多个怪物的游戏

清单 6-26 中列出了游戏的修改后的 Python 代码,该代码支持每一关使用多个怪物。请注意,num_monsters类变量必须添加到所有级别的类中。在前两个级别中,它被设置为 1。对于第三类,是 2。如果这个变量设置为 0 呢?

import kivy.app
import kivy.uix.screenmanager
import random
import kivy.core.audio
import os
import functools

class TestApp(kivy.app.App):

    def screen_on_pre_leave(self, screen_num):
        curr_screen = self.root.screens[screen_num]

        for i in range(curr_screen.num_monsters):
            curr_screen.ids['monster'+str(i+1)+'_image_lvl'+str(screen_num)].pos_hint = {'x': 0.8, 'y': 0.8}
        curr_screen.ids['character_image_lvl'+str(screen_num)].pos_hint = {'x': 0.2, 'y': 0.6}

    def screen_on_pre_enter(self, screen_num):
        curr_screen = self.root.screens[screen_num]
        curr_screen.character_killed = False
        curr_screen.num_coins_collected = 0
        curr_screen.ids['character_image_lvl'+str(screen_num)].im_num = 0
        for i in range(curr_screen.num_monsters):
            curr_screen.ids['monster'+str(i+1)+'_image_lvl'+str(screen_num)].im_num = 10
        curr_screen.ids['num_coins_collected_lvl'+str(screen_num)].text = "Coins 0"

        for key, coin in curr_screen.coins_ids.items():
            curr_screen.ids['layout_lvl'+str(screen_num)].remove_widget(coin)
        curr_screen.coins_ids = {}

        coin_width = 0.05
        coin_height = 0.05

        curr_screen = self.root.screens[screen_num]

        section_width = 1.0/curr_screen.num_coins
        for k in range(curr_screen.num_coins):
            x = random.uniform(section_width*k, section_width*(k+1)-coin_width)
            y = random.uniform(0, 1-coin_height)
            coin = kivy.uix.image.Image(source="coin.png", size_hint=(coin_width, coin_height), pos_hint={'x': x, 'y': y}, allow_stretch=True)
            curr_screen.ids['layout_lvl'+str(screen_num)].add_widget(coin, index=-1)
            curr_screen.coins_ids['coin'+str(k)] = coin

    def screen_on_enter(self, screen_num):
        music_dir = os.getcwd()+"/music/"
        self.bg_music = kivy.core.audio.SoundLoader.load(music_dir+"bg_music_piano.wav")
        self.bg_music.loop = True

        self.coin_sound = kivy.core.audio.SoundLoader.load(music_dir+"coin.wav")
        self.level_completed_sound = kivy.core.audio.SoundLoader.load(music_dir+"level_completed_flaute.wav")
        self.char_death_sound = kivy.core.audio.SoundLoader.load(music_dir+"char_death_flaute.wav")

        self.bg_music.play()

        curr_screen = self.root.screens[screen_num]
        for i in range(curr_screen.num_monsters):
            monster_image = curr_screen.ids['monster'+str(i+1)+'_image_lvl'+str(screen_num)]
            new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]/4), random.uniform(0.0, 1 - monster_image.size_hint[1]/4))
            self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.0, 3.0))
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def start_monst_animation(self, monster_image, new_pos, anim_duration):
        monst_anim = kivy.animation.Animation(pos_hint={'x': new_pos[0], 'y': new_pos[1]}, im_num=17,duration=anim_duration)

        monst_anim.bind(on_complete=self.monst_animation_completed)
        monst_anim.start(monster_image)

    def monst_animation_completed(self, *args):
        monster_image = args[1]
        monster_image.im_num = 10

        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0]), random.uniform(0.0, 1 - monster_image.size_hint[1]))
        self.start_monst_animation(monster_image=monster_image, new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self, monster_image):
        screen_num = int(monster_image.parent.parent.name[5:])
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]

        character_center = character_image.center
        monster_center = monster_image.center

        gab_x = character_image.width / 2
        gab_y = character_image.height / 2
        if character_image.collide_widget(monster_image) and abs(character_center[0] - monster_center[0]) <= gab_x and abs(character_center[1] - monster_center[1]) <= gab_y and curr_screen.character_killed == False:
            self.bg_music.stop()
            self.char_death_sound.play()
            curr_screen.character_killed = True

            kivy.animation.Animation.cancel_all(character_image)
            for i in range(curr_screen.num_monsters):
                kivy.animation.Animation.cancel_all(curr_screen.ids['monster'+str(i+1)+'_image_lvl'+str(screen_num)])

            character_image.im_num = 91
            char_anim = kivy.animation.Animation(im_num=95, duration=1.0)
            char_anim.start(character_image)
            kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)

    def change_monst_im(self, monster_image):
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def touch_down_handler(self, screen_num, args):
        curr_screen = self.root.screens[screen_num]
        if curr_screen.character_killed == False:
            self.start_char_animation(screen_num, args[1].spos)

    def start_char_animation(self, screen_num, touch_pos):
        curr_screen = self.root.screens[screen_num]
        character_image = curr_screen.ids['character_image_lvl'+str(screen_num)]
        character_image.im_num = 0
        char_anim = kivy.animation.Animation(pos_hint={'x': touch_pos[0] - character_image.size_hint[0] / 2,'y': touch_pos[1] - character_image.size_hint[1] / 2}, im_num=7)
        char_anim.bind(on_complete=self.char_animation_completed)
        char_anim.start(character_image)

    def char_animation_completed(self, *args):
        character_image = args[1]
        character_image.im_num = 0

    def char_pos_hint(self, character_image):
        screen_num = int(character_image.parent.parent.name[5:])
        character_center = character_image.center

        gab_x = character_image.width / 3
        gab_y = character_image.height / 3
        coins_to_delete = []
        curr_screen = self.root.screens[screen_num]

        for coin_key, curr_coin in curr_screen.coins_ids.items():
            curr_coin_center = curr_coin.center
            if character_image.collide_widget(curr_coin) and abs(character_center[0] - curr_coin_center[0]) <= gab_x and abs(character_center[1] - curr_coin_center[1]) <= gab_y:
                self.coin_sound.play()
                coins_to_delete.append(coin_key)
                curr_screen.ids['layout_lvl'+str(screen_num)].remove_widget(curr_coin)
                curr_screen.num_coins_collected = curr_screen.num_coins_collected + 1
                curr_screen.ids['num_coins_collected_lvl'+str(screen_num)].text = "Coins "+str(curr_screen.num_coins_collected)
                if curr_screen.num_coins_collected == curr_screen.num_coins:
                    self.bg_music.stop()

                    self.level_completed_sound.play()
                    kivy.animation.Animation.cancel_all(character_image)
                    kivy.clock.Clock.schedule_once(functools.partial(self.back_to_main_screen, curr_screen.parent), 3)
                    for i in range(curr_screen.num_monsters):
                        kivy.animation.Animation.cancel_all(curr_screen.ids['monster' + str(i + 1) + '_image_lvl' + str(screen_num)])

        if len(coins_to_delete) > 0:
            for coin_key in coins_to_delete:
                del curr_screen.coins_ids[coin_key]

    def change_char_im(self, character_image):
        character_image.source = str(int(character_image.im_num)) + ".png"

    def back_to_main_screen(self, screenManager, *args):
        screenManager.current = "main"

class MainScreen(kivy.uix.screenmanager.Screen):
    pass

class Level1(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}
    num_monsters = 1

class Level2(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 8
    num_coins_collected = 0
    coins_ids = {}
    num_monsters = 1

class Level3(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 12
    num_coins_collected = 0
    coins_ids = {}
    num_monsters = 2

app = TestApp()
app.run()

Listing 6-26Python File for Supporting More Than One Monster Within the Game

如果num_monsters变量设置为 0,则不会执行for循环,因为角色不会被激活,因此其位置不会改变。on_pos_hint事件不会被激发。因为碰撞检测发生在事件的回调函数中,所以怪物和角色之间不会发生碰撞。怪物将是可见的,因为它被添加到 KV 文件中。清单 6-27 显示了创建三个级别后修改后的 KV 文件。

ScreenManager:
    MainScreen:
    Level1:
    Level2:
    Level3:

<MainScreen>:
    name: "main"
    BoxLayout:
        Button:
            text: "Go to Level 1"
            on_press: app.root.current="level1"
        Button:
            text: "Go to Level 2"
            on_press: app.root.current="level2"
        Button:
            text: "Go to Level 3"
            on_press: app.root.current = "level3"

<Level1>:
    name: "level1"
    on_pre_enter: app.screen_on_pre_enter(1)

    on_pre_leave: app.screen_on_pre_leave(1)
    on_enter: app.screen_on_enter(1)
    FloatLayout:
        id: layout_lvl1
        on_touch_down: app.touch_down_handler(1, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl1.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl1
        Monster:
            id: monster_image_lvl1
        Character:
            id: character_image_lvl1

<Level2>:
    name: "level2"
    on_pre_enter: app.screen_on_pre_enter(2)
    on_pre_leave: app.screen_on_pre_leave(2)
    on_enter: app.screen_on_enter(2)
    FloatLayout:
        id: layout_lvl2
        on_touch_down: app.touch_down_handler(2, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl2.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl2

        Monster:
            id: monster_image_lvl2
        Character:
            id: character_image_lvl2

<Level3>:
    name: "level3"
    on_pre_enter: app.screen_on_pre_enter(3)
    on_pre_leave: app.screen_on_pre_leave(3)
    on_enter: app.screen_on_enter(3)
    FloatLayout:
        id: layout_lvl3
        on_touch_down: app.touch_down_handler(3, args)
        canvas.before:
            Rectangle:
                size: self.size
                pos: self.pos
                source: "bg_lvl3.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl3
        Monster:
            id: monster1_image_lvl3
        Monster:
            id: monster2_image_lvl3
        Character:
            id: character_image_lvl3

<NumCollectedCoins@Label>:
    size_hint: (0.1, 0.02)
    pos_hint: {'x': 0.0, 'y': 0.97}
    text: "Coins 0"
    font_size: 20

<Monster@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.8, 'y': 0.8}
    source: "10.png"
    im_num: 10
    allow_stretch: True
    on_im_num: app.change_monst_im(self)
    on_pos_hint: app.monst_pos_hint(self)

<Character@Image>:
    size_hint: (0.15, 0.15)
    pos_hint: {'x': 0.2, 'y': 0.6}

    source: "0.png"
    im_num: 0
    allow_stretch: True
    on_im_num: app.change_char_im(self)
    on_pos_hint: app.char_pos_hint(self)

Listing 6-27Adding Three Levels Inside the KV File

关于小部件属性的提示

每个级别中的每个小部件都有许多属性,您应该决定在哪里定义它们。以下是一些提示:

  • 类中所有小部件共享的属性应该添加到 Python 文件的类头中。

  • 在特定小部件的所有实例之间共享但在其他类型的小部件之间不共享的属性应该添加到 KV 文件中的自定义小部件的定义中。

  • 从一个小部件的一个实例到同一个小部件的另一个实例发生变化的属性应该添加到 KV 文件中自定义小部件实例的定义中。

这些规则有助于避免重复代码的某些部分。例如,假设我们想要定义一个属性,该属性定义所有级别中所有怪物的动画持续时间。因为该属性在所有级别的所有怪物中都是静态的,所以我们可以在应用类中创建一个变量来完成这项工作。如果变量被添加到级别Screen类中,相同的变量将在每个类中重复,这不是必需的。

如果可以为不同等级的怪物分配不同的动画持续时间,那么这个变量不能在应用类中定义,最好将其添加到Screen类头中。

如果怪物持续时间动画要为每个怪物改变,即使是在同一级别,我们需要为每个Monster小部件的实例分配一个属性。

下一节将改变角色和怪物动画来应用这种理解。

更改动画持续时间

在之前的游戏中,角色和所有怪物的动画持续时间在所有关卡中都是固定的。持续时间可以改变,以使游戏更具挑战性。例如,如果怪物移动得更快,玩家必须快速做出决定。为了改变持续时间,我们必须问自己定义持续时间的属性应该在哪里指定。

关于角色,我们想改变它在每一关的持续时间。因为它在级别中只存在一次,所以应该在 Python 级别类中指定字符持续时间。清单 6-28 显示了添加名为char_anim_duration的属性后的Level3类头。

class Level3(kivy.uix.screenmanager.Screen):
    character_killed = False
    num_coins = 12
    num_coins_collected = 0
    coins_ids = {}
    char_anim_duration = 1.2
    num_monsters = 2

Listing 6-28Adding the char_anim_duration Property to the Class of Level 3

关于怪物,我们也想改变他们在每个级别的持续时间。但是因为每个级别有不止一个怪物,我们不能在类头中指定它们的持续时间,而是在 KV 小部件实例中指定。清单 6-29 指定了 KV 文件内三级两个怪物的持续时间。因为怪物动画是通过设置最小和最大可能值随机生成的,所以属性monst_anim_duration_lowmonst_anim_duration_high设置持续时间的最小和最大值。

<Level3>:
    name: "level3"
    on_pre_enter: app.screen_on_pre_enter(3)
    on_pre_leave: app.screen_on_pre_leave(3)
    on_enter: app.screen_on_enter(3)
    FloatLayout:
        id: layout_lvl3
        on_touch_down: app.touch_down_handler(3, args)
        canvas.before:
            Rectangle:
                pos: self.pos
                size: self.size
                source: "levels-bg/bg_lvl3.jpg"
        NumCollectedCoins:
            id: num_coins_collected_lvl3
        Monster:
            id: monster1_image_lvl3
            monst_anim_duration_low: 1.0
            monst_anim_duration_high: 1.6
        Monster:
            id: monster2_image_lvl3
            monst_anim_duration_low: 1.0
            monst_anim_duration_high: 2.0
        Character:
            id: character_image_lvl3

Listing 6-29Specifying the Monster Duration Within the KV File

在设置了两者的持续时间之后,我们需要在 Python 代码中引用它们。角色动画在start_char_animation()回调函数中被引用一次。清单 6-30 显示了修改后的函数,它引用了类中定义的char_anim_duration变量。

def start_char_animation(self, screen_num, touch_pos):
    curr_screen = self.root.screens[screen_num]
    character_image = curr_screen.ids['character_image_lvl' + str(screen_num)]
    character_image.im_num = character_image.start_im_num
    char_anim = kivy.animation.Animation(pos_hint={'x': touch_pos[0] - character_image.size_hint[0] / 2, 'y': touch_pos[1] - character_image.size_hint[1] / 2},im_num=character_image.end_im_num, duration=curr_screen.char_anim_duration)
    char_anim.bind(on_complete=self.char_animation_completed)
    char_anim.start(character_image)

Listing 6-30Referencing the char_anim_duration Variable to Return the Duration of the Monster Character

关于怪物动画,引用两次。第一个引用在screen_on_enter()回调函数中,以便在屏幕启动时立即启动动画。在被修改为使用Monster小部件中定义的monst_anim_duration_lowmonst_anim_duration_high后,它被列在清单 6-31 中。

def screen_on_enter(self, screen_num):
    music_dir = os.getcwd() + "/music/"
    self.bg_music = kivy.core.audio.SoundLoader.load(music_dir + "bg_music_piano.wav")
    self.bg_music.loop = True

    self.coin_sound = kivy.core.audio.SoundLoader.load(music_dir + "coin.wav")
    self.level_completed_sound = kivy.core.audio.SoundLoader.load(music_dir + "level_completed_flaute.wav")
    self.char_death_sound = kivy.core.audio.SoundLoader.load(music_dir + "char_death_flaute.wav")

    self.bg_music.play()

    curr_screen = self.root.screens[screen_num]
    for i in range(curr_screen.num_monsters):
        monster_image = curr_screen.ids['monster' + str(i + 1) + '_image_lvl' + str(screen_num)]
        new_pos = (random.uniform(0.0, 1 - monster_image.size_hint[0] / 4),
                   random.uniform(0.0, 1 - monster_image.size_hint[1] / 4))
        self.start_monst_animation(monster_image=monster_image, new_pos=new_pos, anim_duration=random.uniform(monster_image.monst_anim_duration_low, monster_image.monst_anim_duration_high))

    for i in range(curr_screen.num_fires):
        fire_widget = curr_screen.ids['fire' + str(i + 1) + '_lvl' + str(screen_num)]
        self.start_fire_animation(fire_widget=fire_widget, pos=(0.0, 0.5), anim_duration=5.0)

Listing 6-31Setting the Monster Animation Duration Inside the screen_on_enter() Callback Function

怪物动画第二次被引用是在名为monst_animation_completed()的回调函数内部,该函数在清单 6-32 中列出。它会在动画完成后重复播放。

def monst_animation_completed(self, *args):
    monster_image = args[1]
    monster_image.im_num = monster_image.start_im_num

    new_pos = (
    random.uniform(0.0, 1 - monster_image.size_hint[0] / 4), random.uniform(0.0, 1 - monster_image.size_hint[1] / 4))
    self.start_monst_animation(monster_image=monster_image, new_pos=new_pos,
                               anim_duration=random.uniform(monster_image.monst_anim_duration_low, monster_image.monst_anim_duration_high))

Listing 6-32Setting the Monster Animation Duration Inside the monst_animation_completed() Callback Function

更多事情要做

请注意,您可以对游戏做更多的事情来使它变得更有趣。这是由你的想象力提出新的想法,使游戏更有趣。以之前游戏的这些变化为例:

  • NumCollectedCoins标签除了显示每关的硬币总数外,还可以显示收集到的硬币数量。这有助于玩家知道还有多少硬币需要收集。

  • 可以添加新标签来显示级别编号。

  • 除了怪物之外,还可以投掷火焰来杀死角色。

  • 我们可以等待两到三次碰撞,而不是在第一次碰撞中杀死角色。

  • 角色也可以用火杀死怪物。

  • 与其创建使用相同图像的新怪物,不如添加一个新的Monster小部件,使用不同的图像。可以添加新的属性start_im_numend_im_num来指定属性im_num的第一个和最后一个值。属性被添加到自定义小部件中,而不是它们的实例中,因为它们在所有怪物和角色中都是相同的。这使得创造新的怪物更加容易。

  • 限制角色每次触摸可以移动的空间。目前没有限制,但是我们可以限制角色每次触摸移动屏幕宽度和高度的四分之一。

  • 将怪物动画的随机范围pos_hint更改为靠近角色的当前位置。

  • 要求玩家在限定时间内收集一定数量的硬币。

  • 添加奖励等级。

  • 当玩家在一次触摸中收集到五个硬币时,为玩家创建一个奖金。

  • 仅当通过将完成的最高级别的编号保存在文件中来完成级别 i-1 时,才打开编号为 I 的级别。这个数字在每个级别完成后更新,并在应用启动时恢复。

  • 根据碰撞次数评定完成的等级。例如,无碰撞两次启动,10 次碰撞两次启动,10 次以上碰撞三次启动。

根据附录中显示的 KV 和 Python 文件,其中一些更改将应用到游戏中。

以前,一个按钮指的是主屏幕内的每个级别。这个按钮被一个名为ImageButton的新定制小部件取代,它是ButtonImage小部件的混合体。这些新的小部件具有 source 属性,可以添加图像,还可以处理on_presson_release事件。

参考级别的ImageButton小部件被添加到GridLayout中。这将小部件排列在一个网格中,而不是一个盒子中,这有助于向屏幕添加更多的小部件。在GridLayout中添加背景图像,如图 6-14 所示。

在主屏幕的顶部添加了一个新的按钮,它将应用导航到一个新的屏幕,给出的类名为AboutUs,打印关于开发人员的详细信息。记住将这个新屏幕作为子屏幕添加到ScreenManager中,以免影响关卡的索引。

img/481739_1_En_6_Fig14_HTML.jpg

图 6-14

向主屏幕添加背景图像

一个名为Fire的新部件扩展了Label部件,用来表示杀死玩家的投掷火焰。使用canvas.before,使用矩形顶点指令添加一个火焰图像背景。在小部件的实例中,添加了两个名为fire_start_pos_hintfire_end_pos_hint的属性。它们指明了火势蔓延的路径。因为在同一个关卡中可能有不止一个Fire小部件,所以它们被赋予了类似于怪物被赋予 id 的 id。例如,第三层中的第一个和第二个Fire小部件分别具有fire1_lvl3fire2_lvl3id。

类似于num_monsters类变量,有一个名为num_fires的变量保存级别中Fire小部件的数量。当一个操作被应用到Fires上时,一个循环遍历所有的操作。

处理新的Fire小部件的位置和碰撞的方式与Monster小部件非常相似。on_pos_hint事件被附加到Fire小部件,该小部件使用回调函数fire_pos_hint()来处理。在这个函数中,火的位置与怪物的位置进行比较。当碰撞发生时,most_pos_hint()函数中的相同内容会重复。

20 级的屏幕如图 6-15 所示,有八个Fire小部件向不同方向移动。

img/481739_1_En_6_Fig15_HTML.jpg

图 6-15

20 级的屏幕,有八个火的部件

根据图 6-15 ,在NumCollectedCoins旁边增加一个标签,显示当前级别号。这个小部件在 KV 文件中被命名为LevelNumber

在新游戏中,角色不再在第一次碰撞中被杀死。在每个级别的num_collision_level类变量中指定了最大碰撞次数。另一个名为num_collisions_hit的类变量从 0 开始,并在每次发生冲突时递增。当num_collision_levelnum_collisions_hit中的数值相等时,角色就会死亡。

关卡编号旁边还有一个红色的横条,代表角色剩余的碰撞次数。这个条是使用名为RemainingLifePercent的自定义小部件中的矩形和颜色顶点指令创建的,它扩展了Label小部件。对于每次碰撞,负责碰撞检测的回调函数(即monst_pos_hint()fire_pos_hint())中的小部件的大小都会减小。

一个名为Monster2的新怪物小部件被创建,它的行为几乎与Monster小部件相同。不同之处在于它为im_num属性使用了新的值。因此,新属性start_im_numend_im_num接受图像编号的第一个和最后一个数字。在 Python 代码中,这些属性中的值用于动画中。根据图 6-16 显示的屏幕,第 16 关使用了Monster2的两个实例。

img/481739_1_En_6_Fig16_HTML.jpg

图 6-16

16 级中使用的两种不同的怪物

这两个属性也在Character小部件中使用,使它们都以同样的方式运行。角色还有dead_start_im_numdead_end_im_num属性,指的是角色被杀死时显示的图像的编号。

提供这些文件(imagesaudio)的正确路径很重要。新游戏将图像组织到以下三个文件夹中:

img/481739_1_En_6_Fig17_HTML.jpg

图 6-17

关于我们屏幕

  • levels-bg:保存图像名称为bg_lvl<num>.jpg after replacing <num>的各层背景图像,图像编号为层号。请记住,级别编号从 1 开始。

  • levels-images:保存主屏幕上显示的ImageButton控件使用的背景图像,图像名称为<num>.png,将<num>替换为等级号。

  • other-images:保存ImageButton小工具上显示的图像,参照AboutUs屏幕,背景图像名为About-Us.png。在那个屏幕里面,有另一个ImageButton,指的是带有名为Main-Screen.png的背景图像的主屏幕。“关于我们”屏幕如图 6-17 所示。

所有音频文件都在music文件夹中,文件名如下:

  • bg_music_piano_flute.wav:主屏幕的背景音乐。

  • bg_music_piano.wav:每一关的背景音乐。

  • char_death_flaute.wav:在角色被杀死后播放。

  • 收集硬币时玩。

  • level_completed_flaute.wav:完成一关后玩。

您可以使用 https://onlinesequencer.net 免费创建背景序列。

只有当第一关完成后,游戏才会开启第一关。这个想法是启用主屏幕中所有已完成级别的所有ImageButton小部件。这是对启用新级别的ImageButton的补充。这就需要知道游戏开始时玩家完成的最后一关。为了做到这一点,游戏使用pickle库将玩家的进度保存在一个文件中。为此,创建了两个新方法— read_game_info()activate_levels()

read_game_info()方法读取一个名为game_info的文件,该文件存储了一个包含两个条目的字典。第一个项目有一个钥匙lastlvl,代表玩家完成的最后一关。第二项有一个键congrats_displayed_once。该物品有助于玩家在完成所有关卡后显示一次祝贺信息。默认情况下,该项的值为False,表示消息尚未显示。当完成所有关卡并显示祝贺信息时,它变为True

on_start()方法内部,调用read_game_info()方法,并使用pickle.load()函数返回存储在字典中的内容(即字典)。字典值被返回,然后作为参数传递给第二个方法activate_levels()。这个方法遍历所有的ImageButton窗口小部件,并根据最后一关完成的次数激活它们。

注意,game_info文件在使用pickle.dump()函数完成每一关后,在char_pos_hint()回调函数中被更新。

在 Google Play 发布游戏

在测试游戏并确保它如预期那样运行之后,我们可以在 Google Play 上发布它,供用户下载和玩。用于发布 CamShare 应用的相同步骤将在本游戏中重复。

在完成 APK 签名过程之后,使用下面的命令创建一个发布 APK。记得将项目的最小 API 设置为至少 26。

ahmedgad@ubuntu:~/Desktop$ buildozer android release

如果游戏的标题为CoinTex,将会在 Google Play 上提供,如图 6-18 。

img/481739_1_En_6_Fig18_HTML.jpg

图 6-18

这款游戏的安卓版可以在 Google Play 上找到

完整游戏

附录中提供了 CoinTex 的完整源代码。

摘要

总之,这一章通过增加更多的关卡继续发展我们在第五章开始的游戏。游戏界面是用屏幕组织的。增加了更多的怪物。在开发游戏时,我们报道了出现的问题并讨论了它们的解决方案。我们在 Google Play 上以 CoinTex 的名字发布了这款适用于 Android 设备的游戏,供任何用户下载和安装。通过完成这个游戏,你将对 Kivy 的许多特性有一个坚实的理解。

Python 是开发人员构建 Android 应用的好方法,但它不能构建像使用 Java Android Studio 开发的丰富应用,Java Android Studio 是构建 Android 应用的官方 IDE。下一章将介绍如何使用 Android Studio 来丰富用 Kivy 创建的 Python 应用。如果 Python 不支持某个特性,不要担心,因为它可以在 Android Studio 中添加。