Rendering and refreshing tmx file map with python and pytmx - python

I am using pytmx to load my map with pygame, but some items needs to be set as sprites and displayed above the surface, so the layering doesn't stay, I will show you 2 examples, one with the player displaying on top of decorations (where the point layer in under the decorations) the other one is the door displaying above the walls.
I tried to refresh my map with my player and door object by rewriting make_map() and render(surface) functions from Renderer, but instead of passing object I iterated threw them to blit the one it was, it has displaying, but still not layered, and also half FPS goes down.
Si if anyone knows how to be able to keep the layering even with player and other object used to render animated sprites I would be glad to get your help please.
This is the code I tried to use to refresh map every time with layering (it was displaying, but it has the exact same results as if I draw only the surface and then draw sprites above) :
def draw(self, display, camera = None):
self.surface = self.make_map()
self.rect = self.surface.get_rect()
if not camera:
display.blit(self.surface, self.rect)
else:
display.blit(self.surface, (self.rect.x - camera.offset.x, self.rect.y - camera.offset.y))
def render(self, surface):
tw = self.tmx_data.tilewidth
th = self.tmx_data.tileheight
if self.tmx_data.background_color:
surface.fill(self.tmx_data.background_color)
else:
surface.fill((0, 0, 0))
for layer in self.tmx_data.layers:
if isinstance(layer, pytmx.TiledTileLayer):
for x, y, image in layer.tiles():
if image:
surface.blit(image.convert_alpha() , (x * tw, y * th))
elif isinstance(layer, pytmx.TiledObjectGroup):
for tile_object in layer:
if tile_object.name == 'player':
surface.blit(self.level.game.player.image.convert_alpha(), (self.level.game.player.rect.x, self.level.game.player.rect.y))
if tile_object.name == 'door':
for door in self.level.game.doors:
if door.type == tile_object.type:
surface.blit(door.image.convert_alpha(), (door.rect.x, door.rect.y))
elif isinstance(layer, pytmx.TiledImageLayer):
if image:
surface.blit(image , (0, 0))
def make_map(self):
temp_surface = pygame.Surface(self.renderer.size)
self.render(temp_surface)
return temp_surface

So I have found a way to get what I wanted, I had used pyagme sprites groups layeredUpdates, I added to my original map only floor, I created layered sprites with my other layers (walls,decorations) and then displayed with the layer
The map rendering
def render(self, surface):
tw = self.tmx_data.tilewidth
th = self.tmx_data.tileheight
if self.tmx_data.background_color:
surface.fill(self.tmx_data.background_color)
else:
surface.fill((0, 0, 0))
for layer in self.tmx_data.layers:
if isinstance(layer, pytmx.TiledTileLayer):
for x, y, image in layer.tiles():
if image:
if layer.name == 'walls' or 'wall' in layer.name:
Wall(self.game, x * tw, y * th, image.convert_alpha())
elif 'decoration' in layer.name:
Decoration(self.game, x * tw, y * th, image.convert_alpha())
else:
surface.blit(image.convert_alpha() , (x * tw, y * th))
elif isinstance(layer, pytmx.TiledObjectGroup):
pass
elif isinstance(layer, pytmx.TiledImageLayer):
if image:
surface.blit(image , (0, 0))
def make_map(self):
temp_surface = pygame.Surface(self.size)
self.render(temp_surface)
return temp_surface
The objects :
class Decoration(pygame.sprite.Sprite):
def __init__(self, game, x, y, image, layer = DECORATION_TOP_LAYER):
self.groups = game.all_sprites, game.decorations
self._layer = layer
pygame.sprite.Sprite.__init__(self, self.groups)
class Wall(pygame.sprite.Sprite):
def __init__(self, game, x, y, image):
self.groups = game.all_sprites, game.walls
self._layer = WALL_LAYER
pygame.sprite.Sprite.__init__(self, self.groups)
class Door(pygame.sprite.Sprite):
def __init__(self, game, x, y, w, h, open_actions, animated_image, type):
self._layer = DOOR_LAYER
self.groups = game.all_sprites, game.doors
pygame.sprite.Sprite.__init__(self, self.groups)
class NPC(pygame.sprite.Sprite):
def __init__(self, game, x, y, w, h, animated_image):
self.groups = game.all_sprites, game.NPCs
self._layer = NPC_LAYER
pygame.sprite.Sprite.__init__(self, self.groups)
And the thing that makes all works together is simply :
self.all_sprites = pygame.sprite.LayeredUpdates()

