在体验中的任何环境中创建运动,可以让它立即感觉更沉浸、更真实地融入我们的世界,无论是来自环境树的运动、玩家互动的反应门,或是碰到它们时会移动的盒子。工作室有许多独特的方法来创建运动,以帮助世界感觉更活跃,包括物理系统、TweenService 和动画,分析您的体验特定需求可以帮助您确定要使用哪一个。在本节中,我们将展示我们如何在 Studio 中确定想要创建的运动类型以及我们使用的工具来实现这些不同的目标。

创建风暴
风暴经历了许多迭代,在我们确定在 Duvall Drive 神秘之中发生的事情之前。早期,我们认为风暴是一根巨大的黑曜石柱,在之后的迭代中,我们认为它是通往腐化空间的巨型传送门。经过多次尝试不同风暴的独特外观和感觉后,我们最终选择了一个拥有更小中心“眼睛”的风暴,因为:
- 风暴应该让玩家感受到这次事件对世界的影响 ,包括树被吹倒和碎片飞扬 。
- 云自身的旋转涡流应该让玩家看到中央门户 ,而不是透露一切 。这将鼓励玩家更近地调查,看看发生了什么。
- 光线的更紧凑点将允许我们专注于房屋的组成,这是主角和大部分游戏玩法所在的地方。




为了让风暴在其环境中感觉动态、激进和不断变化,我们使用了以下系统和功能:
- TweenService - 用于云移动。
- 照明变更 - 用于创建云到云闪电。
- 光束 - 对于"体积照明"和闪电。
- 粒子发射器 - 用于飞向传送门并由于风吹飞的碎片。
- 动画 - 对于那些被风吹动的树。
添加带纹理的云
虽然 动态云 对于普通、高海拔的真实云来说很棒,但我们需要一些感觉很戏剧的东西,可以更多地指导和自定义。为了实现这一目标,我们将表面外观对象应用于一系列堆叠重叠的云网格,以伪造云覆盖。为什么我们要将它们堆叠并层次如此重?因为当每个云网格以不同速度移动时,它们会交叉并创建互相内部和外部的云形状。这个过程使云感觉更动态和自然一些,尽管只是旋转圆盘。云也必须是半透明的,因为我们希望玩家能够通过它们看到房子前中央的一些亮色!


由于每个云网格必须巨大才能完全环绕房屋并传达风暴的巨大程度,我们知道我们需要在个人云网格上使用的纹理,以便它在网格表面上大量重复,因此我们知道我们需要地砖我们想使用的纹理,以便它在网格表面上大量重复。我们在这些简单的部件上测试了我们为云制作的材料,然后将它们应用到涡流上!

与粒子发射器或光束不同,网格允许我们能够将光反弹到每个网格上,这对于我们想要实现云到云闪电非常重要。我们还模型了扭曲,以便灯光反弹时看起来有深度!这在特别情况下非常重要,因为体验的性能要求降低了我们表面外观对象的质量等级。

旋转云朵网格
在我们对云的整体视觉外观满意之后,我们需要让它移动起来!我们已经有每层云的一般形状,但在确保旋转效果在实践中看起来很好之前,需要进行一些试验和错误。我们最初尝试使用 约束 来介绍速度,可以物理驱动云移移动工具。这比我们想要的要更难实现,玩家永远不会与它互动,所以我们不需要它在移动中那么精确。
我们想要一个易于使用的方法来旋转那些太远以至于无法互动的实例,例如云朵,或太小或装饰性以至于对游戏/物理学没有重要意义的内部家具,例如小灯。我们决定使用一个 LocalScript 来减少客户端-服务器带宽、允许更平滑的移动,每个云网格都能够有不同的旋转率和延迟。为了使其更通用,我们还允许指定旋转轴。是可以使用 3 个属性的,但对于我们的情况,我们使用了 3 个值:Axis、Delay 和 Speed 。

