Page 1 of 1

## Matrix-Rain, a walk-through with focus on class objects.

### #1 DK3250 Reputation: 607
• Posts: 1,927
• Joined: 27-December 13

Posted 20 January 2017 - 06:30 AM

Matrix-Rain, a walk-through with focus on class objects.

This tutorial is made with Python 3.4 and uses Pygame.

Inspired by a recent question in the Python forum by albert003 about a Matrix-Rain program, I have made this small walk-through. The tutorial focuses on problem analysis and use of class objects. Use of class objects has been the subject for several questions in the forum lately.

Problem analysis

In the Matrix-Rain the screen is covered by columns of letters that appears to fall down the screen. Lets focus on a single column. On close inspection, is becomes clear that a letter (or symbol) actually stays in place and new symbols are spawn below the existing ones.

The new symbol is white/grey but soon turns green  after a while is fades away. Ajacent columns do not necessarily have same characteristic in term of how to fade. Within a single column, however, all symbols behave like a train  in next pass of the game-loop, symbol no. x will behave like symbol no. x-1.

When the whole column has faded away, a new column spawn from the top of the screen.

Considerations

Two different objects are in play here: Symbols and Columns.
The (individual) Symbol is generated with a position and an 'age' = 0; as time goes by (with each pass of the game loop) the 'age' grows.
We can quite easy let this simple age parameter control the color of the symbol.

The Column is generated with an x-position and a pointer to the y-position for the next Symbol.
To ensure that the columns appear at random from the screen top, the pointer starts at a (random) negative value, pointing to a position above the screen. The pointer grows by the height of the symbols and will sooner or later get to the screen top  at this point we start to generate Symbols and store them in a list. When the pointer gets to the bottom of the screen, we stop the generation of new symbols, but continue to let the pointer grow; allowing time for the symbols to fade away before starting over. The criteria for starting the column over is that the last symbol in the list has faded to black.

Code

Lets look at this in code:
First the basic initializations:
```import pygame, sys, random
pygame.init()

BLACK = (0, 0, 0)

X = 1400
Y = 900
screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("The Matrix Rain Effect")

symbols = list("[email protected]&#%\$")

SIZE = 16

font = pygame.font.Font(None, SIZE)
row_height = SIZE * 0.6
row_width = SIZE
```
Note the simple way to generate a list of symbols (line 11)  I find it handy.

The 'SIZE' determines the size of the symbols and thus the size of row height and row width. I multiply the row height with 0.6 the get a nice dense look  with other fonts this value may be different.

I always use X and Y for screen size; very often the screen dimensions are needed throughout the Pygame code, and I find this short form easy to handle. The use of Capitals indicate that the screen size is constant. This is not always so, Pygame do support flexible screen size. But for now, we assume a constant screen size.

Now, lets look at the Column object:
```class Column():
def __init__(self, x):
self.x = x
self.clear_and_restart(1000)

def clear_and_restart(self, start_pos=250):
pygame.draw.rect(screen, BLACK, (self.x  - row_width//2, 0, row_width, Y), 0)
self.list = []
self.y = - random.randint(0, start_pos//row_height) * row_height

if 0 < self.y < Y:
self.list.append(Symbol(self))

def move(self):
if self.list and self.list[-1].color == BLACK:
self.clear_and_restart()
self.y += row_height

def update(self):
for symbol in self.list:
symbol.update()

```
Here explained one bit at a time:
```class Column():
def __init__(self, x):
self.x = x
self.clear_and_restart(1000)
```
The x is simply the x position for the column.
The method 'clear_and_restart()' is called with the parameter '1000'; you will see below that this creates a pointer starting at random 0-1000 pixels above the screen top.
```    def clear_and_restart(self, start_pos=250):
pygame.draw.rect(screen, BLACK, (self.x  - row_width//2, 0, row_width, Y), 0)
self.list = []
self.y = - random.randint(0, start_pos//row_height) * row_height   # see text

```
The 'clear_and_restart()' first draws a black rectangle in the full column dimension. This is explained in the Symbol section.
The self.list is initiated  this is the list where all the symbols are stored.
The pointer 'self.y' is chosen at random (in accordance with the 'start_pos' parameter) but adjusted to fit a whole number of row heights; first start_pos//row_height calculates how many full rows is maximum possible  after choosing a random number in this interval the row height is multiplied the get the pointer value. The pointer is negative.
fade_age is the age of the symbols when they start to fade (same value for all symbols in a column, hence a Column value).
fade_speed is a parameter determining how fast the symbols go from green to black (also a Column value).
In general the new column starts 0-250 pixel above the screen, at the very first generation, a little higher separation is used, 0-1000 pixel above screen top.

