A Basic Grid Object
What’s the point of formalizing something so basic? Judicious laziness, as always. Anything that makes for less typing and/or less thinking is a bonus in my world. Here’s a grid and some accessories that help on both fronts.
The Overload2D protocol
Overload2D
is a set of arithmetic operations on ordered pairs, for example, CGPoint
. With Overload2D
you can add and subtract a CGPoint
to another (or CGSize
or CGVector
, or indeed, any ordered pair that conforms to the Overload2D
protocol). You can multiply and divide by scalars or by ordered pairs. There are various utility functions and variables available, such as hypoteneuse
and area
for size-like pairs, and (r, θ) representation for vector- and point-like pairs.
KGPoint
, KGSize
, KGVector
Basic ordered pairs: (x, y), (width, height), and (dx, dy), respectively. Analagous to Swift’s CGPoint/Size/Vector
, but with Int
rather than CGFloat as the data type for the pair elements. This results in each cell having a unique, whole-number address that will never be affected by rounding when used in calculations. These all conform to Overload2D
, of course.
Sensor Rings
In the handful of “Snake” AI genetic algorithm implementations I’ve looked at, the snake can “sense” only the cells adjacent to its head. In Arkonia, my gremlins’ sensory ranges vary based on genetics and health.
In the upper diagram, consider a snake with its head sitting in the center, at the cell marked ‘0’. The usual snake’s sensor range is one “ring” – it reaches out to all the adjacent cells, 1-9 (actually, it seems that most of the snakes can see only three adjacent cells; an evolutionary dead-end if I ever saw one). This grid implementation enables linear iteration through the surrounding cells in these squarish “sensor rings”.
Asteroidization
Each element in the sense ring contains two important landmarks:
- The real grid position, representing the wrap-around point where you need to put your gremlin if your world is a wrap-around world.
- The virtual grid position, representing a point off the grid where the gremlin would go if it were to continue beyond the boundary without wrapping.
The primary purpose of the virtual position is to allow your game to determine the distance the gremlin will be moving, in case there’s a cost for moving, for example. In the lower diagram, consider the purple cell to be where the gremlin is currently sitting. When requesting cells by sense ring, you will get back the real positions as shown here in yellow, and the virtual positions as shown in gray.
Of course in most of the Snake AI implementations I’ve seen, the edges are walls; there’s no warping across space, the snake just dies under the cruel talons of natural selection. Sadists and snake-haters need not worry; there is an isOnGrid()
function for them.
The Details
Using Overload2D and KGPoint/Size/Vector
There aren’t any surprises here. You can add and subtract 2D objects, you can multiply and divide 2D objects with scalars. Size objects have an area
variable, vectors have an analagous magnitude
variable. Have a quick scan of Overload2D.swift
to see what’s available.
Asteroidization
struct AsteroidPoint {
let realCell: GridCell
let relativeVirtualPosition: KGPoint
}
For use with sensory functions, ie, functions that read cell contents via an index relative to a center
- Properties:
- realCell: the cell at the real grid location corresponding to the center+localIx. This is where you will want to place your gremlin to make it wrap to the other edge of the grid
- relativeVirtualPosition: a position (not a cell) that’s not on the grid, where the gremlin would go if the grid were actually to extend far enough
Cell Addressing Functions
cellAt(_ position: KGPoint) -> GridCell
Gets the cell at the indicated absolute position on the grid
Parameter: position
The cell’s position on the grid. The point (0, 0) is in the center of the grid, with y increasing upward and x increasing to the right
Returns:
The indicated cell
cellAt(_ localIndex: Int, from center: KGPoint) -> GridCell
Gets the cell at the “ring index” relative to the indicated cell
Parameters:
- localIx: The index to offset from the center
- center: The cell to use as the center; a typical use for this function is to enable your game gremlin to read information (“sensory” input) in the surrounding cells, out to any arbitrary distance
Returns:
The indicated cell
first(fromCenterAt centerCell: GridCell, cMaxCells: Int, where predicate: @escaping (AsteroidPoint) -> Bool) -> AsteroidPoint?
Find the first cell from among the cells surrounding the center that results in predicate(_: AsteroidPoint)
returning true
Parameters:
- centerCell: The cell from which to offset
- cMaxCells: The maximum number of cells to read for the search
- predicate: Your function for testing the characteristics of candidate cells.
Returns:
The first qualifying cell – nil
if no qualifying cell is found
Miscellany
isOnGrid(_ position: KGPoint) -> Bool
Indicates whether the specified position is on the grid.
makeIterator() -> IndexingIterator<[GridCell]>
For iterating over all the cells in the grid
Note: Although the index is an Int
, don’t try to use it
for calculating offsets or positions of cells. Instead, if you need
to know the position of a cell, grab the cell using the iterator then
get cell.properties.gridPosition