動く世界を作る

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

ゲーム内のどんな環境でも動きを作り出すことは、周囲の木々の動きやプレイヤーのインタラクションによる反応するドア、さらには衝突したときに動く箱などを通じて、瞬時に私たちの世界により没入感とリアリティを感じさせます。スタジオには、物理システム、TweenService、アニメーションなど、世界をより生き生きと感じさせるための動きを作り出すための多くのユニークな方法があります。ゲームの特定のニーズを分析することで、どの方法を使用するかを決定するのに役立ちます。このセクションでは、スタジオで作成したい動きのタイプをどのように決定し、それを達成するためにどのツールを使用したかを示します。

嵐を作る

嵐は「ダヴァルドライブの謎」でライブしているものに落ち着くまでに多くの反復を経ました。初期の段階では、嵐を巨大な黒曜石の柱として考えていましたが、後の反復ではそれを腐敗した空間への巨大なポータルと見なしました。ユニークな外観と感触を持つさまざまな嵐を試した後、私たちは次の理由から中央に小さな「目」を持つ嵐に落ち着きました:

  • 嵐はプレイヤーにこのイベントが世界に与える影響を感じさせるべきであり、木々が吹き飛ばされ、破片が飛び交う様子を含みます。
  • 雲自体の回転する渦は、プレイヤーに中央のポータルをすべてを明らかにすることなく覗かせるべきです。これにより、プレイヤーは何が起こっているのかを調査するよう促されます。
  • より狭い光のポイントは、家の構成に焦点を当てることを可能にし、これは主なキャラクターであり、ほとんどのゲームプレイが行われる場所です。

嵐を動的で攻撃的、そして環境内で常に変化するように感じさせるために、次のシステムと機能を使用しました:

  1. TweenService - 雲の動きのため。
  2. ライティングの変更 - 雲から雲への雷を作成するため。
  3. ビーム - 「ボリュメトリックライティング」と雷のためのビーム。
  4. パーティクルエミッター - ポータルに向かって飛び上がる破片や風で飛び回るためのもの。
  5. アニメーション - 風に吹かれている木々のため。

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

動的な雲は、通常の高高度のリアルな雲には最適ですが、私たちは劇的で、より強く指示しカスタマイズできるものが必要でした。これを実現するために、半透明の表面外観オブジェクトを、重ねて積み重ねた雲メッシュのシリーズに適用して、雲の覆いを偽装しました。なぜそれらを重ねて積み重ねたのか?それは、各雲メッシュが異なる速度で動くと、互いに交差して雲の形を作り出すからです。このプロセスにより、雲は回転するディスクであるにもかかわらず、より動的で自然に感じられました。また、雲が半透明であることが重要でした。なぜなら、プレイヤーが家に到着する前に、中央に何か明るいものが見えるようにしたかったからです!

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

各雲メッシュは、家を完全に囲み、嵐の巨大さを伝えるために巨大である必要があったため、個々の雲メッシュに使用したいテクスチャをタイル状にして、メッシュの表面全体に繰り返し使用できるようにしました。私たちは、これらのシンプルなパーツで雲のために作成した材料をテストし、その後、渦に適用しました!

パーティクルエミッターやビームとは異なり、メッシュは各メッシュから光を反射させることができ、これは雲から雲への雷を実装したいときに重要でした。また、光が反射するようにねじれをモデリングしました!これは、ゲームのパフォーマンス要求が表面外観オブジェクトの品質レベルを下げた場合に特に重要でした。

ライティングを追加し始めたとき、メッシュがライティングにより良く反応するように詳細を追加する必要がありました!

雲メッシュを回転させる

雲の全体的な視覚的外観に満足した後、動かす必要がありました!各雲層の一般的な形状は整っていましたが、回転効果が実際に良く見えるようにするためには試行錯誤が必要でした。最初は、制約を使用して、物理的に雲を動かす速度を導入しようとしました。これは、後で反復するには難しすぎたため、プレイヤーがそれにインタラクトすることはないので、動きの精度をそれほど必要としませんでした。