```    def add_new_symbol(self):
if 0 < self.y < Y:
self.list.append(Symbol(self))

```
'self.y' is the column pointer; when it is inside the screen dimension, new Symbols are added to the list.
The new object 'Symbol' is generated with the Column instance (self) as argument. In this way all Column attributes (x, y, fade_age, fade_speed) will be available for the Symbol constructor.
```    def move(self):
if self.list and self.list[-1].color == BLACK:
self.clear_and_restart()
self.y += row_height

```

First it is checked that self.list is not empty (the 'if self.list' part) and then it is checked if the last symbol had faded to black. If so, the column is restarted.
The pointer is advanced on step and a new symbol is added (if allowed by the add_new_symbol() method)
```    def update(self):
for symbol in self.list:
symbol.update()
```

This bit simply runs through all symbols in the list and execute the update method, see below.

The code for Symbol is simpler than the Column:
```class Symbol():
def __init__(self, column):  # see comment in text
self.x = column.x
self.y = column.y
self.symbol = random.choice(symbols)
self.age = 0  # all symbols start at age = 0

def update(self):
self.draw()
self.age += 1

def draw(self):
self.color_function()

self.surf = font.render(self.symbol, 1, self.color)  # see text
self.rect = self.surf.get_rect(center=(self.x, self.y))
screen.blit(self.surf, self.rect)

def color_function(self):
"""
The color_function is the big trick in Matrix-rain.
At 'age' 0-10, the symbol turn from grey (225, 225, 225) to green (0, 155, 0)
At high 'age' (random value) the symbol turn from green to black over a period
"""
if self.age < 11:
self.color = (225-self.age*22, 225-7*self.age, 225-self.age*22)

```
In the __init__() section we just copy the relevant column parameters to the symbol. We pick a random symbol from the list of all symbols and we assign the 'age' = 0 to the new symbol.

In the update() section we call the draw() method and add 1 to the 'age' of the symbol.

In the draw() section we call the color_function() method and create a surface using the relevant color. This surface is then blitted to the screen.

Note that I use anti-aliased text by applying the parameter '1' in the font.render() call. As the screen is not wiped between updates, the same symbol is printed to the same position on the screen many times using different colors. Because of little imperfection in the anti-aliasing this cause the symbol to grow somewhat fuzzy or woolly in the appearance. You will get a sharper picture of the symbols using the standard (not anti-aliased) symbol, this is obtained by change the parameter to '0'  it is a matter of taste, I like the fuzzy the most.
This fuzziness is also the reason that the symbols, when printed in black, may still leave single colored pixels around the symbol profile; and this is the reason to draw a black rectangle when a Column is restarted. Otherwise those individual pixels will just add up and look awful.

Finally we have the color_function(). This is really the bread and butter of Matrix-Rain, - and only 4 lines. The change of color from grey to green is here fixed to occur in 10 age-steps (my decision).
Look at the first 2 lines of the function:
```if self.age < 11:
self.color = (225-self.age*22, 225-7*self.age, 225-self.age*22)
```

Starting a age=0, self.color will be (225, 225, 225), at age=10, self.color = (5, 155, 5); all the intermediate age values produces intermediate colors.

When age passes fade_age, the fade begins. Now we need to calculate how much older than fade_age the symbol is: We only fade proportional to the difference (age - fade_age); the speed of fade is multiplied to the difference, and the result is covered by a max() function handling old symbols that would otherwise create negative values. The code is:
```elif self.age > self.fade_age:

```
Now all preparations are in place; we only need to create the Column instances and enter the game-loop. I think this part of the code needs no further comments.
```col = []
for i in range(1, X//SIZE):
col.append(Column(i*row_width))

screen.fill(BLACK)  # can be placed inside the loop

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

for c in col:
c.move()
c.update()
pygame.time.wait(20)
pygame.display.flip()
```

