Right now our code creates a grid starting at the top left and filling in rows and columns from left to right, row by row. Currently, there are a bunch of images it can pick from. It is set using a handful of IF statements that picks between shapes and rareshapes. What I am trying to figure out how to do is change the code so instead of it picking a random rareshape, I can decide what rareshape spawns. Still new to Python and finding a lot of little things that make sense to me from other languages don't work in Python so its throwing me off a little.
EDIT:
Here is the full code. Credit for the base code written by cactusbin and revised by Gareth Rees.
import pygame, random, time, sys
from pygame.locals import *
import itertools
import os
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
SHAPE_WIDTH = 64 # Width of each shape (pixels).
SHAPE_HEIGHT = 64 # Height of each shape (pixels).
PUZZLE_COLUMNS = 10 # Number of columns on the board.
PUZZLE_ROWS = 11 # Number of rows on the board.
MARGIN = 118 # Margin around the board (pixels).
WINDOW_WIDTH = PUZZLE_COLUMNS * SHAPE_WIDTH + 2 * MARGIN + 485
WINDOW_HEIGHT = PUZZLE_ROWS * SHAPE_HEIGHT + 2 * MARGIN - 150
FONT_SIZE = 60
TEXT_OFFSET = MARGIN + 950
# Map from number of matches to points scored.
MINIMUM_MATCH = 10
EXTRA_LENGTH_POINTS = .1
RANDOM_POINTS = .3
DELAY_PENALTY_SECONDS = 1
DELAY_PENALTY_POINTS = 0
FPS = 30
EXPLOSION_SPEED = 15 # In frames per second.
SPIN_SPEED = 15
REFILL_SPEED = 10 # In cells per second.
VERTICAL = False
class Cell(object):
"""
A cell on the board, with properties:
`image` -- a `Surface` object containing the sprite to draw here.
`offset` -- vertical offset in pixels for drawing this cell.
"""
def __init__(self, image):
self.offset = 0.0
self.image = image
def tick(self, dt):
self.offset = max(0.0, self.offset - dt * REFILL_SPEED)
class Board(object):
"""
A rectangular board of cells, with properties:
`w` -- width in cells.
`h` -- height in cells.
`size` -- total number of cells.
`board` -- list of cells.
`matches` -- list of matches, each being a list of exploding cells.
`refill` -- list of cells that are moving up to refill the board.
`score` -- score due to chain reactions.
"""
def __init__(self, width, height):
self.explosion = [pygame.image.load('images/explosion{}.png'.format(i))
for i in range(1, 7)]
self.spin = [pygame.image.load('images/powerframe{}.png'.format(i))
for i in range (1, 12)]
self.image_color = {}
self.shapes = []
self.rareshapes = []
colors = 'red blue yellow'
letters = 'acgtu'
for c in colors.split():
im = pygame.image.load('images/{}.png'.format(c))
self.shapes.append(im)
self.image_color[im] = c
for l in letters:
im = pygame.image.load('rareimages/{}{}.png'.format(c, l))
self.rareshapes.append(im)
self.image_color[im] = l
self.background = pygame.image.load("images/bg.png")
self.blank = pygame.image.load("images/blank.png")
self.x = pygame.image.load("images/x.png")
self.w = width
self.h = height
self.size = width * height
self.board = [Cell(self.blank) for _ in range(self.size)]
self.matches = []
self.refill = []
self.score = 0.0
self.spin_time = 15
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
rare_shapes = [1, 9, 23, 27, 40, 42, 50, 56, 70, 81, 90]
for i in range(self.size):
if i in rare_shapes:
self.board[i] = Cell(random.choice(self.rareshapes))
else:
self.board[i] = Cell(random.choice(self.shapes))
def pos(self, i, j):
"""
Return the index of the cell at position (i, j).
"""
assert(0 <= i < self.w)
assert(0 <= j < self.h)
return j * self.w + i
def busy(self):
"""
Return `True` if the board is busy animating an explosion or a
refill and so no further swaps should be permitted.
"""
return self.refill or self.matches
def tick(self, dt):
"""
Advance the board by `dt` seconds: move rising blocks (if
any); otherwise animate explosions for the matches (if any);
otherwise check for matches.
"""
if self.refill:
for c in self.refill:
c.tick(dt)
self.refill = [c for c in self.refill if c.offset > 0]
if self.refill:
return
elif self.matches:
self.explosion_time += dt
f = int(self.explosion_time * EXPLOSION_SPEED)
if f < len(self.explosion):
self.update_matches(self.explosion[f])
return
self.update_matches(self.blank)
self.refill = list(self.refill_columns())
self.explosion_time = 0
self.matches = self.find_matches()
def draw(self, display):
"""
Draw the board on the pygame surface `display`.
"""
display.blit(self.background, (0, 0))
for i, c in enumerate(self.board):
display.blit(c.image,
(MARGIN + SHAPE_WIDTH * (i % self.w),
MARGIN + SHAPE_HEIGHT * (i // self.w - c.offset) - 68))
display.blit(self.x, (995, 735))
display.blit(self.x, (1112, 735))
display.blit(self.x, (1228, 735))
def swap(self, cursor):
"""
Swap the two board cells covered by `cursor` and update the
matches.
"""
i = self.pos(*cursor)
b = self.board
b[i], b[i+1] = b[i+1], b[i]
self.matches = self.find_matches()
def find_matches(self):
"""
Search for matches (lines of cells with identical images) and
return a list of them, each match being represented as a list
of board positions.
"""
def lines():
for j in range(self.h):
yield range(j * self.w, (j + 1) * self.w)
for i in range(self.w):
yield range(i, self.size, self.w)
def key(i):
return self.image_color.get(self.board[i].image)
def matches():
for line in lines():
for _, group in itertools.groupby(line, key):
match = list(group)
if len(match) >= MINIMUM_MATCH:
yield match
self.score = self.score + 1
return list(matches())
def update_matches(self, image):
"""
Replace all the cells in any of the matches with `image`.
"""
for match in self.matches:
for position in match:
self.board[position].image = image
def refill_columns(self):
"""
Move cells downwards in columns to fill blank cells, and
create new cells as necessary so that each column is full. Set
appropriate offsets for the cells to animate into place.
"""
for i in range(self.w):
target = self.size - i - 1
for pos in range(target, -1, -self.w):
if self.board[pos].image != self.blank:
c = self.board[target]
c.image = self.board[pos].image
c.offset = (target - pos) // self.w
target -= self.w
yield c
offset = 1 + (target - pos) // self.w
for pos in range(target, -1, -self.w):
c = self.board[pos]
c.image = random.choice(self.shapes)
c.offset = offset
yield c
class Game(object):
"""
The state of the game, with properties:
`clock` -- the pygame clock.
`display` -- the window to draw into.
`font` -- a font for drawing the score.
`board` -- the board of cells.
`cursor` -- the current position of the (left half of) the cursor.
`score` -- the player's score.
`last_swap_ticks` --
`swap_time` -- time since last swap (in seconds).
"""
def __init__(self):
pygame.init()
pygame.display.set_caption("Nucleotide")
self.clock = pygame.time.Clock()
self.display = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT),
DOUBLEBUF)
self.board = Board(PUZZLE_COLUMNS, PUZZLE_ROWS)
self.font = pygame.font.Font(None, FONT_SIZE)
def start(self):
"""
Start a new game with a random board.
"""
self.board.randomize()
self.cursor = [0, 0]
self.score = 0.0
self.swap_time = 125
def quit(self):
"""
Quit the game and exit the program.
"""
pygame.quit()
sys.exit()
def play(self):
"""
Play a game: repeatedly tick, draw and respond to input until
the QUIT event is received.
"""
self.start()
while True:
self.draw()
dt = min(self.clock.tick(FPS) / 1000, 1 / FPS)
self.swap_time -= dt
for event in pygame.event.get():
if event.type == KEYUP:
self.input(event.key)
elif event.type == QUIT:
self.quit()
elif self.swap_time == 0:
self.quit()
self.board.tick(dt)
def input(self, key):
"""
Respond to the player pressing `key`.
"""
if key == K_q:
self.quit()
elif key == K_RIGHT and self.cursor[0] < self.board.w - 2:
self.cursor[0] += 1
elif key == K_LEFT and self.cursor[0] > 0:
self.cursor[0] -= 1
elif key == K_DOWN and self.cursor[1] < self.board.h - 1:
self.cursor[1] += 1
elif key == K_UP and self.cursor[1] > 0:
self.cursor[1] -= 1
elif key == K_SPACE and not self.board.busy():
self.swap()
def swap(self):
"""
Swap the two cells under the cursor and update the player's score.
"""
self.board.swap(self.cursor)
def draw(self):
self.board.draw(self.display)
self.draw_score()
self.draw_time()
if VERTICAL == False:
self.draw_cursor()
elif VERTICAL == True:
self.draw_cursor2()
pygame.display.update()
def draw_time(self):
s = int(self.swap_time)
text = self.font.render(str(int(s/60)) + ":" + str(s%60).zfill(2),
True, BLACK)
self.display.blit(text, (TEXT_OFFSET, WINDOW_HEIGHT - 170))
def draw_score(self):
total_score = self.score
def draw_cursor(self):
topLeft = (MARGIN + self.cursor[0] * SHAPE_WIDTH,
MARGIN + self.cursor[1] * SHAPE_HEIGHT - 68)
topRight = (topLeft[0] + SHAPE_WIDTH * 2, topLeft[1])
bottomLeft = (topLeft[0], topLeft[1] + SHAPE_HEIGHT)
bottomRight = (topRight[0], topRight[1] + SHAPE_HEIGHT)
pygame.draw.lines(self.display, WHITE, True,
[topLeft, topRight, bottomRight, bottomLeft], 3)
if __name__ == '__main__':
Game().play()
If what you are asking for is a way to more easily specify at which rareshapecount intervals you should place a rare shape instead of a normal shape, the following approach is more readable:
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
# locations we want to place a rare shape
rare_shapes = [9, 23, 27]
for i in range(self.size):
if i in rare_shapes:
self.board[i] = Cell(random.choice(self.rareshapes))
else:
self.board[i] = Cell (random.choice(self.shapes))
Optionally, you could randomly populate rare_shapes if you don't feel like hardcoding the intervals each time, making for a more varied experience (i.e., if you're designing a game or something similar).
What you mean by "I can decide what rareshape spawns instead of it picking a random rareshape" is unclear to me. Would you care to give more explanations ? Like how you would tell the program which rareshape to use ?
In the meantime, here's a somewhat more pythonic version of your code:
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
specials = dict((x, self.rareshapes) for x in (9, 23, 27))
get_shape_source = lambda x: specials.get(x, self.shapes)
for i in xrange(min(self.size, 41)):
self.board[i] = Cell(random.choice(get_shape_source(i)))
Note that this would break if len(self.board) < min(self.size, 41) but well, that's still basically what your current code do.
edit: given your comment, the obvious way to explicitly choose which rareshape goes where is to explicitly associate images with spots. Now what's the best way to do so / the best place ton configure this really depends on your whole code or at least on more than what you posted. As a very simple and minimal exemple, you could just have this:
from collections import ordereddict
def load_images(self)
self.image_color = {}
self.shapes = []
self.rareshapes = ordereddict()
colors = 'red', 'blue', 'yellow'
letters = 'acgtu'
for c in colors:
im = pygame.image.load('images/{}.png'.format(c))
self.shapes.append(im)
self.image_color[im] = c
for l in letters:
im = pygame.image.load('rareimages/{}{}.png'.format(c, l))
self.rareshapes.[(c, l)] = im
self.image_color[im] = l
def randomize(self):
"""
Replace the entire board with fresh shapes.
"""
raremap = {
# spot index : rareshape
9: ('red', 'a')],
23: ('blue', 'u'),
27: ('yellow', 'g')
}
for i in xrange(self.size):
if i in raremap:
im = self.rareshapes[raremap[i]]
else:
im = random.choice(self.shapes)
self.board[i] = Cell(im)
But it will be just unmaintainable in the long run - too much hardcoded stuff, and too much knowledge leaking from one method to another. I don't know what 'self' is an instance of, but you should considered splitting the responsabilities so you have the invariant parts in one class and the "configration" (images to load, spots / rareshapes mapping etc) in another. Some design patterns that come to mind are TemplateMethod (where you have an abstract base class with the invariant parts and concrete subclasses implementing the "configuration" part), Builder, and of course Strategy (in your case the "Strategy" class would take care of the configuration).
Related
So there must be something wrong with the code which detects wether it should be alive or not in Cell.update(), but the glider i hardcoded in is not working as intended. The first and second generations work as intended, but on the third it dies out. Anyone know what the problem is? I know the code is a bit messy, but I'm quite new to python so it is expected. Thanks in advance!
Here is the code:
import pygame
"""
rules:
1. Any live cell with two or three live neighbours survives.
2. Any dead cell with three live neighbours becomes a live cell.
3. All other live cells die in the next generation. Similarly, all other dead cells stay dead.
number cells on screen = 32x18
cell size = 30x30
"""
# variables
WIDTH = 960
HEIGHT = 540
TITLE = "Conway's Game Of Life"
GRID_COLOUR = (200, 200, 200)
BG_COLOUR = (255, 255, 255)
grid = [[False] * 32] * 18
cells = []
live_queue = []
die_queue = []
# window
wn = pygame.display.set_mode((WIDTH, HEIGHT), vsync=1)
pygame.display.set_caption(TITLE)
# classes
class Cell:
def __init__(self, x, y, alive, index_x, index_y):
self.x = x
self.y = y
self.alive = alive
self.indexX = index_x
self.indexY = index_y
def die(self):
self.alive = False
def live(self):
self.alive = True
def get_index(self):
return self.indexX, self.indexY
def update(self):
grid_temp = grid[self.indexY]
grid_temp.pop(self.indexY)
grid_temp.insert(self.indexY, self.alive)
grid.pop(self.indexY)
grid.insert(self.indexX, grid_temp)
adjacent_alive = 0
for i in cells:
if i.x == self.x - 30 and i.y == self.y and i.alive:
adjacent_alive += 1
elif i.x == self.x + 30 and i.y == self.y and i.alive:
adjacent_alive += 1
elif i.x == self.x and i.y == self.y - 30 and i.alive:
adjacent_alive += 1
elif i.x == self.x and i.y == self.y + 30 and i.alive:
adjacent_alive += 1
elif i.x == self.x - 30 and i.y == self.y - 30 and i.alive:
adjacent_alive += 1
elif i.x == self.x - 30 and i.y == self.y + 30 and i.alive:
adjacent_alive += 1
elif i.x == self.x + 30 and i.y == self.y - 30 and i.alive:
adjacent_alive += 1
elif i.x == self.x + 30 and i.y == self.y + 30 and i.alive:
adjacent_alive += 1
if self.alive:
if adjacent_alive < 2:
return False
elif adjacent_alive > 3:
return False
else:
return True
if not self.alive:
if adjacent_alive == 3:
return True
else:
return False
def render(self):
if self.alive:
pygame.draw.rect(wn, (0, 0, 0), (self.x, self.y, 30, 30))
# functions
def render_grid():
for y in range(0, HEIGHT, 30):
pygame.draw.line(wn, GRID_COLOUR, (0, y), (WIDTH, y))
for x in range(0, WIDTH, 30):
pygame.draw.line(wn, GRID_COLOUR, (x, 0), (x, HEIGHT))
def parse_to_x_y(x, y):
return x * 30, y * 30
def parse_to_index(x, y):
return int(x / 30), int(y / 30)
indexX = 0
indexY = 0
x_pos = 0
y_pos = 0
for y_ in range(18):
for x_ in range(32):
cells.append(Cell(x_pos, y_pos, False, indexX, indexY))
indexX += 1
x_pos += 30
y_pos += 30
x_pos = 0
indexY += 1
cells[2].live()
cells[35].live()
cells[65].live()
cells[66].live()
cells[67].live()
# main loop
fps = 1
clock = pygame.time.Clock()
while True:
# start_time = time.time()
wn.fill(BG_COLOUR)
for item in cells:
item.render()
render_grid()
# events loop
for event in pygame.event.get():
if event.type == pygame.QUIT:
quit()
for item in cells:
if item.update():
live_queue.append(item)
else:
die_queue.append(item)
for i in live_queue:
i.live()
for i in die_queue:
i.die()
pygame.display.update()
clock.tick(fps)
# end_time = time.time()
# print(round(1 / (end_time - start_time)), "fps")
The problem is that you don't reset your queues in the main loop.
So add this before adding to the queues:
live_queue = [] # <----
die_queue = [] # <----
for item in cells:
if item.update():
live_queue.append(item)
else:
die_queue.append(item)
Some other remarks
You never use grid or grid_temp in a useful way. Even the operations you make on them are strange. Any way, you can just remove all references to them.
You never use the indexX or indexY attributes, nor the method around it, nor the corresponding arguments to the constructor. All that can go.
You should avoid scanning all the cells just to find the (up to) 8 neighbors of one cell: this has a bad impact on performance.
I agree with commenter Rabbid76, in that you need to update the entire grid at once, not cell by cell. This is usually done by using two separate grids, an "previous state" grid and a "new state grid". Loop through each position in the "new state" grid and calculate its number of live neighbors using the "previous state" grid. After the entire "new state" grid is calculated, you can copy to "new state" grid to the "old state" grid.
Another fatal flaw in your algorithm is grid = [[False] * 32] * 18. This will not work as expected in Python. With this code, each row is a reference to the same array. For instance, the expression grid[0] is grid[1] will evaluate to True. If you set a certain cell in the grid to true, the entire column will be set to true. You can fix this via the code:
grid=[]
for r in range(18):
row=[]
for c in range(32):
row.append(False)
grid.append(row)
Though it isn't directly related to the bug, I suggest a bit of a redesign of your algorithm. It is not really needed to encapsulate each cell in a class; doing so creates a redundancy between the grid and list of cells. This also leads you to identify cells by their pixel position (hence the i.x == self.x - 30 clause), which can easily lead to bugs. I suggest checking adjacent indices in the grid variable instead. Try looking into https://www.geeksforgeeks.org/conways-game-life-python-implementation/ for some inspiration.
I've been migrating from Pygame to Arcade and overall it's much better. That said, the method I was using to draw the lines of my track for my car game in Pygame is exorbitantly laggy in Arcade. I know it's lagging from drawing all the lines, I'm just wondering if there's a better way to do it that doesn't cause so much lag.
import arcade
import os
import math
import numpy as np
SPRITE_SCALING = 0.5
SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
SCREEN_TITLE = "Move Sprite by Angle Example"
MOVEMENT_SPEED = 5
ANGLE_SPEED = 5
class Player(arcade.Sprite):
""" Player class """
def __init__(self, image, scale):
""" Set up the player """
# Call the parent init
super().__init__(image, scale)
# Create a variable to hold our speed. 'angle' is created by the parent
self.speed = 0
def update(self):
# Convert angle in degrees to radians.
angle_rad = math.radians(self.angle)
# Rotate the ship
self.angle += self.change_angle
# Use math to find our change based on our speed and angle
self.center_x += -self.speed * math.sin(angle_rad)
self.center_y += self.speed * math.cos(angle_rad)
def upgraded_distance_check(player_sprite, point_arrays, distance_cap):
center_coord = player_sprite.center
intersect_array = []
distance_array = []
sensor_array = player_sprite.points
for sensor in sensor_array:
intersect_array.append([-10000, -10000])
for point_array in point_arrays:
for i in range(len(point_array[:-1])):
v = line_intersection(
[sensor, center_coord], [point_array[i], point_array[i + 1]]
)
if v == (None, None):
continue
if (
(point_array[i][0] <= v[0] and point_array[i + 1][0] >= v[0])
or (point_array[i][0] >= v[0] and point_array[i + 1][0] <= v[0])
) and (
(point_array[i][1] <= v[1] and point_array[i + 1][1] >= v[1])
or (point_array[i][1] >= v[1] and point_array[i + 1][1] <= v[1])
):
if intersect_array[-1] is None or math.sqrt(
(intersect_array[-1][0] - center_coord[0]) ** 2
+ (intersect_array[-1][1] - center_coord[1]) ** 2
) > math.sqrt(
(v[0] - center_coord[0]) ** 2
+ (v[1] - center_coord[1]) ** 2
):
if not is_between(sensor, center_coord, v):
intersect_array[-1] = v
for i in range(len(sensor_array)):
if distance(sensor_array[i], intersect_array[i]) > distance_cap:
intersect_array[i] = None
distance_array.append(None)
else:
distance_array.append(distance(sensor_array[i], intersect_array[i]))
return intersect_array
class MyGame(arcade.Window):
"""
Main application class.
"""
def __init__(self, width, height, title):
"""
Initializer
"""
# Call the parent class initializer
super().__init__(width, height, title)
# Set the working directory (where we expect to find files) to the same
# directory this .py file is in. You can leave this out of your own
# code, but it is needed to easily run the examples using "python -m"
# as mentioned at the top of this program.
file_path = os.path.dirname(os.path.abspath(__file__))
os.chdir(file_path)
# Variables that will hold sprite lists
self.player_list = None
# Set up the player info
self.player_sprite = None
# Set the background color
arcade.set_background_color(arcade.color.WHITE)
def setup(self):
""" Set up the game and initialize the variables. """
# Sprite lists
self.player_list = arcade.SpriteList()
# Set up the player
self.player_sprite = Player(":resources:images/space_shooter/playerShip1_orange.png", SPRITE_SCALING)
self.player_sprite.center_x = SCREEN_WIDTH / 2
self.player_sprite.center_y = SCREEN_HEIGHT / 2
self.player_list.append(self.player_sprite)
#Setup all the array BS
self.track_arrays = []
self.drawing = False
def on_draw(self):
"""
Render the screen.
"""
# This command has to happen before we start drawing
arcade.start_render()
# Draw all the sprites.
# for i, ele in enumerate(self.player_sprite.points):
# arcade.draw_circle_filled(ele[0], ele[1], 7, arcade.color.AO)
self.player_list.draw()
if len(self.track_arrays) > 0 and len(self.track_arrays[0]) > 2:
for track_array in self.track_arrays:
for i in range(len(track_array[:-1])):
arcade.draw_line(track_array[i][0], track_array[i][1], track_array[i+1][0], track_array[i+1][1], arcade.color.BLACK, 1)
def on_update(self, delta_time):
""" Movement and game logic """
# Call update on all sprites (The sprites don't do much in this
# example though.)
self.player_list.update()
# print(self.drawing)
# print(self.player_sprite.points)
def on_key_press(self, key, modifiers):
"""Called whenever a key is pressed. """
# Forward/back
if key == arcade.key.UP:
self.player_sprite.speed = MOVEMENT_SPEED
elif key == arcade.key.DOWN:
self.player_sprite.speed = -MOVEMENT_SPEED
# Rotate left/right
elif key == arcade.key.LEFT:
self.player_sprite.change_angle = ANGLE_SPEED
elif key == arcade.key.RIGHT:
self.player_sprite.change_angle = -ANGLE_SPEED
def on_key_release(self, key, modifiers):
"""Called when the user releases a key. """
if key == arcade.key.UP or key == arcade.key.DOWN:
self.player_sprite.speed = 0
elif key == arcade.key.LEFT or key == arcade.key.RIGHT:
self.player_sprite.change_angle = 0
def on_mouse_press(self, x, y, button, modifiers):
"""
Called when the user presses a mouse button.
"""
self.track_arrays.append([])
self.drawing = True
def on_mouse_release(self, x, y, button, modifiers):
"""
Called when a user releases a mouse button.
"""
self.drawing = False
def on_mouse_motion(self, x, y, dx, dy):
""" Called to update our objects. Happens approximately 60 times per second."""
if self.drawing:
self.track_arrays[-1].append([x,y])
def main():
""" Main method """
window = MyGame(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)
window.setup()
arcade.run()
if __name__ == "__main__":
main()
I have a gravity vector (in the form [r, theta]) which I add to my ball's velocity vector. For some reason, the ball doesn't return to the same height after bouncing, but instead slowly loses height sporadically. I am guessing there's some rounding error or something in a calculation I'm using, but I can't isolate the issue.
Here is my code. You need both files and pygame to run it. Sorry if it's a little confusing. I can comment anything some more if you want.
I added a marker whenever the ball reaches its max height so you guys what I mean. I want the ball to return to exactly the same height every time it bounces.
I took a little bit of unnecessary code out. The full program is under the pastebin links.
https://pastebin.com/FyejMCmg - PhysicsSim
import pygame, sys, math, tools, random, time
from pygame.locals import *
clock = pygame.time.Clock()
lines = []
class Particle:
def __init__(self,screen,colour, mass, loc, vel):
self.screen = screen
self.colour = colour
self.mass = mass
self.x = loc[0]
self.y = loc[1]
self.location = self.x,self.y
self.speed = vel[0]
self.angle = vel[1]
def update(self):
global lines
# add gravity
self.speed,self.angle = tools.add_vectors2([self.speed,self.angle], tools.GRAVITY)
# update position
dt = clock.tick(60)
self.x += self.speed * tools.SCALE * math.cos(self.angle) * dt
self.y -= self.speed * tools.SCALE * math.sin(self.angle) * dt
self.location = int(self.x),int(self.y)
# border checking
do = False
n=[]
if ((self.y+self.mass) > tools.SCREEN_HEIGHT):
self.y = tools.SCREEN_HEIGHT-self.mass
n = [0,1]
do = True
# adds position to array so max height so max height can be recorded
if (self.speed==0):
lines.append([self.screen, self.location, self.mass])
# bounce
if do:
#init, convert everything to cartesian
v = tools.polarToCartesian([self.speed, self.angle])
#final -> initial minus twice the projection onto n, where n is the normal to the surface
a = tools.scalarP(2*abs(tools.dotP(v,n)),n) #vector to be added to v
v = tools.add_vectors(v,a)
self.angle = tools.cartesianToPolar(v)[1] # does not set magnitude
# drawing
pygame.draw.circle(self.screen, self.colour, self.location, self.mass, 0)
# draws max height line
def draw_line(l):
screen = l[0]
location = l[1]
radius = l[2]
pygame.draw.line(screen, tools.BLACK, [location[0] + 15, location[1]-radius],[location[0] - 15, location[1]-radius])
def main():
pygame.init()
DISPLAY = pygame.display.set_mode(tools.SCREEN_SIZE,0,32)
DISPLAY.fill(tools.WHITE)
particles = []
particles.append(Particle(DISPLAY, tools.GREEN, 10, [100,100], [0,0]))
done = False
while not done:
global lines
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
sys.exit()
DISPLAY.fill(tools.WHITE)
for i in particles:
i.update()
for l in lines:
draw_line(l)
pygame.display.update()
main()
https://pastebin.com/Epgqka31 - tools
import math
#colours
WHITE = (255, 255, 255)
BLUE = ( 0, 0, 255)
GREEN = ( 0, 255, 0)
RED = ( 255, 0, 0)
BLACK = ( 0, 0, 0)
COLOURS = [WHITE,BLUE,GREEN,RED,BLACK]
#screen
SCREEN_SIZE = SCREEN_WIDTH,SCREEN_HEIGHT = 1000,700
#vectors
GRAVITY = [5.0, 3*math.pi/2] # not 9.8 because it seems too high
SCALE = 0.01
# converts polar coordinates to cartesian coordinates in R2
def polarToCartesian(v):
return [v[0]*math.cos(v[1]), v[0]*math.sin(v[1])]
# converts cartesian coordinates to polar coordinates in R2
def cartesianToPolar(v):
return [math.sqrt(v[0]**2 + v[1]**2), math.atan2(v[1],v[0])]
# dots two cartesian vectors in R2
def dotP(v1, v2):
return v1[0]*v2[0] + v1[1]*v2[1]
# multiplies cartesian vector v by scalar s in Rn
def scalarP(s,v):
v_=[]
for i in v:
v_.append(s*i)
return v_
# returns the sum of two cartesian vectors in R2
def add_vectors(v1, v2):
return [v1[0]+v2[0], v1[1]+v2[1]]
# returns the sum of two polar vectors in R2, equations from https://math.stackexchange.com/questions/1365622/adding-two-polar-vectors
def add_vectors2(v1,v2):
r1,r2,t1,t2 = v1[0],v2[0],v1[1],v2[1]
return [math.sqrt(r1**2 + r2**2 + 2*r1*r2*math.cos(t2-t1)), t1 + math.atan2(r2*math.sin(t2 - t1), r1 + r2*math.cos(t2 - t1))]
Your time interval, dt = clock.tick(60), is not a constant. If you change it to dt = 60 your program runs as expected.
Have a look a the Verlet Algorithm and implement it in your code. You are on the right track!
I'm trying to create a program that displays a large grid of numbers (say, filling up a 6 by 4000 grid), where the user can move a cursor around via keyboard or mouse and enter in numbers into the grid. (This is for a guitar tablature program.) I'm new to python GUI programming, and thus far my idea is to have a very large QWidget window (say, 1000x80000 pixels) inside of a QScrollArea inside of the main window. The problem is that every mouse click or cursor movement causes the whole thing to repaint, causing a delay, when I just want to repaint whatever changes I just made to make things faster. In PyQt, is there a way to buffer already-painted graphics and change just the graphics that need changing?
edit: I've posted the code below, which I've run with python3.3 on Mac OS 10.7. The main point is that in the TabWindow init function, the grid size can be set by numXGrid and numYGrid (currently set to 200 and 6), and this grid is filled with random numbers by the generateRandomTablatureData() method. If the grid is filled with numbers, then there's a noticeable lag with every key press, which gets worse with larger grids. (There is also an initial delay due to generating the data, but my question is on the delay after each key press which I assume is due to having to repaint every number.)
There are two files. This is the main one, which I called FAIT.py:
import time
start_time = time.time()
import random
import sys
from PyQt4 import QtGui, QtCore
import Tracks
# generate tracks
tracks = [Tracks.Track(), Tracks.Track(), Tracks.Track()]
fontSize = 16
# margins
xMar = 50
yMar = 50
trackMar = 50 # margin between tracks
class MainWindow(QtGui.QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.initUI()
end_time = time.time()
print("Initializing time was %g seconds" % (end_time - start_time))
def initUI(self):
# attach QScrollArea to MainWindow
l = QtGui.QVBoxLayout(self)
l.setContentsMargins(0,0,0,0)
l.setSpacing(0)
s=QtGui.QScrollArea()
l.addWidget(s)
# attach TabWindow to QScrollArea so we can paint on it
self.tabWindow=TabWindow(self)
self.tabWindow.setFocusPolicy(QtCore.Qt.StrongFocus)
self.setFocusPolicy(QtCore.Qt.NoFocus)
vbox=QtGui.QVBoxLayout(self.tabWindow)
s.setWidget(self.tabWindow)
self.positionWindow() # set size and position of main window
self.setWindowTitle('MainWindow')
self.show()
def positionWindow(self):
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
width = QtGui.QDesktopWidget().availableGeometry().width() - 100
height = QtGui.QDesktopWidget().availableGeometry().height() - 100
self.resize(width, height)
qr = self.frameGeometry()
cp = QtGui.QDesktopWidget().availableGeometry().center()
qr.moveCenter(cp)
self.move(qr.topLeft())
def keyPressEvent(self, e):
print('key pressed in MainWindow')
def mousePressEvent(self, e):
print('mouse click in MainWindow')
class TabWindow(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent)
# size of tablature grid
numXGrid = 200
numYGrid = 6
# initialize tablature information first
for i in range(0, len(tracks)):
tracks[i].numXGrid = numXGrid
self.arrangeTracks() # figure out offsets for each track
self.trackFocusNum = 0 # to begin with, focus is on track 0
self.windowSizeX = tracks[0].x0 + tracks[0].dx*(tracks[0].numXGrid+2)
self.windowSizeY = tracks[0].y0
for i in range(0, len(tracks)):
self.windowSizeY = self.windowSizeY + tracks[i].dy * tracks[i].numYGrid + trackMar
self.resize(self.windowSizeX,self.windowSizeY) # size of actual tablature area
# generate random tablature data for testing
self.generateRandomTablatureData()
def keyPressEvent(self, e):
print('key pressed in TabWindow')
i = self.trackFocusNum
if e.key() == QtCore.Qt.Key_Up:
tracks[i].moveCursorUp()
if e.key() == QtCore.Qt.Key_Down:
tracks[i].moveCursorDown()
if e.key() == QtCore.Qt.Key_Left:
tracks[i].moveCursorLeft()
if e.key() == QtCore.Qt.Key_Right:
tracks[i].moveCursorRight()
# check for number input
numberKeys = (QtCore.Qt.Key_0,
QtCore.Qt.Key_1,
QtCore.Qt.Key_2,
QtCore.Qt.Key_3,
QtCore.Qt.Key_4,
QtCore.Qt.Key_5,
QtCore.Qt.Key_6,
QtCore.Qt.Key_7,
QtCore.Qt.Key_8,
QtCore.Qt.Key_9)
if e.key() in numberKeys:
num = int(e.key())-48
# add data
tracks[i].data.addToTab(tracks[i].iCursor, tracks[i].jCursor, num)
# convert entered number to pitch and play note (do later)
# spacebar, backspace, or delete remove data
if e.key() in (QtCore.Qt.Key_Space, QtCore.Qt.Key_Backspace, QtCore.Qt.Key_Delete):
tracks[i].data.removeFromTab(tracks[i].iCursor, tracks[i].jCursor)
self.update()
def mousePressEvent(self, e):
print('mouse click in TabWindow')
xPos = e.x()
yPos = e.y()
# check tracks one by one
for i in range(0, len(tracks)):
if (tracks[i].isPositionInside(xPos, yPos)):
tracks[i].moveCursorToPosition(xPos, yPos)
self.trackFocusNum = i
break
else:
continue
self.update()
def paintEvent(self, e):
qp = QtGui.QPainter()
qp.begin(self)
qp.setPen(QtCore.Qt.black)
qp.setBrush(QtCore.Qt.white)
qp.drawRect(0, 0, self.windowSizeX, self.windowSizeY)
self.paintTracks(qp)
self.paintTunings(qp)
self.paintCursor(qp)
self.paintNumbers(qp)
qp.end()
def paintTracks(self, qp):
qp.setPen(QtCore.Qt.black)
qp.setBrush(QtCore.Qt.white)
for i in range(0, len(tracks)):
qp.drawPolyline(tracks[i].polyline)
def paintCursor(self, qp):
i = self.trackFocusNum
qp.setPen(QtCore.Qt.black)
qp.setBrush(QtCore.Qt.black)
qp.drawPolygon(tracks[i].getCursorQPolygon())
def paintNumbers(self, qp):
# iterate through tracks, and iterate through numbers on each track
for i in range(0, len(tracks)):
# make sure track has data to draw
if len(tracks[i].data.data) > 0:
for j in range(0, len(tracks[i].data.data)):
# do actual painting here
# first set color to be inverse cursor color if at cursor
if i == self.trackFocusNum and \
tracks[i].iCursor == tracks[i].data.data[j][0] and \
tracks[i].jCursor == tracks[i].data.data[j][1]:
qp.setPen(QtCore.Qt.white)
else:
qp.setPen(QtCore.Qt.black)
font = QtGui.QFont('Helvetica', fontSize)
qp.setFont(font)
text = str(tracks[i].data.data[j][2])
x1 = tracks[i].convertIndexToPositionX(tracks[i].data.data[j][0])
y1 = tracks[i].convertIndexToPositionY(tracks[i].data.data[j][1])
dx = tracks[i].dx
dy = tracks[i].dy
# height and width of number character(s)
metrics = QtGui.QFontMetrics(font)
tx = metrics.width(text)
ty = metrics.height()
# formula for alignment:
# xMar = (dx-tx)/2 plus offset
x11 = x1 + (dx-tx)/2
y11 = y1 + dy/2+ty/2
qp.drawText(x11, y11, text)
def paintTunings(self, qp):
qp.setPen(QtCore.Qt.black)
font = QtGui.QFont('Helvetica', fontSize)
qp.setFont(font)
for i in range(0, len(tracks)):
for j in range(0, tracks[i].numStrings):
text = tracks[i].convertPitchToLetter(tracks[i].stringTuning[j])
# height and width of characters
metrics = QtGui.QFontMetrics(font)
tx = metrics.width(text)
ty = metrics.height()
x11 = tracks[i].x0 - tx - 10
y11 = tracks[i].convertIndexToPositionY(j) + (tracks[i].dy+ty)/2
qp.drawText(x11, y11, text)
def arrangeTracks(self):
tracks[0].x0 = xMar
tracks[0].y0 = yMar
tracks[0].generateGridQPolyline()
for i in range(1, len(tracks)):
tracks[i].x0 = xMar
tracks[i].y0 = tracks[i-1].y0 + tracks[i-1].height + trackMar
tracks[i].generateGridQPolyline()
def generateRandomTablatureData(self):
t1 = time.time()
for i in range(0, len(tracks)):
# worst case scenario: fill every number
for jx in range(0, tracks[i].numXGrid):
for jy in range(0, tracks[i].numYGrid):
val = random.randint(0,9)
tracks[i].data.addToTab(jx, jy, val)
t2 = time.time()
print("Random number generating time was %g seconds" % (t2 - t1))
def main():
app = QtGui.QApplication(sys.argv)
ex = MainWindow()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
This is the other file, Tracks.py, which contains the Track class and supplementary methods:
# contains classes and methods relating to individual tracks
import math
from PyQt4 import QtGui, QtCore
# class for containing information about a track
class Track:
def __init__(self):
self.data = TabulatureData()
# position offset
self.x0 = 0
self.y0 = 0
self.dx = 20 # default rectangle width
self.dy = 40 # default rectangle height
# current cursor index coordinates
self.iCursor = 0
self.jCursor = 0
# default size of grid
self.numXGrid = 4000
self.numYGrid = 6
self.numStrings = self.numYGrid
# calculated maximum width and height in pixels
self.maxWidth = self.dx * self.numXGrid
self.maxHeight = self.dy * self.numYGrid
# generate points of grid and cursor
self.generateGridQPolyline()
# tuning
self.setTuning([40, 45, 50, 55, 59, 64])
# calculate bounds
self.height = self.numYGrid*self.dy
self.width = self.numXGrid*self.dx
def getCursorIndexX(self, xPos):
iPos = int(math.floor( (xPos-self.x0)/self.dx ))
return iPos
def getCursorIndexY(self, yPos):
jPos = int(math.floor( (yPos-self.y0)/self.dy ))
return jPos
def convertIndexToCoordinates(self, iPos, jPos):
return [self.ConvertIndexToPositionX(iPos),
self.ConvertIndexToPositionY(jPos)]
def convertIndexToPositionX(self, iPos):
return self.x0 + iPos*self.dx
def convertIndexToPositionY(self, jPos):
return self.y0 + jPos*self.dy
def getCursorQPolygon(self):
x1 = self.convertIndexToPositionX(self.iCursor)
y1 = self.convertIndexToPositionY(self.jCursor)
x2 = self.convertIndexToPositionX(self.iCursor+1)
y2 = self.convertIndexToPositionY(self.jCursor+1)
return QtGui.QPolygonF([QtCore.QPoint(x1, y1),
QtCore.QPoint(x1, y2),
QtCore.QPoint(x2, y2),
QtCore.QPoint(x2, y1)])
def generateGridQPolyline(self):
self.points = []
self.polyline = QtGui.QPolygonF()
for i in range(0, self.numXGrid):
for j in range(0, self.numYGrid):
x1 = self.convertIndexToPositionX(i)
y1 = self.convertIndexToPositionY(j)
x2 = self.convertIndexToPositionX(i+1)
y2 = self.convertIndexToPositionY(j+1)
self.points.append([(x1, y1), (x1, y2), (x2, y2), (x2, y1)])
self.polyline << QtCore.QPoint(x1,y1) << \
QtCore.QPoint(x1,y2) << \
QtCore.QPoint(x2,y2) << \
QtCore.QPoint(x2,y1) << \
QtCore.QPoint(x1,y1)
# smoothly connect different rows
jLast = self.numYGrid-1
x1 = self.convertIndexToPositionX(i)
y1 = self.convertIndexToPositionY(jLast)
x2 = self.convertIndexToPositionX(i+1)
y2 = self.convertIndexToPositionY(jLast+1)
self.polyline << QtCore.QPoint(x2,y1)
def isPositionInside(self, xPos, yPos):
if (xPos >= self.x0 and xPos <= self.x0 + self.width and
yPos >= self.y0 and yPos <= self.y0 + self.height):
return True
else:
return False
def moveCursorToPosition(self, xPos, yPos):
self.iCursor = self.getCursorIndexX(xPos)
self.jCursor = self.getCursorIndexY(yPos)
self.moveCursorToIndex(self.iCursor, self.jCursor)
def moveCursorToIndex(self, iPos, jPos):
# check if bounds are breached, and if cursor's already there,
# and if not, move cursor
if iPos == self.iCursor and jPos == self.jCursor:
return
if iPos >= 0 and iPos < self.numXGrid:
if jPos >= 0 and jPos < self.numYGrid:
self.iCursor = iPos
self.jCursor = jPos
return
def moveCursorUp(self):
self.moveCursorToIndex(self.iCursor, self.jCursor-1)
def moveCursorDown(self):
self.moveCursorToIndex(self.iCursor, self.jCursor+1)
def moveCursorLeft(self):
self.moveCursorToIndex(self.iCursor-1, self.jCursor)
def moveCursorRight(self):
self.moveCursorToIndex(self.iCursor+1, self.jCursor)
# return pitch in midi integer notation
def convertNumberToPitch(self, jPos, pitchNum):
return pitchNum + self.stringTuning[(self.numStrings-1) - jPos]
def convertPitchToLetter(self, pitchNum):
p = pitchNum % 12
letters = ('C', 'Db', 'D', 'Eb', 'E', 'F', 'Gb', 'G', 'Ab', 'A', 'Bb', 'B')
return letters[p]
def setTuning(self, tuning):
self.stringTuning = tuning
class TabulatureData:
def __init__(self):
self.data = []
def addToTab(self, i, j, value):
# check if data is already there, and remove it first
if self.getValue(i, j) > -1:
self.removeFromTab(i, j)
self.data.append([i, j, value])
def getValue(self, i, j):
possibleTuples = [x for x in self.data if x[0] == i and x[1] == j]
if possibleTuples == []:
return -1
elif len(possibleTuples) > 1:
print('Warning: more than one number at a location!')
return possibleTuples[0][2] # return third number of tuple
def removeFromTab(self, i, j):
# first get value, if it exists
value = self.getValue(i,j)
if value == -1:
return
else:
# if it exists, then remove
self.data.remove([i, j, value])
1000*80000 is really huge.
So,maybe you should try QGLWidget or something like that?
Or according to Qt document, you should set which region you want to repaint.
some slow widgets need to optimize by painting only the requested region: QPaintEvent::region(). This speed optimization does not change the result, as painting is clipped to that region during event processing. QListView and QTableView do this, for example.
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