开发移动世界

*此内容使用人工智能(Beta)翻译,可能包含错误。若要查看英文页面,请点按 此处

在体验中创建移动对体验感更加迷人、更有逼真感,这取决于从环境树的移动、玩家交互或甚至是撞到它们时的箱子。 Studio 有许多独特方法来创建动作,包括物理系统、 Class

创建风暴

风暴经过了许多版本,直到我们在 Duvall Drive 的神秘中结束了。 早期,我们将风暴视为巨大的黑曜石柱,并在后续版本中考虑它是一个巨大的传送门到腐败空间。 经过多个尝试不同的风暴,我们最终落在了一个小中心“眼”上,因为:

  • 风暴应该让玩家有一种感觉, 这个事件对世界的影响 ,包括被吹到飞翔的树和周围的废墟。
  • 云自身旋转的漩涡应该给玩家一览中央传送门 without revealing everything 。这将鼓励玩家调查更仔细,以便了解发生了什么事。
  • 更紧密的光点允许我们专注 房屋的组成 , 这是主角和游戏玩法大部分的地方。

为了让风暴在其环境中感觉动态、进攻、并随时变化,我们使用了以下系统和功能:

  1. TweenService > - 对于云端移动。
  2. 照明变更 - 用于创建云端闪电。
  3. 光波 - 对于“音量照明”和闪电。
  4. 粒子发射器 - 对于因风而起飞到传送门并在风的扰动下飞行的废墟。
  5. 动画 > - 对于被风吹扇的树。

添加云朵以图像

虽然 动态云 很棒,对于普通的高海拔实景云,我们需要感觉戏剧性的并

一个单独的云网格。
不带纹理的多云网格!

由于每个云网格需要包围房屋并且将其巨大的风暴描述,我们知道我们需要在个人云网格上使用的材质上瓷砖,以便在网格的表面上重复出现。我们在这些简单的零件上测试了我们为云网格打造的材质,然后将它们应用到漩涡!

与粒子发射器或光束不同,网格允许我们能够从每个网格上反射光,这对于我们想要实现云端闪电很重要。 我们还模型在扭曲,以便能够从每个网格上反射光,这让光从它上面看起来有深度! 这对于特别在情况下,体验的性能要求降低表面的表现对象的质量。

当我们开始添加照明时,我们需要在网格上添加细节,以便让它们更好地反应照明!

旋转云网格

云的总体视觉外观确定后,我们需要让它移动起来!我们有各个云层的形状位置,但需要一些试验和错误来确保旋转效果在实际中看起来很好。我们最初尝试使用 限制 来介绍速度,以驱动云朵在移动工具动中看起来良好。这比我们想要的更难以在后

我们想要一个简单的方法来旋转实例,它们既太远了以便交互,例如云,又太小以便重要,例如家具。 我们决定使用一个

在许多示例中,我们使用了一个 LocalSpaceRotation 标签,以便我们能够使用实例标记插件在 Studio 中管理受影响的实例。我们仅使用了一个单个 LocalScript ,它处理所有使用 CollectionService 标记的实例。因此,我们在开发过程中没有一堆脚本需要管理。


local function Init()
for _, obj in CollectionService:GetTagged("LocalSpaceRotation") do
if obj:IsDescendantOf(workspace) then
SetupObj(obj)
end
end
end
CollectionService:GetInstanceAddedSignal("LocalSpaceRotation"):Connect(function(obj)
objInfoQueue[obj] = true
end)
CollectionService:GetInstanceRemovedSignal("LocalSpaceRotation"):Connect(function(obj)
if objInfo[obj] then
objInfo[obj] = nil
if objInfoQueue[obj] then
objInfoQueue[obj] = nil
end
end
end)
Init()

obj

我们旋转了实例在 Update 函数连接到心跳。我们得到了父亲的


local parentTransform
if parentObj:IsA("Model") then
if not parentObj.PrimaryPart then
-- 主要部分可能还没有流行
continue -- 等待主要部分复制
end
parentTransform = parentObj.PrimaryPart.CFrame
else
parentTransform = parentObj.CFrame
end
curObjInfo.curAngle += dT * curObjInfo.timeToAngle
local rotatedLocalCFrame = curObjInfo.origLocalCFrame * CFrame.Angles( curObjInfo.axisMask.X * curObjInfo.curAngle, curObjInfo.axisMask.Y * curObjInfo.curAngle, curObjInfo.axisMask.Z * curObjInfo.curAngle )
if obj:IsA("Model") then
obj.PrimaryPart.CFrame = parentTransform * rotatedLocalCFrame
else
obj.CFrame = parentTransform * rotatedLocalCFrame
end

