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

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

协议:CC BY-NC-SA 4.0

四、创建和管理多个屏幕

在前一章中,我们使用相机小部件访问了 Android 相机。引入了 Kivy 画布来调整摄像机的旋转。为了限制给定 canvas 指令对某些小部件的影响,我们讨论了PushMatrixPopMatrix指令。之后,我们创建了一个 Android Kivy 应用来连续捕捉图像并将它们发送到 Flask 服务器,后者将它们显示在一个 HTML 页面中。

在这一章中,我们通过将按钮分离到不同的屏幕来创建一个更方便的设计。Kivy 支持用于构建屏幕的Screen类和用于管理此类屏幕的ScreenManager类。我们可以从一个屏幕导航到另一个屏幕。本章从讨论如何创建定制小部件开始,这将帮助我们理解如何创建一个具有多个屏幕的应用。

修改现有小部件

Kivy 支持许多现有的小部件,比如ButtonLabelTextInput等等。它支持修改现有的小部件来覆盖它们的默认行为。我们可以使用Label小部件作为测试用例。

Label类包含一些默认值作为它的属性。例如,默认情况下,text 属性设置为空字符串,文本颜色为白色,默认字体大小等于 15 SP(与缩放比例无关的像素)。我们将根据清单 4-1 中显示的 KV 代码覆盖这三个属性。标签的文字设置为"Hello",文字颜色为红色,字体大小为 50 SP。

Label:
    text: "Hello"
    color: 1,0,0,1
    font_size: "50sp"

Listing 4-1Overriding Properties of a Widget Inside the KV File

清单 4-2 中显示的 Python 代码创建了一个名为TestApp的新类,该类扩展了用于构建新应用的kivy.app.App类。它假设您将之前的 KV 代码保存在一个名为test.kv的文件中。

import kivy.app

class TestApp(kivy.app.App):
    pass

app = TestApp()
app.run()

Listing 4-2The Generic Code for Building a Kivy Application

当您运行应用时,您将在图 4-1 中看到结果。属性已正确更改。您可能想知道这些属性是否会因新标签而改变。我们可以通过创建一个新的标签小部件来回答这个问题。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-1

一个只有标签部件的 Kivy 应用

清单 4-3 中的新 KV 代码创建了一个保存两个标签的BoxLayout。第一个标签的属性是根据前面的示例设置的,而第二个标签只是将其文本更改为“第二个标签”。

BoxLayout:
    Label:
        text: "Hello"
        color: 1,0,0,1
        font_size: "50sp"
    Label:
        text: "Second Label"

Listing 4-3Adding Two Label Widgets to the Application Inside the BoxLayout Root Widget

运行应用后,第二个标签没有根据图 4-2 中的窗口改变颜色和字体大小。原因是这两个标签都是Label类的独立实例。当创建一个新实例时,它从Label类继承属性的默认值。如果给定实例的某些属性发生了更改,这并不意味着其他实例的属性也会发生更改。为了使两个标签具有相同的文本颜色,我们可以改变Label类的color属性。因此,它的所有实例都将继承这种颜色。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-2

仅覆盖一个标签小部件的属性,而将另一个设置为默认值

为了编辑 KV 文件中的类,类名被插入到<>之间,没有任何缩进。清单 4-4 中的 KV 文件覆盖了Label类的文本颜色和字体大小属性。通过创建Label类的两个实例,两者都将根据图 4-3 继承文本颜色和字体大小。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-3

更改所有标签小部件的属性

BoxLayout:
    Label:
        text: "Hello"
    Label:
        text: "Second Label"

<Label>:
    color: 1,0,0,1
    font_size: "50sp"

Listing 4-4Editing a Class in the KV Language

创建自定义小部件

清单 4-4 中的代码修改了Label类,使得它的所有实例都具有指定的文本颜色和字体大小。先前的属性会丢失。有时,我们可能会对此类属性以前的默认值感兴趣。

为了保持Label类以前的属性,我们可以创建一个新的自定义类来扩展Label类。这个自定义类继承了父类Label的默认属性,我们也可以修改它的一些属性。

清单 4-5 中的 KV 代码创建了一个名为CustomLabel的新定制类,它继承了Label类。因此,如果您需要创建一个带有默认属性的标签小部件,您可以实例化Label类。要使用修改后的属性,实例化CustomLabel类。在这个例子中,第一个标签是CustomLabel类的一个实例,其中文本颜色和字体大小被改变。第二个标签是Label类的一个实例,具有这两个属性的默认值。

BoxLayout:
    CustomLabel:
        text: "Hello"
    Label:
        text: "Second Label"

<CustomLabel@Label>:
    color: 1,0,0,1
    font_size: "50sp"

Listing 4-5Creating a New Custom Label Widget by Extending the Label Class Inside the KV File

使用该 KV 文件运行应用后的结果如图 4-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-4

使用自定义标签小部件

在 Python 中定义自定义类

在清单 4-5 中,在 KV 文件中创建了一个名为CustomLabel的新定制类,它继承了Label类并修改了它的一些属性。在 KV 文件中进行继承限制了新类的能力,因为我们不能在其中编写函数。

我们可以创建新的类,并在 Python 代码中进行继承。然后,我们将在 KV 文件中引用这个类来修改它的属性。这有助于在新的自定义类中编写 Python 函数。清单 4-6 中的例子创建了一个名为CustomLabel的新的空类,它扩展了Label类。

import kivy.app
import kivy.uix.label

class CustomLabel(kivy.uix.label.Label):
    pass

class TestApp(kivy.app.App):
    pass

app = TestApp()
app.run()

Listing 4-6Inheriting the Label Class Within the Python File

test.kv文件的内容如清单 4-7 所示。注意,我们只是引用了 KV 中现有的类,而不是像上一节那样创建它。

BoxLayout:
    CustomLabel:
        text: "Hello"
    Label:
        text: "Second Label"

<CustomLabel>:
    color: 1,0,0,1
    font_size: "50sp"

Listing 4-7Referring to the Custom Class Created in the Python File Inside the KV File

我们可以通过在名为MyLayout的 Python 文件中创建一个扩展了BoxLayout类的类来稍微改变一下前面的应用,如清单 4-8 所示。因为这个类继承了BoxLayout类,所以我们可以在任何使用BoxLayout的地方使用它。例如,我们可以用新的类替换 KV 文件中的BoxLayout

import kivy.app
import kivy.uix.label
import kivy.uix.boxlayout

class CustomLabel(kivy.uix.label.Label):
    pass

class MyLayout(kivy.uix.boxlayout.BoxLayout):
    pass

class TestApp(kivy.app.App):
    def build(self):
        return MyLayout()

app = TestApp()
app.run()

Listing 4-8Creating a New Custom Layout by Extending the BoxLayout Class

清单 4-9 中给出了新的 KV 文件。它通过在<>之间添加名称来引用自定义的MyLayout类。这个类有两个子部件,分别是CustomLabelLabel

注意,我们必须在TestApp类中定义build()函数来返回MyLayout类的一个实例。这是因为 KV 文件本身不会为TestApp返回布局。KV 文件简单地创建了两个名为MyLayoutCustomLabel的定制小部件。

<MyLayout>:
    CustomLabel:
        text: "Hello"
    Label:
        text: "Second Label"

<CustomLabel>:
    color: 1,0,0,1
    font_size: "50sp"

Listing 4-9Referencing the Custom BoxLayout Class Inside the KV File

我们还可以根据清单 4-10 中的 KV 文件返回 KV 文件中 TestApp 类的布局。在本例中,KV 文件定义了两个新的小部件,并返回了一个名为MyLayout的小部件。这个小部件代表了TestApp类的布局。Python 代码目前不必实现build()函数。

MyLayout:

<MyLayout>:
    CustomLabel:
        text: "Hello"
    Label:
        text: "Second Label"

<CustomLabel>:
    color: 1,0,0,1
    font_size: "50sp"

Listing 4-10Using the Custom BoxLayout Class

此时,我们能够在 Python 文件中创建一个新的类来扩展一个小部件类,在 KV 文件中引用它,并修改它的一些属性。这使我们能够开始学习如何创建一个具有多个屏幕的应用。

创建和管理屏幕

以前,在构建应用时会创建一个自定义类来扩展kivy.app.App类。该应用有一个窗口,我们可以在其中添加小部件。所有小工具都在一个屏幕内。有时,我们需要将同一个应用的小部件组织到不同的屏幕中,每个屏幕做不同的工作。Kivy 里的屏幕和 Android 里的活动差不多。一个 Android 应用可以有多个活动,一个 Kivy 应用可以有多个屏幕。

为了创建一个屏幕,我们将扩展kivy.uix.screenmanager.Screen类,而不是扩展kivy.app.App类。清单 4-11 显示了创建两个名为Screen1Screen2的类的 Python 文件,每个屏幕一个,扩展了Screen类。还有一个应用类叫做TestApp

import kivy.app
import kivy.uix.screenmanager

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

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

class TestApp(kivy.app.App):
    pass

app = TestApp()
app.run()

Listing 4-11Creating Two Screens by Extending the Screen Class

根据清单 4-11 中的 Python 代码,创建了两个空屏幕。它们的布局在与该应用相关的test.kv文件中给出,如清单 4-12 所示。注意屏幕类名写在<>之间。每个屏幕都有一个name属性。两个屏幕的名字分别是Screen1Screen2。有一个屏幕管理器有两个孩子,这是两个屏幕。屏幕管理器有一个名为current的属性,它告诉窗口中哪个屏幕当前是活动的。该属性接受屏幕名称。每个屏幕都有一个名为manager的属性,对应于屏幕的管理者。我们可以用它来访问 KV 文件中的管理器。

ScreenManager:
   Screen1:
   Screen2:

<Screen1>:
    name: "Screen1"
    Button:
        text: "Button @ Screen 1"
        on_press: root.manager.current = "Screen2"

<Screen2>:
    name: "Screen2"
    Button:
        text: "Button @ Screen 2"
        on_press: root.manager.current = "Screen1"

Listing 4-12Defining the Layout of the Two Screens and Adding Them as Children to the ScreenManager Class

为了从一个屏幕切换到另一个屏幕,我们在每个屏幕上添加了一个按钮。当这样的按钮被按下时,使用root.manager.current属性改变当前屏幕。在第一个屏幕中,当前屏幕变为第二个屏幕。第二个屏幕的情况正好相反。如果当前属性没有在屏幕管理器中指定,它默认为管理器中的第一个屏幕。图 4-5 显示了运行应用后的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-5

屏幕管理器中添加的第一个屏幕显示为应用启动屏幕

点击按钮使用管理器的current属性改变当前屏幕,如图 4-6 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-6

从一个屏幕移动到另一个屏幕

我们可以明确指定当应用开始使用current属性时应该显示哪个屏幕,如清单 4-13 所示。当应用启动时,它将打开第二个屏幕。