インタラクトできないほど遠くにあるインスタンスや、ゲームプレイ/物理的に重要でないほど小さな装飾的なインスタンス(小さなランプなど)を回転させるための使いやすい方法が必要でした。LocalScriptを使用して、クライアントとサーバーの帯域幅を削減し、よりスムーズな動きを実現し、各雲メッシュが異なる回転速度と遅延を持つことができるようにしました。より一般的にするために、回転軸を指定できるようにしました。3つの属性を使用することも可能ですが、私たちのケースでは3つの値を使用しました:Axis、Delay、Speed。

デモの多くのケースと同様に、LocalSpaceRotationタグを使用して、インスタンスタグ付けプラグインを使用してスタジオ内で影響を受けるインスタンスを管理できるようにしました。私たちは、すべてのタグ付けされたインスタンスをCollectionServiceを使用して処理する単一のLocalScriptを利用しました。これにより、開発プロセス全体で維持するスクリプトの数が増えることはありませんでした。

デモでは、世界の一部が必要に応じて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は、すべての関連オブジェクトの回転速度や軸などの情報を持つマップです。CollectionService.GetInstanceAddedSignalからSetupObjを即座に呼び出さないことに注意してください。代わりに、オブジェクトをobjInfoQueueに追加しました。サーバー上でオブジェクトをストリーミングおよびクローンする際に、CollectionService.GetInstanceAddedSignalが呼び出されると、Axis、Delay、Speedの値がまだ取得できていない可能性があるため、オブジェクトをキューに追加し、次のフレームから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
-- プライマリパートがまだストリーミングされていない可能性があります
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(子メッシュを指す可能性がある)に対してUpdateが呼び出されたときにまだストリーミングされていない場合、更新をスキップします。現在のシステムはオブジェクト回転の2回目の反復であり、以前のシステムは異なる方法で機能しました:値は12倍異なっていました!同じデータを保持するために、スクリプト内で「12 * obj.Speed.Value」のように変換しました。

雷撃をデザインする

スタジオは、すぐに使える雷生成器を提供していないため、パーティクルシステムにはヒーローの雷撃に適さない制限があったため、ヒーローの雷撃の解決策を考え出す必要がありました。雷を構成するために、2つの主要なシステムを決定しました:嵐の目から出るヒーローの雷撃のためのテクスチャ付きビームは、音声とポストプロセス効果と同期して表示されるスクリプト化されたテクスチャ付きビームであり、遠くの雲から雲への雷のためのシンプルなパーティクル効果です。

テクスチャビーム

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

  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)
…
-- 屋内では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)
-- など

プレイヤーが屋内にいるかどうかを確認するために、inVolumesCheckerFunc関数を使用し、屋内エリアを近似するために事前に配置されたボリュームを確認し、プレイヤーの位置がそれらのいずれかの内部にあるかどうかをチェックします(PointInABox)。タッチベースの検出を使用することもできましたが、プレイヤーがボリューム内に座ると、もはや「触れていない」ことがわかりました。いくつかのボックス内のポイントをテストする方が簡単で、プレイヤーが以前にテストした位置から十分に離れたときにのみ行います。

プレイヤーが腐敗したエリアにいるかどうかを確認するために、gameStateInfoFunc関数を呼び出し、現在のゲーム状態を確認します。フォルダーからランダムな音を再生するために、PlayOneShot関数も使用しました。雷撃自体は、Photoshopで非常に簡単に作成できました。くねくねした線を描き、「外側のグロー」レイヤー効果を追加しました。

パーティクルエミッターシステムを利用する

ヒーローの雷撃は、遠くの雷を示唆するパーティクルシステムによってサポートされ、遠くのストライクから光を受ける雲の層を作成します。私たちは、メインの嵐の雲の周辺に雲のビルボードを点滅させる非常にシンプルなパーティクルシステムを通じてこの効果を達成しました。システムは、ランダム化された透明度の曲線で定期的に雲のパーティクルを放出します:

