Blitting subsurface from tilesheet is doing weird things - python

I am coding a prototype platformer in pygame. I'm using a .png as a tilesheet, I load it and then get a list tileset.tiles of all the different tile textures in it. I then use three layers of .csv tilemaps to associate every tile in the grid with its own corresponding texture. I bake all of the tile layers onto a map surface once, and then blit this surface at every frame.
The problem is that the outcome is not as expected, apparently not all of the tiles are properly blit onto the surface. The problem seems to arise when the same subsurface has to be blit a second time by the load method in the Room class. It's not clear to me what exactly causes this. I have tried playing around with the .csv files, and it seems that different arrangements of tiles, even across layers, have an influence on what is actually rendered on screen. I've added screenshots to illustrate this better. For reference, the id number 8 corresponds to a blue square texture, which should be the sky. The other numbers correspond to several different textures.
bottom layer csv
middle layer csv
top layer csv
outcome
By changing the first tile of the middle layer no difference is shown (tile 0 corresponds to the flower texture):
alternate middle layer
outcome
Or, if I try and change the first few tiles of the bottom layer:
alternate bottom layer
outcome
Generally, if I change some tile number in a csv file, weird things happen, and other seemingly random tiles get blit. Also, I am able to manually place tiles at any position on the screen without any problems (bypassing the load method and directly blitting subsurfaces from the tileset class) so I think that the tileset class is working properly.
Here's the full code:
import pygame as pg
class Tileset:
def __init__(self,img:pg.Surface):
self.tiles = []
self.img = img
self.loadtiles()
def loadtiles(self):
for i in range(16):
for n in range(16):
currentimg = self.img.subsurface(32*n,32*i,32,32)
self.tiles.append(currentimg.copy())
class Tile(pg.sprite.Sprite):
def __init__(self,image:pg.Surface,position:tuple):
self.img = image
self.pos = position
def draw (self,surface:pg.Surface):
surface.blit(self.img,self.pos)
class Room:
def __init__(self,id,size:tuple):
self.id = id
self.layers = [[],[],[]]
self.size = (size[0]*32,size[1]*32)
# Call when a new room must be loaded: reads room csv, stores tile info, overwrites drawn map with new map
def load(self,map):
map = pg.Surface(self.size)
for layer in self.layers:
with open ('levels/final/room'+str(self.id)+'_'+str(self.layers.index(layer))+'.csv') as file:
data = file.readlines()
for rrrow in data: # unprocessed row
rrow = rrrow.strip('\n').split(',') # semi processed row
row = [] # processed row
for rtile in rrow: # unprocessed tile in row
if rtile != -1:
tileimg = tileset.tiles[int(rtile)]
tile = Tile(tileimg,(rrow.index(rtile)*32, data.index(rrrow)*32)) # process tile
tile.draw(map) # draw tile on current tilemap
row.append(tile) # store tile in row
layer.append(row) # store row in layer (to use later for collisions)
return map
### pygame loop setup (incomplete, shouldn't matter ###
res = (32*30,32*20)
scr = pg.display.set_mode(res)
tileset = Tileset(pg.image.load('graphics\stock.png'))
room0 = Room(0,(30,20))
map = pg.Surface((0,0))
map = room0.load(map)
running = True
while running:
scr.fill((0,0,0))
scr.blit(map,(0,0))
pg.display.flip()

I have just solved it. The problem was in the use of the '.index' method to determine what the position of that tile would be. .index would just return the first occurrence of that tile in the list, and not the actual one, so any time a tile would have to appear for a second time in the same row, or every time two rows looked the same, that tile/row would just be positioned on top of its previous copy. Solved by using i and n as counters for the tile and row positions instead of the .index method.

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

I need to make a "mosaic" - but very simple

