動く世界を開発する

*このコンテンツは、ベータ版のAI(人工知能)を使用して翻訳されており、エラーが含まれている可能性があります。このページを英語で表示するには、 こちら をクリックしてください。

体験の中のどんな環境でも動きを生み出すことで、すぐに私たちの世界により没入感がありリアルに感じられるようになります。これは、環境音や、プレイヤーの相互作用によって反応するドア、またはぶつかったときに動く箱などから生じます。Studioには、物理システム、TweenService、アニメーションなどを含む、世界をより生き生きと感じさせるためにモーションを作成するための多くのユニークな方法があります。このセクションでは、Studioで作成したい動きのタイプと、それを達成するために使用したツールを説明します。

嵐を作る

嵐は、私たちが『The Mystery of Duvall Drive』で使用するものが決定される前に、多くの反復を経ました。初期には、嵐を巨大な黒曜石の柱と考え、後の反復ではそれを腐敗した空間への巨大なポータルと考えました。異なる外見と感触を持つさまざまな嵐を試みた結果、私たちは小さな中央「目」を持つ嵐に決定しました。理由は以下の通りです:

  • 嵐はプレイヤーに この出来事が世界に与える影響、すなわち木々が吹き飛ばされ、ゴミが飛び回る様子を感じさせるべきです。
  • 雲自体の回転する渦は、すべてを明らかにすることなく、プレイヤーに中央のポータルを 覗かせる べきです。これによって、プレイヤーは何が起こっているのかを確認するためにさらに調査するよう促されます。
  • 光の密集点は、家の構図に集中させることができ、家自体が主要なキャラクターであり、ほとんどのゲームプレイが行われる場所であるためです。

嵐をダイナミックで攻撃的、そして環境の中で常に変化し続けるものにするために、以下のシステムと機能を使用しました:

  1. TweenService - 雲の動きのために。
  2. 照明の変化 - 雲から雲への雷を作成するために。
  3. ビーム - 「ボリュメトリックな照明」と雷のために。
  4. パーティクルエミッター - ポータルへ向かうゴミや風によって飛び回るために。
  5. アニメーション - 風に吹かれている木のために。

テクスチャ付きの雲を追加する

動的雲は、通常の高高度のリアルな雲に最適ですが、私たちが必要としていたのはドラマチックに感じられ、より強く指示しカスタマイズできるものでした。それを実現するために、半透明のサーフェス外観オブジェクトを、厚く重ねた雲メッシュのシリーズに適用しました。このようにして、雲のカバーを偽装するためです。なぜそんなに重ねて扱ったかというと、各雲メッシュが異なる速度で動くと、交差して相互に入ったり出たりする雲の形を作るからです。このプロセスにより、雲は単に回転する円盤に過ぎないにもかかわらず、よりダイナミックで自然に感じられるようになりました。また、雲は半透明であることが重要でした。なぜなら、プレイヤーが家に到着する前に中央に明るいものを確認できるようにしたかったからです!

単一の雲メッシュ。
テクスチャなしの重ねられた雲メッシュ!

各雲メッシュは巨大で、家を完全に囲み、嵐の大きさを伝える必要があったため、使用するテクスチャを個々の雲メッシュにタイルする必要があることがわかりました。このテクスチャをメッシュの表面全体に強く繰り返すためです。雲のために作成した材料をこれらの単純なパーツでテストした後、渦に適用しました!

粒子エミッターやビームとは異なり、メッシュを使用すると、各メッシュで光を反射させることができました。このことは、雲から雲への雷を実装したいときに重要です。それに加えて、ねじれをモデル化したことで、光が反射して奥行き感を持つように見えるようにしました!これは特に、パフォーマンスの要求が高まり、サーフェス外観オブジェクトの品質レベルが低下する状況では重要でした。

照明を追加すると、メッシュが照明により良く反応するように詳細を追加する必要がありました!

雲メッシュを回転させる