木々を風に揺らす

雲と雷が望むように機能した後、嵐の他の2つの主要な要素、風と雨を追加する必要がありました!これらの要素は、スタジオの物理および特殊効果システムの現在の制限内で機能する必要があり、いくつかの課題を提示しました。たとえば、実際の風で木を動かすことは、今日のエンジンでは不可能なので、パーティクルエミッター効果とカスタムキャラクターアニメーションを木々に使用しました。

風と雨の効果を本当に売り込むためには、木々自体が動く必要があることを知っていました。エンジン内でこれを行う方法はいくつかあり、プラグインを使用してパーツを移動させることができる、TweenServiceを使用する、またはモデルを直接アニメーション化することができます。私たちの目的のために、アニメーションは木々から望む動きを制御する能力を与え、ゲーム内のすべての木々で共有できる単一のアニメーションを使用することを可能にしました。

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

フォレストパックには、いくつかの木の種類が含まれており、あなたのゲームで時間を節約できます。

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

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

木々には、風に吹かれて揺れる信じられる動きを持たせるために、主要、二次、三次のジョイントがあります。

ジョイント/ボーンを作成したら、スタジオ内でそれらすべてを動かすテストアニメーションを作成する時間です。これを行うために、インポーターのカスタムリグ設定を通じて木をスタジオにインポートし、アニメーションエディタを使用してメッシュを移動/アニメーション化しました。これらのテストの後に材料とテクスチャを設定しましたが、以下に結果を示します。

スタジオ内の同じ階層。

その木の結果に満足したら、別の木で同じアニメーションをテストする時間です!異なる木の種類の間で同じアニメーションになることはすでにわかっていたので、アニメーションが高木のレッドウッドと頑丈なブナの木の間で機能するのに十分一般的であることを確認しました!

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

これを行うために、フォレストパックからブナの木を取り、同じ正確なジョイントの命名を使用して類似のリグを構築しました。これにより、以前にインポートしたアニメーションをこの木にも適用できるようになりました。アニメーションはすべて回転するジョイントに基づいているため、木の大きさや小ささ、高さや幅は関係ありません!

ブナの木は、ジョイントの命名がまったく同じですが、数は同じではありません。アニメーションシステムは、名前が一致する特定のジョイントにのみアニメーションを適用するため、この理由から、ジョイント名が一致するものには同じアニメーションを適用できました!

リグとスキンを行った後、ブナの木をインポートし、まったく同じアニメーションを適用できるようになりました。これにより、反復と編集は1つのファイルでのみ行う必要があり、ゲームを実行する際のアニメーション数が少なくなり、パフォーマンスも向上しました。

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

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

私たちは、渦が最も強く、視覚効果がプレイヤーにとって最も影響力のある場所である家の周りにアニメーション付きの木を使用しました。

嵐の破片を作る

雨が重く見え、霧や破片が木々を吹き抜けるようにしたいと考えました。これを実現するために、いくつかの見えないパーツを設定して、巨大な嵐の雲のすぐ下に子のパーティクルエミッターを持つパーティクルボリュームとして機能させました。スタジオのパーティクル数制限のため、全体の空間に1つのパーティクルエミッターを使用することはできませんでした。代わりに、プレイ可能なエリアの上にグリッドパターンで同じサイズのいくつかを追加しました。木々の存在により、プレイヤーは遠くを見ることができないためです。

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

雨のパーティクルは、ParticleEmitter.Squashという新しいパーティクルエミッターのプロパティを活用し、パーティクルを長くしたり、短くしたりすることができます。これは、雨に特に便利で、大きな雨のテクスチャを必要とせず、そこにあるものを伸ばすだけで済みました。ただし、ParticleEmitter.Squashの値を増やすと、全体のParticleEmitter.Sizeプロパティも増やす必要があるかもしれませんので、細すぎないようにしてください!全体として、値を調整して雨を十分に重くし、ゲームの視認性を妨げないようにすることが重要でした!

