Problem
I have a scrolling game with 250+ enemy aliens and I want to implement basic crowd behaviour and maybe even Goal-Oriented Pathfinding.
For that I thought that implementing a tile system would be the best. The problem is that getting the location of each alien and using it to "block" the tile it is on, is way too slow with my implementation.
Can you help me with a better implementation or a good concept on how to achieve better performance?
Setup
I created a tilemap, which is an array, self.tiles, containing the tiles as dictionaries with each tile being a 16px rectangle. (each alien is around 16px wide).
In order to retrieve each tile, I have an array, self.chunks_location, containing all tile coordinates.
Each alien passes its (x,y) to the function below, converting its (x,y) to multipel of 16.
The converted (x,y) gets then looked up in self.chunks_location and the retrieved index is used to block the corresponding tile inside self.tiles.
Function: Convert (x,y)
(The function was originally intended to output multiple tile coordinates, if the alien is right between two tiles, so it looks a bit weird right now.)
def get_chunks_from_cord(self, x, y):
"""
Returns the chunk or chunks for the given location on the map, based on the tilesize of the map.
Being in between chunks leads to multiple chunks being returned.
(float or int) -> [tuple(int, int)]
(20, 58) -> [[16, 68]]
"""
"""
Adds the cords separate rounded to 16 to tilesize coordinates to the blocked list.
"""
decimal_16 = [x % self.chunksize, y % self.chunksize]
blocked = [[], []] # [[x values], [y values]]
for cnt, cord in enumerate((x, y)):
if decimal_16[cnt] >= self.chunksize_05:
blocked[cnt].append(cord + self.chunksize - decimal_16[cnt])
else:
blocked[cnt].append(cord - decimal_16[cnt])
result = []
for array in blocked[0]:
for var in blocked[1]:
result.append((round(array), round(var)))
return result
Function block tile
Block tiles based on the previously converted coordinates.
def block_chunks(self, array):
if len(array) == 1:
target = self.chunks_location.index(array[0])
self.chunks[target]["blocked"] = True
self.chunks_blocked.append(self.chunks[target])
else:
for cords in array:
target = self.chunks_location.index(cords)
self.chunks[target]["blocked"] = True
self.chunks_blocked.append(self.chunks[target])
Related
I am creating a charge smear function. I have a matrix were each row is a particle with a charge and position. I then look at each particles position in a grid, to count how many particles are in each grid-cell, but I need to know which cell each particle is in, so that I may find the average of the positions for every particle in a specific grid-cell. My idea for a fix is to create an list where the number of rows is the amount of grid-cells in my matrix, and let the column be positions in x,y and z direction, but obviously I can't append more then one number to each index, but maybe some variation will work? Sorry for open ended question. Thank you in advance
import matplotlib.pyplot as plt
import random
import numpy as np
###Initalize particle lists
particle_arrayX=[]
particle_arrayY=[]
###The resolution
N = 10
###Number of particles
M = 1000
col=None
row=None
###Size of box
Box_size=100
###gridsize
Grid_size=Box_size/N
###Initalize particles
for i in range(M):
particle_arrayX.append(random.random()*Box_size)
particle_arrayY.append(random.random()*Box_size)
###Intialize matrix
ParticleMatrix_Inital=[[0 for i in range(N)]]*N
###Measure density in each cell
for i in range(M):
col=None
row=None
#The x and y components are diveded by the gridsize
#Then they are converted to integers and then asigned either to a row or column
#If value is float with decimal 0 EX 2.0, then 1 is substracted before converted to int
coln=particle_arrayX[i]/Grid_size
rown=particle_arrayY[i]/Grid_size
if coln.is_integer()==True:
col=int(coln)-1
else:
col=int(coln)
if rown.is_integer()==True:
row=int(rown)-1
else:
row=int(rown)
ParticleMatrix_Inital=np.array(ParticleMatrix_Inital)
ParticleMatrix_Inital[row,col]=ParticleMatrix_Inital[row,col]+1
ParticleMatrix_Inital=ParticleMatrix_Inital.tolist()
#Plot matrix
plt.imshow(ParticleMatrix_Inital)
plt.colorbar()
plt.show()
Welcome to SO!
There are many ways to approach the problem of "bin-ing" empirical data. I'm proposing an object oriented (OO) solution below, because (in my subjective opinion) it provides clean, tidy and highly readable code. On the other hand, OO-solutions might not be the most efficient if you're simulating huge many-particles systems. If the below code doesn't entirely solve your issues, I still hope that parts of it can be of some help to you.
That being said, I propose implementing your grid as a class. To make life easier for our self, we may apply the convention that all particles have positive coordinates. That is x, y and even z (if introduced) stretches from 0 to whatever box_size you define. However, the class Grid does not really care about the actual box_size, only the resolution of the grid!
class Grid:
def __init__(self, _delta_x, _delta_y):
self.delta_x = _delta_x
self.delta_y = _delta_y
def bounding_cell(self, x, y):
# Note that y-coordinates corresponds to matrix rows,
# and that x-coordinates corresponds to matrix columns
return (int(y/self.delta_y), int(x/self.delta_x))
Yes, this could have been a simple function. However, as a class it is easily expandable. Also, a function would have rely on global variables (yuck!) or explicitly be given the grid spanning (delta) in each dimensional direction, for every determining of which matrix cell (or bin) the given coordinate (x,y) belongs to.
Now, how does it work? Imagine the simplest of cases, where your grid resolution is 1. Then, a particle at position (x,y) = (1.2, 4,9) should be placed in the matrix at (row,col) = (4,1). That is row = int(y/delta_y) and likewise for x. The higher resolution (smaller delta) you have, the larger the matrix gets in terms of number of rows and cols.
Now that we have a Grid, let us also object orient the Particle! Rather straight forward:
class Particle:
def __init__(self, _id, _charge, _pos_x, _pos_y):
self.id = _id
self.charge = _charge
self.pos_x = _pos_x
self.pos_y = _pos_y
def __str__(self):
# implementing the __str__ method let's us 'print(a_particle)'
return "{%d, %d, %3.1f, %3.1f}" % (self.id, self.charge, self.pos_x, self.pos_y)
def get_position(self):
return self.pos_x, self.pos_y
def get_charge(self):
return self.charge
This class is more or less just a collection of data, and could easily have been replaced by a dict. However, the class screams its intent clearly, it is clean and tidy, and also easily expanded.
Now, let's create some instances of particles! Here is a function which by list comprehension creates a list of particles with an id, charge and position (x,y):
import random
def create_n_particles(n_particles, max_pos):
return [Particle(id, # unique ID
random.randint(-1,1), # charge [-1, 0, 1]
random.random()*max_pos, # x coord
random.random()*max_pos) # y coord
for id in range(n_particles)]
And finally, we get to the fun part: putting it all together:
import numpy as np
if __name__ == "__main__":
n_particles = 1000
box_size = 100
grid_resolution = 10
grid_size = int(box_size / grid_resolution)
grid = Grid(grid_resolution, grid_resolution)
particles = create_n_particles(n_particles, box_size)
charge_matrix = np.zeros((grid_size, grid_size))
id_matrix = [[ [] for i in range(grid_size)] for j in range(grid_size)]
for particle in particles:
x, y = particle.get_position()
row, col = grid.bounding_cell(x, y)
charge_matrix[row][col] += particle.get_charge()
# The ID-matrix is similar to the charge-matrix,
# but every cell contains a new list of particle IDs
id_matrix[row][col].append(particle.id)
Notice the initialization of the ID-matrix: This is the list of particle positions for each grid cell that you asked for. It is a matrix, representing the particle container, and each cell contains a list to be filled with particle IDs. You could also populate these lists with entire particle instances (not just their IDs): id_matrix[row][col].append(particle).
The last for loop does the real work, and here the Object Oriented strategy shows us how charming it is: The loop is short and it is very easy to read and understand what is going on: A cell in the charge_matrix contains the total charge within this grid cell/bin. Meanwhile, the id_matrix is filled with the IDs of the particles that is contained within this grid cell/bin.
From the way we've constructed the list of particles, particles, we see that a particle's ID is equivalent to that particle's index in the list. Hence, they may be retrieved like this,
for i,row in enumerate(id_matrix):
for j,col in enumerate(row):
print("[%d][%d] : " % (i, j), end="")
for particle_id in id_matrix[i][j]:
p = particles[particle_id]
# do some work with 'p', or just print it:
print(p, end=", ")
print("") # print new line
# Output:
# [0][0] : {32, -1, 0.2, 0.4}, ... <-- all data of that particle
# ....
I leave optimization of this retrieval to you as I don't really know what data you need and what you're going to do with it. Maybe it's better to contain all the particles in a dict instead of a list; I don't know(?). You choose!
At the very end, I'll suggest that you use matshow which is inteded for displaying matrices, as opposed to imshow which is more aiming more for images.
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subplot(111)
cax = ax.matshow(charge_matrix)
fig.colorbar(cax)
ax.invert_yaxis() # flip the matrix such that the y-axis points upwards
fig.savefig("charge_matrix.png")
We can also scatter plot the particles and add grid lines corresponding to our the grid in the matshow above. We color the scatter plots such that negative charges are blue, neutral are gray and positive are red.
def charge_color(charge):
if charge > 0: return 'red'
elif charge < 0: return 'blue'
else: return 'gray'
fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_aspect('equal')
ax.set_xticks(np.arange(0, 101, grid_resolution))
ax.set_yticks(np.arange(0, 101, grid_resolution))
ax.grid()
ax.scatter([p.pos_x for p in particles],
[p.pos_y for p in particles],
c=[charge_color(p.get_charge()) for p in particles])
fig.savefig("particle_dist.png")
I'm finally a member of the stackoverflow community, so this is my first post. I'm trying my best to create a good question.
I have a problem with a simple variable declaration in a for-loop. The variable gets declared in an if-statement and is the variable of a class. Basically what the code does is to create a box with points in it, initialise the positions and velocities of the points, edit the velocities and calculate the new positions according to their new velocities. I now want to save the initial positions of the points (means time = 0) outside of the class in a for-loop. I do this by if time = 0 then save position to a variable, but the variable gets updated to the new position in every loop iteration. The actual code is about hydrodynamics and particle interaction, but the basic structure of the code is something like this:
import numpy as np
class box():
def __init__(self, boxsize, num_points, timestep):
"""
boxsize is the size of the quadratic box in x and y direction
num_points is the number of points in the box
timestep is the time after that the positions should be updated
"""
self.boxsize = float(boxsize)
self.num_points = int(num_points)
self.timestep = float(timestep)
self.positions = np.zeros((self.num_points, 2)).astype(float)
self.velocities = np.zeros((self.num_points, 2)).astype(float)
def initialise(self):
"""initialise the positions and velocites of the points in the box, both with x- and y-components"""
self.positions[:, :] = np.random.uniform(0., self.boxsize, size=(self.num_points, 2))
self.velocities[:, :] = np.random.uniform(0., 1., size=(self.num_points, 2))
def update_positions(self):
"""update position according to velocities, x- and y-components"""
self.positions += self.velocities*self.timestep
def new_velocities(self):
""" create new velocities, x- and y-components """
self.velocities[:, :] = np.random.uniform(0., 1., size=(self.num_points, 2))
def connect_steps(self):
"""update the positions according to their velocities and create new velocities"""
self.update_positions()
self.new_velocities()
system = box(10., 1, 0.1) #box is 10 in x and y; 1 point in the box
system.initialise() #initialise the positions and velocities of the box
for i in range(10): #10 timesteps
system.connect_steps()
if i == 0.:
r0 = system.positions
print(r0) #here r0 should always be the same array from i = 0 but isn't
print(r0 == system.positions) #yields True every iteration, so r0 is always the new position
What I want is that r0 is always the position at i = 0 (initial position), but every iteration the variable r0 gets updated according to its new position, although the if-clause and so the variable definition only gets entered once at i = 0.
It is intended to first update the positions and after that generate new velocities although they are first used in the next iteration because the real algorithm behind this velocity-generation needs the structure this way.
Maybe there is just a characteristic or property of classes I don't know.
I hope the question makes sense and anybody can help me out.
Thanks a lot!
You might try np.copy(system.positions) to get a copy that won't continue to mutate as you update.
Reference https://numpy.org/doc/stable/reference/generated/numpy.copy.html
I want to get a list of indices (row,col) for all raster cells that fall within or are intersected by a polygon feature. Looking for a solution in python, ideally with gdal/ogr modules.
Other posts have suggested rasterizing the polygon, but I would rather have direct access to the cell indices if possible.
Since you don't provide a working example, it's bit unclear what your starting point is. I made a dataset with 1 polygon, if you have a dataset with multiple but only want to target a specific polygon you can add SQLStatement or where to the gdal.Rasterize call.
Sample polygon
geojson = """{"type":"FeatureCollection",
"name":"test",
"crs":{"type":"name","properties":{"name":"urn:ogc:def:crs:OGC:1.3:CRS84"}},
"features":[
{"type":"Feature","properties":{},"geometry":{"type":"MultiPolygon","coordinates":[[[[-110.254,44.915],[-114.176,37.644],[-105.729,36.41],[-105.05,43.318],[-110.254,44.915]]]]}}
]}"""
Rasterizing
Rasterizing can be done with gdal.Rasterize. You need to specify the properties of the target grid. If there is no predefined grid these could be extracted from the polygon itself
ds = gdal.Rasterize('/vsimem/tmpfile', geojson, xRes=1, yRes=-1, allTouched=True,
outputBounds=[-120, 30, -100, 50], burnValues=1,
outputType=gdal.GDT_Byte)
mask = ds.ReadAsArray()
ds = None
gdal.Unlink('/vsimem/tmpfile')
Converting to indices
Retrieving the indices from the rasterized polygon can be done with Numpy:
y_ind, x_ind = np.where(mask==1)
Clearly Rutger's solution above is the way to go with this, however I will leave my solution up. I developed a script that accomplished what I needed with the following:
Get the bounding box for each vector feature I want to check
Use the bounding box to limit the computational window (determine what portion of the raster could potentially have intersections)
Iterate over the cells within this part of the raster and construct a polygon geometry for each cell
Use ogr.Geometry.Intersects() to check if the cell intersects with the polygon feature
Note that I have only defined the methods, but I think implementation should be pretty clear -- just call match_cells with the appropriate arguments (ogr.Geometry object and geotransform matrix). Code below:
from osgeo import ogr
# Convert projected coordinates to raster cell indices
def parse_coords(x,y,gt):
row,col = None,None
if x:
col = int((x - gt[0]) // gt[1])
# If only x coordinate is provided, return column index
if not y:
return col
if y:
row = int((y - gt[3]) // gt[5])
# If only x coordinate is provided, return column index
if not x:
return row
return (row,col)
# Construct polygon geometry from raster cell
def build_cell((row,col),gt):
xres,yres = gt[1],gt[5]
x_0,y_0 = gt[0],gt[3]
top = (yres*row) + y_0
bottom = (yres*(row+1)) + y_0
right = (xres*col) + x_0
left = (xres*(col+1)) + x_0
# Create ring topology
ring = ogr.Geometry(ogr.wkbLinearRing)
ring.AddPoint(left,bottom)
ring.AddPoint(right,bottom)
ring.AddPoint(right,top)
ring.AddPoint(left,top)
ring.AddPoint(left,bottom)
# Create polygon
box = ogr.Geometry(ogr.wkbPolygon)
box.AddGeometry(ring)
return box
# Iterate over feature geometries & check for intersection
def match_cells(inputGeometry,gt):
matched_cells = []
for f,feature in enumerate(inputGeometry):
geom = feature.GetGeometryRef()
bbox = geom.GetEnvelope()
xmin,xmax = [parse_coords(x,None,gt) for x in bbox[:2]]
ymin,ymax = [parse_coords(None,y,gt) for y in bbox[2:]]
for cell_row in range(ymax,ymin+1):
for cell_col in range(xmin,xmax+1):
cell_box = build_cell((cell_row,cell_col),gt)
if cell_box.Intersects(geom):
matched_cells += [[(cell_row,cell_col)]]
return matched_cells
if you want to do this manually you'll need to test each cell for:
Square v Polygon intersection and
Square v Line intersection.
If you treat each square as a 2d point this becomes easier - it's now a Point v Polygon problem. Check in Game Dev forums for collision algorithms.
Good luck!
I'm making a game that involves a map of square tiles (like Civ III). Each square in the map is an object with x,y coordinates and some other attributes (resources on the tile, elevation, etc).
My map file is stored as an array, like so:
tilemap = [
[GRASS, DIRT, DIRT],
[GRASS, GRASS, WATER],
[GRASS, GRASS, WATER],
[DIRT, GRASS, WATER],
[DIRT, GRASS, GRASS]
]
I want to go through the map and make a square at the x,y coordinates given by the location in the array, and with the type indicated by the text (grass, dirt, etc).
So far, I've tried this:
class LevelMap():
def __init__(self, level_array=tilemap):
self.level_array = level_array
# Go through every row in the map...
for row in range(MAPHEIGHT):
# ... and every column...
for column in range(MAPWIDTH):
# ...and make a square at that location.
Square(row, column, tilemap[row][column])
The Square class takes the arguments Square(x,y,square_type).
However, I do not know how to give each square its own unique name, so I can say
square_1 = the square at 0,0
square_2 = the square at 0,1
and so on, so that I can use e.g. square1.get_square_type() or square_1.change_terrain_type(rocky).
How can I turn my map array into a group of Square objects with unique names?
I'm using Python 3.4 and pygame.
You don't.
You said already that you have your map stored as a 2D list. You only reference your tiles by their index in that array.
class LevelMap():
def __init__(self, level_array=tilemap):
self.level_array = level_array
# Go through every row in the map...
for row in range(MAPHEIGHT):
# ... and every column...
for column in range(MAPWIDTH):
# ...and make a square at that location.
Square(row, column, tilemap[row][column])
map = LevelMap(level_array=[])
# change the tile at (0, 1) to rocky:
map.level_array[1][0].change_terrain_to("rocky")
I'd even strongly recommend writing a helper function onto LevelMap that gets a tile given its (x,y).
class LevelMap():
...
def get_tile(self, x, y):
return self.level_array[y][x]
map = LevelMap()
tile = map.get_tile(0, 1)
# or
location = (0, 1)
tile = map.get_tile(*location)
When I've written things like this in the past, I've made my Map a child of list
class Map(list):
def __init__(self, height, width):
for y in range(height):
self.append([Square(y, x, tilemap[y][x]) for x in range(width)])
def get_tile(self, x, y):
return self[y][x]
The goal here is to take some list of coordinates, like [[1,2],[3,4],[7,1]], then figure out how big the canvas should be if you want to print all these coords. Take the maximal bottom left coordinate and minimal upper right coordinate that will snugly fit a canvas to these points. In the above list, for example, we're looking for [[1,1],[7,4]], which defines the smallest rectangle where all those points will fit.
In the middle of this function, I'm seeing the incoming "board" assigned a new value.
def print_board(board):
# import pdb; pdb.set_trace()
dimensions = None
for i in board:
if dimensions == None:
dimensions = [i, i]
else:
dimensions[0][0] = min(dimensions[0][0], i[0])
#'board' is redefined !!!
dimensions[0][1] = min(dimensions[0][1], i[1])
#dimensions[1][0] = max(dimensions[1][0], i[0])
#dimensions[1][1] = max(dimensions[1][1], i[1])
# (after we get the canvas size
# we print the canvas with the points on it
# but we never make it that far without an error)
As the for loop moves through the coordinates in the incoming board, it seems to be setting board[0] to whatever coordinate it's looking at at the time. So [[1,2],[3,4],[7,1]] will change first to [[3,4],[3,4],[7,1]], then to [[7,1],[3,4],[7,1]].
I wouldn't expect board to change at all.
(Python 3.2.2)
When you do
dimensions = [i, i]
you're setting both items in dimensions to the first point in your board -- not making copies of that point.
Then when you do
dimensions[0][0] = min(dimensions[0][0], i[0])
dimensions[0][1] = min(dimensions[0][1], i[1])
you're updating that same point --- the first point in your board -- to the results of the min functions.
Try something like this, instead:
def print_board(board):
xs, ys = zip(*board) # separate out the x and y coordinates
min_x, max_x = min(xs), max(xs) # find the mins and maxs
min_y, max_y = min(ys), max(ys)
dimensions = [[min_x, min_y], [max_x, max_y]] # make the dimensions array
As an extension of agfs answer, you can use numpy for even more efficient and succinct code:
import numpy as np
def print_board(board):
a = np.array(board)
return [a.min(axis=0).tolist(), a.max(axis=0).tolist()]
If your board is a numpy array already, and you let the function return a tuple of numpy arrays, it shortens even more:
def print_board(board):
return board.min(axis=0), board.max(axis=0)