kivy python_另一款带有Kivy,Python的Android蛇

kivy python

A lot of people want to start programming apps for Android, but they prefer not to use Android Studio and/or Java. Why? Because it's an overkill. «I just wanna create Snake and nothing more!»

许多人想开始为Android编写应用程序,但他们不希望不使用Android Studio和/或Java。 为什么? 因为这太过分了。 «我只想创建Snake而已!»

Let's snake without java! (with a bonus at the end)

让我们在没有Java的情况下走蛇! (最后有奖金)

为什么还要创建另一个蛇教程? (Why create yet another snake tutorial?)

this (Eng) or its 了(Eng)或其 translation (Rus). So did I. Unfortunately, I found the article extremely useless because: 翻译(Rus) 。 我也是。不幸的是,我发现这篇文章极其无用,因为:
他们的代码不好 (Their code is bad)

Minor issues:

次要问题:

  1. Using «tile» and «head» instead of «tiles» or «cells» doesn't make sense to me. The head is not distinct enough from the tile to be different variables.

    对我来说,用“ tile”和“ head”代替“ tiles”或“ cells”是没有意义的。 磁头与磁贴的区别不够大,无法成为不同的变量。
  2. Clock.schedule for self.update is called from… self.update.

    用于self.update的Clock.schedule从... self.update调用。
  3. Second-level class Playground is implemented in the beginning, while first-level class SnakeApp is implemented at the end.

    第二层类Playground在开头实现,而第一层SnakeApp在结尾实现。
  4. Names for directions («up», «down», ...) are used instead of vectors ( (0, 1), (1, 0)… ).

    使用方向名称(«上»,«下»,...)代替向量((0,1),(1,0)…)。

Major issues:

主要问题:

  1. Most dynamic objects (for example, the fruit) are attached to the kv file, so you can't make more than 1 apple because then you should rewrite this part

    大多数动态对象(例如水果)都附加到了kv文件中,因此您制作的苹果不能超过1个,因为这时您应该重写此部分
  2. Weird logic for the snake's motion instead of «cell-after-cell» motion.

    蛇的动作而不是“ cell-after-cell”动作的怪异逻辑。
  3. The code is very long being more than 350 lines.

    该代码很长,超过350行。
对于新手来说,这篇文章不清楚 (The article is not clear for novices)

This is my PERSONAL opinion. Moreover, I do not guarantee that my tutorial will be more interesting and clear. But I'll do my best and, for my article, guarantee that:

这是我个人的看法。 而且,我不保证我的教程会更加有趣和清晰。 但是,我会竭尽所能,并在我的文章中保证:

  1. The code is short

    代码很短
  2. The snake is nice

    蛇很好
  3. The instructions will have clear step-by-step implementations and smooth progression from «hello, world» to a ready snake with transitional steps.

    这些说明将具有清晰的分步实施方式,并通过过渡步骤从“ hello,world”顺利过渡到现成的蛇。
不良结果 (The undesirable result)

No space between the cells, the triangle is embarrassing, graphics are glitchy.

单元格之间没有空间,三角形令人尴尬,图形出现故障。

熟悉 (Get familiarized)

第一个应用 (First app)

Please, confirm that you have already installed Kivy (if not, follow the instructions) and ran

请确认您已安装Kivy(如果没有,请按照说明进行操作 )并运行


在项目目录中的 buildozer init in the project directory. buildozer init

Let's run our first app:

让我们运行第一个应用程序:

main.py

main.py

from kivy.app import App
from kivy.uix.widget import Widget

class WormApp(App):
    def build(self):
        return Widget()

if __name__ == '__main__':
    WormApp().run()

We created a Widget. Analogously, we can create a button or any other UI element:

我们创建了一个小部件。 类似地,我们可以创建一个按钮或任何其他UI元素:

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button

class WormApp(App):
    def build(self):
        self.but = Button()
        self.but.pos = (100, 100)
        self.but.size = (200, 200)
        self.but.text = "Hello, cruel world"

        self.form = Widget()
        self.form.add_widget(self.but)
        return self.form

if __name__ == '__main__':
    WormApp().run()

Wow! Congratulations! You've created a button!

哇! 恭喜你! 您已经创建了一个按钮!

.kv文件 (.kv files)

However, there's another way to create UI elements. First, we implement our form:

但是,还有另一种创建UI元素的方法。 首先,我们实现以下形式:

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.uix.button import Button


class Form(Widget):
    def __init__(self):
        super().__init__()
        self.but1 = Button()
        self.but1.pos = (100, 100)
        self.add_widget(self.but1)


class WormApp(App):
    def build(self):
        self.form = Form()
        return self.form


if __name__ == '__main__':
    WormApp().run()

Then, we create «worm.kv» file.

然后,我们创建«worm.kv»文件。

worm.kv

蠕虫病毒

<Form>:
    but2: but_id

    Button:
        id: but_id
        pos: (200, 200)

What just happened? We created another Button and assign id but_id. Then, but_id was matched to but2 of the form. It means that now we can refer to this button by but2

刚才发生了什么? 我们创建了另一个Button并分配ID but_id。 然后,将but_id与该格式的but2匹配。 这意味着现在我们可以通过but2来引用此按钮

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.but1 = Button()
        self.but1.pos = (100, 100)
        self.add_widget(self.but1)   #
        self.but2.text = "OH MY"

