{"id":16218129,"url":"https://github.com/quasilyte/pathing","last_synced_at":"2025-03-16T11:30:59.869Z","repository":{"id":194974112,"uuid":"691906995","full_name":"quasilyte/pathing","owner":"quasilyte","description":"A very fast \u0026 zero-allocation, grid-based, pathfinding library for Go.","archived":false,"fork":false,"pushed_at":"2023-10-12T08:17:43.000Z","size":166,"stargazers_count":116,"open_issues_count":0,"forks_count":4,"subscribers_count":4,"default_branch":"master","last_synced_at":"2025-02-27T08:19:59.197Z","etag":null,"topics":["a-star","ebiten","ebitengine","game-development","gamedev","go","golang","greedy-bfs","library","pathfinding","performance"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/quasilyte.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-09-15T06:30:06.000Z","updated_at":"2024-12-25T13:22:31.000Z","dependencies_parsed_at":"2023-10-12T17:05:18.022Z","dependency_job_id":null,"html_url":"https://github.com/quasilyte/pathing","commit_stats":null,"previous_names":["quasilyte/pathing"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quasilyte%2Fpathing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quasilyte%2Fpathing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quasilyte%2Fpathing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/quasilyte%2Fpathing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/quasilyte","download_url":"https://codeload.github.com/quasilyte/pathing/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243814864,"owners_count":20352037,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["a-star","ebiten","ebitengine","game-development","gamedev","go","golang","greedy-bfs","library","pathfinding","performance"],"created_at":"2024-10-10T11:48:37.375Z","updated_at":"2025-03-16T11:30:59.513Z","avatar_url":"https://github.com/quasilyte.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# quasilyte/pathing\n\n![Build Status](https://github.com/quasilyte/pathing/workflows/Go/badge.svg)\n[![PkgGoDev](https://pkg.go.dev/badge/mod/github.com/quasilyte/pathing)](https://pkg.go.dev/mod/github.com/quasilyte/pathing)\n\nA very fast \u0026 zero-allocation, grid-based, pathfinding library for Go.\n\n## Overview\n\nThis library has several things that make it much faster than the alternatives. Some of them are fundamental, and some of them are just a side-effect of the goal I was pursuing for myself.\n\nSome of the limitations you may want to know about before using this library:\n\n1. Its max path length per `BuildPath()` is limited\n2. Only 4 tile kinds per `Grid` are supported\n\nBoth of these limitations can be worked around:\n\n1. Connect the partial results to traverse a bigger map\n2. Use different \"layers\" for different biomes\n\nTo learn more about this library and its internals, see [this presentation](https://speakerdeck.com/quasilyte/zero-alloc-pathfinding).\n\nWhen to use this library?\n\n* You need a very fast pathfinding\n* You can live with the limitations listed above\n\nIf you answer \"yes\" to both, consider using this library.\n\nSome games that use this library:\n\n* [Roboden](https://store.steampowered.com/app/2416030/Roboden/)\n* [Cavebots](https://quasilyte.itch.io/cavebots)\n* [Assemblox](https://itch.io/jam/gmtk-2023/rate/2157747)\n\n## Quick Start\n\n```bash\n$ go get github.com/quasilyte/pathing\n```\n\nThis is a simplified example. See the [full example](example_detailed_test.go) if you want to learn more.\n\n```go\nfunc main() {\n\t// Grid is a \"map\" that stores cell info.\n\tconst cellSize = 40\n\tg := pathing.NewGrid(pathing.GridConfig{\n\t\t// A 5x4 map.\n\t\tWorldWidth:  5 * cellSize,\n\t\tWorldHeight: 4 * cellSize,\n\t\tCellWidth:   cellSize,\n\t\tCellHeight:  cellSize,\n\t})\n\n\t// We'll use Greedy BFS pathfinder (A* is also available).\n\tbfs := pathing.NewGreedyBFS(pathing.GreedyBFSConfig{})\n\n\t// Tile kinds are needed to interpret the cell values.\n\t// Let's define some.\n\tconst (\n\t\ttilePlain = iota\n\t\ttileForest\n\t\ttileMountain\n\t)\n\n\t// Grid map cells contain \"tile tags\"; these are basically\n\t// a tile enum values that should fit the 2 bits (max 4 tags per Grid).\n\t// The default tag is 0 (tilePlain).\n\t// Let's add some forests and mountains.\n\t//\n\t// The result map layout will look like this:\n\t// . . m . . | [m] - mountain\n\t// . . f . . | [f] - forest\n\t// . . f . . | [.] - plain\n\t// . . . . .\n\tg.SetCellTile(pathing.GridCoord{X: 2, Y: 0}, tileMountain)\n\tg.SetCellTile(pathing.GridCoord{X: 2, Y: 1}, tileForest)\n\tg.SetCellTile(pathing.GridCoord{X: 2, Y: 2}, tileForest)\n\n\t// Now we need to tell the pathfinding library how to interpret\n\t// these tiles. For instance, which tiles are passable and not.\n\t// We do that by using layers. I'll define two layers here\n\t// to show you how it's possible to interpret the grid differently\n\t// depending on the layer.\n\tnormalLayer := pathing.MakeGridLayer([4]uint8{\n\t\ttilePlain:    1, // passable\n\t\ttileMountain: 0, // not passable\n\t\ttileForest:   0, // not passable\n\t})\n\tflyingLayer := pathing.MakeGridLayer([4]uint8{\n\t\ttilePlain:    1,\n\t\ttileMountain: 1,\n\t\ttileForest:   1,\n\t})\n\n\t// Our map with markers will look like this:\n\t// . . m . . | [m] - mountain\n\t// . A f B . | [f] - forest\n\t// . . f . . | [.] - plain\n\t// . . . . . | [A] - start, [B] - finish\n\tstartPos := pathing.GridCoord{X: 1, Y: 1}\n\tfinishPos := pathing.GridCoord{X: 3, Y: 1}\n\n\t// Let's build a normal path first, for a non-flying unit.\n\tp := bfs.BuildPath(g, startPos, finishPos, normalLayer)\n\n\t// The path reads as: Down, Down, Right, Right, Up, Up.\n\tfmt.Println(p.Steps.String(), \"- normal layer path\")\n\n\t// You can iterate the path.\n\tfor p.Steps.HasNext() {\n\t\tfmt.Println(\"\u003e step:\", p.Steps.Next())\n\t}\n\n\t// A flying unit can go in a straight line.\n\tp = bfs.BuildPath(g, startPos, finishPos, flyingLayer)\n\tfmt.Println(p.Steps.String(), \"- flying layer path\")\n}\n```\n\nSome terminology hints:\n\n* Grid - a compact matrix that holds the \"tile tags\"\n* GridCoord - an `{X,Y}` object that addresses the grid cell\n* Pos - a world coordinate that can be mapped to a grid\n* Tile (or a tile tag) - a enum-like value that represents a tile kind\n* GridLayer - translates the tag into a pathing value cost (where 0 means \"blocked\")\n\nNote that it's possible to convert between the GridCoord and world positions via the `Grid` type API.\n\n## Greedy BFS paths quality\n\nThis library provides both greedy best-first search as well as A* algorithms.\n\nYou may be concerned about the Greedy BFS vs A* results. Due to a couple of tricks I used during the implementation, an unexpected thing happened: some of the paths are actually better than you would expect from a Greedy BFS.\n\n\u003ctable\u003e\n\t\u003ctr\u003e\n\t\t\u003ctd\u003eA* (21 steps)\u003c/td\u003e\n\t\t\u003ctd\u003eGreedy BFS (27 steps)\u003c/td\u003e\n\t\t\u003ctd\u003eThis library's BFS (21 steps)\u003c/td\u003e\n\t\u003ctr\u003e\n\t\t\u003ctd\u003e\n\t\t\t\u003cimg src=\"https://github.com/quasilyte/pathing/assets/6286655/ba657850-8321-4586-80bd-5e466fa3504c\"\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003e\n\t\t\t\u003cimg src=\"https://github.com/quasilyte/pathing/assets/6286655/bef9228a-2b0b-4f6d-a5a3-c676c96149e5\"\u003e\n\t\t\u003c/td\u003e\n\t\t\u003ctd\u003e\n\t\t\t\u003cimg src=\"https://github.com/quasilyte/pathing/assets/6286655/b1da357d-5a9c-40b2-a0d0-e8c6a4bbfdea\"\u003e\n\t\t\u003c/td\u003e\n\t\u003c/tr\u003e\n\u003c/table\u003e\n\nThis library worked well enough for me even without A*. You still may want to use A* if you need to have different movement costs for tiles.\n\nIn general, A* always build an optimal path and can handle cost-based pathfinding. Greedy BFS requires less memory and works faster.\n\n## Benchmarks \u0026 Performance\n\nSee [_bench](_bench) folder to reproduce the results.\n\n```bash\n# If you're using Linux+Intel processor, consider doing this\n# to reduce the noise and make your results more stable:\n$ echo \"1\" | sudo tee /sys/devices/system/cpu/intel_pstate/no_turbo\n\n# Running and analyzing the benchmarks:\n$ cd _bench\n$ go test -bench=. -benchmem -count=10 | results.txt\n$ benchstat results.txt\n```\n\nTime - **ns/op**:\n\n| Library | no_wall | simple_wall | multi_wall |\n|---|---|---|---|\n| quasilyte/pathing BFS | 3525 | 6353 | 16927 |\n| quasilyte/pathing A* | 20140 | 35846 | 44756 |\n| fzipp/astar | 948367 | 1554290 | 1842812 |\n| beefsack/go-astar | 453939 | 939300 | 1032581 |\n| kelindar/tile | 107632 ns | 169613 ns | 182342 ns |\n| s0rg/grid | 1816039 | 1154117 | 1189989 |\n| SolarLune/paths | 6588751 | 5158604 | 6114856 |\n\nAllocations - **allocs/op**:\n\n| Library | no_wall | simple_wall | multi_wall |\n|---|---|---|---|\n| quasilyte/pathing BFS | 0 | 0 | 0 |\n| quasilyte/pathing A* | 0 | 0 | 0 |\n| fzipp/astar | 2008 | 3677 | 3600 |\n| beefsack/go-astar | 529 | 1347 | 1557 |\n| kelindar/tile | 3 | 3 | 3 |\n| s0rg/grid | 2976 | 1900 | 1759 |\n| SolarLune/paths | 7199 | 6368 | 7001 |\n\nAllocations -  **bytes/op**:\n\n| Library | no_wall | simple_wall | multi_wall |\n|---|---|---|---|\n| quasilyte/pathing BFS | 0 | 0 | 0 |\n| quasilyte/pathing A* | 0 | 0 | 0 |\n| fzipp/astar | 337336 | 511908 | 722690 |\n| beefsack/go-astar | 43653 | 93122 | 130731 |\n| tile | 123118 | 32950 | 65763 |\n| s0rg/grid | 996889 | 551976 | 740523 |\n| SolarLune/paths | 235168 | 194768 | 230416 |\n\nI hope that my contribution to this lineup will increase the competition, so we get better Go gamedev libraries in the future.\n\nSome of my findings that can make these libraries faster:\n\n* Never use `container/heap`; use a generic non-interface version\n* Better yet, try a bucket priority queue instead of minheap\n* Do not use `map`, prefer something that allows a memory re-use\n* The [sparse-dense](https://research.swtch.com/sparse) is a good structure to consider\n* The [generations array](https://quasilyte.dev/blog/post/gen-map/) is also a good option\n* Allocating the result path slice is expensive; consider deltas (2 bits per step)\n* Interface method calls are slow for a hot loop\n* Try to be cache-friendly; everything that can be packed should be packed\n* Not every game needs A*, don't underestimate the power of a simpler (and faster) algorithm\n\nIf you want to learn more details, look at my library implementation and/or see [these slides](https://speakerdeck.com/quasilyte/zero-alloc-pathfinding).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquasilyte%2Fpathing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fquasilyte%2Fpathing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fquasilyte%2Fpathing/lists"}