雲の全体的なビジュアル外観に満足した後、動かす必要がありました!各雲レイヤーの一般的な形状は整いましたが、実際にスピニング効果が良く見えるかを確認するにはいくらかのトライアンドエラーが必要でした。最初は、制約を使用して、物理的に雲を動かす速度を導入しようとしました。これは後に反復する際に望むほど簡単ではなく、プレイヤーがそれに対して何のインタラクションもしないため、動きの精度をそれほど必要としませんでした。

インタラクティブではない、あるいはプレイや物理的に重要ではない内部家具のような雲に対しても、簡単に回転させる方法が必要でした。そこで、LocalScriptを使用することに決しました。これにより、クライアントサーバーの帯域幅を削減し、よりスムーズな動きを実現し、それぞれの雲メッシュが異なる回転速度と遅延を持てるようになりました。一般的にするために、回転軸を指定できるようにもしました。3つの属性を使用することが可能ですが、私たちの場合は3つの値を使用しました:AxisDelaySpeed

デモの多くのケースで、LocalSpaceRotation タグを使用して、インスタンスタグ付けプラグインを使用してStudioで影響を受けるインスタンスを管理できるようになりました。私たちは、CollectionServiceを使用してすべてのタグ付きインスタンスを処理する単一のLocalScriptだけを利用しました。これにより、開発プロセス全体にわたって維持する必要があるスクリプトが大量に作成されるのを防ぎました。

私たちのデモでは、世界の一部は必要に応じて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 は、回転速度や軸など、すべての関連オブジェクトに関する情報を持つマップです。CollectionService.GetInstanceAddedSignal から SetupObj を即座に呼び出すことはせず、オブジェクトを objInfoQueue に追加しています。ストリーミングとサーバーでクローンされたオブジェクトのため、CollectionService.GetInstanceAddedSignal が呼ばれた際には、AxisDelaySpeedの値がまだ取得できていない場合があるため、オブジェクトをキューに追加し、Update 関数からの後続フレームで SetupObj を呼び出します。そうすれば、値をそこに持ち込み、各オブジェクトの「情報」構造に読み取ることができます。

Update 関数を接続して、ハートビートに基づいてインスタンスを回転させました。親のトランスフォーム(parentTransform)を取得し、このオブジェクトの回転速度に基づいて新しい回転角度(curObjInfo.curAngle)を蓄積し、ローカルトランスフォーム(rotatedLocalCFrame)を計算し、最終的にそれを CFrame に設定しました。親とオブジェクトの両方が Model または MeshPart である可能性があるため、IsA("Model") を確認し、PrimaryPart.CFrame または CFrame を使用する必要がありました。


local parentTransform
if parentObj:IsA("Model") then
if not parentObj.PrimaryPart then
-- primary part might might not be streamed in yet
continue -- wait for primary part to replicate
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は何も手を加えずに出力できる雷ジェネレーターを提供していないため、粒子システムには制約があり、ヒーローの雷打撃に適さないという制限があったため、ヒーローの雷打撃の解決策を考え出す必要がありました。雷の効果を構成するために、私たちは2つの主要なシステムを決定しました:嵐の目からのヒーローの雷打撃用のテクスチャビームは、音声とポストプロセス効果と同期し、明示的に操作されるテクスチャビームであり、遠くの雲間の雷に対してはシンプルな粒子効果を使用します。

テクスチャビーム

通常、このような雷撃のタイミングを制御するためにシーケンサーやタイムラインツールを使用しますが、Studioはこの機能をまだ提供していないため、雷撃のタイミングをコントロールするスクリプトを書くことに決めました。この効果のスクリプト化は非常にシンプルであり、以下の重要な目標を達成します:

  1. 雷撃の要素、例えばテクスチャ、明るさ、遅延などは、すべてのストライクでランダム化されます。
  2. 音声とポスト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)
-- indoorsではFXなし
if inVolumesCheckerFunc:Invoke() then
return
end
-- "通常"世界にいないときはFXなし
if not gameStateInfoFunc:Invoke("IsInNormal") then
return
end

さらに、テクスチャ、位置、明るさを設定するためのシーケンスを実行し、ツイーンを実行し、task.wait(数値) を利用します。ランダム化されたパラメータはサーバーから受け取った情報構造に由来し、一部の数値は固定されています。