ScreenManager:
   current: "Screen2"
   Screen1:
   Screen2:

Listing 4-13Using the current Property of the ScreenManager Class to Explicitly Specify the Startup Screen

访问屏幕内的小部件

添加屏幕及其管理器后,小组件树如下所示。根小部件是ScreenManager,它包含两个子部件Screen。每个屏幕都有一个Button小部件。为了理解如何访问树中的特定部件,研究部件树是很重要的。

  • 应用
    • Root(屏幕管理器)
      • 屏幕

        1. 纽扣
      • 屏幕 2

        1. 纽扣

假设我们需要从 KV 文件访问第一个屏幕中的按钮。我们如何做到这一点?首先,我们需要使用app关键字访问应用本身。然后,使用root关键字访问应用的根小部件。注意,根小部件是一个ScreenManager。因此,当前命令是app.root。根小部件中的子部件是可以使用screens属性访问的屏幕。app.root.screens命令返回管理器中可用屏幕的列表,如下一行所示:

[<Screen name="Screen1">, <Screen name="Screen2">]

第一个屏幕是列表的第一个元素,因此可以使用索引 0 来访问。因此,访问第一个屏幕的完整命令是app.root.screens[0]

在访问目标屏幕后,我们可以像以前一样使用ids字典访问其中的按钮。假设按钮的 ID 为b1。如果是这种情况,访问该按钮的命令如下:

app.root.screens[0].ids["b1"]

在创建屏幕并使用屏幕管理器控制它们之后,我们可以开始修改前面的项目,将小部件分隔在两个屏幕上。

修改实时摄像机捕捉应用以使用屏幕

在前一章的清单 3-37 和 3-38 中,创建了一个 Kivy 应用,该应用持续捕获要发送到 HTTP 服务器的图像,在那里接收到的图像显示在一个 HTML 页面中。配置和捕获图像所需的所有小部件都在同一个屏幕上。在本节中,他们将被分成不同的屏幕,每个屏幕都有特定的工作要做。

第一步是通过添加两个屏幕来准备 Python 文件。第一个屏幕用要捕捉的图像的宽度和高度配置服务器。第二个屏幕捕捉图像并将其发送到服务器。清单 4-14 中修改后的 Python 代码有两个新类,名为ConfigureCapture,它们扩展了Screen类。

import kivy.app
import requests
import kivy.clock
import kivy.uix.screenmanager
import threading

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

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

class PycamApp(kivy.app.App):
    num_images = 0

    def cam_size(self):
        camera = self.root.screens[1].ids['camera']
        cam_width_height = {'width': camera.resolution[0], 'height': camera.resolution[1]}

        ip_addr = self.root.screens[0].ids['ip_address'].text

        port_number = self.root.screens[0].ids['port_number'].text
        url = 'http://' + ip_addr + ':' + port_number + '/camSize'

        try:
            self.root.screens[0].ids['cam_size'].text = "Trying to Establish a Connection..."
            requests.post(url, params=cam_width_height)
            self.root.screens[0].ids['cam_size'].text = "Done."
            self.root.current = "capture"
        except requests.exceptions.ConnectionError:
            self.root.screens[0].ids['cam_size'].text = "Connection Error! Make Sure Server is Active."

    def capture(self):
        kivy.clock.Clock.schedule_interval(self.upload_images, 1.0)

    def upload_images(self, *args):
        self.num_images = self.num_images + 1
        print("Uploading image", self.num_images)

        camera = self.root.screens[1].ids['camera']

        print("Image Size ", camera.resolution[0], camera.resolution[1])
        print("Image corner ", camera.x, camera.y)

        pixels_data = camera.texture.get_region(x=camera.x, y=camera.y, width=camera.resolution[0], height=camera.resolution[1]).pixels

        ip_addr = self.root.screens[0].ids['ip_address'].text

        port_number = self.root.screens[0].ids['port_number'].text
        url = 'http://' + ip_addr + ':' + port_number + '/'
        files = {'media': pixels_data}

        t = threading.Thread(target=self.send_files_server, args=(files, url))
        t.start()

    def build(self):
        pass

    def send_files_server(self, files, url):
        try:
            requests.post(url, files=files)
        except requests.exceptions.ConnectionError:
            self.root.screens[1].ids['capture'].text = "Connection Error! Make Sure Server is Active."

app = PycamApp()
app.run()

Listing 4-14Using the Screen Class to Redesign the Live Camera Capture Application Created in Listing 3-37

KV 文件中的小部件树如下所示。请注意,小部件在两个屏幕上是分开的。

  • 应用
    • 根(ScreenManager)

    • 配置屏幕

      • BoxLayout

        • Label

        • TextInput ( ip_address)

        • TextInput ( port_number)

        • Button ( cam_size)

      • 捕获屏幕

        • BoxLayout
          • Camera ( camera)

          • Button ( capture)

应用的 KV 文件如清单 4-15 所示,其中每个屏幕都有一个BoxLayout用于分组其小部件。配置屏幕有一个Label小部件,为用户显示说明。有两个TextInput小部件,用户可以在其中输入 IPv4 地址和服务器监听请求的端口号。它还包括Button小部件,用于根据摄像机的尺寸发送POST消息。捕捉屏幕包括相机小部件本身和一个开始捕捉图像的按钮。

两个屏幕都分组在ScreenManager下。请注意,配置屏幕是添加到管理器的第一个屏幕,因此它将在应用启动时显示。

ScreenManager:
    Configure:
    Capture:

<Capture>:
    name: "capture"
    BoxLayout:
        orientation: "vertical"
        Camera:
            id: camera
            size_hint_y: 18
            resolution: (1024, 1024)
            allow_stretch: True
            play: True
            canvas.before:
                PushMatrix:
                Rotate:
                    angle: -90
                    origin: root.width/2, root.height/2
            canvas.after:
                PopMatrix:
        Button:
            id: capture
            font_size: 30
            text: "Capture"
            size_hint_y: 1
            on_press: app.capture()

<Configure>:
    name: "configure"

    BoxLayout:
        orientation: "vertical"
        Label:
            text: "1) Enter the IPv4 address of the server.\n2) Enter the port number. \n3) Press the Configure Server button. \nMake sure that the server is active."
            font_size: 20
            text_size: self.width, None
            size_hint_y: 1
        TextInput:
            text: "192.168.43.231"
            font_size: 30
            id: ip_address
            size_hint_y: 1
        TextInput:
            text: "6666"
            font_size: 30
            id: port_number
            size_hint_y: 1
        Button:
            id: cam_size
            font_size: 30
            text_size: self.width, None
            text: "Configure Server"
            size_hint_y: 1
            on_press: app.cam_size()

Listing 4-15The KV File of the Live Camera Capture Project After Using Screens

一旦用户按下配置服务器的按钮,就会返回摄像头小部件的尺寸,并根据从TextInput小部件中检索到的 IPv4 地址和端口号向服务器发送一条POST消息。第一屏如图 4-7 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-7

指定 IP 和端口号的应用的主屏幕

消息发送成功后,管理器的当前屏幕变为截图屏幕,如图 4-8 所示。在该屏幕中,用户可以按下捕获按钮,以便开始捕获并将捕获的图像发送到服务器。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 4-8

应用中的第二个屏幕,在这里可以捕捉图像并将其发送到服务器

请注意如何从小部件树中访问小部件。如前一节所述,ScreenManager是根,有两个屏幕。每个屏幕都有许多小部件,可以使用它们的 id 进行访问。例如,可以使用以下命令从 KV 文件访问 Camera 小部件。

app.root.screens[1].ids['camera']

在这个项目中,我们对从 KV 文件引用小部件不感兴趣,而是从 Python 文件引用。例如,必须从PycamApp类的cam_size()函数内部访问 Camera 小部件。在这种情况下,与前一个命令的区别在于如何访问应用。可以使用self关键字来引用它。因此,用于访问 Python 中的 Camera 小部件的命令如下。

self.root.screens[1].ids['camera']

我们使用索引为1screen,因为 Camera 小部件位于其中。这样,我们成功地从索引为1的第二个屏幕访问了一个小部件。如果我们需要访问 ID 为ip_addressTextInput小部件,这可以在 Python 代码的第一个屏幕中找到,那么使用下一个命令。除了小部件的 ID 之外,只需指定屏幕的索引。

self.root.screens[0].ids['ip_address']

要访问端口号,使用下一个命令:

self.root.screens[0].ids['port_number']

完成服务器端和客户端应用后,我们就可以开始发布它们了。

发布服务器端应用

为了从 Python 项目创建可执行文件,我们可以使用 PyInstaller 库。我们可以使用pip install pyinstaller命令安装这个库。

在构建可执行文件之前,我们可以稍微改变一下服务器应用。这是因为它不允许我们更改 IPv4 地址和端口号。我们曾经使用以下终端命令来执行服务器应用:

ahmedgad@ubuntu:~/Desktop$ python3 FlaskServer.py

当从终端执行一个 Python 文件时,一些参数在sys.argv列表中传递给它。如果在终端中没有指定参数,那么列表中将有一个包含 Python 脚本名称的项目,可以通过以下命令访问该项目:

sys.argv[0]

参数可以列在 Python 脚本的名称之后。例如,下一个命令将 IPv4 地址和端口号作为参数传递给脚本。

ahmedgad@ubuntu:~/Desktop$ python3 FlaskServer.py 192.168.43.231 6666

为了访问 Python 脚本中的 IPv4 地址并将其存储在名为ip_address的变量中,我们使用了下一个命令。使用索引1,因为它是列表中的第二个参数。

ip_address = sys.argv[1]

同样,使用下一个命令将端口号存储到port_number变量中。请注意,使用了索引2

port_number = sys.argv[2]

清单 4-16 中列出的服务器应用的新 Python 代码从终端参数中获取 IPv4 地址和端口号。在app.run()方法中,主机和端口参数从ip_addressport_number变量中取值,而不是静态定义的。

import flask
import PIL.Image
import base64
import webbrowser
import sys
import os

app = flask.Flask(import_name="FlaskUpload")

cam_width = 0
cam_height = 0

html_opened = False

@app.route('/camSize', methods = ['GET', 'POST'])
def cam_size():
    global cam_width
    global cam_height

    cam_width = int(float(flask.request.args["width"]))
    cam_height = int(float(flask.request.args["height"]))

    print('Width',cam_width,'& Height',cam_height,'Received Successfully.')

    return "OK"

