Giter Site home page Giter Site logo

hex-grid's Introduction

HexGrid

Swift 5.6+ Swift Package Manager Compatible MIT License Documentation

Development in progress!

API might change without further notice until first major release 1.x.x.

The HexGrid library provides an easy and intuitive way of working with hexagonal grids. Under the hood it handles all the math so you can focus on more important stuff.

  • Support for grids of any shape, including irregular shapes, or grids with holes in them.

The library is meant for generic backend use. Therefore it doesn't perform any UI or rendering. However, it provides the calculations that will be needed for rendering.

Features

  • Create or generate a grid of hexagonal cells.
  • Various coordinate systems (cube, axial, offset), and conversion between them.
  • Rotation, Manhattan distance, linear interpolation.
  • Get Neighbors or diagonal neighbors.
  • Get Line (get all hexes making a line from A to B).
  • Get Ring (get all hexes making a ring from an origin coordinates in specified radius).
  • Get Filled Ring (get all hexes making a filled ring from an origin coordinates in specified radius).
  • Find reachable hexes within n steps (Breath First Search).
  • Find the shortest path from A to B (optimized A* search algorithm).
  • FieldOfView algorithm (ShadowCasting designed for hexagonal grids).
  • Hexagon rendering related functions (e.g. get polygon corners).
  • Code inline documentation (quick help).
  • Solid unit tests coverage.
  • Automated documentation generator (SwiftDoc + GitHub Actions -> hosted on repo GitHub Pages).
  • Demo with visualization.

What's coming next?

  • We are done for the moment. Any feature requests or ideas are welcome.

HexGrid in action

HexGrid demo app using SpriteKit. (Also available in the App Store.) HexGrid demo app using SwiftUI.

Getting Started

Integrating HexGrid to your project

Add HexGrid as a dependency to your Package.swift file.

import PackageDescription

