次のシステムは、デュヴァルドライブの謎 で私たちが望んでいたゲームプレイを確立する基礎でした。
ゲーム状態マネージャー
ゲームステートマネージャー/ゲームステートクライアント は、経験で最も複雑なシステムであり、次のような処理を行っています:
- ロビー内でプレイヤーを開始し、グループをメインのゲームプレイエリアにテレポートするカウントダウンを開始し、プレイヤーを予約サーバーにテレポートします。
- 破損したルームをクローンし、同期ストリーミングし、プレイヤーを特定の割り当てられた CFrame 座標にテレポートします。
- シールメカニズムの掴みと配置。
- ドアのロックとアンロック。
- 最終的なテレポートをフォイアに初期化し、最後のカットシーンを再生する。
単純な状態マシン (更新機能) として実装し、状態は DemoConfig (ゲームステート枚列) にあります。いくつかの状態は、最初のロビー/予約されたサーバーテレポートを処理し、他の状態は、ミッションを見つけ、パズルをトリガーし、ミッションを解決します。シール以外に注意して、 GameStateManager にミッション固有のコードを持たせないようにしました。
ゲーム状態はほとんどがサーバー側ですが、クライアントが何かを行う必要があるとき、カウントダウン、ロア、またはストリーミングの一時停止 UI を無効にするなど、サーバーとクライアント (GameStatesClient) は、リモートイベント GameStateEvent を介して通信します。ほとんどの場合と同様、イベントペイロードには「タイプ」イベント (Config.GameEvents) が最初のパラメータであり、その後にイベント固有のデータがあります。
テレポートゲーム状態
3つのゲーム状態のグループが、テレポートを破損した部屋に隠す 3つのユニークなカットシーンを実行します:ウォームアップ、インフライト、クールダウン。 ウォームアップ は、全期間実行し、3Dワールドが見えなくなるほぼ黒い画面で終了します。この期間中、我々は部屋をクローンし、各プレイヤーに破損した部屋で望ましいプレイヤーの位置を取得し、Player.RequestStreamAroundAsync を呼び出し、プレイヤーを破損した部屋内の特定の割り当てられた CFrame 座標に移動します。この種類のテレポートは、ストリーミングの一時停止をトリガーする可能性があります。ストリーミング停止が発生すると、クライアントは警告メッセージを表示します。このデフォルトの UI を無効にして、エクスペリエンスを没没入感のあるさせました。
ストリーミングが処理されている間、 インフライト が実行され、わずかにパルスする暗い画面を保持します。両方の Player.RequestStreamAroundAsync が返済し、クライアントがストリーミングの一時停止がオフであり、各プレイヤーが望ましい場所に十分近いことをサーバーに通知すると、InFlight カットシーンをキャンセルし、Cooldown カットシーンを開始します。 クールダウン の目標は、暗い画面をスムーズに削除して、3D 世界を再び見えるようにすることです。If Player.RequestStreamAroundAsync が返すのに時間がかかりすぎるか、クライアントがストリーミングの一時停止がオフであると報告しない場合、いくつかの秒後にクールダウンカットシーンに進みます。
同様のウォームアップ、インフライト、クールダウンのシーンが発生するのは、プレイヤーを通常の部屋の状態にテレポートするとき、 TeleportWarmupBack 、 TeleportInFlightBack 、 TeleportCooldownBack 、そして、エクスペリエンスの終わりに、 TeleportWarmupFinal 、 TeleportInFlightFinal 、 TeleportCooldownFinal を実行して、最後のカットシーンのためにプレイヤーをロビーにテレポートするときです。
照明と雰囲気ゲーム状態
各ルームの通常と汚れた状態が異なる視覚的外観を持つことを望んでいることを知っていましたが、プレイヤーに完全に異なる場所にいることを明確に視覚フィードバックできるようにすることができました。ゲーム状態は、普通の部屋と汚れた部屋の照明と雰囲気のプロパティを変更できるようにしてくれました、ゲーム状態マネージャーが、プレイヤーが通常から汚れた状態の部屋へテレポートするかどうかに基づいて、使用するインスタンスを選択しました(TeleportWarmup)、またはその逆です(TeleportWarmupBack)。テレポート中に再生されるイベントは、全画面を暗黒または白にしますので、その瞬間に Lighting および Atmosphere インスタンスを変更してプレイヤーからプロセスを隠すことにしました。変更を簡単にするために、 デモ構成 には、これらのサービスの下のインスタンスが変更する必要があるものを定義するマップが含まれています。
ドアゲーム状態をロック中
プレイヤーを特定の部屋に留めておきたいと考え、ミッションを終えたときにドアをロックするゲーム状態を作成しました:InMission と CanGetSeal 。InMission はプレイヤーをアクティブなミッションルームにロックし、CanGetSeal はミッションルームのドアを "復元" されるまでロックします。主に、プレイヤーがミッションから戻ったときにドアがロックされるように使用し、シールを拾う動機を与えました。シールを拾った後、ドアがアンロックされるため、フォイヤー内のシールの場所に配置できます。最後のミッションは、部屋の鍵付きドアがプレイヤーが他のすべてのパズルを解くまでロックされているという典型的なプロセスに唯一のものです(EnableRegularMissionDoors、EnableOneMissionDoors機能)。
イベントマネージャ
イベントマネージャ は、キーフレームを使用して、「アクション」を時間をかけて実行できるようにしてくれました、例えば:
- インスタンスプロパティと属性をインターポレートします。
- スクリプトの実行。
- オーディオ再生。
- カメラシェイクを実行中。
理想的には、トラックベースの UI を持つツールを使用しますが、このデモでは、キーとプロパティ名を手動で入力しました。 イベントマネージャー システムは、以下のスクリプトとイベント/機能を含む複数の構成で構成されています:
- EventManager - イベントの作成と停止、およびサーバー側のアクションの全体的なロジック
- EventManagerClient - クライアント側のアクション。
- EventManagerModule - サーバーとクライアント側のアクションの共通コード。
- EventManagerConfig - コマンド宣言の少ない小さなファイル。
- EventManagerDemo - このデモのすべての実際のイベントがゲーム固有のスクリプトで定義されている場所
- EventManagerEvent , EventManagerFunc - クライアントまたはサーバーからイベントを実行し停止するリモートイベントとバインド可能な機能他のシステムがイベントを設定、実行、停止する方法です。
それぞれのイベントには名前、クールダウンに関するオプション情報、開始または終了で実行する機能、インターポレーター(時間経過で任意のプロパティまたは属性をインターポレートする)、セクション(時間経過で任意の数のプロパティまたは属性をインターポレートする)、およびオーディオ再生があります。
インターポレーション
インターポレーションでは、キーフレーム間でバラバラにジャンプするのではなく、オブジェクトのプロパティと属性を 1つの値から別の値にスムーズに変更できます。例えば、次のコードスニペットは、 プロパティを開始時の値から に変換し、その後、 パラメータによって定義されたオブジェクトに戻り、後で に再び変換した方法を示しています:
interpolants = {objectParam = "TextLabel",property = "TextTransparency",keys = {{value = 1},{time = .5, value = 0},{time = 2.25, value = 0},{time = 3, value = 1}}}
以下のコードサンプルのように、どのオブジェクトプロパティまたは属性がどのようなものに属するかを定義できる一方で、クライアントでストリーミングを使用したり、ランタイムに作成されたオブジェクトで作業したりすることを可能にするために、同じイベントを異なる「オブジェクトグループ」で再使用できるようにしたいと考えました。
object = workspace.SomeFolder.SomeModel
これを実現するために、オブジェクト名を参照し、イベント開始時に名前のパラメータを渡すことを許可しました。名前付きオブジェクトを見つけるには、イベントの "ルート" を指定することを許可し、イベントが開始されると、このルートの下で名前でオブジェクトを見つけることができました。たとえば、次のコードスニペットでは、 イベントマネージャー は、Workspace.Content.Interior.Foyer["Ritual-DemoVersion"] のどこかに名前が "Wander" のオブジェクトを見つけようとします。
params = {["RootObject"] = workspace.Content.Interior.Foyer["Ritual-DemoVersion"],},interpolants = {objectName = "Wander",attribute = "TimeScale",keys = {{value = 0.2}}}
パラメータセクションでイベントにパラメータを渡すことを許可し、イベント開始時に実行されているスクリプトは、既存のパラメータを変更するか、「パラメータ」テーブルに新しいパラメータを追加することができました。次の例では、デフォルト値が false の isEnabled パラメータがあり、名前が FocuserGlow のオブジェクトの「有効」プロパティが isEnabled に設定されます。イベント開始時に実行されるスクリプトまたはイベントを呼び出すスクリプトは、isEnabled を設定できるため、FocuserGlow の有効化と無効化の両方に同じイベントの説明を使用できます。
params = {isEnabled = false},interpolants = {{objectName = "FocuserGlow",property = "Enabled",keys = {{valueParam = "isEnabled"}}}
パラメータにより、エクスペリエンスの開始時に存在しないオブジェクトにも参照できました。たとえば、次のコードサンプルでは、イベント開始時に実行されている関数がオブジェクトを作成し、パラメータに BlackScreenObject 入力を設定して、作成されたオブジェクトを指します。
{objectParam = "BlackScreenObject",property = "BackgroundTransparency",keys = {{value = 0},{time = 19, value = 0},{value = 1},}}
イベント、イベントインスタンスを実行し、トリガーに接続
イベントを実行するには、クライアントからのリモートイベントまたはサーバーからの機能を使用します。次の例では、RootObject および isEnabled イベントにパラメータを数個伝えました。内部では、イベントの説明のインスタンスが作成され、パラメータは実際のオブジェクトに解決され、機能はイベントインスタンスのIDを返しました。
local params = {RootObject = workspace.Content.Interior.Foyer["Ritual-DemoVersion"]["SealDropoff_" .. missionName],isEnabled = enabled}local eventId = eventManagerFunc:Invoke("Run", {eventName = "Ritual_Init_Dropoff", eventParams = params} )
「Stop」で関数を呼び出してイベントを実行を停止できます:
eventManagerFunc:Invoke("Stop", {eventInstId = cooldownId} )
「コスメティック」なインターポレントやその他のアクション(すべてのプレイヤーのシミュレーションを変更しない)は、クライアントで実行でき、それによりスムーズなインターポレーションが得られます。イベントの説詳細では、onServer = true ですべてのアクションにデフォルト値を提供できます(それなしでは、デフォルトはクライアントです)。各アクションは、独自の onServer を設定して上書きできます。
イベントの実行をトリガーに簡単に接続するには、ヘルパー関数 ConnectTriggerToEvent または ConnectSpawnedTriggerToEvent を使用し、後者は名前でトリガーを見つけます。異なるトリガーを使用して同じイベントをトリガーできるようにするには、「設定」キーとトリガーボリュームのセットを使用して eventManagerFunc を呼び出すことができます。アクション中のトリガーボリュームの例は、拡張パントリを作成す操作 を参照してください。
イベントパラメータ
スクリプトからパスされるカスタムイベントパラメータに加えて、イベントを作成するときにオプションで送信できる他のデータには、プレイヤー、コールバック(イベントが終了すると呼び出される)、およびコールバックパラメータが含まれます。いくつかのイベントは、1人のプレイヤーのみに実行する必要があります(クライアント上で実行されるアクションのあるイベント)、他のはすべてに実行する必要があります。1人のプレイヤーだけで実行するために、パラメータに onlyTriggeredPlayer = true を使用しました。
イベントは、 minCooldownTime および maxCooldownTime によって定義されたクールダウンを持つことができます。最小と最大は、プレイヤー数に基づいてスケーリングする範囲を提供しますが、このデモでは使用しませんでした。プレイヤーごとにクールダウンが必要になった場合、perPlayerCooldown = true を使用する能力がありました。各イベントには、秒単位の期間があり、クールダウンタイミングとコールバックはそれに基づいています。イベントを終了することを通知するには、コードを呼び出して、取得するコールバックとパラメータをパスできます。
スクリプトを呼び出す
Scripts を スクリプト セクションの特定のキーフレームに呼び出すことができます。例:
scripts = {{startTime = 2, scriptName = "EnablePlayerControls", params = {true}, onServer = false }}
前の例では、 プレイヤーコントロールを有効にする Script は、次のようにイベントマネージャモジュルで登録する必要があります:
emModule.RegisterFunction("EnablePlayerControls", EnablePlayerControls)
RegisterFunction は、クライアントスクリプトでクライアントに呼び出された関数、およびサーバースクリプトで onServer = true 呼ばれた関数のためにクライアントスクリプトで呼び出されなければなりません。機能自体はイベントインスタンスとパラメータを受け取りますが、この場合は、真の値でパラメータを 1つだけ伝えます。
local function EnablePlayerControls(eventInst, params)
音オーディオを再生
例えば、非位置指定オーディオ をキーフレームで再生するためのサポートが限られています: サウンド セクション
sounds = {{startTime = 2, name = "VisTech_ethereal_voices-001"},}
イベント終了コールバックは、イベント期間が期限切れすると発動しますが、オーディオアクションは後でまだ再生している可能性があります。
カメラシェイクを実行
カメラシェイク セクションで、次のようにカメラシェイクを定義できます:
cameraShakes = {{startTime = 15, shake = "small", sustainDuration = 7, targets = emConfig.ShakeTargets.allPlayers, onServer = true},}
「ターゲット」は、イベントをトリガーしたプレイヤー、allPlayer、またはプレイヤーInRadius にのみ、トリガープレイヤーに向けて開始できます。カメラのシェイクには3rdパーティのスクリプトを使用し、シェイクは事前に定義されました:eventManagerDemo.bigShake および eventManagerDemo.smallShake 。sustainDuration もパスできます。
ミッションロジック
合計 7 のミッションがあり、そのうち 6 つだけがシールを使用します。ほとんどのミッションには共通のパラメータがありますが、いくつかはシールとテレポートして汚れた部屋にのみ使用されます。それぞれのミッションには、 DemoConfig スクリプトに配置パラメータのセットが含まれる Config.Missions マップのエントリがあります:
- ミッションルート : オブジェクトのすべての非破損バージョンのフォルダ。
- ドア : プレイヤーがシールを拾うまでロックするドア。
- シール名 / 解決済みシール名 : 非破損シールと破損したシール名。
- SealPlaceName : シールを置く場所。
- PlacedSealPlaceholderName : シールを配置する場所のプレースホルダーオブジェクト
- TeleportPositionsName : 破損した部屋に移動するとき、正常の領域に戻るときにプレイヤーのテレポート位置と回転を定義するフォルダの名前、プレースホルダーメッシュ。両方の場合、同じ名前が使用されます。
- CorruptRoomName : 破損したルームの根フォルダの名前 (サーバーストレージに対する)。ミッションが開始すると、TempStorage.Cloned の下で壊れた部屋がクローンされ、ミッションが終了すると破壊されます。
- ミッション完了ボタン名 : 破損した部屋のチートボタンで、ミッションをすぐに終了させます。これは デバッグ目的 です。
- チートキー : 数字と同じチートまたは Ctrl Shift [Number] 。
ミッションロジックの一部は、シールとドアがほとんどのミッションのメインゲームフローを提供しているため、 GameStateManager スクリプトにありますが、ミッション固有のロジックのほとんどは、MissionsLogic および MissionsLogicClient スクリプトで定義される複数の「タイプ」のミッションにあります。タイプは、ミッションの説詳細に特定の名前のメンバーが存在するだけで定義されます。ミッションにはいくつかの種類があります:
- ロックのキーを使用する - ドアを開く最初のミッション。このタイプは LockName 、 KeyName によって定義されます。
- 一致するアイテム - 4 ミッションの一致アイテム。このタイプは MatchItems によって定義されます。
- レイヤードクロスを使用してマネキンをドレスアップする - 屋根裏の 1 ミッションには、プレイヤーが 3 つのアイテムを集めるタイプが定義されています。このタイプは DressItemsTagList によって定義されます。
- アイテムをクリックして終了する - 1 ミッションには、ClickTargetName によって定義されたこのタイプがあります。
各ミッションタイプには、独自の StartMissionFunc と CompleteMissionFunc があります。開始機能は通常、 MatchItem マップからパラメータを読み込み、名前をオブジェクトに解決し、任意のクリック検出器またはUI要素を設定します。ほぼすべてのロジックがサーバー上にありますが、MissionsLogicClient は、多くのミッションで使用されるアイテムカウンターを表示する UI を提供します。MissionLogicEvent リモートイベントは、パスされたコマンドの種類を定義する小さなミッションイベントを使用して、サーバー - クライアント通信に使用されます。MiscGameLogic スクリプトは、いくつかのトリガーをイベントにバインドし、リリースバージョンでデバッグオブジェクトを削除します。
マッチアイテムロジックでは、PuzzlePieceXX タグでマークされたアイテムを「使用」 (PuzzleSlotYY タグの付いたアイテムでクリックしながら) で PuzzlePieceXX タグの付いたアイテムを「使用」できます。 マッチアイテム マップでパラメータとして利用できるオプションがいくつかあります (ピースが順番に適用される必要がある場合、それぞれのうちの 1つだけが必要な場合)。単純なオーディオとビジュアルFX の名前を指定できました。ピースが特定の場所に配置する必要があるとき、追加の「配置」マップが、変換を定義するプレースホルダーパーツの名前からピースタグへのマッピングを提供します。
グラビング
オブジェクトをキャラクターの右腕に付属させることでオブジェクトを保持するシンプルな掴みシステムを開発しました。グラビングは GrabServer2 および GrabClient スクリプトで実装されています。クリック/タッチされたポイントを通過するレイを発射する ProcessClick で始まります。次に、掴めるメッシュをヒットしたかどうかをチェックし、ヒットは maxMovingDist で始まるインタラクションを掴むことができる場所にあります。モデルがクリックしたものが Attachments 呼び出された GrabHint を持っている場合、クリックした場所に最も近いものを選択します。捕捉されたパーツ、それに属するモデル、および構造の GrabHint またはクリック位置を覚えています。距離が最大グラブ距離よりも長い場合、まずプレイヤーは、試みられたグラブスポットに十分に近づく必要があるので、Humanoid.MoveTo を呼び出します。
各フレームで、グラブアクティビティが進行中かどうかをチェックします。プレイヤーが reachDist 内にいる場合、 ToolHoldAnim を再生し始めます。プレイヤーが maxGrabDist 内にいると、クライアントはモデルを実際にグラブするリクエストをサーバーに発行します (performGrab 機能)。
サーバー側のスクリプトには 2つの主な機能があります:
- グラブ - クライアントのモデルをグラブするリクエストを処理する
- リリース - 捕捉モデルをリリースするリクエストを処理する
各プレイヤーが保持しているものに関する情報は、 playerInfos マップに保持されます。グラブ機能では、このモデルがすでに別のプレイヤーによって捕獲されているかどうかをチェックします。そうなると - "EquipWorldFail" がクライアントに送信され、グラブ試行をキャンセルできます。注: プレイヤーが同じ Model の異なる部分を掴んでキャンセルする状況を処理する必要があり、この場合はキャンセルを行う必要がありました。
採択が許可されている場合、スクリプトは右手とオブジェクトを使用してクライアントからパスされたグラブスポットを使用して、2つの Attachments を作成します。それから、2つの RigidConstraint の間に Attachments を作成します。 Constraints と Attachments は、プレイヤーのキャラクターの 現在のグリップ フォルダに保存されます。グラビングはまた、サウンドを再生し、捕捉したオブジェクトの衝突を無効にし、必要に応じて復元可能なオブジェクトを処理します。
グラブモデルをリリースするには、クライアントスクリプトが HUD スクリーンGUI の GrabReleaseButton ボタンに接続します。A Connected 機能は、サーバーにイベントを発行します。サーバーでは、リリースが Attachments と Constraints を削除し、衝突を回復し、適用可能なすべてのレストアブルを処理し、このクライアントのグラブデータを playerInfos でクリアします。