This commit is contained in:
boybook
2025-12-01 20:59:16 +08:00
parent 12738a142c
commit 760c2dd9ad
5535 changed files with 21070 additions and 2021 deletions

View File

@@ -0,0 +1,16 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 入门
time: 5分钟
---
# 摘要
在本章中,我们将一起学习**维度****Dimension**)的制作。了解维度的构成,以及如何制作维度中的**生物群系****Biome**)。
- 在第一节(*开始创建新维度*)中,我们将使用编辑器配置一个维度,学习维度的基本功能。
- 在第二节(*设计维度传送门*)中,我们将设计一个维度传送门方块,用于传送至我们的新维度。
- 在第三节(*改变维度的生物群系*)中,我们将学习如何改变我们新维度的生物群系。
- 在最后一节(*挑战:海洋世界*)中,我们将一起制作一个挑战,创造一个海洋世界。
本章关键词:维度 地形生成器 生物群系 生物群系源 噪声 梯度 倍频 高度图 传送门 气候 转化 海洋世界

View File

@@ -0,0 +1,68 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 10分钟
---
# 开始创建新维度
**维度****Dimension**)是世界中重要的组成部分。在一个世界中,我们往往存在多个维度。在原版游戏中,主世界、下界和末路之地便是三个原生的维度。每个维度的都是相互独立的,其中的玩家、实体、方块和各种逻辑都互不干涉。我们可以认为,一个维度便是一个独立的“世界”,而一个玩家可以通过种种方式在这些“世界”中进行穿梭跳跃,来回于不同的维度。
在模组开发中,如果开发者能够向游戏中新增一些新的自定义维度,那么模组的可玩性将大大增强。在本节中,我们就通过我的世界开发工作台的编辑器来新建一个维度。
## 使用编辑器配置新维度
![](./images/15.1_dim_create.png)
![](./images/15.1_dim_creating.png)
我们打开编辑器,在创建配置中找到“**维度**”,即可创建一个**维度配置文件****Dimension Config File**)。这个文件将告诉编辑器和游戏我们的模组都自定义了哪些维度。
![](./images/15.1_dim_created.png)
我们可以看到,这个维度配置文件中包含一个当前模组已创建的维度列表和**维度标识符****Dimension Identifier**,简称**维度ID**列表中已经自动为我们创建了一个维度。我们可以通过修改维度名和维度ID来修改维度的信息。该文件的JSON内容如下
```json
{
"netease:dimension": {
"modId": "tutorial_demo",
"modDimensionId": [
1688560817
]
}
}
```
在编辑器中,我们每通过“+”按钮添加了一个维度,编辑器都会在行为包的`netease_dimension`文件夹中创建一个`dm<dimension id>.json`的文件,其中`<dimension id>`代表该维度的数字ID比如上面的1688560817。编辑器在创建维度配置文件时会自动创建一个空白的维度文件比如上面演示中编辑器就除了`dimension_config.json`文件之外还创建了一个`dm1688560817.json`文件。维度是使用一个数字ID作为其唯一标识符的所以如果不同的模组使用了相同的数字ID将会造成存档和生成器冲突。所以编辑器在创建维度时会尽可能随机生成一个维度ID。目前玩家可以自定义的维度ID区间为[22, 2147483647]。
类似于`dm1688560817.json`的文件是我们的**维度信息文件****Dimension Info File**)。每个维度信息文件都存储着一个维度必要的信息数据。维度的信息数据是使用组件的形式存储的。我们来看这里编辑器自动给我们生成的`dm1688560817.json`文件:
```json
{
"format_version": "1.14.0",
"netease:dimension_info": {
"components": {
"netease:dimension_type": "minecraft:overworld",
"netease:generator_noise": {}
}
}
}
```
我们可以看到,格式版本为`1.14.0`,模式标识符为`netease:dimension_info`。在组件中,我们可以填写如下几种主要组件:
- `netease:dimension_type`:字符串类型,维度所继承的原版维度的类型。如果该文件正在修改的是原版维度,这个组件是无效的。这里可以填写`minecraft:overworld``minecraft:nether``minecraft:the_end`
- `netease:generator_noise``netease:generator_flat``netease:generator_legacy`:空对象,世界**地形生成器****Terrain Generator**)的类型,分别是无限世界的噪声生成器、平坦世界的平坦生成器和旧世界的旧版生成器。对于三种生成器我们至多只能填写一个。如果该文件正在修改的是原版维度,该组件也是无效的。
- `netease:ban_vanilla_feature`:空对象,阻止原版特征(又译地物)的生成。如不欲阻止,则无需填写该对象。
- `netease:spawn_biomes`:字符串数组,该维度中允许玩家出生的生物群系的标识符列表。
- `netease:biome_source`:对象,用于定义该维度的**生物群系源****Biome Source**)。定义了生物群系源的维度将自动开启中国版自定义生物群系的生成流程,若没有定义生物群系源,则使用国际版原版依赖气候和噪声的生物群系生成流程。
目前世界的地形生成器只能定义使用原版的生成器,也就是上述列出的噪声、平坦和旧版生成器,尚不能自定义地形生成器,也不能进一步自定义基础地形生成器的噪声。不过,我们这里依旧稍微提点一下噪声生成地形的概念,方便各位开发者对维度有一个更深入的理解。
原版的地形是使用**噪声****Noise**)来生成的,特别地,使用的是分形的**Perlin噪声****Perlin Noise**,又译**柏林噪声**。Perlin噪声的基本原理是利用在**格****Lattice**)上,也可以理解为在坐标系整数格点上,生成一系列随机数作为该点的**梯度****Gradient**。然后在其他的点上分别使用邻近格点二维为4个三维为8个上的梯度值来进行一个插值得出一个噪声值。事实上这里格的顶点不一定是整点而是符合步长采样要求的点即可。而分形的Perlin噪声则利用了多个不同“采样”的噪声值叠加而到到最终值。分形过程中会生成同一个种子下的多个单噪声每个单噪声在生成时频率都为上一次的二倍而振幅都为上一次的一半因此每个单噪声称为一个**倍频****Octave**)。最后所有的倍频叠加起来就是我们最终的噪声。
原版在生成基础地形时分别使用了一个三维噪声和一个二维噪声,三维噪声用于生成地形的竖直结构,而二维噪声又称**高度图噪声****Heightmap Noise**),用于生成地形的起伏。
事实上,噪声不仅用于基础地形的生成,生物群系映射、特征(又译地物)的生成也需要额外的噪声。虽然我们目前还不能自定义维度生成基础地形时的噪声算法,但是我们可以在接下来自定义生物群系或特征时自定义噪声的参数。
至此,我们成功自定义了一个维度。下一节中,我们将为其制作一个传送门方块,用于进入该维度。

