How do I save a level layout in a variable - python

Currently, My next project is going to be a platformer and when I look around stackoverflow for research on several mechanics, I see many people doing the same thing: They save a layout with some variable, then go and unload it somewhere and it just renders in the game. I was interested, so I looked further and I found nothing on how to load/unload states like that, or maybe I'm just not wording my search correctly.
Either way, How do I do this?
ex: I would save a level layout as either an array or a single multi-line string and then somehow generate a single tile sprite for each letter, like T.
import pygame
# Storage method A
level = '''
X X X X X
X X X X X
T T X X X
X X X T T
T T T T T
'''
# Storage Method B
level2 = [
'XXXXX',
'XXXXX',
'TTXXX',
'XXXTT',
'TTTTT'
]
# X is blank space, T is tiles
# Then what? Thats what I need to know.
# If someone already answered this and I'm just not using the right keywords let me know.

You will need to calculate the pixel-positions for each tile. To draw any tile, you need to know
the size of the canvas
the size of your grid
the position of the tile in your grid
1: Finding the size of your canvas should be trivial.
2: For the second storage method you can do
height = len(level2)
width = len(level2[0]) #Assuming all rows are of equal length and there's at least one row
3: We're going to iterate through the rows and characters which will keep track of our position in the grid on the side.
def draw_tiles(canvas_width, canvas_height, width, height, level2):
for row in range(height):
for column in range(width):
if list(level2[row])[column] == 'T':
pixel_x = int(canvas_width/width)*column
pixel_y = int(canvas_height/height)*row
draw_tile(pixel_x, pixel_y)
Now all you need to do is define the draw_tile(x, y) function to draw a tile on the canvas with its top-left corner being on the pixel co-ordinates (x, y). I'm sure pygame has something for that.
Make sure you set the grid width/height so that canvas_width/width and canvas_height/height are both integers. Otherwise your tiles will be slightly offset due to rounding.

You could iterate over the enumerated rows and characters in the layout, create the tile instances and add them to a sprite group.
In the example I just give the tiles different colors depending on the character in the layout (X=blue, T=green) before I add them to the group, but you could also create completely different Tile types or subclasses if the character is a 'T' or an 'X'.
import pygame
class Tile(pygame.sprite.Sprite):
def __init__(self, pos, color):
super().__init__()
self.image = pygame.Surface((50, 50))
self.image.fill(color)
self.rect = self.image.get_rect(topleft=pos)
def create_tile_group(layout):
"""Turn the layout into a sprite group with Tile instances."""
group = pygame.sprite.Group()
for y, row in enumerate(layout):
for x, tile in enumerate(row):
if tile == 'T':
color = (50, 150, 50)
else:
color = (0, 0, 200)
group.add(Tile((x*tile_size, y*tile_size), color))
return group
pygame.init()
screen = pygame.display.set_mode((250, 250))
clock = pygame.time.Clock()
layout1 = [
'XXXXX',
'XTXXX',
'XXXXT',
'XXXXX',
'TTTTT',
]
tile_size = 50
tile_group = create_tile_group(layout1)
loop = True
while loop:
for event in pygame.event.get():
if event.type == pygame.QUIT:
loop = False
tile_group.update()
screen.fill((30, 30, 30))
tile_group.draw(screen)
pygame.display.flip()
clock.tick(30)
If you get performance problems because you blit too many small surfaces, you could blit them onto a big background surface before the while loop starts and then just blit the background once each frame.

There is no magic here: "it just renders in the game" is not accurate. There's more software behind the rendering call, a module that defines the tile sprites, scans the level character by character, and places the sprites accordingly. The developer (e.g. you) decides on the level representation, sprite form, sprite placement, etc.
You have several bits of code to write. The good news is that you decide the format; you just have to stay consistent when you write those modules.

Related

Does pytmx provide you with a way to tell if an image has been rotated, flipped horizontally/diag working with Tiled?