beam.Texture = textures[info.textIdx]
beamPart.Position = Vector3.new(info.center.X + og_center.X, og_center.Y, info.center.Y + og_center.Z)
-- Wipe
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()
-- audio
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で非常に簡単に作成できました。私たちは曲がりくねった線を描き、その後「外側の輝き」レイヤー効果を追加しました。

粒子エミッターシステムを活用する

ヒーローの雷撃は、遠くの雷が光をひろい、雲と雲の間の雷を示唆するような粒子システムでサポートされています。私たちは、主要な嵐の雲の周辺にいるクラウドビルボードを点滅させる非常にシンプルな粒子システムを通じてこの効果を達成しました。システムは定期的に雲の粒子を発生させて、ランダム化された透明度のカーブを持っています。

木を風に吹かせる

雲と雷が私たちの望むように機能した後、嵐のもう2つの主要な要素を追加する必要がありました:風と雨!これらの要素はいくつかの課題を提示しました。たとえば、木々を本当の風で動かすことは、今日のエンジンでは不可能ですので、粒子エミッターエフェクトと木々のためのカスタムキャラクターアニメーションを利用しました。

風と雨の効果を本当に実現するためには、木々自体に動きが必要だということが分かっていました。エンジンの中でこれを行う方法はいくつかありましたが、公開されているプラグインを使用してパーツを移動させたり、TweenServiceを使用したり、モデルを直接アニメーションさせたりすることができます。それらの目的のために、アニメーションは木々から望む動きを制御できる能力を提供し、体験内のすべての木で共有可能な単一のアニメーションを使用できるようにしました。

私たちはまず、Endorse Model Pack - Forest Assetsから数本の木をスキニングしました。これらの木はすでに存在しており、私たちの体験は太平洋北西部で行われるため、モデルを一つ一つ作成する時間を早い段階で節約できました。

フォレストパックにはいくつかの木の種類が含まれており、自身の体験で時間を節約できます。

木を選択した後、スキンを付ける必要があることを理解しました。メッシュをスキンすることは、3Dモデリングアプリケーション(Blenderマヤなど)でメッシュに関節(またはボーン)を追加し、それらの関節/ボーンに影響を与えてメッシュを動かす行為です。これはヒューマノイドキャラクターで最も一般的に使用されますが、カスタムキャラクターがあれば、ほぼすべてのものをスキンできます。

時間を節約し、同じアニメーションを再利用したいと思ったため、最初の木のリグを構築し、関節の名前を一般的なものにしました。他の木に対するリグでも同様の名前を使用したいと思ったからです。また、風に吹かれる際に幹が曲がるための主要、二次、三次の関節/ボーンを含める必要があるとも理解しました。枝が揺れるため、葉が振動する様子にも関与する必要があります。このプロセスには、二次動作を作成する必要がありました。これは、任意の動作がオブジェクトの他の部分にその動作に反応させ、最初の動きに追いつくように見えるアニメーションの概念です。

木には、風に吹かれることで信じやすい動きを得るために、一次、二次、三次の関節があります。

関節/ボーンを作成した後、すべての関節とボーンをStudioで動かす試験アニメーションを作成する必要がありました。それをするために、インポーターカスタムリグ設定を通じて、木をStudioにインポートし、アニメーションエディタを使用してメッシュを動かす/アニメーションさせました。これらのテスト後、マテリアルおよびテクスチャを設定しましたが、以下の結果を参照してください。

Studio内の同じ階層。

その木の結果に満足したら、違う木で同じアニメーションをテストする時間が来ました!木の種類それぞれの異なるリグ間で同じアニメーションになることが分かっていたため、アニメーションが背の高いレッドウッドとたくましいブナの木の間で一般的に機能するように見えることを確認しただけでした!

レッドウッドの木にインポートされたアニメーション。

そのため、フォレストパックのブナの木を使って、同様のリグを構築し、関節の名前をまったく同じものにしました。こうすることで、以前にインポートされたアニメーションをこの木にも適用することができました!アニメーションがすべて回転する関節に基づいているため、木のサイズや大きさは問題になりませんでした!