图形 (Graphics)

What we do next is creating a graphical element. First, we implement it in worm.kv:

接下来,我们将创建一个图形元素。 首先,我们在worm.kv中实现它:

<Form>:

<Cell>:
    canvas:
        Rectangle:
            size: self.size
            pos: self.pos

We linked the rectangle's position to self.pos and its size to self.size. So now those properties are available from Cell, for example, once we create a cell, we can do

我们将矩形的位置链接到self.pos并将其大小链接到self.size。 因此,现在可以从Cell中获得这些属性,例如,一旦创建了一个单元,就可以

class Cell(Widget):
    def __init__(self, x, y, size):
        super().__init__()
        self.size = (size, size)   # As you can see, we can change self.size which is "size" property of a rectangle
        self.pos = (x, y)

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cell = Cell(100, 100, 30)
        self.add_widget(self.cell)

Ok, we have created a cell.

好的,我们已经创建了一个单元。

必要方法 (Necessary methods)

Let's try to move it. To do that, we should add Form.update function and schedule it.

让我们尝试移动它。 为此,我们应该添加Form.update函数并安排它。

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock

class Cell(Widget):
    def __init__(self, x, y, size):
        super().__init__()
        self.size = (size, size)
        self.pos = (x, y)


class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cell = Cell(100, 100, 30)
        self.add_widget(self.cell)

    def start(self):
        Clock.schedule_interval(self.update, 0.01)

    def update(self, _):
        self.cell.pos = (self.cell.pos[0] + 2, self.cell.pos[1] + 3)


class WormApp(App):
    def build(self):
        self.form = Form()
        self.form.start()
        return self.form


if __name__ == '__main__':
    WormApp().run()

The cell will move across the form. As you can see, we can schedule any function with Clock.

单元格将跨表格移动。 如您所见,我们可以使用Clock安排任何功能。

Next, let's make a touch event. Rewrite Form:

接下来,让我们进行触摸事件。 重写表格:

class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cells = []

    def start(self):
        Clock.schedule_interval(self.update, 0.01)

    def update(self, _):
        for cell in self.cells:
            cell.pos = (cell.pos[0] + 2, cell.pos[1] + 3)

    def on_touch_down(self, touch):
        cell = Cell(touch.x, touch.y, 30)
        self.add_widget(cell)
        self.cells.append(cell)

Each touch_down creates a cell with coordinates = (touch.x, touch.y) and size of 30. Then, we add it as a widget of the form AND to our own array (in order to easily access them).

每个touch_down都会创建一个坐标=(touch.x,touch.y)且大小为30的单元格。然后,将其作为AND形式的小部件添加到我们自己的数组中(以便于访问它们)。

Now you can tap on your form and generate cells.

现在,您可以点击表格并生成单元格。

整洁的设置 (Neat settings)

Because we want to get a nice snake, we should distinguish the graphical positions and the actual positions of cells.

因为我们想得到一条漂亮的蛇,所以我们应该区分图形位置和单元格的实际位置。

为什么? (Why?)

P. S. We wouldn't do it if we dealt with classical on_draw. But here we don't have to program on_draw.

PS:如果我们处理经典的on_draw,我们不会这样做。 但是这里我们不必编程on_draw。

Let's fix the worm.kv file:

让我们修复worm.kv文件:

<Form>:

<Cell>:
    canvas:
        Rectangle:
            size: self.graphical_size
            pos: self.graphical_pos

and main.py:

和main.py:

...
from kivy.properties import *
...
class Cell(Widget):
    graphical_size = ListProperty([1, 1])
    graphical_pos = ListProperty([1, 1])

    def __init__(self, x, y, size, margin=4):
        super().__init__()
        self.actual_size = (size, size)
        self.graphical_size = (size - margin, size - margin)
        self.margin = margin
        self.actual_pos = (x, y)
        self.graphical_pos_attach()

    def graphical_pos_attach(self):
        self.graphical_pos = (self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2)
...
class Form(Widget):
    def __init__(self):
        super().__init__()
        self.cell1 = Cell(100, 100, 30)
        self.cell2 = Cell(130, 100, 30)
        self.add_widget(self.cell1)
        self.add_widget(self.cell2)
...

The margin appeared so it looks pretty although we created the second cell with X = 130 instead of 132. Later, we will make smooth motion based on the distance between actual_pos and graphical_pos.

尽管我们用X = 130而不是132创建了第二个像元,但出现了边距,因此看起来很漂亮。稍后,我们将基于actual_pos和graphics_pos之间的距离进行平滑运动。

编码蠕虫 (Coding the worm)

实作 (Implementation)

Init config in main.py

main.py中的初始化配置

class Config:
    DEFAULT_LENGTH = 20
    CELL_SIZE = 25
    APPLE_SIZE = 35
    MARGIN = 4
    INTERVAL = 0.2
    DEAD_CELL = (1, 0, 0, 1)
    APPLE_COLOR = (1, 1, 0, 1)

