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?)
他们的代码不好 (Their code is bad)
Minor issues:
次要问题:
- 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”是没有意义的。 磁头与磁贴的区别不够大,无法成为不同的变量。
- Clock.schedule for self.update is called from… self.update. 用于self.update的Clock.schedule从... self.update调用。
- Second-level class Playground is implemented in the beginning, while first-level class SnakeApp is implemented at the end. 第二层类Playground在开头实现,而第一层SnakeApp在结尾实现。
- Names for directions («up», «down», ...) are used instead of vectors ( (0, 1), (1, 0)… ). 使用方向名称(«上»,«下»,...)代替向量((0,1),(1,0)…)。
Major issues:
主要问题:
- 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个,因为这时您应该重写此部分
- Weird logic for the snake's motion instead of «cell-after-cell» motion. 蛇的动作而不是“ cell-after-cell”动作的怪异逻辑。
- 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:
这是我个人的看法。 而且,我不保证我的教程会更加有趣和清晰。 但是,我会竭尽所能,并在我的文章中保证:
- The code is short 代码很短
- The snake is nice 蛇很好
- 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.
随意问任何问题。
kivy python