正如在演示中的许多情况一样,我们使用了一个 LocalSpaceRotation 标签,以便我们能够使用实例标记插件来管理受影响的实例。我们只使用了一个单独的 LocalScript 来处理所有标记的实例,使用 CollectionService 因此我们没有太多的脚本需要维护整个开发过程。
在我们的演示中,世界的部分区域从 ServerStorage 复制到工作区,根据需要,所以我们需要处理标记的对象被创建和被销毁的情况。使用 LocalScripts 时,我们还必须意识到流式传输,其中网格和其子值可能会流出流入。我们最初在 Init() 函数中处理放置的对象,并连接到 CollectionService.GetInstanceAddedSignal 和 CollectionService.GetInstanceRemovedSignal 来处理标记的新创建/已删除对象。同一个 SetupObj 函数被用来初始化新对象在 Init() 和在 CollectionService.GetInstanceAddedSignal 中。
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()
objInfo 是一个包含所有相关对象信息的地图,例如其旋转速度和轴。请注意,我们不会立即调用 SetupObj 从 CollectionService.GetInstanceAddedSignal ,但我们添加了一个对象到 objInfoQueue 。当服务器上的 streaming 和 cloning 对象被调用时,当 CollectionService.GetInstanceAddedSignal 被调用时,我们可能还没有得到我们的 Axis 、 Delay 和 Speed 值,因此我们将对象添加到队列中,并在 SetupObj 函数的后续帧中调用 Update 以直到值出现并我们能够阅读它们到每个对象的 "信息" 结构。
我们在 Update 函数连接到心跳的情况下旋转了实例。我们得到了父转换(parentTransform),积累了新的旋转角度(curObjInfo.curAngle),根据该对象的旋转速度计算了本地转换(rotatedLocalCFrame)),最后将其设置为 CFrame 。请注意,父级和对象都可以是 Model 或 MeshPart ,因此我们必须检查 IsA("模型") 并使用 PrimaryPart.CFrame 或 CFrame 。
local parentTransformif parentObj:IsA("Model") thenif not parentObj.PrimaryPart then-- 主要部分可能还没有被传输continue -- 等待主要部分复制endparentTransform = parentObj.PrimaryPart.CFrameelseparentTransform = parentObj.CFrameendcurObjInfo.curAngle += dT * curObjInfo.timeToAnglelocal rotatedLocalCFrame = curObjInfo.origLocalCFrame * CFrame.Angles( curObjInfo.axisMask.X * curObjInfo.curAngle, curObjInfo.axisMask.Y * curObjInfo.curAngle, curObjInfo.axisMask.Z * curObjInfo.curAngle )if obj:IsA("Model") thenobj.PrimaryPart.CFrame = parentTransform * rotatedLocalCFrameelseobj.CFrame = parentTransform * rotatedLocalCFrameend
我们检查了是否需要设置有效的 Model.PrimaryPart 来处理流媒体。如果在我们的对象上调用了更新,而 Model.PrimaryPart (可以指向子网格)尚未流传,我们将简单跳过更新。当前系统是对象旋转的第二次重复,而以前的系统工作了不同的方式:值是 12 倍不同的!为了保持相同的数据,我们在我们的脚本中将其转换为“12 * obj.Speed.Value”!
设计闪电击
因为 Studio 不提供出盒闪电生成器,并且粒子系统有一些限制,不适合英雄闪电攻击,我们必须想办法为英雄闪电攻击提供解决方案。我们决定使用两个主系统来构成闪电:从风暴眼来的英雄闪电攻击使用纹理光束,这些纹理光束会显示并同步到音频和后期效果,而远程云到云闪电使用简单的粒子效果。
纹理光线
我们通常会使用顺序器或时间线工具来驱动这样的灯光束击中效果的时间,但由于 Studio尚未提供此功能,我们决定编写控制灯光束时间的脚本。这效果的脚本相对简单,但它实现了以下重要目标:
- 闪电波次的元素,例如其纹理、亮度和延迟,每次击中都会随机化。
- 音频和后期效果的更改与打击效果同步。
- 室内或在污损区的玩家将无法看到或听到它们。
我们有一侧服务器 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 = 10ppCC.Brightness = maxPPBrightnessppBloom.Intensity = 1.1bottom.Position = top.PositiontweenBrightness:Play()tweenPPBrightness:Play()tweenPPBrightness:Play()tweenBottomPos:Play()tweenBrightness.Completed:Wait()-- 音频if audioFolder and audioPart thenif audioFolder.Value and audioPart.Value thenaudioUtils.PlayOneShot(audioObj, audioFolder.Value, audioPart.Value)endendtask.wait(info.waitTillFlashes)-- and so on
要检查玩家是否在室内,我们使用了一个助助函数 inVolumesCheckerFunc,它查看预先放置的音量接近室内区域,并检查玩家位置是否位于其中任何一个(PointInABox)。我们可以使用触摸基于检测,但我们发现当玩家在音量内坐下时,他们不再“触碰”音量。在几个箱子中测试一个点是更简单的,我们只在玩家移动到足够远的位置时才这样做。
要检查玩家是否在损坏区域,我们调用了一个助助函数 gameStateInfoFunc,该函数检查当前游戏状态。要从文件夹中随机播放声音,我们还使用了一个助助函数 PlayOneShot。对于闪电本身,这在 Photoshop 中很容易创建;我们画了一条扭曲的线,然后添加了一个“外部发光”层效果。


