開發移動世界

*此內容是使用 AI(Beta 測試版)翻譯,可能含有錯誤。若要以英文檢視此頁面,請按一下這裡

在體驗內的任何環境創建運動,都會幫助它立即感覺更沉浸於我們的世界,無論是從環境樹的運動、玩家互動的反應門,或是碰到它們時會移動的盒子。工作室有許多獨特的方法來創建運動,以幫助世界感覺更活躍,包括物理系統、TweenService 和動畫,分析您的體驗特定需求可以幫助您決定使用哪一個。在這一節中,我們將展示我們如何在 Studio 中決定要創建哪種類型的運動,以及我們使用哪些工具來實現這些不同的目標。

創建風暴

風暴經歷了許多反覆之後,我們才決定在 Duvall Drive 神秘之中使用什麼。早期,我們認為風暴是一根巨大的黑曜石柱,在後續的迭代中,我們認為它是通往腐化空間的巨型傳送門。經過多次測試不同風暴的外觀和感覺後,我們選擇了一個擁有較小中心「眼」的風暴,因為:

  • 暴風應該讓玩家感受到這個事件對世界的影響 ,包括樹木被吹倒和碎片飛行
  • 雲朵本身的旋轉漩渦應該讓玩家看到中央傳送門 ,而不是透露一切 。這將鼓勵玩家更近地調查,看看發生了什麼事。
  • 光線更緊的地方可以讓我們專注於房屋的構成,這是主角和大部分遊戲發生的地方。

為了讓風暴在環境內感覺動態、具有攻擊性且不斷變化,我們使用了以下系統和功能:

  1. TweenService - 用於雲端移動。
  2. 燈光變更 - 用於創建雲到雲的閃電。
  3. 光束 - 對於"體積照明"和閃電。
  4. 粒子發射器 - 對於飛向傳送門並因風吹而飛行的碎片。
  5. 動畫 - 對於在風中吹擊的樹木。

添加擁有紋理的雲

雖然 動態雲對於普通、高海拔的真實雲來說很棒,但我們需要一些感覺極具戲劇性的東西,可以更多地指導和自訂。為了做到這一點,我們將表面外觀物體與半透明度應用到一系列堆疊和分層的雲網格,以模擬雲覆蓋。為什麼我們要將它們堆疊和覆蓋得如此嚴重?因為當每個雲網格以不同速度移動時,它們會相交並創建雲形狀,進入和離開彼此。這個過程讓雲感覺更動態和自然一點,雖然只是旋轉碟子。重要的是雲朵也必須是半透明的,因為我們希望玩家能夠透過它們看到房屋中央的亮物體!

單一雲端網格。
>

沒有紋理的分層雲朵網格!
>

因為每個雲網格需要巨大地包圍住房屋並傳達風暴的巨大程度,我們知道我們需要在個別雲網格上使用的紋理,以便它在網格表面上大量重複,所以我們知道我們需要瓷磚我們想使用的紋理,以便它在網格表面上大量重複。我們在這些簡單的零件上測試了我們為雲端製作的材料,然後應用到漩渦上!

與粒子發射器或光束不同,網格允許我們能夠將光反彈到每個網格上,這對我們想要實現雲到雲閃電非常重要。我們也模型了扭轉,以至於照明反彈到它上面會看起來有深度!這在特定情況下非常重要,因為體驗的性能需求降低了我們表面外觀對象的品質等級。

一旦我們開始將燈光添加到它上,我們就需要將細節添加到網格上,讓它們更好地反應燈光!

旋轉雲端網格

當我們對雲的整體視覺外觀感到滿意後,我們需要讓它運行!我們已經擁有每層雲的一般形狀,但在確保旋轉效果在實踐中看起來很好之前,還需要進行一些試錯。我們最初試圖使用 限制 來介紹速度,將物雲驅動移移動工具。這比我們想要的更難以迭代,玩家永遠不會與它互動,因此我們不需要它在移動中那麼準確。

我們想要一個易於使用的方法來旋轉那些太遠以至無法互動的實例,例如雲朵,或太小或裝飾以至不能對游戲/物理有重要影響的室內家具,例如小燈。我們決定使用 LocalScript 來減少客戶端與服務器的帶寬,允許更平滑的運動,並讓每個雲網格能夠擁有不同的旋轉速率和延遲。為了使其更一般化,我們也讓指定旋轉軸成為可能。是可以使用 3 個特性,但在我們的情況下,我們使用了 3 個值:AxisDelaySpeed