(Trust me, you'll love it!)

(相信我,您会爱上它!)

Then, assign config to the app:

然后,为应用分配配置:

class WormApp(App):
    def __init__(self):
        super().__init__()
        self.config = Config()
        self.form = Form(self.config)
    
    def build(self):
        self.form.start()
        return self.form

Rewrite init and start:

重写init并开始:

class Form(Widget):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None

    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        Clock.schedule_interval(self.update, self.config.INTERVAL)

Then, the Cell:

然后,该单元格:

class Cell(Widget):
    graphical_size = ListProperty([1, 1])
    graphical_pos = ListProperty([1, 1])

    def __init__(self, x, y, size, margin=4):
        super().__init__()
        self.actual_size = (size, size)
        self.graphical_size = (size - margin, size - margin)
        self.margin = margin
        self.actual_pos = (x, y)
        self.graphical_pos_attach()

    def graphical_pos_attach(self):
        self.graphical_pos = (self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2)

    def move_to(self, x, y):
        self.actual_pos = (x, y)
        self.graphical_pos_attach()

    def move_by(self, x, y, **kwargs):
        self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs)

    def get_pos(self):
        return self.actual_pos

    def step_by(self, direction, **kwargs):
        self.move_by(self.actual_size[0] * direction[0], self.actual_size[1] * direction[1], **kwargs)

Hopefully, it's more or less clear.

希望它或多或少清楚。

and finally the Worm:

最后是蠕虫:

class Worm(Widget):
    def __init__(self, config):
        super().__init__()
        self.cells = []
        self.config = config
        self.cell_size = config.CELL_SIZE
        self.head_init((100, 100))
        for i in range(config.DEFAULT_LENGTH):
            self.lengthen()

    def destroy(self):
        for i in range(len(self.cells)):
            self.remove_widget(self.cells[i])
        self.cells = []

    def lengthen(self, pos=None, direction=(0, 1)):
        # If pos is set, we put the cell in pos, otherwise accordingly to the specified direction
        if pos is None:
            px = self.cells[-1].get_pos()[0] + direction[0] * self.cell_size
            py = self.cells[-1].get_pos()[1] + direction[1] * self.cell_size
            pos = (px, py)
        self.cells.append(Cell(*pos, self.cell_size, margin=self.config.MARGIN))
        self.add_widget(self.cells[-1])

    def head_init(self, pos):
        self.lengthen(pos=pos)

Let's give life to our wormie.

让我们为蠕虫赋予生命。

运动 (Motion)

Now we will make it move.

现在,我们将其移动。

It's simple:

这很简单:

class Worm(Widget):
...
    def move(self, direction):
        for i in range(len(self.cells) - 1, 0, -1):
            self.cells[i].move_to(*self.cells[i - 1].get_pos())
        self.cells[0].step_by(direction)
class Form(Widget):
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)

    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        self.cur_dir = (1, 0)
        Clock.schedule_interval(self.update, self.config.INTERVAL)

    def update(self, _):
        self.worm.move(self.cur_dir)

It's alive! It's alive!

它还活着! 它还活着!

控制性 (Controlling)

As you could judge by the preview image, the controls of the snake will be the following:

通过预览图像可以判断,蛇的控件如下:

class Form(Widget):
...
    def on_touch_down(self, touch):
        ws = touch.x / self.size[0]
        hs = touch.y / self.size[1]
        aws = 1 - ws
        if ws > hs and aws > hs:
            cur_dir = (0, -1)         # Down
        elif ws > hs >= aws:
            cur_dir = (1, 0)          # Right
        elif ws <= hs < aws:
            cur_dir = (-1, 0)         # Left
        else:
            cur_dir = (0, 1)           # Up
        self.cur_dir = cur_dir

Cool.

凉。

创造水果 (Creating the fruit)

First, we initialize it.

首先,我们对其进行初始化。

class Form(Widget):
...
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)
        self.fruit = None
...
    def random_cell_location(self, offset):
        x_row = self.size[0] // self.config.CELL_SIZE
        x_col = self.size[1] // self.config.CELL_SIZE
        return random.randint(offset, x_row - offset), random.randint(offset, x_col - offset)

    def random_location(self, offset):
        x_row, x_col = self.random_cell_location(offset)
        return self.config.CELL_SIZE * x_row, self.config.CELL_SIZE * x_col

    def fruit_dislocate(self):
        x, y = self.random_location(2)
        self.fruit.move_to(x, y)
...
    def start(self):
        self.fruit = Cell(0, 0, self.config.APPLE_SIZE, self.config.MARGIN)
        self.worm = Worm(self.config)
        self.fruit_dislocate()
        self.add_widget(self.worm)
        self.add_widget(self.fruit)
        self.cur_dir = (1, 0)
        Clock.schedule_interval(self.update, self.config.INTERVAL)

The current result:

当前结果:

Now we should implement some Worm methods:

现在我们应该实现一些蠕虫方法:

class Worm(Widget):
...
    # Here we get all the positions of our cells
    def gather_positions(self):
        return [cell.get_pos() for cell in self.cells]
    # Just check if our head has the same position as another Cell
    def head_intersect(self, cell):
        return self.cells[0].get_pos() == cell.get_pos()

集合位置的其他好处 (Other benefits of gather_positions)

class Form(Widget):
    def fruit_dislocate(self):
        x, y = self.random_location(2)
        while (x, y) in self.worm.gather_positions():
            x, y = self.random_location(2)
        self.fruit.move_to(x, y)

At this point, the fruit won't be located in the worm's tile.

此时,水果不会位于蠕虫的瓷砖中。

...and add this check to update()

...并将此检查添加到update()

