How to render an isometric tile-based world in Python? - python

I'm having a bit of trouble rendering an isometric 2D tile-based world using Python and Pygame with the following code:
'''
Map Rendering Demo
rendermap.py
By James Walker (trading as Ilmiont Software).
Copyright (C)Ilmiont Software 2013. All rights reserved.
This is a simple program demonstrating rendering a 2D map in Python with Pygame from a list of map data.
Support for isometric or flat view is included.
'''
import pygame
from pygame.locals import *
pygame.init()
DISPLAYSURF = pygame.display.set_mode((640, 480), DOUBLEBUF) #set the display mode, window title and FPS clock
pygame.display.set_caption('Map Rendering Demo')
FPSCLOCK = pygame.time.Clock()
map_data = [
[1, 1, 1, 1, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 0, 0, 0, 1],
[1, 1, 1, 1, 1]
] #the data for the map expressed as [row[tile]].
wall = pygame.image.load('wall.png').convert() #load images
grass = pygame.image.load('grass.png').convert()
tileWidth = 64 #holds the tile width and height
tileHeight = 64
currentRow = 0 #holds the current map row we are working on (y)
currentTile = 0 #holds the current tile we are working on (x)
for row in map_data: #for every row of the map...
for tile in row:
tileImage = wall
cartx = currentTile * 64 #x is the index of the currentTile * the tile width
print(cartx)
carty = currentRow * 64 #y is the index of the currentRow * the tile height
print(carty)
x = cartx - carty
print(x)
y = (cartx + carty) / 2
print(y)
print('\n\n')
currentTile += 1 #increase the currentTile holder so we know that we are starting rendering a new tile in a moment
DISPLAYSURF.blit(tileImage, (x, y)) #display the actual tile
currentTile = 0 #reset the current working tile to 0 (we're starting a new row remember so we need to render the first tile of that row at index 0)
currentRow += 1 #increment the current working row so we know we're starting a new row (used for calculating the y coord for the tile)
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
if event.type == KEYUP:
if event.key == K_ESCAPE:
pygame.quit()
sys.exit()
pygame.display.flip()
FPSCLOCK.tick(30)
The tile size used is 64x64; the above code when run generates the following output:
The tiles all have transparent edges and only the 'wall' tile is featured in this example but obviously something is going wrong as the tiles are all too far apart.
I have tried reading some tutorials online but I can't seem to find one actually written in Python so please advise me as to where I am going wrong.
Thanks in advance,
Ilmiont

Try this:
print(carty)
x = (cartx - carty) / 2
print(x)
y = (cartx + carty)/4*3
And, after test, modify convert() to convert_alpha(), because optimisation kill alpha on convert()
I also modify y ( /4*3 ), to take in account your image. It's work for me.

You can get rid of the black triangles by setting the colorkey to black, I copied your example and was able to fix it by changing:
for row in map_data: #for every row of the map...
for tile in row:
tileImage = wall
to
for row in map:
for tile in row:
tileImage = wall
tileImage.set_colorkey((0,0,0))
Found from:
https://www.pygame.org/docs/ref/surface.html#pygame.Surface.set_colorkey
There is probably a better way to set the colorkey for all, but I tried this and it seems to work. I see the comment about about alpha, I'm sure that works better too.
In regards to positioning, You would just need to start rendering from the middle of the screen. I was able to do this with your code by simple changing:
x = cartx - carty
print(x)
y = (cartx + carty) / 2
to
x = 320 + ((cartx - carty) / 2)
y = ((cartx+carty) / 4 * 3)
I hope this helps any newcomers to this thread!

Related

how can i move a rectangle in py game without mouse and keys?