利用粒子发射系统
英雄闪电击是由一个粒子系统支持的,该系统通过创建背景中捕捉到遥远攻击的云层印象或云到云闪电来建议远程闪电。我们通过一个非常简单的粒子系统实现了这个效果,该系统闪烁着云端主风暴云的边缘上的云广告牌。系统定期发出云朵粒子,使用随机透明曲线:


让树在风中吹动
我们让云和闪电按我们想要的方式工作后,我们还需要添加两个主要组成部分的暴风:风和雨!这些元素带来了一些挑战,包括需要在 Studio 当前限制我们物理和特效系统的限制内工作。例如,使树随着实际风移动是今天的引擎不可能的,因此我们使用了粒子发射器效果和自定义角色动画为树。
我们知道要真正地出售风和雨的效果,我们需要树本身移移动工具。有几种方法可以在引擎内执行此操作,包括使用 插件 公开可用的部件移动,使用 TweenService 或直接动画模型。对于我们的目的,动画给了我们控制我们想要从我们的树中获得的运动的能力,并允许我们使用我们在体验中可以分享给所有树的单个动画。
我们从 终极模型包 - 森林资产 开始皮肤了几棵树。由于这些树已经存在,而且我们的经验发生在太平洋北西部,因此它节省了我们在早期创建每个树模型的时间。

在我们选择了我们的树之后,我们知道我们需要把它们剥皮。将网格皮肤化是将另一个3D建模应用程序(例如Blender或Maya)中的节点(或骨头)添加到网格上,然后对那些节点/骨头施加影响,以移动网格。这最常用于 人形角色 ,但使用 自定义角色 ,你可以皮肤几乎一切。
我们知道我们想节省时间并重复使用相同的动画,因此我们建造了我们的第一个树架,并确保关节名称是通用的,因为我们想在其他树架上使用这些相同的名称。我们还知道我们需要包括主要、次要和三级关节/骨头,使树干与风同弯,树枝摆动,叶子似乎在回应中摇摆。为了这个过程,我们需要创建一个 次要运动 ,这是一个动画概念,其中任何行动都会导致对象的其他部分反应该行动并似乎超越初始运动。

一旦我们创建了我们的关节/骨头,就是时候创建测试动画来移动 Studio 中所有的关节和骨头,看看它是否按照我们想要的方式移动。要做到这一点,我们必须通过 导入树到工作室 通过 自定义装备 设置在 3D 导入器 中,然后使用 动画编辑器 移动/动画网格。我们在这些测试后设置了材料和纹理,但您可以在下面看到结果。