class Form(Widget):
...
    def update(self, _):
        self.worm.move(self.cur_dir)
        if self.worm.head_intersect(self.fruit):
            directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
            self.worm.lengthen(direction=random.choice(directions))
            self.fruit_dislocate()

检测自身瓷砖撞击 (Detection of self tile hitting)

We want to know whether the head has the same position as one of the worm's cells.

我们想知道头部与蠕虫细胞之一的位置是否相同。

class Form(Widget):
...
    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)
        self.fruit = None
        self.game_on = True

    def update(self, _):
        if not self.game_on:
            return
        self.worm.move(self.cur_dir)
        if self.worm.head_intersect(self.fruit):
            directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
            self.worm.lengthen(direction=random.choice(directions))
            self.fruit_dislocate()
       if self.worm_bite_self():
            self.game_on = False

    def worm_bite_self(self):
        for cell in self.worm.cells[1:]:
            if self.worm.head_intersect(cell):
                return cell
        return False

着色,修饰和代码重构 (Coloring, decorating, and code refactoring)

Let's start with code refactoring.

让我们从代码重构开始。

Rewrite and add

重写并添加

class Form(Widget):
...
    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        if self.fruit is not None:
            self.remove_widget(self.fruit)
        self.fruit = Cell(0, 0, self.config.APPLE_SIZE)
        self.fruit_dislocate()
        self.add_widget(self.fruit)
        Clock.schedule_interval(self.update, self.config.INTERVAL)
        self.game_on = True
        self.cur_dir = (0, -1)

    def stop(self):
        self.game_on = False
        Clock.unschedule(self.update)

    def game_over(self):
        self.stop()
...
    def on_touch_down(self, touch):
        if not self.game_on:
            self.worm.destroy()
            self.start()
            return
        ...

Now if the worm is dead (frozen), if you tap again, the game will be reset.

现在,如果蠕虫已死(冻结),再次单击该蠕虫,将重置游戏。

Now let's go to decorating and coloring.

现在让我们去装饰和着色。

worm.kv

蠕虫病毒

<Form>:
    popup_label: popup_label
    score_label: score_label

    canvas:
        Color:
            rgba: (.5, .5, .5, 1.0)

        Line:
            width: 1.5
            points: (0, 0), self.size

        Line:
            width: 1.5
            points: (self.size[0], 0), (0, self.size[1])


    Label:
        id: score_label
        text: "Score: " + str(self.parent.worm_len)
        width: self.width

    Label:
        id: popup_label
        width: self.width


<Worm>:


<Cell>:
    canvas:
        Color:
            rgba: self.color
        Rectangle:
            size: self.graphical_size
            pos: self.graphical_pos

Rewrite WormApp:

重写WormApp:

class WormApp(App):
    def build(self):
        self.config = Config()
        self.form = Form(self.config)
        return self.form

    def on_start(self):
        self.form.start()

Let's color it. Rewrite Cell in .kv:

让我们上色吧。 重写.kv中的单元格:

<Cell>:
    canvas:
        Color:
            rgba: self.color

        Rectangle:
            size: self.graphical_size
            pos: self.graphical_pos

Add this to Cell.__init__

将此添加到Cell .__ init__

self.color = (0.2, 1.0, 0.2, 1.0)    #

and this to Form.start

然后到Form.start

self.fruit.color = (1.0, 0.2, 0.2, 1.0)

Great, enjoy your snake

太好了,享受你的蛇

Finally, we will make a «game over» label

最后,我们将制作一个“游戏结束”标签

class Form(Widget):
...
    def __init__(self, config):
    ...
        self.popup_label.text = ""
...
    def stop(self, text=""):
        self.game_on = False
        self.popup_label.text = text
        Clock.unschedule(self.update)

    def game_over(self):
        self.stop("GAME OVER" + " " * 5 + "\ntap to reset")

and make the hit cell red:

并将命中单元格设为红色:

instead of

代替

def update(self, _):
    ...
        if self.worm_bite_self():
            self.game_over()
    ...

write

def update(self, _):
        cell = self.worm_bite_self()
        if cell:
            cell.color = (1.0, 0.2, 0.2, 1.0)
            self.game_over()

Are you still paying attention? Coming next is the most interesting part.

您还在注意吗? 接下来是最有趣的部分。

奖励部分-平稳运动 (Bonus section — smooth motion)

Because the worm's step is equal to the cell_size, it's not that smooth. But we want to make it step as frequently as possible, without rewriting the entire logic of the game. So, we need to create a mechanism moving our graphical poses but not our actual poses. So, I wrote a simple file:

因为蠕虫的步长等于cell_size,所以它并不那么平滑。 但是我们希望使其步伐尽可能频繁,而不必重写游戏的全部逻辑。 因此,我们需要创建一种机制来移动图形姿势而不是实际姿势。 所以,我写了一个简单的文件:

smooth.py

光滑的

from kivy.clock import Clock
import time


class Timing:
    @staticmethod
    def linear(x):
        return x

