JS游戏大尺寸地图程序化生成卡顿 求类似MC的种子分块生成方案
可行优化方案与实现思路
1. 随机数生成器改造(区域衔接的核心基础)
你当前使用的顺序型随机数生成器无法支持局部生成,因为随机值输出完全依赖调用顺序,生成不同区域的顺序变化会导致结果不一致,更无法保证边缘衔接。你需要换成基于坐标的确定性哈希随机数函数,输入固定种子、x坐标、y坐标就能输出唯一且固定的0-1随机值,和调用顺序完全无关,只要种子不变,同个坐标的随机值永远一致。
2. 全局元胞自动机改为局部带padding的分块计算
原有的全图迭代逻辑是性能瓶颈的核心,你可以把地图拆成固定大小的区块(比如64x64、128x128均可),每次生成单个区块时,根据元胞自动机迭代次数,额外扩展对应宽度的边界padding:比如单次迭代元胞自动机的影响范围是1格,要迭代5次就上下左右各多取5格的padding区域,把区块+padding一起参与迭代计算,计算完成后只保留中间正式区块的内容即可,这样相邻区块的边缘计算结果会完全一致,不会出现裂缝。
3. 实现区块按需加载与缓存
参考类Minecraft的区块加载逻辑:
- 仅加载光标/玩家视野范围内的区块,比如视距设为3块,只需加载周围7x7的区块即可,无需生成全图
- 生成过的区块存入内存缓存,移出视距超过一定时间的区块可以销毁,下次需要时因为种子固定,重新生成的结果和原来完全一致,不会出现内容变化
- 可以搭配Web Worker做后台异步生成,不会卡住主界面的交互操作
4. 现有代码性能优化点
- 用
Uint8Array等类型化数组替代普通数组存储地图数据,读写速度比普通JS数组快3-5倍 - 渲染逻辑放弃逐点
fillRect,改用ImageData批量写入像素数据后一次性提交到canvas,渲染速度能提升几十倍 - 你的多级缩放生成逻辑可以同步改造成局部化:低分辨率层的数值直接通过坐标哈希生成,无需先生成全量低分辨率地图
原始代码格式化后如下:
const scale = 0.25; let rngCount = 0; function* RNG() { let seed = Math.floor(Math.random() * 10000) / 10000; console.log(seed); const factor = 297; while(true) { seed = (seed * factor) - Math.floor(seed * factor); yield Math.floor(seed * 10000) / 10000; }; }; let generationRNG = RNG(); function clamp(min, max, value) { if(value < min) { return min; } else if(value > max) { return max; } else { return value; }; }; let map = []; let mapW = 4096; let mapH = 4096; function iterate(e, map) { const height = map.length; const width = map[0].length; let updatedMap, mapRow, neighborCount, iy, ix; for(i = 0; i < e; i++) { updatedMap = []; for(y = 0; y < height; y++) { mapRow = []; for(x = 0; x < width; x++) { neighborCount = 0; for(k = -1; k <= 1; k++) { for(l = -1 ; l <= 1; l++) { if(y + k < 0) { iy = height - 1; } else if(y + k >= height) { iy = 0; } else { iy = y + k; }; if(x + l < 0) { ix = width - 1; } else if(x + l >= width) { ix = 0; } else { ix = x + l; }; if(!(l == 0 && k == 0)) { neighborCount += map[iy][ix]; }; }; }; if(neighborCount >= 5) { mapRow.push(1); } else if(neighborCount <= 3) { mapRow.push(0); } else { mapRow.push(map[y][x]); }; }; updatedMap.push(mapRow); }; map = updatedMap; }; return map; }; function terrainGeneration() { let width = mapW / 16; let height = mapH / 16; let mapRow, factor = 4, fillPercent = 0.45, updatedMap, map = []; for(y = 0; y < height; y++) { mapRow = []; for(x = 0; x < width; x++) { if(generationRNG.next().value <= fillPercent) { mapRow.push(1); } else { mapRow.push(0); }; }; map.push(mapRow); }; map = iterate(10, map); for(a = 0; a < 2; a++) { if(a == 0) { fillPercent = 0.33; } else if (a == 1) { fillPercent = 0.2; }; width *= factor; height *= factor; updatedMap = []; for(y = 0; y < height; y++) { mapRow = []; for(x = 0; x < width; x++) { mapRow.push(map[Math.floor(y / factor)][Math.floor(x / factor)]); }; updatedMap.push(mapRow); }; map = updatedMap; updatedMap = []; for(y = 0; y < height; y++) { mapRow = []; for(x = 0; x < width; x++) { if(map[y][x] == 1) { if(generationRNG.next().value <= fillPercent) { mapRow.push(0); } else { mapRow.push(1); }; } else { if(generationRNG.next().value <= fillPercent) { mapRow.push(1); } else { mapRow.push(0); }; }; }; updatedMap.push(mapRow); }; map = updatedMap; map = iterate(5, map); }; return map; }; map = terrainGeneration(); for(i = 0; i < map.length; i++) { for(j = 0; j < map[i].length; j++) { c.fillStyle = "rgba(" + map[i][j] * 255 + ", " + map[i][j] * 255 + ", " + map[i][j] * 255 + ", 1)"; c.fillRect(j * scale, i * scale, scale, scale); }; }; c.fillStyle = "rgba(0, 0, 255, 1)"; c.fillRect(0, 0, 40 * scale, 40 * scale);
生成效果示例

内容的提问来源于stack exchange,提问作者Red Wizard
相关产品推荐
相关产品推荐

