diff --git a/assets/test/shaders/pathfinding/CMakeLists.txt b/assets/test/shaders/pathfinding/CMakeLists.txt new file mode 100644 index 0000000000..2d30f59426 --- /dev/null +++ b/assets/test/shaders/pathfinding/CMakeLists.txt @@ -0,0 +1,4 @@ +install(DIRECTORY "." + DESTINATION "${ASSET_DIR}/test/shaders/pathfinding" + FILES_MATCHING PATTERN "*.glsl" +) diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl new file mode 100644 index 0000000000..7ccb86dcf4 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.frag.glsl @@ -0,0 +1,17 @@ +#version 330 + +in float v_cost; + +out vec4 out_col; + +void main() +{ + if (v_cost == 255.0) { + out_col = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + float cost = (v_cost / 256) * 2.0; + float red = clamp(cost, 0.0, 1.0); + float green = clamp(2.0 - cost, 0.0, 1.0); + out_col = vec4(red, green, 0.0, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl new file mode 100644 index 0000000000..d12f0db1d9 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_cost_field.vert.glsl @@ -0,0 +1,15 @@ +#version 330 + +layout (location = 0) in vec3 position; +layout (location = 1) in float cost; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +out float v_cost; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); + v_cost = cost; +} diff --git a/assets/test/shaders/pathfinding/demo_0_display.frag.glsl b/assets/test/shaders/pathfinding/demo_0_display.frag.glsl new file mode 100644 index 0000000000..a6732d0c7f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_display.frag.glsl @@ -0,0 +1,10 @@ +#version 330 + +uniform sampler2D color_texture; + +in vec2 v_uv; +out vec4 col; + +void main() { + col = texture(color_texture, v_uv); +} diff --git a/assets/test/shaders/pathfinding/demo_0_display.vert.glsl b/assets/test/shaders/pathfinding/demo_0_display.vert.glsl new file mode 100644 index 0000000000..6112530242 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_display.vert.glsl @@ -0,0 +1,10 @@ +#version 330 + +layout(location=0) in vec2 position; +layout(location=1) in vec2 uv; +out vec2 v_uv; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); + v_uv = uv; +} diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl new file mode 100644 index 0000000000..b5ff4042a3 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.frag.glsl @@ -0,0 +1,29 @@ +#version 330 + +in float v_cost; + +out vec4 outcol; + +void main() { + int cost = int(v_cost); + if (bool(cost & 0x40)) { + // wavefront blocked + outcol = vec4(0.9, 0.9, 0.9, 1.0); + return; + } + + if (bool(cost & 0x20)) { + // line of sight + outcol = vec4(1.0, 1.0, 1.0, 1.0); + return; + } + + if (bool(cost & 0x10)) { + // pathable + outcol = vec4(0.7, 0.7, 0.7, 1.0); + return; + } + + // not pathable + outcol = vec4(0.0, 0.0, 0.0, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl new file mode 100644 index 0000000000..2c2190e57c --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_flow_field.vert.glsl @@ -0,0 +1,15 @@ +#version 330 + +layout(location=0) in vec3 position; +layout(location=1) in float cost; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +out float v_cost; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); + v_cost = cost; +} diff --git a/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl b/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl new file mode 100644 index 0000000000..19d2e459c3 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_grid.frag.glsl @@ -0,0 +1,8 @@ +#version 330 + +out vec4 out_col; + +void main() +{ + out_col = vec4(0.0, 0.0, 0.0, 0.3); +} diff --git a/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl b/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl new file mode 100644 index 0000000000..2f5eaeaa15 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_grid.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout (location = 0) in vec3 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl new file mode 100644 index 0000000000..d40715a338 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_integration_field.frag.glsl @@ -0,0 +1,16 @@ +#version 330 + +in float v_cost; + +out vec4 out_col; + +void main() +{ + if (v_cost > 512.0) { + out_col = vec4(0.0, 0.0, 0.0, 1.0); + return; + } + float cost = 0.05 * v_cost; + float green = clamp(1.0 - cost, 0.0, 1.0); + out_col = vec4(0.75, green, 0.5, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl new file mode 100644 index 0000000000..d12f0db1d9 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_integration_field.vert.glsl @@ -0,0 +1,15 @@ +#version 330 + +layout (location = 0) in vec3 position; +layout (location = 1) in float cost; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +out float v_cost; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); + v_cost = cost; +} diff --git a/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl b/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl new file mode 100644 index 0000000000..ebbb10bc8f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_obj.frag.glsl @@ -0,0 +1,9 @@ +#version 330 + +uniform vec4 color; + +out vec4 outcol; + +void main() { + outcol = color; +} diff --git a/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl b/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl new file mode 100644 index 0000000000..527e0bb652 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_obj.vert.glsl @@ -0,0 +1,7 @@ +#version 330 + +layout(location=0) in vec2 position; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl b/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl new file mode 100644 index 0000000000..ebbb10bc8f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_vector.frag.glsl @@ -0,0 +1,9 @@ +#version 330 + +uniform vec4 color; + +out vec4 outcol; + +void main() { + outcol = color; +} diff --git a/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl b/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl new file mode 100644 index 0000000000..8b6b015408 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_0_vector.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout(location=0) in vec3 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_1_display.frag.glsl b/assets/test/shaders/pathfinding/demo_1_display.frag.glsl new file mode 100644 index 0000000000..a6732d0c7f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_display.frag.glsl @@ -0,0 +1,10 @@ +#version 330 + +uniform sampler2D color_texture; + +in vec2 v_uv; +out vec4 col; + +void main() { + col = texture(color_texture, v_uv); +} diff --git a/assets/test/shaders/pathfinding/demo_1_display.vert.glsl b/assets/test/shaders/pathfinding/demo_1_display.vert.glsl new file mode 100644 index 0000000000..6112530242 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_display.vert.glsl @@ -0,0 +1,10 @@ +#version 330 + +layout(location=0) in vec2 position; +layout(location=1) in vec2 uv; +out vec2 v_uv; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); + v_uv = uv; +} diff --git a/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl b/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl new file mode 100644 index 0000000000..19d2e459c3 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_grid.frag.glsl @@ -0,0 +1,8 @@ +#version 330 + +out vec4 out_col; + +void main() +{ + out_col = vec4(0.0, 0.0, 0.0, 0.3); +} diff --git a/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl b/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl new file mode 100644 index 0000000000..cbf6712eef --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_grid.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout (location = 0) in vec2 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 0.0, 1.0); +} diff --git a/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl b/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl new file mode 100644 index 0000000000..ebbb10bc8f --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_obj.frag.glsl @@ -0,0 +1,9 @@ +#version 330 + +uniform vec4 color; + +out vec4 outcol; + +void main() { + outcol = color; +} diff --git a/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl b/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl new file mode 100644 index 0000000000..bf804ffe53 --- /dev/null +++ b/assets/test/shaders/pathfinding/demo_1_obj.vert.glsl @@ -0,0 +1,11 @@ +#version 330 + +layout(location=0) in vec2 position; + +uniform mat4 model; +uniform mat4 view; +uniform mat4 proj; + +void main() { + gl_Position = proj * view * model * vec4(position, 0.0, 1.0); +} diff --git a/doc/code/images/pathfinder_architecture.svg b/doc/code/images/pathfinder_architecture.svg new file mode 100644 index 0000000000..2046a1a486 --- /dev/null +++ b/doc/code/images/pathfinder_architecture.svg @@ -0,0 +1,175 @@ + + +GamestateMovement,collision, etc.PathfinderPathFlowFieldIntegrationFieldIntegratorPathfinderPortalCostFieldSectorGrid diff --git a/doc/code/images/pathfinder_architecture.uxf b/doc/code/images/pathfinder_architecture.uxf new file mode 100644 index 0000000000..3a051b2acf --- /dev/null +++ b/doc/code/images/pathfinder_architecture.uxf @@ -0,0 +1,271 @@ + + + 10 + + UMLClass + + 710 + 510 + 120 + 60 + + +*Grid* + + + + UMLClass + + 710 + 620 + 120 + 60 + + +*Sector* + + + + UMLClass + + 630 + 730 + 120 + 60 + + +*CostField* + + + + UMLClass + + 790 + 730 + 120 + 60 + + +*Portal* + + + + UMLClass + + 710 + 400 + 120 + 60 + + +*Pathfinder* + + + + UMLClass + + 980 + 400 + 120 + 60 + + +*Integrator* + + + + UMLClass + + 1040 + 490 + 160 + 60 + + +*IntegrationField* + + + + UMLClass + + 1040 + 560 + 120 + 60 + + +*FlowField* + + + + UMLClass + + 520 + 400 + 120 + 60 + + +*Path* + + + + Relation + + 760 + 450 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 760 + 560 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 720 + 670 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 800 + 670 + 30 + 80 + + lt=<. + 10.0;60.0;10.0;10.0 + + + Relation + + 820 + 420 + 180 + 30 + + lt=<. + 160.0;10.0;10.0;10.0 + + + Relation + + 990 + 450 + 30 + 160 + + lt=. + 10.0;140.0;10.0;10.0 + + + Relation + + 990 + 510 + 70 + 30 + + lt=<. + 50.0;10.0;10.0;10.0 + + + Relation + + 990 + 580 + 70 + 30 + + lt=<. + 50.0;10.0;10.0;10.0 + + + Relation + + 630 + 420 + 100 + 30 + + lt=<. + 10.0;10.0;80.0;10.0 + + + Relation + + 760 + 200 + 30 + 220 + + lt=<. + 10.0;200.0;10.0;10.0 + + + Text + + 460 + 330 + 240 + 70 + + *Pathfinder* +fontsize=22 +style=wordwrap + + + + Relation + + 450 + 300 + 770 + 30 + + lt=- + 10.0;10.0;750.0;10.0 + + + Text + + 730 + 160 + 130 + 70 + + Movement, collision, etc. +style=wordwrap + + + + Text + + 460 + 160 + 240 + 70 + + *Gamestate* +fontsize=22 +style=wordwrap + + + diff --git a/doc/code/pathfinding/README.md b/doc/code/pathfinding/README.md new file mode 100644 index 0000000000..a7ec70d1bd --- /dev/null +++ b/doc/code/pathfinding/README.md @@ -0,0 +1,122 @@ +# Pathfinding + +openage's pathfinding subsystem implements structures used for navigating game entities in the +game world. These structures define movement costs for a map and allow search for a path +from one coordinate in the game world to another. + +Pathfinding is a subsystem of the [game simulation](/doc/code/game_simulation/README.md) where +it is primarily used for movement and placement of game entities. + +1. [Architecture](#architecture) +2. [Workflow](#workflow) + + +## Architecture + +The architecture of the pathfinder is heavily based on the article *Crowd Pathfinding and Steering* +*Using Flow Field Tiles* by Elijah Emerson (available [here](http://www.gameaipro.com/GameAIPro/GameAIPro_Chapter23_Crowd_Pathfinding_and_Steering_Using_Flow_Field_Tiles.pdf)). A core design +decision taken from this article was the usage of flow fields as the basis for the pathing algorithm. + +Flow fields offer a few advantages for large group movements, i.e. the movement you typically see in +RTS games like Age of Empires. For example, they allow reusing already computed path results for +subsequent pathing requests and can also be used for smart collision avoidance. One downside +of using flow fields can be the heavy upfront cost of pathing calculations. However, the downsides +can be mitigated to some degree with caching and high-level optimizations. + +The openage pathfinder is tile-based, like most flow field pathfinding implementations. Every tile +has a movement cost associated with it that represents the cost of a game entity moving on that tile. +When computing a path from A to B, the pathfinder tries to find the sequence of tiles with the cheapest +accumulated movement cost. + +In openage, the pathfinding subsystem is independent from the terrain implementation in the game +simulation. Terrain data may be used to influence movement cost during gameplay, but pathfinding +can be initialized without any requirements for terrain code. By only loosely connecting these two system, +we have a much more fine-grained control that allows for better optimization of the pathfinder. +The most relevant similarity between terrain and pathfinding code is that they use the same +[coordinate systems](/doc/code/coordinate-systems.md#tiletile3). To make the distinction between +pathfinding and terrain more clear, we use the term *cells* for tiles in the pathfinder. + +![UML pathfinding classes](/doc/code/images/pathfinder_architecture.svg) + +The relationship between classes in the pathfinder can be seen above. `Grid` is the top-level structure +for storage of movement cost. There may be multiple grids defined, one for each movement type +used by game entities in the game simulation. + +Every grid is subdivided into sectors, represented by the `Sector` class. Each sectors holds a +pointer to a `CostField` which stores the movement cost of individual cells. All sectors on the +same grid have a fixed square size that is determined by the grid. Thus, the sector size on +a grid is always consistent but different grid may utilize different sector sizes. + +Sectors on a grid are connect to each other with so-called portals, see the `Portal` +class. Portals represent a passable gateway between two sectors where game entities +can pass through. As such, portals store the coordinates of the cells in each sector +where game entities can pass. `Portal` objects are created for every continuous sequence of cells +on the edges between two sectors where the cells are passable on both sides of the two sectors. +Therefore, there may be multiple portals defined for the edge between two sectors. + +Additionally, each portal stores which other portals it can reach in a specific sector. The +result is a portal "graph" that can be searched separately to determine which portals +and sectors are visited for a specific pathing request. This is used in the high-level +pathfinder to preselect the sectors that flow fields need to be generated for (see the +[Workflow section](#workflow)). + +The individual movement cost of each cell in a sector are recorded in a `CostField` object. +The cost of a cell can range from 1 (minimum cost) to 254 (maximum cost), while a cost of 255 +makes a cell impassible. For Age of Empires, usually only the minimum and impassable cost +values are relevant. The cost field is built when the grid is first initialized and +individual cost of cells can be altered during gameplay events. + +To get a path between two coordinates, the game simulation mainly interfaces with a `Pathfinder` +object. The `Pathfinder` class calls the actual pathfinding algorithms used for searching +for a path and stores references to all available grids in the pathfinding subsystems. It can receive +`PathRequest` objects that contain information about the grid that should be searched as well as +the start and target coordinates of the desired path. Found paths are returned as `Path` objects +which store the waypoint coordinates for the computed path. + +Flow field calculations are controlled by an `Integrator` object, which process a cost field +from a sector as well as target coordinates to compute an `IntegrationField` and a `FlowField` object. +`IntegrationField`s are intermediary objects that store the accumulated cost of reaching +the target cell for each other cell in the field, using the cell values in the cost field as basis. +From the integration field, a `FlowField` object is created. Cells in the flow field store +movement vectors that point to their next cheapest neighbor cell in the integration field. `Path`s +may be computed from these flow field by simply following the movement vectors until the +target coordinate is reached. + + +## Workflow + +To initiate a new search, the pathfinder receives a `PathRequest` object, e.g. from the game simulation. +Every path request contains the following information: + +- ID of the grid to search +- start cell coordinates +- target cell coordinates + +The actual pathfinding algorithm is split into two stages. + +1. High-level portal-based search to identify the visited sectors on the grid +2. Low-level flow field-based search in the identified sectors to find the waypoints for the path + +The high-level search is accomplished by utilizing the portal graph, i.e. the +connections between portals in each sector. From the portal graph, a node mesh is created +that can be searched with a graph-traversal algorithm. For this purpose, the A\* algorithm +is used. The result of the high-level search is a list of sectors and portals that are +traversed by the path. + +The high-level search is mainly motivated by the need to avoid costly flow field +calculations on the whole grid. As the portal graph should already be precomputed when +a path request is made, the main influence on performance is the A\* algorithm. Given +a limited number of portals, the A\* search should overall be very cheap. + +The resulting list of sectors and portals is subsequently used in the low-level flow +field calculations. As a first step, the pathfinder uses its integrator to generate +a flow field for each identified sector. Generation starts with the target sector +and ends with the start sector. Flow field results are passed through at the cells +of the identified portals to make the flow between sectors seamless. + + + +In a second step, the pathfinder follows the movement vectors in the flow fields from +the start cell to the target cell. Waypoints are created for every direction change, so +that game entities can travel in straight lines between them. The list of waypoints +from start to target is then returned by the pathfinder via a `Path` object. diff --git a/etc/gdb_pretty/printers.py b/etc/gdb_pretty/printers.py index ca8f92083c..0dfac99af5 100644 --- a/etc/gdb_pretty/printers.py +++ b/etc/gdb_pretty/printers.py @@ -279,6 +279,136 @@ def children(self): yield ('time', self.__val['time']) yield ('value', self.__val['value']) + +@printer_typedef('openage::path::flow_t') +class PathFlowTypePrinter: + """ + Pretty printer for openage::path::flow_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + FLOW_FLAGS: dict = { + 0x10: 'PATHABLE', + 0x20: 'LOS', + 0x40: 'WAVEFRONT_BLOCKED', + 0x80: 'UNUSED', + } + + FLOW_DIRECTION: dict = { + 0x00: 'NORTH', + 0x01: 'NORTHEAST', + 0x02: 'EAST', + 0x03: 'SOUTHEAST', + 0x04: 'SOUTH', + 0x05: 'SOUTHWEST', + 0x06: 'WEST', + 0x07: 'NORTHWEST', + } + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the flow type as a string. + """ + flow = int(self.__val) + flags = flow & 0xF0 + direction = flow & 0x0F + return (f"{self.FLOW_DIRECTION.get(direction, 'INVALID')} (" + f"{', '.join([flag for mask, flag in self.FLOW_FLAGS.items() if mask & flags])})") + + def children(self): + """ + Get the displayed children of the flow type. + """ + flow = int(self.__val) + flags = flow & 0xF0 + direction = flow & 0x0F + yield ('direction', self.FLOW_DIRECTION[direction]) + for mask, flag in self.FLOW_FLAGS.items(): + yield (flag, bool(flags & mask)) + + +# Integrated flags +INTEGRATED_FLAGS: dict = { + 0x01: 'LOS', + 0x02: 'WAVEFRONT_BLOCKED', +} + + +def get_integrated_flags_list(value: int) -> str: + """ + Get the list of flags as a string. + + :param value: The value to get the flags for. + :type value: int + """ + flags = [] + for mask, flag in INTEGRATED_FLAGS.items(): + if value & mask: + flags.append(flag) + + return ' | '.join(flags) + + +@printer_typedef('openage::path::integrated_flags_t') +class PathIntegratedFlagsTypePrinter: + """ + Pretty printer for openage::path::integrated_flags_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the integrate type as a string. + """ + integrate = int(self.__val) + return get_integrated_flags_list(integrate) + + def children(self): + """ + Get the displayed children of the integrate type. + """ + integrate = int(self.__val) + for mask, flag in INTEGRATED_FLAGS.items(): + yield (flag, bool(integrate & mask)) + + +@printer_typedef('openage::path::integrated_t') +class PathIntegratedTypePrinter: + """ + Pretty printer for openage::path::integrated_t. + + TODO: Inherit from gdb.ValuePrinter when gdb 14.1 is available in all distros. + """ + + def __init__(self, val: gdb.Value): + self.__val = val + + def to_string(self): + """ + Get the integrate type as a string. + """ + output_str = f'cost = {self.__val["cost"]}' + flags = get_integrated_flags_list(int(self.__val['flags'])) + if len(flags) > 0: + output_str += f' ({flags})' + return output_str + + def children(self): + """ + Get the displayed children of the integrate type. + """ + yield ('cost', self.__val['cost']) + yield ('flags', self.__val['flags']) + + # TODO: curve types # TODO: pathfinding types # TODO: input event codes diff --git a/libopenage/coord/tile.h b/libopenage/coord/tile.h index 5cb3f2475d..e8e61a09c2 100644 --- a/libopenage/coord/tile.h +++ b/libopenage/coord/tile.h @@ -1,4 +1,4 @@ -// Copyright 2016-2023 the openage authors. See copying.md for legal info. +// Copyright 2016-2024 the openage authors. See copying.md for legal info. #pragma once @@ -9,9 +9,6 @@ #include "declarations.h" namespace openage { - -class Terrain; - namespace coord { /* diff --git a/libopenage/pathfinding/CMakeLists.txt b/libopenage/pathfinding/CMakeLists.txt index ad828995c9..7f85264d01 100644 --- a/libopenage/pathfinding/CMakeLists.txt +++ b/libopenage/pathfinding/CMakeLists.txt @@ -1,6 +1,17 @@ add_sources(libopenage - a_star.cpp - heuristics.cpp + cost_field.cpp + definitions.cpp + flow_field.cpp + grid.cpp + integration_field.cpp + integrator.cpp path.cpp + pathfinder.cpp + portal.cpp + sector.cpp tests.cpp + types.cpp ) + +add_subdirectory("demo") +add_subdirectory("legacy") diff --git a/libopenage/pathfinding/cost_field.cpp b/libopenage/pathfinding/cost_field.cpp new file mode 100644 index 0000000000..248a260d8e --- /dev/null +++ b/libopenage/pathfinding/cost_field.cpp @@ -0,0 +1,53 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "cost_field.h" + +#include "error/error.h" +#include "log/log.h" + +#include "coord/tile.h" +#include "pathfinding/definitions.h" + + +namespace openage::path { + +CostField::CostField(size_t size) : + size{size}, + cells(this->size * this->size, COST_MIN) { + log::log(DBG << "Created cost field with size " << this->size << "x" << this->size); +} + +size_t CostField::get_size() const { + return this->size; +} + +cost_t CostField::get_cost(const coord::tile &pos) const { + return this->cells.at(pos.ne + pos.se * this->size); +} + +cost_t CostField::get_cost(size_t idx) const { + return this->cells.at(idx); +} + +void CostField::set_cost(const coord::tile &pos, cost_t cost) { + this->cells[pos.ne + pos.se * this->size] = cost; +} + +void CostField::set_cost(size_t idx, cost_t cost) { + this->cells[idx] = cost; +} + +const std::vector &CostField::get_costs() const { + return this->cells; +} + +void CostField::set_costs(std::vector &&cells) { + ENSURE(cells.size() == this->cells.size(), + "cells vector has wrong size: " << cells.size() + << "; expected: " + << this->cells.size()); + + this->cells = std::move(cells); +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/cost_field.h b/libopenage/pathfinding/cost_field.h new file mode 100644 index 0000000000..ffb27bdec8 --- /dev/null +++ b/libopenage/pathfinding/cost_field.h @@ -0,0 +1,96 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "pathfinding/types.h" + + +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { + +/** + * Cost field in the flow-field pathfinding algorithm. + */ +class CostField { +public: + /** + * Create a square cost field with a specified size. + * + * @param size Side length of the field. + */ + CostField(size_t size); + + /** + * Get the size of the cost field. + * + * @return Size of the cost field. + */ + size_t get_size() const; + + /** + * Get the cost at a specified position. + * + * @param pos Coordinates of the cell. + * @return Cost at the specified position. + */ + cost_t get_cost(const coord::tile &pos) const; + + /** + * Get the cost at a specified position. + * + * @param idx Index of the cell. + * @return Cost at the specified position. + */ + cost_t get_cost(size_t idx) const; + + /** + * Set the cost at a specified position. + * + * @param pos Coordinates of the cell. + * @param cost Cost to set. + */ + void set_cost(const coord::tile &pos, cost_t cost); + + /** + * Set the cost at a specified position. + * + * @param idx Index of the cell. + * @param cost Cost to set. + */ + void set_cost(size_t idx, cost_t cost); + + /** + * Get the cost field values. + * + * @return Cost field values. + */ + const std::vector &get_costs() const; + + /** + * Set the cost field values. + * + * @param cells Cost field values. + */ + void set_costs(std::vector &&cells); + +private: + /** + * Side length of the field. + */ + size_t size; + + /** + * Cost field values. + */ + std::vector cells; +}; + +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/definitions.cpp b/libopenage/pathfinding/definitions.cpp new file mode 100644 index 0000000000..6428dd7ded --- /dev/null +++ b/libopenage/pathfinding/definitions.cpp @@ -0,0 +1,10 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "types.h" + + +namespace openage::path { + +// this file is intentionally empty + +} // namespace openage::path diff --git a/libopenage/pathfinding/definitions.h b/libopenage/pathfinding/definitions.h new file mode 100644 index 0000000000..d61c5fe43c --- /dev/null +++ b/libopenage/pathfinding/definitions.h @@ -0,0 +1,86 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "pathfinding/types.h" + + +namespace openage::path { + +/** + * Init value for a cells in the cost grid. + * + * Should not be used for actual costs. + */ +constexpr cost_t COST_INIT = 0; + +/** + * Minimum possible cost for a passable cell in the cost grid. + */ +constexpr cost_t COST_MIN = 1; + +/** + * Maximum possible cost for a passable cell in the cost grid. + */ +constexpr cost_t COST_MAX = 254; + +/** + * Cost value for an impassable cell in the cost grid. + */ +constexpr cost_t COST_IMPASSABLE = 255; + + +/** + * Start value for goal cells. + */ +const integrated_cost_t INTEGRATED_COST_START = 0; + +/** + * Unreachable value for a cells in the integration grid. + */ +constexpr integrated_cost_t INTEGRATED_COST_UNREACHABLE = std::numeric_limits::max(); + +/** + * Line of sight flag in an integrated_flags_t value. + */ +constexpr integrated_flags_t INTEGRATE_LOS_MASK = 0x01; + +/** + * Wavefront blocked flag in an integrated_flags_t value. + */ +constexpr integrated_flags_t INTEGRATE_WAVEFRONT_BLOCKED_MASK = 0x02; + +/** + * Initial value for a cell in the integration grid. + */ +constexpr integrated_t INTEGRATE_INIT = {INTEGRATED_COST_UNREACHABLE, 0}; + + +/** + * Initial value for a flow field cell. + */ +constexpr flow_t FLOW_INIT = 0; + +/** + * Mask for the flow direction bits in a flow_t value. + */ +constexpr flow_t FLOW_DIR_MASK = 0x0F; + +/** + * Pathable flag in a flow_t value. + */ +constexpr flow_t FLOW_PATHABLE_MASK = 0x10; + +/** + * Line of sight flag in a flow_t value. + */ +constexpr flow_t FLOW_LOS_MASK = 0x20; + +/** + * Wavefront blocked flag in a flow_t value. + */ +constexpr flow_t FLOW_WAVEFRONT_BLOCKED_MASK = 0x40; + +} // namespace openage::path diff --git a/libopenage/pathfinding/demo/CMakeLists.txt b/libopenage/pathfinding/demo/CMakeLists.txt new file mode 100644 index 0000000000..938d6ee3f0 --- /dev/null +++ b/libopenage/pathfinding/demo/CMakeLists.txt @@ -0,0 +1,9 @@ +add_sources(libopenage + demo_0.cpp + demo_1.cpp + tests.cpp +) + +pxdgen( + tests.h +) diff --git a/libopenage/pathfinding/demo/demo_0.cpp b/libopenage/pathfinding/demo/demo_0.cpp new file mode 100644 index 0000000000..da7ae17492 --- /dev/null +++ b/libopenage/pathfinding/demo/demo_0.cpp @@ -0,0 +1,892 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "demo_0.h" + +#include +#include + +#include "coord/tile.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/flow_field.h" +#include "pathfinding/integration_field.h" +#include "renderer/camera/camera.h" +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/renderable.h" +#include "renderer/renderer.h" +#include "renderer/resources/mesh_data.h" +#include "renderer/resources/shader_source.h" +#include "renderer/resources/texture_info.h" +#include "renderer/shader_program.h" +#include "util/math_constants.h" +#include "util/vector.h" + + +namespace openage::path::tests { + +void path_demo_0(const util::Path &path) { + // Side length of the field + // Creates a 10x10 grid + auto field_length = 10; + + // Cost field with some obstacles + auto cost_field = std::make_shared(field_length); + cost_field->set_cost(coord::tile{0, 0}, COST_IMPASSABLE); + cost_field->set_cost(coord::tile{1, 0}, 254); + cost_field->set_cost(coord::tile{4, 3}, 128); + cost_field->set_cost(coord::tile{5, 3}, 128); + cost_field->set_cost(coord::tile{6, 3}, 128); + cost_field->set_cost(coord::tile{4, 4}, 128); + cost_field->set_cost(coord::tile{5, 4}, 128); + cost_field->set_cost(coord::tile{6, 4}, 128); + cost_field->set_cost(coord::tile{1, 7}, COST_IMPASSABLE); + log::log(INFO << "Created cost field"); + + // Create an integration field from the cost field + auto integration_field = std::make_shared(field_length); + log::log(INFO << "Created integration field"); + + // Set cell (7, 7) to be the initial target cell + auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{7, 7}); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + log::log(INFO << "Calculated integration field for target cell (7, 7)"); + + // Create a flow field from the integration field + auto flow_field = std::make_shared(field_length); + log::log(INFO << "Created flow field"); + + flow_field->build(integration_field); + log::log(INFO << "Built flow field from integration field"); + + // Render the grid and the pathfinding results + auto qtapp = std::make_shared(); + + // Create a window for rendering + renderer::window_settings settings; + settings.width = 1440; + settings.height = 720; + auto window = std::make_shared("openage pathfinding test", settings); + auto render_manager = std::make_shared(qtapp, window, path); + log::log(INFO << "Created render manager for pathfinding demo"); + + // Show the cost field on startup + render_manager->show_cost_field(cost_field); + auto current_field = RenderManager0::field_t::COST; + log::log(INFO << "Showing cost field"); + + // Make steering vector visibility toggleable + auto vectors_visible = false; + + // Enable mouse button callbacks + window->add_mouse_button_callback([&](const QMouseEvent &ev) { + if (ev.type() == QEvent::MouseButtonRelease) { + if (ev.button() == Qt::RightButton) { // Set target cell + auto tile_pos = render_manager->select_tile(ev.position().x(), ev.position().y()); + auto grid_x = tile_pos.first; + auto grid_y = tile_pos.second; + + if (grid_x >= 0 and grid_x < field_length and grid_y >= 0 and grid_y < field_length) { + log::log(INFO << "Selected new target cell (" << grid_x << ", " << grid_y << ")"); + + // Recalculate the integration field and the flow field + integration_field->reset(); + auto wavefront_blocked = integration_field->integrate_los(cost_field, coord::tile{grid_x, grid_y}); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + log::log(INFO << "Calculated integration field for target cell (" + << grid_x << ", " << grid_y << ")"); + + flow_field->reset(); + flow_field->build(integration_field); + log::log(INFO << "Built flow field from integration field"); + + // Show the new field values and vectors + switch (current_field) { + case RenderManager0::field_t::COST: + render_manager->show_cost_field(cost_field); + break; + case RenderManager0::field_t::INTEGRATION: + render_manager->show_integration_field(integration_field); + break; + case RenderManager0::field_t::FLOW: + render_manager->show_flow_field(flow_field); + break; + } + + if (vectors_visible) { + render_manager->show_vectors(flow_field); + } + } + } + } + }); + + // Enable key callbacks + window->add_key_callback([&](const QKeyEvent &ev) { + if (ev.type() == QEvent::KeyRelease) { + if (ev.key() == Qt::Key_F1) { // Show cost field + render_manager->show_cost_field(cost_field); + current_field = RenderManager0::field_t::COST; + log::log(INFO << "Showing cost field"); + } + else if (ev.key() == Qt::Key_F2) { // Show integration field + render_manager->show_integration_field(integration_field); + current_field = RenderManager0::field_t::INTEGRATION; + log::log(INFO << "Showing integration field"); + } + else if (ev.key() == Qt::Key_F3) { // Show flow field + render_manager->show_flow_field(flow_field); + current_field = RenderManager0::field_t::FLOW; + log::log(INFO << "Showing flow field"); + } + else if (ev.key() == Qt::Key_F4) { // Show steering vectors + if (vectors_visible) { + render_manager->hide_vectors(); + vectors_visible = false; + log::log(INFO << "Hiding steering vectors"); + } + else { + render_manager->show_vectors(flow_field); + vectors_visible = true; + log::log(INFO << "Showing steering vectors"); + } + } + } + }); + + log::log(INFO << "Instructions:"); + log::log(INFO << " 1. Press F1/F2/F3 to show cost/integration/flow field"); + log::log(INFO << " 2. Press F4 to toggle steering vectors"); + + // Run the render loop + render_manager->run(); +} + + +RenderManager0::RenderManager0(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path) : + path{path}, + app{app}, + window{window}, + renderer{window->make_renderer()}, + camera{std::make_shared(renderer, window->get_size())} { + // Position camera to look at center of the grid + camera->look_at_coord({5, 5, 0}); + window->add_resize_callback([&](size_t w, size_t h, double /*scale*/) { + camera->resize(w, h); + }); + + this->init_shaders(); + this->init_passes(); +} + + +void RenderManager0::run() { + while (not this->window->should_close()) { + this->app->process_events(); + + this->renderer->render(this->background_pass); + this->renderer->render(this->field_pass); + this->renderer->render(this->vector_pass); + this->renderer->render(this->grid_pass); + this->renderer->render(this->display_pass); + + this->window->update(); + + this->renderer->check_error(); + } + this->window->close(); +} + +void RenderManager0::show_cost_field(const std::shared_ptr &field) { + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto unifs = this->cost_shader->new_uniform_input( + "model", + model, + "view", + this->camera->get_view_matrix(), + "proj", + this->camera->get_projection_matrix()); + auto mesh = RenderManager0::get_cost_field_mesh(field); + auto geometry = this->renderer->add_mesh_geometry(mesh); + renderer::Renderable renderable{ + unifs, + geometry, + true, + true, + }; + this->field_pass->set_renderables({renderable}); +} + +void RenderManager0::show_integration_field(const std::shared_ptr &field) { + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto unifs = this->integration_shader->new_uniform_input( + "model", + model, + "view", + this->camera->get_view_matrix(), + "proj", + this->camera->get_projection_matrix()); + auto mesh = get_integration_field_mesh(field, 4); + auto geometry = this->renderer->add_mesh_geometry(mesh); + renderer::Renderable renderable{ + unifs, + geometry, + true, + true, + }; + this->field_pass->set_renderables({renderable}); +} + +void RenderManager0::show_flow_field(const std::shared_ptr &field) { + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto unifs = this->flow_shader->new_uniform_input( + "model", + model, + "view", + this->camera->get_view_matrix(), + "proj", + this->camera->get_projection_matrix()); + auto mesh = get_flow_field_mesh(field, 4); + auto geometry = this->renderer->add_mesh_geometry(mesh); + renderer::Renderable renderable{ + unifs, + geometry, + true, + true, + }; + this->field_pass->set_renderables({renderable}); +} + +void RenderManager0::show_vectors(const std::shared_ptr &field) { + static const std::unordered_map offset_dir{ + {flow_dir_t::NORTH, {-0.25f, 0.0f, 0.0f}}, + {flow_dir_t::NORTH_EAST, {-0.25f, 0.0f, -0.25f}}, + {flow_dir_t::EAST, {0.0f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH_EAST, {0.25f, 0.0f, -0.25f}}, + {flow_dir_t::SOUTH, {0.25f, 0.0f, 0.0f}}, + {flow_dir_t::SOUTH_WEST, {0.25f, 0.0f, 0.25f}}, + {flow_dir_t::WEST, {0.0f, 0.0f, 0.25f}}, + {flow_dir_t::NORTH_WEST, {-0.25f, 0.0f, 0.25f}}, + }; + + this->vector_pass->clear_renderables(); + for (size_t y = 0; y < field->get_size(); ++y) { + for (size_t x = 0; x < field->get_size(); ++x) { + auto cell = field->get_cell(coord::tile(x, y)); + if (cell & FLOW_PATHABLE_MASK and not(cell & FLOW_LOS_MASK)) { + Eigen::Affine3f arrow_model = Eigen::Affine3f::Identity(); + arrow_model.translate(Eigen::Vector3f(y + 0.5, 0, -1.0f * x - 0.5)); + auto dir = static_cast(cell & FLOW_DIR_MASK); + arrow_model.translate(offset_dir.at(dir)); + + auto rotation_rad = (cell & FLOW_DIR_MASK) * -45 * math::DEGSPERRAD; + arrow_model.rotate(Eigen::AngleAxisf(rotation_rad, Eigen::Vector3f::UnitY())); + auto arrow_unifs = this->vector_shader->new_uniform_input( + "model", + arrow_model.matrix(), + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix(), + "color", + Eigen::Vector4f{0.0f, 0.0f, 0.0f, 1.0f}); + auto arrow_mesh = get_arrow_mesh(); + auto arrow_geometry = renderer->add_mesh_geometry(arrow_mesh); + renderer::Renderable arrow_renderable{ + arrow_unifs, + arrow_geometry, + true, + true, + }; + this->vector_pass->add_renderables(std::move(arrow_renderable)); + } + } + } +} + +void RenderManager0::hide_vectors() { + this->vector_pass->clear_renderables(); +} + +std::pair RenderManager0::select_tile(double x, double y) { + auto grid_plane_normal = Eigen::Vector3f{0, 1, 0}; + auto grid_plane_point = Eigen::Vector3f{0, 0, 0}; + auto camera_direction = renderer::camera::cam_direction; + auto camera_position = camera->get_input_pos( + coord::input(x, y)); + + Eigen::Vector3f intersect = camera_position + camera_direction * (grid_plane_point - camera_position).dot(grid_plane_normal) / camera_direction.dot(grid_plane_normal); + auto grid_x = static_cast(-1 * intersect[2]); + auto grid_y = static_cast(intersect[0]); + + return {grid_x, grid_y}; +} + +void RenderManager0::init_shaders() { + // Shader sources + auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding"; + + // Shader for rendering the cost field + auto cf_vshader_file = shaderdir / "demo_0_cost_field.vert.glsl"; + auto cf_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + cf_vshader_file); + + auto cf_fshader_file = shaderdir / "demo_0_cost_field.frag.glsl"; + auto cf_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + cf_fshader_file); + + // Shader for rendering the integration field + auto if_vshader_file = shaderdir / "demo_0_integration_field.vert.glsl"; + auto if_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + if_vshader_file); + + auto if_fshader_file = shaderdir / "demo_0_integration_field.frag.glsl"; + auto if_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + if_fshader_file); + + // Shader for rendering the flow field + auto ff_vshader_file = shaderdir / "demo_0_flow_field.vert.glsl"; + auto ff_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + ff_vshader_file); + + auto ff_fshader_file = shaderdir / "demo_0_flow_field.frag.glsl"; + auto ff_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + ff_fshader_file); + + // Shader for rendering steering vectors + auto vec_vshader_file = shaderdir / "demo_0_vector.vert.glsl"; + auto vec_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + vec_vshader_file); + + auto vec_fshader_file = shaderdir / "demo_0_vector.frag.glsl"; + auto vec_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + vec_fshader_file); + + // Shader for rendering the grid + auto grid_vshader_file = shaderdir / "demo_0_grid.vert.glsl"; + auto grid_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + grid_vshader_file); + + auto grid_fshader_file = shaderdir / "demo_0_grid.frag.glsl"; + auto grid_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + grid_fshader_file); + + // Shader for 2D monocolored objects + auto obj_vshader_file = shaderdir / "demo_0_obj.vert.glsl"; + auto obj_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + obj_vshader_file); + + auto obj_fshader_file = shaderdir / "demo_0_obj.frag.glsl"; + auto obj_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + obj_fshader_file); + + // Shader for rendering to the display target + auto display_vshader_file = shaderdir / "demo_0_display.vert.glsl"; + auto display_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + display_vshader_file); + + auto display_fshader_file = shaderdir / "demo_0_display.frag.glsl"; + auto display_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + display_fshader_file); + + // Create the shaders + this->cost_shader = renderer->add_shader({cf_vshader_src, cf_fshader_src}); + this->integration_shader = renderer->add_shader({if_vshader_src, if_fshader_src}); + this->flow_shader = renderer->add_shader({ff_vshader_src, ff_fshader_src}); + this->vector_shader = renderer->add_shader({vec_vshader_src, vec_fshader_src}); + this->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src}); + this->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); + this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); +} + +void RenderManager0::init_passes() { + auto size = this->window->get_size(); + + // Make a framebuffer for the background render pass to draw into + auto background_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto fbo = renderer->create_texture_target({background_texture, depth_texture}); + + // Background object for contrast between field and display + auto background_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{64.0 / 256, 128.0 / 256, 196.0 / 256, 1.0}); + auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable background_obj{ + background_unifs, + background, + true, + true, + }; + this->background_pass = renderer->add_render_pass({background_obj}, fbo); + + // Make a framebuffer for the field render pass to draw into + auto field_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_2 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); + this->field_pass = renderer->add_render_pass({}, field_fbo); + + // Make a framebuffer for the vector render passes to draw into + auto vector_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_3 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto vector_fbo = renderer->create_texture_target({vector_texture, depth_texture_3}); + this->vector_pass = renderer->add_render_pass({}, vector_fbo); + + // Make a framebuffer for the grid render passes to draw into + auto grid_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_4 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_4}); + + // Create object for the grid + Eigen::Matrix4f model = Eigen::Matrix4f::Identity(); + auto grid_unifs = grid_shader->new_uniform_input( + "model", + model, + "view", + camera->get_view_matrix(), + "proj", + camera->get_projection_matrix()); + auto grid_mesh = this->get_grid_mesh(10); + auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); + renderer::Renderable grid_obj{ + grid_unifs, + grid_geometry, + true, + true, + }; + this->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo); + + // Make two objects that draw the results of the previous passes onto the screen + // in the display render pass + auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture); + auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable bg_pass_obj{ + bg_texture_unif, + quad, + true, + true, + }; + auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture); + renderer::Renderable field_pass_obj{ + field_texture_unif, + quad, + true, + true, + }; + auto vector_texture_unif = display_shader->new_uniform_input("color_texture", vector_texture); + renderer::Renderable vector_pass_obj{ + vector_texture_unif, + quad, + true, + true, + }; + auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); + renderer::Renderable grid_pass_obj{ + grid_texture_unif, + quad, + true, + true, + }; + this->display_pass = renderer->add_render_pass( + {bg_pass_obj, field_pass_obj, vector_pass_obj, grid_pass_obj}, + renderer->get_display_target()); +} + +renderer::resources::MeshData RenderManager0::get_cost_field_mesh(const std::shared_ptr &field, + size_t resolution) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cost(coord::tile((i - 1) / resolution, (j - 1) / resolution)); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cost(coord::tile((i - 1) / resolution, j / resolution)); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cost(coord::tile(i / resolution, j / resolution)); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cost(coord::tile(i / resolution, (j - 1) / resolution)); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +renderer::resources::MeshData RenderManager0::get_integration_field_mesh(const std::shared_ptr &field, + size_t resolution) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)).cost; + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)).cost; + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)).cost; + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)).cost; + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +renderer::resources::MeshData RenderManager0::get_flow_field_mesh(const std::shared_ptr &field, + size_t resolution) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + field->get_size() * resolution + 1, + field->get_size() * resolution + 1, + }; + auto vert_distance = 1.0f / resolution; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 4); + for (int i = 0; i < static_cast(size[0]); ++i) { + for (int j = 0; j < static_cast(size[1]); ++j) { + // for each vertex, compare the surrounding tiles + std::vector surround{}; + if (j - 1 >= 0 and i - 1 >= 0) { + auto cost = field->get_cell(coord::tile((i - 1) / resolution, (j - 1) / resolution)); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i - 1 >= 0) { + auto cost = field->get_cell(coord::tile((i - 1) / resolution, j / resolution)); + surround.push_back(cost); + } + if (j < static_cast(field->get_size()) and i < static_cast(field->get_size())) { + auto cost = field->get_cell(coord::tile(i / resolution, j / resolution)); + surround.push_back(cost); + } + if (j - 1 >= 0 and i < static_cast(field->get_size())) { + auto cost = field->get_cell(coord::tile(i / resolution, (j - 1) / resolution)); + surround.push_back(cost); + } + // use the cost of the most expensive surrounding tile + auto max_cost = *std::max_element(surround.begin(), surround.end()); + + coord::scene3 v{ + static_cast(i * vert_distance), + static_cast(j * vert_distance), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + verts.push_back(max_cost); + } + } + + // split the grid into triangles using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 6); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we split each tile into two triangles + // with counter-clockwise vertex order + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32, renderer::resources::vertex_input_t::F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +renderer::resources::MeshData RenderManager0::get_arrow_mesh() { + // vertices for the arrow + // x, y, z + std::vector verts{ + // clang-format off + 0.2f, 0.01f, -0.05f, + 0.2f, 0.01f, 0.05f, + -0.2f, 0.01f, -0.05f, + -0.2f, 0.01f, 0.05f, + // clang-format on + }; + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + return {std::move(vert_data), info}; +} + +renderer::resources::MeshData RenderManager0::get_grid_mesh(size_t side_length) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{side_length + 1, side_length + 1}; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 3); + for (int i = 0; i < (int)size[0]; ++i) { + for (int j = 0; j < (int)size[1]; ++j) { + coord::scene3 v{ + static_cast(i), + static_cast(j), + 0, + }; + auto world_v = v.to_world_space(); + verts.push_back(world_v[0]); + verts.push_back(world_v[1]); + verts.push_back(world_v[2]); + } + } + + // split the grid into lines using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 8); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we just draw a square using the 4 vertices + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + i * size[1]); // bottom left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V3F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::LINES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + + +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_0.h b/libopenage/pathfinding/demo/demo_0.h new file mode 100644 index 0000000000..7a07ebfd7e --- /dev/null +++ b/libopenage/pathfinding/demo/demo_0.h @@ -0,0 +1,278 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include + +#include "util/path.h" + + +namespace openage { +namespace renderer { +class RenderPass; +class Renderer; +class ShaderProgram; +class Window; + +namespace camera { +class Camera; +} // namespace camera + +namespace gui { +class GuiApplicationWithLogger; +} // namespace gui + +namespace resources { +class MeshData; +} // namespace resources +} // namespace renderer + +namespace path { +class CostField; +class IntegrationField; +class FlowField; + +namespace tests { + +/** + * Show the functionality of the different flowfield types: + * - Cost field + * - Integration field + * - Flow field + * + * Visualizes the pathfinding results using our rendering backend. + * + * @param path Path to the project rootdir. + */ +void path_demo_0(const util::Path &path); + + +/** + * Manages the graphical display of the pathfinding demo. + */ +class RenderManager0 { +public: + enum class field_t { + COST, + INTEGRATION, + FLOW + }; + + /** + * Create a new render manager. + * + * @param app GUI application. + * @param window Window to render to. + * @param path Path to the project rootdir. + */ + RenderManager0(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path); + ~RenderManager0() = default; + + /** + * Run the render loop. + */ + void run(); + + /** + * Draw a cost field to the screen. + * + * @param field Cost field. + */ + void show_cost_field(const std::shared_ptr &field); + + /** + * Draw an integration field to the screen. + * + * @param field Integration field. + */ + void show_integration_field(const std::shared_ptr &field); + + /** + * Draw a flow field to the screen. + * + * @param field Flow field. + */ + void show_flow_field(const std::shared_ptr &field); + + /** + * Draw the steering vectors of a flow field to the screen. + * + * @param field Flow field. + */ + void show_vectors(const std::shared_ptr &field); + + /** + * Hide drawn steering vectors. + */ + void hide_vectors(); + + /** + * Get the cell coordinates at a given screen position. + * + * @param x X coordinate. + * @param y Y coordinate. + */ + std::pair select_tile(double x, double y); + +private: + /** + * Load the shader sources for the demo and create the shader programs. + */ + void init_shaders(); + + /** + * Create the following render passes for the demo: + * - Background pass: Mono-colored background object. + * - Field pass; Renders the cost, integration and flow fields. + * - Vector pass: Renders the steering vectors of a flow field. + * - Grid pass: Renders a grid on top of the fields. + * - Display pass: Draws the results of previous passes to the screen. + */ + void init_passes(); + + + /** + * Create a mesh for the cost field. + * + * @param field Cost field to visualize. + * @param resolution Determines the number of subdivisions per grid cell. The number of + * quads per cell is resolution^2. (default = 2) + * + * @return Mesh data for the cost field. + */ + static renderer::resources::MeshData get_cost_field_mesh(const std::shared_ptr &field, + size_t resolution = 2); + + /** + * Create a mesh for the integration field. + * + * @param field Integration field to visualize. + * @param resolution Determines the number of subdivisions per grid cell. The number of + * quads per cell is resolution^2. (default = 2) + * + * @return Mesh data for the integration field. + */ + static renderer::resources::MeshData get_integration_field_mesh(const std::shared_ptr &field, + size_t resolution = 2); + + /** + * Create a mesh for the flow field. + * + * @param field Flow field to visualize. + */ + static renderer::resources::MeshData get_flow_field_mesh(const std::shared_ptr &field, + size_t resolution = 2); + + /** + * Create a mesh for an arrow. + * + * @return Mesh data for an arrow. + */ + static renderer::resources::MeshData get_arrow_mesh(); + + /** + * Create a mesh for the grid. + * + * @param side_length Length of the grid's side. + * + * @return Mesh data for the grid. + */ + static renderer::resources::MeshData get_grid_mesh(size_t side_length); + + /** + * Path to the project rootdir. + */ + const util::Path &path; + + /* Renderer objects */ + + /** + * Qt GUI application. + */ + std::shared_ptr app; + + /** + * openage window to render to. + */ + std::shared_ptr window; + + /** + * openage renderer instance. + */ + std::shared_ptr renderer; + + /** + * Camera to view the scene. + */ + std::shared_ptr camera; + + /* Shader programs */ + + /** + * Shader program for rendering a cost field. + */ + std::shared_ptr cost_shader; + + /** + * Shader program for rendering a integration field. + */ + std::shared_ptr integration_shader; + + /** + * Shader program for rendering a flow field. + */ + std::shared_ptr flow_shader; + + /** + * Shader program for rendering steering vectors. + */ + std::shared_ptr vector_shader; + + /** + * Shader program for rendering a grid. + */ + std::shared_ptr grid_shader; + + /** + * Shader program for rendering 2D mono-colored objects. + */ + std::shared_ptr obj_shader; + + /** + * Shader program for rendering the final display. + */ + std::shared_ptr display_shader; + + /* Render passes */ + + /** + * Background pass: Mono-colored background object. + */ + std::shared_ptr background_pass; + + /** + * Field pass: Renders the cost, integration and flow fields. + */ + std::shared_ptr field_pass; + + /** + * Vector pass: Renders the steering vectors of a flow field. + */ + std::shared_ptr vector_pass; + + /** + * Grid pass: Renders a grid on top of the fields. + */ + std::shared_ptr grid_pass; + + /** + * Display pass: Draws the results of previous passes to the screen. + */ + std::shared_ptr display_pass; +}; + +} // namespace tests +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/demo/demo_1.cpp b/libopenage/pathfinding/demo/demo_1.cpp new file mode 100644 index 0000000000..21aaa2c9d8 --- /dev/null +++ b/libopenage/pathfinding/demo/demo_1.cpp @@ -0,0 +1,716 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "demo_1.h" + +#include + +#include "pathfinding/cost_field.h" +#include "pathfinding/grid.h" +#include "pathfinding/path.h" +#include "pathfinding/pathfinder.h" +#include "pathfinding/portal.h" +#include "pathfinding/sector.h" +#include "util/timer.h" + +#include "renderer/gui/integration/public/gui_application_with_logger.h" +#include "renderer/opengl/window.h" +#include "renderer/render_pass.h" +#include "renderer/renderable.h" +#include "renderer/resources/shader_source.h" +#include "renderer/resources/texture_info.h" +#include "renderer/shader_program.h" +#include "renderer/window.h" + + +namespace openage::path::tests { + +void path_demo_1(const util::Path &path) { + auto grid = std::make_shared(0, util::Vector2s{4, 3}, 10); + + // Initialize the cost field for each sector. + for (auto §or : grid->get_sectors()) { + auto cost_field = sector->get_cost_field(); + std::vector sector_cost = sectors_cost.at(sector->get_id()); + cost_field->set_costs(std::move(sector_cost)); + } + + // Initialize portals between sectors. + util::Vector2s grid_size = grid->get_size(); + portal_id_t portal_id = 0; + for (size_t y = 0; y < grid_size[1]; y++) { + for (size_t x = 0; x < grid_size[0]; x++) { + auto sector = grid->get_sector(x, y); + + if (x < grid_size[0] - 1) { + auto neighbor = grid->get_sector(x + 1, y); + auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id); + for (auto &portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + if (y < grid_size[1] - 1) { + auto neighbor = grid->get_sector(x, y + 1); + auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id); + for (auto &portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + } + } + + // Connect portals inside sectors. + for (auto sector : grid->get_sectors()) { + sector->connect_exits(); + + log::log(MSG(info) << "Sector " << sector->get_id() << " has " << sector->get_portals().size() << " portals."); + for (auto portal : sector->get_portals()) { + log::log(MSG(info) << " Portal " << portal->get_id() << ":"); + log::log(MSG(info) << " Connects sectors: " << sector->get_id() << " to " + << portal->get_exit_sector(sector->get_id())); + log::log(MSG(info) << " Entry start: " << portal->get_entry_start(sector->get_id())); + log::log(MSG(info) << " Entry end: " << portal->get_entry_end(sector->get_id())); + log::log(MSG(info) << " Connected portals: " << portal->get_connected(sector->get_id()).size()); + } + } + + // Create a pathfinder for searching paths on the grid + auto pathfinder = std::make_shared(); + pathfinder->add_grid(grid); + + util::Timer timer; + + // Create a path request and get the path + coord::tile start{2, 26}; + coord::tile target{36, 2}; + + PathRequest path_request{ + grid->get_id(), + start, + target, + }; + timer.start(); + Path path_result = pathfinder->get_path(path_request); + timer.stop(); + + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + + // Create a renderer to display the grid and path + auto qtapp = std::make_shared(); + + renderer::window_settings settings; + settings.width = 1024; + settings.height = 768; + auto window = std::make_shared("openage pathfinding test", settings); + auto render_manager = std::make_shared(qtapp, window, path, grid); + log::log(INFO << "Created render manager for pathfinding demo"); + + window->add_mouse_button_callback([&](const QMouseEvent &ev) { + if (ev.type() == QEvent::MouseButtonRelease) { + auto cell_count_x = grid->get_size()[0] * grid->get_sector_size(); + auto cell_count_y = grid->get_size()[1] * grid->get_sector_size(); + auto window_size = window->get_size(); + + double cell_size_x = static_cast(window_size[0]) / cell_count_x; + double cell_size_y = static_cast(window_size[1]) / cell_count_y; + + coord::tile_t grid_x = ev.position().x() / cell_size_x; + coord::tile_t grid_y = ev.position().y() / cell_size_y; + + if (ev.button() == Qt::RightButton) { // Set target cell + target = coord::tile{grid_x, grid_y}; + PathRequest new_path_request{ + grid->get_id(), + start, + target, + }; + + timer.reset(); + timer.start(); + path_result = pathfinder->get_path(new_path_request); + timer.stop(); + + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } + else if (ev.button() == Qt::LeftButton) { // Set start cell + start = coord::tile{grid_x, grid_y}; + PathRequest new_path_request{ + grid->get_id(), + start, + target, + }; + + timer.reset(); + timer.start(); + path_result = pathfinder->get_path(new_path_request); + timer.stop(); + + log::log(INFO << "Pathfinding took " << timer.getval() / 1000 << " ps"); + + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + } + } + }); + + // Create renderables for the waypoints of the path + render_manager->create_waypoint_tiles(path_result); + + // Run the renderer pss to draw the grid and path into a window + // TODO: Make this a while (not window.should_close()) loop + render_manager->run(); +} + + +RenderManager1::RenderManager1(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path, + const std::shared_ptr &grid) : + path{path}, + grid{grid}, + app{app}, + window{window}, + renderer{window->make_renderer()} { + this->init_shaders(); + this->init_passes(); +} + +void RenderManager1::run() { + while (not this->window->should_close()) { + this->app->process_events(); + + this->renderer->render(this->background_pass); + this->renderer->render(this->field_pass); + this->renderer->render(this->waypoint_pass); + this->renderer->render(this->grid_pass); + this->renderer->render(this->display_pass); + + this->window->update(); + + this->renderer->check_error(); + } + this->window->close(); +} + +void RenderManager1::init_passes() { + auto size = this->window->get_size(); + + // Make a framebuffer for the background render pass to draw into + auto background_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto fbo = renderer->create_texture_target({background_texture, depth_texture}); + + // Background object for contrast between field and display + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + auto background_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{1.0, 1.0, 1.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto background = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable background_obj{ + background_unifs, + background, + true, + true, + }; + this->background_pass = renderer->add_render_pass({background_obj}, fbo); + + // Make a framebuffer for the field render pass to draw into + auto field_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_2 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto field_fbo = renderer->create_texture_target({field_texture, depth_texture_2}); + this->field_pass = renderer->add_render_pass({}, field_fbo); + this->create_impassible_tiles(this->grid); + this->create_portal_tiles(this->grid); + + // Make a framebuffer for the grid render passes to draw into + auto grid_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_3 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto grid_fbo = renderer->create_texture_target({grid_texture, depth_texture_3}); + + // Make a framebuffer for the waypoint render pass to draw into + auto waypoint_texture = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::rgba8)); + auto depth_texture_4 = renderer->add_texture( + renderer::resources::Texture2dInfo(size[0], + size[1], + renderer::resources::pixel_format::depth24)); + auto waypoint_fbo = renderer->create_texture_target({waypoint_texture, depth_texture_4}); + this->waypoint_pass = renderer->add_render_pass({}, waypoint_fbo); + + // Create object for the grid + auto model = Eigen::Affine3f::Identity(); + model.prescale(Eigen::Vector3f{ + 2.0f / (this->grid->get_size()[0] * this->grid->get_sector_size()), + 2.0f / (this->grid->get_size()[1] * this->grid->get_sector_size()), + 1.0f}); + model.pretranslate(Eigen::Vector3f{-1.0f, -1.0f, 0.0f}); + auto grid_unifs = grid_shader->new_uniform_input( + "model", + model.matrix(), + "view", + id_matrix, + "proj", + id_matrix); + auto grid_mesh = this->get_grid_mesh(this->grid); + auto grid_geometry = renderer->add_mesh_geometry(grid_mesh); + renderer::Renderable grid_obj{ + grid_unifs, + grid_geometry, + true, + true, + }; + this->grid_pass = renderer->add_render_pass({grid_obj}, grid_fbo); + + // Make two objects that draw the results of the previous passes onto the screen + // in the display render pass + auto bg_texture_unif = display_shader->new_uniform_input("color_texture", background_texture); + auto quad = renderer->add_mesh_geometry(renderer::resources::MeshData::make_quad()); + renderer::Renderable bg_pass_obj{ + bg_texture_unif, + quad, + true, + true, + }; + auto field_texture_unif = display_shader->new_uniform_input("color_texture", field_texture); + renderer::Renderable field_pass_obj{ + field_texture_unif, + quad, + true, + true, + }; + auto waypoint_texture_unif = display_shader->new_uniform_input("color_texture", waypoint_texture); + renderer::Renderable waypoint_pass_obj{ + waypoint_texture_unif, + quad, + true, + true, + }; + auto grid_texture_unif = display_shader->new_uniform_input("color_texture", grid_texture); + renderer::Renderable grid_pass_obj{ + grid_texture_unif, + quad, + true, + true, + }; + + this->display_pass = renderer->add_render_pass( + {bg_pass_obj, field_pass_obj, waypoint_pass_obj, grid_pass_obj}, + renderer->get_display_target()); +} + +void RenderManager1::init_shaders() { + // Shader sources + auto shaderdir = this->path / "assets" / "test" / "shaders" / "pathfinding"; + + // Shader for rendering the grid + auto grid_vshader_file = shaderdir / "demo_1_grid.vert.glsl"; + auto grid_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + grid_vshader_file); + + auto grid_fshader_file = shaderdir / "demo_1_grid.frag.glsl"; + auto grid_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + grid_fshader_file); + + // Shader for 2D monocolored objects + auto obj_vshader_file = shaderdir / "demo_1_obj.vert.glsl"; + auto obj_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + obj_vshader_file); + + auto obj_fshader_file = shaderdir / "demo_1_obj.frag.glsl"; + auto obj_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + obj_fshader_file); + + // Shader for rendering to the display target + auto display_vshader_file = shaderdir / "demo_1_display.vert.glsl"; + auto display_vshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::vertex, + display_vshader_file); + + auto display_fshader_file = shaderdir / "demo_1_display.frag.glsl"; + auto display_fshader_src = renderer::resources::ShaderSource( + renderer::resources::shader_lang_t::glsl, + renderer::resources::shader_stage_t::fragment, + display_fshader_file); + + // Create the shaders + this->grid_shader = renderer->add_shader({grid_vshader_src, grid_fshader_src}); + this->obj_shader = renderer->add_shader({obj_vshader_src, obj_fshader_src}); + this->display_shader = renderer->add_shader({display_vshader_src, display_fshader_src}); +} + + +renderer::resources::MeshData RenderManager1::get_grid_mesh(const std::shared_ptr &grid) { + // increase by 1 in every dimension because to get the vertex length + // of each dimension + util::Vector2s size{ + grid->get_size()[0] * grid->get_sector_size() + 1, + grid->get_size()[1] * grid->get_sector_size() + 1}; + + // add vertices for the cells of the grid + std::vector verts{}; + auto vert_count = size[0] * size[1]; + verts.reserve(vert_count * 2); + for (int i = 0; i < (int)size[0]; ++i) { + for (int j = 0; j < (int)size[1]; ++j) { + verts.push_back(i); + verts.push_back(j); + } + } + + // split the grid into lines using an index array + std::vector idxs; + idxs.reserve((size[0] - 1) * (size[1] - 1) * 8); + // iterate over all tiles in the grid by columns, i.e. starting + // from the left corner to the bottom corner if you imagine it from + // the camera's point of view + for (size_t i = 0; i < size[0] - 1; ++i) { + for (size_t j = 0; j < size[1] - 1; ++j) { + // since we are working on tiles, we just draw a square using the 4 vertices + idxs.push_back(j + i * size[1]); // bottom left + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + 1 + i * size[1]); // bottom right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + 1 + i * size[1]); // top right + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + size[1] + i * size[1]); // top left + idxs.push_back(j + i * size[1]); // bottom left + } + } + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::LINES, + renderer::resources::index_t::U16}; + + auto const vert_data_size = verts.size() * sizeof(float); + std::vector vert_data(vert_data_size); + std::memcpy(vert_data.data(), verts.data(), vert_data_size); + + auto const idx_data_size = idxs.size() * sizeof(uint16_t); + std::vector idx_data(idx_data_size); + std::memcpy(idx_data.data(), idxs.data(), idx_data_size); + + return {std::move(vert_data), std::move(idx_data), info}; +} + +void RenderManager1::create_impassible_tiles(const std::shared_ptr &grid) { + auto width = grid->get_size()[0]; + auto height = grid->get_size()[1]; + auto sector_size = grid->get_sector_size(); + + float tile_offset_width = 2.0f / (width * sector_size); + float tile_offset_height = 2.0f / (height * sector_size); + + for (size_t sector_x = 0; sector_x < width; sector_x++) { + for (size_t sector_y = 0; sector_y < height; sector_y++) { + auto sector = grid->get_sector(sector_x, sector_y); + auto cost_field = sector->get_cost_field(); + for (size_t y = 0; y < sector_size; y++) { + for (size_t x = 0; x < sector_size; x++) { + auto cost = cost_field->get_cost(coord::tile(x, y)); + if (cost == COST_IMPASSABLE) { + std::array tile_data{ + -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + x * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - y * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (x + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (y + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + }; + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const data_size = tile_data.size() * sizeof(float); + std::vector verts(data_size); + std::memcpy(verts.data(), tile_data.data(), data_size); + + auto tile_mesh = renderer::resources::MeshData(std::move(verts), info); + auto tile_geometry = renderer->add_mesh_geometry(tile_mesh); + + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + auto tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{0.0, 0.0, 0.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto tile_obj = renderer::Renderable{ + tile_unifs, + tile_geometry, + true, + true, + }; + this->field_pass->add_renderables({tile_obj}); + } + } + } + } + } +} + +void RenderManager1::create_portal_tiles(const std::shared_ptr &grid) { + auto width = grid->get_size()[0]; + auto height = grid->get_size()[1]; + auto sector_size = grid->get_sector_size(); + + float tile_offset_width = 2.0f / (width * sector_size); + float tile_offset_height = 2.0f / (height * sector_size); + + for (size_t sector_x = 0; sector_x < width; sector_x++) { + for (size_t sector_y = 0; sector_y < height; sector_y++) { + auto sector = grid->get_sector(sector_x, sector_y); + + for (auto &portal : sector->get_portals()) { + auto start = portal->get_entry_start(sector->get_id()); + auto end = portal->get_entry_end(sector->get_id()); + auto direction = portal->get_direction(); + + std::vector tiles; + if (direction == PortalDirection::NORTH_SOUTH) { + auto y = start.se; + for (auto x = start.ne; x <= end.ne; ++x) { + tiles.push_back(coord::tile(x, y)); + } + } + else { + auto x = start.ne; + for (auto y = start.se; y <= end.se; ++y) { + tiles.push_back(coord::tile(x, y)); + } + } + + for (auto tile : tiles) { + std::array tile_data{ + -1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + tile.ne * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - tile.se * tile_offset_height - sector_size * sector_y * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width + sector_size * sector_x * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height - sector_size * sector_y * tile_offset_height, + }; + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const data_size = tile_data.size() * sizeof(float); + std::vector verts(data_size); + std::memcpy(verts.data(), tile_data.data(), data_size); + + auto tile_mesh = renderer::resources::MeshData(std::move(verts), info); + auto tile_geometry = renderer->add_mesh_geometry(tile_mesh); + + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + auto tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{0.0, 0.0, 0.0, 0.3}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto tile_obj = renderer::Renderable{ + tile_unifs, + tile_geometry, + true, + true, + }; + this->field_pass->add_renderables({tile_obj}); + } + } + } + } +} + +void RenderManager1::create_waypoint_tiles(const Path &path) { + auto width = grid->get_size()[0]; + auto height = grid->get_size()[1]; + auto sector_size = grid->get_sector_size(); + + float tile_offset_width = 2.0f / (width * sector_size); + float tile_offset_height = 2.0f / (height * sector_size); + + Eigen::Matrix4f id_matrix = Eigen::Matrix4f::Identity(); + + this->waypoint_pass->clear_renderables(); + + // Draw in-between waypoints + for (size_t i = 1; i < path.waypoints.size() - 1; i++) { + auto tile = path.waypoints[i]; + std::array tile_data{ + -1.0f + tile.ne * tile_offset_width, + 1.0f - tile.se * tile_offset_height, + -1.0f + tile.ne * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width, + 1.0f - tile.se * tile_offset_height, + -1.0f + (tile.ne + 1) * tile_offset_width, + 1.0f - (tile.se + 1) * tile_offset_height, + }; + + renderer::resources::VertexInputInfo info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const data_size = tile_data.size() * sizeof(float); + std::vector verts(data_size); + std::memcpy(verts.data(), tile_data.data(), data_size); + + auto tile_mesh = renderer::resources::MeshData(std::move(verts), info); + auto tile_geometry = renderer->add_mesh_geometry(tile_mesh); + + auto tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{0.0, 0.25, 1.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto tile_obj = renderer::Renderable{ + tile_unifs, + tile_geometry, + true, + true, + }; + this->waypoint_pass->add_renderables({tile_obj}); + } + + // Draw start and end waypoints with different colors + auto start_tile = path.waypoints.front(); + std::array start_tile_data{ + -1.0f + start_tile.ne * tile_offset_width, + 1.0f - start_tile.se * tile_offset_height, + -1.0f + start_tile.ne * tile_offset_width, + 1.0f - (start_tile.se + 1) * tile_offset_height, + -1.0f + (start_tile.ne + 1) * tile_offset_width, + 1.0f - start_tile.se * tile_offset_height, + -1.0f + (start_tile.ne + 1) * tile_offset_width, + 1.0f - (start_tile.se + 1) * tile_offset_height, + }; + + renderer::resources::VertexInputInfo start_info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const start_data_size = start_tile_data.size() * sizeof(float); + std::vector start_verts(start_data_size); + std::memcpy(start_verts.data(), start_tile_data.data(), start_data_size); + + auto start_tile_mesh = renderer::resources::MeshData(std::move(start_verts), start_info); + auto start_tile_geometry = renderer->add_mesh_geometry(start_tile_mesh); + auto start_tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{0.0, 0.5, 0.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + auto start_tile_obj = renderer::Renderable{ + start_tile_unifs, + start_tile_geometry, + true, + true, + }; + + auto end_tile = path.waypoints.back(); + std::array end_tile_data{ + -1.0f + end_tile.ne * tile_offset_width, + 1.0f - end_tile.se * tile_offset_height, + -1.0f + end_tile.ne * tile_offset_width, + 1.0f - (end_tile.se + 1) * tile_offset_height, + -1.0f + (end_tile.ne + 1) * tile_offset_width, + 1.0f - end_tile.se * tile_offset_height, + -1.0f + (end_tile.ne + 1) * tile_offset_width, + 1.0f - (end_tile.se + 1) * tile_offset_height, + }; + + renderer::resources::VertexInputInfo end_info{ + {renderer::resources::vertex_input_t::V2F32}, + renderer::resources::vertex_layout_t::AOS, + renderer::resources::vertex_primitive_t::TRIANGLE_STRIP}; + + auto const end_data_size = end_tile_data.size() * sizeof(float); + std::vector end_verts(end_data_size); + std::memcpy(end_verts.data(), end_tile_data.data(), end_data_size); + + auto end_tile_mesh = renderer::resources::MeshData(std::move(end_verts), end_info); + auto end_tile_geometry = renderer->add_mesh_geometry(end_tile_mesh); + auto end_tile_unifs = obj_shader->new_uniform_input( + "color", + Eigen::Vector4f{1.0, 0.5, 0.0, 1.0}, + "model", + id_matrix, + "view", + id_matrix, + "proj", + id_matrix); + + auto end_tile_obj = renderer::Renderable{ + end_tile_unifs, + end_tile_geometry, + true, + true, + }; + + this->waypoint_pass->add_renderables({start_tile_obj, end_tile_obj}); +} + +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/demo_1.h b/libopenage/pathfinding/demo/demo_1.h new file mode 100644 index 0000000000..db70efe084 --- /dev/null +++ b/libopenage/pathfinding/demo/demo_1.h @@ -0,0 +1,357 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "pathfinding/definitions.h" +#include "pathfinding/path.h" +#include "renderer/resources/mesh_data.h" +#include "util/path.h" + + +namespace openage { +namespace renderer { +class Renderer; +class RenderPass; +class ShaderProgram; +class Window; + +namespace gui { +class GuiApplicationWithLogger; +} // namespace gui + +} // namespace renderer + +namespace path { +class Grid; + +namespace tests { + + +/** + * Show the functionality of the high-level pathfinder: + * - Grids + * - Sectors + * - Portals + * + * Visualizes the pathfinding results using our rendering backend. + * + * @param path Path to the project rootdir. + */ +void path_demo_1(const util::Path &path); + + +/** + * Manages the graphical display of the pathfinding demo. + */ +class RenderManager1 { +public: + /** + * Create a new render manager. + * + * @param app GUI application. + * @param window Window to render to. + * @param path Path to the project rootdir. + */ + RenderManager1(const std::shared_ptr &app, + const std::shared_ptr &window, + const util::Path &path, + const std::shared_ptr &grid); + ~RenderManager1() = default; + + /** + * Run the render loop. + */ + void run(); + + /** + * Create renderables for the waypoint tiles of a path. + * + * @param path Path object. + */ + void create_waypoint_tiles(const Path &path); + +private: + /** + * Load the shader sources for the demo and create the shader programs. + */ + void init_shaders(); + + /** + * Create the following render passes for the demo: + * - Background pass: Mono-colored background object. + * - Field pass; Renders the cost, integration and flow fields. + * - Grid pass: Renders a grid on top of the cost field. + * - Display pass: Draws the results of previous passes to the screen. + */ + void init_passes(); + + /** + * Create a mesh for the grid. + * + * @param grid Pathing grid. + * + * @return Mesh data for the grid. + */ + static renderer::resources::MeshData get_grid_mesh(const std::shared_ptr &grid); + + /** + * Create renderables for the impassible tiles in the grid. + * + * @param grid Pathing grid. + */ + void create_impassible_tiles(const std::shared_ptr &grid); + + /** + * Create renderables for the portal tiles in the grid. + * + * @param grid Pathing grid. + */ + void create_portal_tiles(const std::shared_ptr &grid); + + /** + * Path to the project rootdir. + */ + const util::Path &path; + + /** + * Pathing grid of the demo. + */ + std::shared_ptr grid; + + /* Renderer objects */ + + /** + * Qt GUI application. + */ + std::shared_ptr app; + + /** + * openage window to render to. + */ + std::shared_ptr window; + + /** + * openage renderer instance. + */ + std::shared_ptr renderer; + + /* Shader programs */ + + /** + * Shader program for rendering a grid. + */ + std::shared_ptr grid_shader; + + /** + * Shader program for rendering 2D mono-colored objects. + */ + std::shared_ptr obj_shader; + + /** + * Shader program for rendering the final display. + */ + std::shared_ptr display_shader; + + /* Render passes */ + + /** + * Background pass: Mono-colored background object. + */ + std::shared_ptr background_pass; + + /** + * Field pass: Renders the cost field. + */ + std::shared_ptr field_pass; + + /** + * Grid pass: Renders a grid on top of the cost field. + */ + std::shared_ptr grid_pass; + + /** + * Waypoint pass: Renders the path and its waypoints. + */ + std::shared_ptr waypoint_pass; + + /** + * Display pass: Draws the results of previous passes to the screen. + */ + std::shared_ptr display_pass; +}; + +// Cost for the sectors in the grid +// taken from Figure 23.1 in "Crowd Pathfinding and Steering Using Flow Field Tiles" +const std::vector> sectors_cost = { + { + // clang-format off + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 255, 255, 255, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 255, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 255, 255, 255, 255, 255, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 255, 255, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 255, 1, 1, 1, + 1, 1, 1, 255, 1, 255, 255, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 255, 1, 1, 1, 1, 1, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 1, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 255, 255, 255, 255, 255, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 255, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 1, 1, 1, 255, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 1, 1, 1, 1, 1, + 1, 1, 255, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 255, 255, 255, 255, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 255, 255, + // clang-format on + }, + { + // clang-format off + 1, 1, 1, 255, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 255, 1, 255, 255, 255, 255, 255, + 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, + 1, 1, 1, 255, 1, 1, 1, 1, 255, 255, + 1, 255, 255, 1, 1, 1, 1, 1, 255, 255, + 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, + 1, 1, 255, 1, 1, 255, 1, 1, 1, 1, + 255, 255, 255, 255, 255, 255, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 255, 255, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 255, 255, 255, 255, + 255, 1, 1, 1, 255, 255, 255, 255, 255, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 255, 1, 1, 1, 1, 255, 255, 255, + 1, 1, 255, 1, 1, 1, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 255, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 255, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 1, 1, 1, 1, 1, 1, + 1, 1, 255, 255, 255, 255, 255, 255, 1, 1, + 1, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 255, 255, 255, 1, 1, 1, 1, 1, 1, + 255, 1, 1, 1, 1, 1, 1, 1, 1, 255, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 255, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 255, 1, 1, + 1, 1, 255, 255, 1, 1, 1, 255, 1, 1, + 1, 255, 255, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 1, 1, 1, 1, 1, + 255, 255, 1, 1, 1, 255, 255, 255, 1, 1, + 1, 1, 1, 1, 1, 1, 255, 1, 1, 1, + 1, 1, 1, 255, 255, 255, 255, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }, + { + // clang-format off + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + // clang-format on + }}; + +} // namespace tests +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/demo/tests.cpp b/libopenage/pathfinding/demo/tests.cpp new file mode 100644 index 0000000000..633dddb621 --- /dev/null +++ b/libopenage/pathfinding/demo/tests.cpp @@ -0,0 +1,29 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "tests.h" + +#include "log/log.h" + +#include "pathfinding/demo/demo_0.h" +#include "pathfinding/demo/demo_1.h" + + +namespace openage::path::tests { + +void path_demo(int demo_id, const util::Path &path) { + switch (demo_id) { + case 0: + path_demo_0(path); + break; + + case 1: + path_demo_1(path); + break; + + default: + log::log(MSG(err) << "Unknown pathfinding demo requested: " << demo_id << "."); + break; + } +} + +} // namespace openage::path::tests diff --git a/libopenage/pathfinding/demo/tests.h b/libopenage/pathfinding/demo/tests.h new file mode 100644 index 0000000000..2f411f6bf1 --- /dev/null +++ b/libopenage/pathfinding/demo/tests.h @@ -0,0 +1,20 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include "../../util/compiler.h" +// pxd: from libopenage.util.path cimport Path + + +namespace openage { +namespace util { +class Path; +} // namespace util + +namespace path::tests { + +// pxd: void path_demo(int demo_id, Path path) except + +OAAPI void path_demo(int demo_id, const util::Path &path); + +} // namespace path::tests +} // namespace openage diff --git a/libopenage/pathfinding/flow_field.cpp b/libopenage/pathfinding/flow_field.cpp new file mode 100644 index 0000000000..0aa8daf84b --- /dev/null +++ b/libopenage/pathfinding/flow_field.cpp @@ -0,0 +1,234 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "flow_field.h" + +#include "error/error.h" +#include "log/log.h" + +#include "coord/tile.h" +#include "pathfinding/integration_field.h" +#include "pathfinding/portal.h" + + +namespace openage::path { + +FlowField::FlowField(size_t size) : + size{size}, + cells(this->size * this->size, FLOW_INIT) { + log::log(DBG << "Created flow field with size " << this->size << "x" << this->size); +} + +FlowField::FlowField(const std::shared_ptr &integration_field) : + size{integration_field->get_size()}, + cells(this->size * this->size, FLOW_INIT) { + this->build(integration_field); +} + +size_t FlowField::get_size() const { + return this->size; +} + +flow_t FlowField::get_cell(const coord::tile &pos) const { + return this->cells.at(pos.ne + pos.se * this->size); +} + +flow_dir_t FlowField::get_dir(const coord::tile &pos) const { + return static_cast(this->get_cell(pos) & FLOW_DIR_MASK); +} + +void FlowField::build(const std::shared_ptr &integration_field, + const std::unordered_set &target_cells) { + ENSURE(integration_field->get_size() == this->get_size(), + "integration field size " + << integration_field->get_size() << "x" << integration_field->get_size() + << " does not match flow field size " + << this->get_size() << "x" << this->get_size()); + + auto &integrate_cells = integration_field->get_cells(); + auto &flow_cells = this->cells; + + for (size_t y = 0; y < this->size; ++y) { + for (size_t x = 0; x < this->size; ++x) { + size_t idx = y * this->size + x; + + if (target_cells.contains(idx)) { + // Ignore target cells + continue; + } + + if (integrate_cells[idx].cost == INTEGRATED_COST_UNREACHABLE) { + // Cell cannot be used as path + continue; + } + + if (integrate_cells[idx].flags & INTEGRATE_LOS_MASK) { + // Cell is in line of sight + this->cells[idx] = this->cells[idx] | FLOW_LOS_MASK; + + // we can skip calculating the flow direction as we can + // move straight to the target from this cell + this->cells[idx] = this->cells[idx] | FLOW_PATHABLE_MASK; + continue; + } + + if (integrate_cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // Cell is blocked by a line-of-sight corner + this->cells[idx] = this->cells[idx] | FLOW_WAVEFRONT_BLOCKED_MASK; + } + + // Find the neighbor with the smallest cost. + flow_dir_t direction = static_cast(this->cells[idx] & FLOW_DIR_MASK); + auto smallest_cost = INTEGRATED_COST_UNREACHABLE; + if (y > 0) { + auto cost = integrate_cells[idx - this->size].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::NORTH; + } + } + if (x < this->size - 1 && y > 0) { + auto cost = integrate_cells[idx - this->size + 1].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::NORTH_EAST; + } + } + if (x < this->size - 1) { + auto cost = integrate_cells[idx + 1].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::EAST; + } + } + if (x < this->size - 1 && y < this->size - 1) { + auto cost = integrate_cells[idx + this->size + 1].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH_EAST; + } + } + if (y < this->size - 1) { + auto cost = integrate_cells[idx + this->size].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH; + } + } + if (x > 0 && y < this->size - 1) { + auto cost = integrate_cells[idx + this->size - 1].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::SOUTH_WEST; + } + } + if (x > 0) { + auto cost = integrate_cells[idx - 1].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::WEST; + } + } + if (x > 0 && y > 0) { + auto cost = integrate_cells[idx - this->size - 1].cost; + if (cost < smallest_cost) { + smallest_cost = cost; + direction = flow_dir_t::NORTH_WEST; + } + } + + // Set the flow field cell to pathable. + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + + // Set the flow field cell to the direction of the smallest cost. + flow_cells[idx] = flow_cells[idx] | static_cast(direction); + } + } +} + +void FlowField::build(const std::shared_ptr &integration_field, + const std::shared_ptr & /* other */, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + ENSURE(integration_field->get_size() == this->get_size(), + "integration field size " + << integration_field->get_size() << "x" << integration_field->get_size() + << " does not match flow field size " + << this->get_size() << "x" << this->get_size()); + + auto &flow_cells = this->cells; + auto direction = portal->get_direction(); + + // portal entry and exit cell coordinates + auto entry_start = portal->get_entry_start(other_sector_id); + auto exit_start = portal->get_exit_start(other_sector_id); + auto exit_end = portal->get_exit_end(other_sector_id); + + // TODO: Compare integration values from other side of portal + // auto &integrate_cells = integration_field->get_cells(); + + // cells that are part of the portal + std::unordered_set portal_cells; + + // set the direction for the flow field cells that are part of the portal + if (direction == PortalDirection::NORTH_SOUTH) { + bool other_is_north = entry_start.se > exit_start.se; + if (other_is_north) { + auto y = exit_start.se; + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::NORTH); + portal_cells.insert(idx); + } + } + else { + auto y = exit_start.se; + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::SOUTH); + portal_cells.insert(idx); + } + } + } + else if (direction == PortalDirection::EAST_WEST) { + bool other_is_east = entry_start.ne < exit_start.ne; + if (other_is_east) { + auto x = exit_start.ne; + for (auto y = exit_start.se; y <= exit_end.se; ++y) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::EAST); + portal_cells.insert(idx); + } + } + else { + auto x = exit_start.ne; + for (auto y = exit_start.se; y <= exit_end.se; ++y) { + auto idx = y * this->size + x; + flow_cells[idx] = flow_cells[idx] | FLOW_PATHABLE_MASK; + flow_cells[idx] = flow_cells[idx] | static_cast(flow_dir_t::WEST); + portal_cells.insert(idx); + } + } + } + else { + throw Error(ERR << "Invalid portal direction: " << static_cast(direction)); + } + + this->build(integration_field, portal_cells); +} + +const std::vector &FlowField::get_cells() const { + return this->cells; +} + +void FlowField::reset() { + for (auto &cell : this->cells) { + cell = FLOW_INIT; + } + + log::log(DBG << "Flow field has been reset"); +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/flow_field.h b/libopenage/pathfinding/flow_field.h new file mode 100644 index 0000000000..1c1adcfe33 --- /dev/null +++ b/libopenage/pathfinding/flow_field.h @@ -0,0 +1,112 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include + +#include "pathfinding/definitions.h" +#include "pathfinding/types.h" + + +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { +class IntegrationField; +class Portal; + +class FlowField { +public: + /** + * Create a square flow field with a specified size. + * + * @param size Side length of the field. + */ + FlowField(size_t size); + + /** + * Create a flow field from an existing integration field. + * + * @param integration_field Integration field. + */ + FlowField(const std::shared_ptr &integration_field); + + /** + * Get the size of the flow field. + * + * @return Size of the flow field. + */ + size_t get_size() const; + + /** + * Get the flow field value at a specified position. + * + * @param pos Coordinates of the cell. + * + * @return Flowfield value at the specified position. + */ + flow_t get_cell(const coord::tile &pos) const; + + /** + * Get the flow field direction at a specified position. + * + * @param pos Coordinates of the cell. + * + * @return Flowfield direction at the specified position. + */ + flow_dir_t get_dir(const coord::tile &pos) const; + + /** + * Build the flow field. + * + * @param integration_field Integration field. + * @param target_cells Target cells of the flow field. These cells are ignored + * when building the field. + */ + void build(const std::shared_ptr &integration_field, + const std::unordered_set &target_cells = {}); + + /** + * Build the flow field for a portal. + * + * @param integration_field Integration field. + * @param other Integration field of the other sector. + * @param other_sector_id Sector ID of the other field. + * @param portal Portal connecting the two fields. + */ + void build(const std::shared_ptr &integration_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + + /** + * Get the flow field values. + * + * @return Flow field values. + */ + const std::vector &get_cells() const; + + /** + * Reset the flow field values for rebuilding the field. + */ + void reset(); + +private: + /** + * Side length of the field. + */ + size_t size; + + /** + * Flow field cells. + */ + std::vector cells; +}; + +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/grid.cpp b/libopenage/pathfinding/grid.cpp new file mode 100644 index 0000000000..62a2d3156f --- /dev/null +++ b/libopenage/pathfinding/grid.cpp @@ -0,0 +1,101 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "grid.h" + +#include "error/error.h" + +#include "coord/chunk.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/sector.h" + + +namespace openage::path { + +Grid::Grid(grid_id_t id, + const util::Vector2s &size, + size_t sector_size) : + id{id}, + size{size}, + sector_size{sector_size} { + for (size_t y = 0; y < size[1]; y++) { + for (size_t x = 0; x < size[0]; x++) { + this->sectors.push_back( + std::make_shared(x + y * this->size[0], + coord::chunk(x, y), + sector_size)); + } + } +} + +Grid::Grid(grid_id_t id, + const util::Vector2s &size, + std::vector> &§ors) : + id{id}, + size{size}, + sectors{std::move(sectors)} { + ENSURE(this->sectors.size() == size[0] * size[1], + "Grid has size " << size[0] << "x" << size[1] << " (" << size[0] * size[1] << " sectors), " + << "but only " << this->sectors.size() << " sectors were provided"); + + this->sector_size = sectors.at(0)->get_cost_field()->get_size(); +} + +grid_id_t Grid::get_id() const { + return this->id; +} + +const util::Vector2s &Grid::get_size() const { + return this->size; +} + +size_t Grid::get_sector_size() const { + return this->sector_size; +} + +const std::shared_ptr &Grid::get_sector(size_t x, size_t y) { + return this->sectors.at(x + y * this->size[0]); +} + +const std::shared_ptr &Grid::get_sector(sector_id_t id) const { + return this->sectors.at(id); +} + +const std::vector> &Grid::get_sectors() const { + return this->sectors; +} + +void Grid::init_portals() { + // Create portals between neighboring sectors. + portal_id_t portal_id = 0; + for (size_t y = 0; y < this->size[1]; y++) { + for (size_t x = 0; x < this->size[0]; x++) { + auto sector = this->get_sector(x, y); + + if (x < this->size[0] - 1) { + auto neighbor = this->get_sector(x + 1, y); + auto portals = sector->find_portals(neighbor, PortalDirection::EAST_WEST, portal_id); + for (auto &portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + if (y < this->size[1] - 1) { + auto neighbor = this->get_sector(x, y + 1); + auto portals = sector->find_portals(neighbor, PortalDirection::NORTH_SOUTH, portal_id); + for (auto &portal : portals) { + sector->add_portal(portal); + neighbor->add_portal(portal); + } + portal_id += portals.size(); + } + } + } + + // Connect mutually reachable exits of sectors. + for (auto §or : this->sectors) { + sector->connect_exits(); + } +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/grid.h b/libopenage/pathfinding/grid.h new file mode 100644 index 0000000000..83b4c2b721 --- /dev/null +++ b/libopenage/pathfinding/grid.h @@ -0,0 +1,121 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include + +#include "pathfinding/types.h" +#include "util/vector.h" + + +namespace openage::path { +class Sector; + +/** + * Grid for flow field pathfinding. + */ +class Grid { +public: + /** + * Create a new empty grid of width x height sectors with a specified size. + * + * @param id ID of the grid. + * @param size Size of the grid (width x height). + * @param sector_size Side length of each sector. + */ + Grid(grid_id_t id, + const util::Vector2s &size, + size_t sector_size); + + /** + * Create a grid of width x height sectors from a list of existing sectors. + * + * @param id ID of the grid. + * @param size Size of the grid (width x height). + * @param sectors Existing sectors. + */ + Grid(grid_id_t id, + const util::Vector2s &size, + std::vector> &§ors); + + /** + * Get the ID of the grid. + * + * @return ID of the grid. + */ + grid_id_t get_id() const; + + /** + * Get the size of the grid. + * + * @return Size of the grid (width x height). + */ + const util::Vector2s &get_size() const; + + /** + * Get the side length of the sectors on the grid. + * + * @return Sector side length. + */ + size_t get_sector_size() const; + + /** + * Get the sector at a specified position. + * + * @param x X coordinate. + * @param y Y coordinate. + * + * @return Sector at the specified position. + */ + const std::shared_ptr &get_sector(size_t x, size_t y); + + /** + * Get the sector with a specified ID + * + * @param id ID of the sector. + * + * @return Sector with the specified ID. + */ + const std::shared_ptr &get_sector(sector_id_t id) const; + + /** + * Get the sectors of the grid. + * + * @return Sectors of the grid. + */ + const std::vector> &get_sectors() const; + + /** + * Initialize the portals of the sectors on the grid. + * + * This should be called after all sectors' cost fields have been initialized. + */ + void init_portals(); + +private: + /** + * ID of the grid. + */ + grid_id_t id; + + /** + * Size of the grid (width x height). + */ + util::Vector2s size; + + /** + * Side length of the grid sectors. + */ + size_t sector_size; + + /** + * Sectors of the grid. + */ + std::vector> sectors; +}; + + +} // namespace openage::path diff --git a/libopenage/pathfinding/heuristics.cpp b/libopenage/pathfinding/heuristics.cpp deleted file mode 100644 index 9a1a1213dc..0000000000 --- a/libopenage/pathfinding/heuristics.cpp +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2014-2018 the openage authors. See copying.md for legal info. - -#include "heuristics.h" - -#include -#include - - -namespace openage { -namespace path { - -cost_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end) { - cost_t dx = std::abs(start.ne - end.ne).to_float(); - cost_t dy = std::abs(start.se - end.se).to_float(); - return dx + dy; -} - -cost_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end) { - cost_t dx = std::abs(start.ne - end.ne).to_float(); - cost_t dy = std::abs(start.se - end.se).to_float(); - return std::max(dx, dy); -} - -cost_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end) { - return (end - start).length(); -} - -cost_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end) { - cost_t dx = (start.ne - end.ne).to_float(); - cost_t dy = (start.se - end.se).to_float(); - return dx * dx + dy * dy; -} - -}} // openage::path diff --git a/libopenage/pathfinding/integration_field.cpp b/libopenage/pathfinding/integration_field.cpp new file mode 100644 index 0000000000..bc2745879f --- /dev/null +++ b/libopenage/pathfinding/integration_field.cpp @@ -0,0 +1,520 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "integration_field.h" + +#include + +#include "error/error.h" +#include "log/log.h" + +#include "coord/tile.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/definitions.h" +#include "pathfinding/portal.h" + + +namespace openage::path { + +IntegrationField::IntegrationField(size_t size) : + size{size}, + cells(this->size * this->size, INTEGRATE_INIT) { + log::log(DBG << "Created integration field with size " << this->size << "x" << this->size); +} + +size_t IntegrationField::get_size() const { + return this->size; +} + +const integrated_t &IntegrationField::get_cell(const coord::tile &pos) const { + return this->cells.at(pos.ne + pos.se * this->size); +} + +const integrated_t &IntegrationField::get_cell(size_t idx) const { + return this->cells.at(idx); +} + +std::vector IntegrationField::integrate_los(const std::shared_ptr &cost_field, + const coord::tile &target) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Store the wavefront_blocked cells + std::vector wavefront_blocked; + + // Target cell index + auto target_idx = target.ne + target.se * this->size; + + // Lookup table for cells that have been found + std::unordered_set found; + found.reserve(this->size * this->size); + + // Cells that still have to be visited by the current wave + std::deque current_wave; + + // Cells that have to be visited in the next wave + std::deque next_wave; + + // Cost of the current wave + integrated_cost_t cost = INTEGRATED_COST_START; + + // Move outwards from the target cell, updating the integration field + current_wave.push_back(target_idx); + do { + while (!current_wave.empty()) { + auto idx = current_wave.front(); + current_wave.pop_front(); + + if (found.contains(idx)) { + // Skip cells that have already been found + continue; + } + else if (this->cells[idx].flags & INTEGRATE_WAVEFRONT_BLOCKED_MASK) { + // Stop at cells that are blocked by a LOS corner + this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); + found.insert(idx); + continue; + } + + // Add the current cell to the found cells + found.insert(idx); + + // Get the x and y coordinates of the current cell + auto x = idx % this->size; + auto y = idx / this->size; + + // Get the cost of the current cell + auto cell_cost = cost_field->get_cost(idx); + + if (cell_cost > COST_MIN) { + // cell blocks line of sight + // and we have to check for corners + if (cell_cost != COST_IMPASSABLE) { + // Add the current cell to the blocked wavefront if it's not a wall + wavefront_blocked.push_back(idx); + this->cells[idx].cost = cost - 1 + cost_field->get_cost(idx); + } + + // check each neighbor for a corner + auto corners = this->get_los_corners(cost_field, target, coord::tile(x, y)); + for (auto &corner : corners) { + // draw a line from the corner to the edge of the field + // to get the cells blocked by the corner + auto blocked_cells = this->bresenhams_line(target, corner.first, corner.second); + for (auto &blocked_idx : blocked_cells) { + if (cost_field->get_cost(blocked_idx) > COST_MIN) { + // stop if blocked_idx is impassable + break; + } + // set the blocked flag for the cell + this->cells[blocked_idx].flags |= INTEGRATE_WAVEFRONT_BLOCKED_MASK; + + // clear los flag if it was set + this->cells[blocked_idx].flags &= ~INTEGRATE_LOS_MASK; + } + wavefront_blocked.insert(wavefront_blocked.end(), blocked_cells.begin(), blocked_cells.end()); + } + continue; + } + + // The cell is in the line of sight at min cost + // Set the LOS flag and cost + this->cells[idx].cost = cost; + this->cells[idx].flags |= INTEGRATE_LOS_MASK; + + // Search the neighbors of the current cell + if (y > 0) { + next_wave.push_back(idx - this->size); + } + if (x > 0) { + next_wave.push_back(idx - 1); + } + if (y < this->size - 1) { + next_wave.push_back(idx + this->size); + } + if (x < this->size - 1) { + next_wave.push_back(idx + 1); + } + } + + // increment the cost and advance the wavefront outwards + cost += 1; + current_wave.swap(next_wave); + } + while (!current_wave.empty()); + + return wavefront_blocked; +} + +void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, + const coord::tile &target) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Target cell index + auto target_idx = target.ne + target.se * this->size; + + // Move outwards from the target cell, updating the integration field + this->cells[target_idx].cost = INTEGRATED_COST_START; + this->integrate_cost(cost_field, {target_idx}); +} + +void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + ENSURE(cost_field->get_size() == this->get_size(), + "cost field size " + << cost_field->get_size() << "x" << cost_field->get_size() + << " does not match integration field size " + << this->get_size() << "x" << this->get_size()); + + // Integrate the cost of the cells on the exit side (this) of the portal + std::vector start_cells; + auto exit_start = portal->get_exit_start(other_sector_id); + auto exit_end = portal->get_exit_end(other_sector_id); + for (auto y = exit_start.se; y <= exit_end.se; ++y) { + for (auto x = exit_start.ne; x <= exit_end.ne; ++x) { + // every portal cell is a target cell + auto target_idx = x + y * this->size; + + // Set the cost of all target cells to the start value + this->cells[target_idx].cost = INTEGRATED_COST_START; + start_cells.push_back(target_idx); + + // TODO: Transfer flags and cost from the other integration field + } + } + + // Integrate the rest of the cost field + this->integrate_cost(cost_field, std::move(start_cells)); +} + +void IntegrationField::integrate_cost(const std::shared_ptr &cost_field, + std::vector &&start_cells) { + // Lookup table for cells that are in the open list + std::unordered_set in_list; + in_list.reserve(this->size * this->size); + + // Cells that still have to be visited + // they may be visited multiple times + std::deque open_list; + + // Stores neighbors of the current cell + std::vector neighbors; + neighbors.reserve(4); + + // Move outwards from the wavefront, updating the integration field + open_list.insert(open_list.end(), start_cells.begin(), start_cells.end()); + while (!open_list.empty()) { + auto idx = open_list.front(); + open_list.pop_front(); + + // Get the x and y coordinates of the current cell + auto x = idx % this->size; + auto y = idx / this->size; + + auto integrated_current = this->cells.at(idx).cost; + + // Get the neighbors of the current cell + if (y > 0) { + neighbors.push_back(idx - this->size); + } + if (x > 0) { + neighbors.push_back(idx - 1); + } + if (y < this->size - 1) { + neighbors.push_back(idx + this->size); + } + if (x < this->size - 1) { + neighbors.push_back(idx + 1); + } + + // Update the integration field of the neighboring cells + for (auto &neighbor_idx : neighbors) { + this->update_neighbor(neighbor_idx, + cost_field->get_cost(neighbor_idx), + integrated_current, + open_list, + in_list); + } + + // Clear the neighbors vector + neighbors.clear(); + + // Remove the current cell from the open list lookup table + in_list.erase(idx); + } +} + +const std::vector &IntegrationField::get_cells() const { + return this->cells; +} + +void IntegrationField::reset() { + for (auto &cell : this->cells) { + cell = INTEGRATE_INIT; + } + log::log(DBG << "Integration field has been reset"); +} + +void IntegrationField::update_neighbor(size_t idx, + cost_t cell_cost, + integrated_cost_t integrated_cost, + std::deque &open_list, + std::unordered_set &in_list) { + ENSURE(cell_cost > COST_INIT, "cost field cell value must be non-zero"); + + // Check if the cell is impassable + // then we don't need to update the integration field + if (cell_cost == COST_IMPASSABLE) { + return; + } + + auto cost = integrated_cost + cell_cost; + if (cost < this->cells.at(idx).cost) { + // If the new integration value is smaller than the current one, + // update the cell and add it to the open list + this->cells[idx].cost = cost; + + if (not in_list.contains(idx)) { + in_list.insert(idx); + open_list.push_back(idx); + } + } +} + +std::vector> IntegrationField::get_los_corners(const std::shared_ptr &cost_field, + const coord::tile &target, + const coord::tile &blocker) { + std::vector> corners; + + // Get the cost of the blocking cell's neighbors + + // Set all costs to IMPASSABLE at the beginning + auto top_cost = COST_IMPASSABLE; + auto left_cost = COST_IMPASSABLE; + auto bottom_cost = COST_IMPASSABLE; + auto right_cost = COST_IMPASSABLE; + + std::pair top_left{blocker.ne, blocker.se}; + std::pair top_right{blocker.ne + 1, blocker.se}; + std::pair bottom_left{blocker.ne, blocker.se + 1}; + std::pair bottom_right{blocker.ne + 1, blocker.se + 1}; + + // Get neighbor costs (if they exist) + if (blocker.se > 0) { + top_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se - 1}); + } + if (blocker.ne > 0) { + left_cost = cost_field->get_cost(coord::tile{blocker.ne - 1, blocker.se}); + } + if (static_cast(blocker.se) < this->size - 1) { + bottom_cost = cost_field->get_cost(coord::tile{blocker.ne, blocker.se + 1}); + } + if (static_cast(blocker.ne) < this->size - 1) { + right_cost = cost_field->get_cost(coord::tile{blocker.ne + 1, blocker.se}); + } + + // Check which corners are blocking LOS + // TODO: Currently super complicated and could likely be optimized + if (blocker.ne == target.ne) { + // blocking cell is parallel to target on y-axis + if (blocker.se < target.se) { + if (left_cost == COST_MIN) { + // top + corners.push_back(bottom_left); + } + if (right_cost == COST_MIN) { + // top + corners.push_back(bottom_right); + } + } + else { + if (left_cost == COST_MIN) { + // bottom + corners.push_back(top_left); + } + if (right_cost == COST_MIN) { + // bottom + corners.push_back(top_right); + } + } + } + else if (blocker.se == target.se) { + // blocking cell is parallel to target on x-axis + if (blocker.ne < target.ne) { + if (top_cost == COST_MIN) { + // right + corners.push_back(top_right); + } + if (bottom_cost == COST_MIN) { + // right + corners.push_back(bottom_right); + } + } + else { + if (top_cost == COST_MIN) { + // left + corners.push_back(top_left); + } + if (bottom_cost == COST_MIN) { + // left + corners.push_back(bottom_left); + } + } + } + else { + // blocking cell is diagonal to target on + if (blocker.ne < target.ne) { + if (blocker.se < target.se) { + // top and right + if (top_cost == COST_MIN and right_cost == COST_MIN) { + // right + corners.push_back(top_right); + } + if (left_cost == COST_MIN and bottom_cost == COST_MIN) { + // bottom + corners.push_back(bottom_left); + } + } + else { + // bottom and right + if (bottom_cost == COST_MIN and right_cost == COST_MIN) { + // right + corners.push_back(bottom_right); + } + if (left_cost == COST_MIN and top_cost == COST_MIN) { + // top + corners.push_back(top_left); + } + } + } + else { + if (blocker.se < target.se) { + // top and left + if (top_cost == COST_MIN and left_cost == COST_MIN) { + // left + corners.push_back(top_left); + } + if (right_cost == COST_MIN and bottom_cost == COST_MIN) { + // bottom + corners.push_back(bottom_right); + } + } + else { + // bottom and left + if (bottom_cost == COST_MIN and left_cost == COST_MIN) { + // left + corners.push_back(bottom_left); + } + if (right_cost == COST_MIN and top_cost == COST_MIN) { + // top + corners.push_back(top_right); + } + } + } + } + + return corners; +} + +std::vector IntegrationField::bresenhams_line(const coord::tile &target, + int corner_x, + int corner_y) { + std::vector cells; + + // cell coordinates + // these have to be offset depending on the line direction + auto cell_x = corner_x; + auto cell_y = corner_y; + + // field edge boundary + int boundary = this->size; + + // target coordinates + // offset by 0.5 to get the center of the cell + double tx = target.ne + 0.5; + double ty = target.se + 0.5; + + // slope of the line + double dx = std::abs(tx - corner_x); + double dy = std::abs(ty - corner_y); + auto m = dy / dx; + + // error margin for the line + // if the error is greater than 1.0, we have to move in the y direction + auto error = m; + + // Check which direction the line is going + if (corner_x < tx) { + if (corner_y < ty) { // left and up + cell_y -= 1; + cell_x -= 1; + while (cell_x >= 0 and cell_y >= 0) { + cells.push_back(cell_x + cell_y * this->size); + if (error > 1.0) { + cell_y -= 1; + error -= 1.0; + } + else { + cell_x -= 1; + error += m; + } + } + } + + else { // left and down + cell_x -= 1; + while (cell_x >= 0 and cell_y < boundary) { + cells.push_back(cell_x + cell_y * this->size); + if (error > 1.0) { + cell_y += 1; + error -= 1.0; + } + else { + cell_x -= 1; + error += m; + } + } + } + } + else { + if (corner_y < ty) { // right and up + cell_y -= 1; + while (cell_x < boundary and cell_y >= 0) { + cells.push_back(cell_x + cell_y * this->size); + if (error > 1.0) { + cell_y -= 1; + error -= 1.0; + } + else { + cell_x += 1; + error += m; + } + } + } + else { // right and down + while (cell_x < boundary and cell_y < boundary) { + cells.push_back(cell_x + cell_y * this->size); + if (error > 1.0) { + cell_y += 1; + error -= 1.0; + } + else { + cell_x += 1; + error += m; + } + } + } + } + + return cells; +} + + +} // namespace openage::path diff --git a/libopenage/pathfinding/integration_field.h b/libopenage/pathfinding/integration_field.h new file mode 100644 index 0000000000..183fb9cff6 --- /dev/null +++ b/libopenage/pathfinding/integration_field.h @@ -0,0 +1,175 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include +#include + +#include "pathfinding/types.h" + + +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { +class CostField; +class Portal; + +/** + * Integration field in the flow-field pathfinding algorithm. + */ +class IntegrationField { +public: + /** + * Create a square integration field with a specified size. + * + * @param size Side length of the field. + */ + IntegrationField(size_t size); + + /** + * Get the size of the integration field. + * + * @return Size of the integration field. + */ + size_t get_size() const; + + /** + * Get the integration value at a specified position. + * + * @param pos Coordinates of the cell. + * @return Integration value at the specified position. + */ + const integrated_t &get_cell(const coord::tile &pos) const; + + /** + * Get the integration value at a specified position. + * + * @param idx Index of the cell. + * @return Integration value at the specified position. + */ + const integrated_t &get_cell(size_t idx) const; + + /** + * Calculate the line-of-sight integration flags for a target cell. + * + * Returns a list of cells that are flagged as "wavefront blocked". These cells + * can be used as a starting point for the cost integration. + * + * @param cost_field Cost field to integrate. + * @param target Coordinates of the target cell. + * + * @return Cells flagged as "wavefront blocked". + */ + std::vector integrate_los(const std::shared_ptr &cost_field, + const coord::tile &target); + + /** + * Calculate the cost integration field starting from a target cell. + * + * @param cost_field Cost field to integrate. + * @param target Coordinates of the target cell. + */ + void integrate_cost(const std::shared_ptr &cost_field, + const coord::tile &target); + + /** + * Calculate the cost integration field starting from a portal to another + * integration field. + * + * @param cost_field Cost field to integrate. + * @param other_sector_id Sector ID of the other integration field. + * @param portal Portal connecting the two fields. + */ + void integrate_cost(const std::shared_ptr &cost_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + + /** + * Calculate the cost integration field starting from a wavefront. + * + * @param cost_field Cost field to integrate. + * @param start_cells Cells flagged as "wavefront blocked" from a LOS pass. + */ + void integrate_cost(const std::shared_ptr &cost_field, + std::vector &&start_cells); + + /** + * Get the integration field values. + * + * @return Integration field values. + */ + const std::vector &get_cells() const; + + /** + * Reset the integration field for a new integration. + */ + void reset(); + +private: + /** + * Update a neigbor cell during the cost integration process. + * + * @param idx Index of the neighbor cell that is updated. + * @param cell_cost Cost of the neighbor cell from the cost field. + * @param integrated_cost Current integrated cost of the updating cell in the integration field. + * @param open_list List of cells to be updated. + * @param in_list Set of cells that have been updated. + * + * @return New integration value of the cell. + */ + void update_neighbor(size_t idx, + cost_t cell_cost, + integrated_cost_t integrated_cost, + std::deque &open_list, + std::unordered_set &in_list); + + /** + * Get the LOS corners around a cell. + * + * @param cost_field Cost field to integrate. + * @param target Cell coordinates of the target. + * @param blocker Cell coordinates of the cell blocking LOS. + * + * @return Field coordinates of the LOS corners. + */ + std::vector> get_los_corners(const std::shared_ptr &cost_field, + const coord::tile &target, + const coord::tile &blocker); + + /** + * Get the cells in a bresenham's line between the corner cell and the field edge. + * + * This function is a modified version of the bresenham's line algorithm that + * retrieves the cells between the corner point and the field's edge, rather than + * the cells between two arbitrary points. We do this because the intersection + * point with the field edge is unknown. + * + * @param target Cell coordinates of the target. + * @param corner_x X field coordinate edge of the LOS corner. + * @param corner_y Y field coordinate edge of the LOS corner. + * + * @return Cell indices of the LOS line. + */ + std::vector bresenhams_line(const coord::tile &target, + int corner_x, + int corner_y); + + /** + * Side length of the field. + */ + size_t size; + + /** + * Integration field values. + */ + std::vector cells; +}; + +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/integrator.cpp b/libopenage/pathfinding/integrator.cpp new file mode 100644 index 0000000000..28f8a829ca --- /dev/null +++ b/libopenage/pathfinding/integrator.cpp @@ -0,0 +1,66 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "integrator.h" + +#include "pathfinding/cost_field.h" +#include "pathfinding/flow_field.h" +#include "pathfinding/integration_field.h" + + +namespace openage::path { + +std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, + const coord::tile &target) { + auto integration_field = std::make_shared(cost_field->get_size()); + + auto wavefront_blocked = integration_field->integrate_los(cost_field, target); + integration_field->integrate_cost(cost_field, std::move(wavefront_blocked)); + + return integration_field; +} + +std::shared_ptr Integrator::integrate(const std::shared_ptr &cost_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + auto integration_field = std::make_shared(cost_field->get_size()); + integration_field->integrate_cost(cost_field, other_sector_id, portal); + + return integration_field; +} + +std::shared_ptr Integrator::build(const std::shared_ptr &integration_field) { + auto flow_field = std::make_shared(integration_field->get_size()); + flow_field->build(integration_field); + + return flow_field; +} + +std::shared_ptr Integrator::build(const std::shared_ptr &integration_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + auto flow_field = std::make_shared(integration_field->get_size()); + flow_field->build(integration_field, other, other_sector_id, portal); + + return flow_field; +} + +Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, + const coord::tile &target) { + auto integration_field = this->integrate(cost_field, target); + auto flow_field = this->build(integration_field); + + return std::make_pair(integration_field, flow_field); +} + +Integrator::build_return_t Integrator::build(const std::shared_ptr &cost_field, + const std::shared_ptr &other_integration_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal) { + auto integration_field = this->integrate(cost_field, other_sector_id, portal); + auto flow_field = this->build(integration_field, other_integration_field, other_sector_id, portal); + + return std::make_pair(integration_field, flow_field); +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/integrator.h b/libopenage/pathfinding/integrator.h new file mode 100644 index 0000000000..027f627274 --- /dev/null +++ b/libopenage/pathfinding/integrator.h @@ -0,0 +1,109 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "pathfinding/types.h" + + +namespace openage { +namespace coord { +struct tile; +} // namespace coord + +namespace path { +class CostField; +class FlowField; +class IntegrationField; +class Portal; + +/** + * Integrator for the flow field pathfinding algorithm. + */ +class Integrator { +public: + Integrator() = default; + ~Integrator() = default; + + /** + * Integrate the cost field for a target. + * + * @param cost_field Cost field. + * @param target Coordinates of the target cell. + * + * @return Integration field. + */ + std::shared_ptr integrate(const std::shared_ptr &cost_field, + const coord::tile &target); + + /** + * Integrate the cost field from a portal. + * + * @param cost_field Cost field. + * @param other_sector_id Sector ID of the other side of the portal. + * @param portal Portal. + * + * @return Integration field. + */ + std::shared_ptr integrate(const std::shared_ptr &cost_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + + /** + * Build the flow field from an integration field. + * + * @param integration_field Integration field. + * + * @return Flow field. + */ + std::shared_ptr build(const std::shared_ptr &integration_field); + + /** + * Build the flow field from a portal. + * + * @param integration_field Integration field. + * @param other Integration field of the other side of the portal. + * @param other_sector_id Sector ID of the other side of the portal. + * @param portal Portal. + * + * @return Flow field. + */ + std::shared_ptr build(const std::shared_ptr &integration_field, + const std::shared_ptr &other, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + + using build_return_t = std::pair, std::shared_ptr>; + + /** + * Build the integration field and flow field for a target. + * + * @param cost_field Cost field. + * @param target Coordinates of the target cell. + * + * @return Flow field. + */ + build_return_t build(const std::shared_ptr &cost_field, + const coord::tile &target); + + /** + * Build the integration field and flow field from a portal. + * + * @param cost_field Cost field. + * @param other_integration_field Integration field of the other side of the portal. + * @param other_sector_id Sector ID of the other side of the portal. + * @param portal Portal. + */ + build_return_t build(const std::shared_ptr &cost_field, + const std::shared_ptr &other_integration_field, + sector_id_t other_sector_id, + const std::shared_ptr &portal); + +private: + // TODO: Cache created flow fields. +}; + +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/legacy/CMakeLists.txt b/libopenage/pathfinding/legacy/CMakeLists.txt new file mode 100644 index 0000000000..ad828995c9 --- /dev/null +++ b/libopenage/pathfinding/legacy/CMakeLists.txt @@ -0,0 +1,6 @@ +add_sources(libopenage + a_star.cpp + heuristics.cpp + path.cpp + tests.cpp +) diff --git a/libopenage/pathfinding/a_star.cpp b/libopenage/pathfinding/legacy/a_star.cpp similarity index 86% rename from libopenage/pathfinding/a_star.cpp rename to libopenage/pathfinding/legacy/a_star.cpp index f80879eb91..afeabeeefd 100644 --- a/libopenage/pathfinding/a_star.cpp +++ b/libopenage/pathfinding/legacy/a_star.cpp @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. /** @file * @@ -14,15 +14,15 @@ #include -#include "../datastructure/pairing_heap.h" -#include "../log/log.h" -#include "../util/strings.h" -#include "heuristics.h" -#include "path.h" +#include "datastructure/pairing_heap.h" +#include "log/log.h" +#include "pathfinding/legacy/heuristics.h" +#include "pathfinding/legacy/path.h" +#include "util/strings.h" namespace openage { -namespace path { +namespace path::legacy { Path to_point(coord::phys3 start, @@ -31,7 +31,7 @@ Path to_point(coord::phys3 start, auto valid_end = [&](const coord::phys3 &point) -> bool { return euclidean_squared_cost(point, end) < path_grid_size.to_float(); }; - auto heuristic = [&](const coord::phys3 &point) -> cost_t { + auto heuristic = [&](const coord::phys3 &point) -> cost_old_t { return euclidean_cost(point, end); }; return a_star(start, valid_end, heuristic, passable); @@ -41,13 +41,13 @@ Path find_nearest(coord::phys3 start, std::function valid_end, std::function passable) { // Use Dijkstra (heuristic = 0) - auto zero = [](const coord::phys3 &) -> cost_t { return .0f; }; + auto zero = [](const coord::phys3 &) -> cost_old_t { return .0f; }; return a_star(start, valid_end, zero, passable); } Path a_star(coord::phys3 start, std::function valid_end, - std::function heuristic, + std::function heuristic, std::function passable) { // path node storage, always provides cheapest next node. heap_t node_candidates; @@ -94,7 +94,7 @@ Path a_star(coord::phys3 start, } bool not_visited = (visited_tiles.count(neighbor->position) == 0); - cost_t new_past_cost = best_candidate->past_cost + best_candidate->cost_to(*neighbor); + cost_old_t new_past_cost = best_candidate->past_cost + best_candidate->cost_to(*neighbor); // if new cost is better than the previous path if (not_visited or new_past_cost < neighbor->past_cost) { @@ -128,5 +128,5 @@ Path a_star(coord::phys3 start, } -} // namespace path +} // namespace path::legacy } // namespace openage diff --git a/libopenage/pathfinding/a_star.h b/libopenage/pathfinding/legacy/a_star.h similarity index 83% rename from libopenage/pathfinding/a_star.h rename to libopenage/pathfinding/legacy/a_star.h index 23d3e76747..a2704bb5f1 100644 --- a/libopenage/pathfinding/a_star.h +++ b/libopenage/pathfinding/legacy/a_star.h @@ -1,4 +1,4 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once @@ -9,7 +9,7 @@ namespace openage { -namespace path { +namespace path::legacy { /** * path between two static points @@ -35,8 +35,8 @@ Path find_nearest(coord::phys3 start, */ Path a_star(coord::phys3 start, std::function valid_end, - std::function heuristic, + std::function heuristic, std::function passable); -} // namespace path +} // namespace path::legacy } // namespace openage diff --git a/libopenage/pathfinding/legacy/heuristics.cpp b/libopenage/pathfinding/legacy/heuristics.cpp new file mode 100644 index 0000000000..e064fa1ad7 --- /dev/null +++ b/libopenage/pathfinding/legacy/heuristics.cpp @@ -0,0 +1,35 @@ +// Copyright 2014-2024 the openage authors. See copying.md for legal info. + +#include "heuristics.h" + +#include +#include + + +namespace openage { +namespace path::legacy { + +cost_old_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end) { + cost_old_t dx = std::abs(start.ne - end.ne).to_float(); + cost_old_t dy = std::abs(start.se - end.se).to_float(); + return dx + dy; +} + +cost_old_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end) { + cost_old_t dx = std::abs(start.ne - end.ne).to_float(); + cost_old_t dy = std::abs(start.se - end.se).to_float(); + return std::max(dx, dy); +} + +cost_old_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end) { + return (end - start).length(); +} + +cost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end) { + cost_old_t dx = (start.ne - end.ne).to_float(); + cost_old_t dy = (start.se - end.se).to_float(); + return dx * dx + dy * dy; +} + +} // namespace path::legacy +} // namespace openage diff --git a/libopenage/pathfinding/heuristics.h b/libopenage/pathfinding/legacy/heuristics.h similarity index 51% rename from libopenage/pathfinding/heuristics.h rename to libopenage/pathfinding/legacy/heuristics.h index 7755f1288a..77707fbc9f 100644 --- a/libopenage/pathfinding/heuristics.h +++ b/libopenage/pathfinding/legacy/heuristics.h @@ -1,45 +1,45 @@ -// Copyright 2014-2017 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once -#include "path.h" +#include "pathfinding/legacy/path.h" namespace openage { -namespace path { +namespace path::legacy { /** * function pointer type for distance estimation functions. */ -using heuristic_t = cost_t (*)(const coord::phys3 &start, const coord::phys3 &end); +using heuristic_t = cost_old_t (*)(const coord::phys3 &start, const coord::phys3 &end); /** * Manhattan distance cost estimation. * @returns the sum of the x and y difference. */ -cost_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t manhattan_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Chebyshev distance cost estimation. * @returns y or x difference, whichever is higher. */ -cost_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t chebyshev_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Euclidean distance cost estimation. * @returns the hypotenuse length of the rectangular triangle formed. */ -cost_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t euclidean_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Squared euclidean distance cost estimation. * @returns the square of the hypotenuse length of the rectangular triangle formed. */ -cost_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end); +cost_old_t euclidean_squared_cost(const coord::phys3 &start, const coord::phys3 &end); /** * Calculate euclidean distance from a already calculated squared euclidean distance */ -cost_t euclidean_squared_to_euclidean_cost(const cost_t euclidean_squared_value); +cost_old_t euclidean_squared_to_euclidean_cost(const cost_old_t euclidean_squared_value); -} // namespace path +} // namespace path::legacy } // namespace openage diff --git a/libopenage/pathfinding/legacy/path.cpp b/libopenage/pathfinding/legacy/path.cpp new file mode 100644 index 0000000000..dafb63e686 --- /dev/null +++ b/libopenage/pathfinding/legacy/path.cpp @@ -0,0 +1,115 @@ +// Copyright 2014-2024 the openage authors. See copying.md for legal info. + +#include + +#include "pathfinding/legacy/path.h" + +namespace openage::path::legacy { + + +bool compare_node_cost::operator()(const node_pt &lhs, const node_pt &rhs) const { + // TODO: use node operator < + return lhs->future_cost < rhs->future_cost; +} + + +Node::Node(const coord::phys3 &pos, node_pt prev) : + position(pos), + tile_position(pos.to_tile3().to_tile()), + direction{}, + visited{false}, + was_best{false}, + factor{1.0f}, + path_predecessor{prev}, + heap_node(nullptr) { + if (prev) { + this->direction = (this->position - prev->position).normalize(); + + // TODO: add dot product to coord + cost_old_t similarity = ((this->direction.ne.to_float() * prev->direction.ne.to_float()) + (this->direction.se.to_float() * prev->direction.se.to_float())); + this->factor += (1 - similarity); + } +} + + +Node::Node(const coord::phys3 &pos, node_pt prev, cost_old_t past, cost_old_t heuristic) : + Node{pos, prev} { + this->past_cost = past; + this->heuristic_cost = heuristic; + this->future_cost = past + heuristic; +} + + +bool Node::operator<(const Node &other) const { + return this->future_cost < other.future_cost; +} + + +bool Node::operator==(const Node &other) const { + return this->position == other.position; +} + + +cost_old_t Node::cost_to(const Node &other) const { + // ignore the up-position, thus convert to phys2 + return ((this->position - other.position).to_phys2().length() + * other.factor * this->factor); +} + + +Path Node::generate_backtrace() { + std::vector waypoints; + + node_pt current = this->shared_from_this(); + do { + Node other = *current; + waypoints.push_back(*current); + current = current->path_predecessor; + } + while (current != nullptr); + waypoints.pop_back(); // remove start + + return {waypoints}; +} + + +std::vector Node::get_neighbors(const nodemap_t &nodes, float scale) { + std::vector neighbors; + neighbors.reserve(8); + for (int n = 0; n < 8; ++n) { + coord::phys3 n_pos = this->position + (neigh_phys[n] * scale); + + if (nodes.count(n_pos) > 0) { + neighbors.push_back(nodes.at(n_pos)); + } + else { + neighbors.push_back(std::make_shared(n_pos, this->shared_from_this())); + } + } + return neighbors; +} + + +bool passable_line(node_pt start, node_pt end, std::function passable, float samples) { + // interpolate between points and make passablity checks + // (dont check starting position) + for (int i = 1; i <= samples; ++i) { + // TODO: needs more fixed-point + double percent = static_cast(i) / samples; + coord::phys_t ne = (1.0 - percent) * start->position.ne.to_double() + percent * end->position.ne.to_double(); + coord::phys_t se = (1.0 - percent) * start->position.se.to_double() + percent * end->position.se.to_double(); + coord::phys_t up = (1.0 - percent) * start->position.up.to_double() + percent * end->position.up.to_double(); + + if (!passable(coord::phys3{ne, se, up})) { + return false; + } + } + return true; +} + + +Path::Path(const std::vector &nodes) : + waypoints{nodes} {} + + +} // namespace openage::path::legacy diff --git a/libopenage/pathfinding/legacy/path.h b/libopenage/pathfinding/legacy/path.h new file mode 100644 index 0000000000..59851ecd51 --- /dev/null +++ b/libopenage/pathfinding/legacy/path.h @@ -0,0 +1,209 @@ +// Copyright 2014-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include +#include + +#include "coord/phys.h" +#include "coord/tile.h" +#include "datastructure/pairing_heap.h" +#include "util/hash.h" +#include "util/misc.h" + + +namespace openage { + +namespace path::legacy { + +class Node; +class Path; + +/** + * The data type for movement cost + */ +using cost_old_t = float; + +/** + * Type for storing navigation nodes. + */ +using node_pt = std::shared_ptr; + +/** + * Type for mapping tiles to nodes. + */ +using nodemap_t = std::unordered_map; + + +/** + * Cost comparison for node_pt. + * Extracts the ptr from the shared_ptr. + * Calls operator < on Node. + */ +struct compare_node_cost { + bool operator()(const node_pt &lhs, const node_pt &rhs) const; +}; + +/** + * Priority queue node item type. + */ +using heap_t = datastructure::PairingHeap; + +/** + * Size of phys-coord grid for path nodes. + */ +constexpr coord::phys_t path_grid_size{1.f / 8}; + +/** + * Phys3 delta coordinates to select for path neighbors. + */ +constexpr coord::phys3_delta const neigh_phys[] = { + {path_grid_size * 1, path_grid_size * -1, 0}, + {path_grid_size * 1, path_grid_size * 0, 0}, + {path_grid_size * 1, path_grid_size * 1, 0}, + {path_grid_size * 0, path_grid_size * 1, 0}, + {path_grid_size * -1, path_grid_size * 1, 0}, + {path_grid_size * -1, path_grid_size * 0, 0}, + {path_grid_size * -1, path_grid_size * -1, 0}, + {path_grid_size * 0, path_grid_size * -1, 0}}; + +/** + * + */ +bool passable_line(node_pt start, node_pt end, std::function passable, float samples = 5.0f); + +/** + * One navigation waypoint in a path. + */ +class Node : public std::enable_shared_from_this { +public: + Node(const coord::phys3 &pos, node_pt prev); + Node(const coord::phys3 &pos, node_pt prev, cost_old_t past, cost_old_t heuristic); + + /** + * Orders nodes according to their future cost value. + */ + bool operator<(const Node &other) const; + + /** + * Compare the node to another one. + * They are the same if their position is. + */ + bool operator==(const Node &other) const; + + /** + * Calculates the actual movement cose to another node. + */ + cost_old_t cost_to(const Node &other) const; + + /** + * Create a backtrace path beginning at this node. + */ + Path generate_backtrace(); + + /** + * Get all neighbors of this graph node. + */ + std::vector get_neighbors(const nodemap_t &, float scale = 1.0f); + + /** + * The tile position this node is associated to. + * todo make const + */ + coord::phys3 position; + coord::tile tile_position; + coord::phys3_delta direction; // for path smoothing + + /** + * Future cost estimation value for this node. + */ + cost_old_t future_cost; + + /** + * Evaluated past cost value for the node. + * This stores the actual cost from start to this node. + */ + cost_old_t past_cost; + + /** + * Heuristic cost cache. + * Calculated once, is the heuristic distance from this node + * to the goal. + */ + cost_old_t heuristic_cost; + + /** + * Can this node be passed? + */ + bool accessible = false; + + /** + * Has this Node been visited? + */ + bool visited = false; + + /** + * Does this node already have an alternative path? + * If the node was once selected as the best next hop, + * this is set to true. + */ + bool was_best = false; + + /** + * Factor to adjust movement cost. + * default: 1 + */ + cost_old_t factor; + + /** + * Node where this one was reached by least cost. + */ + node_pt path_predecessor; + + /** + * Priority queue node that contains this path node. + */ + heap_t::element_t heap_node; +}; + + +/** + * Represents a planned trajectory. + * Generated by pathfinding algorithms. + */ +class Path { +public: + Path() = default; + Path(const std::vector &nodes); + + /** + * These are the waypoints to navigate in order. + * Includes the start and end node. + */ + std::vector waypoints; +}; + +} // namespace path::legacy +} // namespace openage + + +namespace std { + +/** + * Hash function for path nodes. + * Just uses their position. + */ +template <> +struct hash { + size_t operator()(const openage::path::legacy::Node &x) const { + openage::coord::phys3 node_pos = x.position; + size_t hash = openage::util::type_hash(); + hash = openage::util::hash_combine(hash, std::hash{}(node_pos.ne)); + hash = openage::util::hash_combine(hash, std::hash{}(node_pos.se)); + return hash; + } +}; + +} // namespace std diff --git a/libopenage/pathfinding/path_utils.h b/libopenage/pathfinding/legacy/path_utils.h similarity index 50% rename from libopenage/pathfinding/path_utils.h rename to libopenage/pathfinding/legacy/path_utils.h index c4f4b6fcca..2c87923ad4 100644 --- a/libopenage/pathfinding/path_utils.h +++ b/libopenage/pathfinding/legacy/path_utils.h @@ -1,9 +1,9 @@ -// Copyright 2014-2016 the openage authors. See copying.md for legal info. +// Copyright 2014-2024 the openage authors. See copying.md for legal info. #pragma once namespace openage { -namespace path { +namespace path::legacy { } // namespace path } // namespace openage diff --git a/libopenage/pathfinding/legacy/tests.cpp b/libopenage/pathfinding/legacy/tests.cpp new file mode 100644 index 0000000000..b160eff91a --- /dev/null +++ b/libopenage/pathfinding/legacy/tests.cpp @@ -0,0 +1,317 @@ +// Copyright 2015-2024 the openage authors. See copying.md for legal info. + +#include "log/log.h" +#include "testing/testing.h" + +#include "pathfinding/legacy/heuristics.h" +#include "pathfinding/legacy/path.h" + +namespace openage { +namespace path { +namespace tests { + +/** + * This function tests setting up basic nodes that point to a previous node. + * Tests that direction is set correctly and that factor is set correctly. + */ +void node_0() { + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{1, 0, 0}; + coord::phys3 p2{1, 1, 0}; + coord::phys3 p3{1, -1, 0}; + coord::phys3 p4{2, 0, 0}; + coord::phys3 p5{2, 2, 0}; + coord::phys3 p6{2, -2, 0}; + + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); + legacy::node_pt n2 = std::make_unique(p2, n1); + legacy::node_pt n3 = std::make_unique(p3, n1); + legacy::node_pt n4 = std::make_unique(p0, n1); + + // Testing how the factor is effected from the change in + // direction from one node to another + TESTEQUALS(n1->direction.ne, 1); + TESTEQUALS(n1->direction.se, 0); + + // Expect this to be 2 since the similarity between nodes is zero + TESTEQUALS(n1->factor, 2); + + TESTEQUALS(n2->direction.ne, 0); + TESTEQUALS(n2->direction.se, 1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n2->factor, 2); + + TESTEQUALS(n3->direction.ne, 0); + TESTEQUALS(n3->direction.se, -1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n3->factor, 2); + + TESTEQUALS(n4->direction.ne, -1); + TESTEQUALS(n4->direction.se, 0); + + // Expect this to be 3 since it takes a 180 degree turn from n1 + TESTEQUALS(n4->factor, 3); + + // Testing that the distance from the previous node noes not + // effect the factor, only change in direction + + n1 = std::make_unique(p4, n0); + n2 = std::make_unique(p5, n1); + n3 = std::make_unique(p6, n1); + n4 = std::make_unique(p0, n1); + + TESTEQUALS(n1->direction.ne, 1); + TESTEQUALS(n1->direction.se, 0); + + // Expect this to be 2 since the similarity between nodes is zero + TESTEQUALS(n1->factor, 2); + TESTEQUALS(n2->direction.ne, 0); + TESTEQUALS(n2->direction.se, 1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n2->factor, 2); + TESTEQUALS(n3->direction.ne, 0); + TESTEQUALS(n3->direction.se, -1); + + // Expect this to be 2 since it takes a 90 degree turn from n1 + TESTEQUALS(n3->factor, 2); + TESTEQUALS(n4->direction.ne, -1); + TESTEQUALS(n4->direction.se, 0); + + // Expect this to be 3 since it takes a 180 degree turn from n1 + TESTEQUALS(n4->factor, 3); +} + +/** + * This function tests Node->cost_to. The testing is done on 2 unrelated + * nodes (They have no previous node) to test the basic cost without adding + * the cost from node->factor. + */ +void node_cost_to_0() { + // Testing basic cost_to with ne only + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{10, 0, 0}; + + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, nullptr); + + TESTEQUALS(n0->cost_to(*n1), 10); + TESTEQUALS(n1->cost_to(*n0), 10); + + // Testing basic cost_to with se only + coord::phys3 p2{0, 5, 0}; + + legacy::node_pt n2 = std::make_unique(p2, nullptr); + + TESTEQUALS(n0->cost_to(*n2), 5); + TESTEQUALS(n2->cost_to(*n0), 5); + + // Testing cost_to with both se and ne: + coord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5 + + legacy::node_pt n3 = std::make_unique(p3, nullptr); + TESTEQUALS(n0->cost_to(*n3), 5); + TESTEQUALS(n3->cost_to(*n0), 5); + + // Test cost_to and check that `up` has no effect + coord::phys3 p4{3, 4, 8}; + + legacy::node_pt n4 = std::make_unique(p4, nullptr); + + TESTEQUALS(n0->cost_to(*n4), 5); + TESTEQUALS(n4->cost_to(*n0), 5); +} + +/** + * This function tests Node->cost_to. The testing is done on the neighbor + * nodes to test how the directional factor effects the cost. + */ +void node_cost_to_1() { + // Set up coords so that n1 will have a direction of ne = 1 + // but n0 with not be in n1s neighbors + coord::phys3 p0{-0.125, 0, 0}; + coord::phys3 p1{0.125, 0, 0}; + + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); + + // We expect twice the normal cost since n0 had not direction + // thus we get a factor of 2 on n1 + TESTEQUALS_FLOAT(n0->cost_to(*n1), 0.5, 0.001); + TESTEQUALS_FLOAT(n1->cost_to(*n0), 0.5, 0.001); + + legacy::nodemap_t visited_tiles; + visited_tiles[n0->position] = n0; + + // Collect the costs to go to all the neighbors of n1 + std::vector costs; + for (legacy::node_pt neighbor : n1->get_neighbors(visited_tiles, 1)) { + costs.push_back(n1->cost_to(*neighbor)); + } + + TESTEQUALS_FLOAT(costs[0], 0.45711, 0.001); + TESTEQUALS_FLOAT(costs[1], 0.25, 0.001); + TESTEQUALS_FLOAT(costs[2], 0.45711, 0.001); + TESTEQUALS_FLOAT(costs[3], 0.5, 0.001); + TESTEQUALS_FLOAT(costs[4], 0.95709, 0.001); + TESTEQUALS_FLOAT(costs[5], 0.75, 0.001); + TESTEQUALS_FLOAT(costs[6], 0.95709, 0.001); + TESTEQUALS_FLOAT(costs[7], 0.5, 0.001); +} + +/** + * This function does a basic test of generating a backtrace from the + * last node in a path. + */ +void node_generate_backtrace_0() { + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{10, 0, 0}; + coord::phys3 p2{20, 0, 0}; + coord::phys3 p3{30, 0, 0}; + + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); + legacy::node_pt n2 = std::make_unique(p2, n1); + legacy::node_pt n3 = std::make_unique(p3, n2); + + legacy::Path path = n3->generate_backtrace(); + + (path.waypoints[0] == *n3) or TESTFAIL; + (path.waypoints[1] == *n2) or TESTFAIL; + (path.waypoints[2] == *n1) or TESTFAIL; +} + +/** + * This function tests Node->get_neighbors and how the scale effects + * the neighbors given. + */ +void node_get_neighbors_0() { + coord::phys3 p0{0, 0, 0}; + + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::nodemap_t map; + + // Testing get_neighbors returning all surounding tiles with + // a factor of 1 + + std::vector neighbors = n0->get_neighbors(map, 1); + TESTEQUALS(neighbors.size(), 8); + + TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.125, 0.001); + TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.125, 0.001); + + TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.125, 0.001); + + // Testing how a larger scale changes the neighbors generated + neighbors = n0->get_neighbors(map, 2); + TESTEQUALS(neighbors.size(), 8); + + TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); + + TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.25, 0.001); + TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.25, 0.001); + + TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); + TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.25, 0.001); +} + +/** + * This is a helper passable function that alwalys returns true. + */ +bool always_passable(const coord::phys3 &) { + return true; +} + +/** + * This is a helper passable function that always returns false. + */ +bool not_passable(const coord::phys3 &) { + return false; +} + +/** + * This is a helper passable function that only returns true when + * pos.ne == 20. + */ +bool sometimes_passable(const coord::phys3 &pos) { + if (pos.ne == 20) { + return false; + } + else { + return true; + } +} + +/** + * This function tests passable_line. Tests with always false, always true, + * and position dependant functions being passed in as args. + */ +void node_passable_line_0() { + coord::phys3 p0{0, 0, 0}; + coord::phys3 p1{1000, 0, 0}; + + legacy::node_pt n0 = std::make_unique(p0, nullptr); + legacy::node_pt n1 = std::make_unique(p1, n0); + + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::always_passable), true); + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::not_passable), false); + + // The next 2 cases show that a different sample can change the results + // for the same path + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::sometimes_passable, 10), true); + TESTEQUALS(path::legacy::passable_line(n0, n1, path::tests::sometimes_passable, 50), false); +} + +/** + * Top level node test. + */ +void path_node() { + node_0(); + node_cost_to_0(); + node_cost_to_1(); + node_generate_backtrace_0(); + node_get_neighbors_0(); + node_passable_line_0(); +} + +} // namespace tests +} // namespace path +} // namespace openage diff --git a/libopenage/pathfinding/path.cpp b/libopenage/pathfinding/path.cpp index f1da51af78..fe13989e77 100644 --- a/libopenage/pathfinding/path.cpp +++ b/libopenage/pathfinding/path.cpp @@ -1,115 +1,10 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. - -#include +// Copyright 2024-2024 the openage authors. See copying.md for legal info. #include "path.h" -namespace openage::path { - - -bool compare_node_cost::operator()(const node_pt &lhs, const node_pt &rhs) const { - // TODO: use node operator < - return lhs->future_cost < rhs->future_cost; -} - - -Node::Node(const coord::phys3 &pos, node_pt prev) : - position(pos), - tile_position(pos.to_tile3().to_tile()), - direction{}, - visited{false}, - was_best{false}, - factor{1.0f}, - path_predecessor{prev}, - heap_node(nullptr) { - if (prev) { - this->direction = (this->position - prev->position).normalize(); - - // TODO: add dot product to coord - cost_t similarity = ((this->direction.ne.to_float() * prev->direction.ne.to_float()) + (this->direction.se.to_float() * prev->direction.se.to_float())); - this->factor += (1 - similarity); - } -} - - -Node::Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic) : - Node{pos, prev} { - this->past_cost = past; - this->heuristic_cost = heuristic; - this->future_cost = past + heuristic; -} - - -bool Node::operator<(const Node &other) const { - return this->future_cost < other.future_cost; -} - - -bool Node::operator==(const Node &other) const { - return this->position == other.position; -} - -cost_t Node::cost_to(const Node &other) const { - // ignore the up-position, thus convert to phys2 - return ((this->position - other.position).to_phys2().length() - * other.factor * this->factor); -} - - -Path Node::generate_backtrace() { - std::vector waypoints; - - node_pt current = this->shared_from_this(); - do { - Node other = *current; - waypoints.push_back(*current); - current = current->path_predecessor; - } - while (current != nullptr); - waypoints.pop_back(); // remove start - - return {waypoints}; -} - - -std::vector Node::get_neighbors(const nodemap_t &nodes, float scale) { - std::vector neighbors; - neighbors.reserve(8); - for (int n = 0; n < 8; ++n) { - coord::phys3 n_pos = this->position + (neigh_phys[n] * scale); - - if (nodes.count(n_pos) > 0) { - neighbors.push_back(nodes.at(n_pos)); - } - else { - neighbors.push_back(std::make_shared(n_pos, this->shared_from_this())); - } - } - return neighbors; -} - - -bool passable_line(node_pt start, node_pt end, std::function passable, float samples) { - // interpolate between points and make passablity checks - // (dont check starting position) - for (int i = 1; i <= samples; ++i) { - // TODO: needs more fixed-point - double percent = static_cast(i) / samples; - coord::phys_t ne = (1.0 - percent) * start->position.ne.to_double() + percent * end->position.ne.to_double(); - coord::phys_t se = (1.0 - percent) * start->position.se.to_double() + percent * end->position.se.to_double(); - coord::phys_t up = (1.0 - percent) * start->position.up.to_double() + percent * end->position.up.to_double(); - - if (!passable(coord::phys3{ne, se, up})) { - return false; - } - } - return true; -} - - -Path::Path(const std::vector &nodes) : - waypoints{nodes} {} +namespace openage::path { +// this file is intentionally empty } // namespace openage::path diff --git a/libopenage/pathfinding/path.h b/libopenage/pathfinding/path.h index e41ddbcc28..8d64f5914f 100644 --- a/libopenage/pathfinding/path.h +++ b/libopenage/pathfinding/path.h @@ -1,209 +1,36 @@ -// Copyright 2014-2023 the openage authors. See copying.md for legal info. +// Copyright 2024-2024 the openage authors. See copying.md for legal info. #pragma once -#include -#include -#include #include -#include "../coord/phys.h" -#include "../coord/tile.h" -#include "../datastructure/pairing_heap.h" -#include "../util/hash.h" -#include "../util/misc.h" +#include "coord/tile.h" -namespace openage { - -namespace path { - -class Node; -class Path; - -/** - * The data type for movement cost - */ -using cost_t = float; - -/** - * Type for storing navigation nodes. - */ -using node_pt = std::shared_ptr; - -/** - * Type for mapping tiles to nodes. - */ -using nodemap_t = std::unordered_map; - - -/** - * Cost comparison for node_pt. - * Extracts the ptr from the shared_ptr. - * Calls operator < on Node. - */ -struct compare_node_cost { - bool operator()(const node_pt &lhs, const node_pt &rhs) const; -}; - -/** - * Priority queue node item type. - */ -using heap_t = datastructure::PairingHeap; - -/** - * Size of phys-coord grid for path nodes. - */ -constexpr coord::phys_t path_grid_size{1.f / 8}; - -/** - * Phys3 delta coordinates to select for path neighbors. - */ -constexpr coord::phys3_delta const neigh_phys[] = { - {path_grid_size * 1, path_grid_size * -1, 0}, - {path_grid_size * 1, path_grid_size * 0, 0}, - {path_grid_size * 1, path_grid_size * 1, 0}, - {path_grid_size * 0, path_grid_size * 1, 0}, - {path_grid_size * -1, path_grid_size * 1, 0}, - {path_grid_size * -1, path_grid_size * 0, 0}, - {path_grid_size * -1, path_grid_size * -1, 0}, - {path_grid_size * 0, path_grid_size * -1, 0}}; - -/** - * - */ -bool passable_line(node_pt start, node_pt end, std::function passable, float samples = 5.0f); - -/** - * One navigation waypoint in a path. - */ -class Node : public std::enable_shared_from_this { -public: - Node(const coord::phys3 &pos, node_pt prev); - Node(const coord::phys3 &pos, node_pt prev, cost_t past, cost_t heuristic); - - /** - * Orders nodes according to their future cost value. - */ - bool operator<(const Node &other) const; - - /** - * Compare the node to another one. - * They are the same if their position is. - */ - bool operator==(const Node &other) const; - - /** - * Calculates the actual movement cose to another node. - */ - cost_t cost_to(const Node &other) const; - - /** - * Create a backtrace path beginning at this node. - */ - Path generate_backtrace(); - - /** - * Get all neighbors of this graph node. - */ - std::vector get_neighbors(const nodemap_t &, float scale = 1.0f); - - /** - * The tile position this node is associated to. - * todo make const - */ - coord::phys3 position; - coord::tile tile_position; - coord::phys3_delta direction; // for path smoothing - - /** - * Future cost estimation value for this node. - */ - cost_t future_cost; - - /** - * Evaluated past cost value for the node. - * This stores the actual cost from start to this node. - */ - cost_t past_cost; - - /** - * Heuristic cost cache. - * Calculated once, is the heuristic distance from this node - * to the goal. - */ - cost_t heuristic_cost; - - /** - * Can this node be passed? - */ - bool accessible = false; - - /** - * Has this Node been visited? - */ - bool visited = false; - - /** - * Does this node already have an alternative path? - * If the node was once selected as the best next hop, - * this is set to true. - */ - bool was_best = false; - - /** - * Factor to adjust movement cost. - * default: 1 - */ - cost_t factor; - - /** - * Node where this one was reached by least cost. - */ - node_pt path_predecessor; - - /** - * Priority queue node that contains this path node. - */ - heap_t::element_t heap_node; -}; - +namespace openage::path { /** - * Represents a planned trajectory. - * Generated by pathfinding algorithms. + * Path request for the pathfinder. */ -class Path { -public: - Path() = default; - Path(const std::vector &nodes); - - /** - * These are the waypoints to navigate in order. - * Includes the start and end node. - */ - std::vector waypoints; +struct PathRequest { + // ID of the grid to use for pathfinding. + size_t grid_id; + // Start position of the path. + coord::tile start; + // Target position of the path. + coord::tile target; }; -} // namespace path -} // namespace openage - - -namespace std { - /** - * Hash function for path nodes. - * Just uses their position. + * Path found by the pathfinder. */ -template <> -struct hash { - size_t operator()(const openage::path::Node &x) const { - openage::coord::phys3 node_pos = x.position; - size_t hash = openage::util::type_hash(); - hash = openage::util::hash_combine(hash, std::hash{}(node_pos.ne)); - hash = openage::util::hash_combine(hash, std::hash{}(node_pos.se)); - return hash; - } +struct Path { + // ID of the grid to used for pathfinding. + size_t grid_id; + // Waypoints of the path. + // First waypoint is the start position of the path request. + // Last waypoint is the target position of the path request. + std::vector waypoints; }; -} // namespace std +} // namespace openage::path diff --git a/libopenage/pathfinding/pathfinder.cpp b/libopenage/pathfinding/pathfinder.cpp new file mode 100644 index 0000000000..7e09278f7c --- /dev/null +++ b/libopenage/pathfinding/pathfinder.cpp @@ -0,0 +1,417 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "pathfinder.h" + +#include "coord/phys.h" +#include "pathfinding/flow_field.h" +#include "pathfinding/grid.h" +#include "pathfinding/integrator.h" +#include "pathfinding/portal.h" +#include "pathfinding/sector.h" + + +namespace openage::path { + +Pathfinder::Pathfinder() : + grids{}, + integrator{std::make_shared()} { +} + +const Path Pathfinder::get_path(const PathRequest &request) { + // High-level pathfinding + // Find the portals to use to get from the start to the target + auto portal_path = this->portal_a_star(request); + + // Low-level pathfinding + // Find the path within the sectors + auto grid = this->grids.at(request.grid_id); + auto sector_size = grid->get_sector_size(); + + auto target_sector_x = request.target.ne / sector_size; + auto target_sector_y = request.target.se / sector_size; + auto target_sector = grid->get_sector(target_sector_x, target_sector_y); + + coord::tile_t target_x = request.target.ne % sector_size; + coord::tile_t target_y = request.target.se % sector_size; + auto target = coord::tile{target_x, target_y}; + + auto sector_fields = this->integrator->build(target_sector->get_cost_field(), target); + auto prev_integration_field = sector_fields.first; + auto prev_sector_id = target_sector->get_id(); + + std::vector>> flow_fields; + flow_fields.reserve(portal_path.size() + 1); + + flow_fields.push_back(std::make_pair(target_sector->get_id(), sector_fields.second)); + for (auto &portal : portal_path) { + auto prev_sector = grid->get_sector(prev_sector_id); + auto next_sector_id = portal->get_exit_sector(prev_sector_id); + auto next_sector = grid->get_sector(next_sector_id); + + sector_fields = this->integrator->build(next_sector->get_cost_field(), + prev_integration_field, + prev_sector_id, + portal); + flow_fields.push_back(std::make_pair(next_sector_id, sector_fields.second)); + + prev_integration_field = sector_fields.first; + prev_sector_id = next_sector_id; + } + + // reverse the flow fields so they are ordered from start to target + std::reverse(flow_fields.begin(), flow_fields.end()); + + // traverse the flow fields to get the waypoints + std::vector waypoints{request.start}; + auto flow_field_waypoints = this->get_waypoints(flow_fields, request); + waypoints.insert(waypoints.end(), flow_field_waypoints.begin(), flow_field_waypoints.end()); + + return Path{request.grid_id, waypoints}; +} + +const std::shared_ptr &Pathfinder::get_grid(grid_id_t id) const { + return this->grids.at(id); +} + +void Pathfinder::add_grid(const std::shared_ptr &grid) { + this->grids[grid->get_id()] = grid; +} + +const std::vector> Pathfinder::portal_a_star(const PathRequest &request) const { + std::vector> result; + + auto grid = this->grids.at(request.grid_id); + auto sector_size = grid->get_sector_size(); + + auto start_sector_x = request.start.ne / sector_size; + auto start_sector_y = request.start.se / sector_size; + auto start_sector = grid->get_sector(start_sector_x, start_sector_y); + + auto target_sector_x = request.target.ne / sector_size; + auto target_sector_y = request.target.se / sector_size; + auto target_sector = grid->get_sector(target_sector_x, target_sector_y); + + if (start_sector == target_sector) { + // exit early if the start and target are in the same sector + return result; + } + + // path node storage, always provides cheapest next node. + heap_t node_candidates; + + // list of known portals and corresponding node. + nodemap_t visited_portals; + + // Cost to travel from one portal to another + // TODO: Determine this cost for each portal + // const int distance_cost = 1; + + // start nodes: all portals in the start sector + for (auto &portal : start_sector->get_portals()) { + auto portal_node = std::make_shared(portal, start_sector->get_id(), nullptr); + + auto sector_pos = grid->get_sector(portal->get_exit_sector(start_sector->get_id()))->get_position(); + auto portal_pos = portal->get_exit_center(start_sector->get_id()); + auto portal_abs_pos = portal_pos + coord::tile_delta(sector_pos.ne * sector_size, sector_pos.se * sector_size); + auto heuristic_cost = Pathfinder::heuristic_cost(portal_abs_pos, request.target); + + portal_node->current_cost = 0; + portal_node->heuristic_cost = heuristic_cost; + portal_node->future_cost = portal_node->current_cost + heuristic_cost; + + portal_node->heap_node = node_candidates.push(portal_node); + visited_portals[portal->get_id()] = portal_node; + } + + // track the closest we can get to the end position + // used when no path is found + auto closest_node = node_candidates.top(); + + // while there are candidates to visit + while (not node_candidates.empty()) { + auto current_node = node_candidates.pop(); + + current_node->was_best = true; + + // check if the current node is the target + if (current_node->portal->get_exit_sector(current_node->entry_sector) == target_sector->get_id()) { + auto backtrace = current_node->generate_backtrace(); + for (auto &node : backtrace) { + result.push_back(node->portal); + } + return result; + } + + // check if the current node is the closest to the target + if (current_node->heuristic_cost < closest_node->heuristic_cost) { + closest_node = current_node; + } + + // get the exits of the current node + auto exits = current_node->get_exits(visited_portals, current_node->entry_sector); + + // evaluate all neighbors of the current candidate for further progress + for (auto &exit : exits) { + if (exit->was_best) { + continue; + } + + // Get distance cost from current node to exit + auto distance_cost = Pathfinder::distance_cost( + current_node->portal->get_exit_center(current_node->entry_sector), + exit->portal->get_entry_center(exit->entry_sector)); + + bool not_visited = !visited_portals.contains(exit->portal->get_id()); + auto tentative_cost = current_node->current_cost + distance_cost; + + if (not_visited or tentative_cost < exit->current_cost) { + if (not_visited) { + // calculate the heuristic cost + exit->heuristic_cost = Pathfinder::heuristic_cost( + exit->portal->get_exit_center(exit->entry_sector), + request.target); + } + + // update the cost knowledge + exit->current_cost = tentative_cost; + exit->future_cost = exit->current_cost + exit->heuristic_cost; + exit->prev_portal = current_node; + + if (not_visited) { + exit->heap_node = node_candidates.push(exit); + visited_portals[exit->portal->get_id()] = exit; + } + else { + node_candidates.decrease(exit->heap_node); + } + } + } + } + + // no path found, return the closest node + auto backtrace = closest_node->generate_backtrace(); + for (auto &node : backtrace) { + result.push_back(node->portal); + } + + return result; +} + +const std::vector Pathfinder::get_waypoints(const std::vector>> &flow_fields, + const PathRequest &request) const { + ENSURE(flow_fields.size() > 0, "At least 1 flow field is required for finding waypoints."); + + std::vector waypoints; + + auto grid = this->get_grid(request.grid_id); + auto sector_size = grid->get_sector_size(); + coord::tile_t start_x = request.start.ne % sector_size; + coord::tile_t start_y = request.start.se % sector_size; + coord::tile_t target_x = request.target.ne % sector_size; + coord::tile_t target_y = request.target.se % sector_size; + + coord::tile_t current_x = start_x; + coord::tile_t current_y = start_y; + flow_dir_t current_direction = flow_fields.at(0).second->get_dir(coord::tile{current_x, current_y}); + for (size_t i = 0; i < flow_fields.size(); ++i) { + auto sector = grid->get_sector(flow_fields.at(i).first); + auto flow_field = flow_fields.at(i).second; + + // navigate the flow field vectors until we reach its edge (or the target) + while (current_x < static_cast(sector_size) + and current_y < static_cast(sector_size) + and current_x >= 0 + and current_y >= 0) { + auto cell = flow_field->get_cell(coord::tile{current_x, current_y}); + if (cell & FLOW_LOS_MASK) { + // check if we reached an LOS cell + auto sector_pos = sector->get_position(); + auto cell_pos = coord::tile(sector_pos.ne * sector_size, + sector_pos.se * sector_size) + + coord::tile_delta(current_x, current_y); + waypoints.push_back(cell_pos); + break; + } + + // check if we need to change direction + auto cell_direction = flow_field->get_dir(coord::tile(current_x, current_y)); + if (cell_direction != current_direction) { + // add the current cell as a waypoint + auto sector_pos = sector->get_position(); + auto cell_pos = coord::tile(sector_pos.ne * sector_size, + sector_pos.se * sector_size) + + coord::tile_delta(current_x, current_y); + waypoints.push_back(cell_pos); + current_direction = cell_direction; + } + + // move to the next cell + switch (current_direction) { + case flow_dir_t::NORTH: + current_y -= 1; + break; + case flow_dir_t::NORTH_EAST: + current_x += 1; + current_y -= 1; + break; + case flow_dir_t::EAST: + current_x += 1; + break; + case flow_dir_t::SOUTH_EAST: + current_x += 1; + current_y += 1; + break; + case flow_dir_t::SOUTH: + current_y += 1; + break; + case flow_dir_t::SOUTH_WEST: + current_x -= 1; + current_y += 1; + break; + case flow_dir_t::WEST: + current_x -= 1; + break; + case flow_dir_t::NORTH_WEST: + current_x -= 1; + current_y -= 1; + break; + default: + throw Error{ERR << "Invalid flow direction: " << static_cast(current_direction)}; + } + } + + // reset the current position for the next flow field + switch (current_direction) { + case flow_dir_t::NORTH: + current_y = sector_size - 1; + break; + case flow_dir_t::NORTH_EAST: + current_x = current_x + 1; + current_y = sector_size - 1; + break; + case flow_dir_t::EAST: + current_x = 0; + break; + case flow_dir_t::SOUTH_EAST: + current_x = 0; + current_y = current_y + 1; + break; + case flow_dir_t::SOUTH: + current_y = 0; + break; + case flow_dir_t::SOUTH_WEST: + current_x = current_x - 1; + current_y = 0; + break; + case flow_dir_t::WEST: + current_x = sector_size - 1; + break; + case flow_dir_t::NORTH_WEST: + current_x = sector_size - 1; + current_y = current_y - 1; + break; + default: + throw Error{ERR << "Invalid flow direction: " << static_cast(current_direction)}; + } + } + + // add the target position as the last waypoint + auto sector_pos = grid->get_sector(flow_fields.back().first)->get_position(); + auto target_pos = coord::tile(sector_pos.ne * sector_size, sector_pos.se * sector_size) + + coord::tile_delta{target_x, target_y}; + waypoints.push_back(target_pos); + + return waypoints; +} + +int Pathfinder::heuristic_cost(const coord::tile &portal_pos, + const coord::tile &target_pos) { + auto portal_phys_pos = portal_pos.to_phys2(); + auto target_phys_pos = target_pos.to_phys2(); + auto delta = target_phys_pos - portal_phys_pos; + + return delta.length(); +} + +int Pathfinder::distance_cost(const coord::tile &portal1_pos, + const coord::tile &portal2_pos) { + auto delta = portal2_pos.to_phys2() - portal1_pos.to_phys2(); + + return delta.length(); +} + + +PortalNode::PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal) : + portal{portal}, + entry_sector{entry_sector}, + future_cost{std::numeric_limits::max()}, + current_cost{std::numeric_limits::max()}, + heuristic_cost{std::numeric_limits::max()}, + was_best{false}, + prev_portal{prev_portal}, + heap_node{nullptr} {} + +PortalNode::PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal, + int past_cost, + int heuristic_cost) : + portal{portal}, + entry_sector{entry_sector}, + future_cost{past_cost + heuristic_cost}, + current_cost{past_cost}, + heuristic_cost{heuristic_cost}, + was_best{false}, + prev_portal{prev_portal}, + heap_node{nullptr} { +} + +bool PortalNode::operator<(const PortalNode &other) const { + return this->future_cost < other.future_cost; +} + +bool PortalNode::operator==(const PortalNode &other) const { + return this->portal->get_id() == other.portal->get_id(); +} + +std::vector PortalNode::generate_backtrace() { + std::vector waypoints; + + node_t current = this->shared_from_this(); + do { + waypoints.push_back(current); + current = current->prev_portal; + } + while (current != nullptr); + + return waypoints; +} + +std::vector PortalNode::get_exits(const nodemap_t &nodes, + sector_id_t entry_sector) { + std::vector exits; + + auto exit_sector = this->portal->get_exit_sector(entry_sector); + for (auto &exit : this->portal->get_exits(entry_sector)) { + auto exit_id = exit->get_id(); + + if (nodes.contains(exit_id)) { + exits.push_back(nodes.at(exit_id)); + } + else { + exits.push_back(std::make_shared(exit, + exit_sector, + this->shared_from_this())); + } + } + return exits; +} + + +bool compare_node_cost::operator()(const node_t &lhs, const node_t &rhs) const { + return *lhs < *rhs; +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/pathfinder.h b/libopenage/pathfinding/pathfinder.h new file mode 100644 index 0000000000..a4df0350d8 --- /dev/null +++ b/libopenage/pathfinding/pathfinder.h @@ -0,0 +1,224 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + +#include "coord/tile.h" +#include "datastructure/pairing_heap.h" +#include "pathfinding/path.h" +#include "pathfinding/types.h" + + +namespace openage::path { +class Grid; +class Integrator; +class Portal; +class FlowField; + +/** + * Pathfinder for flow field pathfinding. + * + * The pathfinder manages the grids defining the pathable ingame areas and + * provides an interface for making pathfinding requests. + * + * Pathfinding consists of a multi-step process: First, there is a high-level + * search using A* to identify the sectors of the grid that should be traversed. + * Afterwards, flow fields are calculated from the target sector to the start + * sector, which are then used to guide the actual unit movement. + */ +class Pathfinder { +public: + /** + * Create a new pathfinder. + */ + Pathfinder(); + ~Pathfinder() = default; + + /** + * Get the grid at a specified index. + * + * @param id ID of the grid. + * + * @return Pathfinding grid. + */ + const std::shared_ptr &get_grid(grid_id_t id) const; + + /** + * Add a grid to the pathfinder. + * + * @param grid Grid to add. + */ + void add_grid(const std::shared_ptr &grid); + + /** + * Get the path for a pathfinding request. + * + * @param request Pathfinding request. + * + * @return Path found by the pathfinder. + */ + const Path get_path(const PathRequest &request); + +private: + /** + * High-level pathfinder. Uses A* to find the path through the portals of sectors. + * + * @param request Pathfinding request. + * + * @return Portals to traverse in order to reach the target. + */ + const std::vector> portal_a_star(const PathRequest &request) const; + + /** + * Low-level pathfinder. Uses flow fields to find the path through the sectors. + * + * @param flow_fields Flow fields for the sectors. + * @param request Pathfinding request. + * + * @return Waypoint coordinates to traverse in order to reach the target. + */ + const std::vector get_waypoints(const std::vector>> &flow_fields, + const PathRequest &request) const; + + /** + * Calculate the heuristic cost between a portal and a target cell. + * + * @param portal_pos Position of the portal. This should be the center of the portal exit. + * @param target_pos Position of the target cell. + * + * @return Heuristic cost between the cells. + */ + static int heuristic_cost(const coord::tile &portal_pos, const coord::tile &target_pos); + + /** + * Calculate the distance cost between two portals. + * + * @param portal1_pos Center of the first portal. + * @param portal2_pos Center of the second portal. + * + * @return Distance cost between the portal centers. + */ + static int distance_cost(const coord::tile &portal1_pos, const coord::tile &portal2_pos); + + /** + * Grids managed by this pathfinder. + * + * Each grid can have separate pathing. + */ + std::unordered_map> grids; + + /** + * Integrator for flow field calculations. + */ + std::shared_ptr integrator; +}; + + +class PortalNode; + +using node_t = std::shared_ptr; + +/** + * Cost comparison for node_t on the pairing heap. + * + * Extracts the nodes from the shared_ptr and compares them. We have + * to use a custom comparison function because otherwise the shared_ptr + * would be compared instead of the actual node. + */ +struct compare_node_cost { + bool operator()(const node_t &lhs, const node_t &rhs) const; +}; + +using heap_t = datastructure::PairingHeap; +using nodemap_t = std::unordered_map; + +/** + * One navigation waypoint in a path. + */ +class PortalNode : public std::enable_shared_from_this { +public: + PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal); + PortalNode(const std::shared_ptr &portal, + sector_id_t entry_sector, + const node_t &prev_portal, + int past_cost, + int heuristic_cost); + + /** + * Orders nodes according to their future cost value. + */ + bool operator<(const PortalNode &other) const; + + /** + * Compare the node to another one. + * They are the same if their portal is. + */ + bool operator==(const PortalNode &other) const; + + /** + * Calculates the actual movement cose to another node. + */ + int cost_to(const PortalNode &other) const; + + /** + * Create a backtrace path beginning at this node. + */ + std::vector generate_backtrace(); + + /** + * Get all exits of a node. + */ + std::vector get_exits(const nodemap_t &nodes, sector_id_t entry_sector); + + /** + * The portal this node is associated to. + */ + std::shared_ptr portal; + + /** + * Sector where the portal is entered. + */ + sector_id_t entry_sector; + + /** + * Future cost estimation value for this node. + */ + int future_cost; + + /** + * Evaluated past cost value for the node. + * This stores the actual cost from start to this node. + */ + int current_cost; + + /** + * Heuristic cost cache. + * Calculated once, is the heuristic distance from this node + * to the goal. + */ + int heuristic_cost; + + /** + * Does this node already have an alternative path? + * If the node was once selected as the best next hop, + * this is set to true. + */ + bool was_best = false; + + /** + * Node where this one was reached by least cost. + */ + node_t prev_portal; + + /** + * Priority queue node that contains this path node. + */ + heap_t::element_t heap_node; +}; + + +} // namespace openage::path diff --git a/libopenage/pathfinding/portal.cpp b/libopenage/pathfinding/portal.cpp new file mode 100644 index 0000000000..da3581047f --- /dev/null +++ b/libopenage/pathfinding/portal.cpp @@ -0,0 +1,162 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "portal.h" + +#include "error/error.h" + + +namespace openage::path { + +Portal::Portal(portal_id_t id, + sector_id_t sector0, + sector_id_t sector1, + PortalDirection direction, + const coord::tile &cell_start, + const coord::tile &cell_end) : + id{id}, + sector0{sector0}, + sector1{sector1}, + sector0_exits{}, + sector1_exits{}, + direction{direction}, + cell_start{cell_start}, + cell_end{cell_end} { +} + +portal_id_t Portal::get_id() const { + return this->id; +} + +const std::vector> &Portal::get_connected(sector_id_t sector) const { + ENSURE(sector == this->sector0 || sector == this->sector1, "Portal does not connect to sector"); + + if (sector == this->sector0) { + return this->sector0_exits; + } + return this->sector1_exits; +} + +const std::vector> &Portal::get_exits(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->sector1_exits; + } + return this->sector0_exits; +} + +void Portal::set_exits(sector_id_t sector, const std::vector> &exits) { + ENSURE(sector == this->sector0 || sector == this->sector1, "Portal does not connect to sector"); + + if (sector == this->sector0) { + this->sector0_exits = exits; + } + else { + this->sector1_exits = exits; + } +} + +sector_id_t Portal::get_exit_sector(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->sector1; + } + return this->sector0; +} + +const coord::tile Portal::get_entry_start(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector0_start(); + } + + return this->get_sector1_start(); +} + +const coord::tile Portal::get_entry_center(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + auto start = this->get_sector0_start(); + auto end = this->get_sector0_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; + } + + auto start = this->get_sector1_start(); + auto end = this->get_sector1_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; +} + +const coord::tile Portal::get_entry_end(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector0_end(); + } + + return this->get_sector1_end(); +} + +const coord::tile Portal::get_exit_start(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector1_start(); + } + + return this->get_sector0_start(); +} + +const coord::tile Portal::get_exit_center(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + auto start = this->get_sector1_start(); + auto end = this->get_sector1_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; + } + + auto start = this->get_sector0_start(); + auto end = this->get_sector0_end(); + return {(start.ne + end.ne) / 2, (start.se + end.se) / 2}; +} + +const coord::tile Portal::get_exit_end(sector_id_t entry_sector) const { + ENSURE(entry_sector == this->sector0 || entry_sector == this->sector1, "Invalid entry sector"); + + if (entry_sector == this->sector0) { + return this->get_sector1_end(); + } + + return this->get_sector0_end(); +} + +PortalDirection Portal::get_direction() const { + return this->direction; +} + +const coord::tile &Portal::get_sector0_start() const { + return this->cell_start; +} + +const coord::tile &Portal::get_sector0_end() const { + return this->cell_end; +} + +const coord::tile Portal::get_sector1_start() const { + if (this->direction == PortalDirection::NORTH_SOUTH) { + return {this->cell_start.ne, 0}; + } + return {0, this->cell_start.se}; +} + +const coord::tile Portal::get_sector1_end() const { + if (this->direction == PortalDirection::NORTH_SOUTH) { + return {this->cell_end.ne, 0}; + } + return {0, this->cell_end.se}; +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/portal.h b/libopenage/pathfinding/portal.h new file mode 100644 index 0000000000..2487e449c7 --- /dev/null +++ b/libopenage/pathfinding/portal.h @@ -0,0 +1,242 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "coord/tile.h" +#include "pathfinding/types.h" + + +namespace openage::path { +class CostField; + +/** + * Possible directions of a portal node. + */ +enum class PortalDirection { + NORTH_SOUTH, + EAST_WEST +}; + +/** + * Biderectional gateway for connecting two sectors in the flow field pathfinder. + * + * Portals are located at the border of two sectors (0 and 1), and allow units to move between them. + * For each of these sectors, the portal stores the start and end coordinates where the + * sectors overlap as well as the other portals that can be reached in the same + * sector. For simplicity, the portal is assumed to be a straight line of cells from the start + * to the end. + * + * The portal is bidirectional, meaning that it can be entered from either sector and + * exited into the other sector. The direction of the portal from one sector to the other + * is stored in the portal node. As a convention and to simplify computations, sector 0 must be + * the either the north or east sector on the grid in relation to sector 1. + */ +class Portal { +public: + /** + * Create a new portal between two sectors. + * + * As a convention, sector 0 must be the either the north or east sector + * on the grid in relation to sector 1. + * + * @param id ID of the portal. Should be unique per grid. + * @param sector0 First sector connected by the portal. + * Must be north or east on the grid in relation to sector 1. + * @param sector1 Second sector connected by the portal. + * Must be south or west on the grid in relation to sector 0. + * @param direction Direction of the portal from sector 0 to sector 1. + * @param cell_start Start cell coordinate in sector 0. + * @param cell_end End cell coordinate in sector 0. + */ + Portal(portal_id_t id, + sector_id_t sector0, + sector_id_t sector1, + PortalDirection direction, + const coord::tile &cell_start, + const coord::tile &cell_end); + + ~Portal() = default; + + /** + * Get the ID of the portal. + * + * IDs are unique per grid. + * + * @return ID of the portal. + */ + portal_id_t get_id() const; + + /** + * Get the connected portals in the specified sector. + * + * @param sector Sector ID. + * + * @return Connected portals in the sector. + */ + const std::vector> &get_connected(sector_id_t sector) const; + + /** + * Get the exit portals reachable via the portal when entering from a specified sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Exit portals reachable from the portal. + */ + const std::vector> &get_exits(sector_id_t entry_sector) const; + + /** + * Set the exit portals reachable for a specified sector. + * + * @param sector Sector for which the exit portals are set. + * @param exits Exit portals reachable from the portal. + */ + void set_exits(sector_id_t sector, const std::vector> &exits); + + /** + * Get the cost field of the sector where the portal is exited. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cost field of the sector where the portal is exited. + */ + sector_id_t get_exit_sector(sector_id_t entry_sector) const; + + /** + * Get the cell coordinates of the start of the portal in the entry sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the start of the portal in the entry sector. + */ + const coord::tile get_entry_start(sector_id_t entry_sector) const; + + /** + * Get the cell coordinates of the center of the portal in the entry sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the center of the portal in the entry sector. + */ + const coord::tile get_entry_center(sector_id_t entry_sector) const; + + /** + * Get the cell coordinates of the start of the portal in the entry sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the start of the portal in the entry sector. + */ + const coord::tile get_entry_end(sector_id_t entry_sector) const; + + /** + * Get the cell coordinates of the start of the portal in the exit sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the start of the portal in the exit sector. + */ + const coord::tile get_exit_start(sector_id_t entry_sector) const; + + /** + * Get the cell coordinates of the center of the portal in the exit sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the center of the portal in the exit sector. + */ + const coord::tile get_exit_center(sector_id_t entry_sector) const; + + /** + * Get the cell coordinates of the end of the portal in the exit sector. + * + * @param entry_sector Sector from which the portal is entered. + * + * @return Cell coordinates of the end of the portal in the exit sector. + */ + const coord::tile get_exit_end(sector_id_t entry_sector) const; + + /** + * Get the direction of the portal from sector 0 to sector 1. + * + * @return Direction of the portal. + */ + PortalDirection get_direction() const; + +private: + /** + * Get the start cell coordinates of the portal. + * + * @return Start cell coordinates of the portal. + */ + const coord::tile &get_sector0_start() const; + + /** + * Get the end cell coordinates of the portal. + * + * @return End cell coordinates of the portal. + */ + const coord::tile &get_sector0_end() const; + + /** + * Get the start cell coordinates of the portal. + * + * @return Start cell coordinates of the portal. + */ + const coord::tile get_sector1_start() const; + + /** + * Get the end cell coordinates of the portal. + * + * @return End cell coordinates of the portal. + */ + const coord::tile get_sector1_end() const; + + /** + * ID of the portal. + */ + portal_id_t id; + + /** + * First sector connected by the portal. + */ + sector_id_t sector0; + + /** + * Second sector connected by the portal. + */ + sector_id_t sector1; + + /** + * Exits in sector 0 reachable from the portal. + * + * TODO: Also store avarage cost to reach each exit. + */ + std::vector> sector0_exits; + + /** + * Exits in sector 1 reachable from the portal. + * + * TODO: Also store avarage cost to reach each exit. + */ + std::vector> sector1_exits; + + /** + * Direction of the portal from sector 0 to sector 1. + */ + PortalDirection direction; + + /** + * Start cell coordinate. + */ + coord::tile cell_start; + + /** + * End cell coordinate. + */ + coord::tile cell_end; +}; +} // namespace openage::path diff --git a/libopenage/pathfinding/sector.cpp b/libopenage/pathfinding/sector.cpp new file mode 100644 index 0000000000..cf6ffaa755 --- /dev/null +++ b/libopenage/pathfinding/sector.cpp @@ -0,0 +1,252 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "sector.h" + +#include +#include + +#include "error/error.h" + +#include "coord/tile.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/definitions.h" + + +namespace openage::path { + +Sector::Sector(sector_id_t id, const coord::chunk &position, size_t field_size) : + id{id}, + position{position}, + cost_field{std::make_shared(field_size)} { +} + +Sector::Sector(sector_id_t id, const coord::chunk &position, const std::shared_ptr &cost_field) : + id{id}, + position{position}, + cost_field{cost_field} { +} + +const sector_id_t &Sector::get_id() const { + return this->id; +} + +const coord::chunk &Sector::get_position() const { + return this->position; +} + +const std::shared_ptr &Sector::get_cost_field() const { + return this->cost_field; +} + +const std::vector> &Sector::get_portals() const { + return this->portals; +} + +void Sector::add_portal(const std::shared_ptr &portal) { + this->portals.push_back(portal); +} + +std::vector> Sector::find_portals(const std::shared_ptr &other, + PortalDirection direction, + portal_id_t next_id) const { + ENSURE(this->cost_field->get_size() == other->get_cost_field()->get_size(), "Sector size mismatch"); + + std::vector> result; + + // cost field of the other sector + auto other_cost = other->get_cost_field(); + + // compare the edges of the sectors + size_t start = 0; + size_t end = 0; + bool passable_edge = false; + for (size_t i = 0; i < this->cost_field->get_size(); ++i) { + auto coord_this = coord::tile{0, 0}; + auto coord_other = coord::tile{0, 0}; + if (direction == PortalDirection::NORTH_SOUTH) { + // right edge; top to bottom + coord_this = coord::tile(i, this->cost_field->get_size() - 1); + coord_other = coord::tile(i, 0); + } + else if (direction == PortalDirection::EAST_WEST) { + // bottom edge; east to west + coord_this = coord::tile(this->cost_field->get_size() - 1, i); + coord_other = coord::tile(0, i); + } + + if (this->cost_field->get_cost(coord_this) != COST_IMPASSABLE + and other_cost->get_cost(coord_other) != COST_IMPASSABLE) { + // both sides of the edge are passable + if (not passable_edge) { + // start a new portal + start = i; + passable_edge = true; + } + // else: we already started a portal + + end = i; + if (i != this->cost_field->get_size() - 1) { + // continue to next tile unless we are at the last tile + // then we have to end the current portal + continue; + } + } + + if (passable_edge) { + // create a new portal + auto coord_start = coord::tile{0, 0}; + auto coord_end = coord::tile{0, 0}; + if (direction == PortalDirection::NORTH_SOUTH) { + // right edge; top to bottom + coord_start = coord::tile(start, this->cost_field->get_size() - 1); + coord_end = coord::tile(end, this->cost_field->get_size() - 1); + } + else if (direction == PortalDirection::EAST_WEST) { + // bottom edge; east to west + coord_start = coord::tile(this->cost_field->get_size() - 1, start); + coord_end = coord::tile(this->cost_field->get_size() - 1, end); + } + + result.push_back( + std::make_shared( + next_id, + this->id, + other->get_id(), + direction, + coord_start, + coord_end)); + passable_edge = false; + next_id += 1; + } + } + + return result; +} + +void Sector::connect_exits() { + if (this->portals.empty()) { + return; + } + + std::unordered_set portal_ids; + for (const auto &portal : this->portals) { + portal_ids.insert(portal->get_id()); + } + + // check all portals in the sector + std::vector> search_portals = this->portals; + while (not portal_ids.empty()) { + auto portal = search_portals.back(); + search_portals.pop_back(); + portal_ids.erase(portal->get_id()); + + auto start = portal->get_entry_start(this->id); + auto end = portal->get_entry_end(this->id); + + std::unordered_set visited; + std::deque open_list; + std::vector neighbors; + neighbors.reserve(4); + + if (portal->get_direction() == PortalDirection::NORTH_SOUTH) { + // right edge; top to bottom + for (auto i = start.se; i <= end.se; ++i) { + open_list.push_back(start.ne + i * this->cost_field->get_size()); + } + } + else if (portal->get_direction() == PortalDirection::EAST_WEST) { + // bottom edge; east to west + for (auto i = start.ne; i <= end.ne; ++i) { + open_list.push_back(i + start.se * this->cost_field->get_size()); + } + } + + // flood fill the grid to find connected portals + while (not open_list.empty()) { + auto current = open_list.front(); + open_list.pop_front(); + + if (visited.contains(current)) { + continue; + } + + // Get the x and y coordinates of the current cell + auto x = current % this->cost_field->get_size(); + auto y = current / this->cost_field->get_size(); + + // check the neighbors + if (y > 0) { + neighbors.push_back(current - this->cost_field->get_size()); + } + if (x > 0) { + neighbors.push_back(current - 1); + } + if (y < this->cost_field->get_size() - 1) { + neighbors.push_back(current + this->cost_field->get_size()); + } + if (x < this->cost_field->get_size() - 1) { + neighbors.push_back(current + 1); + } + + // add the neighbors to the open list + for (const auto &neighbor : neighbors) { + if (this->cost_field->get_cost(neighbor) != COST_IMPASSABLE) { + open_list.push_back(neighbor); + } + } + neighbors.clear(); + + // mark the current cell as visited + // TODO: Record the cost of reaching this cell + visited.insert(current); + } + + // check if the visited cells are connected to another portal + std::vector> connected_portals; + for (auto &exit : this->portals) { + if (exit->get_id() == portal->get_id()) { + // skip the current portal + continue; + } + + // get the start cell of the exit portal + // we only have to check one cell since the flood fill + // should reach any exit cell + auto exit_start = exit->get_entry_start(this->id); + auto exit_cell = exit_start.ne + exit_start.se * this->cost_field->get_size(); + + // check if the exit cell is connected to the visited cells + if (visited.contains(exit_cell)) { + connected_portals.push_back(exit); + } + } + + // set the exits for the current portal + portal->set_exits(this->id, connected_portals); + + // All connected portals share the same exits + // so we can connect them here + for (auto &connected : connected_portals) { + // make a new vector with all connected portals except the current one + std::vector> other_connected; + for (auto &other : connected_portals) { + if (other->get_id() != connected->get_id()) { + other_connected.push_back(other); + } + } + + // add the original portal as it is not in the connected portals vector + other_connected.push_back(portal); + + // set the exits for the connected portal + connected->set_exits(this->id, other_connected); + + // we don't need to food fill for this portal since we have + // found all exits, so we can remove it from the portals that + // should be searched + portal_ids.erase(connected->get_id()); + } + } +} + +} // namespace openage::path diff --git a/libopenage/pathfinding/sector.h b/libopenage/pathfinding/sector.h new file mode 100644 index 0000000000..f39f1904a9 --- /dev/null +++ b/libopenage/pathfinding/sector.h @@ -0,0 +1,129 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include +#include + +#include "coord/chunk.h" +#include "pathfinding/portal.h" +#include "pathfinding/types.h" + + +namespace openage::path { +class CostField; +class Portal; + +/** + * Sector in a grid for flow field pathfinding. + * + * Sectors consist of a cost field and a list of portals connecting them to adjacent + * sectors. + */ +class Sector { +public: + /** + * Create a new sector with a specified ID and an uninitialized cost field. + * + * @param id ID of the sector. Should be unique per grid. + * @param position Position of the sector in the grid. + * @param field_size Size of the cost field. + */ + Sector(sector_id_t id, + const coord::chunk &position, + size_t field_size); + + /** + * Create a new sector with a specified ID and an existing cost field. + * + * @param id ID of the sector. Should be unique per grid. + * @param position Position of the sector in the grid. + * @param cost_field Cost field of the sector. + */ + Sector(sector_id_t id, + const coord::chunk &position, + const std::shared_ptr &cost_field); + + /** + * Get the ID of this sector. + * + * IDs are unique per grid. + * + * @return ID of the sector. + */ + const sector_id_t &get_id() const; + + /** + * Get the position of this sector in the grid. + * + * @return Position of the sector. + */ + const coord::chunk &get_position() const; + + /** + * Get the cost field of this sector. + * + * @return Cost field of this sector. + */ + const std::shared_ptr &get_cost_field() const; + + /** + * Get the portals connecting this sector to other sectors. + * + * @return Outgoing portals of this sector. + */ + const std::vector> &get_portals() const; + + /** + * Add a portal to another sector. + * + * @param portal Portal to another sector. + */ + void add_portal(const std::shared_ptr &portal); + + /** + * Find portals connecting this sector to another sector. + * + * @param other Sector to which the portals should connect. + * @param direction Direction from this sector to \p other sector. + * @param next_id ID of the next portal to be created. Should be unique per grid. + * + * @return Portals connecting this sector to \p other sector. + */ + std::vector> find_portals(const std::shared_ptr &other, + PortalDirection direction, + portal_id_t next_id) const; + + /** + * Connect all portals that are mutually reachable. + * + * This method should be called after all sectors and portals have + * been created and initialized. + */ + void connect_exits(); + +private: + /** + * ID of the sector. + */ + sector_id_t id; + + /** + * Position of the sector in the grid. + */ + coord::chunk position; + + /** + * Cost field of the sector. + */ + std::shared_ptr cost_field; + + /** + * Portals of the sector. + */ + std::vector> portals; +}; + + +} // namespace openage::path diff --git a/libopenage/pathfinding/tests.cpp b/libopenage/pathfinding/tests.cpp index 35f29295e8..2c9775ca36 100644 --- a/libopenage/pathfinding/tests.cpp +++ b/libopenage/pathfinding/tests.cpp @@ -1,316 +1,117 @@ -// Copyright 2015-2018 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. -#include "../log/log.h" -#include "../testing/testing.h" +#include "log/log.h" +#include "testing/testing.h" + +#include "coord/tile.h" +#include "pathfinding/cost_field.h" +#include "pathfinding/definitions.h" +#include "pathfinding/flow_field.h" +#include "pathfinding/integration_field.h" +#include "pathfinding/integrator.h" +#include "pathfinding/types.h" -#include "heuristics.h" -#include "path.h" namespace openage { namespace path { namespace tests { -/** - * This function tests setting up basic nodes that point to a previous node. - * Tests that direction is set correctly and that factor is set correctly. - */ -void node_0() { - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{1, 0, 0}; - coord::phys3 p2{1, 1, 0}; - coord::phys3 p3{1, -1, 0}; - coord::phys3 p4{2, 0, 0}; - coord::phys3 p5{2, 2, 0}; - coord::phys3 p6{2, -2, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - node_pt n2 = std::make_unique(p2, n1); - node_pt n3 = std::make_unique(p3, n1); - node_pt n4 = std::make_unique(p0, n1); - - // Testing how the factor is effected from the change in - // direction from one node to another - TESTEQUALS(n1->direction.ne, 1); - TESTEQUALS(n1->direction.se, 0); - - // Expect this to be 2 since the similarity between nodes is zero - TESTEQUALS(n1->factor, 2); - - TESTEQUALS(n2->direction.ne, 0); - TESTEQUALS(n2->direction.se, 1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n2->factor, 2); - - TESTEQUALS(n3->direction.ne, 0); - TESTEQUALS(n3->direction.se, -1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n3->factor, 2); - - TESTEQUALS(n4->direction.ne, -1); - TESTEQUALS(n4->direction.se, 0); - - // Expect this to be 3 since it takes a 180 degree turn from n1 - TESTEQUALS(n4->factor, 3); - - // Testing that the distance from the previous node noes not - // effect the factor, only change in direction - - n1 = std::make_unique(p4, n0); - n2 = std::make_unique(p5, n1); - n3 = std::make_unique(p6, n1); - n4 = std::make_unique(p0, n1); - - TESTEQUALS(n1->direction.ne, 1); - TESTEQUALS(n1->direction.se, 0); - - // Expect this to be 2 since the similarity between nodes is zero - TESTEQUALS(n1->factor, 2); - TESTEQUALS(n2->direction.ne, 0); - TESTEQUALS(n2->direction.se, 1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n2->factor, 2); - TESTEQUALS(n3->direction.ne, 0); - TESTEQUALS(n3->direction.se, -1); - - // Expect this to be 2 since it takes a 90 degree turn from n1 - TESTEQUALS(n3->factor, 2); - TESTEQUALS(n4->direction.ne, -1); - TESTEQUALS(n4->direction.se, 0); - - // Expect this to be 3 since it takes a 180 degree turn from n1 - TESTEQUALS(n4->factor, 3); -} - -/** - * This function tests Node->cost_to. The testing is done on 2 unrelated - * nodes (They have no previous node) to test the basic cost without adding - * the cost from node->factor. - */ -void node_cost_to_0() { - // Testing basic cost_to with ne only - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{10, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, nullptr); - - TESTEQUALS(n0->cost_to(*n1), 10); - TESTEQUALS(n1->cost_to(*n0), 10); - - // Testing basic cost_to with se only - coord::phys3 p2{0, 5, 0}; - - node_pt n2 = std::make_unique(p2, nullptr); - - TESTEQUALS(n0->cost_to(*n2), 5); - TESTEQUALS(n2->cost_to(*n0), 5); - - // Testing cost_to with both se and ne: - coord::phys3 p3{3, 4, 0}; // -> sqrt(3*3 + 4*4) == 5 - - node_pt n3 = std::make_unique(p3, nullptr); - TESTEQUALS(n0->cost_to(*n3), 5); - TESTEQUALS(n3->cost_to(*n0), 5); - - // Test cost_to and check that `up` has no effect - coord::phys3 p4{3, 4, 8}; - - node_pt n4 = std::make_unique(p4, nullptr); - - TESTEQUALS(n0->cost_to(*n4), 5); - TESTEQUALS(n4->cost_to(*n0), 5); -} - -/** - * This function tests Node->cost_to. The testing is done on the neighbor - * nodes to test how the directional factor effects the cost. - */ -void node_cost_to_1() { - // Set up coords so that n1 will have a direction of ne = 1 - // but n0 with not be in n1s neighbors - coord::phys3 p0{-0.125, 0, 0}; - coord::phys3 p1{0.125, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - - // We expect twice the normal cost since n0 had not direction - // thus we get a factor of 2 on n1 - TESTEQUALS_FLOAT(n0->cost_to(*n1), 0.5, 0.001); - TESTEQUALS_FLOAT(n1->cost_to(*n0), 0.5, 0.001); - - nodemap_t visited_tiles; - visited_tiles[n0->position] = n0; - - // Collect the costs to go to all the neighbors of n1 - std::vector costs; - for (node_pt neighbor : n1->get_neighbors(visited_tiles, 1)) { - costs.push_back(n1->cost_to(*neighbor)); +void flow_field() { + // Create initial cost grid + auto cost_field = std::make_shared(3); + + // | 1 | 1 | 1 | + // | 1 | X | 1 | + // | 1 | 1 | 1 | + cost_field->set_costs({1, 1, 1, 1, 255, 1, 1, 1, 1}); + + // Test the different field types + { + auto integration_field = std::make_shared(3); + integration_field->integrate_cost(cost_field, coord::tile{2, 2}); + auto int_cells = integration_field->get_cells(); + + // The integration field should look like: + // | 4 | 3 | 2 | + // | 3 | X | 1 | + // | 2 | 1 | 0 | + auto int_expected = std::vector{ + 4, + 3, + 2, + 3, + 65535, + 1, + 2, + 1, + 0, + }; + + // The flow field for targeting (2, 2) hould look like this: + // | E | SE | S | + // | SE | X | S | + // | E | E | N | + auto ff_expected = std::vector{ + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + 0, + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::NORTH), + }; + + // Compare the integration field cells with the expected values + for (size_t i = 0; i < int_cells.size(); i++) { + TESTEQUALS(int_cells[i].cost, int_expected[i]); + } + + // Build the flow field + auto flow_field = std::make_shared(3); + flow_field->build(integration_field); + auto ff_cells = flow_field->get_cells(); + + // Compare the flow field cells with the expected values + for (size_t i = 0; i < ff_cells.size(); i++) { + TESTEQUALS(ff_cells[i], ff_expected[i]); + } } - TESTEQUALS_FLOAT(costs[0], 0.45711, 0.001); - TESTEQUALS_FLOAT(costs[1], 0.25, 0.001); - TESTEQUALS_FLOAT(costs[2], 0.45711, 0.001); - TESTEQUALS_FLOAT(costs[3], 0.5, 0.001); - TESTEQUALS_FLOAT(costs[4], 0.95709, 0.001); - TESTEQUALS_FLOAT(costs[5], 0.75, 0.001); - TESTEQUALS_FLOAT(costs[6], 0.95709, 0.001); - TESTEQUALS_FLOAT(costs[7], 0.5, 0.001); -} - -/** - * This function does a basic test of generating a backtrace from the - * last node in a path. - */ -void node_generate_backtrace_0() { - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{10, 0, 0}; - coord::phys3 p2{20, 0, 0}; - coord::phys3 p3{30, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - node_pt n2 = std::make_unique(p2, n1); - node_pt n3 = std::make_unique(p3, n2); - - Path path = n3->generate_backtrace(); - - (path.waypoints[0] == *n3) or TESTFAIL; - (path.waypoints[1] == *n2) or TESTFAIL; - (path.waypoints[2] == *n1) or TESTFAIL; -} - -/** - * This function tests Node->get_neighbors and how the scale effects - * the neighbors given. - */ -void node_get_neighbors_0() { - coord::phys3 p0{0, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - nodemap_t map; - - // Testing get_neighbors returning all surounding tiles with - // a factor of 1 - - std::vector neighbors = n0->get_neighbors(map, 1); - TESTEQUALS(neighbors.size(), 8); - - TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.125, 0.001); - TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.125, 0.001); - - TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.125, 0.001); - - // Testing how a larger scale changes the neighbors generated - neighbors = n0->get_neighbors(map, 2); - TESTEQUALS(neighbors.size(), 8); - - TESTEQUALS_FLOAT(neighbors[0]->position.ne.to_double(), 0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[0]->position.se.to_double(), -0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[1]->position.ne.to_double(), 0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[1]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[2]->position.ne.to_double(), 0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[2]->position.se.to_double(), 0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[3]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[3]->position.se.to_double(), 0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[4]->position.ne.to_double(), -0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[4]->position.se.to_double(), 0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[5]->position.ne.to_double(), -0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[5]->position.se.to_double(), 0, 0.001); - - TESTEQUALS_FLOAT(neighbors[6]->position.ne.to_double(), -0.25, 0.001); - TESTEQUALS_FLOAT(neighbors[6]->position.se.to_double(), -0.25, 0.001); - - TESTEQUALS_FLOAT(neighbors[7]->position.ne.to_double(), 0, 0.001); - TESTEQUALS_FLOAT(neighbors[7]->position.se.to_double(), -0.25, 0.001); -} - -/** - * This is a helper passable function that alwalys returns true. - */ -bool always_passable(const coord::phys3 &) { - return true; -} - -/** - * This is a helper passable function that always returns false. - */ -bool not_passable(const coord::phys3 &) { - return false; -} - -/** - * This is a helper passable function that only returns true when - * pos.ne == 20. - */ -bool sometimes_passable(const coord::phys3 &pos) { - if (pos.ne == 20) { - return false; - } else { - return true; + // Integrator test + { + // Integrator for managing the flow field + auto integrator = std::make_shared(); + + // Build the flow field + auto flow_field = integrator->build(cost_field, coord::tile{2, 2}).second; + auto ff_cells = flow_field->get_cells(); + + // The flow field for targeting (2, 2) hould look like this: + // | E | SE | S | + // | SE | X | S | + // | E | E | N | + auto ff_expected = std::vector{ + FLOW_PATHABLE_MASK | static_cast(flow_dir_t::EAST), + FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_WAVEFRONT_BLOCKED_MASK | FLOW_PATHABLE_MASK | static_cast(flow_dir_t::SOUTH_EAST), + 0, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + FLOW_LOS_MASK | FLOW_PATHABLE_MASK, + }; + + // Compare the flow field cells with the expected values + for (size_t i = 0; i < ff_cells.size(); i++) { + TESTEQUALS(ff_cells[i], ff_expected[i]); + } } } -/** - * This function tests passable_line. Tests with always false, always true, - * and position dependant functions being passed in as args. - */ -void node_passable_line_0() { - coord::phys3 p0{0, 0, 0}; - coord::phys3 p1{1000, 0, 0}; - - node_pt n0 = std::make_unique(p0, nullptr); - node_pt n1 = std::make_unique(p1, n0); - - TESTEQUALS(path::passable_line(n0, n1, path::tests::always_passable), true); - TESTEQUALS(path::passable_line(n0, n1, path::tests::not_passable), false); - - // The next 2 cases show that a different sample can change the results - // for the same path - TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 10), true); - TESTEQUALS(path::passable_line(n0, n1, path::tests::sometimes_passable, 50), false); -} - -/** - * Top level node test. - */ -void path_node() { - node_0(); - node_cost_to_0(); - node_cost_to_1(); - node_generate_backtrace_0(); - node_get_neighbors_0(); - node_passable_line_0(); -} } // namespace tests -} // namespace pathfinding +} // namespace path } // namespace openage diff --git a/libopenage/pathfinding/types.cpp b/libopenage/pathfinding/types.cpp new file mode 100644 index 0000000000..6428dd7ded --- /dev/null +++ b/libopenage/pathfinding/types.cpp @@ -0,0 +1,10 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#include "types.h" + + +namespace openage::path { + +// this file is intentionally empty + +} // namespace openage::path diff --git a/libopenage/pathfinding/types.h b/libopenage/pathfinding/types.h new file mode 100644 index 0000000000..11190f6551 --- /dev/null +++ b/libopenage/pathfinding/types.h @@ -0,0 +1,92 @@ +// Copyright 2024-2024 the openage authors. See copying.md for legal info. + +#pragma once + +#include +#include + + +namespace openage::path { + +/** + * Movement cost in the cost field. + * + * TODO: Cost stamps + * + * 0: uninitialized + * 1-254: normal cost + * 255: impassable + */ +using cost_t = uint8_t; + +/** + * Integrated cost in the integration field. + */ +using integrated_cost_t = uint16_t; + +/** + * Integrated field cell flags. + */ +using integrated_flags_t = uint8_t; + +/** + * Integration field cell value. + */ +struct integrated_t { + /** + * Total integrated cost. + */ + integrated_cost_t cost; + + /** + * Flags. + * + * Bit 6: Wave front blocked flag. + * Bit 7: Line of sight flag. + */ + integrated_flags_t flags; +}; + +/** + * Flow field direction types. + * + * Encoded into the flow_t values. + */ +enum class flow_dir_t : uint8_t { + NORTH = 0x00, + NORTH_EAST = 0x01, + EAST = 0x02, + SOUTH_EAST = 0x03, + SOUTH = 0x04, + SOUTH_WEST = 0x05, + WEST = 0x06, + NORTH_WEST = 0x07, +}; + +/** + * Flow field cell value. + * + * Bit 0: Unused. + * Bit 1: Wave front blocked flag. + * Bit 2: Line of sight flag. + * Bit 3: Pathable flag. + * Bits 4-7: flow direction. + */ +using flow_t = uint8_t; + +/** + * Grid identifier. + */ +using grid_id_t = size_t; + +/** + * Sector identifier (unique per grid). + */ +using sector_id_t = size_t; + +/** + * Portal identifier (unique per grid). + */ +using portal_id_t = size_t; + +} // namespace openage::path diff --git a/libopenage/renderer/opengl/geometry.cpp b/libopenage/renderer/opengl/geometry.cpp index a01b4183af..0e2c930a2a 100644 --- a/libopenage/renderer/opengl/geometry.cpp +++ b/libopenage/renderer/opengl/geometry.cpp @@ -1,4 +1,4 @@ -// Copyright 2015-2023 the openage authors. See copying.md for legal info. +// Copyright 2015-2024 the openage authors. See copying.md for legal info. #include "geometry.h" @@ -68,7 +68,7 @@ void GlGeometry::draw() const { glDrawElements(mesh.primitive, mesh.vert_count, *mesh.index_type, nullptr); } else { - glDrawArrays(GL_TRIANGLE_STRIP, 0, mesh.vert_count); + glDrawArrays(mesh.primitive, 0, mesh.vert_count); } break; diff --git a/openage/CMakeLists.txt b/openage/CMakeLists.txt index a9233357dd..1b31e7110f 100644 --- a/openage/CMakeLists.txt +++ b/openage/CMakeLists.txt @@ -27,6 +27,7 @@ add_subdirectory(gamestate) add_subdirectory(log) add_subdirectory(main) add_subdirectory(nyan) +add_subdirectory(pathfinding) add_subdirectory(renderer) add_subdirectory(testing) add_subdirectory(util) diff --git a/openage/pathfinding/CMakeLists.txt b/openage/pathfinding/CMakeLists.txt new file mode 100644 index 0000000000..03ab1f8e49 --- /dev/null +++ b/openage/pathfinding/CMakeLists.txt @@ -0,0 +1,7 @@ +add_cython_modules( + tests.pyx +) + +add_py_modules( + __init__.py +) diff --git a/openage/pathfinding/__init__.py b/openage/pathfinding/__init__.py new file mode 100644 index 0000000000..8c497dfafb --- /dev/null +++ b/openage/pathfinding/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +openage pathfinding system +""" diff --git a/openage/pathfinding/tests.pyx b/openage/pathfinding/tests.pyx new file mode 100644 index 0000000000..f180cb786d --- /dev/null +++ b/openage/pathfinding/tests.pyx @@ -0,0 +1,49 @@ +# Copyright 2024-2024 the openage authors. See copying.md for legal info. + +""" +tests for the pathfinding system. +""" + +import argparse + +from libopenage.util.path cimport Path as Path_cpp +from libopenage.pyinterface.pyobject cimport PyObj +from cpython.ref cimport PyObject +from libopenage.pathfinding.demo.tests cimport path_demo as path_demo_c + +def path_demo(list argv): + """ + invokes the available pathfinding demos. + """ + + cmd = argparse.ArgumentParser( + prog='... path_demo', + description='Demo of the pathfinding system') + cmd.add_argument("test_id", type=int, help="id of the demo to run.") + cmd.add_argument("--asset-dir", + help="Use this as an additional asset directory.") + cmd.add_argument("--cfg-dir", + help="Use this as an additional config directory.") + + args = cmd.parse_args(argv) + + from ..cvar.location import get_config_path + from ..assets import get_asset_path + from ..util.fslike.union import Union + + # create virtual file system for data paths + root = Union().root + + # mount the assets folder union at "assets/" + root["assets"].mount(get_asset_path(args.asset_dir)) + + # mount the config folder at "cfg/" + root["cfg"].mount(get_config_path(args.cfg_dir)) + + cdef int demo_id = args.test_id + + cdef Path_cpp root_cpp = Path_cpp(PyObj(root.fsobj), + root.parts) + + with nogil: + path_demo_c(demo_id, root_cpp) diff --git a/openage/testing/testlist.py b/openage/testing/testlist.py index 75a684e85e..37f323e8a8 100644 --- a/openage/testing/testlist.py +++ b/openage/testing/testlist.py @@ -1,4 +1,4 @@ -# Copyright 2015-2023 the openage authors. See copying.md for legal info. +# Copyright 2015-2024 the openage authors. See copying.md for legal info. """ Lists of all possible tests; enter your tests here. """ @@ -52,6 +52,8 @@ def demos_py(): "play pong on steroids through future prediction") yield ("openage.gamestate.tests.simulation_demo", "showcases the game simulation") + yield ("openage.pathfinding.tests.path_demo", + "showcases the pathfinding system") yield ("openage.renderer.tests.renderer_demo", "showcases the renderer") yield ("openage.renderer.tests.renderer_stresstest", @@ -85,6 +87,7 @@ def tests_cpp(): yield "openage::datastructure::tests::pairing_heap" yield "openage::job::tests::test_job_manager" yield "openage::path::tests::path_node", "pathfinding" + yield "openage::path::tests::flow_field", "pathfinding" yield "openage::pyinterface::tests::pyobject" yield "openage::pyinterface::tests::err_py_to_cpp" yield "openage::renderer::tests::font"