如何在Phaser.js中实现Minecraft/Terraria风格方块光照?
在Phaser中实现类似Minecraft/Terraria的方块光照效果
核心思路验证
你提到的思路完全正确:以天空区域(地图顶部可透光方块)为最高光照源(比如设为15级,和Minecraft一致),光照向下方/周围传播时,每穿过一个可透光方块等级降低1,最终每个方块的光照值由传播后的结果决定。
实现步骤详解
1. 为每个Tile维护光照数值
是的,必须为每个Tile单独存储光照值——这是这类方块光照效果的核心,因为每个位置的光照强度是独立的。你有两种实现方式:
- 方式一:利用Phaser Tile的内置数据
用tile.setData('lightLevel', value)给每个Tile附加光照等级数据,后续通过tile.getData('lightLevel')读取。 - 方式二:独立维护光照数组
创建一个和地图尺寸一致的二维数组lightMap,比如lightMap[x][y]直接存储对应坐标Tile的光照值,这种方式读取效率更高,适合大地图。
2. 初始化光照源
把地图顶部的可透光方块(比如空气Tile)的光照值设为最高等级(比如15),固体方块(比如石头、泥土)直接设为0(因为不透光,无法传播光照)。
示例代码片段:
// 假设map是你的Phaser Tilemap实例 const layer = map.getLayer('Ground').tilemapLayer; const lightMap = Array(map.width).fill().map(() => Array(map.height).fill(0)); const queue = []; // 初始化顶部行的光照 for (let x = 0; x < map.width; x++) { const tile = map.getTileAt(x, 0, true); if (tile && tile.properties.transparent) { // 假设你给可透光Tile加了transparent属性 lightMap[x][0] = 15; queue.push({x, y: 0}); } }
3. 光照传播计算(BFS算法)
用广度优先搜索来实现光照的逐层传播,确保每个方块的光照值是周围能传播过来的最高值:
const directions = [[-1,0], [1,0], [0,-1], [0,1]]; // 上下左右四个方向 while (queue.length > 0) { const {x, y} = queue.shift(); const currentLight = lightMap[x][y]; // 光照等级为0时停止传播 if (currentLight <= 1) continue; // 遍历四个方向 for (const [dx, dy] of directions) { const nx = x + dx; const ny = y + dy; // 检查坐标是否在地图范围内 if (nx < 0 || nx >= map.width || ny < 0 || ny >= map.height) continue; const tile = map.getTileAt(nx, ny, true); // 只有可透光Tile才能接收光照 if (!tile || !tile.properties.transparent) continue; // 计算新的光照值:当前光照-1 const newLight = currentLight - 1; // 如果新光照值比现有值高,才更新并加入队列 if (newLight > lightMap[nx][ny]) { lightMap[nx][ny] = newLight; queue.push({x: nx, y: ny}); } } }
4. 渲染时应用光照效果
有两种常用的渲染方式,根据你的需求选择:
方式一:半透明遮罩(简单高效)
在Tile渲染后,给每个Tile叠加一个黑色遮罩,光照值越低,遮罩透明度越高:// 在update或者render回调中更新Tile遮罩 layer.forEachTile(tile => { const lightLevel = lightMap[tile.x][tile.y]; // 计算透明度:光照15对应0(完全亮),0对应0.8(几乎全暗) const maskAlpha = 1 - (lightLevel / 15); // 可通过创建独立光照层绘制黑色矩形,避免影响原Tile透明度 const mask = this.add.rectangle(tile.pixelX, tile.pixelY, tile.width, tile.height, 0x000000, maskAlpha); mask.setOrigin(0); mask.setDepth(layer.depth + 1); });方式二:多亮度纹理(效果更细腻)
给每个方块预先生成16种亮度等级的纹理(从最亮到最暗),然后根据光照值切换对应的纹理:// 假设你已经加载了不同亮度的纹理,比如'dirt_0'到'dirt_15' layer.forEachTile(tile => { const lightLevel = lightMap[tile.x][tile.y]; const textureKey = `${tile.texture.key}_${lightLevel}`; tile.setTexture(textureKey); });
额外优化建议
- 局部更新:如果地图有方块被破坏/放置,只重新计算该区域周围3-5格的光照,不用全图重新计算,提升性能。
- 自定义透光率:比如玻璃完全透光(光照不减)、树叶衰减2级光照,只需修改传播时的
newLight计算逻辑即可。 - 动态光源:加入火把等动态光源时,只需把火把所在Tile的光照值设为对应等级(比如14),加入BFS队列重新传播即可。
内容的提问来源于stack exchange,提问作者Vatsa Pandey
相关产品推荐
相关产品推荐