ブナの木は、その関節に対して全く同じ命名がされていますが、数は異なります。アニメーションシステムは、特定の関節名に一致する関節にのみアニメーションを適用するため、この理由から、同じアニメーションを当てはめることができました!

ブナの木をリグおよびスキンした後、インポートでき、全く同じアニメーションを適用することができました。これにより、イテレーションや編集は1つのファイルだけで済み、経験を実行する際のアニメーションが減る分パフォーマンスを向上させることができました。

アニメーションエディタを使用すると、ブナの木に同じレッドウッドの木のアニメーションを適用できました!

アニメーションをつけたい木の種類がすべて決まったら、それぞれをパッケージにまとめ、メインエリアの周りでいくつかのアニメーションを再生している間も編集や更新を続けることができるようにしました。それらにはパフォーマンスコストがあるため、最も価値のあるエリアの周りで控え目に使用しました!将来的には、これがより効率的になることで、より多くのスキンメッシュインスタンスを追加できるようになります。

渦が最も強く、視覚的効果がプレイヤーにとって最も影響力がある周囲の家の周りにアニメーション化された木を使用しました。

嵐のゴミを作る

私たちは、雨を重そうに見せ、霧やゴミを木々の中で吹き流す必要がありました。それを実現するために、大きな嵐の雲のすぐ下に、粒子ボリュームとして機能するいくつかの不可視パーツを設置し、子の粒子エミッターを配置しました。Studio内の粒子数制限のため、全スペースに対して1つの粒子エミッターを使用することはできませんでした。代わりに、同じサイズの複数の粒子エミッターをプレイヤーが見ることのできない木々の存在ある領域に合わせる形でグリッド状に配置しました。

雨量と特定の雨のカバレッジを得るために、いくつかのボリュームを使用しました。

雨粒子は、ParticleEmitter.Squashという新しいパーティクルエミッターのプロパティを活用しました。これにより、パーティクルを長くしたり、短くしたりすることができます。これが雨に特に便利なのは、粒子を大きくする必要がなく、ただ既存のテクスチャを引き伸ばせばよかったからです。ただし、ParticleEmitter.Squashの値を増加させた場合は、ParticleEmitter.Sizeプロパティも増やさなければならないことに注意が必要です。全体的には、値をいじっていくつかのテストを行いましたが、どれほど雨が重たくてもレベルの可視性を妨げない程度を見つけるのが重要でした!

3のスクワッシュ値は、テクスチャを長く引き伸ばし始めます。
20のスクワッシュ値は、粒子を非常に長く引き伸ばしますが、同時にサイズ値も増やす必要がありました。

霧、霧、飛び回る葉については、パーティクルを多く同時に動かす必要がなかったため、少し大きなボリュームを配置して、これらを追加する方が簡単でした。まずボリュームを設定し、粒子の頻度を配置したい位置にしました。

いくつかのパーティクルボリュームがあり、霧が木の中に入らないようにし、霧がさまよう必要はないと感じたからです。
霧のパーティクルボリュームは、粒子が大きかったため、粒子をまったく狙いを定めないで済みました。

その後、葉が吹き飛ぶ際のテクスチャを設定し、すべてのパーティクルが異なる速度と開始速度で回転/移動するように設定しました。これにより、大きな霧粒子はより自然に相互作用し、特にそのサイズを考慮すると繰り返しのテクスチャのように見えにくくなりました。

霧粒子
葉の粒子

その結果、木々の動き、窓の吹く風、雷の間で素晴らしいアクションが生まれ、嵐が中央の目を取り囲む効果が生まれました。

嵐の目を構築する

欠損した石の目と輝く核心は、プレイヤーに家で展開されている邪悪で神秘的な出来事があることを示す最初のヒントを与えます。私たちのシーンが暗く、目が空高くあるため、信じられる破損した石のシルエットを作成することが重要でしたが、プレイヤーはそれを見ることができないため、信じられる石の表面の詳細を作成することはそれほど重要ではありませんでした。シーンの照明内でプレイヤーが見ることが現実的であるかを理解すれば、不要なディテールに多くの時間をかける前に、開発プロセスで多くの資源を節約できます。