正如在演示中的許多情況一樣,我們使用了 LocalSpaceRotation 標籤,因此我們可以使用實例標示外掛程式來管理 Studio 中受影響的實例。我們只使用了單個 LocalScript 來處理所有標記的實例,使用 CollectionService 來保持整個開發過程中的大量腳本。

在我們的示範中,世界的部分會從 ServerStorage 複製到工作區,根據需求,所以我們需要處理標籤對象被創建和摧毀的情況。使用 LocalScripts 時,我們也必須知道流式傳輸,其中網格和其子值可能會在進出流式傳輸。我們最初在 Init() 函數中處理放置的對象,然後連接到 CollectionService.GetInstanceAddedSignalCollectionService.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 是一個包含所有相關對象資訊的地圖,例如它們的旋轉速度和軸。請注意,我們不會立即呼叫 SetupObjCollectionService.GetInstanceAddedSignal ,但我們添加了一個對象到 objInfoQueue 。當服務伺服器上的對象進行串流和複製時,當 CollectionService.GetInstanceAddedSignal 被呼叫時,我們可能還沒有得到我們的 AxisDelaySpeed 值,因此我們將對象添加到隊列中,並在 SetupObjUpdate 函數後的下一個框架中呼叫 ,直到值出現並我們能夠將其讀入每個對象的"信息"結構。

我們在 Update 功能連接到心跳的情況下旋轉了實例。我們得到了父轉換(parentTransform),累積了新的旋轉角度(curObjInfo.curAngle),根據這個對物件的旋轉速度計算了本地轉換(rotatedLocalCFrame) ),最後將其設置為CFrame 。請注意,父和對象都可以是 ModelMeshPart ,因此我們必須檢查 IsA("模型") 並使用 PrimaryPart.CFrameCFrame


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 (可以指向兒子網格)尚未傳送,我們就會跳過更新。目前系統是對物件旋轉的第二次重複,而以前的系統運作不同:值是 12 倍不同!為了保持相同的資料,我們在我們的指令碼本中轉換成 "12 * obj.Speed.Value" 一樣。

設計閃電攻擊

因為 Studio 沒有提供盒子外的閃電生成器,並且粒子系統有一些限制,不適合英雄閃電攻擊,我們必須創造一個解決方案來應對英雄閃電攻擊。我們決定使用兩個主要系統來組成閃電:從風暴眼來的英雄閃光攻擊使用紋理光束,揭示並同步到音頻和後期效果,遠程雲到雲閃光使用簡單的粒子效果。

紋理光束

我們通常會使用順序器或時間線工具來驅動照明波擊效果的時間,但由於 Studio尚未提供此功能,我們決定寫出控制照明波時間的腳本。這效果的腳本相當簡單,但它實現了以下重要目標:

  1. 閃電波擊的元素,例如紋理、亮度和延遲,每次擊中都會隨機化。
  2. 音頻和後期效果變更與打擊效果同步。
  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 創建這些非常簡單;我們畫了一條扭曲的線,然後添加了一個「外部發光」層效果。

使用粒子發射系統

英雄閃電擊中是由一個粒子系統支持的,該粒子系統通過創建背景中捕捉遠處閃光的雲層印象或雲到雲閃光來建議遠處閃光。我們通過一個非常簡單的粒子系統來實現這種效果,該系統會在主風暴雲的外緣閃爍一個雲廣告牌。系統定期以隨機透明曲線發出雲朵粒子:

讓樹在風中吹拂

當我們擁有雲和閃電在我們想要的方式工作後,我們需要添加兩個暴風雨的主要組成部分:風和雨!這些元素帶來了一些挑戰,包括需要在 Studio 目前的物理和特效系統限制內工作。例如,使樹木隨著實際風移動是今天的引擎無法實現的,因此我們使用了 粒子發射器 效果和 自訂角色動畫 為樹木。

我們知道要真正賣出風和雨的效果,我們需要樹本身移移動工具。有幾種方法可以在引擎內執行此操作,包括使用 插件 公開可用的部件移動,使用 TweenService 或直接動畫模型。對我們而言,動畫讓我們能夠控制我們想要從樹上獲得的運動,並允許我們使用單一動畫來分享所有樹木內的經體驗。

我們從 認可模型包 - 森林資產 開始脫皮幾棵樹。因為這些樹已經存在,而我們的經驗發生在太平洋北西部,所以它可以讓我們在早期就省下一些時間,不用一一創建每個樹模型。

森林包含多種樹種,可以節省您在自己的體驗中的時間。

