3

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)
Rabbid76
  • 202,892
  • 27
  • 131
  • 174
Raker
  • 344
  • 2
  • 6
  • 15

2 Answers2

6

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

sloth
  • 99,095
  • 21
  • 171
  • 219
  • Thank you! Your code is so beautiful and I can see that you are talented way above my level. I can see you are including the text in one textblock (text). How would I go about making separate entries. So I can feed it new lines to write, rather than 1 text block? Because in the end, I want the program to write the messages as a status of some subcommands it's running. – Raker Dec 12 '16 at 15:36
  • Amazing answer! You're unbelievable! @Raker Just do `cursor.write(text)` but with another text. Check when it's done writing by checking if `cursor.text` is empty/false. – Ted Klein Bergman Dec 12 '16 at 16:06
  • @TedKleinBergman Thank you! It got me the final piece of the way. – Raker Dec 12 '16 at 20:44
0

Use the pygame.event module. Use pygame.time.set_timer() to repeatedly create a USEREVENT in the event queue. The time has to be set in milliseconds. e.g.:

typewriter_event = pygame.USEREVENT+1
pygame.time.set_timer(typewriter_event, 100)

Add a new letter to the text, when the timer event occurs:

while run:
    for event in pygame.event.get():
        # [...]

        if event.type == typewriter_event:
            text_len += 1

See also Typewriter


Minimal example:

repl.it/@Rabbid76/PyGame-Typewriter

import pygame

pygame.init()
window = pygame.display.set_mode((500, 150))
clock = pygame.time.Clock()
font = pygame.font.SysFont(None, 100)

background = pygame.Surface(window.get_size())
ts, w, h, c1, c2 = 50, *window.get_size(), (32, 32, 32), (64, 64, 64)
tiles = [((x*ts, y*ts, ts, ts), c1 if (x+y) % 2 == 0 else c2) for x in range((w+ts-1)//ts) for y in range((h+ts-1)//ts)]
for rect, color in tiles:
    pygame.draw.rect(background, color, rect)

text = 'Hello World'
text_len = 0
typewriter_event = pygame.USEREVENT+1
pygame.time.set_timer(typewriter_event, 100)
text_surf = None

run = True
while run:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == typewriter_event:
            text_len += 1
            if text_len > len(text):
                text_len = 0
            text_surf = None if text_len == 0 else font.render(text[:text_len], True, (255, 255, 128))

    window.blit(background, (0, 0))
    if text_surf:
        window.blit(text_surf, text_surf.get_rect(midleft = window.get_rect().midleft).move(40, 0))
    pygame.display.flip()

pygame.quit()
exit()
Rabbid76
  • 202,892
  • 27
  • 131
  • 174