シーンの初期段階で最終的な照明を設定すると、不要な作業が大幅に削減できます。シーンの最終的な照明ではリングの表面ディテールが見えないので、そこに時間をかける必要はありませんでした!

プレイヤーとの距離も、目の表面詳細をノーマルマップに完全に依存させることを考慮しており、メッシュは単なる平面球になります!高ポリメッシュで詳細を彫刻して、ノーマルマップをより低いポリメッシュにベーキングすることで、パフォーマンスコストを抑えつつ、美しいディテールを取得できました。

高ポリスカルプト
低ポリメッシュ
高ポリスカルプトからベーキングされたノーマル情報を持つ低ポリメッシュ

目に超自然的な感覚を加え、その存在感を強調するために、ひび割れから滲み出る発光ネオンマグマを作成することに決めました。サーフェス外観に放出チャネルが存在しないため、私たちは目を2つの球体で作成することによってこの問題を克服しました。ひとつは岩の表面用、もう一つは発光するマグマのために少し小さなものです。Substance Painterで、外側の球体の基準色テクスチャを必要な内部コアが見える部分で透明に作成しました。Blenderで、内部球体を「頂点塗装」しました。これにより、十分な色のバリエーションを得るための安価で簡単な方法が得られました。

内部球体の頂点塗装。目の周りが最も明るい色でグラデーションを作成することで、奥行き感と視覚的な興味を高めました。

目を作成する際に直面した別の課題は、私たちのストリーミングの使用と目のプレイヤーからの距離が結びついていました。この構造の中心性を考慮し、プレイヤーからの距離にもかかわらず常に表示されるようにしたいと考えましたが、メッシュにハックを行わなければ、プレイヤーは太陽室にいない限り目を見ることができませんでした。ジオメトリを目やそのリングに追加することで、目をシーンに常に表示することができました。このジオメトリは地形の表面すぐ下に配置され、エンジンをだまし、球体がプレイヤーに近い位置にあると認識されて、それを常にストリーミングすることができます。ただし、あまりにも多くの大きなオブジェクトをストリーミングさせることは避け、ストリーミングの利点を打ち消すことがあり、パフォーマンスに悪影響を及ぼしますので、注意が必要です。

目とそのリングの動きを追加することができ、以前に雲メッシュを回転させるために使用したものと同じスクリプトを使用しました。最後の仕上げとして、雲の向こうにある異世界の存在感をほのめかすヒントを追加しましたが、それにはシーンにジオメトリを追加することなく、何らかのデザインアプローチを取る必要がありました。サイズと距離の相対性によって多くの深さを持つシーンを作成し、そのシーンの画像をレンダリングし、その画像を目の後ろに設置されたパーツへのデカールとして使用しました。目とそのリングを回転させるために使用したのと同じ方法を用いてこのパーツも回転させました。

雲の向こうにある世界の錯覚を作成するために使用した画像。プレイヤーが何かから遠く離れているときには、単純な画像がシーンのより深さと複雑さの Illusion を作成するのに十分かもしれません!

拡張パントリーを作る

最も楽しいものの一つは、腐敗した空間で、現実の予想を絶えず変えていくことでした。たとえば、父のパズルでは、どんなに速く走っても部屋がますます長く感じられるという悪夢のような瞬間を模倣したいと思いました。プレイヤーが部屋を元の状態に戻すための材料を探している間に動き回る拡張パントリーを作成することに決めました。

この機能は、壁のシンプルな動きと、私たちの部屋の巧妙なレイアウトによって設定され、パントリーの両側に現れる部屋が作られました。部屋の通常の状態では、パントリーは単なる通路だったのですが、腐敗した空間では、実際にははるかに長く、いくつかの羽と偽の壁がありました!

キッチンパントリーの腐敗した状態。
プレイヤーから遠ざかっている偽の壁。