3のSquash値は、テクスチャを長く伸ばし始めます。
20のSquash値は、パーティクルをはるかに長く伸ばしますが、Size値も増やす必要がありました。

霧、霧、葉が吹き抜けるためには、同時に多くのパーティクルを実行する必要がなかったため、少数のエリアをカバーする単一の大きなパートボリュームを追加する方がはるかに簡単でした。ボリュームを設定し、パーティクルの頻度を希望の場所に設定しました。

いくつかのパーティクルパートボリュームがあり、パーティクルが家に入らないようにし、霧のように木々を通過する必要がないと感じました。
霧のパーティクルパートボリュームは、パーティクルが大きいため、はるかに大きく、位置に対してそれほど正確である必要はありませんでした。

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

霧のパーティクル
葉のパーティクル

その結果、木々が動き、窓が吹き飛び、雷が中央の嵐の目を囲む効果を生み出しました。

嵐の目を設定する

ひび割れた石の目と光るコアは、プレイヤーに家で何か不気味で神秘的なことが起こっていることを探求する最初のヒントを与えることを目的としています。私たちのシーンは暗く、目は空高くにあるため、信じられるひび割れた石のシルエットを作成することが重要でしたが、プレイヤーがそれを見ることができないため、信じられる石の表面の詳細を作成することはそれほど重要ではありませんでした。シーンのライティング内でプレイヤーが見ることが現実的であることを知っておくことで、不要な詳細に多くの時間を費やすことを避け、開発プロセスで多くのリソースを節約できます。

シーンの最終的なライティングを早期に設定することで、不要な作業を大幅に削減できます。シーンの最終的なライティングでは、リングの表面の詳細を見ることができないため、そこにそれらを置くために時間を費やす必要はありませんでした!

プレイヤーからの距離も、目の表面の詳細に完全にノーマルマップに依存できることを意味しました。そのため、メッシュは単なる平面の球体です!詳細を高ポリメッシュに彫刻し、そのノーマルマップをはるかに低ポリの球体に焼き付けることで、美しい詳細を得ることができましたが、パフォーマンスコストは大幅に削減されました。

高ポリの彫刻
低ポリメッシュ
高ポリの彫刻から焼き付けられたノーマル情報を持つ低ポリメッシュ

目に超自然的な感覚を加え、その存在を強調するために、ひび割れから漏れ出す光るネオンマグマを作成することに決めました。表面外観には放射チャンネルがないため、この障害を克服するために、目を2つの球体で作成しました:1つは岩の外側の表面用、もう1つは光るマグマ用の少し小さいものです。Substance Painterで、内側のコアが出てくる部分に透明度を持たせた外側の球体のベースカラーテクスチャを作成しました。Blenderでは、内側の球体に「頂点ペイント」を行い、色のバリエーションを得るための安価で簡単な方法を提供しました。

内側の球体の頂点ペイント。目の周りで最も明るいグラデーションを作成し、深さと視覚的な興味を高めました。

目を作成する際に直面したもう1つの課題は、プレイヤーからの目の距離とストリーミングの使用によって課せられました。この構造の中心性を考慮して、距離があっても常に目が見えるようにしたいと考えましたが、メッシュにハックを加えずに、プレイヤーがソラリウムにいる場合を除いて目を見ることができませんでした。目とそのリングに幾分の幾何学を追加することで、目の常時存在をシーンに強制することができました。この幾何学は地形の表面のすぐ下にあり、エンジンが球体をプレイヤーに近いと考えさせ、常にストリーミングされるようにするのに十分です。ただし、あまりにも多くの大きなオブジェクトをストリーミングするように強制すると、ストリーミングの利点が無効になり、パフォーマンスに悪影響を及ぼす可能性があるため、これを行う際には注意が必要です。