選擇我們的樹木之後,我們知道我們需要把它們剝掉。將網格脫皮是將節點(或骨頭)添加到另一個3D建模應用程序的網格,例如BlenderMaya,然後對這些節點/骨頭施加影響,以移動網格。這最常用於 人形角色 ,但使用 自訂角色 ,你可以皮膚差不多任何東西。

我們知道我們想節省時間並重複使用相同的動畫,因此我們建造了我們的第一個樹架並確保聯結名稱是通用的,因為我們想在其他樹架上使用這些相同的名稱。我們也知道我們需要包括主要、次要和三級關節/骨頭,以便樹枝彎曲於風,枝葉揮動,葉子似乎在回應中震動。為了這個過程,我們需要創建一個 次要運動 ,這是一個動畫概念,其中任何行動都會導致物體的其他部分反應於該行動,並似乎追上初始運動。

樹木有主、次和三級節點,這樣我們就可以從被風吹擊中獲得可信的運動。

一旦我們創建了我們的關節/骨頭,就是時候創建測試動畫來移動 Studio 中所有的關節和骨頭,看看它是否按照我們想要的方式移動。為了這麼做,我們必須 將樹子匯入 Studio 通過 自訂模型 設置在 3D 匯入器 中,然後使用 動畫編輯器 移動/動畫網格。我們在這些測試後設置材料和紋理,但您可以在下面看到結果。

Studio 內的相同層級。

在我們對那棵樹的結果感到滿意之後,是時候測試相同的動畫在不同的樹上了!我們已經知道這將是每種樹輸入之間的相同動畫,因此我們只需要確保我們的動畫看起來足夠一般,以便在高大的紅樹和堅固的棕櫚樹之間使用!

我們在紅樹上輸入的動畫。

為了做到這一點,我們從那個森林包中取得 Beechwood 樹,並建造了類似的骨架,使用相同的命名來標示聯結。這樣就可以將之前我們匯入的動畫也應用到這個樹上。由於動畫都是基於旋轉聯節,所以樹的大小、小、高或寬並不重要!

棕櫚木樹有相同的命名方式用於其節點,只不過數量不同。這很好,因為動畫系統只會將動畫應用於那些與名稱相匹配的特定關節!因此,我們可以將相同的動畫應用於與關節名稱相匹配的任何東西!

在我們配置和皮膚蜜蜂樹之後,我們就可以匯入它並應用相同的動畫。這意味著只需要在一個文件上循環和編輯即可完成,並且在執行體驗時也節省了更少的動畫。

使用動畫編輯器,我們可以將同樣的紅樹動畫應用於比奇樹!

一旦我們有所有我們想要動畫的樹種,我們就將它們變成 包裝 ,這樣我們就可以繼續編輯和更新,在體驗主要區域周圍播放多個動畫時。既然我們知道它們有一定的性能成本,我們在效果最有價值的房屋周圍緩緩使用它們!未來隨著它們變得更具有效率,你將能夠添加越來越多的外掛網格實例!

我們使用動畫樹立即在房子周圍,在漩渦最強烈的地方,視覺效果將對玩家產生最大影響。

製作暴風廢棄物

我們想讓雨看起來很重,讓霧和垃圾通過樹木吹走。為了做到這一點,我們設置了幾個隱形零件來作為粒子體積,與兒子 粒子發射器 即時下方的大風雲。由於 Studio 中的粒子數量限制,我們無法使用一個粒子發射器為整個空間。我們反而添加了一些相同尺寸的東西,在播放區域空間上的網格模式中,因為樹木的存在意味著玩家無法看得很遠。

我們使用了多個卷來獲得雨量和我們想要的特定雨量。

雨水粒子利用了新的粒子發射器屬性 ParticleEmitter.Squash ,可以讓粒子變得更長或更沉。對於雨來說特別有用,因為它意味著我們不需要大雨紋理,只需要伸展到那裡的紋理。只需知道,如果您增加 ParticleEmitter.Squash 值,您可能需要增加整體 ParticleEmitter.Size 屬性,以便它不太瘦!整體而言,只是玩弄值直到我們得到雨量足夠多,但不至於阻礙體驗的視覺!

一個 Squash 值為 3 開始伸展更長的紋理。
>

壓縮值 20 延長了粒子的時間,但我們也需要增加尺寸值。
>

對於通過的霧、霧和葉子,添加單一較大的零件體積覆蓋較少的區域要簡單得多,因為我們不需要一次運行大量粒子。我們開始設置音量,並獲得想要的粒子頻率。

