Rendering text with multiple lines in pygame
Question:
I am trying to make a game and I am trying to render a lot of text. When the text renders, the rest of the text goes off the screen. Is there any easy way to make the text go to the next line of the pygame window?
helpT = sys_font.render
("This game is a combination of all of the trendsn of 2016. When you press 'Start Game,' a menu will pop up. In order to beat the game, you must get a perfect score on every single one of these games.",0,(hecolor))
screen.blit(helpT,(0, 0))
Answers:
There is no easy way to render text on multiple lines in pygame, but this helper function could provide some use to you. Just pass in your text (with newlines), x, y, and font size.
def render_multi_line(text, x, y, fsize)
lines = text.splitlines()
for i, l in enumerate(lines):
screen.blit(sys_font.render(l, 0, hecolor), (x, y + fsize*i))
As I said in the comments; you have to render each word separately and calculate if the width of the text extends the width of the surface (or screen). Here’s an example:
import pygame
pygame.init()
SIZE = WIDTH, HEIGHT = (1024, 720)
FPS = 30
screen = pygame.display.set_mode(SIZE, pygame.RESIZABLE)
clock = pygame.time.Clock()
def blit_text(surface, text, pos, font, color=pygame.Color('black')):
words = [word.split(' ') for word in text.splitlines()] # 2D array where each row is a list of words.
space = font.size(' ')[0] # The width of a space.
max_width, max_height = surface.get_size()
x, y = pos
for line in words:
for word in line:
word_surface = font.render(word, 0, color)
word_width, word_height = word_surface.get_size()
if x + word_width >= max_width:
x = pos[0] # Reset the x.
y += word_height # Start on new row.
surface.blit(word_surface, (x, y))
x += word_width + space
x = pos[0] # Reset the x.
y += word_height # Start on new row.
text = "This is a really long sentence with a couple of breaks.nSometimes it will break even if there isn't a break "
"in the sentence, but that's because the text is too long to fit the screen.nIt can look strange sometimes.n"
"This function doesn't check if the text is too high to fit on the height of the surface though, so sometimes "
"text will disappear underneath the surface"
font = pygame.font.SysFont('Arial', 64)
while True:
dt = clock.tick(FPS) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
screen.fill(pygame.Color('white'))
blit_text(screen, text, (20, 20), font)
pygame.display.update()
Result
This is how I did it
amfolyt_beskrivelse_text = ['en amfolyt er et stof som både kan være en base, eller syre','så som']
for x in amfolyt_beskrivelse_text:
descriptioncounter += 1
screen.blit((pygame.font.SysFont('constantia',12).render(x, True, BLACK)),(300,10*descriptioncounter))
descriptioncounter = 0
but of course, I can only do that because my text starts a line distance from the top of the screen. If you start further down the screen you could do
(300,12+12*descriptioncounter)
One thing you could do is use a monospaced font. They have the same size for all characters and so are beloved by programmers. That’s going to be my solution for handling the height/width problem.
You could use a .json file to load each line.
.json file (called first.json):
["Hello!", "How's it going?"]
And then load it into the file:
sys_font = pygame.font.SysFont(("Arial"),30)
def message_box(text):
pos = 560 # depends on message box location
pygame.draw.rect(root, (0,0,0), (100, 550, 800, 200)) #rectangle position varies
for x in range(len(text)):
rendered = sys_font.render(text[x], 0, (255,255,255))
root.blit(rendered, ( 110, pos))
pos += 30 # moves the following line down 30 pixels
with open('first.json') as text:
message_box(json.load(text))
Don’t forget to import json
Hope this helps!
I recommend the ptext
library which is able to recognize newline (n
) characters. You only need to call ptext.draw(text, position)
.
import pygame as pg
import ptext
pg.init()
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
BG_COLOR = pg.Color('gray12')
BLUE = pg.Color('dodgerblue')
# Triple quoted strings contain newline characters.
text = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum."""
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
screen.fill(BG_COLOR)
ptext.draw(text, (10, 10), color=BLUE) # Recognizes newline characters.
pg.display.flip()
clock.tick(60)
pg.quit()
Building on previous answers I made a somewhat comprehensive blit text function I can use with a single short command:
def blittext(text, **kwargs):
#blit text into screen, uses defaults and works for multiline strings
fontface = kwargs.get('font', 'PressStart2P-Regular.ttf')
b = kwargs.get('bold', False)
fontsize = kwargs.get('size', 30)
color = kwargs.get('color', (255, 255, 255))
topleft = kwargs.get('topleft',(w/2,h/2))
center = kwargs.get('center')
textsurf = kwargs.get('surface',surface)
maxwidth = kwargs.get('width',w)
try:
myfont = pygame.font.Font('/storage/emulated/0/games/' + fontface, fontsize)
except:
myfont = pygame.font.SysFont(fontface, fontsize, bold=b)
x,y = topleft
charwidth = myfont.size(' ')[0]
charheight = fontsize + 3
if center:
for l in text.splitlines():
mytext = myfont.render(l, False, color)
textrect = mytext.get_rect(center=center)
center = (center[0],center[1]+charheight)
textsurf.blit(mytext,textrect)
else:
for line in text.splitlines():
for word in line.split(' '):
mytext = myfont.render(word, False, color)
textrect = mytext.get_rect(topleft=(x,y))
wordwidth = textrect.width
if x + wordwidth >= maxwidth:
x,y = (topleft[0], y + charheight)
textsurf.blit(mytext,(x,y))
x += charwidth + wordwidth
x,y = (topleft[0], y + charheight)
Created this function that could help a bit 🙂 Just make sure that each new paragraph is a new item on the list you are calling on this function.
def multilineText(Surface, textAsList: list, font: str, size: int, colour, antialias: bool, centerTupleCoord: tuple, spaceBetweenLines: int):
xPosition = centerTupleCoord[0]
yPosition = centerTupleCoord[1]
for paragraph in textAsList:
fontObjsrt = pygame.font.SysFont(font, size)
TextSurf = fontObjsrt.render(paragraph, antialias, colour)
TextRect = TextSurf.get_rect()
TextRect.center = (xPosition, yPosition)
Surface.blit(TextSurf, TextRect)
yPosition += spaceBetweenLines
There is no automatic solution. You have to implement the text wrap by yourself and draw the text line by line respectively word by word.
Fortunately PyGame wiki provides a function that for this task. See PyGame wiki Simple Text Wrapping for pygame.
I’ve extended the function and added an additional argument, which provides left or right aligned text, centerd text or even block mode.
Minimal example: repl.it/@Rabbid76/PyGame-TextWrap
import pygame
pygame.init()
font = pygame.font.SysFont(None, 40)
textAlignLeft = 0
textAlignRight = 1
textAlignCenter = 2
textAlignBlock = 3
def drawText(surface, text, color, rect, font, align=textAlignLeft, aa=False, bkg=None):
lineSpacing = -2
spaceWidth, fontHeight = font.size(" ")[0], font.size("Tg")[1]
listOfWords = text.split(" ")
if bkg:
imageList = [font.render(word, 1, color, bkg) for word in listOfWords]
for image in imageList: image.set_colorkey(bkg)
else:
imageList = [font.render(word, aa, color) for word in listOfWords]
maxLen = rect[2]
lineLenList = [0]
lineList = [[]]
for image in imageList:
width = image.get_width()
lineLen = lineLenList[-1] + len(lineList[-1]) * spaceWidth + width
if len(lineList[-1]) == 0 or lineLen <= maxLen:
lineLenList[-1] += width
lineList[-1].append(image)
else:
lineLenList.append(width)
lineList.append([image])
lineBottom = rect[1]
lastLine = 0
for lineLen, lineImages in zip(lineLenList, lineList):
lineLeft = rect[0]
if align == textAlignRight:
lineLeft += + rect[2] - lineLen - spaceWidth * (len(lineImages)-1)
elif align == textAlignCenter:
lineLeft += (rect[2] - lineLen - spaceWidth * (len(lineImages)-1)) // 2
elif align == textAlignBlock and len(lineImages) > 1:
spaceWidth = (rect[2] - lineLen) // (len(lineImages)-1)
if lineBottom + fontHeight > rect[1] + rect[3]:
break
lastLine += 1
for i, image in enumerate(lineImages):
x, y = lineLeft + i*spaceWidth, lineBottom
surface.blit(image, (round(x), y))
lineLeft += image.get_width()
lineBottom += fontHeight + lineSpacing
if lastLine < len(lineList):
drawWords = sum([len(lineList[i]) for i in range(lastLine)])
remainingText = ""
for text in listOfWords[drawWords:]: remainingText += text + " "
return remainingText
return ""
msg = "Simple function that will draw text and wrap it to fit the rect passed. If there is any text that will not fit into the box, the remaining text will be returned."
textRect = pygame.Rect(100, 100, 300, 300)
window = pygame.display.set_mode((500, 500))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.fill((255, 255, 255))
pygame.draw.rect(window, (0, 0, 0), textRect, 1)
drawTextRect = textRect.inflate(-5, -5)
drawText(window, msg, (0, 0, 0), drawTextRect, font, textAlignBlock, True)
pygame.display.flip()
pygame.quit()
exit()
This will be similar to what others have posted, but I figured I’d upload my own code too:
def box_text(surface, font, x_start, x_end, y_start, text, colour):
x = x_start
y = y_start
words = text.split(' ')
for word in words:
word_t = font.render(word, True, colour)
if word_t.get_width() + x <= x_end:
surface.blit(word_t, (x, y))
x += word_t.get_width() + 2
else:
y += word_t.get_height() + 4
x = x_start
surface.blit(word_t, (x, y))
x += word_t.get_width() + 2
I think it’s pretty self explanatory, you enter where you want your text to start (x_start) and where it should end, and then it pretty much goes down until your entered String is done.
An example of when I used in a personal project:
x_start = self.W / 2 - info_box.get_width() / 2 + 10
self.box_text(self.WINDOW, self.info_font, x_start, x_start + 430, self.H / 3 + 10, self.mage_text, self.white)
You would have to do some minor changes if you want the text to be justified though.
As of pygame-ce
(a modern fork of pygame
) version 2.1.4
pygame.Font.render
now has newline ("n"
) and wraplength (the width in pixels before wrapping to a new line) support.
Thus with these new and shiny features, solving this issue becomes a piece of cake, you just pass the text you want to pygame.Font.render
and specify the window width as the last argument so that it wraps once the text reaches the width of the window. Since this method doesn’t yet accept keyword arguments you must provide the background colour argument too, it can be None
or you can provide a colour value with an alpha value of 0.
Here is a short code example:
import pygame
WIDTH, HEIGHT = 640, 360
FPS = 60
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
font = pygame.Font(None, 32)
text_surf = font.render(
"This game is a combination of all of the trendsn "
"of 2016. When you press 'Start Game,' a menu will pop up. "
"In order to beat the game, you must get a perfect score on every single one of these games.",
True,
"white",
None, # or (0, 0, 0, 0) for example
WIDTH,
)
running = True
while running:
clock.tick(FPS)
screen.fill("black")
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
running = False
screen.blit(text_surf, (0, 0))
pygame.display.flip()
And the result:
Additionally to install the latest version of pygame-ce
simply follow these steps:
pip uninstall pygame
(to avoid conflict, this has to be done only if you have pygame
installed already)
pip install pygame-ce
- Enjoy the new and shiny features
More info about the fork can be found here: Pygame: Community Edition Announcement
And the version specific announcement here: Pygame-ce 2.1.4 release
I am trying to make a game and I am trying to render a lot of text. When the text renders, the rest of the text goes off the screen. Is there any easy way to make the text go to the next line of the pygame window?
helpT = sys_font.render
("This game is a combination of all of the trendsn of 2016. When you press 'Start Game,' a menu will pop up. In order to beat the game, you must get a perfect score on every single one of these games.",0,(hecolor))
screen.blit(helpT,(0, 0))
There is no easy way to render text on multiple lines in pygame, but this helper function could provide some use to you. Just pass in your text (with newlines), x, y, and font size.
def render_multi_line(text, x, y, fsize)
lines = text.splitlines()
for i, l in enumerate(lines):
screen.blit(sys_font.render(l, 0, hecolor), (x, y + fsize*i))
As I said in the comments; you have to render each word separately and calculate if the width of the text extends the width of the surface (or screen). Here’s an example:
import pygame
pygame.init()
SIZE = WIDTH, HEIGHT = (1024, 720)
FPS = 30
screen = pygame.display.set_mode(SIZE, pygame.RESIZABLE)
clock = pygame.time.Clock()
def blit_text(surface, text, pos, font, color=pygame.Color('black')):
words = [word.split(' ') for word in text.splitlines()] # 2D array where each row is a list of words.
space = font.size(' ')[0] # The width of a space.
max_width, max_height = surface.get_size()
x, y = pos
for line in words:
for word in line:
word_surface = font.render(word, 0, color)
word_width, word_height = word_surface.get_size()
if x + word_width >= max_width:
x = pos[0] # Reset the x.
y += word_height # Start on new row.
surface.blit(word_surface, (x, y))
x += word_width + space
x = pos[0] # Reset the x.
y += word_height # Start on new row.
text = "This is a really long sentence with a couple of breaks.nSometimes it will break even if there isn't a break "
"in the sentence, but that's because the text is too long to fit the screen.nIt can look strange sometimes.n"
"This function doesn't check if the text is too high to fit on the height of the surface though, so sometimes "
"text will disappear underneath the surface"
font = pygame.font.SysFont('Arial', 64)
while True:
dt = clock.tick(FPS) / 1000
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
screen.fill(pygame.Color('white'))
blit_text(screen, text, (20, 20), font)
pygame.display.update()
Result
This is how I did it
amfolyt_beskrivelse_text = ['en amfolyt er et stof som både kan være en base, eller syre','så som']
for x in amfolyt_beskrivelse_text:
descriptioncounter += 1
screen.blit((pygame.font.SysFont('constantia',12).render(x, True, BLACK)),(300,10*descriptioncounter))
descriptioncounter = 0
but of course, I can only do that because my text starts a line distance from the top of the screen. If you start further down the screen you could do
(300,12+12*descriptioncounter)
One thing you could do is use a monospaced font. They have the same size for all characters and so are beloved by programmers. That’s going to be my solution for handling the height/width problem.
You could use a .json file to load each line.
.json file (called first.json):
["Hello!", "How's it going?"]
And then load it into the file:
sys_font = pygame.font.SysFont(("Arial"),30)
def message_box(text):
pos = 560 # depends on message box location
pygame.draw.rect(root, (0,0,0), (100, 550, 800, 200)) #rectangle position varies
for x in range(len(text)):
rendered = sys_font.render(text[x], 0, (255,255,255))
root.blit(rendered, ( 110, pos))
pos += 30 # moves the following line down 30 pixels
with open('first.json') as text:
message_box(json.load(text))
Don’t forget to import json
Hope this helps!
I recommend the ptext
library which is able to recognize newline (n
) characters. You only need to call ptext.draw(text, position)
.
import pygame as pg
import ptext
pg.init()
screen = pg.display.set_mode((640, 480))
clock = pg.time.Clock()
BG_COLOR = pg.Color('gray12')
BLUE = pg.Color('dodgerblue')
# Triple quoted strings contain newline characters.
text = """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum."""
done = False
while not done:
for event in pg.event.get():
if event.type == pg.QUIT:
done = True
screen.fill(BG_COLOR)
ptext.draw(text, (10, 10), color=BLUE) # Recognizes newline characters.
pg.display.flip()
clock.tick(60)
pg.quit()
Building on previous answers I made a somewhat comprehensive blit text function I can use with a single short command:
def blittext(text, **kwargs):
#blit text into screen, uses defaults and works for multiline strings
fontface = kwargs.get('font', 'PressStart2P-Regular.ttf')
b = kwargs.get('bold', False)
fontsize = kwargs.get('size', 30)
color = kwargs.get('color', (255, 255, 255))
topleft = kwargs.get('topleft',(w/2,h/2))
center = kwargs.get('center')
textsurf = kwargs.get('surface',surface)
maxwidth = kwargs.get('width',w)
try:
myfont = pygame.font.Font('/storage/emulated/0/games/' + fontface, fontsize)
except:
myfont = pygame.font.SysFont(fontface, fontsize, bold=b)
x,y = topleft
charwidth = myfont.size(' ')[0]
charheight = fontsize + 3
if center:
for l in text.splitlines():
mytext = myfont.render(l, False, color)
textrect = mytext.get_rect(center=center)
center = (center[0],center[1]+charheight)
textsurf.blit(mytext,textrect)
else:
for line in text.splitlines():
for word in line.split(' '):
mytext = myfont.render(word, False, color)
textrect = mytext.get_rect(topleft=(x,y))
wordwidth = textrect.width
if x + wordwidth >= maxwidth:
x,y = (topleft[0], y + charheight)
textsurf.blit(mytext,(x,y))
x += charwidth + wordwidth
x,y = (topleft[0], y + charheight)
Created this function that could help a bit 🙂 Just make sure that each new paragraph is a new item on the list you are calling on this function.
def multilineText(Surface, textAsList: list, font: str, size: int, colour, antialias: bool, centerTupleCoord: tuple, spaceBetweenLines: int):
xPosition = centerTupleCoord[0]
yPosition = centerTupleCoord[1]
for paragraph in textAsList:
fontObjsrt = pygame.font.SysFont(font, size)
TextSurf = fontObjsrt.render(paragraph, antialias, colour)
TextRect = TextSurf.get_rect()
TextRect.center = (xPosition, yPosition)
Surface.blit(TextSurf, TextRect)
yPosition += spaceBetweenLines
There is no automatic solution. You have to implement the text wrap by yourself and draw the text line by line respectively word by word.
Fortunately PyGame wiki provides a function that for this task. See PyGame wiki Simple Text Wrapping for pygame.
I’ve extended the function and added an additional argument, which provides left or right aligned text, centerd text or even block mode.
Minimal example: repl.it/@Rabbid76/PyGame-TextWrap
import pygame
pygame.init()
font = pygame.font.SysFont(None, 40)
textAlignLeft = 0
textAlignRight = 1
textAlignCenter = 2
textAlignBlock = 3
def drawText(surface, text, color, rect, font, align=textAlignLeft, aa=False, bkg=None):
lineSpacing = -2
spaceWidth, fontHeight = font.size(" ")[0], font.size("Tg")[1]
listOfWords = text.split(" ")
if bkg:
imageList = [font.render(word, 1, color, bkg) for word in listOfWords]
for image in imageList: image.set_colorkey(bkg)
else:
imageList = [font.render(word, aa, color) for word in listOfWords]
maxLen = rect[2]
lineLenList = [0]
lineList = [[]]
for image in imageList:
width = image.get_width()
lineLen = lineLenList[-1] + len(lineList[-1]) * spaceWidth + width
if len(lineList[-1]) == 0 or lineLen <= maxLen:
lineLenList[-1] += width
lineList[-1].append(image)
else:
lineLenList.append(width)
lineList.append([image])
lineBottom = rect[1]
lastLine = 0
for lineLen, lineImages in zip(lineLenList, lineList):
lineLeft = rect[0]
if align == textAlignRight:
lineLeft += + rect[2] - lineLen - spaceWidth * (len(lineImages)-1)
elif align == textAlignCenter:
lineLeft += (rect[2] - lineLen - spaceWidth * (len(lineImages)-1)) // 2
elif align == textAlignBlock and len(lineImages) > 1:
spaceWidth = (rect[2] - lineLen) // (len(lineImages)-1)
if lineBottom + fontHeight > rect[1] + rect[3]:
break
lastLine += 1
for i, image in enumerate(lineImages):
x, y = lineLeft + i*spaceWidth, lineBottom
surface.blit(image, (round(x), y))
lineLeft += image.get_width()
lineBottom += fontHeight + lineSpacing
if lastLine < len(lineList):
drawWords = sum([len(lineList[i]) for i in range(lastLine)])
remainingText = ""
for text in listOfWords[drawWords:]: remainingText += text + " "
return remainingText
return ""
msg = "Simple function that will draw text and wrap it to fit the rect passed. If there is any text that will not fit into the box, the remaining text will be returned."
textRect = pygame.Rect(100, 100, 300, 300)
window = pygame.display.set_mode((500, 500))
run = True
while run:
for event in pygame.event.get():
if event.type == pygame.QUIT:
run = False
window.fill((255, 255, 255))
pygame.draw.rect(window, (0, 0, 0), textRect, 1)
drawTextRect = textRect.inflate(-5, -5)
drawText(window, msg, (0, 0, 0), drawTextRect, font, textAlignBlock, True)
pygame.display.flip()
pygame.quit()
exit()
This will be similar to what others have posted, but I figured I’d upload my own code too:
def box_text(surface, font, x_start, x_end, y_start, text, colour):
x = x_start
y = y_start
words = text.split(' ')
for word in words:
word_t = font.render(word, True, colour)
if word_t.get_width() + x <= x_end:
surface.blit(word_t, (x, y))
x += word_t.get_width() + 2
else:
y += word_t.get_height() + 4
x = x_start
surface.blit(word_t, (x, y))
x += word_t.get_width() + 2
I think it’s pretty self explanatory, you enter where you want your text to start (x_start) and where it should end, and then it pretty much goes down until your entered String is done.
An example of when I used in a personal project:
x_start = self.W / 2 - info_box.get_width() / 2 + 10
self.box_text(self.WINDOW, self.info_font, x_start, x_start + 430, self.H / 3 + 10, self.mage_text, self.white)
You would have to do some minor changes if you want the text to be justified though.
As of pygame-ce
(a modern fork of pygame
) version 2.1.4
pygame.Font.render
now has newline ("n"
) and wraplength (the width in pixels before wrapping to a new line) support.
Thus with these new and shiny features, solving this issue becomes a piece of cake, you just pass the text you want to pygame.Font.render
and specify the window width as the last argument so that it wraps once the text reaches the width of the window. Since this method doesn’t yet accept keyword arguments you must provide the background colour argument too, it can be None
or you can provide a colour value with an alpha value of 0.
Here is a short code example:
import pygame
WIDTH, HEIGHT = 640, 360
FPS = 60
pygame.init()
screen = pygame.display.set_mode((WIDTH, HEIGHT))
clock = pygame.time.Clock()
font = pygame.Font(None, 32)
text_surf = font.render(
"This game is a combination of all of the trendsn "
"of 2016. When you press 'Start Game,' a menu will pop up. "
"In order to beat the game, you must get a perfect score on every single one of these games.",
True,
"white",
None, # or (0, 0, 0, 0) for example
WIDTH,
)
running = True
while running:
clock.tick(FPS)
screen.fill("black")
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
running = False
screen.blit(text_surf, (0, 0))
pygame.display.flip()
And the result:
Additionally to install the latest version of pygame-ce
simply follow these steps:
pip uninstall pygame
(to avoid conflict, this has to be done only if you havepygame
installed already)pip install pygame-ce
- Enjoy the new and shiny features
More info about the fork can be found here: Pygame: Community Edition Announcement
And the version specific announcement here: Pygame-ce 2.1.4 release