目とそのリングに動きを加えることができたのは、雲メッシュを回転させるために使用したのと同じスクリプトのおかげです。最後のタッチとして、雲の向こうに別の世界の存在を示唆するヒントを追加することに決めましたが、シーンに幾何学を追加することを避け、ストリーミングによって引き起こされる以前に述べた障害に対処するために創造的なアプローチを取る必要がありました。相対的なサイズと距離のために深さのあるシーンを作成し、このシーンの画像をレンダリングし、その画像を嵐の目のすぐ後ろに配置されたパートにデカールとして使用しました。このパートを回転させるために、目とそのリングに使用したのと同じ方法を使用しました。

雲の向こうにある世界の幻想を作成するために使用した画像。プレイヤーが何かから遠く離れているとき、シンプルな画像がシーンに深さと複雑さを生み出すのに十分かもしれません!

拡張するパントリーを作る

最も楽しいものの1つは、腐敗した空間で、プレイヤーの現実の期待を文字通り変えることができることでした。たとえば、父のパズルでは、どんなに速く走っても部屋が長く感じるという悪夢のような瞬間を模倣したいと考えました。プレイヤーが材料を探して部屋を元の状態に戻すときに、逃げる拡張するパントリーを作ることに決めました。

これは、壁のシンプルな動きと、パントリーの両側に現れる部屋の巧妙なレイアウトで設定しました。部屋の通常の状態では、パントリーはシンプルな廊下でしたが、腐敗した空間では、実際にははるかに長く、いくつかのウィングと偽の壁がありました!

キッチンパントリーの腐敗した状態。
プレイヤーから逃げる偽の壁。

偽の壁は、プレイヤーがトリガーボリュームに入った瞬間に後ろに移動するモデルグループでした。これは、彼らが通り抜ける透明なパートでした。このトリガーは、すべてのドアで使用されるスクリプトと同様のスクリプトで使用され、TweenServiceを呼び出して、1つの目標から別の目標に移動しました。私たちは、ツイーニング操作が壁の開始位置と終了位置を知るためにパートボリュームを使用しました。

パートボリュームは、背後の偽の壁がその終点に移動するトリガーです。この画像では、黄色の色合いで可視化されています。
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
--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

偽の壁が部屋の後ろに移動したら、残りのコンテンツもそれに合わせて移動する必要がありました。これを行うために、パントリーのすべてのLooseアイテムを壁に溶接する必要がありました。Weld Constraintsを使用することで、すべてのオブジェクトをパントリーの壁に迅速に溶接し、単一のオブジェクトとして移動させることができました。これにより、プレイヤーがそれらにぶつかって動かすことができるように、これらのアイテムを溶接解除するオプションが得られました!

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

スタジオは、揺れるゲートから回転するプラットフォームまで、すべてを作成できる素晴らしい物理ベースのエンジンです。私たちのデモでは、物理を使用して、非現実的な環境のセットでリアリズムの感覚を作り出したいと考えました。わずか数制約を使用することで、独自のゲーム内で楽しく挑戦的な障害物コースを作成できます!

**制約**は、オブジェクトを整列させ、動作を制約する物理ベースのモーターのグループです。たとえば、ロッド制約を使用して、オブジェクトを接続し、固定距離を保つことができます。また、ロープ制約を使用して、ラインの端からランプを吊るすことができます。プレイヤーが腐敗した状態の書斎に運ばれる息子のパズルでは、文字通り世界を横にひっくり返したいと考えました。これにより、プレイヤーの現実とそのルールの期待を覆しながら、物理システムを意図した通りに使用することができます!

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

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

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

なぜここで制約を使用したのか?それは、TweenServiceや他の方法では、プレイヤーがそれらの上に立っている間に移動しないからです。オブジェクトがプレイヤーを動かさない場合、誰かがプラットフォームにジャンプすると、それが彼らの下で回転してしまいます。代わりに、プレイヤーが次のプラットフォームにジャンプしようとする間に回転するプラットフォームを通過するようにしたかったのです。このアプローチにより、プレイヤーは立っている場所に根付いていると感じながら、コースを進む方法を決定することができ、回転する表面と一緒に移動するために特別なことをする必要はありませんでした!

障害物コースをナビゲートしようとする友達が回転しているのを見ることができました。