All-in-all the commented code looks like this:
```"""
Matrix-rain, a demonstration program by DK3250, inspired by albert003

The code revolves around two objects: Column and Symbol.
Column is a vertical 'slide' of the screen and contains a number of Symbol's in
a list.
Column describes what all symblos in that column has in common,
fade position and fade speed. Column also fill and empty the list as appropriate.

Symbol defines what is characteristic for the individual symbol, color, position
and 'age'.

New symbols are added to the column list (at age = 0) and grow older at each pass
of the main loop. The 'age' determines the color; starting out ligth grey, turning
green and finally fading to black.
"""

import pygame, sys, random
pygame.init()

BLACK = (0, 0, 0)

X = 1400
Y = 900
screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("The Matrix Rain Effect")

symbols = list("[email protected]&#%\$")

SIZE = 16  # determines the size of the symbols used

font = pygame.font.Font(None, SIZE)
row_height = SIZE * 0.6
row_width = SIZE

class Column():
""" The columns are created once and never replaced """
def __init__(self, x):
self.x = x
self.clear_and_restart(1000)  # the parameter '1000' ensures separation between vertical starting position

def clear_and_restart(self, start_pos=250):
"""
The start position is somewhere above the screen top.
At first start from __init__() a random symbol position between 0 and -1000 pixels is
used to ensure proper separation. Later, when the code is running, to ensure quick
reappearance of the new column, a lower default value (=250) is used.

* y is really a 'colunm pointer' that starts at negative values (pointing
above the screen) and ends at high positive values (pointing to below
the screen)
"""
pygame.draw.rect(screen, BLACK, (self.x  - row_width//2, 0, row_width, Y), 0)
self.list = []
self.y = - random.randint(0, start_pos//row_height) * row_height   # see text

"""
The list only grows when the column pointer is inside the active screen.
"""
if 0 < self.y < Y:
self.list.append(Symbol(self))  # se comment in text

def move(self):
"""
When the last symbol in the list has turned black, the column is cleared
and restarted.
"""
if self.list and self.list[-1].color == BLACK:
self.clear_and_restart()
self.y += row_height

def update(self):
for symbol in self.list:
symbol.update()

class Symbol():
def __init__(self, column):  # see comment in text
self.x = column.x
self.y = column.y
self.symbol = random.choice(symbols)
self.age = 0  # all symbols start at age = 0

def update(self):
self.draw()
self.age += 1

def draw(self):
self.color_function()

self.surf = font.render(self.symbol, 1, self.color)  # see text
self.rect = self.surf.get_rect(center=(self.x, self.y))
screen.blit(self.surf, self.rect)

def color_function(self):
"""
The color_function is the big trick in Matrix-rain.
At 'age' 0-10, the symbol turn from grey (225, 225, 225) to green (0, 155, 0)
At high 'age' (random value) the symbol turn from green to black over a period
"""
if self.age < 11:
self.color = (225-self.age*22, 225-7*self.age, 225-self.age*22)

" Creation of the Column instances "
col = []
for i in range(1, X//SIZE):
col.append(Column(i*row_width))

screen.fill(BLACK)  # can be placed inside the loop

while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

for c in col:
c.move()
c.update()
pygame.time.wait(20)
pygame.display.flip()

```
From here you can start experimenting.
The code below adds a different color to approximately 1% of the columns.
```import pygame, sys, random
pygame.init()

BLACK = (0, 0, 0)

X = 1400
Y = 900
screen = pygame.display.set_mode((X, Y))
pygame.display.set_caption("The Matrix Rain Effect")

symbols = list("[email protected]%&#%\$")

SIZE = 16
font=pygame.font.Font(None, SIZE)
row_height = SIZE * 0.6
row_width = SIZE

class Column():
def __init__(self, x):
self.x = x
self.clear_and_restart(1000)

if 0 < self.y < Y:
self.list.append(Symbol(self))
self.y += row_height

def clear_and_restart(self, start_pos=250):
pygame.draw.rect(screen, BLACK, (self.x  - row_width//2, 0, row_width, Y), 0)
self.list = []
self.y = - random.randint(0, start_pos//row_height) * row_height

if random.random() < 0.99:  # new in this version
self.color = "green"
else:
self.color = "orange"

def move(self):
if self.list and self.list[-1].color == BLACK:
self.clear_and_restart()

def update(self):
for symbol in self.list:
symbol.update()

class Symbol():
def __init__(self, column):
self.x = column.x
self.y = column.y
self.symbol = random.choice(symbols)
self.age = 0

self.color_function = self.green  # new in this version
if column.color == "orange":
self.color_function = self.orange

def update(self):
self.draw()
self.age += 1

def draw(self):
self.color_function()

self.surf = font.render(self.symbol, 1, self.color)
self.rect = self.surf.get_rect(center=(self.x, self.y))
screen.blit(self.surf, self.rect)

def green(self):  # new name in this version
if self.age < 11:
self.color = (225-self.age*22, 225-7*self.age, 225-self.age*22)

def orange(self):  # alternative color
if self.age < 11:
self.color = (225-8*self.age, 225-16*self.age, 225-self.age*22)

col = []
for i in range(1, X//SIZE):
col.append(Column(i*row_width))

screen.fill(BLACK)

while True:

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

for c in col:
c.move()
c.update()
pygame.time.wait(20)
pygame.display.flip()

```