結果有幾個粒子部分量,所以我們沒有粒子進入房屋,而且因為我們不覺得它們需要像霧一樣穿過樹木。
>

霧粒子部分體積要比粒子大,因為粒子很大,我們不需要那麼精確地定位。
>

之後,我們製作了葉子的吹風和風紋理,並將粒子設為全部以不同速度旋轉/移動,並從不同速度開始。這意味著較大的霧粒子會更自然地互動,而不會看起來像是重複的紋理,特別是考慮到它們的尺寸。

霧粒子
>

葉子粒子
>

結果是在樹木移動、窗戶吹和閃電創建風暴周圍眼睛的效果之間的一些很棒的行動。

設置暴風眼

發光核心的破碎石眼是用來給玩家第一個線索,即房屋裡發生了危險且神秘的事情,他們應該進一步探索。由於我們的場景很暗,眼睛在天空中高處,因此創建一個可信的破碎石側面形狀很重要,但創建可信的石面細節不太重要,因為玩家無法看到它。了解您的玩家在場景燈光中看到的內容是否切實可行,可以在投入大量資源進行不必要細節之前節省許多資源。

早期設置場景中的最終燈光可以節省大量不必要的工作。你不能使用我們場景的最後照明來查看環上的表面細節,所以沒有必要花時間將它們放置在那裡!

玩家與之間的距離也意味著我們可以完全依賴普通地圖來獲得眼睛的表面細節,因此網格只是一個簡單的球體!我們將細節雕刻成高精度的網格,並將其正常地圖烘培到低精度的球體上,這樣我們就可以獲得所有這些美麗的細節,而不會有巨大的性能成本。

高度複合雕刻
>

低維度網格
>

低波點網格與高波點雕刻中包含的普通資訊烘培在一起
>

為了在眼睛上增加超自然的感覺並強調其存在,我們決定創建一種會滲透其裂縫的發光、霓虹岩漿。雖然沒有表面外觀的發射通道,但我們透過創建兩個球體來克服這個障礙:一個用於岩石外表面,另一個稍小一點用於發光的岩漿。在 材質畫家 中,我們為外球體創建了透明區域的基礎顏色紋理,以便內核通過所需的區域。在 攪拌器 中,我們"vertex painted"了內部球體,以便宜且簡單的方式獲得一些顏色變化。

內球上的頂點繪畫。我們創建了一個渐變,在眼睛周圍最輕,以提供更大的深度感和視覺興趣。

當我們創建眼睛時,另一個我們遇到的挑戰是由我們使用 串流 與眼睛距離玩家的距離所決定的。考慮到這個結構的中心性,我們希望它始終能夠可見,但是沒有對其網格進行任何駭入,玩家不能在室外看到眼睛,除非他們在溫室裡。我們能夠通過添加一些幾何圖形到眼睛和它的環來強制眼睛在場景中的持續存在。這個幾何坐落在地形表面下方,這就足夠讓引擎認為球體離玩家比它更近,並且始終在線傳輸。雖然應該很少強制傳輸過多的大型物件,但強制傳輸過多的大型物件可能會否認啟用傳輸的好處,並且對遊戲性履約產生負面影響。

我們能夠透過使用與旋轉雲端網格相同的腳本來將眼睛和其環加以移動旋轉雲端網格。為了最後一次觸碰,我們決定添加一個提示到雲朵之外另一個世界的存在,但我們必須採取創意的方法來避免添加更多的幾何圖形到場景,並且還必須處理之前提到的困難,即啟用了流式傳輸。我們創建了一個場景,由於對象的相對尺寸和距離,使得場景具有很大的深度,渲染了這個場景的圖像,然後使用了這個圖像作為風暴眼後方放置的零件的標誌。我們使用與眼睛和其環相同的方法來旋轉這個部分。

我們使用的圖像,可以創造出超過雲朵的世界虛擬形象。當玩家離某物很遠時,簡單的圖像可能足夠創造出場景的更多深度和複雜度的錯覺!

製作擴展倉庫

產生最有趣的東西之一是破壞空間,在那裡我們可以通過字面地改變環境來扭轉玩家對現實的期望。例如,在父親的謎題中,我們想模擬一種類似惡夢的時刻,無論你執行得多快,房間感覺就像一直在延長一樣。我們決定製作一個擴展倉庫,玩家在尋找配料來將房間恢復正常狀態時會逃走。