在我们对那个树的结果满意之后,是时候在不同的树上测试相同的动画了!我们已经知道每种树输入之间的动画将是相同的,所以我们只是确保我们的动画看起来足够一般,以便在高大的红杉树和坚固的橡树树之间使用!

为了做到这一点,我们从那个森林包中提取了蜜蜂木树,并建造了一个类似的骨架,使用相同的名称为节点进行命名。这是为了确保之前导入的动画也可以应用到这个树。由于动画都基于旋转节点,无论树有多大、小、高或宽!

在我们拆解和皮肤冬青木树之后,我们就可以导入它并应用相同的动画。这意味着循环和编辑只需要在一个文件上进行,并且在运行体验时也节省了更少的动画。

一旦我们拥有了我们想要动画的所有树类型,我们将每个变成 包装 以便我们继续编辑和更新,在主体验区周围播放多个动画。因为我们知道它们有运行成本,所以我们在效果最有价值的房子周围稀罕使用它们!在未来随着它变得更具有效率时,你将能够添加越来越多的磨损网格实例!

制作暴风灾害物
我们想让雨看起来很重,让雾和垃圾通过树木吹走。为了实现这一目标,我们设置了几个隐形部件,作为粒子体积与子粒子发射器immediately below the large storm clouds。由于 Studio 中的粒子数量限制,我们无法使用一个粒子发射器为整个空间。我们反而添加了几个与每个人相同大小的网格模式,因为树的存在意味着玩家无法看得很远。

雨水粒子利用了一个新的粒子发射器属性ParticleEmitter.Squash,可以让你使粒子变得更长或更扎堆。特别适合下雨,因为它意味着我们不需要大雨纹理,只需要拉伸那里的纹理。只需知道,如果您增加 ParticleEmitter.Squash 的值,您可能需要增加整体 ParticleEmitter.Size 属性,以便它不太瘦!整体而言,这只是玩弄值直到我们获得足够的雨量,但不至于阻碍体验的可见性!


对于通过的雾霾和叶子,添加单个较大的部件体积覆盖更少的区域要简单得多,因为我们不需要一次运行大量粒子。我们首先设置了一个卷umen并获得了需要它们的粒子频率。


之后,我们制作了我们的叶子吹风和风纹理,并将粒子设置为所有旋转/移动以不同速度开始,并以不同速度开始。这意味着更大的雾粒子会更自然地互动,而不像重复的纹理那样看起来,尤其是考虑到它们的大小。



结果是树移动、窗口吹动和闪电创造了风暴周围眼球的效果。
设置风暴之眼
发光核心的破碎石眼旨在给玩家第一次提示,房屋内发生了一些邪恶和奥秘的事情,他们应该进一步探索。由于我们的场景很暗,眼睛在天空中很高,因此创建可信的破碎石轮廓很重要,但创建可信的石面细节不太重要,因为玩家无法看到它。了解什么是可行的,让你的玩家在场景灯光中看到的内容,在投入大量时间进行不必要细节之前,可以节省你在开发过程中的许多资源。


玩家距离还意味着我们可以完全依赖普通地图来获得眼睛的表面细节,因此网格只是一个简单的球体!我们雕刻了细节到高 поли网格,并将其正常地图烘烤到更低的 поли球上,以便我们获得所有这些美丽的细节,而不需要承担巨大的性能成本。



为了给眼睛添加超自然的感觉,并强调其存在,我们决定创建一个会渗透其裂缝的发光、霓虹熔岩。虽然没有表面外观的发射通道,但我们通过创建眼睛来克服这个障碍:一个用于岩石外表面,另一个稍小一点用于发光的熔岩。在 材质画家 中,我们为外部球体创建了透明的基础颜色纹理,以便内部核心在我们希望它通过的区域通过。在 搅拌机 中,我们“vertex painted”了内部球体,以便宜且简单的方式获得一些颜色变化。