Related

Source object must be a surface

I am encountering a problem which says that the source object must be a surface. I asked this question before, and the answer worked, but now it's a different scenario. I looked at the previous question I had, and I couldn't figure out how to solve it.
Error Message:
Traceback (most recent call last):
File "C:\Users\Daniel\Desktop\Thing.py", line 22, in <module>
level.run()
File "C:\Users\Daniel\Desktop\level2.py", line 131, in run
self.clouds.draw(self.display_surface, self.world_shift)
File "C:\Users\Daniel\Desktop\decoration.py", line 67, in draw
self.cloud_sprites.draw(surface)
File "C:\Users\Daniel\AppData\Local\Programs\Python\Python310\lib\site-
packages\pygame\sprite.py", line 551, in draw
self.spritedict.update(zip(sprites, surface.blits((spr.image, spr.rect) for
spr in sprites)))
TypeError: Source objects must be a surface
Basically I am trying to blit things onto a surface, and the thing that confuses me the most is that it worked when I was trying to blit water onto the screen, it had the exact same code but instead of 'cloud' it had 'water', however the water blitting worked while the cloud did not.
Water Class:
class Water:
def __init__(self, top, level_width):
water_start = -width
water_tile_width = 192
tile_x_amount = int((level_width + width) / water_tile_width)
self.water_sprites = pygame.sprite.Group()
for tile in range(tile_x_amount):
x = tile * water_tile_width + water_start
y = top
sprite = AnimatedTile(192, x, y, (x, y),
'C:\\Desktop\\Game\\decoration\\water')
self.water_sprites.add(sprite)
def draw(self, surface, shift):
self.water_sprites.update(shift)
self.water_sprites.draw(surface)
Cloud Class:
class Clouds:
def __init__(self, horizon, level_width, cloud_number):
min_x = -width
max_x = level_width + width
min_y = 0
max_y = horizon
cloud_surface_list = 'C:\\Desktop\\Game\\decoration\\clouds'
self.cloud_sprites = pygame.sprite.Group()
for cloud in range(cloud_number):
cloud = choice(cloud_surface_list)
x = randint(min_x, max_x)
y = randint(min_y, max_y)
sprite = StaticTile(0, x, y, (x, y),
'C:\\Desktop\\Game\\decoration\\clouds')
self.cloud_sprites.add(sprite)
def draw(self, surface, shift):
self.cloud_sprites.update(shift)
self.cloud_sprites.draw(surface)
As you can see the draw method is the exact same code, pretty much. The only difference that I can spot that could be causing the problem would be the inheritance. Water inherits from a class called AnimatedTile while Clouds inherits from StaticTile. I will put the code for both of those here:
AnimatedTile:
class AnimatedTile(Tile):
def __init__(self, size, x, y, pos, path):
super().__init__(size, x, y, (x, y))
self.frames = import_folder(path)
self.frame_index = 0
self.image = self.frames[int(self.frame_index)]
def animate(self):
self.frame_index += 0.15
if self.frame_index >= 4:
self.frame_index = 0
self.image = self.frames[int(self.frame_index)]
def update(self, shift):
self.animate()
self.rect.x += shift
StaticTile:
class StaticTile(Tile):
def __init__(self, size, x, y, pos, surface):
super().__init__(size, x, y, (x,y))
self.image = surface
self.surface = surface
Drawing and Updating:
self.clouds.draw(self.display_surface,
self.world_shift)
*I know these are surfaces because I drew them the same exact way to blit the water onto the screen (self.water.draw(self.display_surface, self.world_shift) and it worked.
Other code:
class Level:
def __init__(self, level_data, surface):
self.display_surface = surface
self.world_shift = -8
Tile:
class Tile(pygame.sprite.Sprite):
def __init__(self, size, x, y, pos):
super().__init__()
self.image = pygame.Surface((size, size))
self.rect = self.image.get_rect(topleft = pos)
def update(self, shift):
self.rect.x += shift
world_shift is basically how fast the level is moving (negative means right, positive means left)
I'm sorry if this sounds stupid and the answer is obvious.
You're not correctly assigning to the image attribute of your StaticTile sprite in Clouds. I'm not sure if it's the caller (Clouds) or the tile class that is getting it wrong though, you'll have to decide what the API should be.
The mismatch is between your call, here:
sprite = StaticTile(0, x, y, (x, y),
'C:\\Desktop\\Game\\decoration\\clouds')
And the StaticTile.__init__ method, which is declared like this:
def __init__(self, size, x, y, pos, surface):
super().__init__(size, x, y, (x,y))
self.image = surface
self.surface = surface
Note that the calling code is passing a string as the surface argument. That's similar to how AnimatedTile is initialized (with a path), but the StaticTile class expects a surface object to already be loaded.

Change color of Pygame Surface

I've made a Rectangle class:
class Rectangle(pygame.sprite.Sprite):
def __init__(self, len, x, y, color):
pygame.sprite.Sprite.__init__(self)
self.image = pygame.Surface((len, len), flags=pygame.SRCALPHA)
# self.image.set_colorkey(Constants.WHITE)
self.image.fill(color)
self.rect = self.image.get_rect()
self.rect.center = (x + len / 2, y + len / 2)
self.is_blocked = False
self.x_pos = x
self.y_pos = y
self.x = math.floor((x - Constants.PADTOPBOTTOM) / Constants.CELL_SIZE)
self.y = math.floor((y - Constants.PADLEFTRIGHT) / Constants.CELL_SIZE)
def update(self):
if self.is_blocked:
print("is update working?") # it is working.
self.image.fill(Constants.GREEN)
And when update() is called I want to change the color of the rectangle to green but it doesn't work.
I'm new to pygame and I don't know what to do.
Your problem is that you keep creating new sprites every frame, so when you change the color of one Rectangle to green, it will be covered by a white one.
You do this by calling the fill function (which creates new Rectangle instances) inside the drawGrid function.

Collision detection between pygame.Surface and mouse not working [duplicate]

This question already has answers here:
Why is my collision test always returning 'true' and why is the position of the rectangle of the image always wrong (0, 0)?
(1 answer)
How to detect collisions between two rectangular objects or images in pygame
(1 answer)
Closed 2 years ago.
I am trying to make a canvas for pixel art.
class Canvas:
def __init__(self):
self.__blocks = []
self.__positions = []
for i in range(1830):
self.__blocks.append(pygame.Surface((20, 20)).convert())
for y in range(30):
y *= 20
for x in range(61):
x = x* 20
self.__positions.append([x, y])
self.__color = False
def draw(self, window):
for i in range(1830):
self.__color = not self.__color
if self.__color:
self.__blocks[i].fill((200, 200, 200))
else:
self.__blocks[i].fill((50, 50, 50))
window.blit(self.__blocks[i], (self.__positions[i][0]
, self.__positions[i][1]))
Here I am trying to generate and draw 1830 unique surfaces and this works. I then tried implementing collision detection between each block and the mouse and failed.
def collided(self, pos):
for i in range(1380):
block = self.__blocks[i].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
Then I did different tests on why it might be failing. Here is one of them. I will change a single block's color, in our case the 10th block self.__blocks[10].fill((255, 0, 0)) to red so we know which box to click on. Then we will try to check for collision for that particular block.
def testBlock(self, pos):
block = self.__blocks[10].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x)
And it doesn't work, but the weird thing is it works for the first block(in the 0th index) and only the first block no matter which surface I test. Any idea on how to fix this would be appreciated. The following is copy and paste code.
import pygame
pygame.init()
win = pygame.display
D = win.set_mode((1220, 600))
class Canvas:
def __init__(self):
self.__blocks = []
self.__positions = []
for i in range(1830):
self.__blocks.append(pygame.Surface((20, 20)).convert())
for y in range(30):
y *= 20
for x in range(61):
x = x* 20
self.__positions.append([x, y])
self.__color = False
self.testBlock = 10
def draw(self, window):
for i in range(1830):
self.__color = not self.__color
if self.__color:
self.__blocks[i].fill((200, 200, 200))
else:
self.__blocks[i].fill((50, 50, 50))
self.__blocks[self.testBlock].fill((255, 0, 0)) # Changing the color for testing
window.blit(self.__blocks[i], (self.__positions[i][0]
, self.__positions[i][1]))
def test(self, pos):
block = self.__blocks[self.testBlock].get_rect()
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
canvas = Canvas()
while True:
D.fill((0, 0, 0))
pygame.event.get()
mousepos = pygame.mouse.get_pos()
canvas.draw(D)
canvas.test(mousepos)
win.flip()
When you call .get_rect() on a Surface, it does not know its current position, because that is not Surface information. So you need to assign the location to the Rect before collision detection.
With your current code layout, you could do this during the construction. With the Canvass blocks position now held in the __rects list, the __positions list becomes superfluous.
class Canvass:
def __init__(self):
self.__blocks = []
self.__rects = []
for y in range( 30 ):
for x in range( 61 ):
self.__blocks.append(pygame.Surface((20, 20)).convert())
self.__rects.append( self.__blocks[-1].get_rect() )
self.__rects[-1].topleft = ( x, y )
self.__color = False
self.testBlock = 10
This gives you a simple test:
def collided(self, pos):
hit = False
for i in range( len( self.__rects ) ):
if ( self.__rects[i].collidepoint( pos[0], pos[1] ) ):
print( "Click on block %d" % ( i ) )
hit = True
break
return hit, i
.get_rect() gives rect with block's size but with position (0, 0)
you have real position in __positions and you would need
.get_rect(topleft=self.__positions[self.testBlock])
def test(self, pos):
block = self.__blocks[self.testBlock].get_rect(topleft=self.__positions[self.testBlock])
if block.collidepoint(pos[0], pos[1]):
print(block.x, block.y)
But it would be better to get rect and set its position at start and later not use get_rect().
You could also create class Pixel similar to class Sprite with self.image to keep surface and self.rect to keep its size and position. And then you could use Group to check collision with all pixels.
EDIT:
Example which uses class pygame.sprite.Sprite to create class Pixel and it keeps all pixels in pygame.sprite.Group
It also handle events (MOUSEBUTTONDOWN) to change color in any pixel when it is clicked.
import pygame
# --- classes ---
class Pixel(pygame.sprite.Sprite):
def __init__(self, x, y, color, width=20, height=20):
super().__init__()
self.color_original = color
self.color = color
self.image = pygame.Surface((20, 20)).convert()
self.image.fill(self.color)
self.rect = pygame.Rect(x, y, width, height)
def handle_event(self, event):
if event.type == pygame.MOUSEBUTTONDOWN:
if self.rect.collidepoint(event.pos):
if self.color != self.color_original:
self.color = self.color_original
else:
self.color = (255,0,0)
self.image.fill(self.color)
# event handled
return True
# event not handled
return False
class Canvas:
def __init__(self):
# create group for sprites
self.__blocks = pygame.sprite.Group()
# create sprites
self.__color = False
for y in range(30):
y *= 20
for x in range(61):
x *= 20
self.__color = not self.__color
if self.__color:
color = (200, 200, 200)
else:
color = (50, 50, 50)
self.__blocks.add(Pixel(x, y, color))
# changing the color for testing
self.testBlock = 10
all_sprites = self.__blocks.sprites()
block = all_sprites[self.testBlock]
block.image.fill((255, 0, 0))
def draw(self, window):
# draw all sprites in group
self.__blocks.draw(window)
def test(self, pos):
# test collision with one sprite
all_sprites = self.__blocks.sprites()
block = all_sprites[self.testBlock]
if block.rect.collidepoint(pos):
print(block.rect.x, block.rect.y)
def handle_event(self, event):
for item in self.__blocks:
if item.handle_event(event):
# don't check other pixels if event already handled
return True
# --- main ---
pygame.init()
win = pygame.display
D = win.set_mode((1220, 600))
canvas = Canvas()
while True:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
exit()
canvas.handle_event(event)
#mousepos = pygame.mouse.get_pos()
#canvas.test(mousepos)
# draws (without updates, etc)
#D.fill((0, 0, 0)) # no need clean screen if it will draw all elements again
canvas.draw(D)
win.flip()

