I thought you guys might be able to help me wrap my head around this. I want to be able to generate rects and assign images to those rects. I've been doing this for the whole project and isn't too hard. The hard part here is that I want this particular function to be able to generate as many different rects as I want. The project is a game that takes place on like a chess board. I figure I can write like... if statements for every single space and then have like a bazillion parameters in the function that dictate which rects get generated and where, but I was hoping someone might be able to think of a more elegant solution.
You could use two nested "for" loops --
def make_chessboard(upper_x=0, upper_y=0, size=30):
chessboard = []
for y in range(8):
row = []
for x in range(8):
coords = (upper_x + x * size, upper_y + y * size)
row.append(pygame.Rect(coords, (size, size)))
chessboard.append(row)
return chessboard
Then, to get the rect that's in the top-left corner, you could do chessboard[0][0]. To get the rect that's in the top-right corner, you could do chessboard[0][7].
You wouldn't be able to explicitly name each rect, but then again, you really wouldn't need to.
Note: I'm assuming that you wanted to create a chessboard-like pattern of rects of some kind. I can edit my question if you detail specifically what you're trying to do.
class ChessTile(pygame.sprite.Sprite):
def __init__(self, image, location):
pygame.sprite.Sprite.__init__(self)
self.image = image.convert()
self.mask = pygame.mask.from_surface(self.image)
self.rect = pygame.Rect(location, self.image.get_size())
Then make another method called like "MakeBoard". Call MakeBoad and have a loop setup with the size of the board. so the pseudo code would be:
(let's assume "img" is a 32x32 white or black square)
for y in range(0,7):
for x in range(0,7):
# alternate the tile image from black/white before calling ChessTile with it
# the location parameter is going to be x*32,y*32.. something like that
# so you'd have a tile at (0,0) then at (32,0), then (64,0), etc...
# after the first "j" run is done, "i" increments so now we have
# (0, 32), (32, 32), etc etc.
#
tile = ChessTile(img, (x,y))
then just draw the tile object as you normall would in some render method!
hope that helps!!!
Related
I am implementing collision detection and want to check if a rectangular object is touching the player. My wall uses .set_colorkey(background) where background is the specified background colour. The problem is that when I get my wall's rectangle with .get_rect(), it gets the full image's size which includes the transparent parts instead of just the opaque parts.
I thought about making the wall image file smaller in size to remove the background but that would be inconvenient as I would need to do this for each partially transparent image I have. I also thought about using arrays to get the colour and checking if it matches the background colour and getting the rectangle's size from there but that would be slow and cumbersome.
for x, y in ((i, j) for i in land_x for j in land_y):
# land_x, land_y hold the tiles to be checked
try:
tx1, ty1, tx2, ty2 = \
texture[land[y][x]].get_rect()
# tx1, ty1 coordinates of top-left corner
# tx2, ty2 width and height respectively
if tx2 == 0 and ty2 == 0:
continue # skip to other objects
tx1 = x*64 - tx2/2
ty1 = y*64 - ty2/2
px1, py1, px2, py2 = \
PLAYER.get_rect()
px1 = player_x - px2/2
py1 = -player_y - py2/2
if p.Rect(px1, py1, px2, py2).colliderect(
p.Rect(tx1, ty1, tx2, ty2)
):
player_x -= direction_x
break # go outside loop to start checking y
except IndexError: # incase player is outside map
pass # skip to other objects
The .get_rect() outputs a rectangle the size of the whole image whereas I want a rectangle that doesn't include the transparent parts.
Example:
texture is a 64x64 image with a 48x48 block in the centre.
The background colour is removed and a 48x48 solid coloured block is left (even though the image size is still 64x64).
Expected Output:
texture.get_rect() should output a rectangle of size 48x48.
Actual Output:
texture.get_rect() instead outputs a rectangle of size 64x64.
Any help on this would be appreciated :D
If you want to ignore the transparent pixels in your collision detection, you're talking about pixel-perfect collision.
To do this in pygame, pygame offers the Mask class. You usually create your masks with pygame.mask.from_surface and use it together with pygame.sprite.spritecollide and pygame.sprite.collide_mask.
Maybe think about using pygame's Sprite class to make use of all the features it offers.
Even if you don't want to use pygame's build-in collision detection, you can take a look at the source to see how it works.
You are making this too hard. You know the size of your objects. Add a smaller collision rect to each of your objects at creation time and use that for collision. Or use a circle if that is better for the object.
tile.crect = Rect(whatever)
Or just multiply the existing rect dimensions by some scale factor for your collision rect. Don't do all of these calculations. Store a Rect for each collideable object and have a rect for the player.
tx1 = x*64 - tx2/2
ty1 = y*64 - ty2/2
px1, py1, px2, py2 = \
PLAYER.get_rect()
px1 = player_x - px2/2
py1 = -player_y - py2/2
Then just test collision directly:
for t in tiles:
if player.rect.colliderect( t.rect ):
If the player is a sprite its rect moves around. Look at the example code in the doc.
https://www.pygame.org/docs/ref/sprite.html
I'm attempting to make a tile based game, not done anything like this before so I'm learning as I go along. However, I've got a big problem with speed, and I was wondering if anyone had any solutions/advice. I tried to separate recalculating bits and the actual drawing, though as you can only move the camera currently, it's got to do both at once, and it's very noticeable how slow it runs if you have a small tilesize and large resolution.
I thought an idea would be to split it into chunks, so you calculate an x*x area, and instead of checking each tile if it's within the screen bounds, you only check the group of tiles, then somehow cache it the first time it's drawn so you then end up drawing a single image from memory. However I didn't find anything on that when googling it.
As to the drawing part, it runs to the effect of:
for tile in tile_dict:
pygame.draw.rect(precalculated stuff)
With the same tilesize as the image below, at 720p it runs at 100fps, and at 1080p it runs at 75fps. This is with literally nothing but drawing squares. Each block is a slightly different colour, so I can't just draw a bigger square. I know not to redraw every frame by the way.
As to the recalculation part, it's a bit longer but still quite easy to understand. I calculate which coordinates would be at the edge of the screen, and use that to build a list of all on screen tiles. I then delete any tiles that are outside of this area, move the cooordinate to the new location if the tile has moved on screen, and calculate any tiles that have just appeared. This runs at about 90fps at 720p, or 45fps at 1080p, which is really not good.
def recalculate(self):
overflow = 2
x_min = self.cam.x_int + 1 - overflow
y_min = self.cam.y_int + 1 - overflow
x_max = self.cam.x_int + int(self.WIDTH / self.tilesize) + overflow
y_max = self.cam.y_int + int(self.HEIGHT / self.tilesize) + overflow
self.screen_coordinates = [(x, y)
for x in range(x_min, x_max)
for y in range(y_min, y_max)]
#Delete the keys that have gone off screen
del_keys = []
for key in self.screen_block_data:
if not x_min < key[0] < x_max or not y_min < key[1] < y_max:
del_keys.append(key)
for key in del_keys:
del self.screen_block_data[key]
#Rebuild the new list of blocks
block_data_copy = self.screen_block_data.copy()
for coordinate in self.screen_coordinates:
tile_origin = ((coordinate[0] - self.cam.x_int) - self.cam.x_float,
(coordinate[1] - self.cam.y_int) - self.cam.y_float)
tile_location = tuple(i * self.tilesize for i in tile_origin)
#Update existing point with new location
if coordinate in self.screen_block_data:
self.screen_block_data[coordinate][2] = tile_location
continue
block_type = get_tile(coordinate)
#Generate new point info
block_hash = quick_hash(*coordinate, offset=self.noise_level)
#Get colour
if coordinate in self.game_data.BLOCK_TAG:
main_colour = CYAN #in the future, mix this with the main colour
else:
main_colour = TILECOLOURS[block_type]
block_colour = [min(255, max(0, c + block_hash)) for c in main_colour]
self.screen_block_data[coordinate] = [block_type,
block_colour,
tile_location]
I realised in what I wrote above, I probably could cache the info for a 10x10 area or something to cut down on what needs to be done when moving the camera, but that still doesn't get around the problem with drawing.
I can upload the full code if anyone wants to try stuff with it (it's split over a few files so probably easier to not paste everything here), but here's a screenshot of how it looks currently for a bit of reference:
To increase the speed of drawing the small squares, you can draw them onto non-screen surface (any pygame surface that will be big enough to hold all the squares) and then blit this surface on the screen with correct coordinates.
This way you won't need to check if any squares are outside the screen and it will be only necessary to provide inverted camera (viewpoint) coordinates (If camera position is [50,20] then you should blit the surface with tiles onto [-50,-20]).
I've created a surface that I've used pixel array to put pixels on, but i want to make the surface transparent but leaving the pixels opaque, I've tried making the surface transparent then drawing the pixels tot he surface but that just makes the pixels also transparent, any help or something I've missed?
-Edit- Hopefully this'll help in some way, this is the class object that creates the surface that is the galaxy
Also I have stated what I've tried, there's not much more to tell
class Galaxy(object):
def __init__(self,posx=0,posy=0,radius=0,depth=0):
radius = int(radius)
self.size = [radius*2,radius*2,depth]
self.posx = posx
self.posy = posy
self.radius = radius
#create array for stars
self.starArray = []
#create surface for stars
self.surface = pygame.Surface([radius*2,radius*2])
self.starPixel = pygame.PixelArray(self.surface)
#populate
for x in range(radius*2):
for y in range(radius*2):
#generate stars
num1 = noise.snoise2(x+posx,y+posy,repeatx=radius*10,repeaty=radius*10)
distance = math.sqrt(math.pow((x-radius),2)+math.pow((y-radius),2))
if distance < 0:
distance = distance * -1
#print(x,y,"is",distance,"from",radius,radius)
val = 5
#glaxy density algorithm
num = (num1 / ( ((distance+0.0001)/radius)*(val*10) )) * 10
#density
if num > (1/val):
#create star
self.starArray.append(Stars(x,y,seed=num1*100000,distance=distance))
#print(num*1000)
self.addPixels()
#adds all star pixels to pixel array on surface
def addPixels(self):
for i in self.starArray:
self.starPixel[i.x,i.y] = i.colour
del self.starPixel
#sends to screen to await rendering
def display(self):
screen.displaySurface(self.surface,[self.posx+camPosX,self.posy+camPosY])
Use MyGalaxy.set_colorkey(SomeUnusedRGB) to define a zero-alpha (invisible) background colour, fill MyGalaxy with that colour, then draw the pixels on top of that. You can use pixelArray functions to draw to that surface, but you're probably better to use MyGalaxy.set_at(pixelLocationXY, pixelColourRGB) instead, for reasons of managability and performance.
Make sure that SomeUnusedRGB is never the same as any pixelColourRGB, or those pixels won't appear (since pygame will interpret them as invisible). When you blit MyGalaxy to wherever you want it, it ought to only blit the non-SomeUnusedRGB-coloured pixels, leaving the rest unaltered.
(This is the best I can offer you without knowing more about your code; revise the question to include what you're already trying, and I'll update this answer.)
I want to add a grid to my level that stays with the terrain and not the screen. The way I thought of doing it is to add all the lines that form the grid as sprites and move them with the terrain, but I can't figure out how to represent the line as an image.
I tried to do this myself, but had no success.
EDIT: Here's what I've tried
class Grid():
def __init__(self):
self.grid = pygame.Surface(size)
self.grid.set_colorkey((0,0,0))
def draw(self):
# DRAW TILE LINES ----------------------------------------------------------
grid_x = 0
grid_y = 0
for i in range(total_level_width // TILE_SIZE):
pygame.draw.aaline(self.grid,BLACK,[grid_x,0],[grid_x,total_level_height])
pygame.draw.aaline(self.grid,BLACK,[0,grid_x],[total_level_width,grid_y])
grid_x += TILE_SIZE
grid_y += TILE_SIZE
# tile test
pygame.draw.rect(screen,BLACK,(49*TILE_SIZE,34*TILE_SIZE,TILE_SIZE,TILE_SIZE))
screen.blit(self.grid,(0,0))
Creating the object:
grid = Grid()
Calling class: (in main program loop)
grid.draw()
I had a similar problem while i was trying to do a project. I used the following code to get a line onto a surface and then bliting it onto my screen. I hope this entire function might help you.
def blitBoundary(self):
""" helper function to blit boundary on screen """
# create a surface
self.boundSurf=pygame.Surface((1024,768))
self.boundSurf.set_colorkey((0,0,0))
"""
if not self.boundary.closePoly:
(x,y)=pygame.mouse.get_pos()
pointList=self.boundary.pointList +[[x,y]]
else:
pointList=self.boundary.pointList"""
if len(pointList)>1:
pygame.draw.aalines(self.boundSurf, (255,255,255),
self.boundary.closePoly , pointList, 1)
self.screen.blit(self.boundSurf,(0,0))
I was trying to draw a polygon. The commented out the if statement that would be most probably not useful for you.
All my lines were in a polygon class object.
You might want to look into pygame.draw.aalines function.
def rotate(self):
#Save the original rect center
self.saved_center=self.rect.center
#Rotates a saved image every time to maintain quality
self.image=pygame.transform.rotate(self.saved_image, self.angle)
#Make new rect center the old one
self.rect.center=self.saved_center
self.angle+=10
When I rotate the image, there is a weird shifting of it despite the fact that I'm saving the old rect center and making the rotated rect center the old one. I want it to rotate right at the center of the square.
Here's what it looks like:
http://i.imgur.com/g6Os9.gif
You are just calculating the new rect wrong. Try this:
def rotate(self):
self.image=pygame.transform.rotate(self.saved_image, self.angle)
self.rect = self.image.get_rect(center=self.rect.center)
self.angle+=10
It tells the new rect to center itself around the original center (the center never changes here. Just keeps getting passed along).
The issue was that the self.rect was never being properly updated. You were only changing the center value. The entire rect changes as the image rotates because it grows and shrinks in size. So what you needed to do was completely set the new rect each time.
self.image.get_rect(center=self.rect.center)
This calculates a brand new rect, while basing it around the given center. The center is set on the rect before it calculate the positions. Thus, you get a rect that is properly centered around your point.
I had this issue. My method has a bit of a different purpose, but I solved it quite nicely.
import pygame, math
def draw_sprite(self, sprite, x, y, rot):
#'sprite' is the loaded image file.
#'x' and 'y' are coordinates.
#'rot' is rotation in radians.
#Creates a new 'rotated_sprite' that is a rotated variant of 'sprite'
#Also performs a radian-to-degrees conversion on 'rot'.
rotated_sprite = pygame.transform.rotate(sprite, math.degrees(rot))
#Creates a new 'rect' based on 'rotated_sprite'
rect = rotated_sprite.get_rect()
#Blits the rotated_sprite onto the screen with an offset from 'rect'
self.screen.blit(rotated_sprite, (x-(rect.width/2), y-(rect.height/2)))