@app.route('/', methods = ['POST'])
def upload_file():
    global cam_width
    global cam_height
    global html_opened

    file_to_upload = flask.request.files['media'].read()

    image = PIL.Image.frombytes(mode="RGBA", size=(cam_width, cam_height), data=file_to_upload)
    image = image.rotate(-90)
    print('File Uploaded Successfully.')

    im_base64 = base64.b64encode(image.tobytes())

    html_code = '<html><head><meta http-equiv="refresh" content="1"><title>Displaying Uploaded Image</title></head><body><h1>Uploaded Image to the Flask Server</h1><img src="data:;base64,'+im_base64.decode('utf8')+'" alt="Uploaded Image at the Flask Server"/></body></html>'

    # The HTML page is not required to be opened from the Python code but open it yourself externally.
    html_url = os.getcwd()+"/templates/test.html"
    f = open(html_url,'w')
    f.write(html_code)
    f.close()

    if html_opened == False:
        webbrowser.open(html_url)
        html_opened = True

    return "SUCCESS"

ip_address = sys.argv[1]#"192.168.43.231"
port_number = sys.argv[2]#6666
app.run(host=ip_address, port=port_number, debug=True, threaded=True)

Listing 4-16Modified Python Code for the Server-Side Application for Fetching the IPv4 Address and Port Number from the Command-Line Arguments

安装后,可以使用以下命令将项目转换为可执行文件。只需将<python-file-name>替换为服务器的 Python 文件名。--onefile选项使 PyInstaller 生成一个二进制文件。只要未指定 Python 文件的完整路径,请确保在执行该文件的位置执行该命令。

pyinstaller --onefile <python-file-name>.py

命令完成后,二进制文件将存在于dist文件夹中,根据 Python 文件名命名。PyInstaller 为正在使用的操作系统创建一个可执行文件。如果在 Linux 机器上执行这个命令,那么就会产生一个 Linux 二进制文件。如果在 Windows 中执行,那么将创建一个 Windows 可执行文件(.exe)。

可执行文件可以存放在您选择的存储库中,用户可以下载并运行服务器。Linux 可执行文件可在此页面下载,文件名为: https://www.linux-apps.com/p/1279651 。因此,为了运行服务器,只需下载文件并运行下面的终端命令。记得根据 CamShare 的当前路径更改终端的路径。

ahmedgad@ubuntu:~/Desktop$ python3 CamShare 192.168.43.231 6666

将客户端 Android 应用发布到 Google Play

之前的 APK 文件只是用于调试,不能在 Google Play 上发布,因为它只接受发布版 apk。为了创建应用的发布版本,我们使用下面的命令:

ahmedgad@ubuntu:~/Desktop$ buildozer android release

为了在 Google Play 上被接受,在您的发布 APK 上签名非常重要。有关签署 APK 的说明,请阅读本页: https://github.com/kivy/kivy/wiki/Creating-a-Release-APK 。还记得将目标 API 级别至少设置为 26,如前所述。

您可以在 Google Play 创建一个开发者帐户来发布您自己的应用。CamShare Android 应用可在此处获得: https://play.google.com/store/apps/details?id=camshare.camshare.myapp

您可以下载 Android 应用,将其连接到服务器,并捕捉将在服务器上的 HTML 页面中显示的图像。

摘要

作为总结,本章介绍了通过扩展 Kivy 小部件来构建定制小部件。这允许我们编辑它们的属性一次,并多次使用它们。本章还介绍了用于跨多个屏幕组织应用小部件的ScreenScreenManager类。为了指定应用一启动就显示哪个屏幕,ScreenManager的当前属性被设置为所需屏幕的名称。这些屏幕用于重新设计第三章中现场摄像机捕捉项目的界面。

在下一章中,将应用本章和所有前面章节中介绍的 Kivy 概念来创建一个多关卡跨平台游戏,在该游戏中,玩家收集大量随机分布在屏幕上的硬币。会有怪物试图杀死玩家。下一章通过让游戏开发变得非常简单并解释每一行代码,让读者从零到英雄。

五、在 Kivy 建立你的第一个多关卡游戏

前一章介绍了 Kivy,这样我们就可以开始构建跨平台的应用。作为应用概念的一种方式,我们创建了一个 Android 应用,它可以捕获图像并不断地将它们发送到 Flask 服务器。

本章通过创建一个多关卡跨平台游戏来应用这些相同的概念,玩家在每个级别都有一个任务,就是收集随机分布在屏幕上的硬币。怪物试图杀死收集硬币的玩家。这款游戏可以在不同的平台上成功运行,而无需我们修改任何一行代码。在你学习如何构建游戏之前,我们将介绍一些新概念,包括FloatLayout和动画。

浮动布局

在前面的章节中,BoxLayout小部件用于对多个小部件进行分组。小部件以有序的方式添加到该布局中,根据方向可以是水平的或垂直的。小部件的大小是由布局计算的,对它的控制很小。在本章我们将要创建的游戏中,一些小部件不会遵循预先定义的顺序。我们需要定制它们的尺寸,并自由地将它们移动到任何位置。比如根据触摸位置放置主角。出于这个原因,我们将使用FloatLayout小部件。它根据每个小部件中指定的 x 和 y 坐标放置小部件。

清单 5-1 显示了用于构建 Kivy 应用的通用代码,其中的子类被命名为TestApp

import kivy.app

class TestApp(kivy.app.App):
    pass

app = TestApp()
app.run()

Listing 5-1Generic Python Code to Build a Kivy Application

基于类名,KV 文件必须被命名为test.kv,以便隐式地检测它。test.kv文件内容如清单 5-2 所示。正好有一个FloatLayout小部件带着一个子,叫Button。注意在Button小部件中有两个重要的字段— size_hintpos_hint。与BoxLayout相比,使用FloatLayout添加的小部件可能不会扩展整个屏幕的宽度或高度。

FloatLayout:
    Button:
        size_hint: (1/4, 1/4)
        pos_hint: {'x': 0.5,'y': 0.5}
        text: "Hello"

Listing 5-2KV File with FloatLayout as the Root Widget

如果您运行该应用,您将会看到图 5-1 中的窗口。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-1

在浮动布局中添加了一个按钮

默认情况下,小部件被添加到父窗口的(0,0)位置,该位置对应于窗口的左下角。因此,我们需要移动小部件,以避免将它们放置在彼此之上。pos_hint字段接受一个字典,其中有两个字段指定小部件左下角和窗口左下角之间的距离。该距离相对于父尺寸。

x 的值为 0.5 意味着按钮将离开窗口的左侧 50%的父宽度。y 值为 0.5 意味着按钮将远离窗口底部父高度的 50%。这样,Button小部件的左下角从布局的中心开始。请注意,相对定位是处理不同尺寸屏幕的有效方式。

size_hint字段指定小部件相对于其父尺寸的尺寸。它接受一个保存小部件相对宽度和高度的元组。在此示例中,按钮的宽度和高度被设置为 1/4,这意味着按钮大小是父大小的 40%(即,四分之一)。

注意pos_hintsize_hint字段不能保证改变小部件的大小或位置。小部件只是给父部件一个提示,它希望根据指定的值来设置它的位置和大小。有些布局听从它的请求,至于哪些布局忽略了它。在前面的例子中,如果根据清单 5-3 中的代码将FloatLayout替换为BoxLayout,那么根据图 5-2 的布局将不会应用一些提示。请注意,默认方向是水平的。

BoxLayout:
    Button:
        size_hint: (1/4, 1/4)
        pos_hint: {'x': 0.5,'y': 0.5}
        text: "Hello"

Listing 5-3Horizontal BoxLayout Orientation Does Not Listen to the Width Hint

因为按钮是其水平BoxLayout父级中的唯一子级,所以它的左下角应该从(0,0)位置开始。根据图 5-2 ,按钮不是从(0,0)位置开始。它的 x 坐标如预期的那样从 0 开始,但是它的 y 坐标从父代高度的一半开始。结果家长只是听了关于 Y 位置的暗示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-2

即使宽度提示设置为 1/4,该按钮也会扩展整个屏幕宽度

关于按钮大小,它应该覆盖整个窗口,因为它是父窗口中的唯一子窗口。这在前面的例子中没有发生。高度是父项高度的 1/4,但宽度扩展到父项的整个宽度。

总结一下,当pos_hintsize_hint字段与BoxLayout一起使用时,只有高度和 Y 位置发生了变化,而宽度和 X 位置没有变化。原因是水平方向的BoxLayout只听取与 Y 轴相关的提示(例如,高度和 Y 位置)。如果根据清单 5-4 使用垂直方向,宽度和 X 位置会改变,但是高度和 Y 位置不会根据图 5-3 改变。这就是为什么FloatLayout被用来动态定位和调整窗口小部件的大小。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-3

即使高度提示设置为 1/4,该按钮也会扩展屏幕的整个高度

BoxLayout:
    orientation: 'vertical'
    Button:
        size_hint: (1/4, 1/4)
        pos_hint: {'x': 0.5,'y': 0.5}
        text: "Hello"

Listing 5-4Vertical Orientation for BoxLayout Does Not Listen to the Height Hint

注意,pos_hint域改变了 X 和 Y 坐标。如果我们只对改变一个而不是两个感兴趣,我们可以在字典中指定。请注意,字典中还有其他需要指定的项目,如toprightcenter_xcenter_y

此外,size_hint字段指定了宽度和高度。我们可以使用size_hint_x来指定宽度,或者使用size_hint_y来指定高度。因为水平方向的BoxLayout不会改变小部件的 X 位置和宽度,所以我们可以避免指定它们。清单 5-5 使用较少的提示产生了相同的结果。

BoxLayout:
    Button:
        size_hint_y: 1/4
        pos_hint: {'y': 0.5}
        text: "Hello"

Listing 5-5Just Specifying the Height Hint Using size_hint_y

假设我们想向FloatLayout添加两个小部件,其中第一个小部件从(0,0)位置开始,延伸到布局的中心,第二个小部件从父窗口宽度和高度的 75%开始,延伸到它的右上角。清单 5-6 显示了构建这样一个小部件树所需的 KV 文件。结果如图 5-4 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-4

在 FloatLayout 中添加两个按钮

FloatLayout:
    Button:
        size_hint: (0.5, 0.5)
        text: "First Button"
    Button:
        size_hint: (0.25, 0.25)
        pos_hint: {'x': 0.75, 'y': 0.75}
        text: "Second Button"

Listing 5-6Adding Two Buttons Inside FloatLayout

第一个按钮大小的size_hint字段的宽度和高度都设置为 0.5,使其大小为窗口大小的 50%。它的pos_hint被省略,因为小部件默认从(0,0)位置开始。

第二个按钮的pos_hint对于 x 和 y 都设置为 0.75,使其从距离父按钮的宽度和高度 75%的位置开始。其size_hint设置为 0.25,使按钮延伸到右上角。

动画

为了在 Kivy 中创建一个游戏,动画是必不可少的。它使事情进展顺利。例如,我们可能对制作一个沿着特定路径移动的怪物的动画感兴趣。在 Kivy 中,只需使用kivy.animation.Animation类就可以创建动画。让我们创建一个带有图像小部件的应用,并通过改变它的位置来制作动画。