我们检查了一个有效的 Model.PrimaryPart 以便设置处理串流的。如果调用了我们的对象中的更新,而一个 Model.PrimaryPart (可以指向子网格) 还没有流量,我们就会跳过更新。当前系统是第二个版本的对象

设计闪电击中

因为 Studio 不提供一个预制的闪电生成器,并且粒子系统有一些限制,不会为英雄闪电击中工作,因此我们必须创造出一种解决英雄闪电击中的解决方案。 我们决定了两个主要系统来组成闪电:从风暴眼角射出的英雄闪电击中来的粒子纹理辐射

结构光线

通常情况下,我们使用顺序器或时间线工具来驱动照明螺栓打击效果的时间,但由于 Studio 还没有提供此功能,所以我们决定写脚本来控制照明螺栓打击效果的时间,脚本的实现很简单,但它实现以下重要目标:

  1. 闪电冲击的元素,例如其纹理、亮度和延迟,都是每次击打时随机化。
  2. 音频和 post FX 更改与打击 FX 更改同步。
  3. 玩家在室内或受到污染区域不能看到或听到它们。

我们有一个服务器端的 Script 计算各种参数和时间,将它们发送给所有客户,并等待随机的时间:


local function LightningUpdate()
while true do
task.wait(rand:NextNumber(3.0, 10.0))
local info = CreateFXData()
lightningEvent:FireAllClients(info)
end
end

CreateFXData 里,我们填写信息结构,以便所有客户端获得相同的参数。

在客户端( LightningVFXClient )上,我们检查客户端是否应该运行FX:


local function LightningFunc(info)
-- 室内不要 FX
if inVolumesCheckerFunc:Invoke() then
return
end
-- 当不是“正常”世界时,无FX
if not gameStateInfoFunc:Invoke("IsInNormal") then
return
end

此外,我们运行顺序来设置纹理、位置和亮度,运行十几岁,并使用 task.wait(number) 。随机参数来自我们从服务器那里收到的信息结构,其中一些数字已修复。


beam.Texture = textures[info.textIdx]
beamPart.Position = Vector3.new(info.center.X + og_center.X, og_center.Y, info.center.Y + og_center.Z)
-- 清除
beam.Brightness = 10
ppCC.Brightness = maxPPBrightness
ppBloom.Intensity = 1.1
bottom.Position = top.Position
tweenBrightness:Play()
tweenPPBrightness:Play()
tweenPPBrightness:Play()
tweenBottomPos:Play()
tweenBrightness.Completed:Wait()
-- 音频
if audioFolder and audioPart then
if audioFolder.Value and audioPart.Value then
audioUtils.PlayOneShot(audioObj, audioFolder.Value, audioPart.Value)
end
end
task.wait(info.waitTillFlashes)
-- and so on

为了检查玩家是否在室内,我们使用一个帮助 inVolumesCheckerFunc 函数,它会覆盖预置的音量,并检查玩家是否位于其中的任何一个 (PointInABox)。我们可以使用触摸基础检测,但我们发现当玩家坐在音量的旁边时,他们不再"触�

要检查玩家是否在有腐败区域,我们使用一个帮助gameStateInfoFunc 函数,该函数检查当前游戏状态。 要从文件夹中播放随机声音,我们还使用了一个帮助PlayOneShot 函数。 对于闪电之剑本身,这很容易在 Photoshop 中创建;我们绘制了一个棘手的

使用粒子发射器系统

英雄闪电击中是由一个粒子系统支持,该粒子系统通过在背景中创建一层云雾层从远处创建闪电印象来建议远闪电。 我们通过一个非常简单的粒子系统实现此效果,该粒子系统通过随机透明度曲线闪现一个云朵广告牌: 系统通过随机透明度曲线闪现一个云朵广告牌:

让树在风中摇摆

在我们有云和闪电的工作方式我们想要的时候,我们还需要添加两个其他主要组成风暴的要素:风和雨!这些元素在挑战我们的物理和特效系统的现有限制方面提供了一些挑战,例如需要在今天的引擎中使树木移动使用实际风是不可能的,因此我们使用 <

我们知道真正地销售风和雨的效果,我们需要树本身才能移移动工具。 有几种方法您可以在引擎中使用,包括使用 插件 (是公开的)、使用 TweenService (是动画模型)或直接动画模型。 对于我们的目的,动画让我们能够控制我们想要

我们从Endorse Model Pack - Forest Assets开始。 因为这些树已经存在,因此我们的体验在北美西北部发生了一些时间,因此我们在创建每个树模型之前有一些时间。