pygame 'button' function not detecting mouse events

I copied this function from one of my older projects, on which it works perfectly, but it doesn't work anymore. The button is supposed to detect when the cursor is above it, and redraw itself in a lighter colour, and then when the cursor moves off, it redraws itself in the usual darker colour. But now when the cursor is above it, it doesn't change. It doesn't respond to clicking either. Here is the code
def button(text, x, y, width, height, inactive_color, active_color, action = None):
cur = pygame.mouse.get_pos()
click = pygame.mouse.get_pressed()
if x + width > cur[0] > x and y + height > cur[1] > y:
pygame.draw.rect(gameDisplay, active_color, (x, y, width, height))
pygame.display.update()
if click[0] == 1 and action != None:
if action == 'correct':
print('correct!')
else:
pygame.draw.rect(gameDisplay, inactive_color, (x, y, width, height))
text_to_button(text, black, x, y, width, height)
pygame.display.update()
button('test', 100, 100, 100, 50, darkGreen, green, action = 'correct')
You are calling the button function once. It runs through the code of the function and terminates, and does not respond further to any inputs -- because it only ran once (almost instantly).
Perhaps if you called this method every time a mouse movement event occurs in the Pygame evnet queue it may work.
Alternatively, consider using an object instead of a function for this, such as:
class Button():
def __init__(self, x, y, width, height, text, action):
self.x = x
self.y = y
self.width = width
self.height = height
self.text = text
self.action = action
self.label = myfont.render(self.text, 1, (0,0,0))
def render(self, window):
if self.visible == True:
pygame.draw.rect(window, (255,255,255), [self.x, self.y, self.width, self.height])
window.blit(self.label, (self.x, self.y))
def pressed(self):
self.action(self.arguments)
def hover(self):
#Here you can implement your code that handles when the mouse hovers over the button. This method can be called by checking mouse movement events in your main loop and seeing if they lie within the coordinates, width, and height of the button.