清单 5-7 显示了应用的 KV 文件。根小部件是FloatLayout,它包含两个子小部件。第一个子小部件是一个 ID 为character_imageImage小部件,它显示由源字段指定的图像。当设置为True时,allow_stretch属性拉伸图像以覆盖Image小工具的整个区域。

有一个Button小部件,当它被按下时会启动动画。因此,Python 文件中一个名为start_char_animation()的函数与on_press事件相关联。

FloatLayout:
    Image:
        id: character_image
        size_hint: (0.15, 0.15)
        pos_hint: {'x': 0.2, 'y': 0.6}
        allow_stretch: True
        source: "character.png"
    Button:
        size_hint: (0.3, 0.3)
        text: "Start Animation"
        on_press: app.start_char_animation()

Listing 5-7Adding an Image to the Widget Tree Using the Image Widget

Python 文件的实现如清单 5-8 所示。在TestApp类内部,实现了start_char_animation()函数。在char_animation变量中创建了一个kivy.animation.Animation类的实例。该类接受要动画显示的目标小部件的属性。因为我们对改变Image小部件的位置感兴趣,所以pos_hint属性作为输入参数被提供给Animation类构造函数。

请注意,不可能将小部件中未定义的属性制作成动画。例如,我们不能动画显示width属性,因为它没有在小部件中定义。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_animation = kivy.animation.Animation(pos_hint={'x':0.8, 'y':0.6})
        char_animation.start(character_image)

app = TestApp()
app.run()

Listing 5-8Building and Starting the Animation Over an Image

为了让小部件中的属性具有动画效果,我们必须提供属性名及其新值。动画从属性的前一个值开始,在本例中是 KV 文件中的pos_hint字段指定的值,即{'x': 0.2, 'y': 0.6},并在Animation类的构造函数中指定的值{'x': 0.8, 'y': 0.6}处结束。因为只是 x 位置有变化,所以图像会水平移动。

调用Animation类的start()方法来启动动画。这个方法接受目标小部件的 ID,在这个小部件中,我们希望动画显示在Animation类构造函数中指定的属性。

当我们单击Button小部件时,start_char_animation()功能被执行,动画开始。图 5-5 显示了按下按钮之前和之后窗口的显示。默认情况下,动画需要一秒钟才能完成。这个时间可以使用duration参数来改变。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-5

动画开始前后。左图显示原始图像,右图显示动画结束后的结果

请注意,我们可以在同一个动画实例中制作多个属性的动画。这是通过用逗号分隔不同的属性来实现的。清单 5-9 动画显示图像的大小和位置。通过将其从(0.15,0.15)更改为(0.2,0.2),大小加倍。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_animation = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6}, size_hint=(0.2, 0.2), duration=1.5)
        char_animation.start(character_image)

app = TestApp()
app.run()

Listing 5-9Animating Multiple Properties Within the Same Animation Instance

运行应用并按下按钮后,动画结束后的结果如图 5-6 所示。请注意,持续时间变为 1.5 秒。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-6

动画显示图像的 pos_hint 和 size_hint 属性后的结果

即使动画仍然工作,单击“更多”按钮也不会移动或更改图像的大小。事实上,每次按下按钮后都会执行start_char_animation()功能。对于每一次按压,根据附加的小部件创建并启动一个Animation实例。第一次谈论pos_hint属性时,在 KV 文件中指定的pos_hint属性的旧值和在Animation类构造函数中指定的新值是不同的。这就是图像从 x=0.2 移动到 x=0.8 的原因。动画化图像后,其pos_hint属性将为{'x': 0.8, 'y': 0.6}

再次制作图像动画时,开始值和结束值将等于{'x': 0.8, 'y': 0.6}。这就是为什么图像小部件的位置没有变化。Kivy 支持循环动画,但是循环前一个动画是没有意义的。属性中必须至少有一个其他值,这样小部件才能从一个值转到另一个值。在循环动画之前,我们需要向pos_hint属性添加另一个值。

单个动画接受给定属性的单个值,但是我们可以在另一个动画中添加另一个值,并将这些动画连接在一起。

加入动画

加入动画有两种方式—顺序和并行。在连续动画中,当一个动画结束时,下一个动画开始,并持续到最后一个动画。在这种情况下,使用+操作符将它们连接起来。在并行动画中,所有动画同时开始。使用&操作符将它们连接起来。

清单 5-10 显示了两个动画顺序连接的例子。第一个动画实例名为char_anim1 **,**通过将pos_hint属性更改为{'x': 0.8, 'y': 0.6},将图像水平向右移动,就像前面所做的一样。第二个动画实例名为char_anim2,将小部件垂直移动到底部的新位置{'x': 0.8, 'y': 0.2}。使用+操作符连接两个动画,结果存储在all_anim1变量中。加入的动画通过调用start()方法开始。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim1 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6})
        char_anim2 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.2})
        all_anim = char_anim1 + char_anim2
        all_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-10Joining Animations Sequentially

按下按钮后,运行所有动画后的结果如图 5-7 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-7

将多个要应用于图像小工具的动画按顺序连接起来

图 5-8 中显示了pos_hint属性改变的路径概要。图像从 KV 文件中指定的{'x': 0.2, 'y': 0.6}开始。运行第一个动画后,它移动到新的位置{'x': 0.8, 'y': 0.6}。最后,它在运行第二个动画后移动到{'x': 0.8, 'y': 0.2}。该位置保持图像的当前位置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-8

根据清单 5-10 中定义的两个动画的图像小部件的路径

动画完成后,如果再次按下按钮会发生什么?加入的动画将再次开始。在第一个动画中,它将图像的位置从当前位置更改为其参数pos_hint中指定的新位置,该参数为{'x': 0.8, 'y': 0.6}。因为当前位置{'x': 0.8, 'y': 0.2}与新位置{'x': 0.8, 'y': 0.6}不同,所以图像会移动。图像的当前位置将是{'x': 0.8, 'y': 0.6}

运行第一个动画后,第二个动画开始,它将图像从当前位置{'x': 0.8, 'y': 0.6}移动到其pos_hint参数中指定的新位置,即{'x': 0.8, 'y': 0.2}。因为位置不同,图像会移动。每次按下按钮都重复这个过程。请注意,如果没有备份,KV 文件中属性的初始值会在动画结束后丢失。

每个动画需要一秒钟才能完成,因此合并动画的总时间为两秒钟。您可以使用duration参数控制每个动画的持续时间。

因为pos_hint属性改变的值不止一个,所以我们可以循环前面的动画。根据清单 5-11 ,我们通过将动画实例的repeat属性设置为True来做到这一点。这在两个动画之间创建了一个无限循环。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim1 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6})
        char_anim2 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.2})
        all_anim = char_anim1 + char_anim2
        all_anim.repeat = True
        all_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-11Repeating Animations by Setting the repeat Property to True

可以使用Animation类构造函数中的t参数来改变动画过渡。默认是linear。有不同类型的过渡,如in_backin_quadout_cubic以及许多其他类型。您也可以使用transition属性返回它。清单 5-12 显示了第一个动画的过渡设置为out_cubic的例子。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim1 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6}, t='out_cubic')
        char_anim2 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.2})
        all_anim = char_anim1 + char_anim2
        all_anim.repeat = True
        all_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-12Setting the Transition Property to out_cubic

取消动画

为了停止分配给给定小部件中所有属性的所有动画,我们调用了cancel_all()函数。它停止所有被调用的动画。

我们可以在窗口小部件树中添加另一个按钮,当我们单击它时,它会停止所有动画。新的 KV 文件如清单 5-13 所示。当按下该按钮时,执行stop_animation()功能。请注意,此按钮的位置已经改变,以避免将其放在前面的按钮上。

FloatLayout:
    Image:
        id: character_image
        size_hint: (0.15, 0.15)
        pos_hint: {'x': 0.2, 'y': 0.6}
        allow_stretch: True
        source: "character.png"
    Button:
        size_hint: (0.3, 0.3)
        text: "Start Animation"
        on_press: app.start_char_animation()
    Button:
        size_hint: (0.3, 0.3)
        text: "Stop Animation"
        pos_hint: {'x': 0.3}
        on_press: app.stop_animation()

Listing 5-13Adding a Button Widget to Stop Running Animations

Python 文件如清单 5-14 所示。在stop_animation()函数中,调用cancel_all()函数来停止所有与 ID 为character_image的小部件相关联的动画。当动画被取消时,动画属性的当前值被保存。当动画再次开始时,这些值用作开始值。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim1 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6})
        char_anim2 = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.2})
        all_anim = char_anim1 + char_anim2
        all_anim.repeat = True
        all_anim.start(character_image)

    def stop_animation(self):
        character_image = self.root.ids['character_image']
        kivy.animation.Animation.cancel_all(character_image)

app = TestApp()
app.run()

Listing 5-14Stopping Running Animations Upon Press of the Button Widget

这样,我们能够开始和停止与给定小部件相关的所有属性的动画。我们还可以使用cancel_all()指定选定的属性来停止其动画,同时保持其他属性。我们不只是提供小部件引用,而是添加一个需要停止的属性列表,用逗号分隔。

图像小工具的动画源属性

在先前的应用中,当其位置改变时,显示相同的静态图像。如果我们想让角色行走,最好是随着其位置的变化而改变图像,以给人一种行走角色的印象。例如,我们通过改变它的腿和手的位置来做到这一点。图 5-9 显示了角色在不同位置的一些图像。当角色移动时,我们也可以改变显示的图像。这使得游戏更加真实。因为图像是使用Image小部件中的source属性指定的,所以我们需要激活这个属性来改变显示的图像。问题是我们如何制作source属性的动画?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-9

不同的图像来反映角色的运动

在前面的例子中,pos_hintsize_hint属性是动态的,它们接受数值。但是source属性接受一个指定图像名称的字符串。有可能将字符串属性动画化吗?不幸的是,动画只改变数值。我们可以要求Animation类将一个属性从一个数值(如 1.3 )更改为另一个数值(如 5.8 )。但是我们不能要求它将一个属性从一个字符串值(如character1.png)更改为另一个字符串值(如character2.png)。那么,我们如何制作这个动画呢?

一个懒惰的解决方案包括四个步骤。我们向Image小部件添加一个新的属性,假设它被命名为im_num,它将被分配一个引用图像索引的数字。然后我们激活这个属性来生成当前的图像编号。第三步是返回动画生成的每个值。最后一步是使用生成的数字创建图像名称,方法是创建一个由图像扩展名前面的数字组成的字符串,并将Image小部件的source属性设置为返回的图像名称。该过程的总结如图 5-10 所示。让我们应用这些步骤。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-10

制作 Image 小部件的 source 属性动画的步骤

第一步,清单 5-15 显示了添加im_num属性后的 KV 文件。注意 Python 允许我们向已经存在的类添加新的属性。新属性的值为 0,表示角色的第一个图像。