class Smooth:
    def __init__(self, interval=1.0/60.0):
        self.objs = []
        self.running = False
        self.interval = interval

    def run(self):
        if self.running:
            return
        self.running = True
        Clock.schedule_interval(self.update, self.interval)

    def stop(self):
        if not self.running:
            return
        self.running = False
        Clock.unschedule(self.update)

    def setattr(self, obj, attr, value):
        exec("obj." + attr + " = " + str(value))

    def getattr(self, obj, attr):
        return float(eval("obj." + attr))

    def update(self, _):
        cur_time = time.time()
        for line in self.objs:
            obj, prop_name_x, prop_name_y, from_x, from_y, to_x, to_y, start_time, period, timing = line
            time_gone = cur_time - start_time
            if time_gone >= period:
                self.setattr(obj, prop_name_x, to_x)
                self.setattr(obj, prop_name_y, to_y)
                self.objs.remove(line)
            else:
                share = time_gone / period
                acs = timing(share)
                self.setattr(obj, prop_name_x, from_x * (1 - acs) + to_x * acs)
                self.setattr(obj, prop_name_y, from_y * (1 - acs) + to_y * acs)
        if len(self.objs) == 0:
            self.stop()

    def move_to(self, obj, prop_name_x, prop_name_y, to_x, to_y, t, timing=Timing.linear):
        self.objs.append((obj, prop_name_x, prop_name_y, self.getattr(obj, prop_name_x), self.getattr(obj, prop_name_y), to_x,
                          to_y, time.time(), t, timing))
        self.run()


class XSmooth(Smooth):
    def __init__(self, props, timing=Timing.linear, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.props = props
        self.timing = timing

    def move_to(self, obj, to_x, to_y, t):
        super().move_to(obj, *self.props, to_x, to_y, t, timing=self.timing)

对于那些不喜欢我的代码的人 (To those who dislike my code)

So you just create smooth.py and copy-paste this code to the file.

因此,您只需创建smooth.py并将此代码复制粘贴到文件中即可。

Finally, let's make it work:

最后,让它工作:

class Form(Widget):
...
    def __init__(self, config):
    ...
        self.smooth = smooth.XSmooth(["graphical_pos[0]", "graphical_pos[1]"])

Then we replace self.worm.move() with

然后我们将self.worm.move()替换为

class Form(Widget):
...
    def update(self, _):
    ...
        self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL))

And this is how methods of Cell should look like

这就是Cell方法的外观

class Cell(Widget):
...
    def graphical_pos_attach(self, smooth_motion=None):
        to_x, to_y = self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2
        if smooth_motion is None:
            self.graphical_pos = to_x, to_y
        else:
            smoother, t = smooth_motion
            smoother.move_to(self, to_x, to_y, t)

    def move_to(self, x, y, **kwargs):
        self.actual_pos = (x, y)
        self.graphical_pos_attach(**kwargs)

    def move_by(self, x, y, **kwargs):
        self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs)

That's it, thank you for your attention!

就是这样,谢谢您的关注!

How the final result works:

最终结果如何工作:

最终代码 (The final code)

main.py (main.py)

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock
from kivy.properties import *
import random
import smooth


class Cell(Widget):
    graphical_size = ListProperty([1, 1])
    graphical_pos = ListProperty([1, 1])
    color = ListProperty([1, 1, 1, 1])

    def __init__(self, x, y, size, margin=4):
        super().__init__()
        self.actual_size = (size, size)
        self.graphical_size = (size - margin, size - margin)
        self.margin = margin
        self.actual_pos = (x, y)
        self.graphical_pos_attach()
        self.color = (0.2, 1.0, 0.2, 1.0)

    def graphical_pos_attach(self, smooth_motion=None):
        to_x, to_y = self.actual_pos[0] - self.graphical_size[0] / 2, self.actual_pos[1] - self.graphical_size[1] / 2
        if smooth_motion is None:
            self.graphical_pos = to_x, to_y
        else:
            smoother, t = smooth_motion
            smoother.move_to(self, to_x, to_y, t)

    def move_to(self, x, y, **kwargs):
        self.actual_pos = (x, y)
        self.graphical_pos_attach(**kwargs)

    def move_by(self, x, y, **kwargs):
        self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y, **kwargs)

    def get_pos(self):
        return self.actual_pos

    def step_by(self, direction, **kwargs):
        self.move_by(self.actual_size[0] * direction[0], self.actual_size[1] * direction[1], **kwargs)


class Worm(Widget):
    def __init__(self, config):
        super().__init__()
        self.cells = []
        self.config = config
        self.cell_size = config.CELL_SIZE
        self.head_init((100, 100))
        for i in range(config.DEFAULT_LENGTH):
            self.lengthen()

    def destroy(self):
        for i in range(len(self.cells)):
            self.remove_widget(self.cells[i])
        self.cells = []

    def lengthen(self, pos=None, direction=(0, 1)):
        if pos is None:
            px = self.cells[-1].get_pos()[0] + direction[0] * self.cell_size
            py = self.cells[-1].get_pos()[1] + direction[1] * self.cell_size
            pos = (px, py)
        self.cells.append(Cell(*pos, self.cell_size, margin=self.config.MARGIN))
        self.add_widget(self.cells[-1])

    def head_init(self, pos):
        self.lengthen(pos=pos)

    def move(self, direction, **kwargs):
        for i in range(len(self.cells) - 1, 0, -1):
            self.cells[i].move_to(*self.cells[i - 1].get_pos(), **kwargs)
        self.cells[0].step_by(direction, **kwargs)

    def gather_positions(self):
        return [cell.get_pos() for cell in self.cells]

    def head_intersect(self, cell):
        return self.cells[0].get_pos() == cell.get_pos()


