Pygame Scrolling Map - python

I'm making a rogue-like and I trying to make a camera follow the player as he moves by the map.
I was able to make the Draw function only happens when the tiles are inside the camera viewport[show in gray], but I can't make the camera stay in the corner of the window.
This is how it is:
And this is How it should be:
Is there a way to 'crop' the screen Surface, or perhaps copy only whats inside the camera viewport and blit it in the screen again.
Probably I'm doing this the hard way.
I'm iterating over the whole map, creating a rectangle for each tile, and checking if it's inside the Camera Viewport Rect using the '.contains()'.
EDIT:
This is how I'm drawing the map:
for x in xrange(mymap.width):
for y in xrange(mymap.height):
lit = field_of_view.lit(x, y)
visited = field_of_view.visited(x, y)
graphic = mymap.tileAt(x, y).graphic
if lit:
color = mymap.tileAt(x, y).color
elif visited:
color = GRAY
else:
color = BLACK
renderedgraphic = myfont.render(graphic, 1, color)
screen.blit(renderedgraphic, (x*TILESIZE, y*TILESIZE))
I do the same thing for the player, monsters, items and etc, but everything in it's own classmethod.
My camera is set like this:
class Camera(Object):
def __init__(self, x, y):
graphic = ''
Object.__init__(self, graphic, x, y)
self.rect = pygame.Rect(x, y, CAMERA_WIDTH * 2 + 5, CAMERA_HEIGHT * 2 + 5)
def update(self):
self.x = PLAYER.x
self.y = PLAYER.y
startx = ((self.x * TILESIZE) - CAMERA_WIDTH) + 5
starty = ((self.y * TILESIZE) - CAMERA_HEIGHT) + 5
self.rect = pygame.Rect(startx, starty, CAMERA_WIDTH * 2 + 5, CAMERA_HEIGHT * 2 + 5)
So I tried what user3762084 said.
in short:
for x in xrange(mymap.width):
for y in xrange(mymap.height):
... # do fov stuff
tile = Tile.At(x, y) # get tile instance
if tile:
tile.update() # update it's relative position
screen.blit(renderedgraphic, (tile.relX * TILESIZE, tile.relX * TILESIZE)) # blit the postion to it's relative position
This is what happens:
It's all squished in the side of the window. And if the player moves it all goes black.

What I have done before to make a scrolling environment was to give each object its coordinates in the world, and then give your camera a position. When you are drawing, you then calculate the position of each object on the screen based on the object's actual coordinates and the camera's coordinates.
class Tile:
def __init__(self, x, y, other_variables):
self.x = x
self.y = y
# relative vars for later
self.relX = None
self.relY = None
# then your camera should have a position as well as its width and height:
class Camera:
def __init__(self, x, y, width, height):
# assign those variables here
# your drawing function:
for tile in tiles:
tile.relX = tile.x - camera.x
tile.relY = tile.y - camera.y
# blit your tiles to the screen at the relative coordinates
In addition, you could also implement checks to see if the tile is completely outside of the camera space and not draw those tiles.

Ok I got what I was looking for, I did a litte hack tho. So if anyone have a better way to do this.
I changede the way my map was drawing
I took the Camera Rect positions [topleft and bottomright];
Converted it to World Position;
Iterate over it, with a enumerator too;
Did any lit/visited FOG calcunations with X and Y;
And Blited in the screen using the enumerators 'i' and 'j'.
Here's the code:
topleft = Map.toWorld(camera.rect.topleft)
bottomright = Map.toWorld(camera.rect.bottomright)
for i, x in enumerate(xrange(topleft[0], bottomright[0])):
for j, y in enumerate(xrange(topleft[1], bottomright[1])):
tile = mymap.tileAt(x, y)
object = [obj for obj in Object.OBJECTS if obj.pos == (x,y)]
if tile:
lit = field_of_view.lit(x, y)
visited = field_of_view.visited(x, y)
graphic = tile.graphic
if lit:
color = tile.color
elif visited:
color = GRAY
else:
color = BLACK
renderedgraphic = myfont.render(ch, 1, graphic)
screen.blit(renderedgraphic, Map.toScreen((i + 1, j)))
if object:
Draw.drawObject(object[0], Map.toScreen((i + 1, j)))
The only issue now is when the player is at the sides of the map it shows a black border.

Related

builtins.AttributeError: 'module' object has no attribute 'one_tile'

