最近基于单形噪声和柏林噪声研究设计了一个梯度循环噪声算法,这个算法是为了独立游戏的开发服务的。噪声算法目前已经实现,接下来需要基于它设计实现地图生成算法,以此生成独立游戏所需的地图、植被等数据。
地图生成算法要求生成多种不同的地形地貌、植被类型,并且可生成江河湖海。在实现以上目标的同时,还需要保证地图任意方向上都具备连续性。也就是说,当玩家向一个方向不断前进时,地图边界与另一侧边界保持连续状态。此外,算法要求任意体素位置上的数据不依赖其余体素的数据,而且时间复杂度要尽可能得低。
注’ 以下为设计与实现部分,主要阐述设计思想。
在进行地图生成之前,需要先对噪声算法进行改造。由于柏林噪声算法的时间复杂度较高,因此此处选择以单形噪声作为基底。通过对算法进行适当的改造,实现生成梯度循环连续的噪声图。

此图与常规噪声算法图的差异就在于其梯度循环连续,重复其自身置于其周围,图像依旧连续。效果如下图所示:

关于梯度循环噪声算法读者可自行实现,或参考作者的其它文章,此处不做过多赘述。
为了确保地图生成算法在任意系统平台上都能输出一致的结果,读者需要确保使用的随机数算法是具备跨平台确定性的。也就是说,当种子和迭代步数相同的情况下,输出的随机值也是相同的。
注’ 若读者并不需要考虑跨平台的确定性,那随便用一个随机数生成算法便可
在实现地图生成之前,需要先获取多个噪声生成实例,后面将会基于这些实例生成的噪声数据去构成世界生成的基本信息。
import math
import numpy as np
import matplotlib.pyplot as plt
# 循环流式单形噪声生成算法
import RingFlowSimplexNoise as rfsn
# 跨平台确定性伪随机数生成算法
import RcgRandom as RCG
rdm1 = RCG(seedValue)
randomFun1 = lambda : rdm1.random()
rdm2 = RCG(seedValue + 1)
randomFun2 = lambda : rdm2.random()
rdm3 = RCG(seedValue + 2)
randomFun3 = lambda : rdm3.random()
rdm4 = RCG(seedValue + 3)
randomFun4 = lambda : rdm4.random()
# 基础噪声(海拔)
basicN = rfsn.createNoise2D(randomFun1)
# 流体噪声(流体&气温)
flowN = rfsn.createNoise2D(randomFun2)
# 优化噪声
optimizeN = rfsn.createNoise2D(randomFun3)
# 随机值random
randN = randomFun4
# 以上噪声函数和随机函数都仅生成 [0,1] 区间内的值
在上述代码中,生成了基本的海拔信息、流体信息、优化噪声,并且获取了一个确定性随机函数作为地图内部分数据的生成依据。
拥有噪声函数和随机函数后,需要构造一个世界生成入口函数,这是生成一整个世界的基本入口。
def WorldGeneration(
# 生成器数组
arrGeneration,
# 地图尺寸
wh = 200,
# 地图重复次数控制
diameter = 0.25,
optDiameter = 0.8,
flowDiameter = 0.5,
# 地图细粒度控制
# 河流宽度
streamWidth = 0.02,
# 海平面高度
seaLevel = 0.1,
# 沙滩宽度
sandWidth = 0.05,
# 雪域海拔
snowLevel = 0.95,
# 岩层宽度
rockyAreaWidth = 0.05,
# 植被密度
vegetationDensity = 0.1
):
#
# <--- 这里写具体逻辑
#
上述代码中的生成器数组是地图元素的生成类实例数组,后续会将生成器实例传递进来,以此生成相关地图数据。例如,当传入岩层生成器时,地图上就会生成岩层数据。
在WorldGeneration函数内部,需要对传入的参数进行规范化,使其贴合实际运算范围。并且在进行实际
def WorldGeneration(...):
### 作者这里使用图片作为数据存储和展示的示例,因此初始化图片数据
channels = 3 # BGR
# 创建一张空白的RGB图像(地形)
srcImage = np.zeros((wh, wh, channels), dtype=np.uint8)
# 创建一张空白的RGB图像(置物)
objectImage = np.zeros((wh, wh, channels), dtype=np.uint8)
### 下面是计算数据的标准化,开发者可根据自己的需要进行定制
whHalf = wh / 2.0
# 基础高度图
RECTrange = wh * diameter
RErange = wh / RECTrange
# 优化高度图
optRECTrange = wh * diameter * optDiameter
optRErange = wh / optRECTrange
# 流体高度图
flowRECTrange = wh * flowDiameter
flowRErange = wh / flowRECTrange
# 地图细粒度控制
streamWidth = 1.0 - streamWidth
sandWidth = (1.0 - seaLevel) * sandWidth
# 植被密度
vegetationDensity = 1.0 - vegetationDensity
# 岩石层海拔
rockyAreaLevel = snowLevel - rockyAreaWidth
if rockyAreaLevel < 0:rockyAreaLevel = 0
if rockyAreaLevel > 1:rockyAreaLevel = 1
### 下面是为了程序健壮性(鲁棒性)考虑,确保数据符合范围
if(
whHalf.is_integer() and
RECTrange.is_integer() and RErange.is_integer() and
optRECTrange.is_integer() and optRErange.is_integer() and
flowRECTrange.is_integer() and flowRErange.is_integer() and
streamWidth >= 0 and streamWidth <= 1.0 and
seaLevel >= 0 and seaLevel <= 1.0 and
sandWidth >= 0 and sandWidth <= 1.0 and
snowLevel >= 0 and snowLevel <= 1.0 and
vegetationDensity >= 0 and vegetationDensity <= 1.0
):
#
# <--- 这里是噪声数据的核心生成位置
#
根据地图生成的宽高调用噪声数据生成函数生成相关数据,并根据实际需要对数据进行处理。例如:作者定义经纬度的纬度中,高于世界维度80%或低于20%为北极或南极,此时气候区温度骤降,进而增加生成雪地的几率。
作为示例,作者还使用了缓动函数作为数据映射的手段,读者可根据实际情况设计更符合地形、气温等数据变化规律的映射函数。
for i in range(wh):
for j in range(wh):
# ———— 基本噪声数据
# 计算基础数据
altitude = (basicN.calc(i / RECTrange,j / RECTrange,RErange) + 1) / 2
# 计算优化数据
opt = (optimizeN.calc(flipHorizontal(i,wh) / optRECTrange,j / optRECTrange,optRErange) + 1) / 2
# 计算流数据
flow = (flowN.calc(i / flowRECTrange,j / flowRECTrange,flowRErange) + 1) / 2
# 输出混合海拔数据
mixAltitude = altitude * 0.8 + opt * 0.2# + flow * 0.08
mixAltitude = InOutQuad(mixAltitude,0,1,1)
# 计算气候区气温
climateT = TemperatureConv(i,whHalf)
# 气温分区: 寒带 温带 热带
# 保留原始温度数据 (复杂化波动)
temperature = easeInBounce(TemperatureConv(flow * 0.5 + opt * 0.5,1.0),0,1,1)
# 取高度图中心
flow = easeMiddle(flow,0,1,1)
# 湿度
humidity = easeInBounce(opt * 0.8 + altitude * 0.2,0,1,1)
#
# <--- 这里是场景数据生成部分
#
将噪声规范化后的数据传入生成器,通过每个生成器内部的设计实现具体的内容生成。
# 海拔温度(同时值越接近1代表距离海、湖越近)
if mixAltitude >= seaLevel:
AltitudeT = 1 - (mixAltitude - seaLevel) / (1 - seaLevel)
else:
AltitudeT = 1
# 计算与水源的距离比例,越靠近海和河流则值越接近0
waterDist = (1 - AltitudeT) * (1 - flow)
# 水源湿度百分比
waterHumidity = 1 - waterDist
# 开始遍历每个生成器
# 这样的设计当符合一个生成器的情况后,就会退出(适合生成二维图像数据)
# 若需要确保所有生成器都生成数据,则不能直接返回。
for i in range(len(arrGeneration)):
isG,map,obj = arrGeneration[i].Generation(
x,y,
mixAltitude,
altitude,
optimize,
flow,
streamWidth,
seaLevel,
sandWidth,
snowLevel,
climateT,
temperature,
humidity,
random,
vegetationDensity,
AltitudeT,
waterDist,
waterHumidity,
rockyAreaLevel
)
if isG :
return map,obj
return (0,0,0),(0,0,0)
所有的生成器都继承于一个基类,这个基类要求传入生成参数,并且输出是否符合本生成器生成规则,以及相关的生成数据。
# 生成器基类
class baseGeneration:
def __init__(self,weight):
self.weight = weight
def getWeight(self):
return self.weight
def Generation(self,
# 坐标
x,y,
# 混合海拔值
mixAltitude,
# 基础海拔值
altitude,
# 优化值
optimize,
# 流体值
flow,
# 河流宽度
streamWidth,
# 海平面高度
seaLevel,
# 沙滩宽度
sandWidth,
# 雪域海拔
snowLevel,
# 气候温度/湿度(寒带 < 0;热带 > 1;温带0~1)
climateT,
# 温度图 0~1
temperature,
# 湿度图 0~1
humidity,
# 当前定位点随机值
random,
# 植被密度
vegetationDensity,
# 海拔温度
AltitudeT,
# 水源距离
waterDist,
# 水源湿度
waterHumidity,
# 岩石区海拔
rockyAreaLevel
):
#
# <--- 这里写生成器逻辑
#
return False,(0,0,0),(0,0,0)
下面是一个河流生成器示例,代码如下:
# 是否达到生成植被的条件
gObject = random > vegetationDensity and (x + y) % 2 == 1
# 超高温
tempH = climateT * 0.4 + temperature * 0.5 + (1 - AltitudeT) * 0.4
# 超低温
tempL = climateT * 5 + temperature * 5 - AltitudeT * 0.2
# 潮湿
humH = climateT * 0.4 + humidity * 0.8 + waterHumidity * 0.4 > 1.0
# 干燥
humL = climateT * 0.5 - (1 - humidity) * 0.4 + waterHumidity * 0.4 < 0.0
# 若河流值大于阈值,则生成河流分支
if flow > streamWidth:
if tempH > 1.0:
# 若处于超高温领域,则沙化河流
return True,(255,255,0),(0,0,0) # 沙化河床
if tempL < 0.0:
# 若处于超低温领域,则冰化河流
return True,(225,225,225),(0,0,0) # 冰冻河水
# 常规河流
return True,(0,255,255),(0,0,0)
# 不生成河流
return False,(0,0,0),(0,0,0)
最后,将定制好的场景生成器按设计顺序构成数组,并传递给WorldGeneration函数,就可以得到最终的生成数据。
# 生成宽高为 200px 的2D地图数据
Image,Image2 = WorldGeneration([
# 海湖生成器
seaGeneration(0),
# 雪地岩层生成器
snowGeneration(1),
# 河流生成器
streamGeneration(2),
# 沙滩生成器
sandGeneration(3),
# 陆地生成器
landGeneration(4)
],200)
执行上述的代码后,成功生成地图如下:

完成Python的快速逻辑验证后,就可以将其转换为游戏引擎的代码进行实战开发啦! 作者这里使用的是Cocos Creator游戏引擎,所以转换为 TypeScript 语言代码。 上述代码在游戏引擎中还需要继续细分,分帧执行可以避免导致游戏画面卡顿。