森林包含几种树类型,可以在您自己的体验中节省您的时间。

我们选择了我们的树后,我们知道我们需要将它们皮肤。 皮肤网格 是在另一个 3D 建模应用程序中添加关节(或骨头)到网格的行为,然后应用影响来移动网格。 此常用于人形角色,但 用 自定义角色

我们知道我们想要保存时间并重用同一的动画,所以我们建立了我们的第一个树网格,并确保共同名称是通用因为我们想在树桩上使用这些同名的动画。我们还知道我们需要包含主要、次要和三级共同

树有主要、次要和三级联接,以便我们能够从被风吹扰的角度获得可信的移动。

一旦我们创建了我们的关节/骨头,就是时候创建一个测试动画来移动所有关节和骨头在 Studio 中看到它移动方式。 要实现这一点,我们必须 通过 自定义义务 Rig 设置在 3D 导入器 中导入树,然后使用 动画

Studio 内的相同级别。

当我们对那棵树上的结果满意后,就是时候在另一棵树上测试同一种动画!我们已经知道它是在每个树输入之间的不同装备之间的动画,所以我们只是确保我们的动画看起来像是在高大的红棵树和扎实的桦木树之间的一种通用动画!

我们从红桦树导入的动画。

为此,我们从那个森林包中取得了蜂巢树,并且使用相同的命名为各个部件建造了一个类似的机骨架。 因为我们之前导入的动画都基于旋转关节,所以这也是我们之前导入的动画的全部。因为动画都基于旋转关节,所以无论树是多大、小、高或宽,都没关系!

�echwood树的同一个名字为其共同,只不过不是同一个数量。 这很好,因为动画系统只会为它匹配的特定共同匹配名称! 为此,我们可以应用同一个名字来应用动画到任何与共同名称匹配的东西!

在我们 装备和皮肤 �echwood树后,我们可以导入它并应用相同的动画。这意味着在一个文件上只需要循环并编辑,而且在运行体验时也会保存在性能上与少量动画。

使用动画编辑器,我们可以将同一的红木树动画应用到�echwood树!

当我们有所有的树类型我们想要动画时,我们每个都制作成 包裹 以便我们可以继续编辑并更新游戏中的多个动画。 因为我们知道它们有成本效率,所以我们在玩家在主区域的体验周围使用它们来编辑和更新游戏。 在未来,随着它们的性能越来越高,您将能够添加更多和更多的皮

我们在房子周围的动态树上使用了最强的旋风,这将是对玩家最有影响的视觉效果。

制作风暴残骸

我们想要让雨看起来重,并且让雾和废料通过树枝绕过。 要实现这一点,我们设置了几个隐形部分作为粒子卷帘,即子 粒子发射器 立即在大风云之下。 因为 Studio 的粒子数限制在 Studio ,因此我们不

我们使用了几个卷来实现所有雨的数量和我们想要的特定雨的覆盖。

雨水粒子利用了一个新的粒子发射器属性 ParticleEmitter.Squash 它可以让您使一个粒子更长,或者让它变得更厚

一个 3 的 Squash 值开始伸长材质。
一个 20 的 Squash 值可以伸长零件的时间,但我们还需要提高 Size 值。

对于迷雾、雾和叶子,通过吹拂,添加一个覆盖更少区域的单个部分的音量更简单,因为我们不需要一次运行大量粒子。我们开始设置一个音量并获得粒子的频率,在所需的位置。

结果有几个粒子部分卷,所以我们没有粒子进入房子,和因为我们不觉得它们需要通过树就像雾一样。
由于粒子大小,我们不需要那么精确地设置位置。

然后,我们使用叶子吹风和风的材质,并将其设置为在不同的速度和方向上旋转/移动。这意味着大型雾粒会更自然地与其他材质交互,而不会像重复的纹理一样看起来,尤其是其大小。

雾粒
叶子粒子

结果是移动树木之间、窗户吹拂和闪电产生的一些大的行动,以创建暴风雨环绕中心眼的暴风雨效果。

设置风暴之眼

由于破碎的石头眼睛有发光的核心,它们意味着玩家可以得到一个暗示,暗示着有什么邪恶和奥术在房子中发生。 由于我们的场景黑暗,玩家无法看到那里,因此我们需要创建一个相信的破碎石形状,但它不是重要的石

提前设置照明可以节省您不必花费时间去那里的表面细节,因为我们场景的照明设置了相同的照明效果,所以您不能看到戒指上的表面细节,因此没有必要花时间去那里!