All rectangles in one iteration of a game loop drawing the same colour despite explicit colour arguments

I'm writing a simple toy in Pygame. When you press a key on the home row, it does a little burst of particles.
class Particle():
x = 0
y = 0
size = 0
colour = (255, 255, 255)
rect = None
def __init__(self, x, y, size, colour):
self.x = x
self.y = y
self.size = size
self.colour = colour # Particle has its own colour
self.rect = pygame.Rect(self.x, self.y, self.size, self.size)
class Burst():
x = 0
y = 0
colour = (255, 255, 255)
count = 0
sound = None
particles = []
def __init__(self, x, y, colour, count, sound):
self.x = x
self.y = y
self.colour = colour # Burst has its own colour, too - all its particles should have the same colour as it
self.count = count
self.sound = sound
self.particles.append(Particle(self.x, self.y, 5, self.colour))
def update(self):
self.particles.append(Particle(random.randint(1, 30) + self.x, random.randint(1, 30) + self.y, 5, self.colour))
def draw(self):
global screen
for p in self.particles:
pygame.draw.rect(screen, p.colour, p.rect) # This draws the particles with the correct colours
#pygame.draw.rect(screen, self.colour, (60, 60, 120, 120), 4) # This draws the particles all the same colour
#screen.fill(p.colour, p.rect) # This draws the particles all the same colour
The line you're looking for is in Burst.draw. For some reason, only the uncommented one works correctly. The other two lines, which should be the same as far as I can tell, only draw the first burst's particles correctly. Any subsequent bursts change all particles onscreen to match their colour.
I can provide more code, but there's not much more to it. Basically keypresses add Bursts to an array, and every tick I step through that array calling update() and draw().
Does anyone know what I did wrong, and then accidentally fixed?
Because all particles in the screen belong to the same collection Burst.particles.
And every time you process a Burst you are processing all the particles, and all gets painted with the last colour.
Just move the initialization particles = [] to the init method.
def __init__(self, x, y, colour, count, sound):
...
self.particles = []
self.particles.append(Particle(self.x, self.y, 5, self.colour))
Update
You are using a Java/C# style of coding classes. You shouldn't put any of the initializations at the class level, unless they are constants or class attributes.
IE:
class Burst():
class_attribute = 0 # declaration of class (static) attribute
def __init__(self, ...):
self.attribute = 0 # declaration of object (regular) attribute
You shouldn't make class declarations of attribute you will use a object attributes.
Just remove all the declarations previous to the init method in both classes.

Categories

Resources