View File

@@ -0,0 +1,617 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 进阶
time: 30分钟
---
# 设计维度传送门
在本节中我们为维度设计一个传送门我们需要使用自定义方块和模组SDK相配合来完成这一逻辑。
## 自定义传送门方块
传送门方块是自定义传送门必须的一个方块。我们可以使用`base_block``portal`的自定义方块配合`netease:portal`组件来完成这一配置。我们在编辑器中新建两个方块,分别用于传到我们对应的维度和传回主世界。这两个方块的行为包定义分别设置如下:
```json
{
"format_version": "1.10",
"minecraft:block": {
"description": {
"identifier": "tutorial_demo:custom_dim_gate",
"base_block": "portal"
},
"components": {
"minecraft:destroy_time": {
"value": 9999
},
"minecraft:loot": {
"table": "loot_tables/empty.json"
},
"minecraft:block_light_emission": {
"emission": 1.0
},
"netease:portal": {
"target_dimension": 1688560817,
"particle_east_west": "minecraft:portal_east_west",
"particle_north_south": "minecraft:portal_north_south"
},
"netease:listen_block_remove": {
"value": true
}
}
}
}
```
```json
{
"format_version": "1.10",
"minecraft:block": {
"description": {
"identifier": "tutorial_demo:custom_dim_gate_back",
"base_block": "portal"
},
"components": {
"minecraft:destroy_time": {
"value": 9999
},
"minecraft:loot": {
"table": "loot_tables/empty.json"
},
"minecraft:block_light_emission": {
"emission": 1.0
},
"netease:portal": {
"target_dimension": 0,
"particle_east_west": "minecraft:portal_east_west",
"particle_north_south": "minecraft:portal_north_south"
},
"netease:listen_block_remove": {
"value": true
}
}
}
}
```
其中`netease:portal`中的`target_dimension`分别设置为我们自定义的维度ID和原版的主世界维度ID0`particle_east_west``particle_north_south`为东西朝向的传送门方块散发的粒子和南北朝向散发的粒子我们不妨先设置为国际版原版的传送门方块粒子。为了于模组SDK配合我们将`netease:listen_block_remove`打开。
## 设计传送门结构
我们这部分代码参考演示示例包portalGateDemo中的代码并将对应的方块ID和维度ID替换成我们的自己的ID。然后我们一起来分析传送门的代码。事实上我们如欲设计一个传送门结构只需要在生成传送门前使用`portal`引擎组件的`DetectStructure`方法来判定是否为我们需要的结构即可。如果是我们的结构,我们就将传送门中的空气替换为我们的传送门方块,如果不是,就什么也不执行。我们这里使用的示例为在荧石框架上使用骨粉来激活传送门,所有的逻辑都在服务端执行。
```python
# -*- coding: utf-8 -*-
import time
import math
import mod.server.extraServerApi as serverApi
from mod_log import engine_logger as logger
ServerSystem = serverApi.GetServerSystemCls()
compFactory = serverApi.GetEngineCompFactory()
# 服务端类
# 处理传送门逻辑
class Main(ServerSystem):
def __init__(self, namespace, system):
ServerSystem.__init__(self, namespace, system)
# 自定义维度1688560817
self.TARGET_DIMENSION_ID = 1688560817
# 从主世界去自定义维度自定义方块
self.telePortBlockName = 'tutorial_demo:custom_dim_gate'
# 从自定义维度回主世界自定义方块
self.backPortBlockName = 'tutorial_demo:custom_dim_gate_back'
# portal forcer功能常量
self.PORTAL_SEARCH_RADIUS = 128
self.PORTAL_CREATION_RADIUS = 16
self.PORTAL_RECORDS_KEY = 'tutorial_demo'
self.PORTAL_RECORD_DIMID = 'DimId'
self.PORTAL_RECORD_SPAN = 'Span'
self.PORTAL_RECORD_TPX = 'TpX'
self.PORTAL_RECORD_TPY = 'TpY'
self.PORTAL_RECORD_TPZ = 'TpZ'
# 传送门结构方块的形状
self.pattern = [
'####',
'#**#',
'#**#',
'####',
]
#传送门形状参数
self.defines = {
'#': 'minecraft:glowstone',
'*': 'minecraft:air'
}
# 设置传送门边框可激活的位置
self.touchPos =[(3,1), (3,2)]
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), 'ServerItemUseOnEvent', self, self.OnServerItemUseOnEvent)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), 'DimensionChangeFinishServerEvent', self, self.OnPortalForcerServerEvent)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), 'BlockRemoveServerEvent', self, self.OnBlockRemoveServerEvent)
self.ShowMsg("Portal Main init!")
#region 功能函数
def ShowMsg(self, msg, color='RED', isServer=True):
customComp = compFactory.CreateGame(serverApi.GetLevelId())
text = ("[服务端]" if isServer else "[客户端]") + msg
customComp.SetNotifyMsg(text, serverApi.GenerateColor(color))
def SetBlock(self, playerID, blockPos, blockName):
block = compFactory.CreateBlockInfo(playerID)
return block.SetBlockNew(blockPos, {'name': blockName, 'aux': 0})
def AddPostion(self, origin, offset):
return tuple(map(sum, zip(origin, offset)))
def MulPostion(self, origin, step):
return tuple(step * x for x in origin)
#end region 功能函数
#region 构建传送门函数
# 填充传送门 填充 传送门方块
def FillGateAirBlock(self, playerID, originPos, horizontalDir):
"""
param playerID: 玩家Id
param originPos: 传送门起始位置(左上角/右上角的位置)
param horizontalDir: 传送门方向,如(-1,0,0)
"""
logger.info("FillGameAirBlock pos:({},{},{}) dir:({},{},{})".format(originPos[0], originPos[1], originPos[2], horizontalDir[0], horizontalDir[1], horizontalDir[2]))
comp = compFactory.CreateDimension(playerID)
dimensionId = comp.GetEntityDimensionId()
blockName = self.backPortBlockName if dimensionId == self.TARGET_DIMENSION_ID else self.telePortBlockName
bottomLeftPos = originPos
linePos = originPos
for line in self.pattern:
for i in range(len(line)):
name = self.defines.get(line[i], 'minecraft:air')
pos = self.AddPostion(linePos, self.MulPostion(horizontalDir, i))
if (pos[0] == bottomLeftPos[0] and pos[1] <= bottomLeftPos[1] and pos[2] <= bottomLeftPos[2]) or \
pos[2] == bottomLeftPos[2] and pos[1] <= bottomLeftPos[1] and pos[0] <= bottomLeftPos[0]:
# Y最小同时X最小或者Z最小的点
bottomLeftPos = pos
logger.info("FillGameAirBlock cur pos:({},{},{})".format(pos[0], pos[1], pos[2]))
if name == 'minecraft:air':
self.SetBlock(playerID, pos, blockName)
linePos = self.AddPostion(linePos, (0, -1, 0))
# 保存传送门到存档
self.AddPortalRecord(dimensionId, bottomLeftPos, self.getSpan())
# 使用骨粉激活传送门
def OnServerItemUseOnEvent(self, eventData):
itemName = eventData['itemName']
auxValue = eventData['auxValue']
playerID = eventData['entityId']
if itemName == "minecraft:dye" and auxValue == 15:
# 骨粉激活传送门
pos = (eventData['x'], eventData['y'], eventData['z'])
# 检测自定义门的结构
portalComp = compFactory.CreatePortal(playerID)
ret = portalComp.DetectStructure(playerID, self.pattern, self.defines, self.touchPos, pos)
if ret[0]:
self.ShowMsg('门构建成功!')
self.FillGateAirBlock(playerID, ret[1], ret[2])
self.ShowMsg('传送方块已经填充!')
#消耗物品"骨粉”
def consumeDye():
itemComp = compFactory.CreateItem(playerID)
item = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
item["count"] = item["count"] - 1
newRet = itemComp.SpawnItemToPlayerCarried(item, playerID)
if newRet:
self.ShowMsg("消耗物品成功")
else:
self.ShowMsg("消耗物品失败")
comp = compFactory.CreateGame(serverApi.GetLevelId())
comp.AddTimer(0.1, consumeDye)
else:
self.ShowMsg('传送门构建失败!')
def OnBlockRemoveServerEvent(self, args):
dimension = args['dimension']
blockName = args['fullName']
# 以“底部最左边的点”为key寻找并删除传送门记录
self.RemovePortalRecord(dimension, args['x'], args['y'], args['z'], blockName)
# 保存传送门数据到level extraData中
def AddPortalRecord(self, dimensionId, pos, span):
logger.info("add portal record dim:{} pos:({},{},{})".format(dimensionId, pos[0], pos[1], pos[2]))
entitycomp = compFactory.CreateExtraData(serverApi.GetLevelId())
portalsRecordDict = entitycomp.GetExtraData(self.PORTAL_RECORDS_KEY)
if not portalsRecordDict:
portalsRecordDict = {}
if dimensionId not in portalsRecordDict:
portalsRecordDict[dimensionId] = []
# 以底部最左边的点作为key存储该传送门
record = {
self.PORTAL_RECORD_DIMID: dimensionId,
self.PORTAL_RECORD_TPX: pos[0],
self.PORTAL_RECORD_TPY: pos[1],
self.PORTAL_RECORD_TPZ: pos[2],
self.PORTAL_RECORD_SPAN: span,
}
portalsRecordDict[dimensionId].append(record)
entitycomp.SetExtraData(self.PORTAL_RECORDS_KEY, portalsRecordDict)
return record
# 以“底部最左边的点”为key删除level extraData保存的数据
def RemovePortalRecord(self, dimensionId, x, y, z, blockName):
name = self.backPortBlockName if dimensionId == self.TARGET_DIMENSION_ID else self.telePortBlockName
defines = {
'#': 'minecraft:glowstone',
'*': name
}
if blockName != name:
return False
patternLen = len(self.pattern)
portalPosArray = []
for i in xrange(patternLen): # 高度
line = self.pattern[i]
for j in range(len(line)): # 宽度
name = defines.get(line[j], 'minecraft:air')
if name == blockName:
bottomLeftMostPos = (x - j, y + i - patternLen + 1, z)
if self.RemoveAndSavePortalRecordAt(bottomLeftMostPos, dimensionId):
return True
bottomLeftMostPos = (x, y + i - patternLen + 1, z - j)
if self.RemoveAndSavePortalRecordAt(bottomLeftMostPos, dimensionId):
return True
return False
def RemoveAndSavePortalRecordAt(self, pos, dimensionId):
entitycomp = compFactory.CreateExtraData(serverApi.GetLevelId())
portalsRecordDict = entitycomp.GetExtraData(self.PORTAL_RECORDS_KEY)
if not portalsRecordDict or dimensionId not in portalsRecordDict:
return False
records = portalsRecordDict[dimensionId]
for rec in records:
if rec[self.PORTAL_RECORD_TPX] == pos[0] and \
rec[self.PORTAL_RECORD_TPY] == pos[1] and \
rec[self.PORTAL_RECORD_TPZ] == pos[2]:
records.remove(rec)
entitycomp.SetExtraData(self.PORTAL_RECORDS_KEY, portalsRecordDict)
self.ShowMsg("成功删除传送门信息")
return True
return False
#end region 构建传送门函数
```
## 在传送终点生成返回传送门
我们的传送门不能有去无回,所以我们还需要在玩家进入对应维度后在终点处生成一个返回的传送门。关于这一功能我们其实分两种情况考虑,第一种是在目标点周围没有发现之前存在的传送门,那么我们应该在目标点附近找一个空旷点生成一个传送门,然后将玩家移动至该传送门生成的位置。另一种情况是目标点附近存在一个传送门,我们只需要将玩家的位置移动到该传送门处即可。我们查看补充完整的服务端脚本文件。
```python
# -*- coding: utf-8 -*-
import time
import math
import mod.server.extraServerApi as serverApi
from mod_log import engine_logger as logger
ServerSystem = serverApi.GetServerSystemCls()
compFactory = serverApi.GetEngineCompFactory()
# 服务端类
# 处理传送门逻辑
class Main(ServerSystem):
def __init__(self, namespace, system):
ServerSystem.__init__(self, namespace, system)
# 自定义维度23333
self.TARGET_DIMENSION_ID = 23333
# 从主世界去自定义维度自定义方块
self.telePortBlockName = 'portalGateDemo:gateProtal'
# 从自定义维度回主世界自定义方块
self.backPortBlockName = 'portalGateDemo:gateProtalBack'
# portal forcer功能常量
self.PORTAL_SEARCH_RADIUS = 128
self.PORTAL_CREATION_RADIUS = 16
self.PORTAL_RECORDS_KEY = 'ModPortalRecords'
self.PORTAL_RECORD_DIMID = 'DimId'
self.PORTAL_RECORD_SPAN = 'Span'
self.PORTAL_RECORD_TPX = 'TpX'
self.PORTAL_RECORD_TPY = 'TpY'
self.PORTAL_RECORD_TPZ = 'TpZ'
# 传送门结构方块的形状
self.pattern = [
'####',
'#**#',
'#**#',
'####',
]
#传送门形状参数
self.defines = {
'#': 'minecraft:glowstone',
'*': 'minecraft:air'
}
# 设置传送门边框可激活的位置
self.touchPos =[(3,1), (3,2)]
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), 'ServerItemUseOnEvent', self, self.OnServerItemUseOnEvent)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), 'DimensionChangeFinishServerEvent', self, self.OnPortalForcerServerEvent)
self.ListenForEvent(serverApi.GetEngineNamespace(), serverApi.GetEngineSystemName(), 'BlockRemoveServerEvent', self, self.OnBlockRemoveServerEvent)
self.ShowMsg("Portal Main init!")
#region 功能函数
def ShowMsg(self, msg, color='RED', isServer=True):
customComp = compFactory.CreateGame(serverApi.GetLevelId())
text = ("[服务端]" if isServer else "[客户端]") + msg
customComp.SetNotifyMsg(text, serverApi.GenerateColor(color))
def SetBlock(self, playerID, blockPos, blockName):
block = compFactory.CreateBlockInfo(playerID)
return block.SetBlockNew(blockPos, {'name': blockName, 'aux': 0})
def AddPostion(self, origin, offset):
return tuple(map(sum, zip(origin, offset)))
def MulPostion(self, origin, step):
return tuple(step * x for x in origin)
#end region 功能函数
#region 构建传送门函数
# 填充传送门 填充 传送门方块
def FillGateAirBlock(self, playerID, originPos, horizontalDir):
"""
param playerID: 玩家Id
param originPos: 传送门起始位置(左上角/右上角的位置)
param horizontalDir: 传送门方向,如(-1,0,0)
"""
logger.info("FillGameAirBlock pos:({},{},{}) dir:({},{},{})".format(originPos[0], originPos[1], originPos[2], horizontalDir[0], horizontalDir[1], horizontalDir[2]))
comp = compFactory.CreateDimension(playerID)
dimensionId = comp.GetEntityDimensionId()
blockName = self.backPortBlockName if dimensionId == self.TARGET_DIMENSION_ID else self.telePortBlockName
bottomLeftPos = originPos
linePos = originPos
for line in self.pattern:
for i in range(len(line)):
name = self.defines.get(line[i], 'minecraft:air')
pos = self.AddPostion(linePos, self.MulPostion(horizontalDir, i))
if (pos[0] == bottomLeftPos[0] and pos[1] <= bottomLeftPos[1] and pos[2] <= bottomLeftPos[2]) or \
pos[2] == bottomLeftPos[2] and pos[1] <= bottomLeftPos[1] and pos[0] <= bottomLeftPos[0]:
# Y最小同时X最小或者Z最小的点
bottomLeftPos = pos
logger.info("FillGameAirBlock cur pos:({},{},{})".format(pos[0], pos[1], pos[2]))
if name == 'minecraft:air':
self.SetBlock(playerID, pos, blockName)
linePos = self.AddPostion(linePos, (0, -1, 0))
# 保存传送门到存档
self.AddPortalRecord(dimensionId, bottomLeftPos, self.getSpan())
# 使用骨粉激活传送门
def OnServerItemUseOnEvent(self, eventData):
itemName = eventData['itemName']
auxValue = eventData['auxValue']
playerID = eventData['entityId']
if itemName == "minecraft:dye" and auxValue == 15:
# 骨粉激活传送门
pos = (eventData['x'], eventData['y'], eventData['z'])
# 检测自定义门的结构
portalComp = compFactory.CreatePortal(playerID)
ret = portalComp.DetectStructure(playerID, self.pattern, self.defines, self.touchPos, pos)
if ret[0]:
self.ShowMsg('门构建成功!')
self.FillGateAirBlock(playerID, ret[1], ret[2])
self.ShowMsg('传送方块已经填充!')
#消耗物品"骨粉”
def consumeDye():
itemComp = compFactory.CreateItem(playerID)
item = itemComp.GetPlayerItem(serverApi.GetMinecraftEnum().ItemPosType.CARRIED, 0)
item["count"] = item["count"] - 1
newRet = itemComp.SpawnItemToPlayerCarried(item, playerID)
if newRet:
self.ShowMsg("消耗物品成功")
else:
self.ShowMsg("消耗物品失败")
comp = compFactory.CreateGame(serverApi.GetLevelId())
comp.AddTimer(0.1, consumeDye)
else:
self.ShowMsg('传送门构建失败!')
def OnBlockRemoveServerEvent(self, args):
dimension = args['dimension']
blockName = args['fullName']
# 以“底部最左边的点”为key寻找并删除传送门记录
self.RemovePortalRecord(dimension, args['x'], args['y'], args['z'], blockName)
def OnPortalForcerServerEvent(self, args):
logger.info("OnPortalForcerServerEvent:{}".format(args))
entityId = args['playerId']
toDimensionId = args['toDimensionId']
self.PortalForcer(entityId, toDimensionId)
# portal forcer实例
def PortalForcer(self, entityId, toDimensionId):
pos = compFactory.CreatePos(entityId).GetPos()
if not pos:
return False
ret = self.FindPortal(toDimensionId, pos, self.PORTAL_SEARCH_RADIUS)
if ret[0]:
self.TravelPlayerToPortal(entityId, ret[1], toDimensionId)
self.ShowMsg("在玩家附近找到传送门({}),并且把玩家从{}移动到该传送门附近".format(ret[1], pos))
return
record = self.CreatePortal(entityId, toDimensionId, self.PORTAL_CREATION_RADIUS)
target = self.FindClosestBlockPosToPortal(entityId, record)
self.TravelPlayerToPortal(entityId, target, toDimensionId)
self.ShowMsg("在玩家附近创建一个传送门,并且把传送门信息成功存储到存档中")
# 寻找附近一个位置设置传送门
def FindNearPosAndSetPortal(self, entityId, dimensionId, pos):
defines = {
'#': 'minecraft:glowstone',
'*': self.backPortBlockName if dimensionId == self.TARGET_DIMENSION_ID else self.telePortBlockName
}
blockComp = compFactory.CreateBlockInfo(entityId)
for line in self.pattern: # 高度
for i in range(len(line)): # 宽度
blockName = defines.get(line[i], 'minecraft:air')
blockPos = (pos[0] + i, pos[1], pos[2])
logger.info("create portal with pos:{}".format(blockPos))
blockComp.SetBlockNew(blockPos, {'name': blockName, 'aux': 0})
# 坐标自减
pos[1] -= 1
# 返回传送门“底部最左边的点”
return pos
def GetPortalHeight(self):
return len(self.pattern)
def GetPortalWidth(self):
width = 0
for line in self.pattern:
if len(line) > width:
width = len(line)
return width
# 在玩家附近查找的传送门,返回是否存在传送门、传送门的坐标
def FindPortal(self, dimensionId, centerBlockPos, radius):
closest = -1 # 最近的距离
targetBlockPos = (0, 0, 0)
entitycomp = compFactory.CreateExtraData(serverApi.GetLevelId())
# 在存档中读取传送门信息
portalsRecordDict = entitycomp.GetExtraData(self.PORTAL_RECORDS_KEY)
if not portalsRecordDict:
portalsRecordDict = {}
if dimensionId not in portalsRecordDict:
# 存储的信息中不存在传送门
return False, targetBlockPos
for rec in portalsRecordDict[dimensionId]:
# “底部最左边的点”作为key
baseBlockPos = (rec[self.PORTAL_RECORD_TPX], rec[self.PORTAL_RECORD_TPY], rec[self.PORTAL_RECORD_TPZ])
for span in xrange(0, rec[self.PORTAL_RECORD_SPAN]):
recordBlockPos = (baseBlockPos[0] + span, baseBlockPos[1] + span, baseBlockPos[2])
# search area x by y (at all heights)
xd = abs(recordBlockPos[0] - centerBlockPos[0])
zd = abs(recordBlockPos[1] - centerBlockPos[1])
if xd <= radius and zd <= radius:
# 根据欧拉距离选择最新的点
dist = self.EuclideanDistance(recordBlockPos, centerBlockPos)
if closest < 0 or dist < closest:
closest = dist
targetBlockPos = recordBlockPos
return closest >= 0, targetBlockPos
# 创建传送门
def CreatePortal(self, entityId, dimensionId, radius):
entityPos = compFactory.CreatePos(entityId).GetPos()
entityBlockPos =[math.floor(entityPos[0]) - self.GetPortalWidth() / 2, math.floor(entityPos[1]) - 2 + self.GetPortalHeight(), math.floor(entityPos[2])]
logger.info("player current position:{} portal begin position:{}".format(entityPos, entityBlockPos))
entityBlockPos = self.FindNearPosAndSetPortal(entityId, dimensionId, entityBlockPos)
return self.AddPortalRecord(dimensionId, entityBlockPos, self.getSpan())
# 保存传送门数据到level extraData中
def AddPortalRecord(self, dimensionId, pos, span):
logger.info("add portal record dim:{} pos:({},{},{})".format(dimensionId, pos[0], pos[1], pos[2]))
entitycomp = compFactory.CreateExtraData(serverApi.GetLevelId())
portalsRecordDict = entitycomp.GetExtraData(self.PORTAL_RECORDS_KEY)
if not portalsRecordDict:
portalsRecordDict = {}
if dimensionId not in portalsRecordDict:
portalsRecordDict[dimensionId] = []
# 以底部最左边的点作为key存储该传送门
record = {
self.PORTAL_RECORD_DIMID: dimensionId,
self.PORTAL_RECORD_TPX: pos[0],
self.PORTAL_RECORD_TPY: pos[1],
self.PORTAL_RECORD_TPZ: pos[2],
self.PORTAL_RECORD_SPAN: span,
}
portalsRecordDict[dimensionId].append(record)
entitycomp.SetExtraData(self.PORTAL_RECORDS_KEY, portalsRecordDict)
return record
# 获取玩家离传送门相对比较近的位置
def FindClosestBlockPosToPortal(self, entityId, record):
pos = (record[self.PORTAL_RECORD_TPX], record[self.PORTAL_RECORD_TPY], record[self.PORTAL_RECORD_TPZ])
# 该位置开发者可以根据需要设定
# 注意该位置有可能不是空气,所以在选择位置时需要注意
return tuple((x + 2 for x in pos))
def LengthSquared(self, pos):
return pos[0] * pos[0] + pos[1] * pos[1] + pos[2] * pos[2]
# 把玩家的位置设置到传送门附近
def TravelPlayerToPortal(self, entityId, targetBlockPos, dimensionId):
entityTargetPos = tuple((x + 0.5 for x in targetBlockPos))
originRot = compFactory.CreateRot(entityId).GetRot()
entityRotY = originRot[1]
if dimensionId == self.TARGET_DIMENSION_ID:
entityRotY += 90
else:
entityRotY -= 90
newRot = (originRot[0], entityRotY)
compFactory.CreateRot(entityId).SetRot(newRot)
compFactory.CreatePos(entityId).SetPos(entityTargetPos)
logger.info("set position:({}, {})".format(entityTargetPos[0], entityTargetPos[1]))
# 计算两点的欧拉距离
def EuclideanDistance(self, pos1, pos2):
dx = pos1[0] - pos2[0]
dy = pos1[1] - pos2[1]
dz = pos1[2] - pos2[2]
return dx * dx + dy * dy + dz * dz
# 自定义传送门的最大宽度
def getSpan(self):
span = 1
for line in self.pattern:
if len(line) > span:
span = len(line)
return span
# 以“底部最左边的点”为key删除level extraData保存的数据
def RemovePortalRecord(self, dimensionId, x, y, z, blockName):
name = self.backPortBlockName if dimensionId == self.TARGET_DIMENSION_ID else self.telePortBlockName
defines = {
'#': 'minecraft:glowstone',
'*': name
}
if blockName != name:
return False
patternLen = len(self.pattern)
portalPosArray = []
for i in xrange(patternLen): # 高度
line = self.pattern[i]
for j in range(len(line)): # 宽度
name = defines.get(line[j], 'minecraft:air')
if name == blockName:
bottomLeftMostPos = (x - j, y + i - patternLen + 1, z)
if self.RemoveAndSavePortalRecordAt(bottomLeftMostPos, dimensionId):
return True
bottomLeftMostPos = (x, y + i - patternLen + 1, z - j)
if self.RemoveAndSavePortalRecordAt(bottomLeftMostPos, dimensionId):
return True
return False
def RemoveAndSavePortalRecordAt(self, pos, dimensionId):
entitycomp = compFactory.CreateExtraData(serverApi.GetLevelId())
portalsRecordDict = entitycomp.GetExtraData(self.PORTAL_RECORDS_KEY)
if not portalsRecordDict or dimensionId not in portalsRecordDict:
return False
records = portalsRecordDict[dimensionId]
for rec in records:
if rec[self.PORTAL_RECORD_TPX] == pos[0] and \
rec[self.PORTAL_RECORD_TPY] == pos[1] and \
rec[self.PORTAL_RECORD_TPZ] == pos[2]:
records.remove(rec)
entitycomp.SetExtraData(self.PORTAL_RECORDS_KEY, portalsRecordDict)
self.ShowMsg("成功删除传送门信息")
return True
return False
#end region 构建传送门函数
```
![](./images/15.2_overworld_to_custom_dim.png)
![](./images/15.2_transfering.png)
![](./images/15.2_custom_dim_to_overworld.png)
这样,我们便完成了一个传送门的设计。接下来,为更多地维度设计不同样式的传送门吧!