class Form(Widget):
    worm_len = NumericProperty(0)

    def __init__(self, config):
        super().__init__()
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)
        self.fruit = None
        self.game_on = True
        self.smooth = smooth.XSmooth(["graphical_pos[0]", "graphical_pos[1]"])

    def random_cell_location(self, offset):
        x_row = self.size[0] // self.config.CELL_SIZE
        x_col = self.size[1] // self.config.CELL_SIZE
        return random.randint(offset, x_row - offset), random.randint(offset, x_col - offset)

    def random_location(self, offset):
        x_row, x_col = self.random_cell_location(offset)
        return self.config.CELL_SIZE * x_row, self.config.CELL_SIZE * x_col

    def fruit_dislocate(self):
        x, y = self.random_location(2)
        while (x, y) in self.worm.gather_positions():
            x, y = self.random_location(2)
        self.fruit.move_to(x, y)

    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        if self.fruit is not None:
            self.remove_widget(self.fruit)
        self.fruit = Cell(0, 0, self.config.APPLE_SIZE)
        self.fruit.color = (1.0, 0.2, 0.2, 1.0)
        self.fruit_dislocate()
        self.add_widget(self.fruit)
        self.game_on = True
        self.cur_dir = (0, -1)
        Clock.schedule_interval(self.update, self.config.INTERVAL)
        self.popup_label.text = ""

    def stop(self, text=""):
        self.game_on = False
        self.popup_label.text = text
        Clock.unschedule(self.update)

    def game_over(self):
        self.stop("GAME OVER" + " " * 5 + "\ntap to reset")

    def align_labels(self):
        try:
            self.popup_label.pos = ((self.size[0] - self.popup_label.width) / 2, self.size[1] / 2)
            self.score_label.pos = ((self.size[0] - self.score_label.width) / 2, self.size[1] - 80)
        except:
            print(self.__dict__)
            assert False

    def update(self, _):
        if not self.game_on:
            return
        self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL))
        if self.worm.head_intersect(self.fruit):
            directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
            self.worm.lengthen(direction=random.choice(directions))
            self.fruit_dislocate()
        cell = self.worm_bite_self()
        if cell:
            cell.color = (1.0, 0.2, 0.2, 1.0)
            self.game_over()
        self.worm_len = len(self.worm.cells)
        self.align_labels()

    def on_touch_down(self, touch):
        if not self.game_on:
            self.worm.destroy()
            self.start()
            return
        ws = touch.x / self.size[0]
        hs = touch.y / self.size[1]
        aws = 1 - ws
        if ws > hs and aws > hs:
            cur_dir = (0, -1)
        elif ws > hs >= aws:
            cur_dir = (1, 0)
        elif ws <= hs < aws:
            cur_dir = (-1, 0)
        else:
            cur_dir = (0, 1)
        self.cur_dir = cur_dir

    def worm_bite_self(self):
        for cell in self.worm.cells[1:]:
            if self.worm.head_intersect(cell):
                return cell
        return False


class Config:
    DEFAULT_LENGTH = 20
    CELL_SIZE = 25
    APPLE_SIZE = 35
    MARGIN = 4
    INTERVAL = 0.3
    DEAD_CELL = (1, 0, 0, 1)
    APPLE_COLOR = (1, 1, 0, 1)


class WormApp(App):
    def build(self):
        self.config = Config()
        self.form = Form(self.config)
        return self.form

    def on_start(self):
        self.form.start()


if __name__ == '__main__':
    WormApp().run()

光滑的 (smooth.py)

from kivy.clock import Clock
import time


class Timing:
    @staticmethod
    def linear(x):
        return x


class Smooth:
    def __init__(self, interval=1.0/60.0):
        self.objs = []
        self.running = False
        self.interval = interval

    def run(self):
        if self.running:
            return
        self.running = True
        Clock.schedule_interval(self.update, self.interval)

    def stop(self):
        if not self.running:
            return
        self.running = False
        Clock.unschedule(self.update)

    def setattr(self, obj, attr, value):
        exec("obj." + attr + " = " + str(value))

    def getattr(self, obj, attr):
        return float(eval("obj." + attr))

    def update(self, _):
        cur_time = time.time()
        for line in self.objs:
            obj, prop_name_x, prop_name_y, from_x, from_y, to_x, to_y, start_time, period, timing = line
            time_gone = cur_time - start_time
            if time_gone >= period:
                self.setattr(obj, prop_name_x, to_x)
                self.setattr(obj, prop_name_y, to_y)
                self.objs.remove(line)
            else:
                share = time_gone / period
                acs = timing(share)
                self.setattr(obj, prop_name_x, from_x * (1 - acs) + to_x * acs)
                self.setattr(obj, prop_name_y, from_y * (1 - acs) + to_y * acs)
        if len(self.objs) == 0:
            self.stop()

    def move_to(self, obj, prop_name_x, prop_name_y, to_x, to_y, t, timing=Timing.linear):
        self.objs.append((obj, prop_name_x, prop_name_y, self.getattr(obj, prop_name_x), self.getattr(obj, prop_name_y), to_x,
                          to_y, time.time(), t, timing))
        self.run()


