🙌

【Cluster ベータ機能】目の前にアイテムを出現させるUIボタン

に公開

はじめに

Clusterワールドで、アイテムを獲得したら自由に出現させて使えるようなギミックを実装したい!

概要

この記事では、クリックしたらプレイヤーの目の前にアイテムが出現するUIボタンを実装します。
また、ワールド負荷軽減として、アイテムの一定時間での削除も実装します。

この記事の実装内容はこちらのワールドで体験できます。

https://cluster.mu/w/cdaf1e52-887f-49a3-bf6e-7d686c37a3ae

https://youtu.be/wu6Im-uKS0o

また、以前開発に参加させていただいたこちらのワールドでも同様の機能を実装しています。

https://cluster.mu/w/769cdfc0-4031-48c6-ad0b-d0339898996d

https://youtu.be/3HjwNZ4EB34

目次

  1. Player Interactive Local UIでボタンを表示する
  2. アイテムのPrefabを作成する
  3. ボタンを押した時にアイテムを出せるようにする
    3.1. プレイヤーにplayerScriptを付与できるようにする
    3.2. PrefabをWorldItemTemplateListに登録する
    3.3. UIボタンをPlayerLocalObjectReferenceListに登録する
    3.4. ボタンを押した時にアイテムを生成する処理を実装する
    3.5. アイテムの位置を設定する
    3.6. アイテムの向きを設定する
  4. 放置されたアイテムが一定時間で消えるようにする

環境

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を使用しています。
https://vkao.booth.pm/items/5111235

本題

1. Player Interactive Local UIでボタンを表示する

UIボタンを表示させていきます。

Hierarchy上で右クリック、メニューを表示し、UI>PlayerLocalUI - clusterを選択します。
PlayerLocalUIというGameObjectが生成されます。

add PlayerLocalUI GameObject

このままではボタンを押したりできないので、
PlayerLocalUIコンポーネントのSorting Order TypeをInteractableに設定します。
また、これにはGraphic Raycasterが必要となるので併せて追加します。

set up PlayerLocalUI

SafeAreaの中にボタンを追加していきます。
今回はボタンを2つ追加してみます。

TextMeshProはおそらくClusterが対応していないので、UI>Legacy>Buttonを選択します。
MyItemButtonsというGameObjectに、MyItemButtonAとMyItemButtonBというボタンを追加しました。

add UI buttons

適当に形を整えます。
ボタンを左上隅に配置したかったので、MyItemButtonsを上下左右stretchに、
MyItemButtonA, MyItemButtonBのAnchorを左上に設定して調整しました。

こうすると、画面のサイズが異なっても左上からの距離でいい感じに配置されます。

adjust UI button container
adjust UI buttons

最後に、PlayerLocalUIコンポーネントの「選択できるUIをセットアップ」をクリックします。
これによって、ボタンのGameObjectに適切なサイズのBoxColliderコンポーネントが追加されます。

適切なサイズでないとVRで選択できなかったりするので、「選択できるUIをセットアップ」でいい感じにしてもらいましょう。

set up interactable UIs

BoxCollider is added

これでとりあえず、ボタンが表示できました。

2. アイテムのPrefabを作成する

つぎに、ボタンを押した時に生成されるアイテムのPrefabを準備します。

アイテムの向きがわかりやすいように、真ん中に星印をつけたCapsuleのPrefabを2色用意することにしました。

星印の画像
star texture blue
star texture yellow

各アイテムのPrefabについて、以下の設定を行います。

  1. GameObjectの中にCapsuleを入れる
    • PrefabとしてのRotationを、xyz軸全て0にしておくため
    • あとでアイテムの向きを設定する時に、rotationを0に揃えておくと便利
  2. Capsuleに星印のテクスチャを貼ったMaterialを適用
  3. CapsuleColliderのIsTriggerにチェック
    • アイテムは衝突する必要がないため
  4. CapsuleのRotationYを-90度回転
    • スポーンしたプレイヤーから星がまっすぐ見える向き
  5. CapsuleのScaleを0.3に設定
    • 1.0だと大きすぎるので
  6. 外側のGameObjectにGrabbableItemコンポーネントをアタッチする
    • Item, Rigidbody, MovableItemコンポーネントが自動でアタッチされる
  7. RigidbodyコンポーネントのUse Gravityをオフに、Is Kinematicをオンにする

prefab blue

prefab blue outer

もう一つのPrefabも同様に設定します。

prefab yellow

これでアイテムのPrefabが作成できました。

3. ボタンを押した時にアイテムを出せるようにする