FloatLayout:
    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
    Button:
        size_hint: (0.3, 0.3)
        text: "Start Animation"
        on_press: app.start_char_animation()

Listing 5-15Adding the im_num Property to Change the Image Using Animation

第二步很简单。我们只是将一个名为im_num的参数添加到Animation类的构造函数中。该参数被分配给最后一个要使用的索引。如果有八个图像的索引从 0 到 7,则该参数被指定为 7。清单 5-16 显示了 Python 代码。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6}, im_num=7)
        char_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-16Adding the im_num Argument to the Animation

第三步,我们要回答这个问题,“我们如何返回动画生成的当前值?”答案很简单。为了在给定小部件的名为 X 的属性值发生变化时得到通知,我们在该小部件中添加了一个名为on_X的事件。此事件被分配给一个 Python 函数,每次属性值更改时都会调用该函数。因为我们的目标字段被命名为im_num,所以该事件将被称为on_im_num

清单 5-17 显示了添加该事件后修改后的 KV 文件。每次im_num字段的值改变时,Python 文件内的函数change_char_im()将被调用。

FloatLayout:
    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()
    Button:
        size_hint: (0.3, 0.3)
        text: "Start Animation"
        on_press: app.start_char_animation()

Listing 5-17Adding the on_im_num Event to the Image Widget to Be Notified When the Image Changes

清单 5-18 显示了添加这个函数后修改后的 Python 代码。每次改变时,打印im_num的值。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        print(character_image.im_num)

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6}, im_num=7)
        char_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-18Handling the on_im_num Event to Print the im_num When Changed

在第四步中,将返回的数字连接到图像扩展名,以返回表示图像名称的字符串。这个字符串被分配给图像模块的source属性。根据清单 5-19 ,这项工作是在修改后的change_char_im()函数内部完成的。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6}, im_num=7)
        char_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-19Changing the Source Image of the Image Widget when im_num Changes

请注意,动画会在动画属性的起始值和结束值之间插入浮点数。所以,会有 0.1,2.6,4.3 等值。因为图像名称中有整数,所以im_num属性中的浮点值应该改为整数。

在将其转换为整数后,可以将其与图像扩展名连接起来,以返回表示图像名称的字符串。这个字符串被分配给图像模块的source属性。记住将图像设置在 Python 文件的当前目录下。否则,在图像名称前添加路径。

使用最新的 Python 和 KV 文件运行应用并按下按钮后,图像应该会随着时间的推移而改变。图 5-11 显示了角色在使用动画改变其图像时的四张截图。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-11

当角色移动时,角色图像改变

屏幕触摸事件

到目前为止,当点击按钮时,角色会移动。移动路径仅限于输入到动画类构造函数中的路径。我们需要改变这一点,以便根据整个屏幕上的触摸位置自由移动角色。请注意,Kivy 中的触摸指的是鼠标按压或触摸屏幕。为了做到这一点,我们需要获得屏幕上的触摸位置,然后动画角色移动到那个位置。

为了返回屏幕上的触摸位置,需要使用三个触摸事件,分别是on_touch_upon_touch_downon_touch_move。我们只是对触摸按下时获取触摸位置感兴趣,所以使用了on_touch_down事件。

根据清单 5-20 中修改的 KV 文件,该事件被添加到根小部件(即FloatLayout)。请注意,将触摸事件与布局本身或其一个子布局绑定并不重要,因为它们不检测冲突,因此无法检测触摸位置的边界。它们总是返回整个窗口上的触摸位置。

FloatLayout:
    on_touch_down: app.touch_down_handler(*args)

    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()
    Button:
        size_hint: (0.3, 0.3)
        text: "Start Animation"
        on_press: app.start_char_animation()

Listing 5-20Using the on_touch_down Event to Return the Screen Touch Position

该事件接受一个功能,该功能将在每次触摸屏幕时执行。Python 文件内的touch_down_handler()函数将响应触摸而执行。事件生成的所有参数都可以使用args变量传递给处理程序。这有助于访问 Python 函数内部的触摸位置。

清单 5-21 展示了实现touch_down_handler()函数的修改后的 Python 文件。该函数只是在 args 中打印从事件接收的参数。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def touch_down_handler(self, *args):
        print(args)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def start_char_animation(self):
        character_image = self.root.ids['character_image']
        char_anim = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.6}, im_num=7)
        char_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-21Handling the touch_down_handler() to Get the Screen Touch Position

根据给定的输出,传递给函数的args是一个具有两个元素的元组。第一个元素指示哪个小部件与事件相关联。第二个元素给出了关于触摸和触发它的设备的信息。例如,通过单击鼠标左键触发触摸事件。

(<kivy.uix.floatlayout.FloatLayout object at 0x7fcb70cf4250>, <MouseMotionEvent button="left" device="mouse" double_tap_time="0" dpos="(0.0, 0.0)" dsx="0.0" dsy="0.0" dsz="0.0" dx="0.0" dy="0.0" dz="0.0" grab_current="None" grab_exclusive_class="None" grab_list="[]" grab_state="False" id="mouse1" is_double_tap="False" is_mouse_scrolling="False" is_touch="True" is_triple_tap="False" opos="(335.0, 206.99999999999997)" osx="0.45702592087312416" osy="0.37981651376146786" osz="0.0" ox="335.0" oy="206.99999999999997" oz="0.0" pos="(335.0, 206.99999999999997)" ppos="(335.0, 206.99999999999997)" profile="['pos', 'button']" psx="0.45702592087312416" psy="0.37981651376146786" psz="0.0" push_attrs="('x', 'y', 'z', 'dx', 'dy', 'dz', 'ox', 'oy', 'oz', 'px', 'py', 'pz', 'pos')" push_attrs_stack="[]" px="335.0" py="206.99999999999997" pz="0.0" shape="None" spos="(0.45702592087312416, 0.37981651376146786)" sx="0.45702592087312416" sy="0.37981651376146786" sz="0.0" time_end="-1" time_start="1563021796.776788" time_update="1563021796.776788" triple_tap_time="0" ud="{}" uid="1" x="335.0" y="206.99999999999997" z="0.0">)

事件指定触摸位置有不同的方式。例如,pos属性根据窗口以像素为单位指定位置,而spos返回相对于窗口大小的位置。因为我们游戏中的所有位置都是相对于窗口大小的,spos 用来指定角色移动到的位置。

以前,移动角色的动画是在 Python 文件内的start_char_animation()函数中创建和启动的。该函数使用角色移动到的静态位置。使用触摸事件后,角色将移动到touch_down_handler()函数中触摸事件的spos属性返回的位置。因此,start_char_animation()功能的标题将改变以接收触摸位置。清单 5-22 显示了修改后的 Python 文件。

请注意spos属性是如何从args返回的。因为它位于 args 的第二个元素(即 index 1)中,所以使用了args[1]

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        char_anim = kivy.animation.Animation(pos_hint={'x': touch_pos[0], 'y': touch_pos[1]}, im_num=7)
        char_anim.start(character_image)

app = TestApp()
app.run()

Listing 5-22Moving the Character According to the Touch Position

因为现在动画是通过触摸屏幕开始的,所以 KV 文件中不需要按钮。清单 5-23 给出了移除该按钮后修改后的 KV 文件。

FloatLayout:
    on_touch_down: app.touch_down_handler(args)

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

Listing 5-23Removing the Button That Starts the Animation

运行应用并触摸窗口后,角色将移动到被触摸的位置。因为小部件的位置反映了左下角将要放置的位置,所以将该位置直接提供给图像小部件的pos_hint属性会使其左下角从触摸位置开始,并根据在size_hint属性中指定的大小进行扩展。如图 5-12 所示。将小工具居中在触摸位置更方便。我们如何做到这一点?

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-12

图像小工具的左下角放置在触摸位置

目前,小部件的中心比触摸位置大,水平方向大其宽度的一半,垂直方向大其高度的一半。根据以下等式计算中心坐标:

widgetCenterX = touchPosX + widgetWidth/2
widgetCenterY = touchPosY + widgetHeight/2

为了根据触摸位置使小部件居中,我们可以从touchPosX中减去widgetWidth/2,从touchPosY中减去widgetHeight/2。结果将如下所示:

widgetCenterX = (touchPosX - widgetWidth/2) + widgetWidth/2 = touchPosX
widgetCenterY = (touchPosY + widgetHeight/2) + widgetWidth/2 = touchPosY

这样,小工具将在触摸位置居中。清单 5-24 显示了在start_char_animation()函数中修改动画位置后的 Python 代码。注意,widgetWidth等于size_hint[0]widgetHeight等于size_hint[1]

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):
    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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.start(character_image)

app = TestApp()
app.run()

Listing 5-24Moving the Center of the Image Widget to the Touch Position

图 5-13 显示触摸屏幕后的结果。角色的中心位于触摸位置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-13

图像小工具的中心放置在触摸位置上

on _ 完成

每次触摸屏幕时,都会创建一个动画实例。它动画显示了Image小部件的pos_hintim_num属性。对于第一次屏幕触摸,这两个属性随着角色的移动和图像的改变而被激活。更多地触摸屏幕只会移动角色,但不幸的是,im_num属性不会改变。因此,在第一次触摸屏幕后,将有一个静态图像显示在小工具上。为什么会这样?

KV 文件中im_num的初始值为 0。第一次触摸屏幕时,动画开始播放,因此im_num从 0 到 7 播放动画。动画完成后,存储在im_num中的当前值将是 7。

再次触摸屏幕,im_num将从当前值 7 变为新值,也是 7。结果,显示的图像没有变化。解决方法是在启动start_char_animation()函数内的动画之前,将im_num的值重置为 0。

动画完成后,角色预计处于稳定状态,因此将显示带有im_num=0的第一个图像。但是完成动画后存储在im_num中的值是 7,而不是 0。最好在动画完成后将im_num重置为 0。

幸运的是,Animation类有一个名为on_complete的事件,在动画完成时被触发。我们可以将我们的动画绑定到这个事件,这样每次完成时都会执行一个回调函数。回调函数被命名为char_animation_completed()。在这个函数中,我们可以强制im_num返回 0。将on_complete事件绑定到角色动画后,修改后的 Python 文件在清单 5-25 中列出。这个on_complete事件向回调函数发送参数,这些参数是触发事件的动画和与之关联的小部件。这就是回调函数接受它们进入args的原因。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

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

    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num))+".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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)

app = TestApp()
app.run()

Listing 5-25Resetting the im_num Property to 0 After the Animation Completes

在回调函数中,Image小部件的im_num被改回 0。因此,每次动画完成时,小部件上显示的图像将被重置。

将角色动画制作正确后,我们就可以开始给游戏添加怪物了。玩家/角色在与怪物相撞时死亡。

向游戏中添加怪物