I have code to move a rectangle in pygame from left to right, up and down.
But I want to move my rectangle around the screen that I created..
can someone help me please?
import pygame
from pygame.locals import *
pygame.init()
FPS = 70
fpsclock = pygame.time.Clock()
SIZE = (1000, 700)
form1 = pygame.display.set_mode(SIZE)
WHITE=(255, 255, 255)
x = 0
y = 0
w = 50
h = 60
direction = 'right'
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
form1.fill(WHITE)
pygame.draw.rect(form1, (255, 0, 0), (x, y, w, h), 1)
pygame.display.update()
fpsclock.tick(FPS)
if x,y==0,0:
direction='right'
if x,y==1200-50,0:
direction='down'
if x,y==1200-50,700-60:
direction='left'
if x,y==0,1200-50:
direction='right'
So the first thing you have to look at is the spacing. Even though your code works (after proper indentation) the square goes out of bounds.
The same thing applies to y as well if the square should go up and down.
If you want the square to go around you just need to go left, right, up, or down at the correct time. So if you want to start at the left upper corner and go around you just need to check if the square is in a corner and then change the direction accordingly.
Keep in mind that going down actually increases and going up decreases y.
EDIT:
Here you can see the result of my proposed concept
EDIT 2:
I've copied your code and refactored and completed it. I tried to explain why I did what I did.
import pygame
# Only import the things you need it makes debugging and documenting
# alot easier if you know where everything is coming from
from pygame.locals import QUIT
FPS = 70
# Use variables to define width and height in case you want to
# change it later
width = 200
height = 200
# I would group variables by usage, for example, I would group width,
# height of the screen and w, h of the box so I can easily manipulate
# the numbers if want to.
w = 50
h = 60
# Using an offset variable reduces the hardcoded numbers even more
# if its 0 it will just move along the border but if it >0 it will move
# the square to the centre
offset = 20
# You can declare it as a variable if you need the SIZE tuple somewhere
# else, if you don't need it you can just set it as
# pygame.display.set_mode((width, height))
SIZE = (width, height)
WHITE = (255, 255, 255)
RED = (255, 0, 0)
x = offset
y = offset
direction = 'right'
# This is just my preference but I like to have the variables that are
# changeable at the top for easy access. I think this way the code is
# cleaner.
pygame.init()
fpsclock = pygame.time.Clock()
form1 = pygame.display.set_mode(SIZE)
while True:
for event in pygame.event.get():
if event.type == QUIT:
exit()
form1.fill(WHITE)
# Try to avoid hardcoded numbers as much as possible, hardcoded
# numbers are hard to change later on when the code gets to certain
# size and complexity.
pygame.draw.rect(form1, RED, (x, y, w, h), 1)
pygame.display.update()
fpsclock.tick(FPS)
# Don't harcode conditions, use variables so you can easily change
# them later
if x == offset and y == offset:
direction='right'
if x == width - w - offset and y == offset:
direction='down'
if x == width - w - offset and y == height - h - offset:
direction='left'
if x == offset and y == height - h - offset:
direction='up'
if direction == 'right':
x += 5
elif direction == 'down':
#Keep in mind going down actually means to increment y
y += 5
elif direction == 'left':
x -= 5
elif direction == 'up':
y -= 5

Speed up double for loop PyGame draw

