Game Development 18 min read

Game Development Techniques: Pathfinding, Depth Sorting, and Parallax Effects in a 2D Chicken Game

The article walks through building a 2D chicken game, contrasting a costly ray‑casting pathfinder with an optimal A* grid‑based solution, detailing node classes, heuristics, and JavaScript code, while also covering parallax scrolling, bottom‑Y depth sorting, a unified timer system, and a dual Phaser‑Rax rendering architecture.

DaTaobao Tech
DaTaobao Tech
DaTaobao Tech
Game Development Techniques: Pathfinding, Depth Sorting, and Parallax Effects in a 2D Chicken Game

The article details the development of a 2D interactive chicken game, focusing on technical challenges and solutions.

It compares two pathfinding approaches: a ray‑casting method and the A* algorithm, explaining the ray‑casting steps, its drawbacks (high computational cost, non‑optimal paths, limited to simple obstacles), and then presents A* with grid‑based nodes, heuristic (Manhattan distance), open/closed lists, and a JavaScript implementation.

The author describes map discretization into 50×50 cells, obstacle marking, and how the A* yields optimal routes.

To achieve visual depth, the article covers parallax scrolling (different layer speeds), depth sorting based on object bottomY coordinates, and a simple shadow/illustration approach.

Additional systems include a unified timer manager for guiding users, a dual‑renderer architecture (Phaser + Rax), and code snippets for the timer, parallax factors, depth‑sorting logic, and A* node class.

Overall, the piece records the author’s problem‑solving process in game development, contrasting traditional front‑end work with game‑specific techniques.

// A*算法 Node 类,用于存储节点信息
class Node {
  constructor(parent = null, position = null) {
    this.parent = parent;    // 父节点
    this.position = position; // 节点在网格中的坐标位置
    this.g = 0;              // G值是从起点走到当前格子的成本
    this.h = 0;              // H值是当前格子到终点的估计成本
    this.f = 0;              // F值是G值和H值的总和
  }
  // 判断两个节点是否位于同一个位置
  isEqual(otherNode) {
    return this.position[0] === otherNode.position[0] && this.position[1] === otherNode.position[1];
  }
}

// 启发式函数,用于估计到达目标的成本(此处使用曼哈顿距离)
function heuristic(nodeA, nodeB) {
  const d1 = Math.abs(nodeB.position[0] - nodeA.position[0]);
  const d2 = Math.abs(nodeB.position[1] - nodeA.position[1]);
  return d1 + d2;
}

// 获取一个节点的所有可能的邻居(包括对角线上的位置)
function getNeighbors(currentNode, grid) {
  const neighbors = [];
  // 这里包括了八个方向上的移动
  const directions = [
    [-1, -1], [-1, 0], [-1, 1], // 左上 左 左下
    [0, -1], [0, 1],  // 上    下
    [1, -1], [1, 0], [1, 1],     // 右上 右 右下
  ];
  // 查看每个方向的邻居是否可通行(非障碍)且在网格范围内
  for (const direction of directions) {
    const neighborPos = [
      currentNode.position[0] + direction[0],
      currentNode.position[1] + direction[1],
    ];
    // 确保位置在网格内且不是障碍物
    if (
      neighborPos[0] >= 0 && neighborPos[0] < grid.length &&
      neighborPos[1] >= 0 && neighborPos[1] < grid[0].length &&
      grid[neighborPos[0]][neighborPos[1]] === 1
    ) {
      neighbors.push(new Node(currentNode, neighborPos));
    }
  }
  return neighbors;
}