HI I keep getting that error message from the line below
pygame.one_tile.blit(self.i_list[img_index], pos)
and this is from the function below
def create_grid(self, grid_size):
self.grid = [ ]
tile_width = self.surface.get_width() / grid_size
tile_height = self.surface.get_height() / grid_size
# this for loop creates each row in our grid
for i in range(grid_size):
one_row = [ ]
img_index = 0
for j in range(grid_size):
y = i * tile_height
x = j * tile_width
pos = (x,y)
one_tile = Tile(pos, tile_width, tile_height, self.surface)
pygame.one_tile.blit(self.i_list[img_index], pos)
img_index += 1
one_row.append(one_tile)
self.grid.append(one_row)
I'm writing a code for the memory game version 1 (the game that has 8 pairs of images and you need to memorize which image was on which card and match a pair) and I keep get that error message but I don;t really know what I should do to solve it. Any help would be appreciated very much. Thank you!!
and my full code is
import pygame, random
# User-defined functions
def main():
# initialize all pygame modules (some need initialization)
pygame.init()
# create a pygame display window
pygame.display.set_mode((500, 400))
# set the title of the display window
pygame.display.set_caption('A template for graphical games with two moving dots')
# get the display surface
w_surface = pygame.display.get_surface()
# create a game object
game = Game(w_surface)
# start the main game loop by calling the play method on the game object
game.play()
# quit pygame and clean up the pygame window
pygame.quit()
# User-defined classes
class Game:
# An object in this class represents a complete game.
def __init__(self, surface):
# Initialize a Game.
# - self is the Game to initialize
# - surface is the display window surface object
# === objects that are part of every game that we will discuss
self.surface = surface
self.bg_color = pygame.Color('black')
self.FPS = 60
self.game_Clock = pygame.time.Clock()
self.close_clicked = False
self.continue_game = True
# === game specific objects
self.max_frames = 150
self.frame_counter = 0
self.i_list = []
self.images = ["image1.bmp", "image2.bmp", "image3.bmp", "image4.bmp", "image5.bmp", "image6.bmp", "image7.bmp", "image8.bmp"]
for i in self.images:
pygame.image.load(i)
self.i_list.append(i)
self.i_list.append(i)
random.shuffle(self.i_list)
self.create_grid(4)
def create_grid(self, grid_size):
self.grid = [ ]
tile_width = self.surface.get_width() / grid_size
tile_height = self.surface.get_height() / grid_size
# this for loop creates each row in our grid
for i in range(grid_size):
one_row = [ ]
img_index = 0
for j in range(grid_size):
y = i * tile_height
x = j * tile_width
pos = (x,y)
one_tile = Tile(pos, tile_width, tile_height, self.surface)
pygame.one_tile.blit(self.i_list[img_index], pos)
img_index += 1
one_row.append(one_tile)
self.grid.append(one_row)
def play(self):
# Play the game until the player presses the close box.
# - self is the Game that should be continued or not.
while not self.close_clicked: # until player clicks close box
# play frame
self.handle_events()
self.draw()
if self.continue_game:
self.update()
self.decide_continue()
self.game_Clock.tick(self.FPS) # run at most with FPS Frames Per Second
def handle_events(self):
# Handle each user event by changing the game state appropriately.
# - self is the Game whose events will be handled
events = pygame.event.get()
for event in events:
if event.type == pygame.QUIT:
self.close_clicked = True
def draw(self):
# Draw all game objects.
# - self is the Game to draw
self.surface.fill(self.bg_color) # clear the display surface first
for row in self.grid:
for tile in row:
tile.draw()
pygame.display.update()
def update(self):
# Update the game objects for the next frame.
# - self is the Game to update
pass
def decide_continue(self):
# Check and remember if the game should continue
# - self is the Game to check
return True
class Tile:
# A tile represents one location on a grid. Tiles hold content
# (in this case, an X or an O).
def __init__(self, screen_position, width, height, surface):
# initialize one instance of our Tile class. Tiles represent
# one 'position' in our tic-tac-toe board.
# - self: the tile being initialized
# - screen_position: the [x, y] coordinates to draw the tile at
# - surface: the surface on which to draw the tile
# - height: the height of the tile when it is drawn
# - width: the width of the tile when it is drawn
self.screen_position = screen_position
self.surface = surface
self.content = ''
self.height = height
self.width = width
def draw(self):
# draw the contents of a tile to its surface.
tile_boundary = pygame.Rect(self.screen_position[0],
self.screen_position[1],
self.width,
self.height)
pygame.draw.rect(self.surface, pygame.Color("white"), tile_boundary, 2)
main()
I recommend to add an .image attribute and .visible attribute to the class Tile. Each tile knows the associated image and has a state if the image in on the tile is visible:
class Tile:
# A tile represents one location on a grid. Tiles hold content
# (in this case, an X or an O).
def __init__(self, screen_position, width, height, surface, image):
# initialize one instance of our Tile class. Tiles represent
# one 'position' in our tic-tac-toe board.
# - self: the tile being initialized
# - screen_position: the [x, y] coordinates to draw the tile at
# - surface: the surface on which to draw the tile
# - height: the height of the tile when it is drawn
# - width: the width of the tile when it is drawn
self.screen_position = screen_position
self.surface = surface
self.content = ''
self.height = height
self.width = width
self.image = image
self.visible = False
def show(self, visible):
self.visible = visible
def draw(self):
# draw the contents of a tile to its surface.
tile_boundary = pygame.Rect(self.screen_position[0],
self.screen_position[1],
self.width,
self.height)
pygame.draw.rect(self.surface, pygame.Color("white"), tile_boundary, 2)
if self.visible:
img_rect = self.image.get_rect(center = tile_boundary.center)
self.surface.blit(self.image, img_rect.topleft)
To create an image list, you've to load the image. The return value of pygame.image.load is a pygame.Surface object which can be append to i_list:
self.i_list = []
self.images = ["image1.bmp", "image2.bmp", "image3.bmp", "image4.bmp", "image5.bmp", "image6.bmp", "image7.bmp", "image8.bmp"]
for imgname in self.images:
img = pygame.image.load(imgname)
self.i_list.append(img)
random.shuffle(self.i_list)
Pass the image to the constructor of Tile:
for i in range(grid_size):
one_row = [ ]
img_index = 0
for j in range(grid_size):
pos = (j * tile_width, i * tile_height)
one_tile = Tile(pos, tile_width, tile_height, self.surface, self.i_list[img_index])
img_index += 1
one_row.append(one_tile)
self.grid.append(one_row)
Note, for debug reasons you can all the state of all the images "visible" (self.visible = True in the constructor of Tiles).