I have a pygame game and it is currently bottlenecking in the Draw to screen process. This is the code (pg is pygame):
def draw_living_cells(self):
self.screen.fill(BLACK)
for x in range(0, GRID_WIDTH + 1):
for y in range(0, GRID_HEIGHT):
if self.grid[x + 1][y + 1] == 1:
pos = (int(x * CELL_SIZE), int(y * CELL_SIZE), int(CELL_SIZE), int(CELL_SIZE))
pg.draw.rect(self.screen, LIFE_COLOR, pos, 0)
pg.display.flip()
I thought multiprocessing could help, but I'm not sure how to implement, if it is possible (due to possible shared memory issues) or if it would help at all.
This process takes about 20ms with a self.grid of size 200x150 in a 800x600 display. I think its odd to have ~50fps in such a simple process.
Use pygame.PixelArray for direct pixel access of the target Surface. Set the pixels directly, instead of drawing each cell separately by pygame.draw.rect():
def draw_living_cells(self):
self.screen.fill(BLACK)
pixel_array = pg.PixelArray(self.screen)
size = self.screen.get_size()
for x in range(0, GRID_WIDTH + 1):
for y in range(0, GRID_HEIGHT):
if self.grid[x + 1][y + 1] == 1:
rect = pygame.Rect(x * CELL_SIZE, y * CELL_SIZE, CELL_SIZE, CELL_SIZE)
pixel_array[rect.left : rect.right, rect.top : rect.bottom] = LIFE_COLOR
pixel_array.close()
pg.display.flip()
I would suggest that you modify your code to use pygame's Sprite mechanics and in particular look at the sprite group pygame.sprite.DirtySprite. You can then mark the cells that have changed as dirty and have it only redraw those cells instead of all the cells.
This change would also require you to not redraw the entire background with the self.screen.fill(BLACK).
Since your method is named draw_living_cells(), that implies that there are dead cells that you do not redraw. Since you would not be filling the entire background that means that you have to draw the background onto the screen where the dead cell used to be.
This of course only helps if some the of the cells do not change each pass. Otherwise you are just adding overhead without saving drawing.
Though I recommend Sprites and pygame.sprite.DirtySprite, you can of course do something similar yourself by just marking your cells and doing that yourself in your redraw.
Finally, I tried something similar to what Glenn suggested. I used numpy to obtain where the living cells are and then iterate through those coordinates instead of the whole grid. This gave 2.5~3x performance increase. New Code:
def draw_living_cells(self):
self.screen.fill(BLACK)
living_cells_xy = np.where(self.grid == 1)
living_cells = len(living_cells_xy[0])
for i in range(0, living_cells):
x = living_cells_xy[0][i] - 1
y = living_cells_xy[1][i] - 1
pos = (int(x * CELL_SIZE), int(y * CELL_SIZE), int(CELL_SIZE), int(CELL_SIZE))
pg.draw.rect(self.screen, LIFE_COLOR, pos, 0)
pg.display.flip()

Pygame lags with pygame rectangles added