从玩家那里也意味着我们可以完全依赖于普通地图的眼睛表面细节,因此网格是一个简单的球体! 我们雕刻了细节在高度的聚合物网格上,并将其普通地图烤制到更低的聚合物球上,以便我们可以获得所有那些美丽的细节,而无需那么高的性能成本。

高度聚合雕刻
低聚晶网格
低密度网格,带有高密度雕刻在

为了将超自然感应的眼睛添加到眼睛上,并强调其存外观,我们决定创建一个发光的霓虹岩浆,它将穿透其裂纹。 在表面外

内球上的垂直画。我们创建了一个最轻的渐变,以提供更强的深度和视觉效果。

另一个挑战是我们在创建眼睛时遇到的另一个挑战是由于我们使用串流与眼睛的距离结合而导致的

我们能够通过使用我们之前使用的脚本来将移动添加到眼睛和其戒指,感谢同样的脚本来旋转云朵的网格。 为了最终触碰,我们

我们用来创建云端世界的幻觉的图像。当玩家远离某个东西时,一个简单的图像可能就足以创建更深层次和复杂性的场景效果!

扩建食品仓库

生产的一个最有趣的事情是空白位置,在这里我们可以通过直接改变它们周围的环境来扰乱玩家对现实的期望。 例如,在父亲的谜题中,我们想 emulate 一个夜晚似乎不会结束的时刻,无论您奔跑得多快,房间都会感觉像它在变得更长。 我们决定使一个扩展的食品栏,它将从玩家那里逃走,

我们使用简单的移动来设置这一切,并且设计了我们房间的明智布局,它在食品库的两侧都会出现。 在房间的正常状态下,食品库是一个简单的走廊,但在损坏的空间中,它实际上是多个门和虚假的墙壁的长度!

厨房储藏室的状态。
虚假的墙壁从玩家那里移动。

错误的墙壁是一个我们会在玩家进入触发音量时移动的模型组,该音量是透明的,可以从仓库的所有门上走过。该触发器也用于在所有门上使用的脚本中使用,这些脚本称为“TweenService”,它们从一个目标移动到另一个目标的渐变操作。我们使用部分音量来告诉渐变操

零件音量触发一个假墙,让它移动到其端点。它在这张图像上以黄色色调显示。
Target_Closed是我们所有门的通用目标部分,用于旋转。它被重用来告诉走廊墙去哪前往。

因为 TweenService 是一个通用系统,所有的墙壁数据模型都必须包含相同的组件。例如,以下图是一个示例 general door script 调用声音由“值” 在“Grow_Wall” 模型下定义。

那个脚本,带有以下代码示例中的一些修改,也触发了音频来为仓库移动。这在移动中添加了很多!


local Players = game:GetService("Players")
local TweenService = game:GetService("TweenService")
local model = script.Parent
local sound = model.Sound.Value
local trigger = model.Trigger
local left = model.TargetL_Closed
local right = model.TargetR_Closed
local tweenInfo = TweenInfo.new(
model.Speed.Value, --门梯的时间/速度
Enum.EasingStyle.Quart, --减轻风格
Enum.EasingDirection.InOut, --方向
0, --重复计数
false, --真相反
0 --延迟
)
local DoorState = {
["Closed"] = 1,
["Opening"] = 2,
["Open"] = 3,
["Closing"] = 4,
}
local doorState = DoorState.Closed
local playersNear = {}
local tweenL = TweenService:Create(left, tweenInfo, {CFrame = model.TargetL_Open.CFrame})
local tweenR = TweenService:Create(right, tweenInfo, {CFrame = model.TargetR_Open.CFrame})
local tweenLClose = TweenService:Create(left, tweenInfo, {CFrame = model.TargetL_Closed.CFrame})
local tweenRClose = TweenService:Create(right, tweenInfo, {CFrame = model.TargetR_Closed.CFrame})
local function StartOpening()
doorState = DoorState.Opening
sound:Play()
tweenL:Play()
tweenR:Play()
end
local function StartClosing()
doorState = DoorState.Closing
--玩具门[门]:玩()
tweenLClose:Play()
tweenRClose:Play()
end
local function tweenOpenCompleted(playbackState)
if next(playersNear) == nil then
StartClosing()
else
doorState = DoorState.Open
end
end
local function tweenCloseCompleted(playbackState)
if next(playersNear) ~= nil then
StartOpening()
else
doorState = DoorState.Closed
end
end
tweenL.Completed:Connect(tweenOpenCompleted)
tweenLClose.Completed:Connect(tweenCloseCompleted)
local function touched(otherPart)
if otherPart.Name == "HumanoidRootPart" then
local player = Players:GetPlayerFromCharacter(otherPart.Parent)
if player then
--打印(触摸)
playersNear[player] = 1
if doorState == DoorState.Closed then
StartOpening()
end
end
end
end

