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.
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;
DaTaobao Tech
Official account of DaTaobao Technology
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.