怪物将被添加到 KV 文件中,就像添加角色一样。我们只是在 KV 文件中为怪物创建了一个Image小部件。新的 KV 文件如清单 5-26 所示。

FloatLayout:
    on_touch_down: app.touch_down_handler(args)

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

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

Listing 5-26Adding an Image Widget for the Monster

怪物的Image部件将具有角色中定义的属性。这些属性是用于引用 Python 文件中的小部件的 ID,size_hint用于设置相对于屏幕大小的小部件大小,pos_hint用于相对于屏幕放置小部件,source用于将图像名称保存为字符串,allow_stretch用于拉伸图像以覆盖图像的整个区域,im_num用于保存小部件上显示的图像编号。为了使角色和怪物的图像编号不同,怪物图像编号将从 10 开始。图 5-14 显示了怪物的图像。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-14

怪物的图像序列

on_im_num事件将通过一个名为change_monst_im()的回调函数与im_num属性相关联,以在每次发生变化时访问 Python 文件中的值。

在我们准备好 KV 文件后,我们将看到的应用窗口如图 5-15 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-15

角色和怪物图像部件出现在屏幕上

请注意,怪物Image小部件位于 KV 文件中的字符小部件之前(即,在小部件树中)。这使得角色的 Z 指数低于怪物的 Z 指数,从而画在它的上面,如图 5-16 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-16

角色图像小部件出现在怪物图像小部件的上方

开启 _ 启动

角色的动画在每次触摸屏幕时开始,但是怪物的动画必须在应用启动后开始。那么,我们可以在 Python 文件的什么地方启动这个怪物呢?根据 Kivy 应用生命周期,一旦应用启动,就会执行名为on_start()的方法。这是开始怪物动画的好地方。

在我们添加了change_monst_im()on_start()函数来处理怪物的动画之后,Python 文件如清单 5-27 所示。除了改变怪物Image控件的source属性外,change_monst_im()功能与change_char_im(),类似。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

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

    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num))+".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num))+".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 on_start(self):
        monster_image = self.root.ids['monster_image']
        monst_anim = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.0}, im_num=17, duration=2.0)+kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.8}, im_num=10, duration=2.0)
        monst_anim.repeat = True

        monst_anim.start(monster_image)

app = TestApp()
app.run()

Listing 5-27Adding the Functions to Handle the Monster Animation

on_start()函数中,创建了两个顺序连接的动画来制作怪物Image小部件的pos_hintim_num属性的动画。

根据 KV 文件怪物的初始位置是{'x':0.8, 'y':0.8}。第一个动画将该位置更改为{'x':0.8, 'y':0.0},第二个动画将其更改回{'x':0.8, 'y':0.8}。这个动画循环发生,因为动画实例monst_animrepeat属性被设置为True。为了简单起见,怪物在固定的路径上移动。在接下来的部分,我们将改变它的运动是随机的。

因为 KV 文件中怪物的im_num属性的初始值设置为 10,所以第一个动画的im_num设置为 17。因此,第一个动画将图像编号从 10 更改为 17。第二个动画将该属性设置为 10,以便将图像编号从 17 改回 10。每个动画持续两秒钟。

动画与 monster Image 小部件相关联,该小部件使用其在monster_image变量中的 ID 返回。

冲突

到目前为止,角色和怪物的pos_hintim_num属性的动画工作正常。我们需要修改游戏,使角色在与怪物相撞时被杀死。

Kivy 中碰撞的工作方式是,它检查两个小部件的边界框之间的交集。内置的 Kivy 函数就是这样做的。例如,该命令检测两个图像小部件之间的冲突:

character_image.collide_widget(monster_image)

我们必须不断检查两个小部件之间的冲突。因此,需要将上述命令添加到定期执行的内容中。

每当小部件使用pos_hint属性改变其位置时,就会触发on_pos_hint事件。该事件将在每次触发时执行一个回调函数。因为怪物图像小部件不断改变它的位置,所以我们可以将事件绑定到那个小部件。

请注意,如果你打算稍后杀死怪物,怪物将不会改变它的位置,因此on_pos_hint将永远不会被发射,因此没有碰撞检查。如果有其他可能杀死角色的物体,并且你完全依赖与被杀死的怪物相关联的事件进行碰撞检测,则角色不会被杀死。你得找别的东西来检查碰撞。一种解决方案是将on_pos_hint事件与每个可能杀死角色的对象绑定。

首先,将on_pos_hint事件添加到 KV 文件中,如清单 5-28 所示。它与一个叫做monst_pos_hint()的回调函数相关联。

FloatLayout:
    on_touch_down: app.touch_down_handler(args)

    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()
        on_pos_hint: app.monst_pos_hint()

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

Listing 5-28Using the on_pos_hint Event to Return the Monster Position

在清单 5-29 所示的 Python 文件的末尾实现了monst_pos_hint()函数。它在character_imagemonster_image属性中返回角色和怪物部件,然后调用collide_widget()函数。到目前为止,如果根据if声明发生了碰撞,将会打印一条消息。

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

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

    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num))+".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num))+".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 on_start(self):
        monster_image = self.root.ids['monster_image']
        monst_anim = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.0}, im_num=17, duration=2.0)+kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.8}, im_num=10, duration=2.0)
        monst_anim.repeat = True

        monst_anim.start(monster_image)

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']
        if character_image.collide_widget(monster_image):
            print("Character Killed")

app = TestApp()
app.run()

Listing 5-29Handling the monst_pos_hint() Callback Function

调整 collide_widget()

collide_widget()函数过于严格,因为如果两个小部件之间至少有一行或一列交集,它将返回True。实际上,这种情况并不经常发生。根据图 5-17 ,这样一个函数返回True,因为两个窗口小部件的边界框有交集。结果就是,这个角色即使没碰过怪物也会被杀死。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-17

当小部件框的外部边界发生冲突时,即使图像没有接触,collide_widget()也会返回 True

我们可以通过添加另一个条件来调整collide_widget()函数,该条件检查碰撞区域是否超过字符大小的预定义百分比。这让我们在说有碰撞的时候更加自信。monst_pos_hint()功能修改如清单 5-30 所示。

def monst_pos_hint(self):
    character_image = self.root.ids['character_image']
    monster_image = self.root.ids['monster_image']

    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:
        print("Character Killed")

Listing 5-30Tuning the collide_widget() Function to Return True Only When the Character and Monster Touch Each Other

新条件的结论是,如果两个窗口小部件的当前中心之间的差异至少是字符大小的一半,则发生碰撞。这是通过确保两个中心的 X 和 Y 坐标之差分别小于字符宽度和高度的一半来实现的。

调节collide_widget()功能后,对于图 5-18 和 5-19 所示的情况,条件返回False。因此,结果更加真实。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-18

没有发生碰撞,因为没有超过列表 5-30 中定义的怪物和角色图像中心之间的最大间隙。完整的 Python 代码如清单 5-31 所示

import kivy.app
import kivy.animation

class TestApp(kivy.app.App):

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

    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num))+".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num))+".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 on_start(self):
        monster_image = self.root.ids['monster_image']
        monst_anim = kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.0}, im_num=17, duration=2.0)+kivy.animation.Animation(pos_hint={'x': 0.8, 'y': 0.8}, im_num=10, duration=2.0)
        monst_anim.repeat = True
        monst_anim.start(monster_image)

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            print("Character Killed")

app = TestApp()
app.run()

Listing 5-31Complete Code for the Game in Which the Work for the Animation and Collision Is Completed Successfully

随机怪物运动

在清单 5-31 中的前一个应用中,我们做了很好的工作,成功地制作了角色和怪物的动画。但是怪物在固定的路径上移动。在本节中,我们将修改它的运动,使它看起来是随机的。这个想法非常类似于角色Image部件的动画。

使用一个名为start_char_animation()的函数激活角色,该函数接受角色移动到的新位置。因为只创建了一个动画,所以可以重复。为了在完成后重复动画,将on_complete事件附加到角色的动画中。名为char_animation_completed()的回调函数与该事件相关联。当动画完成时,这个回调函数被执行,它为新的动画准备角色。我们想让怪物的动作也像这样。这适用于清单 5-32 中所示的修改后的 Python 文件。

创建了两个新函数,分别是start_char_animation()char_animation_completed()start_char_animation()函数接受怪物移动到的新位置,作为名为new_pos的参数。然后它创建一个动画实例,根据新的位置改变pos_hint属性。它还将 KV 文件中的im_num属性从初始值 10 更改为 17。

import kivy.app
import kivy.animation
import random

class TestApp(kivy.app.App):

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

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

        new_pos = (random.uniform(), random.uniform())
        self.start_monst_animation(new_pos=new_pos)

    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            print("Character Killed")

app = TestApp()
app.run()

Listing 5-32Repeating the Monster Animation by Handling the on_complete Event of the Animation

on_start()函数中,start_monst_animation()被调用,输入参数被指定为一个随机值。因为只有一个动画,所以动画不能通过将 repeat 属性设置为True来重复自身。因此,on_complete事件被附加到动画上,以便在动画完成后执行回调函数monst_animation_completed()。这给了我们再次开始动画的机会。

在回调函数内部,怪物的im_num属性再次被重置为 10。使用 random 模块中的uniform()函数,为新位置的 X 和 Y 坐标生成一个随机值。返回值是介于 0.0 和 1.0 之间的浮点数。新位置被用作 monster 小部件的左下角。

假设随机返回的位置是(0.0,1.0),这使得角色的底线从屏幕的末端开始。这样一来,怪物就会被隐藏起来。这也适用于位置(1.0,0.0)和(1.0,1.0)。

为了确保怪物在屏幕上始终可见,我们必须考虑它的宽度和高度。将怪物的左下角定位在新的随机位置后,怪物必须有一个适合其宽度和高度的空间。因此,X 的最大可能值是1-monster_width,Y 的最大可能值是1-monster_height。这为怪物在窗口中任何生成的位置完全可见腾出了空间。

修改后的 Python 代码如清单 5-33 所示。在之前的应用中,所有怪物移动的持续时间是 2.0 秒。在新代码中,使用random.uniform()随机返回持续时间。结果,怪物在随机时间内移动到随机生成的位置。

import kivy.app
import kivy.animation
import random

class TestApp(kivy.app.App):

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            print("Character Killed")

app = TestApp()
app.run()

Listing 5-33Randomly Changing the Position and Duration of the Monster Animation

杀死角色

在清单 5-33 中的上一个游戏中,即使怪物与角色发生碰撞,一切仍然正常。我们需要修改应用,使角色在被杀死时停止移动。我们这样做是为了确保它的动画不会再次开始。

touch_down_handler()功能中,角色总是朝着屏幕上被触摸的位置移动,即使在碰撞之后。在清单 5-34 中列出的修改后的 Python 代码中,通过使用一个名为character_killed的标志变量来指示角色是否被杀死,这个问题得到了解决。这样的变量默认设置为False,意味着游戏还在运行,角色还活着。touch_down_handler()函数中的if语句确保角色动画仅在标志设置为False时工作。因为标志与类相关联,所以可以通过在类名前面加上(TestApp.character_killed)来访问它。