创建眼睛时遇到的另一个挑战是由我们使用 流式 结合眼睛与玩家的距离决定的。由于这个结构的中心性,我们希望它始终可以被看到,但是没有对其网格进行任何黑客攻击,玩家不能在室外看到眼睛,除非他们在温室里。我们能够通过添加一些几何图形到眼睛和其环来强制眼睛在场景中的持续存在。这个几何坐在地形表面下方,这就足以让引擎认为球体离玩家比现有更近,并且始终在传输。虽然应该很少强制传输太多大件物体,但强制传输太多大件物体可能会抵消启用传输的好处,并且对游戏性能产生负面影响。
我们能够通过使用与旋转云朵网格相同的脚本将运动添加到眼睛和其环圈上,感谢它能够将云朵网格旋转。为了最后一次触碰,我们决定添加一个提示到云之外另一个世界的存在,但我们必须采取创造性的方法来避免添加更多几何图形到场景,并且还必须处理以前提到的由启用流式传输造成的障碍。我们创建了一个场景,由于对象的相对尺寸和距离很大,渲染了这个场景的图像,然后使用了该图像作为风暴眼后方放置的部件的贴花。我们用于旋转这个部分的方法与我们用于眼睛和其环的方法相同。

制作扩展的食堂
产生最有趣的东西之一是破坏空间,在那里我们可以通过改变周围的现实,实际上扰乱玩家对现实的期望。例如,在父亲的谜题中,我们想模仿一个类似于梦魇的时刻,无论你奔跑得多快,房间似乎一直在变长。我们决定制作一个扩展的储藏室,可以让玩家在寻找配料时逃离,将房间恢复到正常状态。
我们通过简单的墙壁移动和我们房间的智能布局来设置这一点,该布局将出现在烹饪室的两侧。在房间的正常状态下,配餐室是一个简单的走廊,但在损坏的空间中,它实际上要长得多,有几个翼和一堵假墙!


虚假墙是一个模型组,我们将在玩家进入触发音量时返回到它们之前库房透明部分的一部分。那个触发器也被用在了与我们所有门使用的类似的脚本中,该脚本称为 TweenService 从一个目标移动到另一个目标。我们使用部分体积来告诉拐角操作,其开始和结束位置是为墙壁。


因为 TweenService 是如此普遍的系统,所有我们的墙数据模型都必须包含相同的组件。例如,一般的“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
一旦我们拥有了虚假墙移动到房间后方,我们需要其余的内容与其移动。为了做到这一点,我们需要所有零散的物品在移动时焊接到墙上。使用 焊接约束,我们很快就能将所有对象焊接到储物室墙上以作为单个对象移动。这意味着我们可以选择不焊接这些物品,以便玩家可以撞到它们并将其击倒!
制作污损的树屋
工作室是一种奇妙的物理基础引擎,你可以使用它来创建从摆动门到旋转平台的一切。通过我们的演示,我们希望使用物理学来在一组不太现实的环境中创造一种现实感。仅使用几个 约束 ,你就可以在自己的体验中创建一些有趣和挑战性的障碍路线!

约束 是一组基于物理的汽缸,可以对象进行对齐并约束行为。例如,你可以使用 杆限制 来连接对象,以保持它们之间的固定距离,或使用 绳索限制 使灯从线的末端悬挂。对于玩家被运送到研究的腐败状态的儿子的谜题,我们想要真正地翻转世界。这样做会扰乱玩家对现实和其规则的期望,同时仍使用物理系统作为预期的那样!

一旦玩家下到拼图的主区域,他们在 Roblox 上遇到了一个熟悉的景象:障碍赛。这个特定的障碍路线由几个旋转平台和旋转墙组成,以及进展故事的“安全区域”。我们将专注于旋转/旋转元素。

