Search code examples
pythonpygameraspberry-pi2

Typewriter Effect Pygame


This question is really difficult to ask, but I know you guys here at Stack Overflow are the brightest minds.

I'm totally blinded by why this issue happens (I'm fairly at Python and Pygame, so any suggestions on how to improve the code will be received with the love of improving my skills).

What I'm creating: It's really a gimmick project, I have a little 2.5" screen (PiTFT) attached to a Raspberry Pi and the code is creating a typewriter effect with a moving cursor in front of the text as it's being written.

Challenge 1 was that every time you move a sprite in pygame, you must redraw everything, otherwise you will see a trail, and since the cursor is moving in front of the text, the result would look like this:

enter image description here

I managed to solve this issue by blackening / clearing the screen. But then I lost all the previously written letters. So I created a list (entireword), which I'm populing with all the previously written characters. I use this list every time I cycle through the loop to redraw all the previous written text. So now:

Sample of issue

As you can see, the text looks funny. It's supposed to read:

[i] Initializing ...

[i] Entering ghost mode ... []

I've been spending hours and hours getting to this point - and the code ALMOST works perfectly! The magic happens in the function print_screen(), but WHAT in my code is causing the text to include a letter from the other line in the end? :>

Help is GREATLY appreciated <3

Here's the entire code:

import pygame
import time
import os
import sys
from time import sleep
from pygame.locals import *

positionx = 10
positiony = 10
entireword = []
entireword_pos = 10
counter = 0
entire_newline = False


#Sets the width and height of the screen
WIDTH = 320
HEIGHT = 240
speed = 0.05

#Importing the external screen
os.putenv('SDL_FBDEV', '/dev/fb1')
os.putenv('SDL_MOUSEDRV', 'TSLIB')
os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

#Initializes the screen - Careful: all pygame commands must come after the init
pygame.init()

#Sets mouse cursor visibility
pygame.mouse.set_visible(False)
#Sets the screen note: must be after pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))

# initialize font; must be called after 'pygame.init()' to avoid 'Font not Initialized' error
myfont = pygame.font.SysFont("monospace", 18)

#Class

class cursors(pygame.sprite.Sprite):
    def __init__(self):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((10, 20))
        self.image.fill((0,255,0))
        self.rect = self.image.get_rect()
        self.rect.center = (positionx + 10, positiony + 10)

    def update(self):
        self.rect.x = positionx + 10
        self.rect.y = positiony

#Functions

#Prints to the screen
def print_screen(words, speed):
    rel_speed = speed
    for char in words:
        #speed of writing
        if char == ".":
            sleep(0.3)
        else:
            sleep(rel_speed)

        #re-renders previous written letters
        global entireword

        # Old Typewriter functionality - Changes position of cursor and text a newline

        #Makes sure the previous letters are rendered and not lost
        #xx is a delimter so the program can see when to make a newline and ofcourse ignore writing the delimiter
        entireword.append(char)
        if counter > 0:
            loopcount = 1
            linecount = 0 # This is to which line we are on
            for prev in entireword:
                if prev == 'xx':
                    global linecount
                    global positiony
                    global loopcount
                    linecount = linecount + 1
                    positiony = 17 * linecount
                    loopcount = 1
                if prev != 'xx':  #ignore writing the delimiter
                    pchar = myfont.render(prev, 1, (255,255,0))
                    screen.blit(pchar, (loopcount * 10, positiony))
                    loopcount = loopcount + 1

        if char != 'xx':
            # render text
            letter = myfont.render(char, 1, (255,255,0))
            #blits the latest letter to the screen
            screen.blit(letter, (positionx, positiony))

        # Appends xx as a delimiter to indicate a new line
        if entire_newline == True:
            entireword.append('xx')
            global entire_newline
            entire_newline = False

        global positionx
        positionx = positionx + 10
        all_sprites.update()
        all_sprites.draw(screen)
        pygame.display.flip()
        screen.fill((0,0,0)) # blackens / clears the screen

        global counter
        counter = counter + 1

#Positions cursor at new line
def newline():
    global positionx
    global positiony
    positionx = 10
    positiony = positiony + 17

all_sprites = pygame.sprite.Group()
cursor = cursors()
all_sprites.add(cursor)

#Main loop
running = True
while running:
    global speed
    global entire_newline

    words = "[i] Initializing ..."
    entire_newline = True
    newline()
    print_screen(words,speed)

    words = "[i] Entering ghost mode ..."
    entire_newline = True
    newline()
    print_screen(words,speed)

    #Stops the endless loop if False
    running = False
sleep(10)

Solution

  • Sorry if I don't answer your question directly, because your code is too confusing for me now, so I took the liberty to rewrite your code to get done what you want.

    The idea is to have two sprites:

    • the cursor, which is a) displayed on the screen and b) keeps track of what text to write and where

    • the board, which is basically just a surface that the text is rendered on

    Note how all the writing logic is on the Cursor class, and we have a nice, simple and dumb main loop.

    import pygame
    import os
    
    #Sets the width and height of the screen
    WIDTH = 320
    HEIGHT = 240
    
    #Importing the external screen
    os.putenv('SDL_FBDEV', '/dev/fb1')
    os.putenv('SDL_MOUSEDRV', 'TSLIB')
    os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')
    
    #Initializes the screen - Careful: all pygame commands must come after the init
    pygame.init()
    clock = pygame.time.Clock()
    
    #Sets mouse cursor visibility
    pygame.mouse.set_visible(False)
    #Sets the screen note: must be after pygame.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    
    
    class Board(pygame.sprite.Sprite):
        def __init__(self):
            pygame.sprite.Sprite.__init__(self)
            self.image = pygame.Surface((WIDTH, HEIGHT))
            self.image.fill((13,13,13))
            self.image.set_colorkey((13,13,13))
            self.rect = self.image.get_rect()
            self.font = pygame.font.SysFont("monospace", 18)
    
        def add(self, letter, pos):
            s = self.font.render(letter, 1, (255, 255, 0))
            self.image.blit(s, pos)
    
    class Cursor(pygame.sprite.Sprite):
        def __init__(self, board):
            pygame.sprite.Sprite.__init__(self)
            self.image = pygame.Surface((10, 20))
            self.image.fill((0,255,0))
            self.text_height = 17
            self.text_width = 10
            self.rect = self.image.get_rect(topleft=(self.text_width, self.text_height))
            self.board = board
            self.text = ''
            self.cooldown = 0
            self.cooldowns = {'.': 12,
                            '[': 18,
                            ']': 18,
                            ' ': 5,
                            '\n': 30}
    
        def write(self, text):
            self.text = list(text)
    
        def update(self):
            if not self.cooldown and self.text:
                letter = self.text.pop(0)
                if letter == '\n':
                    self.rect.move_ip((0, self.text_height))
                    self.rect.x = self.text_width
                else:
                    self.board.add(letter, self.rect.topleft)
                    self.rect.move_ip((self.text_width, 0))
                self.cooldown = self.cooldowns.get(letter, 8)
    
            if self.cooldown:
                self.cooldown -= 1
    
    all_sprites = pygame.sprite.Group()
    board = Board()
    cursor = Cursor(board)
    all_sprites.add(cursor, board)
    
    text = """[i] Initializing ...
    [i] Entering ghost mode ...
    
    done ...
    
    """
    
    cursor.write(text)
    
    #Main loop
    running = True
    while running:
    
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                running = False
    
        all_sprites.update()
        screen.fill((0, 0, 0))
        all_sprites.draw(screen)
        pygame.display.flip()
        clock.tick(60)
    

    enter image description here