当在mons_pos_hint()函数中检测到碰撞时,采取两个动作,将character_killed标志的值改为True,并取消所有正在运行的动画(即角色和怪物)。

import kivy.app
import kivy.animation
import random

class TestApp(kivy.app.App):
    character_killed = False

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:

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

app = TestApp()
app.run()

Listing 5-34Adding the character_killed Flag to Determine Whether the Character Animation Could Start Again

monst_pos_hint()功能内的标志变为True时,角色动画无法停止。请注意,在标志值变为True后,仍有一个运行的动画响应先前触摸的位置。这意味着角色将继续移动,直到动画完成,然后停止移动。为了在碰撞发生时停止移动动画,我们可以使用cancel_all()功能取消动画。因此,一旦碰撞发生,取消动画将停止它。更改标志值会阻止动画再次开始。

因为怪物动画一旦被取消,用户就没有办法启动了,取消这样的动画就足够了。

角色杀戮动画

根据图 5-19 ,当角色在之前的应用中被杀死时,它会保留由im_num属性指定的图像编号。图像没有反映人物的死亡。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-19

当与怪物发生碰撞时,角色图像停止在其最新状态

我们可以改变形象,给人更好的印象。为此,将使用图 5-20 中显示的图像。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-20

当角色被杀死时显示的图像序列

一旦发生碰撞,只有在根据这些图像制作图像动画的monst_pos_hint()函数内部创建动画后,才会开始。如果这些图像的编号从 91 到 95,修改后的 Python 代码如清单 5-35 所示。新动画只是将im_num属性更改为 95。

需要记住的是,角色动画被取消后,im_num数字会保持在 0 到 7 之间。例如,如果它的值是 5,那么运行新的动画将从 5 到 95。因为我们对从 91 开始感兴趣,所以在动画开始之前,im_num属性值被设置为 91。

import kivy.app
import kivy.animation
import random

class TestApp(kivy.app.App):
    character_killed = False

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            TestApp.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)

app = TestApp()
app.run()

Listing 5-35Running an Animation When the Character Is Killed

根据清单 5-35 中的代码,发生碰撞时结果如图 5-21 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-21

人物形象在与怪物碰撞时会发生变化

添加硬币

玩家的任务是收集分布在屏幕上的大量硬币。一旦收集到正确数量的硬币,游戏的当前级别就完成了,另一个级别开始了。因此,应用的下一步是在小部件树中添加表示硬币的图像小部件。让我们从添加一个代表一枚硬币的图片部件开始。

根据 Kivy 应用的生命周期,build()方法可用于准备小部件树。因此,这是向应用添加新部件的好方法。清单 5-36 中所示的 Python 代码实现了build()方法来添加单个图像小部件。记住导入kivy.uix.image模块,以便访问Image类。

import kivy.app
import kivy.animation
import kivy.uix.image
import random

class TestApp(kivy.app.App):
    character_killed = False

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 build(self):
        coin = kivy.uix.image.Image(source="coin.png", size_hint=(0.05, 0.05), pos_hint={'x': 0.5, 'y': 0.5}, allow_stretch=True)
        self.root.add_widget(coin, index=-1)

    def on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            TestApp.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)

            print("Character Killed")

app = TestApp()
app.run()

Listing 5-36Adding an Image Widget to the Widget Tree Representing the Coin Before the Application Starts

新的小部件使用了sourcesize_hintpos_hintallow_stretch属性。硬币图像来源如图 5-22 所示。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-22

图像角点

新的小部件被返回到coin变量。之后,使用add_widget()方法将它添加到小部件树中。因为窗口小部件树中的最后一个窗口小部件出现在前面窗口小部件的顶部,所以index参数用于改变硬币的 Z 索引。微件的默认 Z 索引是 0。硬币 Z 指数设置为-1,出现在角色和怪物的后面。

运行应用后,我们会看到如图 5-23 所示的窗口。我们可以在窗户上放更多的硬币。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-23

将硬币添加到角色和怪物图像小部件旁边

添加硬币的一种方法是固定它们在屏幕上的位置。在这个游戏中,位置是随机的。修改后的build()函数如清单 5-37 所示,其中一个for循环向窗口小部件树添加了五个硬币图像窗口小部件。注意,在类头中定义了一个名为num_coins的变量,它保存硬币部件的数量。

uniform()函数用于返回每枚硬币的 x 和 y 坐标,并考虑硬币在屏幕上的显示位置。我们这样做是通过从返回的随机数中减去宽度和高度。怪物的随机位置也是这样产生的。

import kivy.app
import kivy.animation
import kivy.uix.image
import random

class TestApp(kivy.app.App):
    character_killed = False
    num_coins = 5

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 build(self):
        coin_width = 0.05

        coin_height = 0.05

        for k in range(TestApp.num_coins):
            x = random.uniform(0, 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.add_widget(coin, index=-1)

    def on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            TestApp.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)

app = TestApp()
app.run()

Listing 5-37Adding Multiple Image Widgets Representing the Coins on the Screen

因为硬币的定位是随机的,所以在使用修改后的build()函数运行应用后,有可能大部分甚至全部硬币都在一个小区域内,如图 5-24 所示。我们需要保证每个硬币与下一个硬币之间的距离最小。该距离可以是水平的或垂直的。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-24

硬币可能彼此非常接近

放置硬币的方法是将屏幕分成与要添加的硬币数量相等的多个垂直部分。如图 5-25 所示。一枚硬币随机放在一个区域的任意位置。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-25

平分屏幕宽度以放置硬币

修改后的build()功能如清单 5-38 所示。因为屏幕是垂直分割的,每个部分将覆盖窗口的整个高度,但其宽度受到所用硬币数量的限制。因此,截面宽度在section_width变量中计算。

import kivy.app
import kivy.animation
import kivy.uix.image
import random

class TestApp(kivy.app.App):
    character_killed = False
    num_coins = 5
    coins_ids = {}

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 build(self):
        coin_width = 0.05
        coin_height = 0.05

        section_width = 1.0/TestApp.num_coins
        for k in range(TestApp.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, id="coin"+str(k))
            self.root.add_widget(coin, index=-1)
            TestApp.coins_ids['coin'+str(k)] = coin

    def on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            TestApp.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)

app = TestApp()
app.run()

Listing 5-38Splitting the Screen Width Uniformly to Add Multiple Image Widgets Representing the Coins

每枚硬币都可以放在该区域的边界内。因为对截面高度没有限制,硬币 y 坐标的计算如前所示。为了将硬币放置在该部分指定的宽度内,选择 x 坐标的范围被限制在它的开始和结束列。起始值由section_width*k定义,而结束值由section_width*(k+1)-coin_width定义。请注意,coin_width被减去,以确保硬币在截面边界内。

对于第一枚硬币,循环变量 k 值为 0,因此起始值为 0.0,但结束值为section_width-coin_width。给定section_width等于 0.2,coin_width等于 0.05,第一段的范围为 0.0:0.15 。对于第二枚硬币,k 将是 1,因此起始值是section_width,而结束值是section_width*2-coin_width。因此,第二段的范围是 0.2:0.35 。同样,其余部分的范围为 0.4:0.550.6:0.750.8:0.95

我们曾经使用根部件的ids字典来引用 Python 文件中的子部件。不幸的是,ids字典不包含对 Python 文件中动态添加的小部件的引用。为了以后能够引用这些小部件,它们的引用保存在类头中定义的coins_ids字典中。在字典中,每枚硬币都有一个字符串键,由从 0 开始的硬币编号后的单词coin组成。因此,这些键是coin0coin1coin2coin3coin4

图 5-26 显示了运行应用后的结果。硬币分布得更好。放置硬币后,下一步是允许玩家收集它们。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-26

将硬币均匀地分布在屏幕上

根据以下输出打印硬币位置:

{'coin0': <kivy.uix.image.Image object at 0x7f0c56ff4388>, 'coin1': <kivy.uix.image.Image object at 0x7f0c56ff44c0>, 'coin2': <kivy.uix.image.Image object at 0x7f0c56ff4590>, 'coin3': <kivy.uix.image.Image object at 0x7f0c56ff4660>, 'coin4': <kivy.uix.image.Image object at 0x7f0c56ff4730>}

收集硬币

为了收集硬币,我们需要检测角色和所有尚未收集的硬币之间的碰撞。为了在每次改变时访问字符位置,on_pos_hint事件被绑定到 KV 文件中的字符图像小部件。因此,修改后的 KV 文件列在清单 5-39 中。事件被赋予回调函数char_pos_hint()

FloatLayout:
    on_touch_down: app.touch_down_handler(args)

    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()
        on_pos_hint: app.monst_pos_hint()

    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()
        on_pos_hint: app.char_pos_hint()

Listing 5-39Adding the on_pos_hint Event to Return the Character Position

根据清单 5-40 中列出的 Python 文件中该函数的实现,它遍历字典中的条目(即硬币)并返回循环头中定义的coin_keycurr_coin变量中每个条目(即硬币)的键值。检测碰撞的方式与检测角色和怪物之间的碰撞的方式相同。

如果两个小部件的边界有交集,即使是在一行或一列中,collide_widget()也会返回True。为了对其进行调优,需要比较两个小部件的中心。如果中心之间的差异超过预定阈值,则表明发生了碰撞。

一旦角色和硬币发生碰撞,通过调用remove_widget()方法,硬币Image小部件将从小部件树中移除。这确保了小部件在被收集后变得隐藏。角色与硬币碰撞的检测类似于用怪物计算,除了在处理硬币时减少gab_xgab_y变量,因为它们的尺寸小于怪物的尺寸。

import kivy.app
import kivy.animation
import kivy.uix.image
import random

class TestApp(kivy.app.App):
    character_killed = False
    num_coins = 5
    coins_ids = {}

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos, anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 build(self):
        coin_width = 0.05
        coin_height = 0.05

        section_width = 1.0/TestApp.num_coins
        for k in range(TestApp.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.add_widget(coin, index=-1)
            TestApp.coins_ids['coin'+str(k)] = coin

    def on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            TestApp.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)

    def char_pos_hint(self):
        character_image = self.root.ids['character_image']
        character_center = character_image.center

        gab_x = character_image.width / 3
        gab_y = character_image.height / 3

        for coin_key, curr_coin in TestApp.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:
                print("Coin Collected", coin_key)
                self.root.remove_widget(curr_coin)

app = TestApp()
app.run()

Listing 5-40Handling the char_pos_hint() Function to Detect Collision with the Coins

以前的应用存在问题。即使在硬币从部件树中删除后,字典中仍然有一个条目。因此,即使收集了所有项目,循环也要经历五次迭代,并且表现得好像没有硬币没有被收集一样。