为什么我们在这里使用约束?因为 TweenService 或其他方法在他们站在它们上时不会移动玩家。没有物体移动玩家,有人可以跳上平台,它将从下面旋出。相反,我们希望玩家在尝试将跳跃到下一个平台时穿过旋转平台。由于这种方法,玩家感觉根植在他们站立的地方,在决定如何继续通过课程时,我们不需要做任何特殊的事情来确保他们移动到旋转表面!

为此,我们需要先使用我们当前的套件的资产,然后添加任何新内容以实现视觉效果。我们制作了一些不完整的墙壁和平台,里面有洞来讲述祖母建造树屋的故事。因为我们不想创建一堆独特的平台,我们制作了 4 个不同的基地部件和栏杆部件单独。这允许我们混合和匹配个人基地和栏杆零件,以获得很多种类。

我们知道,因为我们使用了约束,所以无法将这些网格锚定,因为它们即使有约束/驱动器驱动它们,也不会移动。需要限制成为锚定在世界上的东西的孩子,以便平台不仅仅脱离世界。我们通过一个我们命名为 Motor_Anchor 的部分来解决这个问题,该部分具有 铰链约束 来驱动平台的整体运动。之后,我们需要两个网格像一个整体移动,因此我们创建了一个我们命名为 Motor_Turn 的部分,然后将两个网格焊接到它上。这样,限制将能够在单个部件上工作,而不是多个铰链与多个部件一起工作。

现在是设置铰链约束自身的实际行为时间,并添加将作为零件和约束的限制的附件。我们将旋转附件放置在 Motor_Turn 上,该步道部件与其焊接,另一个附件放置在 Motor_Anchor 本身上,与铰链约束旁边,用于锚定行为。由于这需要单拥有旋转,而不是由玩家影响(像门铰链),我们将 HingeConstraint.ActuatorType 设置为 发动机 ,将约束视为自行移动的发动机。
为了保持平台以恒定速度旋转,我们然后设置了 HingeConstraint.AngularVelocity , HingeConstraint.MotorMaxAcceleration 和 HingeConstraint.MotorMaxTorque 属性为值,以便移动并防止中断,如果玩家跳到它上面。

现在我们需要制作旋转的墙。墙需要在其显著中心旋转,我们知道我们希望它们能够处理与其余等级别相关的任何方向。像平台一样,我们构建了这些,使所有的墙都未锚定且焊接到 Motor_Turn。

我们使用了 Texture 对象来顶替 SurfaceAppearance 对象来添加一些变化到我们的基础材料。纹理,类似于图标,允许您在网格平面上放置图像。如果您想将泥土添加到砖墙上,或使用相同的木材材料让木头看起来老化,这可能有用。Texture 对象的行为略有不同于 Decal 对象,你可以按照自己的需要瓷砖和抵消图像,这很有用,如果你想要能够缩放你的覆盖纹理并不介意它重复!

一旦我们测试了几个平台和旋转墙壁,我们制作了几种变化并与其放置进行了游戏,以确保障碍路线具有挑战性、神秘性,同时也清晰地告诉玩家需要前往哪里!需要调整其价值和位置才能让它们顺利运行。我们有几个地方平台和墙壁正在击中彼此或周围,但通过一些移动和频繁的测试,我们能够降落在我们在演示中拥有的设置上!
如果你不确定你的物理对象正在击中什么,你可以在 3D 视窗右上角的 可视化选项 选项卡中切换 碰撞精度 。



如您在下面看到的门廊/窗口孔是可见的,但较小的细节,例如子面板,不是可见的。这是因为墙壁的 CollisionFidelity 属性被设置为 箱子 。我们不需要这些面板的精度,因此为了节省性能成本,这已经详细到足以让玩家跳上它们。随着平台和旋转墙完成,我们只需要添加像盒子和灯具这样的细节资产,然后就可以播放了!