// A* 算法主函数
function aStar(grid, start, end) {
  const startNode = new Node(null, start);
  const endNode = new Node(null, end);
  let openSet = [startNode]; // 存储待检查的节点
  let closedSet = []; // 存储已检查的节点
  while (openSet.length > 0) {
    console.log('startNode');
    // 在openSet中找到F值最低的节点
    let lowestIndex = 0;
    for (let i = 0; i < openSet.length; i++) {
      if (openSet[i].f < openSet[lowestIndex].f) {
        lowestIndex = i;
      }
    }
    let currentNode = openSet[lowestIndex];
    // 如果当前节点是目的地,那么我们再次构造路径
    if (currentNode.isEqual(endNode)) {
      let path = [];
      let current = currentNode;
      while (current != null) {
        path.push(current.position);
        current = current.parent;
      }
      return path.reverse(); // 把数组反转,因为我们是从终点回溯到起点存储的
    }
    // 当前节点已经被处理过,移出openSet,并加入closedSet
    openSet.splice(lowestIndex, 1);
    closedSet.push(currentNode);
    // 找到所有邻居
    let neighbors = getNeighbors(currentNode, grid);
    for (let neighbor of neighbors) {
      // 如果邻居是不可访问的或已在closedSet中,忽略它们
      // 如果这个邻居在关闭列表中,跳过它
      if (closedSet.some(closedNode => closedNode.isEqual(neighbor))) {
        continue;
      }
      // 对角线移动的成本要考虑 √2
      // 通过查看相邻节点和当前节点的坐标差来判断是否为对角移动
      const isDiagonalMove = Math.abs(currentNode.position[0] - neighbor.position[0]) === 1 && Math.abs(currentNode.position[1] - neighbor.position[1]) === 1;
      // 对角线移动的成本假定为 √2,其他为1
      const tentativeG = currentNode.g + (isDiagonalMove ? Math.sqrt(2) : 1);
      // 如果新的G值更低,或者邻居节点不在开放列表中
      let openNode = openSet.find(openNode => openNode.isEqual(neighbor));
      if (!openNode || tentativeG < neighbor.g) {
        neighbor.g = tentativeG;
        neighbor.h = heuristic(neighbor, endNode);  // H值不变,因为它是启发式估计到终点的成本
        neighbor.f = neighbor.g + neighbor.h;
        neighbor.parent = currentNode;
        // 如果邻居节点不在开放列表中,加入开放列表
        if (!openNode) {
          openSet.push(neighbor);
        }
      }
    }
    // 如果循环结束还没有到达终点,表示没有路径到达终点
    return [];
  }
}
// 远景x轴速率
export const parallaxFactorFarX = 0.2;
// 远景y轴速率
export const parallaxFactorFarY = 0.2;
// 初始坐标
export const farOriginXY = [0, -60];
// 近景x轴速率
export const parallaxFactorNearX = 1.8;
// 近景y轴速率
export const parallaxFactorNearY = 1.05;
// 初始坐标
export const nearOriginXY = [0, gameHeightBounds - 420];

const { scrollX, scrollY } = this.scene.cameras.main;
if (scrollX >= 0 && scrollX <= 750 && scrollY >= 0 && this.bgFar && this.bgNear) {
  this.bgFar.x = -scrollX * parallaxFactorFarX;
  this.bgFar.y = -scrollY * parallaxFactorFarY + farOriginXY[1];
  this.bgNear.x = -scrollX * parallaxFactorNearX;
  this.bgNear.y = -scrollY * parallaxFactorNearY + nearOriginXY[1];
}
// 划分区间
const divideRegional = (blocks: Array<{ id: number, bottomY: number }>) => {
  blocks.sort((a, b) => a.bottomY - b.bottomY);
  return blocks.map((item, idx) => {
    const nextBottomY = blocks[idx + 1] ? blocks[idx + 1].bottomY : Infinity;
    return {
      regional: [(idx + 1) * 100, (idx + 1) * 100 + 100],
      range: [item.bottomY, nextBottomY],
      ...item
    };
  });
}
// 行走时的判断
const currentY = chicken.getPosition().y + 130;
const currentRegion = regionals.find((rengional) => {
  const [start, end] = rengional.range;
  return currentY >= start && currentY <= end;
});
if (currentRegion) {
  chicken.setDepth(currentRegion.regional[0] + 1);
} else {
  chicken.setDepth(99);
}
function ProcessTimer() {
  let id = 0;
  let hasEmit = false;
  const timers = {};
  const flags = {};
  const types: any = {};
  let func: any;

  const run = (cb) => {
    func = cb;
  };

  const startTimer = (type, delayTime) => {
    // 触发过或者定时器存在
    if (hasEmit || timers[type]) return;
    timers[type] = setTimeout(() => {
      flags[type] = true;
      checkTimer();
    }, delayTime);
  };

  const checkTimer = () => {
    const keys = Object.keys(timers) || [];
    const notSatisfied = keys.find(key => !flags[key]);
    // 满足所有的条件,出任务触点,只出一次
    if (!notSatisfied && !hasEmit) {
      hasEmit = true;
      clearAllTimer();
      if (func && typeof func === 'function') {
        func();
      }
    }
  };

  const clearAllTimer = () => {
    const keys = Object.keys(timers) || [];
    keys.forEach(key => {
      clearTimer(key);
    });
  };

  const clearTimer = (type) => {
    flags[type] = false;
    if (timers[type]) {
      clearTimeout(timers[type]);
      timers[type] = null;
    }
  };

  const create = (delayTime = 8000) => {
    const type = `timer${id++}`;
    timers[type] = null; // 所有定时器
    flags[type] = false;
    types[type] = delayTime;
    return {
      start: () => { startTimer(type, delayTime); },
      end: () => { clearTimer(type); },
      type,
    };
  };

  const reset = () => {
    hasEmit = false;
  }

  return {
    create,
    run,
    clearAllTimer,
    reset,
  };
}

export default ProcessTimer;
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaScriptGame DevelopmentA* algorithmdepth sortingparallax scrollingPathfinding
DaTaobao Tech
Written by

DaTaobao Tech

Official account of DaTaobao Technology

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.