class XSmooth(Smooth):
    def __init__(self, props, timing=Timing.linear, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.props = props
        self.timing = timing

    def move_to(self, obj, to_x, to_y, t):
        super().move_to(obj, *self.props, to_x, to_y, t, timing=self.timing)

蠕虫病毒 (worm.kv)

<Form>:
    popup_label: popup_label
    score_label: score_label

    canvas:
        Color:
            rgba: (.5, .5, .5, 1.0)

        Line:
            width: 1.5
            points: (0, 0), self.size

        Line:
            width: 1.5
            points: (self.size[0], 0), (0, self.size[1])
    Label:
        id: score_label
        text: "Score: " + str(self.parent.worm_len)
        width: self.width

    Label:
        id: popup_label
        width: self.width

<Worm>:


<Cell>:
    canvas:
        Color:
            rgba: self.color

        Rectangle:
            size: self.graphical_size
            pos: self.graphical_pos

代码,由@tshirtman调整 (Code, adjusted by @tshirtman)

main.py (main.py)

from kivy.app import App
from kivy.uix.widget import Widget
from kivy.clock import Clock
from kivy.properties import *
import random
import smooth


class Cell:
    def __init__(self, x, y):
        self.actual_pos = (x, y)

    def move_to(self, x, y):
        self.actual_pos = (x, y)

    def move_by(self, x, y):
        self.move_to(self.actual_pos[0] + x, self.actual_pos[1] + y)

    def get_pos(self):
        return self.actual_pos


class Fruit(Cell):
    def __init__(self, x, y):
        super().__init__(x, y)


class Worm(Widget):
    margin = NumericProperty(4)
    graphical_poses = ListProperty()
    inj_pos = ListProperty([-1000, -1000])
    graphical_size = NumericProperty(0)

    def __init__(self, config, **kwargs):
        super().__init__(**kwargs)
        self.cells = []
        self.config = config
        self.cell_size = config.CELL_SIZE
        self.head_init((self.config.CELL_SIZE * random.randint(3, 5), self.config.CELL_SIZE * random.randint(3, 5)))
        self.margin = config.MARGIN
        self.graphical_size = self.cell_size - self.margin
        for i in range(config.DEFAULT_LENGTH):
            self.lengthen()

    def destroy(self):
        self.cells = []
        self.graphical_poses = []
        self.inj_pos = [-1000, -1000]

    def cell_append(self, pos):
        self.cells.append(Cell(*pos))
        self.graphical_poses.extend([0, 0])
        self.cell_move_to(len(self.cells) - 1, pos)

    def lengthen(self, pos=None, direction=(0, 1)):
        if pos is None:
            px = self.cells[-1].get_pos()[0] + direction[0] * self.cell_size
            py = self.cells[-1].get_pos()[1] + direction[1] * self.cell_size
            pos = (px, py)
        self.cell_append(pos)

    def head_init(self, pos):
        self.lengthen(pos=pos)

    def cell_move_to(self, i, pos, smooth_motion=None):
        self.cells[i].move_to(*pos)
        to_x, to_y = pos[0], pos[1]
        if smooth_motion is None:
            self.graphical_poses[i * 2], self.graphical_poses[i * 2 + 1] = to_x, to_y
        else:
            smoother, t = smooth_motion
            smoother.move_to(self, "graphical_poses[" + str(i * 2) + "]", "graphical_poses[" + str(i * 2 + 1) + "]",
                             to_x, to_y, t)

    def move(self, direction, **kwargs):
        for i in range(len(self.cells) - 1, 0, -1):
            self.cell_move_to(i, self.cells[i - 1].get_pos(), **kwargs)
        self.cell_move_to(0, (self.cells[0].get_pos()[0] + self.cell_size * direction[0], self.cells[0].get_pos()[1] +
                              self.cell_size * direction[1]), **kwargs)

    def gather_positions(self):
        return [cell.get_pos() for cell in self.cells]

    def head_intersect(self, cell):
        return self.cells[0].get_pos() == cell.get_pos()


class Form(Widget):
    worm_len = NumericProperty(0)
    fruit_pos = ListProperty([0, 0])
    fruit_size = NumericProperty(0)

    def __init__(self, config, **kwargs):
        super().__init__(**kwargs)
        self.config = config
        self.worm = None
        self.cur_dir = (0, 0)
        self.fruit = None
        self.game_on = True
        self.smooth = smooth.Smooth()

    def random_cell_location(self, offset):
        x_row = self.size[0] // self.config.CELL_SIZE
        x_col = self.size[1] // self.config.CELL_SIZE
        return random.randint(offset, x_row - offset), random.randint(offset, x_col - offset)

    def random_location(self, offset):
        x_row, x_col = self.random_cell_location(offset)
        return self.config.CELL_SIZE * x_row, self.config.CELL_SIZE * x_col

    def fruit_dislocate(self, xy=None):
        if xy is not None:
            x, y = xy
        else:
            x, y = self.random_location(2)
            while (x, y) in self.worm.gather_positions():
                x, y = self.random_location(2)
        self.fruit.move_to(x, y)
        self.fruit_pos = (x, y)

    def start(self):
        self.worm = Worm(self.config)
        self.add_widget(self.worm)
        self.fruit = Fruit(0, 0)
        self.fruit_size = self.config.APPLE_SIZE
        self.fruit_dislocate()
        self.game_on = True
        self.cur_dir = (0, -1)
        Clock.schedule_interval(self.update, self.config.INTERVAL)
        self.popup_label.text = ""

    def stop(self, text=""):
        self.game_on = False
        self.popup_label.text = text
        Clock.unschedule(self.update)

    def game_over(self):
        self.stop("GAME OVER" + " " * 5 + "\ntap to reset")

    def align_labels(self):
        self.popup_label.pos = ((self.size[0] - self.popup_label.width) / 2, self.size[1] / 2)
        self.score_label.pos = ((self.size[0] - self.score_label.width) / 2, self.size[1] - 80)

    def update(self, _):
        if not self.game_on:
            return
        self.worm.move(self.cur_dir, smooth_motion=(self.smooth, self.config.INTERVAL))
        if self.worm.head_intersect(self.fruit):
            directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]
            self.worm.lengthen(direction=random.choice(directions))
            self.fruit_dislocate()
        cell = self.worm_bite_self()
        if cell is not None:
            self.worm.inj_pos = cell.get_pos()
            self.game_over()
        self.worm_len = len(self.worm.cells)
        self.align_labels()

    def on_touch_down(self, touch):
        if not self.game_on:
            self.worm.destroy()
            self.start()
            return
        ws = touch.x / self.size[0]
        hs = touch.y / self.size[1]
        aws = 1 - ws
        if ws > hs and aws > hs:
            cur_dir = (0, -1)
        elif ws > hs >= aws:
            cur_dir = (1, 0)
        elif ws <= hs < aws:
            cur_dir = (-1, 0)
        else:
            cur_dir = (0, 1)
        self.cur_dir = cur_dir

    def worm_bite_self(self):
        for cell in self.worm.cells[1:]:
            if self.worm.head_intersect(cell):
                return cell
        return None