The best code for mosaic I've found you can see at this page:
https://github.com/codebox/mosaic
However, the code doesn't work well on my Windows computer, and also I think the code is too advanced for what it should do. Here are my requirements I've posted on reddit:
1) The main photo already has reduced number of colors (8)
2) I have already every image associated with colour needed to be replaced (e.g. number 1 is supposed to replace black pixels, number 2 replaces green pixels...)
3) I need to enlarge the photo by the small photo's size (9 x 9 small photos will produce 81 times bigger image), which should push the pixels "2n" points away from each other, but instead of producing a n x n same-coloured area around every single one of them (this is how I believe enlarging works in general, correct me if I'm wrong), it will just colour the white spaces with unrecognized colour, which is not associated with any small photo (let's call that colour C)
4) Now all it needs is to run through all non-C coloured pixels and put an image centered on that pixel, which would create the mosaic.
Since I'm pretty new to Python (esp. graphics) and need it just for one use, could someone help me with creating that code? I think that code I got inspired with is too complicated. Two things I don't need:
1) "approximation" - if the enlargement is lesser than needed for 100% quality (e.g. the pictures are 9x9, but every side of the original photo can be only 3 times larger, then the program needs to merge some pixels of different colours together, leading to quality loss)
2) association colour - picture: my palette of pictures is small and of colours as well, I can do it manually
For the ones who didn't get what I mean, here is my idea: https://ibb.co/9GNhqBx
I had a quick go using pyvips:
#!/usr/bin/python3
import sys
import os
import pyvips
if len(sys.argv) != 4:
print("usage: tile-directory input-image output-image")
sys.exit(1)
# the size of each tile ... 16x16 for us
tile_size = 16
# load all the tile images, forcing them to the tile size
print(f"loading tiles from {sys.argv[1]} ...")
for root, dirs, files in os.walk(sys.argv[1]):
tiles = [pyvips.Image.thumbnail(os.path.join(root, name), tile_size,
height=tile_size, size="force")
for name in files]
# drop any alpha
tiles = [image.flatten() if image.hasalpha() else image
for image in tiles]
# copy the tiles to memory, since we'll be using them many times
tiles = [image.copy_memory() for image in tiles]
# calculate the average rgb for an image, eg. image -> [12, 13, 128]
def avg_rgb(image):
m = image.stats()
return [m(4,i)[0] for i in range(1,4)]
# find the avg rgb for each tile
tile_colours = [avg_rgb(image) for image in tiles]
# load the main image ... we can do this in streaming mode, since we only
# make a single pass over the image
main = pyvips.Image.new_from_file(sys.argv[2], access="sequential")
# find the abs of an image, treating each pixel as a vector
def pyth(image):
return sum([band ** 2 for band in image.bandsplit()]) ** 0.5
# calculate a distance map from the main image to each tile colour
distance = [pyth(main - colour) for colour in tile_colours]
# make a distance index -- hide the tile index in the bottom 16 bits of the
# distance measure
index = [(distance[i] << 16) + i for i in range(len(distance))]
# find the minimum distance for each pixel and mask out the bottom 16 bits to
# get the tile index for each pixel
index = index[0].bandrank(index[1:], index=0) & 0xffff
# replicate each tile image to make a set of layers, and zoom the index to
# make an index matching the output size
layers = [tile.replicate(main.width, main.height) for tile in tiles]
index = index.zoom(tile_size, tile_size)
# now for each layer, select pixels matching the index
final = pyvips.Image.black(main.width * tile_size, main.height * tile_size)
for i in range(len(layers)):
final = (index == i).ifthenelse(layers[i], final)
print(f"writing {sys.argv[3]} ...")
final.write_to_file(sys.argv[3])
I hope it's easy to read. I can run it like this:
$ ./mosaic3.py smallpic/ mainpic/Use\ this.jpg x.png
loading tiles from smallpic/ ...
writing x.png ...
$
It takes about 5s on this 2015 laptop and makes this image:
I had to shrink it for upload, but here's a detail (bottom left of the first H):
Here's a google drive link to the mosaic, perhaps it'll work: https://drive.google.com/file/d/1J3ofrLUhkuvALKN1xamWqfW4sUksIKQl/view?usp=sharing
And here's this code on github: https://github.com/jcupitt/mosaic

Psychopy: draw all images from a list at once