一旦我们有了错误的墙壁移动到房间的后部,我们需要与其移动的内容。 要实现这一目标,我们需要将所有零散物品都固定在墙壁上,因为它会移动。 使用 焊接约束,我们很快就可以焊接所有物品到墙壁上,以便作为一个整体移动到�

树屋腐败的原因

Studio 是一个基于物理的引擎,您可以使用它来创建从摇摆大门到旋转平台的一切。 使用我们的示例,我们想使用物理创建一个现实感在某个不现实的集合中的环境。 使用 限制 几个,您可以在您自己的体验中创建一些有趣的障碍赛路线!

限制器) 是一个基于物理的机器,可以将对象对齐并限制行为。 例如,您可以使用杆约束器来连接到对象,以确保它们保持相对固定的

儿子的谜题始于同一个房间中的玩家,但一切都是侧面的。

当玩家们到达谜题的主要区域时,他们会发现 Roblox 上的一个熟悉的景象:障碍赛。 此特定的障碍赛由几个旋转平台和旋转墙壁组成,还有“安全区”来进行故事。 我们将专注于旋转/旋转元素。

让人难以置信的外观隐藏了游戏玩法在这里很简单的事实。

我们为什么在这里使用限制?因为 TweenService 或其他方法在玩家站在它们上时不会移动玩家。没有对象将玩家移动到平台上,因此有人可以跳上平台并从它们下面跳出。相反,我们想要玩家通过旋转平台而导航通过下一个

你还可以在试图导航障碍赛时观看你的朋友旋转。

为此,我们需要先使用我们当前套件中的资产,然后为视觉效果添加任何新内容。我们制作了几个不完整的墙壁和平台,因为我们不想创建一堆独特的平台,所以我们单独创建了 4 个基础部分和 4 个扶手部分。这允许我们混合不同的基础部分和扶手部分来有很多变化。

我们知道我们使用限制时,我们不能使用这些网格,因为它们不会移动即使有限制/控制器驱动它们。 限制需要是一个

现在是时候设置该关节约束本身的实际行为,并添加将零件和约束结合起来的附件。我们将转向附件放在 Motor_Turn 上,其走道部分是由螺栓固定拥有,另一个附件是 Motor_Anchor 上的 Anchor 行为

要让平台在常定速度旋转,我们然后设置了 HingeConstraint.AngularVelocityHingeConstraint.MotorMaxAccelerationHingeConstraint.MotorMaxTorque 属性值,以便允许移动并防止中断,如果玩家跳上它。

附件0 是 hinge 的基础,附件1 代表 hinge 本身。我们有 hinge 不断旋转,但您也可以使用门上的门限制。

现在我们需要制作旋转墙。墙壁需要在其明显中心上旋转,我们知道我们想要它们能够处理任何与剩余等级别相关的方向。像平台一样,我们将它们构建为可以牢固地连接到Motor_Turn的所有墙壁。

我们想重用尽可能多的实际树屋网格来节省性能,所以我们跟随了平台的路径。 几个墙壁类型被作成,可以在不同的组合中粘在一起。

我们使用 Texture 对象在 SurfaceAppearance 对象上添加一些变化来我们的基础材料。 Textures

您可以看到类似的行为,以及为关节约束设置,还可以看到我们使用 Texture 对象。

我们测试了几个平台和旋转墙壁,然后我们做了几个变化,并与其放置进行了游戏,以确保障碍赛是有挑战性、引人注目、并且清晰地显示玩家需要前往哪里! 需要进行一些调整,例如其价值和位置,以便使其在测试中运行良好。 我们有几个点,平台和墙壁

如果您不确定您的物理对象正在击中什么,您可以从视图选项 上角右侧的 3D 视窗中切换碰撞忠实度。

A close up view of the 3D viewport with the Visualization Options button indicated in the upper-right corner.
禁用碰撞视图时,您可以看到游戏中显示的普通几何形象表示。
当启用了碰撞视图时,您可以看到树叶不会发生碰撞,因此它们不会影响旋转平台或墙壁。

您可以看到门/窗户洞下面有些可见,但像子-paning等小细节是不是的。 因为墙壁的 CollisionFidelity 属性为 箱子 。 我们不需要精确度为这些板,所以为了节省性能成本,这是一个足够细节的位置以便玩家跳上。