Page 1 of 1

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

#1 DK3250  Icon User is online

  • Pythonian
  • member icon

Reputation: 232
  • View blog
  • Posts: 768
  • 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("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@&#%$€")
 
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
        self.fade_age = random.randint(20, 40)
        self.fade_speed = random.randint(2, 5)

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

        self.add_new_symbol()

    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
        self.fade_age = random.randint(20, 40)
        self.fade_speed = random.randint(2, 5)

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

        self.add_new_symbol()

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
        self.fade_age = column.fade_age
        self.fade_speed = column.fade_speed

    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
    determined by the fade_speed value.
    """
        if self.age < 11:
            self.color = (225-self.age*22, 225-7*self.age, 225-self.age*22)
        elif self.age > self.fade_age:
            self.color = (0, max(0, 155-(self.age-self.fade_age)*self.fade_speed), 0)

Explanation and comments for Symbol:
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:
    self.color = (0, max(0, 155-(self.age-self.fade_age)*self.fade_speed), 0)

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("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@&#%$€")

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.
    
    * fade_age is the 'age' at which the fade starts.
    * fade_speed is the speed of the fading.

    * 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
        self.fade_age = random.randint(20, 40)
        self.fade_speed = random.randint(2, 5)

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

        self.add_new_symbol()

    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
        self.fade_age = column.fade_age
        self.fade_speed = column.fade_speed

    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
    determined by the fade_speed value.
    """
        if self.age < 11:
            self.color = (225-self.age*22, 225-7*self.age, 225-self.age*22)
        elif self.age > self.fade_age:
            self.color = (0, max(0, 155-(self.age-self.fade_age)*self.fade_speed), 0)


" 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("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890@%&#%$€")

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)
        self.add_new_symbol()

    def add_new_symbol(self):
        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
        self.fade_age = random.randint(20, 40)
        self.fade_speed = random.randint(2, 5)
        
        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()
        self.add_new_symbol()

    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.fade_age = column.fade_age
        self.fade_speed = column.fade_speed
        
        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)
        elif self.age > self.fade_age:
            self.color = (0, max(0, 155-(self.age-self.fade_age)*self.fade_speed), 0)
        
    def orange(self):  # alternative color
        if self.age < 11:
            self.color = (225-8*self.age, 225-16*self.age, 225-self.age*22)
        elif self.age > self.fade_age:
            self.color = (max(0, 155-(self.age-self.fade_age)*self.fade_speed),
                          max(0, 75-(self.age-self.fade_age)*self.fade_speed//2), 0)
        

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
  • +

Page 1 of 1