先ほど作成したPrefabを利用して、ボタンを押した時にアイテムを出せるようにします。

3.1. プレイヤーにPlayerScriptを付与できるようにする

新しくGameObjectを作成します。
名前は適当で良いですが、PlayerScriptを付与するGameObjectは色々便利なので、私はGlobalSettingsという名前をつけました。

GlobalSettingsにPlayerScriptコンポーネントとScriptableItemコンポーネントを追加します。

global settings

PlayerScriptとそれを付与するためのclusterスクリプトを作成します。

playerScript.js
// 空のスクリプト
global.js
// インタラクトされたときPlayerScriptを付与する
$.onInteract((player) => {
  $.setPlayerScript(player);
});

作成したスクリプトを先ほど追加したコンポーネントにそれぞれセットします。

set scripts

3.2. PrefabをWorldItemTemplateListに登録する

次に、GlobalSettingsにWorldItemTemplateListコンポーネントをアタッチします。
これはclusterスクリプトでの $.createItem にてPrefabにアクセスするために必要なものです。

このコンポーネントに前節で作成したPrefabを登録します。
IdはMyItemA, MyItemBを、それぞれ"my_item_a", "my_item_b"としておきます。
clusterスクリプトでは、このIdを使ってアクセスすることになります。

setup WorldItemTemplate List component

3.3. UIボタンをPlayerLocalObjectReferenceListに登録する

GlobalSettingsにPlayerLocalObjectReferenceListコンポーネントをアタッチします。
これはPlayerScriptでUIボタンにアクセスするために必要なものです。

このコンポーネントに第1節で作成したボタンを登録します。
ただ、ひとつずつ登録するのは面倒なので、MyItemButtonsごと登録してしまいましょう。

setup PlayerLocalObjectReferenceList

3.4. ボタンを押した時にアイテムを生成する処理を実装する

clusterスクリプトに「ボタンを押したとき、アイテムを生成する」処理を実装します。

playerScript.js
// 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");
  });

global.js
(実装済み部分 前略)

// アイテムを生成する
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. アイテムの位置を設定する

アイテムの位置をプレイヤーの目の前に設定します。

global.js
(前略)

// アイテムを生成する
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ボタン」実装完了です!

create item on button click

4. 放置されたアイテムが一定時間で消えるようにする

前項まででアイテムを出現させることができるようになりましたが、
このままでは、アイテムが残り続けてしまい、だんだんとワールドに負荷がかかっていってしまいます。

その対策として今回は、アイテムがプレイヤーの手から離れているときに、一定時間で消滅させる処理を実装します。

MyItemAとMyItemBのPrefabにそれぞれScriptableItemコンポーネントを追加します。

attach ScriptableItem to MyItemA
attach ScriptableItem to MyItemB

アイテムの処理を実装するためのclusterスクリプトを作成します。

myItem.js

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;
  }
});

作成したスクリプトを先ほど追加したコンポーネントにそれぞれセットします。

set myItem.js to the ScriptableItem of MyItemA
set myItem.js to the ScriptableItem of MyItemB

これでワールドへの負荷対策も実装できました!

おわりに

今回は「目の前にアイテムを出現させるUIボタン」を実装しましたが、いくつか改善できる点があります。

  • 1つは、アイテムを個数制限で消滅させることです。
    現状では、アイテムを無限に生成することができます。
    これはこれで楽しいですが、増やしすぎるとワールドに大きな負荷がかかってしまいます。
    新しくアイテムを生成した時に個数制限で一番古いアイテムを消滅させるなどの方法は良さそうです。

  • 1つは、アイテムの一定時間での消滅機能の改善です。
    現状では、アイテムごとに $.onUpdate() によって毎フレームの処理を行っています。
    ただ、これはアイテムが増えるほどワールドに負荷がかかってしまいます。
    インターバルタイマーをワールドで1つだけにして、アイテムに通知する方が効率が良さそうです。
    この記事中ならGlobalSettingsに実装するべきでしょうか。

  • 1つは、アイテムが見えるプレイヤーを制限することです。
    現状では、アイテムは他プレイヤーにも見え、掴むことができます。
    しかし例えば、「自分の武器を取り出して闘える」ようなゲームでは、他プレイヤーが自分の武器を掴めてしまっては不都合な場合があります。
    その場合は、他プレイヤーにはアイテムが見えないようにすべきです。
    また、アイテムを掴んでいる時だけは他プレイヤーにも見えるようにすると、さらに良いかもしれません。

これらについては、また改めて記事を書こうと思います。

Discussion