我們用牆壁簡單的移動和我們房間的聰明布置來設置這一點,這些房間將出現在飯廳兩側。在房間的正常狀態下,倉庫是一個簡單的走廊,但在破壞的空間中,它實際上要長得多,有幾個翼和一面假牆!

廚房倉庫的破壞狀態。
>

假牆向玩家走開。
>

假牆是一個模型群組,當玩家進入觸發音量時,我們會將其移回到倉庫的透明部分,這是他們之前走過的一部分。那個觸發器也被用於與我們所有門使用相似的腳本,稱為 TweenService 從一個目標移動到另一個目標。我們使用零件體積來告知拐角操作,其開始和結束位置是為牆壁。

零件體積會觸發後面的假牆移動到其終點。它在這張圖像中變得可見,帶有黃色濾色。
Target_Closed 是我們在所有門上使用的通用目標部分,用於旋轉的地方。這裡被重新使用來告訴走廊牆前往哪裡。

因為 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 的部分來解決這個問題,該部分具有 hinge 限制 來驅動平台的整體運動。之後,我們需要兩個網格一起移動,因此我們創建了一個名為 Motor_Turn 的零件,然後將兩個網格焊接到它上面。這樣,限制就能夠在單一零件上運作,而不是多個鏈條與多個零件一起運作。

現在是時候設置hinge約束本身的實際行為,並添加將作為零件和約束的方向的附件。我們將旋轉附件放置在 Motor_Turn 上,該走道部件與其焊接,另一個附件用於 Motor_Anchor 本身的錨定行為,在鉸鏈限制旁邊。因為這需要單擁有旋轉,而不是被玩家影響(像門鉸鏈),我們將 HingeConstraint.ActuatorType 設置為 引擎 ,將限制視為自行移動的引擎。

為了保持平台以恆定速度旋轉,我們然後設置 HingeConstraint.AngularVelocityHingeConstraint.MotorMaxAccelerationHingeConstraint.MotorMaxTorque 屬性為值,以允許移動並防止中斷,如果玩家跳到它上面。

附件0是基本上是鉸鏈的錨,附件1代表了鉸鏈本身。我們一直在旋轉鉸鏈,但您也可以使用鉸鏈限制來控制門。

現在我們需要製作旋轉牆。牆壁需要在它們的顯著中心旋轉,我們知道我們希望它們能處理與其他水等級相關的任何方向。像平台一樣,我們將它們構建成所有牆壁都未錨定且焊接到 Motor_Turn 上。

我們想重複使用尽可能多的實際樹屋網格來節省履約,因此我們遵循了與平台相似的路徑。製作了多種牆壁類型,可以在不同組合中粘在一起以獲得一些變化。

我們使用了 Texture 物件來覆蓋 SurfaceAppearance 物件,以添加一些變化到我們的基本材料。紋理,類似於裝飾,允許您在網格平面上放置圖像。這可能有用,如果你想將灰塵添加到磚牆上,或使用相同的木材料時讓木材看起來老化。Texture 對象的行為稍微不同於 Decal 對象,你可以按照自己的方式瓷磚和抵消圖像,這非常有用,如果你想要能夠縮放你的覆蓋紋理並不介意它重複!

您可以看到相似的行為和設置鉸鏈限制式,以及我們如何使用 Texture 對象。

一旦我們測試了幾個平台和旋轉牆壁,我們做了幾種變化,並與它們的放置玩了,以確保障礙賽是具有挑戰性、迷人的,也清楚玩家需要前往的地方!它們需要調整彼此的值和位置才能正常運行。我們有多個點,平台和牆壁正在互相擊中或周圍環境,但通過一些移動和頻繁測試,我們就能降落在我們在演示中擁有的設置上!

如果您不確定物理對象正在擊中什麼,您可以在 3D 視角右上角的 視覺選項 控件中切換 碰撞精度

A close up view of the 3D viewport with the Visualization Options button indicated in the upper-right corner.

當碰撞視覺化被禁用時,您可以看到在遊戲中顯示的普通幾何表示。
>

當碰撞視覺化啟用時,你可以看到樹葉沒有碰撞,因此它們不會干擾旋轉平台或牆壁。
>

如您在下方可以看到,門口/窗戶孔是可見的,但較小的細節,例如子面板,是不可見的。這是因為牆壁的 CollisionFidelity 屬性被設置為 箱子 。我們沒有需要這些面板的精度,因此為了節省性能成本,這已經足夠詳細,玩家可以跳上它們。完成平台和旋轉牆後,我們只需要添加像盒子和燈具這樣的細節資產,然後就可以播放了!