为了确保从字典中检测到小部件,我们可以跟踪在一个名为coins_to_delete的空列表中收集的硬币,该列表在char_pos_hint()函数中定义,如清单 5-41 所示。对于收集到的每枚硬币,使用append()函数将其在coins_ids字典中的关键字添加到列表中。

def char_pos_hint(self):
    character_image = self.root.ids['character_image']
    character_center = character_image.center

    gab_x = character_image.width / 3
    gab_y = character_image.height / 3
    coins_to_delete = []

    for coin_key, curr_coin in TestApp.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:
            print("Coin Collected", coin_key)
            coins_to_delete.append(coin_key)
            self.root.remove_widget(curr_coin)

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

Listing 5-41Removing the Coins Once They Are Collected

循环结束后,if语句根据列表的长度确定列表是否为空。如果其长度小于 1,则在前一个循环中没有收集到硬币,因此没有要从字典中删除的项目(即硬币)。如果列表的长度大于或等于 1(即大于 0),这意味着有一些来自前一循环的硬币。

为了从字典中删除硬币,一个for循环遍历列表中的元素。注意列表元素代表每枚硬币的钥匙,比如coin0。因此,存储在列表中的关键字将被用作字典的索引,以返回相关的硬币图像小部件。使用 Python 中的del命令,可以从字典中删除该条目。通过这样做,我们已经从部件树和字典中完全删除了硬币。收集完所有硬币后,字典中的条目数将为零,循环将无用。

完整级别

在之前的申请中,没有关于收集的硬币数量的指示。根据清单 5-42 所示的修改后的 KV 文件,在屏幕的左上角增加了一个小的Label控件来显示收集到的硬币数量。标签被赋予一个 IDnum_coins_collected,以便在 Python 代码中更改它的文本。

FloatLayout:
    on_touch_down: app.touch_down_handler(args)

    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

    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()
        on_pos_hint: app.monst_pos_hint()

    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()
        on_pos_hint: app.char_pos_hint()

Listing 5-42Displaying the Number of Collected Coins in a Label Widget Placed at the Top of the Screen

Python 文件内部的char_pos_hint()函数修改为根据当前收集的硬币数量更新添加的标签文本字段。文件如清单 5-43 所示。首先,在类中定义一个名为num_coins_collected的变量,并赋予其初始值 0。如果角色和任何硬币之间发生冲突,那么该变量增加 1,然后Label小部件更新。

因为完成收集所有硬币的任务就意味着当前关卡的结束,不如做点什么来表示关卡的结束。如果num_coins_collected变量中收集的硬币数量等于num_coins变量中的硬币数量,一个标签将被动态添加到小部件树中,并显示"Level Completed"消息。除了创建这个小部件,角色和怪物动画被取消。注意,通过取消怪物动画,它的位置不会改变,因此monst_pos_hint()回调函数不会被执行。

import kivy.app
import kivy.animation
import kivy.uix.image
import kivy.uix.label
import random

class TestApp(kivy.app.App):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 build(self):
        coin_width = 0.05
        coin_height = 0.05

        section_width = 1.0/TestApp.num_coins
        for k in range(TestApp.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.add_widget(coin, index=-1)
            TestApp.coins_ids['coin'+str(k)] = coin

    def on_start(self):
        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            TestApp.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)

    def char_pos_hint(self):
        character_image = self.root.ids['character_image']
        character_center = character_image.center

        gab_x = character_image.width / 3
        gab_y = character_image.height / 3
        coins_to_delete = []

        for coin_key, curr_coin in TestApp.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)
                self.root.remove_widget(curr_coin)
                TestApp.num_coins_collected = TestApp.num_coins_collected + 1

                self.root.ids['num_coins_collected'].text = "Coins "+str(TestApp.num_coins_collected)
                if TestApp.num_coins_collected == TestApp.num_coins:
                    kivy.animation.Animation.cancel_all(character_image)
                    kivy.animation.Animation.cancel_all(self.root.ids['monster_image'])
                    self.root.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 TestApp.coins_ids[coin_key]

app = TestApp()
app.run()

Listing 5-43Updating the Label Displaying the Number of Collected Coins and Displaying a Message When the Level Completes

图 5-27 显示关卡完成后的结果。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-27

当关卡完成时,会显示一条消息

音效

一个没有音效的游戏不是一个很好的游戏。声音是用户体验的一个重要因素。你可以给游戏中发生的每一个动作添加音效。对于我们的游戏,我们会在角色死亡时、完成一关时以及收集硬币时添加音效。这是对背景音乐的补充,有助于玩家参与游戏。

Kivy 提供了一个非常简单的接口,使用kivy.core.audio模块中的SoundLoader类来播放声音。清单 5-44 中显示了修改后的 Python 文件,声音在该文件中被加载和播放。

import kivy.app
import kivy.animation
import kivy.uix.image
import kivy.uix.label
import random
import kivy.core.audio
import os

class TestApp(kivy.app.App):
    character_killed = False
    num_coins = 5
    num_coins_collected = 0
    coins_ids = {}

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

    def monst_animation_completed(self, *args):
        monster_image = self.root.ids['monster_image']
        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(new_pos= new_pos,anim_duration=random.uniform(1.5, 3.5))

    def touch_down_handler(self, args):
        if TestApp.character_killed == False:
            self.start_char_animation(args[1].spos)

    def change_char_im(self):
        character_image = self.root.ids['character_image']
        character_image.source = str(int(character_image.im_num)) + ".png"

    def change_monst_im(self):
        monster_image = self.root.ids['monster_image']
        monster_image.source = str(int(monster_image.im_num)) + ".png"

    def start_char_animation(self, touch_pos):
        character_image = self.root.ids['character_image']
        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 start_monst_animation(self, new_pos, anim_duration):
        monster_image = self.root.ids['monster_image']
        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 build(self):
        coin_width = 0.05
        coin_height = 0.05

        section_width = 1.0/TestApp.num_coins
        for k in range(TestApp.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.add_widget(coin, index=-1)
            TestApp.coins_ids['coin'+str(k)] = coin

    def on_start(self):
        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()

        monster_image = self.root.ids['monster_image']
        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(new_pos=new_pos, anim_duration=random.uniform(1.5, 3.5))

    def monst_pos_hint(self):
        character_image = self.root.ids['character_image']
        monster_image = self.root.ids['monster_image']

        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:
            self.bg_music.stop()
            self.char_death_sound.play()
            TestApp.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)

    def char_pos_hint(self):
        character_image = self.root.ids['character_image']
        character_center = character_image.center

        gab_x = character_image.width / 3
        gab_y = character_image.height / 3
        coins_to_delete = []

        for coin_key, curr_coin in TestApp.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)
                self.root.remove_widget(curr_coin)
                TestApp.num_coins_collected = TestApp.num_coins_collected + 1
                self.root.ids['num_coins_collected'].text = "Coins "+str(TestApp.num_coins_collected)
                if TestApp.num_coins_collected == TestApp.num_coins:
                    self.bg_music.stop()
                    self.level_completed_sound.play()
                    kivy.animation.Animation.cancel_all(character_image)
                    kivy.animation.Animation.cancel_all(self.root.ids['monster_image'])
                    self.root.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 TestApp.coins_ids[coin_key]

app = TestApp()
app.run()

Listing 5-44Adding Sound Effects to the Game

播放声音文件有两个步骤。我们必须首先使用SoundLoader类的load()方法加载声音文件。这个方法接受在music_dir变量中指定的声音文件路径。该变量使用os模块通过os.getcwd()函数返回当前目录。假设声音文件存储在当前目录下名为music的文件夹中,文件的完整路径是os.getcwd()和名为music的文件之间的连接。

所有声音文件都是在应用的TestApp类的on_start()方法中准备的。背景声音文件被加载到bg_music变量中。收集硬币、角色死亡和关卡完成的声音文件分别存储在变量coin_soundchar_death_soundlevel_completed_sound中。注意,这些变量中的每一个都与引用当前对象的self相关联。这有助于在on_start()方法之外控制声音文件。当引用该方法之外的声音文件时,记得使用self

第二步是使用play()方法播放文件。对于背景音乐,在on_start()方法内播放。在char_pos_hint()回调函数中与硬币发生碰撞后,会发出硬币声音。

收集完所有硬币后会播放关卡完成声音。因为关卡已经完成,不再需要背景音乐,因此通过调用stop()方法来停止。

最后与怪物发生碰撞后在monst_pos_hint()回调函数内部播放角色死亡音。加入音效后玩游戏比以前更有趣。

游戏背景

我们可以改变游戏的背景,使之更吸引人,而不是默认的黑色背景。您可以使用纹理、动画图像或静态图像作为背景。

根据清单 5-45 中所示的 KV 文件,游戏背景采用静态图像。使用canvas.beforeFloatLayout内绘制。这保证了图像将覆盖整个窗口。

FloatLayout:
    on_touch_down: app.touch_down_handler(args)
    canvas.before:
        Rectangle:
            size: self.size
            pos: self.pos
            source: 'bg.jpg'

    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

    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()
        on_pos_hint: app.monst_pos_hint()

    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()
        on_pos_hint: app.char_pos_hint()

Listing 5-45Adding a Background Image to the Game

图 5-28 添加背景后的游戏。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-28

向屏幕添加背景图像

游戏开发概述

这个游戏目前只有一个关卡,我们需要增加更多的关卡。在添加更多的关卡之前,对游戏开发到目前为止的进展有一个总体的了解是很重要的。

图 5-29 显示了游戏执行的流程。因为我们的 Kivy 应用实现了build()on_start()方法,根据 Kivy 应用的生命周期,它们将在我们的任何自定义函数之前执行。这从build()功能开始,直到游戏结束,因为角色被杀死或者所有的硬币被收集,关卡完成。每个功能都按执行顺序列出,直到游戏结束,其任务列在右边。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-29

游戏执行的流程

图 5-30 列出了处理服务于角色和怪物动画的事件的回调函数。它还列出了前一个应用中使用的五个类变量。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图 5-30

处理角色和怪物动画的类变量和回调函数的概要

从图 5-30 中,你可以看到怪物的运行需要下面列出的四个函数。请注意,该角色使用的函数与这些函数类似,但名称有所不同。

  • start_monst_animation()

  • change_monst_im()

  • monst_pos_hint()

  • monst_animation_completed()

摘要

该游戏现在有一个角色,使用动画,根据触摸位置移动。一个怪物随机移动,也使用动画。角色在与怪物相撞时被杀死。当它被杀死时,会启动一个一次性动画,改变角色的图像以反映死亡。一些硬币均匀地分布在屏幕上,玩家的任务是收集所有的硬币。屏幕上方的标签显示收集的硬币数量。当角色和硬币发生碰撞时,硬币消失,标签更新。当所有的硬币被收集,水平是完整的。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值