I'm running a python game with approximately 2k rectangles being drawn on screen each frame.
The problem I have is that it runs on 12 fps and I have no idea how to fix that. When I remove all the rectangles it shifts to 100 fps. I'm not rendering all of them at once, but only the ones camera can see currently. How to fix this lag spike issue, is it because I'm using pygame rectangles or because I'm using them wrong?
here's the code
import pygame
black = (0,0,0)
pygame.init()
gameDisplay = pygame.display.set_mode((0,0),pygame.FULLSCREEN)
gameDisplay.fill(black)
gameDisplay.convert()
clock = pygame.time.Clock()
display_width = 1920
display_height = 1080
from opensimplex import OpenSimplex
tmp = OpenSimplex()
dimensions = [100,100]
size = 40
def mapping(x):
y = (x + 1) * 10 + 40
return y
class GroundCell:
def __init__(self,x,y,dim):
self.x = x
self.y = y
self.dim = dim
tempcells = []
allCells = []
for a in range(0,dimensions[0]):
tempcells = []
for b in range(0,dimensions[1]):
tempcells.append(GroundCell(a*size,b*size,mapping(tmp.noise2d(a*0.11,b*0.11))))
allCells.append(tempcells)
font = pygame.font.Font("freesansbold.ttf", 20)
while True:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.quit()
quit()
for a in allCells:
for b in a:
if b.x in range(0,display_width) \
and b.y in range(0,display_height) or\
b.x+size in range(0,display_width) \
and b.y+size in range(0,display_height) :
pygame.draw.rect(gameDisplay,(b.dim,b.dim,b.dim),(b.x,b.y,size,size))
fps = font.render('FPS: ' + str(int(clock.get_fps())), 1, (0, 0, 0))
gameDisplay.blit(fps, (20, 20))
pygame.display.update()
clock.tick(120)
gameDisplay.fill(black)
Instead of drawing all those rects to the screen every tick, create a Surface with the noise once, and reuse it.
Some more notes:
Text rendering is expensive. If you use a lot of text rendering, better cache the surfaces created by Font.render. Note there's also the new freetype module in pygame, which is better than the font module in every way.
Your check if a rect is inside the screen is very strange. You could just use something like if 0 < b.x < display_width ... or even use pygame's Rect class, which offers nice methods like contains().
A good and fast way to create a Surface from arbitary data is to use numpy and pygame's surfarray module. Don't get intimidated, it isn't that hard to use.
Here's a running example based on your code:
import pygame
import numpy as np
black = (0,0,0)
pygame.init()
display_width = 1000
display_height = 1000
gameDisplay = pygame.display.set_mode((display_width, display_height))
gameDisplay.fill(black)
clock = pygame.time.Clock()
from opensimplex import OpenSimplex
tmp = OpenSimplex()
dimensions = [100,100]
size = 16
def mapping(x):
y = (x + 1) * 10 + 40
return y
# create an 2d array from the noise
def get_array():
rgbarray = np.zeros((dimensions[0], dimensions[1]))
for x in range(dimensions[0]):
for y in range(dimensions[1]):
c = int(mapping(tmp.noise2d(x*0.11, y*0.11)))
# simple way to convert the color value to all three (r,g,b) channels
rgbarray[x, y] = c | c << 8 | c << 16
return rgbarray
# create the array and copy it into a Surface
data = get_array()
surface = pygame.Surface((dimensions[0], dimensions[1]))
pygame.surfarray.blit_array(surface, data)
# scale the Surface to the desired size
scaled = pygame.transform.scale(surface, (dimensions[0]*size, dimensions[1]*size))
# simple way to cache font rendering
font = pygame.font.Font("freesansbold.ttf", 20)
cache = {}
def render(text):
if not text in cache:
cache[text] = font.render(text, 1, (0, 0, 0))
return cache[text]
x = 0
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
x-=1
if x < -1000:
x = 0
gameDisplay.blit(scaled, (x, 0))
fps = render('FPS: ' + str(int(clock.get_fps())))
gameDisplay.blit(fps, (20, 20))
pygame.display.update()
clock.tick(120)
As #sloth explained, there are better ways to do the same, but if you want to know what the actual problems are:
You're not drawing 2000 rectangles, you're drawing 10.000 of them, because your dimension is 100x100
You're doing a check if the rectangle is visible in the worst possible way in terms of performance. Just check what happens if you leave the check and don't draw the rectangles. You'll see that the performance is improved but it's still far from 120fps. That's because for every rectangle you generate a list of numbers from 0 to the width of the screen and another list from zero to the height of the screen. You also do that twice. That means, on a 1920x1080 screen: (1920 * 10000) + (1920 * 10000) + (1080 * 10000) + (1080*10000) = 60000000. So, 60 million checks. If you have 120fps, that means 60 million * 120 = 7.2 billion checks per second.
Just changing the check to something similar to if b.x+size < display_width and b.y+size < display_height and b.x > 0 and b.y > 0: will already improve the performance.
That said, it's still 10000 rectangles, and it's still 120fps, which means 1200000 rectangles per second, done with basically no HW acceleration and with a high level language. Don't expect top performance.

Pygame Boundary Not Working