これを行うために、まず現在のキットからのアセットを使用し、視覚効果のために新しいコンテンツを追加する必要がありました。私たちは、ツリーハウスを建設する祖母の物語を伝えるために、穴の開いたいくつかの不完全な壁とプラットフォームを作成しました。ユニークなプラットフォームを作成したくなかったため、4つの異なるベースピースと手すりピースを別々に作成しました。これにより、個々のベースと手すりのピースを組み合わせて、さまざまなバリエーションを持つことができました。

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

次に、ヒンジ制約自体の実際の動作を設定し、パートと制約の方向として機能するアタッチメントを追加する必要がありました。回転アタッチメントをMotor_Turnに配置し、歩道のピースを溶接し、ヒンジ制約の隣にMotor_Anchor自体のアンカー動作のための別のアタッチメントを配置しました。これは、プレイヤーによって影響を受けるのではなく(ドアのヒンジのように)、自分自身で回転する必要があるため、HingeConstraint.ActuatorTypeをMotorに設定し、制約を自己駆動モーターのように扱いました。

プラットフォームを一定の速度で回転させるために、HingeConstraint.AngularVelocity、HingeConstraint.MotorMaxAcceleration、およびHingeConstraint.MotorMaxTorqueプロパティを設定し、動きを可能にし、プレイヤーがジャンプしたときに中断されないようにしました。

Attachment0はヒンジのアンカーであり、Attachment1はヒンジ自体を表します。ヒンジは常に回転していましたが、ドア用のヒンジ制約も使用できます。

次に、回転する壁を作成する必要がありました。壁はその見かけの中心で回転する必要があり、レベルの残りの部分に対して任意の向きを処理できる必要があることがわかりました。プラットフォームと同様に、これらの壁も固定されておらず、Motor_Turnに溶接されるように構築しました。

パフォーマンスを節約するために、実際のツリーハウスメッシュの再利用を望んでいたため、プラットフォームと同様の経路をたどりました。さまざまな組み合わせでくっつけることができるいくつかの壁のタイプが作成されました。

TextureオブジェクトをSurfaceAppearanceオブジェクトの上に使用して、基本材料にいくつかのバリエーションを追加しました。テクスチャは、デカールと同様に、メッシュの平面に画像を配置することを可能にします。これは、レンガの壁に汚れを追加したり、同じ基本的な木材の材料を使用しながら木材を古びたように見せたりするのに便利です。Textureオブジェクトは、Decalオブジェクトとはわずかに異なる動作を持ち、画像を好きなようにタイルやオフセットできるため、オーバーレイテクスチャをスケーリングできる場合に非常に便利です。

ヒンジ制約の類似の動作と設定、またTextureオブジェクトの使用方法がわかります。

いくつかのプラットフォームと回転壁をテストした後、いくつかのバリエーションを作成し、それらの配置を調整して、障害物コースが挑戦的で、心を曲げるものであり、プレイヤーがどこに行くべきかが明確であることを確認しました!それらがうまく動作するように、値と位置の両方を調整する必要がありました。プラットフォームと壁が互いにぶつかるか、周囲にぶつかるポイントがいくつかありましたが、少し動かして頻繁にテストすることで、デモで使用している設定にたどり着くことができました!

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

3Dビューポートのクローズアップビューで、右上隅に視覚化オプションボタンが示されています。
衝突の視覚化が無効になっていると、ゲーム内で表示される通常の幾何学的表現が見えます。
衝突の視覚化が有効になっていると、木の葉が衝突しないため、回転するプラットフォームや壁に干渉しないことがわかります。

下のドア/窓の穴が見えますが、サブパネルのような小さな詳細は見えません。これは、壁のCollisionFidelityプロパティがボックスに設定されているためです。これらのパネルには精度が必要なかったため、パフォーマンスコストを節約するために、これで十分な詳細がありました。プラットフォームと回転壁が完成したので、箱やランプのような詳細なアセットを追加するだけで、プレイできる準備が整いました!

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