【Cluster ベータ機能】目の前にアイテムを出現させるUIボタン
はじめに
Clusterワールドで、アイテムを獲得したら自由に出現させて使えるようなギミックを実装したい!
概要
この記事では、クリックしたらプレイヤーの目の前にアイテムが出現するUIボタンを実装します。
また、ワールド負荷軽減として、アイテムの一定時間での削除も実装します。
この記事の実装内容はこちらのワールドで体験できます。
また、以前開発に参加させていただいたこちらのワールドでも同様の機能を実装しています。
目次
- Player Interactive Local UIでボタンを表示する
- アイテムのPrefabを作成する
- ボタンを押した時にアイテムを出せるようにする
3.1. プレイヤーにplayerScriptを付与できるようにする
3.2. PrefabをWorldItemTemplateListに登録する
3.3. UIボタンをPlayerLocalObjectReferenceListに登録する
3.4. ボタンを押した時にアイテムを生成する処理を実装する
3.5. アイテムの位置を設定する
3.6. アイテムの向きを設定する - 放置されたアイテムが一定時間で消えるようにする
環境
Unity: v2021.3.4f1
Cluster Creator Kit: v2.37.0
PC: M2 macOS Sequoia v15.0
前提となる開発環境については、公式の記事を参照するのが良いと思います。
本記事執筆時点では、Player Interactable Local UIはベータ機能なので有効にしておきます。
UnityでのCluster Scriptの動作確認には、かおもラボさんのCSEmulator V2を使用しています。
本題
1. Player Interactive Local UIでボタンを表示する
UIボタンを表示させていきます。
Hierarchy上で右クリック、メニューを表示し、UI>PlayerLocalUI - cluster
を選択します。
PlayerLocalUIというGameObjectが生成されます。
このままではボタンを押したりできないので、
PlayerLocalUIコンポーネントのSorting Order TypeをInteractableに設定します。
また、これにはGraphic Raycasterが必要となるので併せて追加します。
SafeAreaの中にボタンを追加していきます。
今回はボタンを2つ追加してみます。
TextMeshProはおそらくClusterが対応していないので、UI>Legacy>Button
を選択します。
MyItemButtonsというGameObjectに、MyItemButtonAとMyItemButtonBというボタンを追加しました。
適当に形を整えます。
ボタンを左上隅に配置したかったので、MyItemButtonsを上下左右stretchに、
MyItemButtonA, MyItemButtonBのAnchorを左上に設定して調整しました。
こうすると、画面のサイズが異なっても左上からの距離でいい感じに配置されます。
最後に、PlayerLocalUIコンポーネントの「選択できるUIをセットアップ」をクリックします。
これによって、ボタンのGameObjectに適切なサイズのBoxColliderコンポーネントが追加されます。
適切なサイズでないとVRで選択できなかったりするので、「選択できるUIをセットアップ」でいい感じにしてもらいましょう。
これでとりあえず、ボタンが表示できました。
2. アイテムのPrefabを作成する
つぎに、ボタンを押した時に生成されるアイテムのPrefabを準備します。
アイテムの向きがわかりやすいように、真ん中に星印をつけたCapsuleのPrefabを2色用意することにしました。
星印の画像
各アイテムのPrefabについて、以下の設定を行います。
- GameObjectの中にCapsuleを入れる
- PrefabとしてのRotationを、xyz軸全て0にしておくため
- あとでアイテムの向きを設定する時に、rotationを0に揃えておくと便利
- Capsuleに星印のテクスチャを貼ったMaterialを適用
- CapsuleColliderのIsTriggerにチェック
- アイテムは衝突する必要がないため
- CapsuleのRotationYを-90度回転
- スポーンしたプレイヤーから星がまっすぐ見える向き
- CapsuleのScaleを0.3に設定
- 1.0だと大きすぎるので
- 外側のGameObjectにGrabbableItemコンポーネントをアタッチする
- Item, Rigidbody, MovableItemコンポーネントが自動でアタッチされる
- RigidbodyコンポーネントのUse Gravityをオフに、Is Kinematicをオンにする
もう一つのPrefabも同様に設定します。
これでアイテムのPrefabが作成できました。
3. ボタンを押した時にアイテムを出せるようにする
先ほど作成したPrefabを利用して、ボタンを押した時にアイテムを出せるようにします。
3.1. プレイヤーにPlayerScriptを付与できるようにする
新しくGameObjectを作成します。
名前は適当で良いですが、PlayerScriptを付与するGameObjectは色々便利なので、私はGlobalSettingsという名前をつけました。
GlobalSettingsにPlayerScriptコンポーネントとScriptableItemコンポーネントを追加します。
PlayerScriptとそれを付与するためのclusterスクリプトを作成します。
// 空のスクリプト
// インタラクトされたときPlayerScriptを付与する
$.onInteract((player) => {
$.setPlayerScript(player);
});
作成したスクリプトを先ほど追加したコンポーネントにそれぞれセットします。
3.2. PrefabをWorldItemTemplateListに登録する
次に、GlobalSettingsにWorldItemTemplateListコンポーネントをアタッチします。
これはclusterスクリプトでの $.createItem
にてPrefabにアクセスするために必要なものです。
このコンポーネントに前節で作成したPrefabを登録します。
IdはMyItemA, MyItemBを、それぞれ"my_item_a", "my_item_b"としておきます。
clusterスクリプトでは、このIdを使ってアクセスすることになります。
3.3. UIボタンをPlayerLocalObjectReferenceListに登録する
GlobalSettingsにPlayerLocalObjectReferenceListコンポーネントをアタッチします。
これはPlayerScriptでUIボタンにアクセスするために必要なものです。
このコンポーネントに第1節で作成したボタンを登録します。
ただ、ひとつずつ登録するのは面倒なので、MyItemButtonsごと登録してしまいましょう。
3.4. ボタンを押した時にアイテムを生成する処理を実装する
clusterスクリプトに「ボタンを押したとき、アイテムを生成する」処理を実装します。
// global.jsに対して、アイテムの生成をリクエストするメッセージを送信する
// _.sourceItemIdは、PlayerScriptを付与したオブジェクトのIDを指す
// ここでは、global.jsとなる
//
// a,bどちらのアイテムかは、badgeLabelによって判別する
const sendMessageToCreateMyItem = (badgeLabel) => {
_.sendTo(_.sourceItemId, "createMyItem", badgeLabel);
};
// PlayerLocalObjectReferenceListに登録したIdを指定して、
// MyItemButtonsにアクセスする
const myItemButtons = _.playerLocalObject("my_item_buttons");
// MyItemButtonAが押された時のイベント
myItemButtons
.findObject("MyItemButtonA")
.getUnityComponent("Button")
.onClick((isDown) => {
// badgeLabelには"a"を渡す
if (isDown) sendMessageToCreateMyItem("a");
});
// MyItemButtonBが押された時のイベント
myItemButtons
.findObject("MyItemButtonB")
.getUnityComponent("Button")
.onClick((isDown) => {
// badgeLabelには"b"を渡す
if (isDown) sendMessageToCreateMyItem("b");
});
(実装済み部分 前略)
// アイテムを生成する
const createMyItem = (badgeLabel, sender) => {
if (!(sender instanceof PlayerHandle)) return;
// WorldItemTemplateListに登録したIdを指定して、アイテムのPrefabを取得
// badgeLabelに応じて、生成するアイテムのIdを決定
//
// とりあえず、プレイヤーのpositionとrotationでアイテムを生成する
const newItem = $.createItem(
new WorldItemTemplateId(`my_item_${badgeLabel}`),
sender.getPosition(),
sender.getRotation()
);
};
// PlayerScriptからのメッセージを受信する
$.onReceive(
(messageType, arg, sender) => {
switch (messageType) {
// アイテム生成のリクエストに対する処理
case "createMyItem":
createMyItem(arg, sender);
break;
}
},
{ item: false, player: true }
);
3.5. アイテムの位置を設定する
アイテムの位置をプレイヤーの目の前に設定します。
(前略)
// アイテムを生成する
const createMyItem = (badgeLabel, sender) => {
if (!(sender instanceof PlayerHandle)) return;
// アイテムのpositionを用意する
const distance = 2;
const posY = 1;
const playerForward = getForwardPosition(sender, distance, posY);
const newItemPosition = sender.getPosition().add(playerForward);
// WorldItemTemplateListに登録したIdを指定して、アイテムのPrefabを取得
// badgeLabelに応じて、生成するアイテムのIdを決定
//
// rotationは、Prefabを後ろ向きにしていたのでそのまま使用
const newItem = $.createItem(
new WorldItemTemplateId(`my_item_${badgeLabel}`),
newItemPosition,
sender.getRotation()
);
};
(後略)
3.6. アイテムの向きを設定する
アイテムの向きについては、Prefabを後ろ向きにしていたのでそのままでオッケーです。
これで、「目の前にアイテムを出現させるUIボタン」実装完了です!
4. 放置されたアイテムが一定時間で消えるようにする
前項まででアイテムを出現させることができるようになりましたが、
このままでは、アイテムが残り続けてしまい、だんだんとワールドに負荷がかかっていってしまいます。
その対策として今回は、アイテムがプレイヤーの手から離れているときに、一定時間で消滅させる処理を実装します。
MyItemAとMyItemBのPrefabにそれぞれScriptableItemコンポーネントを追加します。
アイテムの処理を実装するためのclusterスクリプトを作成します。
const interval = 5; // タイマーの更新間隔(秒)
// アイテムが掴まれた状態として初期化
$.onStart(() => {
$.state.intervalSec = 0;
$.state.isReleased = false;
});
// アイテムが手放されている状態での経過時間の計測開始
const startReleasedInterval = () => {
$.state.intervalSec = 0;
$.state.isReleased = true;
};
$.onGrab((isGrab, isLeftHand, player) => {
if (isGrab) {
$.state.isReleased = false;
} else {
startReleasedInterval();
}
});
const onIntervalPassed = () => {
if ($.state.isReleased) {
// アイテムが手放された状態でintervalが経過した場合、アイテムを削除
$.destroy();
} else if (!$.getGrabbingPlayer()) {
// isReleasedがfalseだが、実際にはアイテムが手放された状態の場合、
// アイテムが手放された時の処理を行う
startReleasedInterval();
}
};
$.onUpdate((deltaTime) => {
// intervalSecに経過時間を加算
$.state.intervalSec += deltaTime;
// intervalSecが設定したintervalを超えたかどうかをチェック
if ($.state.intervalSec >= interval) {
onIntervalPassed();
// intervalSecをリセット
$.state.intervalSec = 0;
}
});
作成したスクリプトを先ほど追加したコンポーネントにそれぞれセットします。
これでワールドへの負荷対策も実装できました!
おわりに
今回は「目の前にアイテムを出現させるUIボタン」を実装しましたが、いくつか改善できる点があります。
-
1つは、アイテムを個数制限で消滅させることです。
現状では、アイテムを無限に生成することができます。
これはこれで楽しいですが、増やしすぎるとワールドに大きな負荷がかかってしまいます。
新しくアイテムを生成した時に個数制限で一番古いアイテムを消滅させるなどの方法は良さそうです。 -
1つは、アイテムの一定時間での消滅機能の改善です。
現状では、アイテムごとに$.onUpdate()
によって毎フレームの処理を行っています。
ただ、これはアイテムが増えるほどワールドに負荷がかかってしまいます。
インターバルタイマーをワールドで1つだけにして、アイテムに通知する方が効率が良さそうです。
この記事中ならGlobalSettingsに実装するべきでしょうか。 -
1つは、アイテムが見えるプレイヤーを制限することです。
現状では、アイテムは他プレイヤーにも見え、掴むことができます。
しかし例えば、「自分の武器を取り出して闘える」ようなゲームでは、他プレイヤーが自分の武器を掴めてしまっては不都合な場合があります。
その場合は、他プレイヤーにはアイテムが見えないようにすべきです。
また、アイテムを掴んでいる時だけは他プレイヤーにも見えるようにすると、さらに良いかもしれません。
これらについては、また改めて記事を書こうと思います。
Discussion