偽の壁は、プレイヤーがトリガーボリュームに入った瞬間に後ろに移動させるモデルグループでした。この透明なパーツはパントリーの早い部分で、プレイヤーが通過するようにしました。このトリガーは、すべての扉で使用されるスクリプトに似たスクリプトでも使用され、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, --Time/Speed of Door Tween
Enum.EasingStyle.Quart, --Easing Style
Enum.EasingDirection.InOut, --EasingDirection
0, --Repeat Count
false, --Reverse true
0 --Delay
)
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
--model["Door"]:Play()
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
--print("touch")
playersNear[player] = 1
if doorState == DoorState.Closed then
StartOpening()
end
end
end
end

偽の壁が部屋の裏に移動した後、私たちはその内容すべてを一緒に移動させる必要がありました。それを行うために、パントリーのゆっくりと動く壁に溶接されたすべてのアイテムを、壁に移動させる必要がありました。Weld Constraintsを使用して、すべてのオブジェクトをすぐにパントリーの壁に溶接させることで、一つのオブジェクトとして移動できるようにしました。これにより、プレイヤーがそれらにぶつかり、つかんだりできるようにする選択肢が得られました!

腐敗したツリーハウスを作る

Studioは、揺れるゲートから回転するプラットフォームまで、すべてを作成するために使用できる素晴らしい物理ベースのエンジンです。私たちのデモでは、物理学を使用して、現実的ではない環境でもリアリズムの感覚を創り出すことを目指しました。ほんの少しの制約を用いることで、自分自身の تجربهで楽しみながら挑戦的な障害物コースを作成できます!

**制約**は、オブジェクトを調整し、行動を制約する物理ベースのモーターです。たとえば、ロッド制約を使用してオブジェクトに接続し、固定距離を保つことができます。または、ロープ制約を使用して行灯の最後に吊るすこともできます。息子のパズルでは、プレイヤーが腐敗した状態の書斎に運ばれるとき、私たちは実際の世界を横倒しにしたいと考えました。そうすることで、プレイヤーの現実の期待とそのルールをひっくり返しつつ、物理システムを本来の意図どおりに使用します。

息子のパズルは、プレイヤーが同じ部屋にいる状態から始まりますが、すべてが横倒しになっています。

プレイヤーがパズルのメインエリアに降りると、ロブロックスでおなじみの光景が待っています。それは障害物コースです。この特定の障害物コースは、いくつかの回転プラットフォームと回転壁で構成されており、ストーリーを進める「安全なエリア」があります。私たちは回転する/回転する要素に焦点を当てます。

心を惑わす外観は、ここでのゲームプレイが非常にシンプルであることを隠しています。

私たちがなぜここで制約を使用したのかというと、TweenServiceや他のメソッドがそれらの上に立つプレイヤーを移動させることができないからです。オブジェクトがプレイヤーを移動させない限り、誰かがプラットフォームに飛び乗れば、それが彼らの下で回転しませんでした。代わりに、プレイヤーは次のプラットフォームへのジャンプを試みながら回転するプラットフォームをナビゲートする必要があると考えました。このアプローチにより、プレイヤーは進む方法についての決断をしながら自分が立っているところに根付いていると感じました。しかも、彼らが回転する表面に合わせて動くために特別なことを行わなくて済みました!

障害物コースを横切りながら、友達が回転しているのを見られます。

これを実現するために、私たちはまず現行のキットから資産を使用し、視覚効果のために新しいコンテンツを追加しました。私たちは、木の家を建てた祖母の物語を伝えるためにいくつかの不完全な壁とプラットフォームを作成しました。異なるプラットフォームをたくさん作成したくなかったので、4つの異なる基本部分と手すり部分をそれぞれ別々に作成しました。これにより、個々のベースおよび手すり部分を混ぜ合わせ、さまざまな種類を持つことができました。

私たちは制約を使用していたため、これらのメッシュを固定しても、物の動きはないと分かっていました。制約は、アンカーされた何かの子にする必要がありました。そのため、プラットフォームが世界から単に落ちないように、 Motor_Anchor と名付けたパーツを使って、全体的なプラットフォームの動きを駆動するためのヒンジ制約を使用しました。その後、2つのメッシュが一緒に動く必要があるため、 Motor_Turn と名付けられたパーツを作成し、2つのメッシュを溶接しました。これにより、制約は複数の部分ではなく、単一の部分で機能できるようになりました。

