Flow fields
Now that we're more familiar with the concept and use of cellular automata and grid systems, it's time to stretch our applications using rules that apply to lower levels or organization to generate more interesting applications. A flow field is a representation of how a particle or fluid moves throughout a given space, and it is often used in games of Tower Defense to direct the enemies to a single exit.
We can use the concept of counting neighbors to generate paths, where every tile has a vector, or line,
attached to it that represents the force and the direction in which any object that lands on that tile
will move. The purpose of the algorithm is to calculate a path that connects, whenever possible, any
tile in the grid with a destination tile, which represents the "exit" of the maze.
Amit Patel's webpage contains a
more comprehensive, interactive explanation on the concept of Flow field
generation using diverse algorithms that I encourage you to look at. This tutorial will cover the
Breadth First Search algorithm.
The parameters of the Algorithm
- MAP: [str,str,str, ...] - a collection of 0 (wall) and 1 (path), casted as strings, representing the map
- RES: int - Size of each tile
- SCREEN: (int,int) - Dimensions of the screen given as a width and height in pixels
- VECTOR_LENGTH: int - Direction and force of a vector represented within the tile
- TILE: Tile() - Basic unit of information in the program
- TILEMAP: [[TILE, ..., n=DIMS[0]], ..., n=DIMS[1]] - 2D array of TILE
- TARGET_TILE: TILE - A starting TILE in the grid considered the destination point for the path
- STACK: [TILE, ...] - A collection of TILE, sorted by proximity to TARGET_TILE
# MODULES import pygame # DD MAP = [ "101011", "111010", "100010", "111110", "100010", ] DIMS = (len(MAP[0]), len(MAP)) RES = 64 SCREEN = (DIMS[0]*RES, DIMS[1]*RES) display = pygame.display.set_mode(SCREEN) VECTOR_LENGTH = RES//3 # DD. TILE # tile = Tile() # interp. the basic unit of information that makes the map class Tile: def __init__(self, c, r, isWall): self.c = c self.r = r self.x = self.c*RES self.y = self.r*RES self.rect = pygame.Rect(self.x, self.y, RES, RES) self.isWall = False if isWall == "1" else True self.color = "green" if self.isWall else "white" self.direction = [1, 0, 0, 0] # R, D, L, U self.center = (self.x + RES//2, self.y + RES//2) self.visited = False def draw(self): pygame.draw.rect(display, self.color, self.rect) def drawVector(self): pygame.draw.line(display, "red", self.center, self.getVectorEndPoint()) def getVectorEndPoint(self): if self.isWall: return self.center # RIGHT if bool(self.direction[0]): return (self.center[0]+VECTOR_LENGTH, self.center[1]) # DOWN if bool(self.direction[1]): return (self.center[0], self.center[1]+VECTOR_LENGTH) # LEFT if bool(self.direction[2]): return (self.center[0]-VECTOR_LENGTH, self.center[1]) # UP else: return (self.center[0], self.center[1]-VECTOR_LENGTH) # DD. TILEMAP # tileMap = [[TILE, ..., n = DIMS[0]], ..., n = DIMS[1]] # interp. the map of tiles that represent the map tileMap = [] for r in range(DIMS[1]): tileRow = [] for c in range(DIMS[0]): tile = Tile(c, r, MAP[r][c]) tileRow.append(tile) tileMap.append(tileRow) # TEMPLATE FOR TILEMAP # for tileRow in tileMap: # for tile in tileRow: # ... tile def fn_for_tileMap(fn, *args): for tileRow in tileMap: for tile in tileRow: fn(tile, *args) # DD. TARGET_TILE # tt = tileMap[int][int] # interp. the tile on which objects placed in the map have to move tt = tileMap[0][0] tt.color = "orange" tt.visited = True # DD. STACK # stack = [TILE, ...] # interp. the order in which the tiles will be visited relative to the search algorithm stack = [tt]
The Algorithm
- Are there elements in the Stack?:
- Pick the first TILE in the STACK
- Add neighbors of this TILE to STACK: RIGHT, DOWN, LEFT, UP, if neighbor tile exists, hasn't been visited and is not a wall
- Set neighbor TILE to visited
- Update the neighbor direction and calculate the updated direction of the red line representing the vector
- Erase first TILE from STACK