I have also made a version where the fall speed of some columns are the double of the general fall speed, - and I have introduced an extra font to the symbols.
Some of this you can see on youtube: https://youtu.be/3o1FRJxgqTk
But in order not to spoil the fun, Ill leave it to you. Feel free to ask, however, if you get stuck, or revert with new features of your own brand.

Is This A Good Question/Topic? 1

## Replies To: Matrix-Rain, a walk-through with focus on class objects.

### #2 albert003 Reputation: 39
• Posts: 993
• Joined: 15-December 14

Posted 23 February 2017 - 02:36 PM

Great tutorial. It explained the whole process clearly. I do have a question, in row 24 to 27 you have a for loop and you append the Symbol (which I get why), but why did you put self inside?.

Great tutorial. It explained the whole process clearly. I do have a question, in row 24 to 27 you have a for loop and you append the Symbol (which I get why), but why did you put self inside?.

### #3 DK3250 Reputation: 607
• Posts: 1,927
• Joined: 27-December 13

Posted 24 February 2017 - 05:52 AM

Some of the Column attributes are needed in the Symbol instances.

The traditional way is to 'send' the arguments individually; like:
```def add_new_symbol(self):
if 0 < self.y < Y:

```
Then, naturally, the Symbol __init__() function should be modified to 'receive' those four values.

I just saved some typing by sending the full Column object (self), end then in the Symbol __init__() 'extract' the attributes:
```class Symbol():
def __init__(self, column):  # the name 'column' is used to receive Column(self)
self.x = column.x  # extraction of column attribute
self.y = column.y  # same
self.symbol = random.choice(symbols)
self.age = 0
```

This method can be disputed.
It leaves an open connection between Symbol and Column - makes it possible from Symbol to unintentionally modify Column properties. Probably not best OOP practice in this case.

On the other hand - it demonstrates the possibility; maybe in a different situation this may be useful.

EDIT: To expand on above explanation. You can even drive the approach further, reading the column values directly in the Symbol methods:
```class Symbol():
def __init__(self, column):
self.y = column.y  # column.y is changed dynamically; self.y needs to be constant
self.symbol = random.choice(symbols)
self.age = 0
self.column = column  # save the reference to the Column instance

def update(self):
self.draw()
self.age += 1

def draw(self):
self.color_function()

self.surf = font.render(self.symbol, 1, self.color)
self.rect = self.surf.get_rect(center=(self.column.x, self.y))  # read column.x from the Column instance
screen.blit(self.surf, self.rect)

def color_function(self):
if self.age < 11:
self.color = (225-self.age*22, 225-7*self.age, 225-self.age*22)

```

Again, - just to demonstrate some possibilities...

This post has been edited by DK3250: 24 February 2017 - 07:19 AM

### #4 albert003 Reputation: 39
• Posts: 993
• Joined: 15-December 14

Posted 27 February 2017 - 05:07 PM

Ok, I get it. I have a question I wasn't aware you could append class or functions. I thought when you used an open list you could only append one thing at a time.
for example:
```a="Summer"
b="Winter"
c="Spring"
d="Fall"
e="The seasons..."
x=[]
x.append(e)
x.append(a)
x.append(B)/>
x.append(c)
x.append(d)
print x
['The seasons', 'Summer', 'Winter', 'Spring', 'Fall']

```

I tried to look for some examples to see what other programmers have done (using empty list to append instances of a class) and this was the only example I could find. It was on stackoverflow and it wasn't really what you did in your program. This is the link to that webpage.

http://stackoverflow...jects-in-python

Do you know where I could get some more examples?.

### #5 albert003 Reputation: 39
• Posts: 993
• Joined: 15-December 14

Posted 27 February 2017 - 05:14 PM

Disregard my previous message. I found an example on Daniweb. The programmer isn't using pygame, but I want to study it to see another example of how to append instances of a class. This is the link the website.

https://www.daniweb....-objects-python

### #6 DK3250 Reputation: 607
• Posts: 1,927
• Joined: 27-December 13

Posted 27 February 2017 - 11:31 PM

List of class instances are perfectly normal.
You will find it in almost any game.
Look-up the 'old' Snow class example, or the Platform game in the tutorials.

Page 1 of 1

 .related ul { list-style-type: circle; font-size: 12px; font-weight: bold; } .related li { margin-bottom: 5px; background-position: left 7px !important; margin-left: -35px; } .related h2 { font-size: 18px; font-weight: bold; } .related a { color: blue; }