class Config:
    DEFAULT_LENGTH = 20
    CELL_SIZE = 26  # DO NOT FORGET THAT CELL_SIZE - MARGIN WILL BE DIVIDED BY 4
    APPLE_SIZE = 36
    MARGIN = 2
    INTERVAL = 0.3
    DEAD_CELL = (1, 0, 0, 1)
    APPLE_COLOR = (1, 1, 0, 1)


class WormApp(App):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.form = None

    def build(self, **kwargs):
        self.config = Config()
        self.form = Form(self.config, **kwargs)
        return self.form

    def on_start(self):
        self.form.start()


if __name__ == '__main__':
    WormApp().run()

光滑的 (smooth.py)

from kivy.clock import Clock
import time


class Timing:
    @staticmethod
    def linear(x):
        return x


class Smooth:
    def __init__(self, interval=1.0/60.0):
        self.objs = []
        self.running = False
        self.interval = interval

    def run(self):
        if self.running:
            return
        self.running = True
        Clock.schedule_interval(self.update, self.interval)

    def stop(self):
        if not self.running:
            return
        self.running = False
        Clock.unschedule(self.update)

    def set_attr(self, obj, attr, value):
        exec("obj." + attr + " = " + str(value))

    def get_attr(self, obj, attr):
        return float(eval("obj." + attr))

    def update(self, _):
        cur_time = time.time()
        for line in self.objs:
            obj, prop_name_x, prop_name_y, from_x, from_y, to_x, to_y, start_time, period, timing = line
            time_gone = cur_time - start_time
            if time_gone >= period:
                self.set_attr(obj, prop_name_x, to_x)
                self.set_attr(obj, prop_name_y, to_y)
                self.objs.remove(line)
            else:
                share = time_gone / period
                acs = timing(share)
                self.set_attr(obj, prop_name_x, from_x * (1 - acs) + to_x * acs)
                self.set_attr(obj, prop_name_y, from_y * (1 - acs) + to_y * acs)
        if len(self.objs) == 0:
            self.stop()

    def move_to(self, obj, prop_name_x, prop_name_y, to_x, to_y, t, timing=Timing.linear):
        self.objs.append((obj, prop_name_x, prop_name_y, self.get_attr(obj, prop_name_x), self.get_attr(obj, prop_name_y), to_x,
                          to_y, time.time(), t, timing))
        self.run()


class XSmooth(Smooth):
    def __init__(self, props, timing=Timing.linear, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.props = props
        self.timing = timing

    def move_to(self, obj, to_x, to_y, t):
        super().move_to(obj, *self.props, to_x, to_y, t, timing=self.timing)

蠕虫病毒 (worm.kv)

<Form>:
    popup_label: popup_label
    score_label: score_label

    canvas:
        Color:
            rgba: (.5, .5, .5, 1.0)

        Line:
            width: 1.5
            points: (0, 0), self.size

        Line:
            width: 1.5
            points: (self.size[0], 0), (0, self.size[1])

        Color:
            rgba: (1.0, 0.2, 0.2, 1.0)

        Point:
            points: self.fruit_pos
            pointsize: self.fruit_size / 2

    Label:
        id: score_label
        text: "Score: " + str(self.parent.worm_len)
        width: self.width

    Label:
        id: popup_label
        width: self.width

<Worm>:
    canvas:
        Color:
            rgba: (0.2, 1.0, 0.2, 1.0)
        Point:
            points: self.graphical_poses
            pointsize: self.graphical_size / 2
        Color:
            rgba: (1.0, 0.2, 0.2, 1.0)
        Point:
            points: self.inj_pos
            pointsize: self.graphical_size / 2

Feel free to ask any questions.

随意问任何问题。

翻译自: https://habr.com/en/post/465523/

kivy python

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值