I just made this ASCII based renderer for my game and it doesnt seem to be very fast. Ive tried asking chatGPT and Github Copilot but those werent any help (Thats why you find comments in the form of questions). I sadly cant provide the code im using for my actual game but only drawing a single dot already brings the fps down to 10, and this doesnt change at all when you render more. Im doing a window resolution of 720P and the grid of actual characters is 200x50, ive tried gpu acceleration, threading, and PyOpenGL which did seem to fix the issue but made the code completely unreadable to me.
If you find any solution, please tell me. Heres the code :
from colorama import Fore, Style
import os
import time
import threading
import keyboard
import math
import pygame
import random
if os.name == "nt": # Windows
os.environ["SDL_VIDEODRIVER"] = "directx" # or "windib"
elif os.name == "posix": # Linux
os.environ["SDL_VIDEODRIVER"] = "x11"
# the first list inside of screen is x, and the one inside of that is y
screen = [[]]
last_screen = [[]]
lines = []
stop_cls = False
# initialize pygame for the terminal class
pygame.init()
# make screenX and screenY variables 720p and make a screen
screenX = 1280
screenY = 720
try:
screen = pygame.display.set_mode((screenX, screenY))
except:
# try setting hardware acceleration to windib
os.environ["SDL_VIDEODRIVER"] = "windib"
screen = pygame.display.set_mode((screenX, screenY))
def render(clear=True):
global screen
lines = []
for y in range(len(screen[0])):
line = ""
for x in range(len(screen)):
line += screen[x][y]
lines.append(line)
text = "\n".join(lines)
if clear:
os.system("cls")
print(text, end='', flush=True) # Add flush=True to force immediate update
def clear(size=[10, 10], cnvrt=False):
# cnvrt is if you want to convert the screen size to be more on the x axis so the screen is more symetrical.
# usually if you did 50,50 it would be more of a rectangle than a square so this will provent that by making the size different
"""
global screen
# set the screen x and y size to size
screen = []
for i in range(size):
screen.append([])
for j in range(size):
screen[i].append(" ")
return screen
"""
global screen
if cnvrt:
# make size bigger on the x axis
size[1] = int(size[0] * 3)
screen = []
for i in range(size[1]):
screen.append([])
for j in range(size[0]):
screen[i].append(" ")
return size
def wait(fps):
time.sleep(1/fps)
def get_input(key=pygame.K_0):
# use pygame to return if a key is pressed
return pygame.key.get_pressed()[key]
def set(x, y, char, font_size=10):
global screen
#y -= 38 #20 font size
y -= 70 #10 font size
try:
screen[x][y] = char
except:
pass
def get(x, y):
global screen
#y -= 38 #20 font size
y -= 70 #10 font size
try:
return screen[x][y]
except:
return " "
def set_cube(xy, char):
# xy is a list like [x1, y1, x2, y2]
# and x2 and y2 is x1 and y1 + x2 and y2 so if you do [10, 10, 2, 2] it will make a 2x2 cube at 10, 10
global screen
x1, y1, x2, y2 = xy
screen_slice = [row[x1:x1+x2] for row in screen[y1:y1+y2]]
for row in screen_slice:
row[:] = char * len(row)
def set_line(x1, y1, x2, y2, char):
global screen
dx = abs(x2 - x1)
dy = abs(y2 - y1)
sx = 1 if x1 < x2 else -1
sy = 1 if y1 < y2 else -1
err = dx - dy
while x1 != x2 or y1 != y2:
set(x1, y1, char)
e2 = 2 * err
if e2 > -dy:
err -= dy
x1 += sx
if e2 < dx:
err += dx
y1 += sy
def set_texture(filename, x, y, transparent=False):
"""Draw a texture from a text file at the given position."""
with open(filename, 'r') as f:
lines = f.readlines()
for i, line in enumerate(lines):
for j, char in enumerate(line.strip()):
if char != ' ' or not transparent:
set(x + j, y + i, char)
def rotate_x(x, y, z, angle):
"""Rotate a point around the x-axis."""
rad = math.radians(angle)
cos = math.cos(rad)
sin = math.sin(rad)
y1 = y * cos - z * sin
z1 = y * sin + z * cos
return x, y1, z1
# x, y, z = rotate_x(x, y, z, angle)
def rotate_y(x, y, z, angle):
"""Rotate a point around the y-axis."""
rad = math.radians(angle)
cos = math.cos(rad)
sin = math.sin(rad)
x1 = x * cos - z * sin
z1 = x * sin + z * cos
return x1, y, z1
def rotate_z(x, y, z, angle):
"""Rotate a point around the z-axis."""
rad = math.radians(angle)
cos = math.cos(rad)
sin = math.sin(rad)
x1 = x * cos - y * sin
y1 = x * sin + y * cos
return x1, y1, z
def project(x, y, z):
"""Project a 3D point onto a 2D plane."""
factor = 200 / (200 + z)
x1 = x * factor + 40
y1 = -y * factor + 12
return int(x1), int(y1)
def draw_cube(x, y, z, size, char, angle_x=0, angle_y=0, angle_z=0):
"""Draw a cube at the given 3D position."""
# Define the 8 corners of the cube
corners = [
(x - size, y - size, z - size),
(x + size, y - size, z - size),
(x + size, y + size, z - size),
(x - size, y + size, z - size),
(x - size, y - size, z + size),
(x + size, y - size, z + size),
(x + size, y + size, z + size),
(x - size, y + size, z + size),
]
# Connect the corners to form the edges of the cube
edges = [
(0, 1), (1, 2), (2, 3), (3, 0),
(4, 5), (5, 6), (6, 7), (7, 4),
(0, 4), (1, 5), (2, 6), (3, 7),
]
# Rotate the corners around the x, y, and z axes
for i, corner in enumerate(corners):
corners[i] = rotate_x(*corner, angle_x)
corners[i] = rotate_y(*corners[i], angle_y)
corners[i] = rotate_z(*corners[i], angle_z)
# Project the corners onto the 2D plane and draw the edges
for edge in edges:
x1, y1 = project(*corners[edge[0]])
x2, y2 = project(*corners[edge[1]])
set_line(x1, y1, x2, y2, char)
import pygame
class PygameTerminal:
def __init__(self, width=800, height=600, font_size=20):
pygame.init()
self.width = width
self.height = height
self.font_size = font_size
self.font = pygame.font.SysFont("Courier New", font_size) # Use a monospaced font
self.screen = pygame.display.set_mode((width, height))
self.background_color = (0, 0, 0)
self.text_color = (255, 255, 255)
self.text_buffer = []
self.text_margin = 10
self.line_height = font_size
self.max_lines = (height - 2 * self.text_margin) // self.line_height
pygame.display.set_caption("Pygame Terminal")
self.buffer_needs_update = True # Flag to check if buffer needs re-rendering
def _render_text(self):
# Create an off-screen buffer to render the text
buffer = pygame.Surface((self.width, self.height))
buffer.fill(self.background_color)
# Render the new lines to the buffer
lines_to_render = self.text_buffer[-self.max_lines:]
start_index = max(0, len(self.text_buffer) - self.max_lines)
y_offset = self.text_margin + (start_index * self.line_height)
for line in lines_to_render:
for split_line in line.split("\n"):
text_surface = self.font.render(split_line, True, self.text_color)
buffer.blit(text_surface, (self.text_margin, y_offset))
y_offset += self.line_height
# Copy the buffer to the screen in a single operation
self.screen.blit(buffer, (0, 0))
pygame.display.flip()
# Update the last rendered line index
self.last_rendered_line = len(self.text_buffer)
def print(self, text):
if not text:
return
# Split the text into lines that fit within the terminal height
lines = text.split('\n')
self.text_buffer.extend(lines)
self.text_buffer = self.text_buffer[-self.max_lines:] # Keep the buffer size within limit
self.buffer_needs_update = True # Set the flag to indicate buffer update is required
def update(self):
if self.buffer_needs_update:
self._render_text()
self.buffer_needs_update = False
def clear(self):
self.text_buffer = []
self.buffer_needs_update = True # Clearing buffer requires re-rendering
def run_event_loop(self):
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
pygame.quit()
def updates(dirty_rect=False):
global screen, lines, last_screen
# Update the existing PygameTerminal instance instead of creating a new one
term = PygameTerminal(font_size=10, width=1280, height=720)
if screen != last_screen:
term.clear()
beginX = 0
beginY = 0
endX = 0
endY = 0
if len(screen) != len(last_screen) or len(screen[0]) != len(last_screen[0]) or random.randint(0, 20) == 0 or dirty_rect == False:
# if the screen size changed, just draw the whole screen
lines = []
for y in range(len(screen[0])):
line = ""
for x in range(len(screen)):
line += screen[x][y]
lines.append(line)
last_screen = screen
else:
try:
# now calculate the dirty rect
for y in range(len(screen[0])):
for x in range(len(screen)):
if screen[x][y] != last_screen[x][y]:
if beginX == 0 and beginY == 0:
beginX = x
beginY = y
endX = x
endY = y
# make the begin and end a little bigger so it doesnt make shadows
beginX -= 1
beginY -= 1
endX += 1
endY += 1
# now draw the dirty rect
for y in range(beginY, endY + 1):
line = ""
for x in range(beginX, endX + 1):
line += screen[x][y]
# calculate what needs to change inside lines to change the screen only in the dirty rect
lines[y] = lines[y][:beginX] + line + lines[y][endX + 1:]
last_screen = screen
except:
# just draw the whole screen if there is an error
lines = []
for y in range(len(screen[0])):
line = ""
for x in range(len(screen)):
line += screen[x][y]
lines.append(line)
last_screen = screen
term.print("\n".join(lines))
term.update()
Sorry that its so long, but im extremely certain that the problem lies inside the 'updates()' function and the PygameTerminal class.
I tried PyOpenGL, ive tried rewriting it completely and i was expecting it to render at more then 60 frames per second seeing how this is not suppost to be graphically demanding.