I have a list with 12 images. All of them have different locations (that is, they do not overlap at all). I want to draw them all at once. In other words, I want to see all 12 pictures at the same time on the screen. So far, I only got this:
lines = [line1,line2,line3,line4,line5,line6
line7,line8,line9,line10,line11,line12]
for i in range(12):
lines[i].draw()
But, of course, this code only draws one pic at a time, after I press any key. Is there then a way to draw the 12 pics at the same time?
Thanks in advance!
Your original code only drew one image at a time because of how the loop was set up -- it was (more or less) saying "For each element in circles, draw several things and flip the front and back buffers". Each time the buffers flip, unless you tell it otherwise via win.flip(clearBuffer = False), the previous things on the screen are removed. To draw the images at the same time, you could just loop through the image list and call the draw() method on each element, e.g.:
for i in imglist:
i.draw()
win.flip()
If you are willing to cede control over properties of individual images, one way would be to use BufferImageStim. This takes longer to initialize, but may be faster than drawing individual images (I haven't timed it properly). Both methods are demonstrated below.
from psychopy import visual, event, core
import urllib
import random
win = visual.Window([400, 400], fullscr = False)
# picture of a cat, save to file
urllib.urlretrieve('https://s-media-cache-ak0.pinimg.com/736x/' +
'07/c3/45/07c345d0eca11d0bc97c894751ba1b46.jpg', 'tmp.jpg')
# create five images with (probably) unique positions
imglist = [visual.ImageStim(win = win, image = 'tmp.jpg',
size = (.2, .2),
pos = ((random.random() - 0.5) * 2,
(random.random() - 0.5) * 2))
for i in xrange(5)]
# draw individual images
for i in imglist:
i.draw()
win.flip()
# wait for key press, then clear window
event.waitKeys()
win.flip()
core.wait(0.5)
# create aggregate stimulus (should look identical)
buffs = visual.BufferImageStim(win, stim = imglist)
buffs.draw()
win.flip()
event.waitKeys()
core.quit()

How to change a sprite image in python based on mouse direction

I've seen several different posts about changing a sprite image.
For the current assignment, I have to construct a packman sprite, then it should follow the mouse. No problem there.
Here is that bit of code:
class Packman(games.Sprite):
"""Create the packman that is conrolled by the mouse"""
#load the packman image
image_right = games.load_image("snap_right.png") # initial value
image_left = games.load_image("snap_left.png")
show_image = image_right
def __init__(self, x=games.mouse.x, y = games.mouse.y):
"""Initialise packman"""
super(Packman, self).__init__(image = Packman.show_image, x = games.mouse.x, y = games.mouse.y)
def update(self):
"""Move packmans coordinates"""
self.x = games.mouse.x
self.y = games.mouse.y
if self.left < 0:
self.left = 0
if self.right > games.screen.width:
self.right = games.screen.width
#change Packman's direction that he is facing`
As you can see I try and load two images, but only one image at a time will be displayed.
(I reckon there is a way of just flipping one image horizontally, in stead of using two images.) As is, I can move Packman around. Now I need to add the bit that makes Packman face left/right depending on the direction the mouse is traveling in. My handbook gives me an example to rotate the image through 180deg with keypresses, which work, but then packman is just upside down, with his eye at the bottom.
Is there another way of flipping packman depending on mouse direction? (I only need horizontal flip, i.e. left and right)
Well I use Pygame for my sprites but the solution is pretty simple.
I basically use something like this. Two pointers pointing to initialized images. And then a pointer to pointers called image which will be what we use to draw the sprites.
# image is the pointer that points to the pointer that points to the image being currently used
image1 = pygame.image.load('left.png')
image2 = pygame.image.load('right.png')
image = image1
Then in draw I just do
screen.blit(image, position)
Now since you are using the mouse to track the position of pacman. What you have to do is this. At EACH frame, store the location of the mouse x and y in a class variable. Call it something like old_x and old_y. On the next frame, simply compare your mouse x position to your old_x. If the mouse position is greater, your pacman is trying to move to the right. image = image2 If your mouse position is less, then your pacman is moving to the left. image = image1 Apply the necessary state changes and change your image accordingly.

Create a line using a list of coordinates in python GUI

I've been trying to create a graph using a create_line and a list of (x,y) points.
import Tkinter
Screen = [a list of screen coordinates]
World = []
for x,y in screen:
World.append(somefunctiontochange(x,y))
if len(World) >= 2:
Canvas.create_line(World)
The line doesn't show in my canvas though, and no error was given. Any help?
Took me a while but this is how you draw to a canvas in the way you want:
import Tkinter as tk
root = tk.Tk()
root.geometry("500x500")
root.title("Drawing lines to a canvas")
cv = tk.Canvas(root,height="500",width="500",bg="white")
cv.pack()
def linemaker(screen_points):
""" Function to take list of points and make them into lines
"""
is_first = True
# Set up some variables to hold x,y coods
x0 = y0 = 0
# Grab each pair of points from the input list
for (x,y) in screen_points:
# If its the first point in a set, set x0,y0 to the values
if is_first:
x0 = x
y0 = y
is_first = False
else:
# If its not the fist point yeild previous pair and current pair
yield x0,y0,x,y
# Set current x,y to start coords of next line
x0,y0 = x,y
list_of_screen_coods = [(50,250),(150,100),(250,250),(350,100)]
for (x0,y0,x1,y1) in linemaker(list_of_screen_coods):
cv.create_line(x0,y0,x1,y1, width=1,fill="red")
root.mainloop()
You need to supply create_line with the x,y positions at the start and end point of the line, in the example code above (works) I'm drawing four lines connecting points (50,250),(150,100),(250,250),(350,100) in a zigzag line
Its worth pointing out also that the x,y coords on a canvas start at the top left rather than the bottom left, think of it less like a graph with the x,y = 0,0 in the bottom left of the canvas and more how you would print to a page starting in top left corner moving to the right in the x and with the y incrementing as you move down the page.
I used:
http://www.tutorialspoint.com/python/tk_canvas.htm as reference.
If you aren't getting errors and you're certain your function is being called, you probably have one of three problems:
Is your canvas visible? A common mistake for beginners is to either forget to pack/grid/place the canvas, or to neglect to do that for all of its containers. An easy way to verify is to temporarily give your canvas a really bright background so that it stands out from the rest of the GUI.
Have you set the scroll region? The other explanation is that the drawing is happening, but it's happening in an area that is outside the viewable portion of the canvas. You should be setting the scrollregion attribute of the canvas after creating your drawings, to make sure everything you're drawing can be made visible.
Does your canvas and canvas objects have an appropriate color? It's possible that you've changed the background of the canvas to black (since you don't show that code in your question), and you're using a default color of black when creating your lines.

Categories