- ID da verificação
- 214e241c-d23d-4b4d-a182-e486c644858bConcluído
- URL enviado:
- https://shendriks.dev/posts/2024-09-04-a-star-pathfinding-side-view/
- Relatório concluído:
Links · 9 encontrado(s)
Os links de saída identificados na página
Link | Texto |
---|---|
https://monogame.net/ | https://monogame.net/ |
https://github.com/kniEngine/kni | https://github.com/kniEngine/kni |
https://github.com/shendriks/pathfinding-2d | GitHub |
https://martinfowler.com/eaaCatalog/dataTransferObject.html | https://martinfowler.com/eaaCatalog/dataTransferObject.html |
https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#property-pattern | property patterns |
https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerator | https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerator |
https://en.wikipedia.org/wiki/Coroutine | https://en.wikipedia.org/wiki/Coroutine |
https://gohugo.io | Hugo |
https://github.com/rhazdon/hugo-theme-hello-friend-ng | Hello Friend NG |
Variáveis JavaScript · 7 encontrada(s)
Variáveis JavaScript globais carregadas no objeto janela de uma página são variáveis declaradas fora das funções e acessíveis de qualquer lugar no código dentro do escopo atual
Nome | Tipo |
---|---|
0 | object |
onbeforetoggle | object |
documentPictureInPicture | object |
onscrollend | object |
loadApp | function |
animateProgressBar | function |
fadeToTopLink | function |
Mensagens de registro do console · 0 encontrada(s)
Mensagens registradas no console web
HTML
O corpo HTML bruto da página
<!DOCTYPE html><html lang="en" data-theme="dark"><head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="author" content="">
<meta name="description" content="Implementing the A* algorithm for pathfinding in a 2D side-scrolling platformer with gravity, ladders and hanging bars.">
<meta name="keywords" content="homepage, blog, dev blog, programming, software development, dev, game dev, game development, game design, software engineering, software architecture, software design, design patterns, design principles, architecture principles, SOLID, SOLID principles, C#, c-sharp, csharp, monogame, KNI, platformer, 2D platformer, pathfinding, A*, A-Star, shortest path, tutorial, sven hendriks, dev, gamedev, astar, platformer, pathfinding, monogame, kni, top-down view, side-view, csharp, coroutine">
<meta name="robots" content="noodp">
<meta name="theme-color" content="">
<meta name="fediverse:creator" content="@[email protected]">
<link rel="me" href="https://mastodon.social/@shendriks">
<link rel="canonical" href="https://shendriks.dev/posts/2024-09-04-a-star-pathfinding-side-view/">
<title>
A* Pathfinding in 2D Games: From Top-Down to Side-View :: Sven's Dev Journal
</title>
<link rel="stylesheet" href="/main.aa524554bd0d3e97d08a077bc6f005c70a9e67b9513de9cdc3b5459b9ca69899.css" integrity="sha256-qlJFVL0NPpfQigd7xvAFxwqeZ7lRPenNw7VFm5ymmJk=">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<link rel="shortcut icon" href="/favicon.ico">
<meta name="msapplication-TileColor" content="#da532c">
<meta itemprop="name" content="A* Pathfinding in 2D Games: From Top-Down to Side-View">
<meta itemprop="description" content="Implementing the A* algorithm for pathfinding in a 2D side-scrolling platformer with gravity, ladders and hanging bars.">
<meta itemprop="datePublished" content="2024-09-04T00:00:00+00:00">
<meta itemprop="dateModified" content="2024-10-24T00:00:00+00:00">
<meta itemprop="wordCount" content="4152">
<meta itemprop="image" content="https://shendriks.dev/posts/2024-09-04-a-star-pathfinding-side-view/images/example.png">
<meta itemprop="keywords" content="Dev,Gamedev,Astar,Platformer,Pathfinding,Monogame,Kni,Top-Down View,Side-View,Csharp,Coroutine">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="https://shendriks.dev/posts/2024-09-04-a-star-pathfinding-side-view/images/example.png">
<meta name="twitter:title" content="A* Pathfinding in 2D Games: From Top-Down to Side-View">
<meta name="twitter:description" content="Implementing the A* algorithm for pathfinding in a 2D side-scrolling platformer with gravity, ladders and hanging bars.">
<meta property="og:url" content="https://shendriks.dev/posts/2024-09-04-a-star-pathfinding-side-view/">
<meta property="og:site_name" content="Sven's Dev Journal">
<meta property="og:title" content="A* Pathfinding in 2D Games: From Top-Down to Side-View">
<meta property="og:description" content="Implementing the A* algorithm for pathfinding in a 2D side-scrolling platformer with gravity, ladders and hanging bars.">
<meta property="og:locale" content="en_us">
<meta property="og:type" content="article">
<meta property="article:section" content="posts">
<meta property="article:published_time" content="2024-09-04T00:00:00+00:00">
<meta property="article:modified_time" content="2024-10-24T00:00:00+00:00">
<meta property="article:tag" content="Dev">
<meta property="article:tag" content="Gamedev">
<meta property="article:tag" content="Astar">
<meta property="article:tag" content="Platformer">
<meta property="article:tag" content="Pathfinding">
<meta property="article:tag" content="Monogame">
<meta property="og:image" content="https://shendriks.dev/posts/2024-09-04-a-star-pathfinding-side-view/images/example.png">
<meta property="og:see_also" content="https://shendriks.dev/posts/2024-08-24-a-star-pathfinding-top-down-blazorgl/">
<meta property="og:see_also" content="https://shendriks.dev/posts/2024-07-13-a-star-pathfinding-in-2d-games-the-basics-for-top-down-scenarios/">
<meta property="article:published_time" content="2024-09-04 00:00:00 +0000 UTC">
</head>
<body>
<div class="container">
<header class="header">
<span class="header__inner">
<a href="/" style="text-decoration: none;">
<div class="logo">
<span class="logo__mark">></span>
<span class="logo__text ">
cd $HOME</span>
<span class="logo__cursor" style="
">
</span>
</div>
</a>
<span class="header__right">
<nav class="menu">
<ul class="menu__inner"><li><a href="/about">About</a></li><li><a href="/posts">Blog</a></li>
</ul>
</nav>
<span class="menu-trigger hidden">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"></path>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"></path>
</svg>
</span>
</span>
</span>
</header>
<div class="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<div class="content">
<main class="post">
<div class="post-info">
<p>
2024-09-04
(Last updated: 2024-10-24)
—
20 minutes
</p>
</div>
<article>
<h1 class="post-title">
<a href="https://shendriks.dev/posts/2024-09-04-a-star-pathfinding-side-view/">A* Pathfinding in 2D Games: From Top-Down to Side-View</a>
</h1>
<div class="info-box">
<p>This post is part of the <a href="https://shendriks.dev/series/a-pathfinding-in-2d-games/" style="font-weight: bold">A* Pathfinding in 2D Games</a> series.</p>
<ul class="list-group">
<li class="list-group-item">
<a href="https://shendriks.dev/posts/2024-07-13-a-star-pathfinding-in-2d-games-the-basics-for-top-down-scenarios/">
Part 1: A* Pathfinding Basics for a 2D Top-down Scenario
</a>
</li>
<li class="list-group-item">
<a href="https://shendriks.dev/posts/2024-08-24-a-star-pathfinding-top-down-blazorgl/">
Part 2: MonoGame/KNI Implementation of 2D Top-down A* Pathfinding
</a>
</li>
<li class="list-group-item active">
Part 3: A* Pathfinding in a 2D Side-View Scenario (this post)
</li>
</ul>
</div>
<div class="post-content">
<h2 id="introduction">Introduction</h2>
<p>In the last two articles of this series we looked at how the A* search algorithm can be applied to a simple top-down
scenario. Now let’s finally look at what needs to change for a side-view platformer game. We will implement an example
application in <a href="https://monogame.net/" target="_blank" class="external-link" zgotmplz="">MonoGame</a> / <a href="https://github.com/kniEngine/kni" target="_blank" class="external-link" zgotmplz="">KNI</a>. For this, we will update the code from the
<a href="/posts/2024-08-24-a-star-pathfinding-top-down-blazorgl/">last post</a>.</p>
<div class="notice note">
<p class="notice-title">
<span class="icon-notice baseline">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 128 300 300">
<path d="M150 128c82.813 0 150 67.188 150 150 0 82.813-67.188 150-150 150C67.187 428 0 360.812 0 278c0-82.813 67.188-150 150-150Zm25 243.555v-37.11c0-3.515-2.734-6.445-6.055-6.445h-37.5c-3.515 0-6.445 2.93-6.445 6.445v37.11c0 3.515 2.93 6.445 6.445 6.445h37.5c3.32 0 6.055-2.93 6.055-6.445Zm-.39-67.188 3.515-121.289c0-1.367-.586-2.734-1.953-3.516-1.172-.976-2.93-1.562-4.688-1.562h-42.968c-1.758 0-3.516.586-4.688 1.563-1.367.78-1.953 2.148-1.953 3.515l3.32 121.29c0 2.734 2.93 4.882 6.64 4.882h36.134c3.515 0 6.445-2.148 6.64-4.883Z"></path>
</svg>
</span>Note</p><p>You don’t have to manually copy code snippets around. The complete project is available on <a href="https://github.com/shendriks/pathfinding-2d" target="_blank" class="external-link" zgotmplz="">GitHub</a>.</p></div>
<h2 id="what-seems-to-be-the-problem-with-side-view">What seems to be the Problem with Side-View?</h2>
<p>First of all, in a typical side-view platformer scenario, gravity is involved, pulling every game object down to
the ground. This means that, unlike in a top-down view, a character can’t just keep going up cells until they reach
the ceiling. (Unless the character has the ability to fly, of course. But we’re limiting ourselves to humanoid bipeds
without wings, or a jetpack, or a magic potion that defies gravity.) The only way to move upwards would be to jump or
to climb a ladder. Another limitation is that the character needs ground under their feet to walk sideways.</p>
<p>In summary the following can be said:</p>
<ul>
<li>There is gravity involved, which limits how a character can move.</li>
<li>The character may or may not have certain capabilities such as walking, jumping, climbing ladders and using hanging bars.</li>
<li>Certain cell types can help the character to overcome specific restrictions, such as
ladders and hanging bars with which the character can defy gravity.</li>
</ul>
<h2 id="cell-types-and-capabilities">Cell Types and Capabilities</h2>
<p>We’ll start with the cell types. Before, in the top-down world, we only had two types of cells: free and blocked. Now,
in our little side-view world, we have the following four cell types: empty, blocked, ladder and hanging bar.
Here’s an example of what this might look like:</p>
<img src="images/example.png" class="center" alt="An example of an Side-View Game World">
<p>Here are the properties of the different cell types:</p>
<ul>
<li> <img src="images/CellEmpty.png" class="list-item-image">
An empty cell can obviously be occupied by the game character</li>
<li> <img src="images/CellBlock.png" class="list-item-image">
A block, on the other hand, cannot. The character cannot pass through it.</li>
<li> <img src="images/CellLadder.png" class="list-item-image">
A ladder can be used to climb up or down</li>
<li> <img src="images/CellHangingBar.png" class="list-item-image">
Hanging bars can be used to move sideways</li>
</ul>
<p>Let’s further assume that the character has the following capabilities:</p>
<ol>
<li>Walk sideways</li>
<li>Climb ladders up and down and maybe even sideways</li>
<li>Swing along hanging bars</li>
<li>Jump sideways</li>
</ol>
<p>Furthermore, certain capabilities could be switched on and off. For example, the character might not be able to jump at the
beginning. This ability could be unlocked later, e.g. through a power-up.</p>
<h3 id="walking-climbing-swinging-and-falling">Walking, Climbing, Swinging (and Falling)</h3>
<p>Let’s take a closer look on how we can move without jumping and what that means in terms of neighborhood:</p>
<p></p><div class="flex-container">
<figure class="flex-item flex-item-4"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/neighbourhood-a.svg" alt="When the cell below is empty, then gravity takes over and the character is pulled down to the ground, which can be interpreted as ’the character can go down a cell if that cell is empty'." width="100%"><figcaption>
<p>When the cell below is empty, then gravity takes over and the character is pulled down to the ground, which can be interpreted as ’the character can go down a cell if that cell is empty'.</p>
</figcaption>
</figure>
<figure class="flex-item flex-item-4"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/neighbourhood-b.svg" alt="When there is ground below and the laterally adjoining cell is free, a ladder or a hanging bar, the character can walk in that direction." width="100%"><figcaption>
<p>When there is ground below and the laterally adjoining cell is free, a ladder or a hanging bar, the character can walk in that direction.</p>
</figcaption>
</figure>
<figure class="flex-item flex-item-4"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/neighbourhood-c.svg" alt="When the character is on a ladder, they can walk up and down and can also walk to the side, if the respective side is empty (or also a ladder or a hanging bar)." width="100%"><figcaption>
<p>When the character is on a ladder, they can walk up and down and can also walk to the side, if the respective side is empty (or also a ladder or a hanging bar).</p>
</figcaption>
</figure>
<figure class="flex-item flex-item-4"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/neighbourhood-d.svg" alt="When the character has grabbed the hanging bars, they can move sideways or they can let loose and fall down." width="100%"><figcaption>
<p>When the character has grabbed the hanging bars, they can move sideways or they can let loose and fall down.</p>
</figcaption>
</figure>
</div><p></p>
<h3 id="integrating-jumps">Integrating Jumps</h3>
<div class="notice note">
<p class="notice-title">
<span class="icon-notice baseline">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 128 300 300">
<path d="M150 128c82.813 0 150 67.188 150 150 0 82.813-67.188 150-150 150C67.187 428 0 360.812 0 278c0-82.813 67.188-150 150-150Zm25 243.555v-37.11c0-3.515-2.734-6.445-6.055-6.445h-37.5c-3.515 0-6.445 2.93-6.445 6.445v37.11c0 3.515 2.93 6.445 6.445 6.445h37.5c3.32 0 6.055-2.93 6.055-6.445Zm-.39-67.188 3.515-121.289c0-1.367-.586-2.734-1.953-3.516-1.172-.976-2.93-1.562-4.688-1.562h-42.968c-1.758 0-3.516.586-4.688 1.563-1.367.78-1.953 2.148-1.953 3.515l3.32 121.29c0 2.734 2.93 4.882 6.64 4.882h36.134c3.515 0 6.445-2.148 6.64-4.883Z"></path>
</svg>
</span>Note</p><p>Jumping can be implemented very differently in terms of height, distance, speed, etc. This results in individual jump
paths depending on the specific implementation. In addition, jumps of different heights and distances or even double jumps could also be possible.
The goal of this article is to explain the basic principle in a simple way, so we’ll keep things as straightforward as possible.
It’s even possible that, based on the colliders you’ve implemented, the trajectory we’ve used here, which is a bit
idealized, doesn’t allow a jump at all because the character bounces off the edge of a block, for example. We will
generously ignore this here and leave it to the reader to find a jump trajectory that works for their use case.</p></div>
<p>To simplify things a bit, let’s limit the jump capability to the following: the character can jump two cells high and
three cells to the side. Here are three example obstacles the character can overcome by jumping:</p>
<p></p><div class="flex-container">
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-01-a.svg" alt="Jump onto a platform that’s no more than two cells higher." width="100%"><figcaption>
<p>Jump onto a platform that’s no more than two cells higher.</p>
</figcaption>
</figure>
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-01-b.svg" alt="Jump over a wall that’s no more than two cells high." width="100%"><figcaption>
<p>Jump over a wall that’s no more than two cells high.</p>
</figcaption>
</figure>
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-01-c.svg" alt="Jump over a pit that’s no more than three cells wide." width="100%"><figcaption>
<p>Jump over a pit that’s no more than three cells wide.</p>
</figcaption>
</figure>
</div><p></p>
<p>Looking at the example of a jump over a pit, we can see how the neighboring cells and the costs of reaching them are determined.</p>
<p></p><div class="flex-container">
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-02-a.svg" alt="Each cell the jump trajectory is going through …" width="100%"><figcaption>
<p>Each cell the jump trajectory is going through …</p>
</figcaption>
</figure>
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-02-b.svg" alt="… is considered a neighboring cell which is reachable." width="100%"><figcaption>
<p>… is considered a neighboring cell which is reachable.</p>
</figcaption>
</figure>
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-02-c.svg" alt="The cost is calculated by applying the accumulated manhattan distance." width="100%"><figcaption>
<p>The cost is calculated by applying the accumulated manhattan distance.</p>
</figcaption>
</figure>
</div><p></p>
<p>If there is a block in the jump trajectory, the jump ends there:</p>
<p></p><div class="flex-container">
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-03-a.svg" width="100%">
</figure>
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-03-b.svg" width="100%">
</figure>
<figure class="flex-item flex-item-3"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-03-c.svg" width="100%">
</figure>
</div><p></p>
<p>As you can see the number of neighbors is less compared to a “full” jump.</p>
<h2 id="lets-code">Let’s code!</h2>
<p>Now that we have an idea of how the concept of neighboring cells needs to change, we can cast that in code.</p>
<h3 id="updating-the-cell-class">Updating the <code>Cell</code> class</h3>
<p>First of all, the <code>Cell</code> class from the last post needs another update. Instead of the walkabilty flag we now
need a cell type which will be implemented with an <code>enum</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff5c57">enum</span> CellType
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> Empty,
</span></span><span style="display:flex;"><span> Block,
</span></span><span style="display:flex;"><span> Ladder,
</span></span><span style="display:flex;"><span> HangingBar
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Here is the first update to the <code>Cell</code> class:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">Cell</span>(<span style="color:#9aedfe">int</span> x, <span style="color:#9aedfe">int</span> y, CellType type)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> CellType _type = type;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> CellType Type {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">get</span> => _type;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">set</span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (<span style="color:#ff6ac1">value</span> == _type) <span style="color:#ff6ac1">return</span>;
</span></span><span style="display:flex;"><span> CellTypeChanged?.Invoke();
</span></span><span style="display:flex;"><span> _type = <span style="color:#ff6ac1">value</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#9aedfe">bool</span> IsEmpty => Type == CellType.Empty;
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#9aedfe">bool</span> IsBlock => Type == CellType.Block;
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#9aedfe">bool</span> IsLadder => Type == CellType.Ladder;
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#9aedfe">bool</span> IsHangingBar => Type == CellType.HangingBar;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">event</span> Action? CellTypeChanged;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>We added a public property <code>Type</code> for getting and also setting the cell type. Setting of a cell type is going to happen
via the level editor. When the cell type changes, we invoke a <code>CellTypeChanged</code> event.</p>
<p>In addition we added a few convenience properties for easily checking if the cell is empty or a block and so on.</p>
<p>Now, let’s take another look at the example diagrams above. You will notice that compared to the simpler top-down case
it is no longer sufficient to just look at the cells that are direct neighbors of the current cell. We need a way to
traverse through the grid from a specific cell so we’re able to look at a neighbor’s neighbor and then at the neighbor
of the neighbor’s neighbor and so on.</p>
<p>Instead of fiddling with the grid in the find-neighbor-logic, we will extend the <code>Cell</code> class even further and
add some properties that let us go easily into a certain direction from that cell. The fiddling will then be
done and hidden in these properties. For this, the <code>Cell</code> needs to be aware of the <code>Grid</code> it is part of.</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">Cell</span>(<span style="color:#9aedfe">int</span> x, <span style="color:#9aedfe">int</span> y, CellType type, Grid grid)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? NeighborAt(<span style="color:#9aedfe">int</span> deltaX, <span style="color:#9aedfe">int</span> deltaY) => grid[X + deltaX, Y + deltaY];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? Up => NeighborAt(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">1</span>);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? Down => NeighborAt(<span style="color:#ff9f43">0</span>, <span style="color:#ff9f43">1</span>);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? Left => NeighborAt(-<span style="color:#ff9f43">1</span>, <span style="color:#ff9f43">0</span>);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? Right => NeighborAt(<span style="color:#ff9f43">1</span>, <span style="color:#ff9f43">0</span>);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? DownRight => NeighborAt(<span style="color:#ff9f43">1</span>, <span style="color:#ff9f43">1</span>);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? DownLeft => NeighborAt(-<span style="color:#ff9f43">1</span>, <span style="color:#ff9f43">1</span>);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? UpRight => NeighborAt(<span style="color:#ff9f43">1</span>, -<span style="color:#ff9f43">1</span>);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell? UpLeft => NeighborAt(-<span style="color:#ff9f43">1</span>, -<span style="color:#ff9f43">1</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>As you can see we added a <code>Grid</code> parameter to the primary constructor. Then, there are a handful of properties for going up,
down, left, right, etc. from the cell and get the adjoining cell, if there is one, in the respective direction. If we
leave the grid, <code>null</code> is returned.</p>
<h3 id="updating-the-grid-class">Updating the <code>Grid</code> class</h3>
<p>The <code>Grid</code> class also needs an update. Here is the class boiled down to the changes needed to add support for the new
cell types:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">Grid</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">readonly</span> Cell[,] _cells;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">event</span> Action? GridChanged;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Grid(<span style="color:#9aedfe">char</span>[,] map)
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (map.Length == <span style="color:#ff9f43">0</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">throw</span> <span style="color:#ff6ac1">new</span> ArgumentException(<span style="color:#5af78e">$"{nameof(map)} must contain at least 1 element"</span>);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> _cells = <span style="color:#ff6ac1">new</span> Cell[map.GetLength(<span style="color:#ff9f43">1</span>), map.GetLength(<span style="color:#ff9f43">0</span>)];
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">for</span> (<span style="color:#9aedfe">var</span> y = <span style="color:#ff9f43">0</span>; y < map.GetLength(<span style="color:#ff9f43">0</span>); y++) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">for</span> (<span style="color:#9aedfe">var</span> x = <span style="color:#ff9f43">0</span>; x < map.GetLength(<span style="color:#ff9f43">1</span>); x++) {
</span></span><span style="display:flex;"><span> _cells[x, y] = <span style="color:#ff6ac1">new</span> Cell(x, y, CellTypeFromChar(map[y, x]), <span style="color:#ff6ac1">this</span>);
</span></span><span style="display:flex;"><span> _cells[x, y].CellTypeChanged += () => GridChanged?.Invoke();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> CellType CellTypeFromChar(<span style="color:#9aedfe">char</span> c)
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">return</span> c <span style="color:#ff6ac1">switch</span> {
</span></span><span style="display:flex;"><span> <span style="color:#5af78e">' '</span> => CellType.Empty,
</span></span><span style="display:flex;"><span> <span style="color:#5af78e">'B'</span> => CellType.Block,
</span></span><span style="display:flex;"><span> <span style="color:#5af78e">'L'</span> => CellType.Ladder,
</span></span><span style="display:flex;"><span> <span style="color:#5af78e">'H'</span> => CellType.HangingBar,
</span></span><span style="display:flex;"><span> _ => <span style="color:#ff6ac1">throw</span> <span style="color:#ff6ac1">new</span> ArgumentException(<span style="color:#5af78e">$"Invalid cell type: {c}"</span>)
</span></span><span style="display:flex;"><span> };
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>First of all we got rid of the static constructor <code>CreateFromArray</code> and instead made the constructor public.
You’ll also see that the map array now contains <code>char</code>s instead of <code>int</code>s, and we need to map certain characters to
certain cell types, which is implemented in <code>CellTypeFromChar</code>. This allows us to instantiate the example grid like so:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">Pathfinding</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">readonly</span> <span style="color:#9aedfe">char</span>[,] _map = {
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'H'</span>, <span style="color:#5af78e">'H'</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'H'</span>, <span style="color:#5af78e">'H'</span>, <span style="color:#5af78e">'H'</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">' '</span>, <span style="color:#5af78e">'L'</span> },
</span></span><span style="display:flex;"><span> { <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span>, <span style="color:#5af78e">'B'</span> }
</span></span><span style="display:flex;"><span> };
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">readonly</span> Grid _grid;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Pathfinding()
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> _grid = <span style="color:#ff6ac1">new</span> Grid(_map) {
</span></span><span style="display:flex;"><span> StartPosition = <span style="color:#ff6ac1">new</span> Point(<span style="color:#ff9f43">0</span>, <span style="color:#ff9f43">10</span>),
</span></span><span style="display:flex;"><span> TargetPosition = <span style="color:#ff6ac1">new</span> Point(<span style="color:#ff9f43">5</span>, <span style="color:#ff9f43">1</span>)
</span></span><span style="display:flex;"><span> };
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="updating-the-find-neighbors-logic">Updating the Find-Neighbors Logic</h3>
<p>Now that we have that out of the way, we can update the logic that finds neighbor cells. Because the find-neighbors code
will be a bit more complicated compared to the top-down case, it seems a good idea to put this into its own class. Also,
let’s define an interface, because why not (actually, that let’s you easily implement different neighbor finders and
also makes unit testing easier, because you can mock the neighbor finder interface):</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">interface</span> <span style="color:#f3f99d">INeighborFinder</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> IEnumerable<CellCostPair> FindNeighbors(Cell cell, <span style="color:#9aedfe">bool</span> hasJumpCapability, GetDistanceDelegate getDistance);
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">delegate</span> <span style="color:#9aedfe">float</span> GetDistanceDelegate(Cell <span style="color:#ff6ac1">from</span>, Cell to);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The interface contains the following:</p>
<ul>
<li>A method <code>FindNeighbors</code> which takes the current cell, whether the game character has jump capability, and a
delegate with which the distance between two cells can be calculated (which in our case will be the good old
manhattan distance).</li>
<li>A delegate called <code>GetDistanceDelegate</code> which takes two cells and returns the distance in between.</li>
</ul>
<p>You’ll have noticed that <code>FindNeighbors</code> returns an <code>IEnumerable<CellCostPair></code>. Why not return <code>Cell</code>s? Because now,
that we’re not only looking at adjoining cells but also at neighbors of neighbors, it seems sensible to calculate the
cost to reach a certain neighboring cell right when we’re looking at them, especially in the jumping context.
Thus, we create a simple <a href="https://martinfowler.com/eaaCatalog/dataTransferObject.html" target="_blank" class="external-link" zgotmplz="">DTO</a> which contains a cell and the cost to reach that cell:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">struct</span> <span style="color:#f3f99d">CellCostPair</span>(Cell cell, <span style="color:#9aedfe">float</span> cost)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Cell Cell { <span style="color:#ff6ac1">get</span>; <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">set</span>; } = cell;
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#9aedfe">float</span> Cost { <span style="color:#ff6ac1">get</span>; <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">set</span>; } = cost;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Let’s continue with implementing the <code>FindNeighbors</code> method. This task can be split up into
finding neighboring cells reachable with and without jumping.</p>
<h4 id="cells-reachable-without-jumping">Cells reachable without Jumping</h4>
<p>Here is how to find cells that are reachable without jumping:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">NeighborFinder</span> : INeighborFinder
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> IEnumerable<CellCostPair> FindNeighbors(
</span></span><span style="display:flex;"><span> Cell sourceCell,
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">bool</span> hasJumpCapability,
</span></span><span style="display:flex;"><span> INeighborFinder.GetDistanceDelegate dist
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> cell <span style="color:#ff6ac1">in</span> FindCellsWithoutJumping(sourceCell, dist)) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> cell;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> IEnumerable<CellCostPair> FindCellsWithoutJumping(
</span></span><span style="display:flex;"><span> Cell cell,
</span></span><span style="display:flex;"><span> INeighborFinder.GetDistanceDelegate dist
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell.Down <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Down);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Down, cost);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell <span style="color:#ff6ac1">is</span> { IsLadder: <span style="color:#ff6ac1">true</span>, Up.IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Up);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Up, cost);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> canMoveSideways = cell.Down <span style="color:#ff6ac1">is</span> { IsEmpty: <span style="color:#ff6ac1">false</span> } || cell.IsLadder || cell.IsHangingBar;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (!canMoveSideways) <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">break</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell.Right <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Right);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Right, cost);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell.Left <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Left);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Left, cost);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>A nice thing we can do now is instead of fiddling with grid coordinates, we just use speaking property names like
<code>cell.Down</code>, which we can also combine with <a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#property-pattern" target="_blank" class="external-link" zgotmplz="">property patterns</a> to check if certain conditions are met.</p>
<p>The first bit deals with going down one cell. If the cell below isn’t a block then we can descend:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff6ac1">if</span> (cell.Down <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Down);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Down, cost);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The next part handles whether we can go up one cell. If the current cell is a ladder (i.e. we’re standing on a ladder)
and there is no block in the cell above, then we can ascend:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff6ac1">if</span> (cell <span style="color:#ff6ac1">is</span> { IsLadder: <span style="color:#ff6ac1">true</span>, Up.IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Up);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Up, cost);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The final sections focus on moving sideways. Here’s how we can determine whether the character can move to the right:
If the cell below isn’t empty, we’re on a ladder, or we’re hanging from bars, then we’re able to move sideways.
However, if the aforementioned condition isn’t met, then we can stop evaluating more neighbors.</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#9aedfe">var</span> canMoveSideways = cell.Down <span style="color:#ff6ac1">is</span> { IsEmpty: <span style="color:#ff6ac1">false</span> } || cell.IsLadder || cell.IsHangingBar;
</span></span><span style="display:flex;"><span><span style="color:#ff6ac1">if</span> (!canMoveSideways) <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">break</span>;
</span></span></code></pre></div><p>But, if we can move sideways in principle and the cell to the right isn’t a block, we can move one cell to the right:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff6ac1">if</span> (cell.Right <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Right);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Right, cost);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>It’s the same in the other direction, just the other way around (no kidding):</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff6ac1">if</span> (cell.Left <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">false</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = dist(cell, cell.Left);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(cell.Left, cost);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Now we’re ready to look at how to evaluate additional neighboring cells with jumping incorporated.</p>
<h4 id="add-jumping-to-the-mix">Add Jumping to the Mix</h4>
<p>Without further ado, here’s the additional code:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">NeighborFinder</span> : INeighborFinder
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> <span style="color:#ff6ac1">readonly</span> Point[] JumpRightTrajectoryDeltas = [
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">1</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">2</span>, -<span style="color:#ff9f43">2</span>),
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">3</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">4</span>, <span style="color:#ff9f43">0</span>)
</span></span><span style="display:flex;"><span> ];
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> <span style="color:#ff6ac1">readonly</span> Point[] JumpLeftTrajectoryDeltas = [
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">1</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">2</span>, -<span style="color:#ff9f43">2</span>),
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">3</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">4</span>, <span style="color:#ff9f43">0</span>)
</span></span><span style="display:flex;"><span> ];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> IEnumerable<CellCostPair> FindNeighbors(
</span></span><span style="display:flex;"><span> Cell sourceCell,
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">bool</span> hasJumpCapability,
</span></span><span style="display:flex;"><span> INeighborFinder.GetDistanceDelegate dist
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (!hasJumpCapability || sourceCell.Down <span style="color:#ff6ac1">is</span> not { IsBlock: <span style="color:#ff6ac1">true</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">break</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> cell <span style="color:#ff6ac1">in</span> FindCellsWithJumping(sourceCell, dist)) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> cell;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> IEnumerable<CellCostPair> FindCellsWithJumping(
</span></span><span style="display:flex;"><span> Cell cell,
</span></span><span style="display:flex;"><span> INeighborFinder.GetDistanceDelegate dist
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell.DownRight <span style="color:#ff6ac1">is</span> { IsEmpty: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Right?.Right <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Right?.UpRight <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> ) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> cellCostPair <span style="color:#ff6ac1">in</span> FindCellsOnJumpPath(cell, dist, JumpRightTrajectoryDeltas)) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> cellCostPair;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell.DownLeft <span style="color:#ff6ac1">is</span> { IsEmpty: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Left?.Left <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Left?.UpLeft <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> ) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> cellCostPair <span style="color:#ff6ac1">in</span> FindCellsOnJumpPath(cell, dist, JumpLeftTrajectoryDeltas)) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> cellCostPair;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> IEnumerable<CellCostPair> FindCellsOnJumpPath(
</span></span><span style="display:flex;"><span> Cell cell,
</span></span><span style="display:flex;"><span> INeighborFinder.GetDistanceDelegate dist,
</span></span><span style="display:flex;"><span> IEnumerable<Point> deltaDirection
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = <span style="color:#ff9f43">0f</span>;
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> previousCell = cell;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> delta <span style="color:#ff6ac1">in</span> deltaDirection) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> potentialNeighbor = cell.NeighborAt(delta.X, delta.Y);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (potentialNeighbor <span style="color:#ff6ac1">is</span> <span style="color:#ff6ac1">null</span> or { IsBlock: <span style="color:#ff6ac1">true</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">break</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> cost += dist(previousCell, potentialNeighbor);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(potentialNeighbor, cost);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> previousCell = potentialNeighbor;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>What’s happening here? First of all, we define the jump trajectories in form of deltas from the source cell,
one for a jump to the right and another one for a jump to the left. Looking at the trajectory definitions</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> <span style="color:#ff6ac1">readonly</span> Point[] JumpRightTrajectoryDeltas = [
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">1</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">2</span>, -<span style="color:#ff9f43">2</span>),
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">3</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">4</span>, <span style="color:#ff9f43">0</span>)
</span></span><span style="display:flex;"><span>];
</span></span><span style="display:flex;"><span><span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> <span style="color:#ff6ac1">readonly</span> Point[] JumpLeftTrajectoryDeltas = [
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(<span style="color:#ff9f43">0</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">1</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">2</span>, -<span style="color:#ff9f43">2</span>),
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">3</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">2</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">4</span>, -<span style="color:#ff9f43">1</span>), <span style="color:#ff6ac1">new</span>(-<span style="color:#ff9f43">4</span>, <span style="color:#ff9f43">0</span>)
</span></span><span style="display:flex;"><span>];
</span></span></code></pre></div><p>we see that applying these deltas to the start cell of a jump will unfold the jump path from earlier:</p>
<p></p><div class="flex-container">
<figure class="flex-item flex-item-2"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-trajectory-a.svg" width="100%">
</figure>
<figure class="flex-item flex-item-2"><img src="/posts/2024-09-04-a-star-pathfinding-side-view/images/jumping-trajectory-b.svg" width="100%">
</figure>
</div><p></p>
<p>Next, in the <code>FindNeighbors</code> method we check if the character has jumping capability as well as if the cell below is a
block. If that’s not the case, then jumping is impossible and we stop:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff6ac1">if</span> (!hasJumpCapability || sourceCell.Down <span style="color:#ff6ac1">is</span> not { IsBlock: <span style="color:#ff6ac1">true</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">break</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Next, we’ll jump in the <code>FindCellsWithJumping</code> method and look at the first case, which is jumping to the right:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> IEnumerable<CellCostPair> FindCellsWithJumping(
</span></span><span style="display:flex;"><span> Cell cell,
</span></span><span style="display:flex;"><span> INeighborFinder.GetDistanceDelegate dist
</span></span><span style="display:flex;"><span> )
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell.DownRight <span style="color:#ff6ac1">is</span> { IsEmpty: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Right?.Right <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Right?.UpRight <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> ) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> cellCostPair <span style="color:#ff6ac1">in</span> FindCellsOnJumpPath(cell, dist, JumpRightTrajectoryDeltas)) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> cellCostPair;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>First, we do a simple check if it makes “sense” to try jumping from the current position:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (cell.DownRight <span style="color:#ff6ac1">is</span> { IsEmpty: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Right?.Right <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> || cell.Right?.UpRight <span style="color:#ff6ac1">is</span> { IsBlock: <span style="color:#ff6ac1">true</span> }
</span></span><span style="display:flex;"><span> ) {
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span> }
</span></span></code></pre></div><p>If you look at the three conditions above you might notice that these map to the three example obstacles from earlier
(though in a different order):</p>
<ul>
<li>If <code>cell.DownRight is { IsEmpty: true }</code> is true then there’s a pit to the right.</li>
<li>If <code>cell.Right?.Right is { IsBlock: true }</code> holds true then there’s an obstacle to the right.</li>
<li>Finally, if <code>cell.Right?.UpRight is { IsBlock: true }</code> happens to evaluate to true then there’s a platform we might
be able to hop onto.</li>
</ul>
<p>Again, these are only simple checks to avoid “unnecessary” jumps. In the following step we make use of the trajectory deltas.</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> IEnumerable<CellCostPair> FindCellsOnJumpPath(
</span></span><span style="display:flex;"><span> Cell cell,
</span></span><span style="display:flex;"><span> INeighborFinder.GetDistanceDelegate dist,
</span></span><span style="display:flex;"><span> IEnumerable<Point> deltaDirection
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> cost = <span style="color:#ff9f43">0f</span>;
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> previousCell = cell;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> delta <span style="color:#ff6ac1">in</span> deltaDirection) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> potentialNeighbor = cell.NeighborAt(delta.X, delta.Y);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (potentialNeighbor <span style="color:#ff6ac1">is</span> <span style="color:#ff6ac1">null</span> or { IsBlock: <span style="color:#ff6ac1">true</span> }) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">break</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> cost += dist(previousCell, potentialNeighbor);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">new</span> CellCostPair(potentialNeighbor, cost);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> previousCell = potentialNeighbor;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>Basically, it’s nothing more than a loop over the trajectory deltas, adding each delta to the source cell, checking to
see if it’s a cell at all, and if it is, if it’s not a block either, accumulating the cost to reach each respective cell,
and yield-returning each cell and the cost. And that’s pretty much it.</p>
<p>As you can already imagine, jumping to the left is the same as jumping to the right, just with a different set of trajectory deltas.</p>
<h3 id="updating-the-rest">Updating the Rest</h3>
<p>We also need to update the pathfinding code a bit so we can use the new neighbor finder class. Here’s the updated code:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">AStarPathfinder</span>(Grid grid, INeighborFinder neighborFinder)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> List<Cell> _openSet = [];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> IEnumerator FindPathCoroutine(<span style="color:#9aedfe">bool</span> hasJumpCapability)
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> grid.Start.CostFromStart = <span style="color:#ff9f43">0</span>;
</span></span><span style="display:flex;"><span> grid.Start.CostToTarget = GetDistance(grid.Start, grid.Target);
</span></span><span style="display:flex;"><span> _openSet = [grid.Start];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">while</span> (_openSet.Count > <span style="color:#ff9f43">0</span>) {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> current = _openSet
</span></span><span style="display:flex;"><span> .OrderBy(c => c.CostToTarget)
</span></span><span style="display:flex;"><span> .ThenBy(c => GetDistance(c, grid.Target))
</span></span><span style="display:flex;"><span> .First();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (current == grid.Target) {
</span></span><span style="display:flex;"><span> ReconstructPath(current);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">break</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> _openSet.Remove(current);
</span></span><span style="display:flex;"><span> current.IsInOpenSet = <span style="color:#ff6ac1">false</span>;
</span></span><span style="display:flex;"><span> current.IsCurrentlyBeingExamined = <span style="color:#ff6ac1">true</span>;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">null</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">foreach</span> (<span style="color:#9aedfe">var</span> neighbor <span style="color:#ff6ac1">in</span> neighborFinder.FindNeighbors(current, hasJumpCapability, GetDistance)) {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (!IsNeighborWorthTrying(current, neighbor)) <span style="color:#ff6ac1">continue</span>;
</span></span><span style="display:flex;"><span> _openSet.Add(neighbor.Cell);
</span></span><span style="display:flex;"><span> neighbor.Cell.WasInspected = <span style="color:#ff6ac1">true</span>;
</span></span><span style="display:flex;"><span> neighbor.Cell.IsInOpenSet = <span style="color:#ff6ac1">true</span>;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">yield</span> <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">null</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> current.IsCurrentlyBeingExamined = <span style="color:#ff6ac1">false</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#9aedfe">bool</span> IsNeighborWorthTrying(Cell current, CellCostPair neighbor)
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> tentativeCost = current.CostFromStart + neighbor.Cost;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (tentativeCost >= neighbor.Cell.CostFromStart) <span style="color:#ff6ac1">return</span> <span style="color:#ff6ac1">false</span>;
</span></span><span style="display:flex;"><span> neighbor.Cell.Parent = current;
</span></span><span style="display:flex;"><span> neighbor.Cell.CostFromStart = tentativeCost;
</span></span><span style="display:flex;"><span> neighbor.Cell.CostToTarget = tentativeCost + GetDistance(neighbor.Cell, grid.Target);
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">return</span> !_openSet.Contains(neighbor.Cell);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> <span style="color:#ff6ac1">void</span> ReconstructPath(Cell current)
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> current.IsOnPath = <span style="color:#ff6ac1">true</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#9aedfe">var</span> walkingCell = current;
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">while</span> (walkingCell.Parent != <span style="color:#ff6ac1">null</span>) {
</span></span><span style="display:flex;"><span> walkingCell.Parent.IsOnPath = <span style="color:#ff6ac1">true</span>;
</span></span><span style="display:flex;"><span> walkingCell = walkingCell.Parent;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff5c57">static</span> <span style="color:#9aedfe">float</span> GetDistance(Cell a, Cell b)
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">return</span> Math.Abs(a.X - b.X) + Math.Abs(a.Y - b.Y);
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>In addition to the <code>Grid</code> object we pass an instance of <code>INeighborFinder</code> to the primary constructor. So far so good.
The next thing that should not go unnoticed is that the method <code>FindPathCoroutine</code> now, besides receiving a boolean parameter
for whether jumping is enabled, returns an instance of the
<a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerator" target="_blank" class="external-link" zgotmplz="">IEnumerator interface</a>.
But, if you take a look at the <code>yield return</code> statements, you will see that only <code>null</code> is returned. So what is this?</p>
<p>This is what might be called a <a href="https://en.wikipedia.org/wiki/Coroutine" target="_blank" class="external-link" zgotmplz="">coroutine</a>, which is a component whose execution
can be suspended and resumed later. As you may recall, the previous version of this method performed a single step of
the algorithm and then returned a boolean to indicate whether the algorithm had finished. The version presented here
does something similar, but instead of returning after a single step, it yields <code>null</code>, suspends execution, and then returns
control to the consumer of the enumerator.</p>
<p>Here’s how this thing is used, boiled down to just using the <code>AStarPathfinder</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">class</span> <span style="color:#f3f99d">Pathfinding</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">readonly</span> ControlPanel _controlPanel;
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">readonly</span> AStarPathfinder _pathFinder;
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">readonly</span> Grid _grid;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> IEnumerator _findPathCoroutine;
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#9aedfe">bool</span> _hasJumpCapability;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> Pathfinding()
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> _pathFinder = <span style="color:#ff6ac1">new</span> AStarPathfinder(_grid, <span style="color:#ff6ac1">new</span> NeighborFinder());
</span></span><span style="display:flex;"><span> _findPathCoroutine = _pathFinder.FindPathCoroutine(_hasJumpCapability);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> _controlPanel = <span style="color:#ff6ac1">new</span> ControlPanel(<span style="color:#78787e">/* ... */</span>);
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span> _controlPanel.StepButtonClicked += Step;
</span></span><span style="display:flex;"><span> _controlPanel.ResetButtonClicked += Reset;
</span></span><span style="display:flex;"><span> _controlPanel.JumpingButtonToggled += hasJumpCapability => {
</span></span><span style="display:flex;"><span> _hasJumpCapability = hasJumpCapability;
</span></span><span style="display:flex;"><span> Reset();
</span></span><span style="display:flex;"><span> };
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">public</span> <span style="color:#ff6ac1">void</span> Update(GameTime gameTime)
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#78787e">// ...</span>
</span></span><span style="display:flex;"><span> Step();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">void</span> Step()
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> <span style="color:#ff6ac1">if</span> (_isPathfindingFinished) <span style="color:#ff6ac1">return</span>;
</span></span><span style="display:flex;"><span> _isPathfindingFinished = !_findPathCoroutine.MoveNext();
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span> <span style="color:#ff5c57">private</span> <span style="color:#ff6ac1">void</span> Reset()
</span></span><span style="display:flex;"><span> {
</span></span><span style="display:flex;"><span> _grid.Reset();
</span></span><span style="display:flex;"><span> _findPathCoroutine = _pathFinder.FindPathCoroutine(_hasJumpCapability);
</span></span><span style="display:flex;"><span> _isPathfindingFinished = <span style="color:#ff6ac1">false</span>;
</span></span><span style="display:flex;"><span> }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>At the beginning and whenever we need to start over (e.g. because the grid changed) we call</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>_findPathCoroutine = _pathFinder.FindPathCoroutine(_hasJumpCapability);
</span></span></code></pre></div><p>which returns an <code>IEnumerator</code> over which we can iterate a single step by calling</p>
<div class="highlight"><pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>_findPathCoroutine.MoveNext()
</span></span></code></pre></div><p>which in turn returns <code>true</code> if the enumerator could be moved to the next element or <code>false</code> when we reached the end.</p>
<h2 id="interactive-example">Interactive Example</h2>
<p>At last, here’s the interactive example you’ve been waiting for:</p>
<div id="app-div" style="width: 320px; height: 416px;">
<iframe name="app-iframe" id="app-iframe" data-src="/blazor/pathfinding-side-view/wwwroot/index.html" src="about:blank" title="Pathfinding Example App" allow="fullscreen" referrerpolicy="origin" width="320" height="416">
</iframe>
<div class="link-load-app" onclick="loadApp(this)">
<u>Click to load app</u>
</div>
</div>
<script>
function loadApp(button) {
let iframe = document.getElementById("app-iframe");
iframe.src = iframe.getAttribute("data-src").valueOf();
button.parentNode.removeChild(button);
}
</script>
<p>This example lets you step through the algorithm step by step or watch it do its work. You can also manipulate
the grid by changing the cell types.</p>
<p>Compared to the other interactive example from one of the top-down posts, there’s an additional toggle switch which let’s you
control whether the character has jump capability. Here are the buttons explained:</p>
<ul>
<li> <img src="images/ButtonPlay.png" class="list-item-image">
<img src="images/ButtonStop.png" class="list-item-image">
<strong>Play/Stop</strong>: When first clicked, the algorithm will automatically run at a relatively slow pace so you can watch it in all its beauty; when clicked again, it will stop.</li>
<li> <img src="images/ButtonStep.png" class="list-item-image"> <strong>Step</strong>: Click to execute a single step of the algorithm.</li>
<li> <img src="images/ButtonReset.png" class="list-item-image"> <strong>Reset</strong>: Click to reset the algorithm.</li>
<li> <img src="images/ToggleShowParentOff.png" class="list-item-image">
<img src="images/ToggleShowParentOn.png" class="list-item-image">
<strong>Show/Hide Parent</strong>: When switched on, shows the relationship of the cells to their parent cells.</li>
<li><img class="list-item-image" src="images/ToggleJumpingOff.png">
<img class="list-item-image" src="images/ToggleJumpingOn.png"> <strong>Toggle Jumping</strong>: When switched on, jumping is enabled.</li>
</ul>
<p>Furthermore, you can edit the game world. By clicking one cell you can cycle through the different cell types. If you
click and hold, you can “paint” the world with the new cell type. Plus, you can drag the start (A) and target (B) cells
around.</p>
<p>When the algorithm is running, the cells change color:</p>
<ul>
<li> <img src="images/CellRed.png" class="list-item-image"> <strong>Red</strong>: This is the cell currently being examined.</li>
<li> <img src="images/CellOrange.png" class="list-item-image"> <strong>Orange</strong>: These cells are in the <code>openSet</code> and are about to be examined.</li>
<li> <img src="images/CellYellow.png" class="list-item-image"> <strong>Yellow</strong>: These cells have their parent cell set.</li>
<li> <img src="images/CellGreen.png" class="list-item-image"> <strong>Green</strong>: These cells are part of the path from A to B.</li>
</ul>
<p>Additionally, cells that are being checked by the algorithm display two numbers. The first number in the upper left
corner displays the cost from the start cell to this cell. The second number below shows the estimated cost from the start to the
destination via that cell.</p>
<p><strong>Example</strong>: <img src="images/CellNumbers.png" class="list-item-image"> The cost
from the start to this cell is <code>2</code>, and the estimated cost from the start to the target via this cell is <code>14</code>.</p>
<h2 id="conclusion">Conclusion</h2>
<p>In a nutshell, making the switch from a top-down approach to a side-view platformer brings a few challenges,
because of the involvement of gravity and the limited movement options. Characters can’t just go up and down in
the same way they would in a top-down scenario. Instead, they use ladders, hanging bars, and jumping to overcome obstacles in
the game world. We looked at how we could tweak the A* pathfinding algorithm and the neighbor-finding logic in particular
to make it work with these constraints.</p>
<h2>Resources</h2>
<ul>
<li>
<a href="https://monogame.net/" target="_blank" class="external-link">https://monogame.net/</a>
</li>
<li>
<a href="https://github.com/kniEngine/kni" target="_blank" class="external-link">https://github.com/kniEngine/kni</a>
</li>
<li>
<a href="https://martinfowler.com/eaaCatalog/dataTransferObject.html" target="_blank" class="external-link">https://martinfowler.com/eaaCatalog/dataTransferObject.html</a>
</li>
<li>
<a href="https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#property-pattern" target="_blank" class="external-link">https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/patterns#property-pattern</a>
</li>
<li>
<a href="https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerator" target="_blank" class="external-link">https://learn.microsoft.com/en-us/dotnet/api/system.collections.ienumerator</a>
</li>
<li>
<a href="https://en.wikipedia.org/wiki/Coroutine" target="_blank" class="external-link">https://en.wikipedia.org/wiki/Coroutine</a>
</li>
</ul>
</div>
<div class="info-box">
<p>This post is part of the <a href="https://shendriks.dev/series/a-pathfinding-in-2d-games/" style="font-weight: bold">A* Pathfinding in 2D Games</a> series.</p>
<ul class="list-group">
<li class="list-group-item">
<a href="https://shendriks.dev/posts/2024-07-13-a-star-pathfinding-in-2d-games-the-basics-for-top-down-scenarios/">
Part 1: A* Pathfinding Basics for a 2D Top-down Scenario
</a>
</li>
<li class="list-group-item">
<a href="https://shendriks.dev/posts/2024-08-24-a-star-pathfinding-top-down-blazorgl/">
Part 2: MonoGame/KNI Implementation of 2D Top-down A* Pathfinding
</a>
</li>
<li class="list-group-item active">
Part 3: A* Pathfinding in a 2D Side-View Scenario (this post)
</li>
</ul>
</div>
</article>
<hr>
<div class="post-info">
<p>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-tag meta-icon"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7" y2="7"></line></svg>
<span class="tag"><a href="https://shendriks.dev/tags/dev/">dev</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/gamedev/">gamedev</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/astar/">astar</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/platformer/">platformer</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/pathfinding/">pathfinding</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/monogame/">monogame</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/kni/">kni</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/top-down-view/">top-down view</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/side-view/">side-view</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/csharp/">csharp</a></span>
<span class="tag"><a href="https://shendriks.dev/tags/coroutine/">coroutine</a></span>
</p>
<p>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-file-text">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="16" y1="13" x2="8" y2="13"></line>
<line x1="16" y1="17" x2="8" y2="17"></line>
<polyline points="10 9 9 9 8 9"></polyline>
</svg>
4152 Words
</p>
</div>
<div class="pagination">
<div class="pagination__buttons">
<span class="button next">
<a href="https://shendriks.dev/posts/2024-08-24-a-star-pathfinding-top-down-blazorgl/">
<span class="button__text">A* Pathfinding in 2D Games: A simple Top-down Scenario implemented with MonoGame/KNI</span>
<span class="button__icon">→</span>
</a>
</span>
</div>
</div>
</main>
</div>
<footer class="footer">
<div class="footer__inner">
<div class="footer__content">
<span>© 2024 Sven Hendriks</span>
<span><a href="/impressum">Impressum/Datenschutz</a></span>
<span><a href="https://shendriks.dev/posts/index.xml" target="_blank" title="rss">RSS <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-rss"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg></a></span>
</div>
</div>
<div class="footer__inner">
<div class="footer__content">
<span>Powered by <a href="https://gohugo.io" target="_blank" class="external-link">Hugo</a></span><span>Theme based on <a href="https://github.com/rhazdon/hugo-theme-hello-friend-ng" target="_blank" class="external-link">Hello Friend NG</a></span>
</div>
</div>
</footer>
</div>
<a href="#" class="to-top-link" id="to-top-link"></a>
<script type="text/javascript" src="/bundle.min.9a3041a453f798a3033e80b035f055224350ffd45554a28651f2c8ee4d2ce611b6b1fca3bbf599b27d1a2e41c85410e709b225eed7003e867f103d1c7330807a.js" integrity="sha512-mjBBpFP3mKMDPoCwNfBVIkNQ/9RVVKKGUfLI7k0s5hG2sfyju/WZsn0aLkHIVBDnCbIl7tcAPoZ/ED0cczCAeg=="></script>
</body></html>