次に、ヒンジ制約自体の実際の動作を設定し、パーツと制約を一緒に調整するためのアタッチメントを追加する必要があります。回転するアタッチメントをMotor_Turnに配置し、歩道部分をそこに溶接し、もう一つのアタッチメントをMotor_Anchor自体のヒンジ制約の隣に配置しました。これはプレイヤーに影響されず(ドアのヒンジのように)、自ら回転する必要がありましたので、HingeConstraint.ActuatorTypeMotor に設定し、制約を自ら動くモーターとして扱いました。

プラットフォームを一定の速度で回転させ続けるために、HingeConstraint.AngularVelocityHingeConstraint.MotorMaxAccelerationおよびHingeConstraint.MotorMaxTorqueプロパティを設定し、動きを許可し、プレイヤーが飛び乗った場合、妨害されないようにしました。

Attachment0 は実際にヒンジのアンカーとなり、Attachment1 はヒンジ自体を表しました。ヒンジは常に回転し続けますが、ドア用にヒンジ制約を使用することもできます。

次に、回転する壁を作る必要がありました。壁はその表面中心で回転しなければならず、私たちはレベルの他の部分に関連する任意の向きを処理する必要がありました。プラットフォームと同様、すべての壁をアンカーしないように構築し、Motor_Turnに溶接しました。

パフォーマンスを節約するために、実際のツリーハウスメッシュの大部分を再利用したかったので、プラットフォームのように同じ経路をたどりました。いくつかの壁タイプが異なる組み合わせで組み合わさるように作られました。

SurfaceAppearanceオブジェクトの上にTextureオブジェクトを利用して、基本的な材料のバリエーションを追加しました。テクスチャは、メッシュの平面に画像を配置することを可能にします。これによって、レンガの壁に汚れを加えたり、古びた木で同じ木材の素材を使ったりすることができます。Textureオブジェクトは、Decalオブジェクトとは異なる振る舞いを持って、画像を任意にタイルとオフセットを利用できるため、オーバーレイテクスチャをスケールする必要がない場合には便利です!

ヒンジ制約の類似した振る舞いとセットアップの他に、Textureオブジェクトをどのように使用したかを見ることができます。

数個のプラットフォームと回転壁をテストした後、いくつかのバリエーションを作成し、配置を調整することで、障害物コースが挑戦的で、心を打つものであることを確認しました。また、プレイヤーがどこへ行くべきかを明確にしました!彼らの値と位置を調整する必要があり、うまく機能させるためにテストを行いました。プラットフォームと壁が互いに衝突しないように何度も調整し、頻繁なテストを重ねることで、私たちはデモにある設定にたどり着きました!

物理オブジェクトが何に当たっているか分からない場合は、3Dビューポートの上部右端にある可視化オプションウィジェットから衝突精度を切り替えることができます。

3Dビューポートの拡大表示。可視化オプションボタンが右上に示されています。
衝突ビジュアル化が無効の場合、ゲーム内で表示される通常のジオメトリ表現を見ることができます。
衝突ビジュアル化が有効にされると、木の葉が衝突を持たないため、回転するプラットフォームや壁に干渉しないことが見えます。

ドアや窓の穴が見える一方で、サブパネリングのような小さいディテールは見えないことに注意してください。これは、壁の CollisionFidelity プロパティが Box に設定されているためです。これらのパネルには精度をそれほど必要としていないので、パフォーマンスコストを節約するため、これがプレイヤーが飛び乗るには十分な詳細でした。プラットフォームや回転壁ができたら、最後に箱やランプなどのディテールのアセットを追加すれば、プレイする準備が整いました!

©2026 Roblox Corporation。Roblox(ロブロックス)、RobloxロゴおよびPowering Imaginationは、米国並びにその他の国における登録商標および非登録商標です。