<?xml version="1.0" encoding="UTF-8"?>

<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <atom:link rel="self" type="application/rss+xml" href="https://erredpath.uk/feed.xml"/>
    <title>Erredpath Blog</title>
    <link>https://erredpath.uk/blog/</link>
    <description>Posts on the studio&#39;s games and work.</description>
    <lastBuildDate>Sat, 17 Oct 2026 13:01:43 +0000</lastBuildDate>
    <item>
      <title>Lazy homing</title>
      <link>https://erredpath.uk/blog/lazy-homing</link>
      <guid isPermaLink="true">https://erredpath.uk/blog/lazy-homing</guid>
      <pubDate>Wed, 14 Oct 2026 00:00:00 +0000</pubDate>
      <atom:updated>2026-10-17T00:00:00Z</atom:updated>
      <description>How Erredpath® Tidings 1&#39;s projectiles home by a shared direction field.</description>
      <category>Erredpath_Tidings_1</category>
      <category>flow fields</category>
      <category>pathfinding</category>
      <category>C++</category>
      <content:encoded><![CDATA[<p>In <a class="wikilink" href="/Erredpath_Tidings_1">Erredpath® Tidings 1</a>, local historic protectors defend towns from the Bureau&rsquo;s barrage of enemies by constantly emitting projectiles that lazily home towards them.</p>
<p><span class="img-frame"><img src="https://erredpath.uk/assets/images/erredpath_tidings_1_level_1_rotation_normal.webp" alt="Snow White level, A top-down town square with a swirling circle of pale blue ground, a few birdlike creatures trailing yellow light, a bus, satellite dishes, and surrounding houses" width="840" height="465" loading="lazy"></span></p>
<p>When there are lots of projectiles and enemies on the screen, how do we home all of these towards the correct targets?</p>
<p><span class="img-frame"><img src="https://erredpath.uk/assets/images/erredpath_tidings_1_level_1_rotation_arrows_simple.webp" alt="A top-down town square with a sparse field of pale blue arrows spiraling around a central building and two small birdlike creatures trailing yellow light" width="840" height="465" loading="lazy"></span></p>
<p>Here we have enabled debug arrows which show the internal collection of velocities on a grid over the gameplay. If a projectile is in a grid cell, it will be pushed in the direction that the arrow is pointing. The magnitude is shown on the arrows colour from small to large with pale blue to magenta, respectively. You can see in the picture even with only two enemies in the play area, the arrows throughout the entire grid can be seen directing toward them. In between the two enemies you can see the grid arrows naturally flowing towards their nearest target in a fair/unweighted fashion; it visually makes sense why one of the arrows in between the enemies is pointing to the top left enemy and why the neighbouring arrow below or to the right of it is pointing in the opposite direction.</p>
<p>A <code>Propagator</code> is used in the game&rsquo;s update loop by all the enemies to tell the grid cells where a force is being applied:</p>
<pre><code class="language-cpp">Propagator new_propagate_enemy;
new_propagate_enemy.type = Propagator_Type::ENEMY;
int fetch_index = node_grid-&gt;world_pos_to_index(enemy_pos);
new_propagate_enemy.index = fetch_index;
new_propagate_enemy.pos = util_vector2(enemy_pos);
// `util_vector2(enemy_pos)` is used because the grid is in the X Z plane.
// The engine's Vector3 -&gt; Vector2 auto cast just takes the X Y members.
new_propagate_enemy.power = g_propagate_enemy_power;
node_grid-&gt;propagator.Add(new_propagate_enemy);
</code></pre>
<p>The polarity of the <code>.power</code> defines if projectiles should be repelled or attracted by the propagator. A more advanced enemy could throw an item that repels/attracts the player&rsquo;s projectiles forcing them to work around the item or use that device as a pivot point to aim their projectiles through (these mechanics were not explored in <a class="wikilink" href="/Erredpath_Tidings_1">Erredpath® Tidings 1</a>).</p>
<h2 id="homing">Homing</h2><p>We know when designing the game there might be lots of enemies and projectiles at the same time in the gameplay space. So the straightforward approach to home these projectiles would be to find the closest enemy, by checking against them all, then move towards it. This has a few issues, one being that all of the projectiles are now ultra accurate which results in the player deciding to not move because they can just stay in one spot and all their projectiles will be maximally effective. Secondly, this is potentially very slow due to all the calculations going on.</p>
<p>Here is the approach the game uses to solve this problem:</p>
<pre><code class="language-cpp">struct Homing_Info {
  bool filled;
  float X;
  float Y;
};

// ...

auto fetch_index = node_grid-&gt;world_pos_to_index(projectile_rigid_pos);
auto homing_pos_2d = Vector2(homing_infos[fetch_index].X, homing_infos[fetch_index].Y);
auto projectile_rigid_pos_2d = util_vector2(projectile_rigid_pos);
if (homing_infos[fetch_index].filled &amp;&amp;
    Vector2::DistanceSquared(homing_pos_2d, projectile_rigid_pos_2d) &lt; bullet_to_enemy_snap)
{
  // an enemy is recorded in this cell and is close: snap to it
  auto enemy_pos = Vector3(homing_pos_2d.X, projectile_rigid_pos.Y, homing_pos_2d.Y);
  projectile_rigid.SetPosition(enemy_pos);
} else {
  // no enemy close by: push in the direction the grid cell is pointing
  Vector2 propagate_enemy_dir = node_grid-&gt;grid[fetch_index].propagate_direction;
  auto force_magnitude = dt * bullet_base_speed;
  auto propagation_enemy_norm = util_vector3(propagate_enemy_dir).GetNormalized();
  projectile_rigid.AddForce(propagation_enemy_norm * force_magnitude, ForceMode::Acceleration);
}
</code></pre>
<p>When there are high numbers of enemies and projectiles, having this grid system is more performant. The enemies simply propagate their position and the grid flows the force of those positions through - creating an array of directions to the nearest enemy in all the cells. The projectiles in this method appear to have a &ldquo;turning circle&rdquo; due to the grid applying a force to the physics body of the projectile. For example if a projectile is moving east and reaches a cell that points north, the northward force is applied and the projectile&rsquo;s existing momentum keeps carrying it east for a few frames before its new direction accumulates; sometimes a projectile misses an enemy due to this but the player can mitigate that by moving themselves in line with the enemy&rsquo;s momentum which makes it less likely that the enemies can &ldquo;juke&rdquo;/&ldquo;feint&rdquo; these homing projectiles.</p>
<p>The <code>homing_info</code> records the position of the first enemy found in that cell (to be used as a snap target) which is then cleared and refilled every frame. This distance for snapping the projectiles to the enemy is adjustable per gameplay (the different levels in the game have slightly different gameplays with similar compatible control schemes). The Uncle Tailor level&rsquo;s projectiles are visually small and the camera is more zoomed out than other gameplays, this requires snapping adjustments to make the aiming more cohesive with the other gameplays so that it appears to snap at the same-ish visual/pixel distance.</p>
<h3 id="rectangular-grids">Rectangular Grids</h3><p>The first line of the previous code block calls <code>node_grid-&gt;world_pos_to_index</code>.</p>
<pre><code class="language-cpp">int Node_Grid::world_pos_to_index(Vector3 pos_in) {
  Vector3 pos_mut = pos_in;
  world_pos_in_grid_nearest(pos_mut); // clamps the position to be inside the grid
  float world_grid_pos = (pos_mut - grid_start) / grid_span_size;
  int i_grid_right = Math::TruncToInt(world_grid_pos.X);
  int i_grid_forward = Math::TruncToInt(world_grid_pos.Z);
  return i_grid_right + (i_grid_forward * grid_span_ratio);
}
</code></pre>
<p>The <code>grid_span_size</code> and the <code>grid_span_ratio</code> normalize the grid, allowing the gameplay area to be rectangular instead of only square. Which is how wider gameplay spaces work with this system, such as the Aunt Cinderella level.</p>
<p><span class="img-frame"><img src="https://erredpath.uk/assets/images/erredpath_tidings_1_level_2_fly_arrows.webp" alt="Cinderella level, A side view of the sky above a town with vertical dashed columns of pale blue arrows falling, a large green segmented creature aloft, and small flying creatures trailing yellow light" width="840" height="495" loading="lazy"></span></p>
<h2 id="initialising-the-grid">Initialising the Grid</h2><p>Every level&rsquo;s asset carries a <code>tri_params</code> actor that holds many things but the two that are needed for the grid are <code>tri_box_minimum</code> and <code>tri_box_maximum</code>. They are used for the corners that the grid will cover.</p>
<p>The grid is built at the start of the level from two markers in the editor:</p>
<p><span class="img-frame"><img src="https://erredpath.uk/assets/images/erredpath_tidings_1_level_1_rotation_editor_tri_box.webp" alt="the editor interface with the actor list on the left, the actors tri_box_minimum and tri_box_maximum highlighted beneath the tri_params entry, and the level viewport on the right showing the axis gizmos of both actors at opposite corners of the open square" width="840" height="407" loading="lazy"></span></p>
<p>Every frame, the propagators that were accumulated are injected into the grid and the forces are relaxed into the surrounding velocities over time.</p>
<pre><code class="language-cpp">// per cell, from `Node_Grid::job(int32 index)`
for (auto&amp; it : propagator) {
  if (it.type != g_cell_propagate_type ||
      it.index != index) continue;
  
  grid[index].propagate_strength += it.power * (Vector2::Distance(tower_pos_in_grid, it.pos) + 1.0f);
  auto propagator_dir = (it.power &gt; 0) ? (grid[index].pos - it.pos) : (it.pos - grid[index].pos);
  grid[index].propagate_direction += propagator_dir * it.power;
}

// ...

for (int i = 0; i &lt; 4; i += 1) {
  int grid_around = grid[index].around[i];
  if (grid_around == -1) continue;
  
  float around_strength = grid[grid_around].propagate_strength;
  grid[index].propagate_direction += direction_array[i] * around_strength * 0.25f;
  grid[index].propagate_strength += around_strength * 0.25f;
}

float remove_some_energy_from_the_system = 0.49f;
grid[index].propagate_strength *= remove_some_energy_from_the_system;
grid[index].propagate_direction *= remove_some_energy_from_the_system;
</code></pre>
<p>The propagator&rsquo;s strength and direction are added into its grid cell. Then the cell takes a quarter of its neighbours&rsquo; strength into itself and a quarter of a direction weighted by its neighbours&rsquo; strength. This cell&rsquo;s strength and direction are then multiplied down by <code>0.49f</code>; this decay keeps the system from blowing out and naturally resets the grid when there are no propagators affecting the grid.</p>
<p>The <code>grid[index].propagate_direction</code> is a <code>Vector2</code> on the cell itself, and the multiplier on it works the same way as the one on the <code>grid[index].propagate_strength</code>. The cell&rsquo;s direction accumulates across frames: its own propagator contribution and the neighbours&rsquo; directional blend are added on, and the result decays with the same <code>0.49f</code> factor.</p>
<pre><code class="language-javascript">// the post's grid at seven by seven: a strength and a direction per cell
const size = 7;
const cells = Array.from({ length: size * size }, () =&gt; ({
  strength: 0,
  direction: { x: 0, y: 0 },
}));
const direction_array = [
  { x: 1, y: 0 }, { x: -1, y: 0 }, { x: 0, y: 1 }, { x: 0, y: -1 },
];
const around = (index, i) =&gt; {
  const col = index % size + direction_array[i].x;
  const row = Math.floor(index / size) + direction_array[i].y;
  return col &gt;= 0 &amp;&amp; col &lt; size &amp;&amp; row &gt;= 0 &amp;&amp; row &lt; size ? col + row * size : -1;
};

// two enemies: one drifts, the mouse moves the other, a click flips its polarity
const enemy_a = { col: 2, row: 2, power: 1, t: 0 };
const enemy_b = { col: 4, row: 4, power: 1 };
// the box the page's wrapper carries is a fixed height, so the grid
// scales to fit it, its caption now in the prose beneath the widget
canvas.width = 392;
canvas.height = 392;
canvas.style.cssText =
  &quot;display:block;margin:0 auto;aspect-ratio:1;touch-action:none;width:min(100%, var(--interactive-code-height, 20rem))&quot;;
const cell_at = (event) =&gt; {
  const box = canvas.getBoundingClientRect();
  return {
    col: Math.max(0, Math.min(size - 1, Math.floor(((event.clientX - box.left) / box.width) * size))),
    row: Math.max(0, Math.min(size - 1, Math.floor(((event.clientY - box.top) / box.height) * size))),
  };
};
const press = { down: false, col: 0, row: 0 };
canvas.addEventListener(&quot;pointerdown&quot;, guard((event) =&gt; {
  canvas.setPointerCapture(event.pointerId);
  press.down = true;
  press.col = enemy_b.col;
  press.row = enemy_b.row;
  if (event.pointerType !== &quot;mouse&quot;) Object.assign(enemy_b, cell_at(event));
}));
canvas.addEventListener(&quot;pointermove&quot;, guard((event) =&gt; {
  // the mouse drags on hover, a finger only while it is down
  if (event.pointerType === &quot;mouse&quot; || press.down) Object.assign(enemy_b, cell_at(event));
}));
canvas.addEventListener(&quot;pointerup&quot;, guard(() =&gt; {
  // a press released on the cell it began on is the click or the tap
  if (enemy_b.col === press.col &amp;&amp; enemy_b.row === press.row) enemy_b.power = -enemy_b.power;
  press.down = false;
}));
const ctx = canvas.getContext(&quot;2d&quot;);

// a projectile with momentum, pushed by the cell it sits in
const shot = { x: 3.5, y: 0.4, vx: 0.4, vy: 0 };

function frame() {
  // the per-cell update, the post's own structure: the enemies' contribution,
  // a quarter of each neighbour's strength, the 0.49 decay
  enemy_a.t += 0.02;
  enemy_a.col = Math.round(3 + 1.8 * Math.cos(enemy_a.t));
  enemy_a.row = Math.round(3 + 1.8 * Math.sin(enemy_a.t));
  for (let index = 0; index &lt; cells.length; index += 1) {
    const cell = cells[index];
    for (const enemy of [enemy_a, enemy_b]) {
      if (enemy.col + enemy.row * size !== index) continue;
      cell.strength += enemy.power * 8;
    }
    for (let i = 0; i &lt; 4; i += 1) {
      const grid_around = around(index, i);
      if (grid_around === -1) continue;
      const around_strength = cells[grid_around].strength;
      cell.direction.x += direction_array[i].x * around_strength * 0.25;
      cell.direction.y += direction_array[i].y * around_strength * 0.25;
      cell.strength += around_strength * 0.25;
    }
    cell.strength *= 0.49;
    cell.direction.x *= 0.49;
    cell.direction.y *= 0.49;
  }

  // the projectile: its momentum carries it on, the cell it lands in pushes it
  shot.x = (shot.x + shot.vx + size) % size;
  shot.y = (shot.y + shot.vy + size) % size;
  const shot_cell = cells[Math.floor(shot.x) + Math.floor(shot.y) * size];
  const push = Math.hypot(shot_cell.direction.x, shot_cell.direction.y);
  if (push &gt; 0.01) {
    shot.vx += (shot_cell.direction.x / push) * 0.012;
    shot.vy += (shot_cell.direction.y / push) * 0.012;
  }
  shot.vx *= 0.98;
  shot.vy *= 0.98;

  // the debug arrows, the post's pale blue to magenta, over the grid
  const px = 392 / size;
  ctx.clearRect(0, 0, canvas.width, canvas.height);
  ctx.strokeStyle = &quot;rgba(60, 50, 40, 0.1)&quot;;
  ctx.lineWidth = 1;
  for (let i = 1; i &lt; size; i += 1) {
    ctx.beginPath(); ctx.moveTo(i * px, 0); ctx.lineTo(i * px, 392); ctx.stroke();
    ctx.beginPath(); ctx.moveTo(0, i * px); ctx.lineTo(392, i * px); ctx.stroke();
  }
  for (let index = 0; index &lt; cells.length; index += 1) {
    const cell = cells[index];
    const magnitude = Math.hypot(cell.direction.x, cell.direction.y);
    if (magnitude &lt; 0.03) continue;
    const clamped_magnitude = Math.min(1, magnitude);
    const x = (index % size) * px + px / 2;
    const y = Math.floor(index / size) * px + px / 2;
    const dx = cell.direction.x / magnitude, dy = cell.direction.y / magnitude;
    const reach = 6 + 16 * clamped_magnitude;
    ctx.strokeStyle = ctx.fillStyle = `hsl(${210 + 90 * clamped_magnitude}, 100%, ${92 - 50 * clamped_magnitude}%)`;
    ctx.lineWidth = 1.5;
    ctx.beginPath();
    ctx.moveTo(x - dx * reach, y - dy * reach);
    ctx.lineTo(x + dx * reach, y + dy * reach);
    ctx.stroke();
    ctx.beginPath();
    ctx.moveTo(x + dx * reach, y + dy * reach);
    ctx.lineTo(x + dx * reach - (dx - dy * 0.4) * 5, y + dy * reach - (dy + dx * 0.4) * 5);
    ctx.lineTo(x + dx * reach - (dx + dy * 0.4) * 5, y + dy * reach - (dy - dx * 0.4) * 5);
    ctx.closePath();
    ctx.fill();
  }
  for (const enemy of [enemy_a, enemy_b]) {
    // filled for attracting, hollow for repelling
    const x = enemy.col * px + px / 2, y = enemy.row * px + px / 2;
    ctx.beginPath();
    ctx.arc(x, y, 8, 0, Math.PI * 2);
    if (enemy.power &gt; 0) {
      ctx.fillStyle = &quot;#78948b&quot;;
      ctx.fill();
    } else {
      ctx.fillStyle = &quot;#fdfcf9&quot;;
      ctx.fill();
      ctx.strokeStyle = &quot;#2b2622&quot;;
      ctx.lineWidth = 2;
      ctx.stroke();
    }
  }
  ctx.beginPath();
  ctx.arc(shot.x * px, shot.y * px, 4, 0, Math.PI * 2);
  ctx.fillStyle = &quot;#ecb315&quot;;
  ctx.fill();
  ctx.strokeStyle = &quot;#7a5c3e&quot;;
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(shot.x * px, shot.y * px);
  ctx.lineTo(shot.x * px + shot.vx * 24, shot.y * px + shot.vy * 24);
  ctx.stroke();
}

// the loop pauses while the source view is on screen, the wrapper's
// hidden event the cue
let rafId = 0;
function tick() {
  frame();
  rafId = requestAnimationFrame(tick);
}
root.addEventListener(&quot;live-demo-hidden&quot;, (event) =&gt; {
  if (event.detail.hidden) {
    cancelAnimationFrame(rafId);
    rafId = 0;
  } else if (!rafId) {
    rafId = requestAnimationFrame(tick);
  }
});
rafId = requestAnimationFrame(tick);
</code></pre>
<p>Two enemies on a 7x7 grid: the mouse, or a finger, moves one of them and a click, or a tap, flips its polarity between attracting and repelling. The small dot flying around is a projectile.</p>
<h2 id="downsides">Downsides</h2><p>This grid is updated every frame but there&rsquo;s really no benefit to it. The system is intended to be coarsely accurate - so having it update so frequently is unnecessary. If the grid were larger than the 19 x 19 grid we use, it would make sense to run this on a separate thread at 10 fps - it&rsquo;d still function the same as it does currently (the game&rsquo;s framerate issues entirely stem from how the UI is used by the game, a new system will be made for the sequel). Initially the <code>node_grid</code> was entirely running on a &ldquo;job&rdquo;/&ldquo;task graph&rdquo; <a href="https://docs.flaxengine.com/manual/scripting/advanced/multithreading.html#task-graph">system</a> (probably should have just used a thread) but the single-threaded version ran faster (skill issue) so that was used instead.</p>
<p>After the gameplay code was complete, it became clear that this system is likely over-engineered for what this game demands. It would have been simpler to have a grid and, for each cell, get the direction and distance of the nearest target, then have the projectiles query that grid instead of this odd flowing system. PhysX&rsquo;s <code>AddForce</code> is what drives the &ldquo;turning circle&rdquo; effect, so this method would be &lsquo;gameplay compatible&rsquo;.</p>
<p>Currently, this system we have is under-engineered in terms of enabling a &ldquo;gravity well&rdquo;, meaning: it&rsquo;s not possible to create an attractor point in the grid that pulls objects towards itself and holds them there. Currently this force system would just throw an attracted object through the attractor point due to its momentum. <del>This could be implemented by not using PhysX and instead making a different description of how spatial positions work so that an attractor can still pull objects towards itself whilst making the spatial positions around itself denser. The force on an object would still move towards the attractor at the same spatial speed but because the space is denser nearer the attractor the object would appear to be slowing down/sticking to the attractor point (there&rsquo;s probably an easier way to do this - skill issue). A more developed approach will be attempted in the sequel to <a class="wikilink" href="/Erredpath_Tidings_1">Erredpath® Tidings 1</a> (perhaps we could just place an invisible sphere collider at the attractor point).</del> A bunch of PhysX spring constraints could be used to mimic a &ldquo;gravity well&rdquo; effect.</p>
<h2 id="debug-display">Debug Display</h2><p>The arrows in the pictures are a macro option that can be turned on to view the behaviour of the field:</p>
<pre><code class="language-cpp">#if DEBUG_ARROW
// in PreExecute, before the relaxation: the frame's weakest and strongest cells
length_max = grid[0].propagate_strength;
length_min = grid[0].propagate_strength;

for (auto&amp; it : grid) {
  if (it.propagate_strength &lt; length_min) length_min = it.propagate_strength;
  else if (it.propagate_strength &gt; length_max) length_max = it.propagate_strength;
}
#endif

...

#if DEBUG_ARROW
// in Node_Grid::job, after the relaxation, per cell
float g_cell_color_lerp = (grid[index].propagate_strength - length_min + 0.0001f)
                          / Math::Abs(length_max + 0.001f);
float g_cell_color_length = Math::Lerp(195.0f, 355.0f, g_cell_color_lerp);
auto g_cell_quat = Quaternion::Identity;

if (grid[index].propagate_direction != Vector2::Zero) {
  g_cell_quat = util_quat_from_translation(grid[index].propagate_direction);
}
grid[index].debug_arrow-&gt;SetOrientation(g_cell_quat);
Color color_arrow = Color::FromHSV(g_cell_color_length, 1.0f, 1.0f);

for (auto&amp; it : grid[index].debug_arrow_uber) {
  util_set_parameter_value(*it, TEXT(&quot;Color&quot;), color_arrow);
}
#endif
</code></pre>
<p>At the start of a level, a prefab arrow is spawned at the center of each grid cell and scaled to the smaller of the cell&rsquo;s two dimensions so the arrow fits roughly in its own cell, and each cell keeps a reference to its arrow and to the material the arrow&rsquo;s model uses so its parameters can be changed during gameplay.</p>
<h2 id="visual-hinting">Visual Hinting</h2><p>The propagator&rsquo;s strength is reused to drive some of the visuals for the ground (or sky for side view levels). The strength is used in an array to set the pixels of a texture, one pixel per cell, and the texture is used for the materials:</p>
<pre><code class="language-cpp">float scale_enemy = Math::Clamp(g_cell.propagate_strength * v_scale_enemy, 0.0f, 1.0f);
process_tri-&gt;tri_texture_code-&gt;set_pixel(index, Color32(Color(scale_enemy, 0.0f, 0.0f)));
</code></pre>
<p><span class="img-frame"><img src="https://erredpath.uk/assets/images/erredpath_tidings_1_level_1_rotation_arrows_hectic.webp" alt="A top-down town square whose dense swirling field of cyan arrows flows around a central building, with magenta arrows and small birdlike creatures trailing yellow light" width="840" height="465" loading="lazy"></span></p>
<p>On the Aunt Snow White level the material uses the <code>tri_texture_code</code> to color the ground with a gold glow popping against the dark blue. The player is on the right side of the screen and moving further to the right so it&rsquo;s fair to assume that the player&rsquo;s eyes are looking at the right side of the screen. The gold glow implicitly informs the player that there is a mass of enemies on the left side of the screen that they need to be aware of as they plan their next actions.</p>
<p><span class="img-frame"><img src="https://erredpath.uk/assets/images/erredpath_tidings_1_level_3_defence_arrows.webp" alt="Uncle Tailor level, A side view of the sky above a town with dashed columns of pale blue and magenta arrows falling, birdlike creatures descending, and a small figure standing on a stone platform in the foreground" width="840" height="397" loading="lazy"></span></p>
<p>The same approach is used for the &ldquo;side view&rdquo; levels (which are still top down - the art is just oriented to appear to be on a side view). This is the Uncle Tailor level; here the player is slightly on the left side of the screen and is probably looking at the bigger enemies above. The blues of the sky are darkened on the right side so the player feels there are incoming enemies over there.</p>
<h2 id="further-work">Further Work</h2><p>This system was created from a sort of off-the-cuff process: &ldquo;there&rsquo;s going to be a problem when there are lots of things.. let&rsquo;s try to have a weird grid maybe.. oh it&rsquo;s working, ok I guess we&rsquo;ll use that then.&rdquo; It was only after gameplay was completed that we found that many places have &ldquo;flow fields/grids&rdquo; that appear to be better versions of what we ended up creating, maybe. So we look forward to going back over those existing methods to help us build something new for the sequel to <a class="wikilink" href="/Erredpath_Tidings_1">Erredpath® Tidings 1</a>.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