I'm a kid in middle school and hope to be a programmer when I grow up.I'm going to a summer school coding class and learning python and pygame.I already knew enough python but just got my hands wet in pygame.I was adding trying to add a boundary for my game but it's able to block the left and top of the screen here is my code:
import pygame,sys
from pygame.locals import *
pygame.init()
WIDTH = 400
HEIGHT = 400
pg = "player.gif"
bg = "y.gif"
screen=pygame.display.set_mode((WIDTH,HEIGHT))
background = pygame.image.load(bg)
player = pygame.image.load(pg)
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit
x,y = pygame.mouse.get_pos()
screen.blit(background,[0,0])
screen.blit(player,(x,y))
pygame.display.update()
if x <= WIDTH:
x = 0
if y <= HEIGHT:
y = 0
if x <= WIDTH:
x = 0
if y <= HEIGHT:
y = 0
Is this really what you want to do? Set x and y to zero whenever the mouse is positioned within the boundaries? OR do you want to limit x and y to only existing within the range from 0 to WIDTH or HEIGHT respectively?
x = min(max(x, 0), WIDTH)
y = min(max(y, 0), HEIGHT)
Furthermore, note that your player sprite has a width and height of its own. The x,y coordinate represents the location of the top-left corner of the sprite. If you want to restrict the sprite's position such that the entire thing is always on the screen, you first need to get the size of the sprite
spriteWidth, spriteHeight = player.get_rect().size
And then factor that size into your boundary calculation
x = min(max(x, 0), WIDTH - spriteWidth)
y = min(max(y, 0), HEIGHT - spriteHeight)
Additionally, you need to make sure you do this before you call screen.blit(player, (x, y)), or else the sprite will be drawn with the original, unbounded coordinates.

pygame rectangles drawn in loop scroll across screen

Here is my full code:
import pygame
pygame.init()
i = 0
x = 0
# Define the colors we will use in RGB format
BLACK = ( 0, 0, 0)
WHITE = (255, 255, 255)
BLUE = ( 0, 0, 255)
GREEN = ( 0, 255, 0)
RED = (255, 0, 0)
# Set the height and width of the screen
size = [600, 300]
screen = pygame.display.set_mode(size)
pygame.display.set_caption("Test")
#Loop until the user clicks the close button.
done = False
clock = pygame.time.Clock()
while not done:
clock.tick(10)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
screen.fill(WHITE)
for x in range(x, x+100, 10):
pygame.draw.rect(screen, BLACK, [x, 0, 10, 10], 1)
pygame.display.flip()
pygame.quit()
10 squares are drawn but they scroll across the window to the right and i dont know why. Is there any way I could stop this?
Thanks.
I realise now that it is not about the rectangle loop, but i have changed that to what has been suggested anyway.
Now that you have added more code I see where the problem is coming from and it's just as I suspected - you are doing a static translation the incorrect way namely you use the x (which you have defined outside your main loop) in your for x in range(x, x+100, 10):.
If you add a print(x) statement inside your for-loop you will be able to see that the x gets bigger and bigger, and bigger...This is perfect for adding dynamics to your scene but my guess is (based on your question) that you want to add 10 static rectangles to your scene.
In order to do that you need to reset your x every single time a new iteration of the while loop begins:
while not done:
clock.tick(10)
for event in pygame.event.get():
if event.type == pygame.QUIT:
done=True
screen.fill(WHITE)
# Every single iteration of the while loop will first reset the x to its initial value
# You can make x be any value you want your set of rectangles to start from
x = 0
# Now start adding up to x's initial value
for x in range(x, x+100, 10):
pygame.draw.rect(screen, BLACK, [x, 0, 10, 10], 1)
pygame.display.flip()
You can also omit the definition of x as a variable outside the for loop if you won't be using it to change the x coordinate where your first rectangle will start from and replace the range(x, x+100, 10) with range(CONST, CONST+100, 10) where CONST is a given value such as 0, 100, 1000 etc.
Every time your for loop runs the range is incremented, because the x is persistent. If you remove the x=0, reset it inside the while, or use a different variable in the for loop I think it will work.
x=0
while not done:
for x in range(x, x+100, 10):
pygame.draw.rect(screen, BLACK, [x, 0, 10, 10], 1)

Categories