View File

@@ -0,0 +1,241 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 30分钟
---
# 改变维度的生物群系
**生物群系****Biome**)是一个维度中用于控制地形地貌、生物植被、大气环境、遗迹结构等各种地理要素的区域。一个维度中往往有多种生物群系,一种生物群系又可以转化为多种子生物群系,这使得维度的环境变得十分丰富。
在本节中,我们一起来学习如何自定义生物群系。
## 创建生物群系
在国际版提供的功能中,我们可以开启实验性玩法来修改原版的生物群系或在主世界中自定义新的生物群系,但是,这个功能十分不稳定,而且需要实验性玩法的支持。网易的开发组在国际版生物群系接口的基础上开发了中国版的自定义生物群系系统,并且在实现上与国际版的稍有不同。目前而言,中国版的自定义生物群系是与维度绑定的。每种维度,包括原版维度和自定义维度,都必须有自己独立的一套生物群系,并且互不影响,而每套生物群系都必须只能通过修改原版的生物群系来实现。换句话说,我们目前无法在中国版的自定义生物群系中如国际版的生物群系那样增加一个完完全全崭新的生物群系,所以的生物群系都必须继承并覆盖一个原版的生物群系,而且每个原版的生物群系只能被一个新的群系继承并覆盖。说得更直白一些,中国版的一个新的自定义维度会默认复制并使用一套原版主世界的生物群系,而我们能做的就是在这套与主世界相同的生物群系的基础上修改他们的各种属性,包括各地形层方块、生成噪声参数、气候和温湿度等属性,以将其修改成为一个个看似是新生物群系的生物群系。
针对每种维度,我们都有两种方式创建它的一个生物群系。
### 使用脚本批量生成一个维度的生物群系
上面我们了解到,我们能且只能在原版生物群系种类的基础上修改生物群系的各种属性。但是,这一过程要如何进行呢?对于每个维度而言,可以修改的生物群系的所有标识符事实上已经固定了,那就是与所有原版生物群系对应的带有`dm<dimension id>_`前缀的生物群系。比如,继承了`desert`生物群系的在数字ID为1688560817的维度中的生物群系的标识符就是`dm1688560817_desert`。虽然我们也可以手动对应于每个维度建立我们需要的所有生物群系,但是那将耗费大量的工作。我们有一个可以使用脚本快速生成某个维度所有生物群系的方法。
我们找到演示示例包中的CustomBiomesMod模组在行为包下可以发现`tools`文件夹。这个文件夹是我们用于批量生成生物群系的脚本。我们在这个文件夹下可以找到`remake.py`。我们执行该文件来自动生成一个特定维度ID的生物群系副本。比如我们为ID为1688560817维度生成一套生物群系。
```shell
python .\remake.py dm1688560817
```
通过执行该命令即可生成一套该维度的生物群系,生物群系文件将自动被命名为`dm1688560817_`前缀,生物群系标识符也将被命名为`dm1688560817_`前缀,同时会为生物群系加上`dm1688560817`的标签。
### 使用编辑器创建生物群系
![](./images/15.3_biome_create.png)
我们在配置中选择“**生物群系**”,来创建一个新的生物群系。
![](./images/15.3_biome_creating.png)
我们目前在新建文件向导中只能创建空的生物群系,这没关系,我们之后可以将生物群系绑定到维度上。
![](./images/15.3_biome_created.png)
![](./images/15.3_biome_resource.png)
我们可以看到我们在生物群系中可以修改多种属性。此时我们可以查看新创建的文件的JSON内容
```json
{
"format_version": "1.14.0",
"minecraft:biome": {
"description": {
"identifier": "dm_desert",
"inherits": "desert"
},
"components": {
"minecraft:overworld_generation_rules": {
"generate_for_climates": [
[
"frozen",
0
],
[
"cold",
0
],
[
"medium",
0
],
[
"lukewarm",
0
],
[
"warm",
0
]
],
"hills_transformation": "dm_desert",
"mutate_transformation": "dm_desert"
},
"minecraft:overworld_height": {
"noise_type": "default"
},
"minecraft:surface_parameters": {
"foundation_material": "minecraft:iron_block",
"mid_material": "minecraft:gold_block",
"sea_floor_depth": 7,
"sea_floor_material": "minecraft:emerald_block",
"sea_material": "minecraft:water",
"top_material": "minecraft:diamond_block"
},
"dm": {}
}
}
}
```
此时,我们的群系并没有绑定特定的维度,所有的`dm<dimension id>_`前缀都以`dm_`的形式出现。我们在编辑器中选择我们的维度绑定。
![](./images/15.3_biome_select_dim.png)
我们在“**基础属性**”的“**适用维度**”选择我们需要绑定的维度。然后编辑器便会自动修改我们的维度文件,同时创建一个如同我们用脚本方法创建出一样的一套原版维度。
![](./images/15.3_biome_selected.png)
![](./images/15.3_biome_dim_auto.png)
这套维度会随着我们自定义的生物群系的绑定与否自动增删其对应的那个生物群系文件也会随着我们使用编辑器对外面根目录的这个文件的更改而同步更改。我们来看看此时的JSON文件内容
```json
{
"format_version": "1.14.0",
"minecraft:biome": {
"description": {
"identifier": "dm1688560817_desert",
"inherits": "desert"
},
"components": {
"minecraft:overworld_generation_rules": {
"generate_for_climates": [
[
"frozen",
0
],
[
"cold",
0
],
[
"medium",
0
],
[
"lukewarm",
0
],
[
"warm",
0
]
],
"hills_transformation": "dm1688560817_desert",
"mutate_transformation": "dm1688560817_desert"
},
"minecraft:overworld_height": {
"noise_type": "default"
},
"minecraft:surface_parameters": {
"foundation_material": "minecraft:iron_block",
"mid_material": "minecraft:gold_block",
"sea_floor_depth": 7,
"sea_floor_material": "minecraft:emerald_block",
"sea_material": "minecraft:water",
"top_material": "minecraft:diamond_block"
},
"dm1688560817": {}
}
}
}
```
此时我们的生物群系文件便完成创建了之后我们只需要在编辑器中或者手动修改JSON文件的内容便可以做到自定义生物群系的属性。
## 修改生物群系属性
通过联合分析编辑器中的“属性”窗格和生物群系的JSON文件我们来一起学习生物群系的写法。我们可以看到生物群系目前使用的是`1.14.0`的格式版本,`minecraft:biome`的模式标识符。事实上,我们也可以使用旧版的`1.12.0``1.13.0`格式版本,但是由于版本迭代,这些格式版本的语法已经与`1.14.0`较为不同,我们不推荐使用,同时在这里也不进行介绍与学习。
### 行为包组件
![](./images/15.3_biome_height.png)
“**地形高度**”属性对应的是`minecraft:overworld_height`组件,用于在主世界中修改噪声地形的高度补偿,我们可以通过修改噪声预设的值和噪声的参数来修改噪声地形的高度效果。由于我们的自定义维度是继承的主世界生物群系,因此我们可以使用该组件来控制生物群系的平均高度即高度分布。
![](./images/15.3_biome_surface.png)
“**地表**”对应的是`minecraft:surface_parameters`组件,用于指定海底深度和地表各地形层的**物质****Material**)参数。“水底方块”即`sea_floor_material`字段,用于指定**海底物质****Sea Floor Material**);“水体方块”即`sea_material`字段,用于指定**海体物质****Sea Material**);“浅表方块”即`top_material`字段,用于指定**顶层物质****Top Material**);“中层方块”即`mid_material`字段,用于指定**中层物质****Middle Material**);“深层方块”即`foundation_material`字段,用于指定**地基物质****Foundation Material**)。
![](./images/15.3_biome_transformation.png)
![](./images/15.3_biome_climate.png)
“生成规则”对应的是`minecraft:overworld_generation_rules`组件。该组件用于定义主世界生物群系的**生成规则****Generation Rule**),不过,和之前所说的一样,该组件也可以在中国版的自定义维度中使用,以模仿主世界的生成规则。生成规则分为两部分,分别是**转化****Transformation**)规则和针对**气候****Climate**)的生成规则。转化是一种增加生物群系多样性的操作。我们在定义生物群系时,往往倾向于同时定义一组同类型的生物群系,其中只有一个生物群系会定义`generate_for_climates`字段,也就是针对气候的生成规则,被称为**基生物群系****Base Biome**)。而其他的生物群系都会根据转化规则在生成基生物群系后通过生物群系的转化得到。我们将某个基生物群系和其转化得到的生物群系统称为一个**群丛****Association**)。目前,我们可以定义多种转化,分别是**丘陵转化****Hills Transformation**)、**突变转化****Mutate Transformation**)、**河流转化****River Transformation**)和**海岸转化****Shore Transformation**我们可以在编辑器中选择或者直接在JSON文件中输入对应的转化后的生物群系作为字段的值即可。比如原版丛林的基生物群系只生成在中性气候权重为1只有一个的时候这意味着100%并且会转化成丛林丘陵和丛林变种它的JSON组件是这么写的
```json
"minecraft:overworld_generation_rules": {
"hills_transformation": "jungle_hills",
"mutate_transformation": "jungle_mutated",
"generate_for_climates": [
[ "medium", 1 ]
]
}
```
丛林山丘和丛林变种不会进行其他转化,也不会作为基生物群系生成,因此不具备`minecraft:overworld_generation_rules`组件。事实上丛林生物群系还可能硬编码地转化为丛林边缘或竹林。丛林边缘不会作为基生物群系生成但是会转化为丛林边缘变种它的JSON组件内容为
```json
"minecraft:overworld_generation_rules": {
"mutate_transformation": "jungle_edge_mutated"
}
```
竹林也不会作为基生物群系生成但是会转化为竹林丘陵它的JSON组件内容为
```json
"minecraft:overworld_generation_rules": {
"hills_transformation": "bamboo_jungle_hills"
}
```
丛林边缘变种和竹林丘陵不会再转化为其他生物群系。河流生物群系虽然可以通过JSON组件控制但是依旧存在硬编码部分那就是如果不指定河流转化的生物群系`river`生物群系会自动作为河流转换的生物群系。上述所有群系都存在一个默认的`river`河流转化。这便是整个丛林群丛的生成转化链。
除了编辑器中可以调整的参数外我们还有许多可以自行在JSON文件中手动写入的组件比如`minecraft:climate`组件用于指定该生物群系各气候参数的值,比如温度、降雨量、积雪量等。`minecraft:forced_features`用于生成**强制型特征****Forced Feature**,又译**强制型地物**)。`minecraft:ignore_automatic_features`用于忽略**自动型特征****Automatic Feature**,又译**自动型地物**,即非强制型特征)的生成,只生成上一个组件中指定的强制型特征。强制型特征又称**显式特征****Explicit Feature**,又译**显式地物**),自动型特征又称**隐式特征****Implicit Feature**,又译**隐式地物**)。另外,还有`minecraft:surface_material_adjustments`组件用于精细地调整地表物质,`minecraft:legacy_world_generation_rules`组件用于指定旧版的有限世界生成规则等。这些组件,包括旧格式版本的组件,都可以在[bedrock.dev上托管的生物群系文档](https://bedrock.dev/zh/b/Biomes)上找到更详细的用法。
除了这些组件之外, 中国版还存在一些独占的组件,比如`netease:no_spawn_end_dragon`用于在自定义末地的生物群系中取消生成末影龙及其相关逻辑。而且需要注意的是,如果在生物群系绑定的维度中启用了生物群系源,即`netease:biome_source`组件,则其下的生物群系均不再使用国际版原版的生物群系生成规则来生成,即生物群系的`minecraft:overworld_generation_rules`组件将失效,我们上述说明的生成过程也将无效,一切将按照维度中`netease:biome_source`组件所定义的规则进行生成。
### 生物群系标签
在生物群系的行为包定义文件中,我们除了行为包组件之外还可以定义生物群系**标签****Tag**。生物群系标签是用于标记生物群系的一种功能可以在Molang中使用`query.has_biome_tag`查询得到。我们通过在`minecraft:biome/components`对象中直接定义一个空对象的方式来定义一个标签,空对象的值就是标签的值。
比如,上述示例中,编辑器便自动为我们通过配置自定义的新生物群系添加了`dm1688560817`标签,供我们之后使用。具体代码如下
```json
{
"format_version": "1.14.0",
"minecraft:biome": {
"description": {
"identifier": "dm1688560817_desert",
"inherits": "desert"
},
"components": {
// ...
"dm1688560817": {}
}
}
}
```
我们可以为一个生物群系添加多个标签,比如原版的丛林生物群系便添加了`animal``jungle``monster``overworld``rare`标签。这些标签和特征、实体的生成规则以及一些硬编码内容相配合可以做到允许动物生成、允许丛林神庙生成、允许怪物生成、允许在无限世界的主世界和有限世界中生成和作为中性气候下的稀有群系生成。
至此,我们便完成了一个生物群系的定义。开发者们可以根据自己的意愿改造更多的生物群系了!

View File

@@ -0,0 +1,29 @@
---
front: https://nie.res.netease.com/r/pic/20211104/69055361-2e7a-452f-8b1a-f23e1262a03a.jpg
hard: 高级
time: 15分钟
---
# 挑战:海洋世界
在本节中我们一起制作一个海洋世界。海洋世界顾名思义就是几乎全是海的世界。为了制作出这样的世界我们有两种方案。第一种方案是将除了海洋之外其他的基生物群系的针对气候的生成规则全部关闭即权重设为0第二种是将所有的陆地生物群系高度都调整成与海洋一致的高度依旧可以制作出海洋世界。我们下面演示第二种操作模式。
## 修改陆地群系高度
![](./images/15.4_biome_height.png)
如果我们的生物群系是从编辑器中建立的我们可以调整“地形高度”中的“类型”并将其设置为“海洋”。这等价于在JSON的`minecraft:overworld_height`组件中将`noise_type`字段设置为`ocean`
当然,除了编辑器中配置的生物群系之外,我们还有其他的默认生物群系。我们使用文本编辑器将它们全部修改成或者添加上如下的组件:
```json
"minecraft:overworld_height": {
"noise_type": "ocean"
}
```
这样,所有的生物群系在噪声生成地形时就会像原版海洋那样生成低于海平面高度的地形。这样,我们的海洋世界就完成了。
![](./images/15.4_waterlandt.png)
我们进入游戏自测,可以看到除了村庄等硬编码生成在水面的特征(又译地物)以外,所有的方块都没入了水面以下。这说明我们的自定义海洋世界成功了!