Random walker
A Walker is born
Obviously, having a running app without displaying anything is extremely boring. Let’s draw something inside.
Imagine the window is now another 2D world we just created some mins ago, someone (or something) lives here. Hm, let’s call it Walker. For now, it’s just another class in the same file.
# main.py
class Walker(Widget):
pass
Good, the Walker is now an invisible agent. However, I would like to visualize it. In Kivy, we can implement the visual representation with the kv language in .kv file, and the logic of app with python in the .py file. Let’s just keep it simple and do everything in python for now.
To start, we need the __init__
function to define what we would like to see when the Walker
is initialized. This is also for the visualization later.
class Walker(Widget):
def __init__(self, *args, **kwargs):
# Initialize the parent class
super(Walker, self).__init__(*args, **kwargs)
Every Kivy widget comes with a canvas object, with which you can draw something. This is where you define how the Walker looks like e.g. the color, size, position…etc. For now, I would like the Walker to just be a point, a white point that appears in the center of the window.
# main.py
# import App and Widget class...
from kivy.core.window import Window
from kivy.graphics import Point, Color
class Walker(Widget):
def __init__(self, *args, **kwargs):
# Initialize the parent class
super(Walker, self).__init__(*args, **kwargs)
with self.canvas:
# Value ranges from 0 to 1
Color(1, 1, 1)
# Initialize Walker's position in the center
Point(points = Window.center)
Finally, modify the build
method to return a Walker
object instead of a Widget
object.
class NatureApp(App):
"""A 2D world where the Walker lives"""
def build(self):
return Walker() # A Walker object is returned
if __name__ == "__main__":
NatureApp().run()
Run the python main.py
in the terminal. You should see a white dot appears in the center of the window.
Fantastic.
Locate the Walker
The Walker should be able to move, but not as free as we are in the real world. It can only go left, right, forward, backward, and maybe the combination of these. And that’s just the most you can do in a 2D world. Anyway, I will be happy for now as long as the Walker moves!
As you might have guessed, the position in this 2D world is represented in coordinates. Take the Point(points = Window.center)
in the canvas for example, it’s actually the coordinates (x, y)
that defines the position of Walker. So, if we want the Walker to move, I think we need to play with the coordinates a bit.
In fact, the points
attribute is basically a list in the form of (x1, y1, x2, y2, x3, y3,...)
. Therefore, it’s a great way to represent the path that the Walker has been to. If we could update the list in some way, we could probably make the Walker behaves the way we want. In order to access and add new coordinates later, we make the Point
as an attribute path
of the Widget class. After all, the Walker should know where it is…1
class Walker(Widget):
def __init__(self, *args, **kwargs):
# Initialize the parent class
super(Walker, self).__init__(*args, **kwargs)
with self.canvas:
# Value ranges from 0 to 1
Color(1, 1, 1)
# Initialize Walker's position in the center
self.path = Point(points = Window.center)
The random Walker
Now, what pieces are missing? I imagine the Walker moves one second to the left, next second up, and maybe the next second left again. The direction of its movement should be random, and one step of a time. Yes, I think that’s it.
In order to make the Walker roam around in a random manner, I would like to add a second pair of coordinates to the list. The new coordinates should be not too far away from the old coordinates, maybe just +1
or -1
pixel away. To achieve this, the random
python module is what we need.
from random import randint
How do we add a second pair of coordinates that are close to the first pair to the list? It turns out that we can do this through a add_point
function. However, we also need to generate the random step in someway. Since the Walker knows where it is, let’s wrap everything in one function to update its own position.
class Walker(widget):
# def __init__(self):
# ...
def update(self, *args):
# Get current position
last_x = self.path.points[-2]
last_y = self.path.points[-1]
# Generate random steps in 9 possible directions
new_x = randint(-1,1)
new_y = randint(-1,1)
# Add the next step based on the previous position
self.path.add_point(last_x + new_x, last_y + new_y)
Of course, we can use a for
loop and call this update function as many time as we want, but let’s do something fancier. To animate something, Kivy provides an easy way to do this, namely the function schedule_interval(my_callback, t)
in the Clock
module. Here, the my_callback
is exactly the function we wrote before, while the t
is the defined time interval. Because I want the random walking behavior to start as soon as the Walker appears, I would schedule the clock also in the __init__
.
class Walker(Widget):
def __init__(self, *args, **kwargs):
# Initialize the parent class
super(Walker, self).__init__(*args, **kwargs)
with self.canvas:
# RGB values range from 0 to 1
Color(1, 1, 1)
# Initialize Walker's position in the center
self.path = Point(points = Window.center)
# Update the its own position every 0.1 second
Clock.schedule_interval(self.update, 0.1)
Put everything together:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#main.py
from kivy.app import App
from kivy.core.window import Window
from kivy.uix.widget import Widget
from kivy.graphics import Point, Color
from kivy.clock import Clock
from random import randint
class Walker(Widget):
def __init__(self, *args, **kwargs):
super(Walker, self).__init__(*args, **kwargs)
# start walking
with self.canvas:
# Define the color of stroke
Color(1, 1, 1)
# Walker's initial position
self.path = Point(points = Window.center)
# Update the its own position every 0.1 second
Clock.schedule_interval(self.update, 0.1)
def update(self, *args):
# Get current position
last_x = self.path.points[-2]
last_y = self.path.points[-1]
# Generate random steps in 9 possible directions
new_x = randint(-1,1)
new_y = randint(-1,1)
# Add the next step based on the previous position
self.path.add_point(last_x + new_x, last_y + new_y)
class NatureApp(App):
def build(self):
return Walker()
if __name__ == "__main__":
NatureApp().run()
Again, run python main.py
in your terminal. After letting it run for 25 mins, I got something like this.
Great. Now I’ve got a random Walker.
Warning Notice:
There is currently a limit on the number of points you can add in the list. According to the docs, the list cannot contain more than 2^15-2 elements i.e. x
or y
. Based on the frequency (10Hz) we update the list, the app actually crashes less than half an hour.
-
If you add
print(self.path.points)
in the__init__
function, you will see that there is only one pair of coordinates in the list. In my case, the coordinates are[400.0, 300.0]
. ↩