I am making a game using pygame and Tiled to design the map. I am able to load the tiles and blit them on to the screen. When i flip the tile horizontally/rotate inside of Tiled and run the game, the tiles are not rotated inside of the game.
So i would want to check if the tile is rotated/flipped before blitting the tile onto the screen and i am not sure how exactly to do this or whether if this is even possible and proceed to use the tiles without any rotations or flips inside of Tiled?
I read that there are flags you can check for, but at this point i am not sure of that either, any help would be great. Heres the class used to make the map
import pytmx
class TiledMap:
def __init__(self, filename):
tm = pytmx.load_pygame(filename)
self.width = tm.width * tm.tilewidth
self.height = tm.height * tm.tileheight
self.tmxdata = tm
def render(self, surface):
ti = self.tmxdata.get_tile_image_by_gid
for layer in self.tmxdata.visible_layers:
if isinstance(layer, pytmx.TiledTileLayer):
for x, y, gid, in layer:
tile = ti(gid)
if tile:
# check for flags first.. ie rotating, flips
# do the neccessary transformations... this part i know
# blit the transformed tile
# if no flags just blit the image
surface.blit(tile, (x * self.tmxdata.tilewidth,
y * self.tmxdata.tileheight))
how the same tile flipped horizontally appears inside of Tiled
how the image gets loaded when i run the game
Okay so after reading Tile flipping, here's what i understood from it.
Get the gid
Already have that from this bit of code for x, y, gid, in layer:
Check for the flags
The highest three bits of the gid store the flipped states.
Says that 32nd bit is for horizontal flip. So to check for that we do h_flipped = gid & (1<<31) # correct? But the problem here is when i do that it evaluates to a 0. Printing out the gids gives 1 for the unflipped tile and 2 for the flipped so 2 & (1<<31) # false
The pseudo code was a bit confusing and maybe i understood it incorrectly. So either the gids i am getting is incorrect or the checking for the bit is incorrect? or Both? More help appreciated

Dynamically controlling sprite blit order with pygame

I'm currently writing a Tower Defence game. Everything works fine but there's a visual issue that's bugging me.
Basically, a sprite that has a lower rect.y and should be higher on the map to pass behind a slower mob with a higher rect.y, can be blitted after (and therefore in front of) the foreground sprite.
I could fix it, but not while keeping the random spread-out spawn points that I like.
Could anybody suggest anything to help? I omitted irrelevant code, so there's not much to sift through.
class Pokemon(pygame.sprite.Sprite):
def __init__(self, name, health, speed, attack, typeOf, typeOf2, level, order):
pygame.sprite.Sprite.__init__(self)
self.order = order
self.image = pygame.image.load('C:/Users/#/#/#/pygames/tower_defense/Distributable/images/POKEMON/%sright.png'%(name))
self.rect = self.image.get_rect()
# X/Y axis locations
self.rect.centerx = 0-self.rect.width - self.order*100
self.rect.centery = random.randint(500, 540)
Then the creation block -
for e in range(numberOfEnemies):
poke = random.randint(1, 2)
if poke == 1:
level = random.randint(2, 5)
Pidgey = Pokemon('Pidgey', 15, 2, 5, 'Normal', 'Flying', level, e)
enemies.append(Pidgey)
enemy_sprites.add(Pidgey)
else:
level = random.randint(2, 4)
Rattata = Pokemon('Rattata', 13, 3, 5, 'Normal', None, level, e)
enemies.append(Rattata)
enemy_sprites.add(Rattata)
enemyNumber += 1
If you need to control the layers of the sprites, you can use a LayeredUpdates sprite group. The sprites need a self._layer attribute or you can pass the layer when you add the sprite to the group, e.g.:
layered_group.add(sprite, layer=desired_layer)
When a sprite is moved, you can call the change_layer method of the sprite group:
layered_group.change_layer(sprite, sprite.rect.bottom)
However, if you have to update the layers of all sprites each frame, it would be more efficient to sort the sprites by their rect.bottom position, iterate over the list and blit them:
blit = screen.blit # Local variable to improve the performance.
for sprite in sorted(all_sprites, key=lambda spr: spr.rect.bottom):
blit(sprite.image, sprite.rect)

pygame - Snap Mouse to Grid

I'm making a little platformer game using pygame, and decided that making a level editor for each level would be easier than typing each blocks' coordinate and size.
I'm using a set of lines, horizontally and vertically to make a grid to make plotting points easier.
Here's the code for my grid:
def makeGrid(surface, width, height, spacing):
for x in range(0, width, spacing):
pygame.draw.line(surface, BLACK, (x,0), (x, height))
for y in range(0, height, spacing):
pygame.draw.line(surface, BLACK, (0,y), (width, y))
I want the user's mouse to move at 10px intervals, to move to only the points of intersection. Here's what I tried to force the mouse to snap to the grid.
def snapToGrid(mousePos):
if 0 < mousePos[0] < DISPLAYWIDTH and 0 < mousePos[1] < 700:
pygame.mouse.set_pos(roundCoords(mousePos[0],mousePos[1]))
(BTW, roundCoords() returns the coordinates rounded to the nearest ten unit.)
(Also BTW, snapToGrid() is called inside the main game loop (while not done))
...but this happens, the mouse doesn't want to move anywhere else.
Any suggestions on how to fix this? If I need to, I can change the grid code too.
Thanks a bunch.
P.S. This is using the latest version of PyGame on 64 bit Python 2.7
First of all I think you're not far off.
I think the problem is that the code runs quite fast through each game loop, so your mouse doesn't have time to move far before being set to the position return by your function.
What I would have a look into is rather than to pygame.mouse.set_pos() just return the snapped coordinates to a variable and use this to blit a marker to the screen highlighting the intersection of interest (here I use a circle, but you could just blit the image of a mouse ;) ). And hide your actual mouse using pygame.mouse.set_visible(False):
def snapToGrid(mousePos):
if 0 < mousePos[0] < DISPLAYWIDTH and 0 < mousePos[1] < 700:
return roundCoords(mousePos[0],mousePos[1])
snap_coord = snapToGrid(mousePos)# save snapped coordinates to variable
pygame.draw.circle(Surface, color, snap_coord, radius, 0)# define the remaining arguments, Surface, color, radius as you need
pygame.mouse.set_visible(False)# hide the actual mouse pointer
I hope that works for you !