Rotating a rectangle (not image) in pygame

In pygame I use pygame.draw.rect(screen, color, rectangle) for all the rectangles in my program. I want to be able to rotate these rectangles to any angle. I have seen the following code to rotate IMAGES but my question is with RECTANGLES.
pygame.transform.rotate(image, angle)
But I am working with rectangles, I don't have an image or "surface" that I can rotate. When I try to rotate a rectangle with
rect = pygame.draw.rect(screen, self.color, self.get_rectang())
rotatedRect = pygame.transform.rotate(rect, self.rotation)
screen.blit(rotatedRect)
This gives TypeError: must be pygame.Surface, not pygame.Rect on the line with .rotate()
My question is, how can I rotate a and display a RECTANGLE(x,y,w,h), not an image, in pygame.
The linked post that this is a "potential duplicate" of is not a duplicate. One answer explains about the consequences of rotating a rectangle and the other uses code for rotating an image.
See the second answer here: Rotating a point about another point (2D)
I think rectangles can only be horiz or vertical in their oreintation. You need to define the corners and rotate them and then draw and fill between them.
The other way is to make a class
class myRect(pygame.Surface):
def __init__(self, parent, xpos, ypos, width, height):
super(myRect, self).__init__(width, height)
self.xpos = xpos
self.ypos = ypos
self.parent = parent
def update(self, parent):
parent.blit(self, (self.xpos, self.ypos))
def rotate(self, angle):
#(your rotation code goes here)
and use that instead, as then you will be able to rotate it using the transform function.
import pygame as py
# define constants
WIDTH = 500
HEIGHT = 500
FPS = 30
# define colors
BLACK = (0 , 0 , 0)
GREEN = (0 , 255 , 0)
# initialize pygame and create screen
py.init()
screen = py.display.set_mode((WIDTH , HEIGHT))
# for setting FPS
clock = py.time.Clock()
rot = 0
rot_speed = 2
# define a surface (RECTANGLE)
image_orig = py.Surface((100 , 100))
# for making transparent background while rotating an image
image_orig.set_colorkey(BLACK)
# fill the rectangle / surface with green color
image_orig.fill(GREEN)
# creating a copy of orignal image for smooth rotation
image = image_orig.copy()
image.set_colorkey(BLACK)
# define rect for placing the rectangle at the desired position
rect = image.get_rect()
rect.center = (WIDTH // 2 , HEIGHT // 2)
# keep rotating the rectangle until running is set to False
running = True
while running:
# set FPS
clock.tick(FPS)
# clear the screen every time before drawing new objects
screen.fill(BLACK)
# check for the exit
for event in py.event.get():
if event.type == py.QUIT:
running = False
# making a copy of the old center of the rectangle
old_center = rect.center
# defining angle of the rotation
rot = (rot + rot_speed) % 360
# rotating the orignal image
new_image = py.transform.rotate(image_orig , rot)
rect = new_image.get_rect()
# set the rotated rectangle to the old center
rect.center = old_center
# drawing the rotated rectangle to the screen
screen.blit(new_image , rect)
# flipping the display after drawing everything
py.display.flip()
py.quit()
a more complex version of the quick replacement, in which you can define an arbitrary rotation center point for your rectangle - even outside of it (tested in python3):
def rectRotated( surface, color, pos, fill, border_radius, rotation_angle, rotation_offset_center = (0,0), nAntialiasingRatio = 1 ):
"""
- rotation_angle: in degree
- rotation_offset_center: moving the center of the rotation: (-100,0) will turn the rectangle around a point 100 above center of the rectangle,
if (0,0) the rotation is at the center of the rectangle
- nAntialiasingRatio: set 1 for no antialising, 2/4/8 for better aliasing
"""
nRenderRatio = nAntialiasingRatio
sw = pos[2]+abs(rotation_offset_center[0])*2
sh = pos[3]+abs(rotation_offset_center[1])*2
surfcenterx = sw//2
surfcentery = sh//2
s = pg.Surface( (sw*nRenderRatio,sh*nRenderRatio) )
s = s.convert_alpha()
s.fill((0,0,0,0))
rw2=pos[2]//2 # halfwidth of rectangle
rh2=pos[3]//2
pg.draw.rect( s, color, ((surfcenterx-rw2-rotation_offset_center[0])*nRenderRatio,(surfcentery-rh2-rotation_offset_center[1])*nRenderRatio,pos[2]*nRenderRatio,pos[3]*nRenderRatio), fill*nRenderRatio, border_radius=border_radius*nRenderRatio )
s = pygame.transform.rotate( s, rotation_angle )
if nRenderRatio != 1: s = pygame.transform.smoothscale(s,(s.get_width()//nRenderRatio,s.get_height()//nRenderRatio))
incfromrotw = (s.get_width()-sw)//2
incfromroth = (s.get_height()-sh)//2
surface.blit( s, (pos[0]-surfcenterx+rotation_offset_center[0]+rw2-incfromrotw,pos[1]-surfcentery+rotation_offset_center[1]+rh2-incfromroth) )
You cannot rotate a rectangle drawn by pygame.draw.rect. You have to create a transparent pygame.Surface and rotate the Surface:
rect_surf = pygame.Surface((widht, height), pygame.SRCLAPHA)
rect_surf.fill(color)
See How do I rotate an image around its center using PyGame?, to rotate the Surface.
I made a class which handles the rotation for you...
Extended from Ashish's design
from pygame import Surface, transform
from consts import screen
class BaseEntity:
def __init__(self, x: int, y: int):
self.x = x
self.y = y
class Rectangle(BaseEntity):
def __init__(self, x: int, y: int, width: int, height: int, color: tuple):
super().__init__(x, y)
self.width = width
self.height = height
self.color = color
self.rotatation = 0
# the rectangle is a surface itself
self.surface = Surface((width, height))
self.surface.set_colorkey((0, 0, 0))
self.surface.fill(color)
self.rect = self.surface.get_rect()
def display(self, angle=None):
# updating values
self.surface.fill(
self.color
) # refill the surface color if you change it somewhere in the program
self.rect = self.surface.get_rect()
self.rect.center = (self.x, self.y)
# renderer
if angle is not None:
self.rotatation = angle
old_center = self.rect.center
new = transform.rotate(self.surface, self.rotatation)
self.rect = new.get_rect()
self.rect.center = old_center
screen.blit(new, self.rect)
Using a bit of trigonometry and the polygon function, I'm able to draw a rotated rectangle.
import math
import pygame.draw
def draw_rectangle(x, y, width, height, color, rotation=0):
"""Draw a rectangle, centered at x, y.
Arguments:
x (int/float):
The x coordinate of the center of the shape.
y (int/float):
The y coordinate of the center of the shape.
width (int/float):
The width of the rectangle.
height (int/float):
The height of the rectangle.
color (str):
Name of the fill color, in HTML format.
"""
points = []
# The distance from the center of the rectangle to
# one of the corners is the same for each corner.
radius = math.sqrt((height / 2)**2 + (width / 2)**2)
# Get the angle to one of the corners with respect
# to the x-axis.
angle = math.atan2(height / 2, width / 2)
# Transform that angle to reach each corner of the rectangle.
angles = [angle, -angle + math.pi, angle + math.pi, -angle]
# Convert rotation from degrees to radians.
rot_radians = (math.pi / 180) * rotation
# Calculate the coordinates of each point.
for angle in angles:
y_offset = -1 * radius * math.sin(angle + rot_radians)
x_offset = radius * math.cos(angle + rot_radians)
points.append((x + x_offset, y + y_offset))
pygame.draw.polygon(screen, color, points)
https://replit.com/#TimSwast1/RotateARectanlge?v=1
a quick replacement of the base pygame function adding rotation:
def rectRotated( surface, color, pos, fill, border_radius, angle ):
"""
- angle in degree
"""
max_area = max(pos[2],pos[3])
s = pg.Surface((max_area,max_area))
s = s.convert_alpha()
s.fill((0,0,0,0))
pg.draw.rect(s, color,(0,0,pos[2],pos[3]),fill, border_radius=border_radius)
s = pygame.transform.rotate(s,angle)
surface.blit( s, (pos[0],pos[1]) )
This code simulates rotating rectangles falling towards the ground. I used it in one of my games to make the background look awesome
import pygame
import random
class Square(pygame.sprite.Sprite):
def __init__(self, x, y):
super(Square, self).__init__()
self.win = win
self.color = (128, 128, 128)
self.speed = 3
self.angle = 0
self.side = random.randint(15, 40)
self.surface = pygame.Surface((self.side, self.side), pygame.SRCALPHA)
self.surface.set_colorkey((200,200,200))
self.rect = self.surface.get_rect(center=(x, y))
def update(self, win):
center = self.rect.center
self.angle = (self.angle + self.speed) % 360
image = pygame.transform.rotate(self.surface , self.angle)
self.rect = image.get_rect()
self.rect.center = center
self.rect.y += 1.5
if self.rect.top >= HEIGHT:
self.kill()
pygame.draw.rect(self.surface, self.color, (0,0, self.side, self.side), 4)
win.blit(image, self.rect)
if __name__ == '__main__':
pygame.init()
SCREEN = WIDTH, HEIGHT = 288, 512
win = pygame.display.set_mode(SCREEN, pygame.NOFRAME)
clock = pygame.time.Clock()
FPS = 60
count = 0
square_group = pygame.sprite.Group()
running = True
while running:
win.fill((200,200,200))
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
running = False
count += 1
if count % 100 == 0:
x = random.randint(40, WIDTH-40)
y = 0
square = Square(x, y)
square_group.add(square)
count = 0
square_group.update(win)
pygame.draw.rect(win, (30,30,30), (0, 0, WIDTH, HEIGHT), 8)
clock.tick(FPS)
pygame.display.update()
pygame.quit()
Here's the output, it's not an gif though
Now if you want color filled rectangle instead of bordered only, update this line on line 31
pygame.draw.rect(self.surface, self.color, (0,0, self.side, self.side))
and if you don't want the rectangle to fall down comment line 26
A concise and fast function to draw a rotated rectangle. Uses NumPy
def rectRotated(self, surface, rect, color, rotation):
"""
Draws a rotated Rect.
surface: pygame.Surface
rect: pygame.Rect
color: pygame.Color
rotation: float (degrees)
return: np.ndarray (vertices)
"""
# calculate the rotation in radians
rot_radians = -rotation * pi / 180
# calculate the points around the center of the rectangle, taking width and height into account
angle = atan2(rect.height / 2, rect.width / 2)
angles = [angle, -angle + pi, angle + pi, -angle]
radius = sqrt((rect.height / 2)**2 + (rect.width / 2)**2)
# create a numpy array of the points
points = np.array([
[rect.x + radius * cos(angle + rot_radians), rect.y + radius * sin(angle + rot_radians)]
for angle in angles
])
# draw the polygon
pygame.draw.polygon(surface, color, points)
# return the vertices of the rectangle
return points

pygame blitting - center

I am trying to make a script in python for pygame to draw a button with text centered, but when I blit onto the screen, it blits to the x and y I give it, not a proportionally centered location. I want to be able to center it to a set of (x,y,w,h). How would I do this? Here's my code:
# Imports
import pygame
class Text:
'Centered Text Class'
# Constructror
def __init__(self, text, (x,y,w,h), color = (0,0,0)):
self.x = x
self.y = y
self.w = w
self.h = h
# Start PyGame Font
pygame.font.init()
font = pygame.font.SysFont("sans", 20)
self.txt = font.render(text, True, color)
# Draw Method
def Draw(self, screen):
coords = (self.x, self.y)
screen.blit(self.txt, coords)
Edit: Comments, yes I know but I only used x and y as temporary variables because I have no idea what the centered x and y would be to center the text. (I want to know how to center its CENTER to a rect, not its top left corner)
You'll want to use the font.size() method to determine how large the rendered text will be.
Something like:
class Text:
"""Centered Text Class"""
# Constructror
def __init__(self, text, (x,y), color = (0,0,0)):
self.x = x #Horizontal center of box
self.y = y #Vertical center of box
# Start PyGame Font
pygame.font.init()
font = pygame.font.SysFont("sans", 20)
self.txt = font.render(text, True, color)
self.size = font.size(text) #(width, height)
# Draw Method
def Draw(self, screen):
drawX = self.x - (self.size[0] / 2.)
drawY = self.y - (self.size[1] / 2.)
coords = (drawX, drawY)
screen.blit(self.txt, coords)
I think something like the following does what you want. It uses pygame.font.Font.size() to determine the amount of space needed to render the text, and then centers that within rectangular region defined by CenteredText instance.
class CenteredText(object):
""" Centered Text Class
"""
def __init__(self, text, (x,y,w,h), color=(0,0,0)):
self.x, self.y, self.w, self.h = x,y,w,h
pygame.font.init()
font = pygame.font.SysFont("sans", 20)
width, height = font.size(text)
xoffset = (self.w-width) // 2
yoffset = (self.h-height) // 2
self.coords = self.x+xoffset, self.y+yoffset
self.txt = font.render(text, True, color)
def draw(self, screen):
screen.blit(self.txt, self.coords)
# for testing purposes, draw the rectangle too
rect = Rect(self.x, self.y, self.w, self.h)
pygame.draw.rect(screen, (0,0,0), rect, 1)
Given:
text = CenteredText('Hello world', (200,150,100,100))
Here's the results from calling text.draw(screen) in a 500x400 pixel window.
If you want to perfectly centre an object:
When you give Pygame coordinates for an object it takes them to be the coordinates for the upper left corner. Thus we have to halve the x and y coordinates.
coords = (self.x/2, self.y/2)
screen.blit(self.txt, coords)
Other than that your question is unclear.
Let pygame do the maths for you by using Rects and assigning the text's center to the destination center:
# Start PyGame Font
pygame.font.init()
font = pygame.font.SysFont("sans", 20)
class Text:
'Centered Text Class'
# Constructror
def __init__(self, text, (x,y,w,h), color = (0,0,0)):
self.rect = pygame.Rect(x, y, w, h)
self.txt = font.render(text, True, color)
# Draw Method
def Draw(self, screen):
coords = self.txt.get_rect()
coords.center = self.rect.center
screen.blit(self.txt, coords)

Displaying movement tiles for a Python game

I am trying to create a simple tactical RPG in python, similar to Fire Emblem or Advanced Wars, but in ASCII. I have been following this tutorial, which uses the libtcod module, and modifying the tutorial code for my own purposes
Right now I am struck trying to display movement tiles for each character. Basically I want the game to display the tiles each character can move to, whenever the user clicks his mouse over that character. I have created a function (get_total_squares) that generates the coordinates of the tiles to be highlighted based on the character's coordinates and movement range (hard-coded to 5, for now) and a function (show_move_tiles) to highlight these tiles. To record the click coordinates I have another function target_tile and a function to see if the coordinates match with a character's (target_character)
The program runs without errors, but clicking the mouse over a character does nothing (no tiles are shown).
Here is the code:
import libtcodpy as libtcod
#actual size of the window
SCREEN_WIDTH = 80
SCREEN_HEIGHT = 50
#size of the map
MAP_WIDTH = 80
MAP_HEIGHT = 43
LIMIT_FPS = 20 #20 frames-per-second maximum
color_dark_wall = libtcod.Color(0, 0, 50)
color_dark_ground = libtcod.Color(5, 50, 0)
color_move_tile = libtcod.Color (100, 0, 0)
class Tile:
#a tile of the map and its properties
def __init__(self, blocked, block_sight = None):
self.blocked = blocked
#by default, if a tile is blocked, it also blocks sight
if block_sight is None: block_sight = blocked
self.block_sight = block_sight
def target_tile():
#return the position of a tile left-clicked, or (None,None) if right-clicked.
global key, mouse
while True:
libtcod.console_flush()
libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS|libtcod.EVENT_MOUSE, key, mouse)
render_all()
(x, y) = (mouse.cx, mouse.cy)
if mouse.rbutton_pressed or key.vk == libtcod.KEY_ESCAPE:
return (None, None) #cancel if the player right-clicked or pressed Escape
if mouse.lbutton_pressed:
return (x, y)
print "works!"
def target_character():
#Shows movement tiles if character is on click-coordinate
(x, y) = target_tile()
#return the first clicked character
for obj in objects:
if obj.x == x and obj.y == y:
obj.show_move_tiles()
def make_map():
global map
#fill map with "unblocked" tiles
map = [[ Tile(False)
for y in range(MAP_HEIGHT) ]
for x in range(MAP_WIDTH) ]
map[30][22].blocked = True
map[30][22].block_sight = True
map[50][22].blocked = True
map[50][22].block_sight = True
class Object:
#this is a generic object: the player, a monster, an item, the stairs...
#it's always represented by a character on screen.
def __init__(self, x, y, char, color):
self.x = x
self.y = y
self.char = char
self.color = color
def move(self, dx, dy):
#move by the given amount
if not map[self.x + dx][self.y + dy].blocked:
self.x += dx
self.y += dy
def draw(self):
#set the color and then draw the character that represents this object at its position
libtcod.console_set_default_foreground(con, self.color)
libtcod.console_put_char(con, self.x, self.y, self.char, libtcod.BKGND_NONE)
def show_move_tiles(self):
global mouse
global color_move_tile
(x,y) = (mouse.cx, mouse.cy)
coord_in_range = get_total_squares(5, self.x, self.y)
for [x,y] in coord_in_range:
if [x,y] is not [self.x, self.y]:
libtcod.console_set_char_background(con, x, y, color_move_tile, libtcod.BKGND_SET)
def clear(self):
#erase the character that represents this object
libtcod.console_put_char_ex(con, self.x, self.y, '.', libtcod.white, libtcod.black)
def handle_keys():
#key = libtcod.console_check_for_keypress() #real-time
global key #turn-based
if key.vk == libtcod.KEY_ENTER and key.lalt:
#Alt+Enter: toggle fullscreen
libtcod.console_set_fullscreen(not libtcod.console_is_fullscreen())
elif key.vk == libtcod.KEY_ESCAPE:
return True #exit game
def get_total_squares(dist, playerx, playery):
coord_in_range = []
for x in range(-dist,dist+1):
for y in range(-dist, dist+1):
if abs(x)+abs(y) <= dist:
coord_in_range.append([playerx+x, playery+y])
return coord_in_range
def render_all():
global color_dark_wall
global color_dark_ground
#go through all tiles, and set their background color
for y in range(MAP_HEIGHT):
for x in range(MAP_WIDTH):
wall = map[x][y].block_sight
if wall:
libtcod.console_put_char_ex(con, x, y, '#', libtcod.white, libtcod.black)
else:
libtcod.console_put_char_ex(con, x, y, '.', libtcod.white, libtcod.black)
#draw all objects in the list and show movement if mouse hovers over player
for item in objects:
item.draw()
if mouse.cx == item.x and mouse.cy == item.y:
item.show_move_tiles()
#blit the contents of "con" to the root console
libtcod.console_blit(con, 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT, 0, 0, 0)
#############################################
# Initialization & Main Loop
#############################################
libtcod.console_set_custom_font('arial10x10.png', libtcod.FONT_TYPE_GREYSCALE | libtcod.FONT_LAYOUT_TCOD)
libtcod.console_init_root(SCREEN_WIDTH, SCREEN_HEIGHT, 'python/libtcod tutorial', False)
libtcod.sys_set_fps(LIMIT_FPS)
con = libtcod.console_new(SCREEN_WIDTH, SCREEN_HEIGHT)
#create object representing the player
player = Object(30, 15, '#', libtcod.white)
#create an NPC
npc = Object(SCREEN_WIDTH/2 - 5, SCREEN_HEIGHT/2, '#', libtcod.yellow)
#the list of objects with those two
objects = [npc, player]
make_map()
mouse = libtcod.Mouse()
key = libtcod.Key()
while not libtcod.console_is_window_closed():
libtcod.sys_check_for_event(libtcod.EVENT_KEY_PRESS|libtcod.EVENT_MOUSE,key,mouse)
#render the screen
render_all()
target_tile()
libtcod.console_flush()
#erase all objects at their old locations, before they move
for object in objects:
object.clear()
#handle keys and exit game if needed
exit = handle_keys()
if exit:
break
EDIT: I solved the previous problem of the movement tiles not showing up when I hovered the mouse over the character, but have updated the problem with a new one: trying to toggle the movement tiles on and off with a mouse click

Python pygame Detect if mouse is over non transparent part of surface

I am trying to make a UI for my game and there are some curves to the UI. Now I can detect collision between two surfaces. I can detect by pixel between two sprites, but it seems mouse detection by pixel is alluding me. Basically I want to detect when the mouse is over the UI and then ignore everything below that while getting the UI.
This is a picture of what I have so far. If you notice the pink square the mouse is over the GUI while the yellow selector box is over a tile. The yellow selector is a box frame over a tile.
I am using pygame with openGL but at this point I am looking for ANY solution to this. I can adapt pretty easily as I am not new to programming and pretty much looking for any solution.
Also I would post the code but to much code to post so if thing specific is needed let me know.
One thing to note is that the GUI is flexable in that the upper left area will slide in and out. Also the white is just placeholder so final colors are not used and would be difficult to check. Is it possible to get the surface elements under the mouse when clicked by z order?
Texture
import pygame
from OpenGL.GL import *
from OpenGL.GLU import *
class Texture(object):
image = None
rect = None
src = ''
x = 0
y = 0
'''
zOrder Layers
0 - background
1 -
2 -
3 - Tile Selector
s - Tiles
5 -
6 -
7 - Panels
8 - Main Menu
9 - GUI Buttons
10 -
'''
def __init__(self, src):
self.src = src
self.image = pygame.image.load(src)
self.image.set_colorkey(pygame.Color(255,0,255,0))
self.rect = self.image.get_rect()
texdata = pygame.image.tostring(self.image,"RGBA",0)
# create an object textures
self.texid = glGenTextures(1)
# bind object textures
glBindTexture(GL_TEXTURE_2D, self.texid)
# set texture filters
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
# Create texture image
glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA,self.rect.w,self.rect.h,0,GL_RGBA,GL_UNSIGNED_BYTE,texdata)
self.newList = glGenLists(2)
glNewList(self.newList, GL_COMPILE)
glBindTexture(GL_TEXTURE_2D, self.texid)
glBegin(GL_QUADS)
glTexCoord2f(0, 0); glVertex3f(0, 0 ,0)
glTexCoord2f(0, 1); glVertex3f(0, self.rect.h, 0)
glTexCoord2f(1, 1); glVertex3f(self.rect.w, self.rect.h, 0)
glTexCoord2f(1, 0); glVertex3f(self.rect.w, 0, 0)
glEnd()
glEndList()
def getImg(self):
return self.image
def getPos(self):
rect = self.getImg().get_rect()
pos = dict(x=self.x,y=self.y,w=rect[2],h=rect[3])
return pos
def draw(self,x,y,rotate=0):
glLoadIdentity()
self.x = int(x)
self.y = int(y-self.rect.h+32)
glTranslatef(x,y-self.rect.h+32,0)
glPushAttrib(GL_TRANSFORM_BIT)
glMatrixMode(GL_TEXTURE)
glLoadIdentity()
glRotatef(rotate,0,0,1)
glPopAttrib()
if glIsList(self.newList):
glCallList(self.newList)
gui Class
import hashlib, string, pygame
from classes.texture import Texture
'''
Created on Jun 2, 2013
#author: Joel
'''
class gui(object):
INSTANCES = 0 # Count of instances of buildings
ID = 0 # Building ID
TYPE = 0 # Building type
NAME = '' # name of Building
DESCRIPTION = '' # Description of building
IMAGE = '' # Image name of building
zOrder = 0
clickable = True
def __init__(self, Game, name = 'Building', description = '', image = 'panel'):
self.INSTANCES += 1
self.setName(name)
self.setDescription(description)
self.setImage(Game, Game.SETTING["DIR"]["IMAGES"] + Game.SETTING["THEME"] + '\\gui\\'+image+'.png')
self.setType(name.lower())
self.setZ(6)
def getDescription(self):
return self.DESCRIPTION
def setDescription(self, description):
self.DESCRIPTION = description
def getID(self):
return self.ID
def setID(self, i):
allchr = string.maketrans('','')
nodigits = allchr.translate(allchr, string.digits)
s = hashlib.sha224(i).hexdigest()
s = s.translate(allchr, nodigits)
self.ID = s[-16:]
def getImage(self):
return self.IMAGE
def setImage(self, Game, i):
self.IMAGE = Texture(Game.CWD + '\\' + i)
def getName(self):
return self.NAME
def setName(self, name):
self.NAME = name
def getType(self):
return self.TYPE
def setType(self, t):
self.TYPE = t
def click(self, x, y):
if pygame.mouse.get_pressed()[0] == 1:
if x > self.x and x < (self.x + self.rect.w):
if y > self.y and y < (self.y + self.rect.h):
print("Clicked: " + str(self.x) + ', ' + str(self.y) + ', ' + str(self.rect.w) + ', ' + str(self.rect.y))
def getClickable(self):
return self.clickable
def setClickable(self, c):
self.clickable = c
def getZ(self):
return self.zOrder
def setZ(self, z):
self.zOrder = z
You could create a mask of the UI (this would be easiest if the UI is contained in one surface which is then applied to the screen surface), and set the threshold of the mask to the appropriate value so that your transparent pixels are set to 0 in the mask.
http://www.pygame.org/docs/ref/mask.html#pygame.mask.from_surface
With the mask object's get_at((x,y)) function you can test if a specific pixel of the mask is set (a non-zero value is returned if the pixel is set).
http://www.pygame.org/docs/ref/mask.html#pygame.mask.Mask.get_at
If you pass in the mouse's position, you can verify that it is over a visible part of the UI if you receive a non-zero value.
Two possible answers:
1) Statically create a 2D array of True or False that is as big as the screen - True if clicking here would click on the UI, False if clicking here would not click on the UI. Test clicks against the position in this array.
2) Use the 'paint and check' algorithm (don't recall the real name). You know how when you draw to the screen you draw the background, then background objects, then foreground objects? You can use a similar trick to detect what object you have clicked on - draw the background in one solid colour, each object in another solid colour, each UI element in another solid colour, etc... and as long as each solid colour is unique, you can test what colour pixel is under the cursor in this buffer and use it to determine what was visible and clicked on by the mouse.
Okay I am thinking of this as the best option rather then some of the alternatives. Will keep everyone up to date if this works or not.
global click variable to store data in a dict
Objects have layer variable ranging from 1 to ? from lowest to greatest layer(similar to html zIndex)
Primary Loop
reset the global click var
click event get position
loop over clickable objects to get everything under mouse
loop over everything under mouse to get highest layer
Return for global click var
run click code in object.
Layer organization currently which can be modified.
zOrder Layers
background
na
Tiles
Tile Selector
na
na
Panels
Main Menu
GUI Buttons
na
Loop
for i in range(len(self.OBJECTS)):
#img = Texture(see op)
img = self.OBJECTS[i].IMAGE
print(img)
e = None
if self.OBJECTS[i].zOrder == 4: # is isometric image
# tx and ty are translated positions for screen2iso. See Below
if ((self.tx >= 0 and self.tx < self.SETTING['MAP_WIDTH']) and (self.ty >= 0 and self.ty < self.SETTING['MAP_HEIGHT'])):
# map_x and map_y are starting points for the map its self.
ix, iy = self.screen2iso(
(x - (self.map_x + (self.SETTING['TILE_WIDTH'] / 2))),
(y - (self.map_y))
)
imgx, imgy = self.screen2iso(
(img.x - (self.map_x + (self.SETTING['TILE_WIDTH'] / 2))),
(img.y - (self.map_y))
)
if (imgx+2) == ix:
if (imgy+1) == iy:
e = self.OBJECTS[i]
else:
continue
else:
continue
else: # Not an isometric image
if x > img.x and x < (img.x + img.rect[2]):
if y > img.y and y < (img.y + img.rect[3]):
#is click inside of visual area of image?
if self.getCordInImage(x, y, self.OBJECTS[i].IMAGE):
if self.getAlphaOfPixel(self.OBJECTS[i]) != 0:
e = self.OBJECTS[i]
else:
continue
else:
continue
else:
continue
if e != None:
if self.CLICKED['zOrder'] < e.getZ():
self.CLICKED['zOrder'] = e.getZ()
self.CLICKED['e'] = e
else:
continue
else:
continue
getCordInImage
def getCordInImage(self, x, y, t):
return [x - t.x, y - t.y]
getAlphaOfPixel
def getAlphaOfPixel(self, t):
mx,my = pygame.mouse.get_pos()
x,y = self.getCordInImage(mx,my,t.IMAGE)
#mask = pygame.mask.from_surface(t.IMAGE.image)
return t.IMAGE.image.get_at([x,y])[3]
screen2iso
def screen2iso(self, x, y):
x = x / 2
xx = (y + x) / (self.SETTING['TILE_WIDTH'] / 2)
yy = (y - x) / (self.SETTING['TILE_WIDTH'] / 2)
return xx, yy
iso2screen
def iso2screen(self, x, y):
xx = (x - y) * (self.SETTING['TILE_WIDTH'] / 2)
yy = (x + y) * (self.SETTING['TILE_HEIGHT'] / 2)
return xx, yy

Categories