let package = Package(
name: "MyApp",
dependencies: [
...
// Add HexGrid package here
.package(url: "https://github.com/fananek/hex-grid.git", from: "0.4.11")
],
...
targets: [
        .target(name: "App", dependencies: [
            .product(name: "HexGrid", package: "hex-grid"),
            ...

Import HexGrid package to your code.

import HexGrid
...
// your code goes here

Using

Creating a grid

Grids can be initialized either with a set of cell coordinates, or HexGrid can generate some standard shaped grids for you.

Standard shape grids

For example:

...
// create grid of hexagonal shape
var grid = HexGrid(shape: GridShape.hexagon(10))
// or rectangular shape
var grid = HexGrid(shape: GridShape.rectangle(8, 12))
// or triangular shape
var grid = HexGrid(shape: GridShape.triangle(6))

See the section on GridShape below for more details, and a full list of the available grid shapes.

Custom grids

Example:

...
// create new HexGrid

let gridCells: Set<Cell> = try [
Cell(CubeCoordinates(x:  2,  y: -2,  z:  0)),
Cell(CubeCoordinates(x:  0,  y: -1,  z:  1)),
Cell(CubeCoordinates(x: -1,  y:  1,  z:  0)),
Cell(CubeCoordinates(x:  0,  y:  2,  z: -2))
]
var grid = HexGrid(cells: gridCells)
...

Initializers and drawing

Note that, assuming you want to draw your grid, you'll want to think about whether to pass a size for each cell (hexSize) or a size for the entire Grid (pixelSize) to the initializer. (See "Drawing the Grid" below.)

HexGrid <-> JSON

HexGrid conforms to swift Codable protocol so it can be easily encoded to or decoded from JSON.

Example:

// encode (grid to JSON)
let grid = HexGrid(shape: GridShape.hexagon(5) )
let encoder = JSONEncoder()
let data = try encoder.encode(grid)
// decode (JSON to grid)
let decoder = JSONDecoder()
let grid = try decoder.decode(HexGrid.self, from: data)

Grid operations examples

Almost all functions have two variants. One that works with Cell and the other one works with CubeCoordinates. Use those which better fulfill your needs.

Get Cell at coordinates

let cell = grid.cellAt(try CubeCoordinates(x: 1, y: 0, z: -1))

Validate coordinates

Check whether a coordinate is valid (meaning it has a corresponding Cell in the grid's cells array).

// returns Bool
isValidCoordinates(try CubeCoordinates(x: 2, y: 4, z: -6))

Get blocked or non blocked Cells

let blockedCells = grid.blockedCells()
// or
let nonBlockedCells = grid.nonBlockedCells()

Get single neighbor

// get neighbor for a specific Cell
let neighbor = try grid.neighbor(
            for: someCell,
            at: Direction.Pointy.northEast.rawValue)

// get just neighbor coordinates
let neighborCoordinates = try grid.neighborCoordinates(
            for: someCoordinates,
            at: Direction.Pointy.northEast.rawValue)

Get all neighbors

// get all neighbors for a specific Cell
let neighbors = try grid.neighbors(for: someCell)

// get only coordinates of all neighbors
let neighborsCoords = try grid.neighbors(for: someCoordinates)

Get line from A to B

// returns nil in case line doesn't exist
let line = try grid.line(from: originCell, to: targetCell)

Get ring

// returns all cells making a ring from origin cell in radius
let ring = try grid.ring(from: originCell, in: 2)

Get filled ring

// returns all cells making a filled ring from origin cell in radius
let ring = try grid.filledRing(from: originCell, in: 2)

Find reachable cells

// find all reachable cells (max. 4 steps away from origin)
let reachableCells = try grid.findReachable(from: origin, in: 4)

Find shortest path

// returns nil in case path doesn't exist at all
let path = try grid.findPath(from: originCell, to: targetCell)

Calculate field of view (FOV)

Cell has an attribute called isOpaque. Its value can be true or false. Based on this information it's possible to calculate so called field of view. It means all cells visible from specific position on grid, considering all opaque obstacles.

// set cell as opaque
obstacleCell.isOpaque = true 

In order to get field of view, simply call following function.

// find all hexes visible in radius 4 from origin cell
let visibleHexes = try grid.fieldOfView(from: originCell, in: 4)

By default cell is considered visible as soon as its center is visible from the origin cell. If you want to include partially visible cells as well, use optional paramter includePartiallyVisible.

// find all hexes even partially visible in radius 4 from origin cell
let visibleHexesIncludingPartials = try grid.fieldOfView(from: originCell, in: 4, includePartiallyVisible: true)

Drawing the Grid

Internally, HexGrid calculates all "pixel coordinates" using one of two methods:

  1. Using a size for each Cell (stored in the hexSize property).

...or...

  1. Using a size for the entire Grid (stored in the pixelSize property).

Note that both the hexSize and pixelSize properties are stored internally as HexSize structures. Try not to get confused by this! HexSize is just a convenient way to store width and height values.

Which flavor of HexGrid initializer you want to use will depend on which of these methods best applies to your use case. When specifying the hexSize, the pixelSize is calculated for you, and when specifying the pixelSize, the hexSize is likewise set automatically.

While it is not possible to modify the hexSize or pixelSize properties directly (after initialization), you can set the grid's pixelSize (and re-calculate hexSize from it) at any time using the fitGrid(in size: HexSize) function. Note that this also resets the origin property.

The origin property

You can think of the HexGrid's' origin property as the center point of the Cell at CubeCoordinate 0,0,0.

Note that you can specify the origin at initialization, but only when using the cellSize method. When specifying pixelSize, the origin is set for you, so the grid "fits" inside the specified width & height.

It will be important to change the origin property any time you want to change the pixel coordinates for your Grid. Changing the origin will modify the return values of all pixel-calculating functions. You can use this to apply an offset for your grid, or "re-center" it later.

Corner Pixel Coordinates

Usually, when drawing hexagons, you will want the screen coordinates of the polygon corners for each Cell.

let corners = grid.polygonCorners(for: someCell)

Center Pixel Coordinates

This function returns a Point struct (x: and y: values) for the center of a Cell.

let screenCoords = grid.pixelCoordinates(for: someCell)

Finding a Cell at screen coordinates

// return cell for specified screen coordinates (or nil if such cell doesn't exists)
let cell = try grid.cellAt(point)

Implementation fundamentals

For detailed information see complete documentation

Data structures you should know

HexGrid

Represents the grid itself as well as it's an entry point of the HexGrid library.

HexGrid is defined by set of Cells and few other properties. All together makes a grid setup. In other words it put grid cells into a meaningful context. Therefore most of available operations are being called directly on a grid instance because it make sense only with such context (grid setup).

Properties:

  • cells: Set<Cell> - grid cells
  • orientation: Orientation - see Orientation enumeration
  • offsetLayout: OffsetLayout - see OffsetLayout enumeration
  • hexSize: HexSize - width and height of a hexagon
  • origin: Point - 'display coordinates' (x, y) of a grid origin
  • pixelSize: HexSize - pixel width and height of the entire grid
  • attributes: [String: Attribute] - dictionary of custom attributes (most primitive types are supported as well as nesting)

Cell

Cell is a building block of a grid.

Properties:

  • coordinates: CubeCoordinates - cell placement on a grid coordinate system
  • attributes: [String: Attribute] - dictionary of custom attributes (most primitive types are supported as well as nesting)
  • isBlocked: Bool - used by algorithms (reachableCells, pathfinding etc.)
  • isOpaque: Bool - used by fieldOfView algorithm
  • cost: Float - used by pathfinding algorithm. For the sake of simplicity let's put graph theory aside. You can imagine cost as an amount of energy needed to pass a cell. Pathfinding algorithm then search for path requiring the less effort.

CubeCoordinates

The most common coordinates used within HexGrid library is cube coordinate system. This type of coordinates has three axis x, y and z. The only condition is that sum of its all values has to be equal zero.

// valid cube coordinates
CubeCoordinates(x: 1, y: 0, z: -1) -> sum = 0

// invalid cube coordinates
CubeCoordinates(x: 1, y: 1, z: -1) -> sum = 1 -> throws error

For more details check Amit Patel's explanation.

Enumerations

Orientation

Options:

  • pointyOnTop - ⬢
  • flatOnTop - ⬣
OffsetLayout

OffsetLayout is used primarily for rectangular shaped grids. It has two options but their meaning can differ based on grid orientation.

Options:

  • odd
  • even

There are four offset types depending on orientation of hexagons. The “row” types are used with with pointy top hexagons and the “column” types are used with flat top.

  • Pointy on top orientation
    • odd-row (slide alternate rows right)
    • even-row (slide alternate rows left)
  • Flat on top orientation
    • odd-column (slide alternate columns up)
    • even-column (slide alternate columns down)
GridShape

Cases from this enumeration can be passed into the HexGrid constructor to generate grids of various shapes and sizes.

Options:

  • rectangle(Int, Int) - Associated values are column width and row height of the generated grid. Sides of the grid will be essentially parallel to the edges of the screen.
  • parallelogram(Int, Int) - Associated values are both side-lengths.
  • hexagon(Int) - This generates a regular hexagon shaped grid, with all sides the length of the associated value.
  • elongatedHexagon(Int, Int) - This is used to generate a grid shaped like a regular hexagon that has been stretched or elongated in one dimension. The associated values are side lengths.
  • irregularHexagon(Int, Int) - This is used to generate a grid shaped like a hexagon where every other side is the length of the two associated values.
  • triangle(Int) - Generates an equilateral triangle with all sides the length of the associated value.
Rotation

Options:

  • left
  • right
Direction

Direction enumeration is consistent and human recognizable direction naming. Using direction enumeration is not only much more convenient but it also help to avoid errors. It's better to say just "Hey, I'm on cell X and want to go north." than think "What the hack was that index of north direction?", isn't it?

Options:

  • Direction.Flat
    • north
    • northEast
    • southEast
    • south
    • southWest
    • northWest
  • Direction.Pointy
    • northEast
    • east
    • southEast
    • southWest
    • west
    • northWest

There is actually separate set of directions for each grid orientation. It's because of two reasons. First, some directions are valid only for one or another orientation. Second, direction raw values are shifted based on orientation.

Authors

See also the list of contributors who participated in this project.

License

All code contained in the HexGrid package is under the MIT license agreement.

hex-grid's People

Contributors

fananek avatar mgrider avatar pmckeown avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

hex-grid's Issues

Intent to flush out drawing instructions in the README

I was just looking at #12 from last year, and realized I never addressed your request to add some details to the README. This issue is just to state and/or discuss my intention to retroactively do that.

I will write some details about that function, as well as how drawing is impacted by some of the the other properties and initializers, mostly likely putting it all under the "Drawing related functions" section. (I might also drop "function" from that header.)

issues with rectangular generator

Describe the bug
As mentioned in #12, when generating cells using the .rectangle GridShape, the resulting grid appears rotated from what is expected in 3 out of 4 possible combinations of Orientation and OffsetLayout.

To Reproduce
It is probably easiest to reproduce this behavior by drawing/visualizing the resulting grid. Here are some screenshots generated using the SKHexGrid project. The coordinates shown are Axial.

Orientation: .pointyOnTop, OffsetLayout: .even
Screen Shot 2022-08-10 at 3 15 10 PM

Orientation: .pointyOnTop, OffsetLayout: .odd
Screen Shot 2022-08-10 at 3 17 55 PM

Orientation: .flatOnTop, OffsetLayout: .even
Screen Shot 2022-08-10 at 3 18 36 PM

Orientation: .flatOnTop, OffsetLayout: .odd
Screen Shot 2022-08-10 at 3 19 06 PM

Expected behavior
You can see from the screenshots that only the .flatOnTop, .even layout appears to be correct.

Finetune coordinates converter and grid generator

Description
Double check coordinate conversions works as expected. Especially focus on Offset, Axial and Cube coordinate systems.

  • Fix any potential issue.
  • In case of any change in converter, grid generators will probably need update as well.
  • Update unit tests accordingly.

Issue relates to #13

HexSize width & height are not public

Similar to #2, it would be nice if we could get at the width & height properties in HexSize. (I'd like to be able to create a HexSize+CGSize extension similar to the one in the example for Point+CGPoint.)

`Generator.createHexagonGrid` doc comment / functionality missmatch

The comment for the radius parameter says 0 will produce a single hexagon, but a value of 0 actually throws an InvalidArgumentError.

I also think the doc comments for GridShape cases could use expanding, maybe with some examples. For instance, the .hexagon case just says "radius", but I think it's more common to talk about regular hex grids by "side length" (which is of course "radius + 1"). At least I understand the meaning of radius, but I don't really understand what the .triangle's "side size" parameter means, because it seems to produce a triangle with a side-size of one more than that. (For consistency?) Maybe the triangle parameter is actually a bug, I don't really know.

Feature request: Store user data in cells

It would be nice to store some user info along with each cell so that I don't need to store a hash of additional cell info outside of HexGrid since I am already using HexGrid to find the cell I want based on its pixel or cube coordinates.

Something as simple as the ubiquitous public var userInfo: [AnyHashable: Any] would do the trick.

Feature Request: an initializer with a single size for the entire HexGrid

Problem
When drawing a grid, it would be nice to "fit" the grid inside a specific CGSize.

Ideal solution
Something like:

let hexGrid = HexGrid(shape: GridShape.hexagon(2), gridSize: CGSize(width: 200, height: 200))

Describe alternatives you've considered
Currently, if I want to "fit" the grid to a size, I need to do the math to figure out how many across and divide the total width by that number, do the same for height, and then I take the smaller number and use that. The new initializer could do something similar, but I'm not even sure how to do that for all the possible grid types, so it might also be nice to have functions that return the max number of hexes horizontally, or vertically, and possibly get the hex "farthest" in a given direction, maybe?

Feature request: Caching

Library Performance

I just want to get a discussion started here around the topic. For my use (a 2-player abstract strategy game with an AI opponent), I need to call some functions on this library hundreds (maybe thousands) of times per second. In general, most of these calls are re-creating their return values every time, even though they are always the same as long as the set of Cell objects hasn't changed. For those functions, the return value can be cached to gain massive speed-up, which I would like the library to do.

Some cache examples

allCellsCoordinates()

This function was taking up the second most time in my project, and currently looks like this:

    public func allCellsCoordinates() -> Set<CubeCoordinates> {
        return Set(self.cells.map { $0.coordinates })
    }

My initial instinct was to use a standard for in loop to improve the speed, like this:

    public func allCellsCoordinates() -> Set<CubeCoordinates> {
        var  set = Set<CubeCoordinates>()
        for cell in cells {
            set.insert(cell.coordinates)
        }
        return set
    }

I wrote the following tests:

    func testAllCellCoordinatesMap() throws {
        let grid = HexGrid(
            shape: GridShape.hexagon(20))
        measure() {
            for _ in 0...10000 {
                _ = grid.allCellsCoordinatesMap()
            }
        }
    }

    func testAllCellCoordinatesUncached() throws {
        let grid = HexGrid(
            shape: GridShape.hexagon(20))
        measure() {
            for _ in 0...10000 {
                _ = grid.allCellsCoordinatesUncached()
            }
        }
    }

The original (.map version) runs in 2.522 sec, and the second almost twice as fast at 1.803 sec. Oddly enough, when I search for whether .map is slow in swift, the consensus in various articles seems to be that it's actually faster than a standard for in loop, so I'm not sure exactly what's happening here. It occurred to me shortly after the re-write that I should use a cache variable for this value (as I'd already done for the primary bottleneck, which I'll describe next).

That function now looks like this:

    /// A cache variable for all coordinates in all cells.
    private var cacheAllCellsCoordinates = Set<CubeCoordinates>()

    /// Coordinates of all available grid cells
    /// - Returns: `Set<CubeCoordinates>`
    public func allCellsCoordinates() -> Set<CubeCoordinates> {
        if !cacheAllCellsCoordinates.isEmpty {
            return cacheAllCellsCoordinates
        } else {
            var  set = Set<CubeCoordinates>()
            for cell in cells {
                set.insert(cell.coordinates)
            }
            cacheAllCellsCoordinates = set
            return set
        }
    }

With a similar test to the ones above, (running it 10,000 times), it takes 0.003 sec, and is no longer a noticeable function when running in the profiler.

neighborsCoordinates(for coordinates:)

As I said, this function was the primary bottleneck in my AI code (after a bit of refactoring to use coordinates everywhere instead of cells--which I needed to do anyway). Here's how it looked originally:

    public func neighborsCoordinates(for coordinates: CubeCoordinates) throws -> Set<CubeCoordinates> {
        return try Math.neighbors(for: coordinates).filter { isValidCoordinates($0) }
    }

I decided to implement a cache solution right away, and that looks like this:

    /// A cache/LUT variable. The thinking here is, unless our `Cell` set changes, the neighbors are always going to be the same.
    private var cacheNeighborsCoordinatesForCoordinates = [CubeCoordinates: Set<CubeCoordinates>]()

    /// Get all available neighbor coordinates for specified coordinates
    /// - Parameter coordinates: `CubeCoordinates`
    /// - Returns: `Set<CubeCoordinates>`
    /// - Throws: `InvalidArgumentsError` in case underlying cube coordinates initializer propagate the error.
    public func neighborsCoordinates(for coordinates: CubeCoordinates) throws -> Set<CubeCoordinates> {
        if let neighbors = cacheNeighborsCoordinatesForCoordinates[coordinates] {
            return neighbors
        } else {
            let neighbors = try Math.neighbors(for: coordinates).filter { isValidCoordinates($0) }
            cacheNeighborsCoordinatesForCoordinates[coordinates] = neighbors
            return neighbors
        }
    }

I also added a new function to invalidate the cache variables that gets called after updatePixelDimensions in the cells didSet:

    /// This gets called whenever the `Cell` set is updated.
    private func invalidateCacheVariables() {
        cacheAllCellsCoordinates = Set<CubeCoordinates>()
        cacheNeighborsCoordinatesForCoordinates = [CubeCoordinates: Set<CubeCoordinates>]()
    }

I wrote some tests for this as well, you can see them on my fork. I've commented out the original test, because even running the loop (calling it on all the cell coordinates in a hex20 grid 10 times), takes longer than I want to wait. (More than a few minutes.)

alternatives / discussion

I can imagine some legitimate reasons you might object to these sorts of changes in the library, so I wanted to outline the changes I've already made before I tackle any others. (For my purposes, it may already be "fast enough", but some additional testing will be needed.) I do feel like, if some function return values are cached, maybe as many as can be should be. That's the main reason I'm creating this issue. Should I go ahead and write cache variables for the others where it makes sense?

If you aren't interested in this type of improvement, then I will definitely not bother until I need it. (And in that case, I'd probably just maintain my own fork of the library for my own use.

Hex to screen wrong implementation

Firstly, thanks for the great work!

I came across this library recently and tried to use it to render a hex grid, but the result was not correct, the hexagons cells were overlapping.

Going back and forth between the doc from redblobgames.com and your source I noticed there is maybe a mix up between the layout size and the hexSize used for calculating hexCornerOffset.

The original doc says : "Note that size.x and size.y are not the width and height of the hexagons."
https://www.redblobgames.com/grids/hexagons/implementation.html#hex-to-pixel

Unfortunately, that's exactly what happens in hexCornerOffset, the given hexSize is used as is, so somehow each hexagon gets it's width doubled.

I fixed the rendering of my hexGridView class by updating hexCornerOffset to :

fileprivate static func hexCornerOffset(at index: Int, hexSize: HexSize, orientation: Orientation) -> Point {
let angle = 2.0 * Double.pi * (
OrientationMatrix(orientation: orientation).startAngle + Double(index)) / 6
return Point(x: hexSize.width/2 * cos(angle), y: hexSize.height/2 * sin(angle))
}

Where hexSize.width and hexSize.height are simply divided by two.

Thanks again.
Take care.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.