PyGame Platformer with Interactive Platforms "Drawn" In

I'm looking for the easiest way to implement this. I'm trying to implement platforms (with full collision detection) that you can draw in via mouse. Right now I have a line drawing function that actually draws small circles, but they're so close together that they more or less look like a line. Would the best solution be to create little pygame.Rect objects at each circle? That's going to be a lot of rect objects. It's not an image so pixel perfect doesn't seem like an option?
def drawGradientLine(screen, index, start, end, width, color_mode):
#color values change based on index
cvar1 = max(0, min(255, 9 * index-256))
cvar2 = max(0, min(255, 9 * index))
#green(0,255,0), blue(0,0,255), red(255,0,0), yellow(255,255,0)
if color_mode == 'green':
color = (cvar1, cvar2, cvar1)
elif color_mode == 'blue':
color = (cvar1, cvar1, cvar2)
elif color_mode == 'red':
color = (cvar2, cvar1, cvar1)
elif color_mode == 'yellow':
color = (cvar2, cvar2, cvar1)
dx = end[0] - start[0]
dy = end[1] - start[1]
dist = max(abs(dx), abs(dy))
for i in xrange(dist):
x = int(start[0]+float(i)/dist*dx)
y = int(start[1]+float(i)/dist*dy)
pygame.draw.circle(screen, color, (x, y), width)
That's my drawing function. And here's my loop that I have put in my main game event loop.
i = 0
while (i < len(pointList)-1):
drawGradientLine(screen, i, pointList[i], pointList[i + 1], r, mode)
i += 1
Thanks for any help, collision detection is giving me a huge headache right now (still can't get it right for my tiles either..).
Any reason you want to stick with circles?
Rectangles will make the line/rectangle a lot more smooth and will make collision detecting a lot easier unless you want to look into pixel perfect collision.
You also don't seem to save your drawn objects anywhere (like in a list or spritegroup), so how are you going to check for collision?
Here's a leveleditor I did for game awhile back, it's not perfect, but it works:
https://gist.github.com/marcusmoller/bae9ea310999db8d8d95
How it works:
The whole game level is divided up into 10x10px grid for easier drawing
The leveleditor check if the mouse is being clicked and then saves that mouse position
The player now moves the mouse to another position and releases the mouse button, the leveleditor now saves that new position.
You now have two different coordinates and can easily make a rectangle out of them.
Instead of creating a whole bunch of rect objects to test collision against, I'm going to recommend creating something called a mask of the drawn-in collideable object, and test for collision against that. Basically, a mask is a map of which pixels are being used and which are not in an image. You can almost think of it as a shadow or silhouette of a surface.
When you call pygame.draw.circle, you are already passing in a surface. Right now you are drawing directly to the screen, which might not be as useful for what I'm suggesting. I would recommend creating a rect which covers the entire area of the line being drawn, and then creating a surface of that size, and then draw the line to this surface. My code will assume you already know the bounds of the line's points.
line_rect = pygame.Rect(leftmost, topmost, rightmost - leftmost, bottommost - topmost)
line_surf = pygame.Surface((line_rect.width, line_rect.height))
In your drawGradientLine function, you'll have to translate the point coordinates to the object space of the line_surf.
while (i < len(pointList)-1):
drawGradientLine(line_surf, (line_rect.x, line_rect.y), i, pointList[i], pointList[i+1], r, mode)
i += 1
def drawGradientLine(surf, offset, index, start, end, width, color_mode):
# the code leading up to where you draw the circle...
for i in xrange(dist):
x = int(start[0]+float(i)/dist*dx) - offset[0]
y = int(start[1]+float(i)/dist*dy) - offset[1]
pygame.draw.circle(surf, color, (x, y), width)
Now you'll have a surface with the drawn object blitted to it. Note that you might have to add some padding to the surface when you create it if the width of the lines you are drawing is greater than 1.
Now that you have the surface, you will want to create the mask of it.
surf_mask = pygame.mask.from_surface(line_surf)
Hopefully this isn't getting too complicated for you! Now you can either check each "active" point in the mask for collision within a rect from your player (or whatever other objects you want to collide withe drawn-in platforms), or you can create a mask from the surface of such a player object and use the pygame.Mask.overlap_area function to check for pixel-perfect collision.
# player_surf is a surface object I am imagining exists
# player_rect is a rect object I am imagining exists
overlap_count = surf_mask.overlap_area(player_surf, (line_rect.x - player_rect.x, line_rect.y - player_rect.y))
overlap_count should be a count of the number of pixels that are overlapping between the masks. If this is greater than zero, then you know there has been a collision.
Here is the documentation for pygame.Mask.overlap_area: http://www.pygame.org/docs/ref/mask.html#pygame.mask.Mask.overlap_area

Is there any way to "clear" a surface?

Is there any way to clear a surface from anything that has been blitted to it?
If what you want is to make a pygame Surface object "clean", that is erase all images blited to it, to blit new images to it every game loop and keep the peer pixel alpha without creating a new surface, you can fill it, but instead of a solid color, use a transparent color
from pygame import Color, Surface
empty = Color(0,0,0,0) #The last 0 indicates 0 alpha, a transparent color
field = Surface((width, height), flags=SRCALPHA)
#Inside the game loop
field.fill(empty)
*Sorry is my english is bad, still learning
When I dont care about performance, I use:
mysurface.fill((0,0,0))
Which will draw the specified color (black in this case) across the entire surface. Is that what you meant by "clear"?
Oh, and you need, of course, to "flip" the surface after doing this for it to be visible on the screen.
I don't know what API you're using, so here's a vague answer:
In virtually all cases "clearing" a surface simply blits a coloured quad of the same size as the surface onto it. The colour used is whatever you want your clear colour to be.
If you know how do blit, just blit a white quad of the same size onto the surface.
You can't undo one graphic written over the top of another graphic any more than you can undo one chalk illustration drawn over the top of another chalk illustration on the same board.
What is typically done in graphics is what you'd do with the chalkboard - clear the whole lot, and next time only redraw what you want to keep.
I had this problem too
To create the surface:
mask=pygame.Surface((180,100),pygame.SRCALPHA)
To clear the surface:
mask.fill
mask.set_alpha(255)
Draw your lines/images etc
Then blit this surface onto your main surface using the Alpha Blend flag
screen.blit(mask,(0,0),special_flags=(pygame.BLEND_RGBA_ADD))
.fill((255,255,255,0)) appears to work for me anyway.
I used the following in a game I made:
self.image = 0 #to empty it
self.image = pygame.image.load("image")
For pygame you can use Surface.fill
If you want to clear a surface before blitting, you can first fill the surface with a unlikely random color, set the alpha (colorkey) with that color, and then blit the original sprite with the transparent background back onto the surface.
idle_image = pg.image.load("sprite.png") #sprite with transparent background
temp_color = pg.Color(132, 23, 213, 255) #random unlikely replacement color
def clear_surface(surface):
surface.fill(st.temp_color)
surface.set_colorkey(st.temp_color)
surface.blit(idle_image, (0,0))
You can clear a surface by making it transparent.
Below is how I used the empty color RGB to make text blink by setting the surface to switch to a transparent version.
from __future__ import absolute_import, division, print_function
from itertools import cycle
import pygame
VISITOR_TTF_FILENAME = 'pixelFont.ttf'
BLINK_EVENT = pygame.USEREVENT + 0
empty = (255,255,255,0)
def main():
pygame.init()
try:
screen = pygame.display.set_mode((800, 600))
screen.fill((255,255,255))
screen_rect = screen.get_rect()
clock = pygame.time.Clock()
font = pygame.font.Font(VISITOR_TTF_FILENAME, 50)
on_text_surface = font.render(
'Press Any Key To Start', True, pygame.Color('green3')
)
blink_rect = on_text_surface.get_rect()
blink_rect.center = screen_rect.center
off_text_surface = pygame.Surface(blink_rect.size)
off_text_surface.fill(empty)
blink_surfaces = cycle([on_text_surface, off_text_surface])
blink_surface = next(blink_surfaces)
pygame.time.set_timer(BLINK_EVENT, 1000)
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
return
if event.type == BLINK_EVENT:
blink_surface = next(blink_surfaces)
screen.blit(blink_surface, blink_rect)
pygame.display.update()
clock.tick(60)
finally:
pygame.quit()
if __name__ == '__main__':
main()
When I want to get rid of something I use
screen.fill("Gray")
every time I update! So it hides it behind the gray screen.
Gray is not the only color available, so here is a list of all of the available colors you can use for pygame
Hope it helped!
Fill the surface with fill, then make it transparent with set_alpha
surface.fill
surface.set_alpha(255)

Categories