💬

Clusterでイベントワールド作ってみた

に公開

はじめに

概要

  • Clusterというメタバースプラットフォーム向けに音楽イベントに使うワールドを制作します。

ワールドを作るきっかけ

経緯

  • 半年くらいワールドを制作していました。それらのBGMは商用利用可能な素材をお借りしており、自分でも1曲くらい作ってみてもいいかもなぁと考えていました。
  • なんとなくAmazonでセールをしていたDTMの入門書を購入し、MIDIキーボードも購入していました。
  • 曲を作らなくとも音を使ったワールドを制作をしてみたいと思っていました。音楽ゲーム自体詳しく知りませんが、楽譜に合わせてアクションを行うゲーム作りは大変だと思っています。
  • 2024年11月現在Clusterで、スマホデスクトップユーザーのできることは左手・右手を上げ下げすること、ジャンプやしゃがむがある程度です。一方VRユーザーはトラッキングに合わせて自由に操作することができます。(悩ましい)
  • 2024年11月末日、とある観客参加型のイベント誕生日に参加し、みんなでコミットするイベントは面白いと思いました。
  • いろいろプラットフォームの制約に悩みつつ、作りこまなくていいメカニクスで、観客参加型のイベントについて悩んでいた時、ふと音楽についての表現の幅はQueenが広げていたなぁと思い出していました。

イベントのテーマや狙い

  • 上記の経緯をもとにQueenの「we will rock you」をみんなで奏でるワールドを作ってみたいと思いました。

作りたいワールドのイメージ

  • 最終的に達成するかわかりませんが、ワールドについて言語化しておきたいと思います。
  1. イベント開催できること
    • ClusterCreatorKitのbeta機能をつかわない
  2. 足踏みと拍手のような、二つの操作を行うこと
    • ベースドラムとハット
      • ×楽器引きたいけど、スマホデスクトップユーザーには細かい位置調整できない
    • 足踏みと拍手
      • ×しゃがむやジャンプでは反応が遅いし、つかむとかすわるとか、難しそう
    • 前前後ろ
      • ×曲が進むと位置を移動することになるので面倒くさい。移動するならそれ自体おもしろい(つまりメカニクス)にしないといけない
    • 旗揚げゲーム
      • ○これなら手を上げ下げするだけだからできそう
  3. 歌えること
    • 主催の歌がワールドに聞こえること
      • サブ音声
    • 観客の歌も歌えること
      • イベントでマイク有効にできるできない、という制御大変そう
      • 一律マイク有効にできるなら、サブ音声有効なオブジェクトを切り替えたりできると楽かも
    • 歌詞が表示されること
  4. 主催と観客の区別がつくこと
    • ステージがあること
      • ステージの位置固定はいやだなぁ…でも動かすの面倒くさそう
    • スポットが当たること
      • 静的レンダリングできないとつらい、難しそう
    • ステージの映像がワールド内スクリーンに表示されること
      • 大変そう、やったことない
  5. ワールドのきれいさ
    • 僕はあまりきれいだったり幻想的だったりすることにこだわりはないかな
  6. 応援グッズ
    • ライブ限定応援グッズ(クラフトアイテムやアクセサリー)
      • こだわり始めると人生が足りないよなぁ

実現可能性の調査

ゴール設定

イベントワールドの完成形のビジョン

僕が毎週同じ時間同じ曲をイベント開催し続ける。それに影響して別のユーザーがそのワールドで同じようにイベントを行いたくなる、ワールド

どのような体験を提供したいか?

VRユーザーもスマホデスクトップユーザーも、音ゲーのようなかかわりができるワールドとイベント

ターゲット層(誰に向けたワールドか?)

VoiceChatもTextChatしないけど、ワールドやイベントに来た人。イベント会場で踊る・座る・ジャンプする以外のインターアクションを提供できること

ワールドの具体的な目標

100人は参加して楽しめるイベント向け会場であること

見た目(デザインやアート)

諦める。綺麗にはしない。
(マインクラフトというゲームは50cm程度のボクセルの世界であってこれを汚いと表現する人はいない。グラデーションのない画素数の低い世界をカクカクした世界観として認めるでしょう。おそらく、世界観と綺麗さはもっと深く考えないといけないようです。)

機能(インタラクションやゲーム要素)

KickDrum()

  • Boxに人やアイテムが触れたら、効果音が鳴るメカニクスの作成
  • 太鼓みたいな見た目
  • 100人分のシンバル

Hat()

  • Boxに人やアイテムが触れたら、違う効果音が鳴るメカニクス
  • シンバルみたいな見た目
  • 100人分のシンバル

マイク(サブ音声)

  • イベントステージ内に全体に響き渡るマイク
  • マイクはいくつかあってもいい
  • マイクはスイッチで遠隔きりかえできてもいい

環境構築

開発環境

  • Unity 2021.3.4f1
  • Windows10
  • Cluster Creator Kit 2.29.0
    • 特に決めていない、最新であればよい
  • Blender 4.1.1

設計と制作

ワールドの設計

クラフトアイテムは許可したほうが、観客の自由度が高まる

スケッチやレイアウトのプロトタイプ

入口から会場までは、通路が欲しい。ステージと客席にハットとドラムを配置する

試行錯誤の記録(ワールド制作v0.1.0)

テンプレートワールド(おまじない)

  • テンプレートワールドを開き[ウィンドウ]>[パッケージマネージャ]でCluster Creator Kitのバージョンを更新する
  • Despawn Heightコンポーネントを持つオブジェクトを用意する
  • Spawn Point(スクリプト)コンポーネントでSpawn TypeをEntranceにしたオブジェクトを用意する
  • (初回アカウントログインしたかもしれない)
  • [Cluster]>[ワールドアップロード]で新規作成ボタンをおして、ワールドアップロードする

KickDrum(箱)

  • とりあえず箱作成
    • [右クリック]>[3Dオブジェクト]>[キューブ]

    • Box Coliderコンポーネントでトリガーにするをonにする

    • On Clide Item Triggerコンポーネントをつける

      • EnterでkickSignalを発火する
    • Audio Souceコンポーネントをつける

      • ゲーム開始時に再生をoffにする
    • Play Audio Source Gimmick(スクリプト)をつける

      • アイテム(自分)のKeyを感知して音を鳴らしたいので、このあたりはパッションで設定する(Unity知らない)
    • 触れると鳴るKickDrum

試行錯誤の記録2(イベントで歌う)

イベント開催したことないのにイベントで何が必要になるかわからないので、とりあえずイベント開催しました。
と、そのまえに楽曲について調べました

楽曲が申請必要か調べる

  • 楽曲登録が必要なケース
    • アカペラ、または伴奏とともに歌う
    • 事前に制作・録音した音源を流す
    • 楽曲を演奏する
  • 使う楽曲が管理されているかを調べる
  • 「WE WILL ROCK YOU」で調べてみるとJASRACが一部の著作権を管理しているようでした。

使用楽曲の登録する

YouTubeなどの動画投稿(共有)サービスで配信されている動画を外部のサイトで利用する

Youtubeで公開されいる動画(音楽)をClusterワールドのYoutubeで再生してよいのだろうか

  • (YouTubeなどの動画投稿(共有)サービスでの音楽利用)[https://www.jasrac.or.jp/users/internet/ugc/index.html]
    • ざっくり、自分で作成していない動画を外部サイトで利用する場合は音源制作会社の権利を侵害するっぽい(厳しい世界)

イベント作成

イベントを作成するから作れるみたいです。

イベント設定はとりあえず面倒くさいので、わかる範囲で設定しておきます。まぁ、どうせ誰も来ません(来ませんでした)。

イベント開催

作成したイベントを開催しても、よくわかりませんでした。開催時刻になって、アプリからイベントページで自分のイベント会場に入場しました。(一般入場と何が違うんだ)
開かれたイベント会場ではクラフトモードが使えなかったので、楽器叩けませんでした。(ナイストライ)
また、アクセサリーをつけてみたのですが、当たり判定をもっていないようでした。。。(ナイストライ)
これはバッチを会場に用意しないといけないみたいです。

試行錯誤の記録3(クラフトアイテムのバチ作成と配布)

  • Blender Tutorials | Drum stickが何となくよいチュートリアル動画に見えました。
    • こういう感じでスティックの形をつくるっぽい
      • 赤色は輪切りをする(Ctrl+R)
  1. 新規作成されるCubeをスモールスケールする
    • Sキーでスケールできる
      • ドラムスティックの幅は14.5mmらしいので、2m幅から0.00725倍しておいた
      • ドラムスティックの長さは400mmらしい。
    • 単位を変える
      • シーンプロパティで長さmmに変える
      • オーバーレイで長さを表示する
      • 辺を選択するとmmで長さ表示される
    • 長さを変える
      • ビュー上でNキーを押すとトランスフォームが出る
    • 頑張って見た目スティックにする
    • Unity向けにFBXをエクスポート
      • クラフトアイテムのエクスポートみたいな資料で設定の説明があるはず
  2. UnityにFBXをインポート
    • FBXの親にGrabbable Itemコンポーネントをつける
    • 感覚的に、透明なCubeをつける(スティックのサイズに合わせる)
    • 結論
      • 0.4mのスティックでは箱をたたくには短すぎる
    • (ささっと0.6m, 0.8m, 1.0mのスティックを用意しました)

サブ音声

  • サブ音声
    • Speackerコンポーネントを追加するだけ見たいです

ハンドベル(使って音を鳴らす、動作確認用)

ハンドベルなら、①振ったときに音が鳴る、②インターアクションしたら音が鳴る、としたい。
ここでは、まず、使ったときに音が鳴る、を実装したい(音が鳴るロジック以外は実現できるようにしたい)

  • VSQ plus+でハンドベルの音源を取得する
    *myeditで音源を編集し、必要な部分をmp3でエクスポートする
  • 【cluster】Logic使ったゲームワールドを作ろう 本編
    • 「アイテムを使う→音が鳴る」を参考にする(Unityのコンポーネント開発はパッション実装)
    • Use Item TriggerでOnUseキーを自分自身に発火する
    • Audio SouceとPlay Audio Source Gimmickをつけて、OnUseキーを受信したら音鳴らす
    • 見た目・コライダー用にcubeつけておく
    • 全音階用意して正しく動く(mp3の名前とか、AudioSourceのデータ設定とか、座標とか)ことを確認しておく

きよしこの夜

ソラソミ、ソラソミ、
レレシ、ドドソ、
ララドシラ、ソラソミ、
ララドシラ、ソラソミ、
レレファレシ、ドミ、
ドソミソ、ファレド

ハンドベル(振って音を鳴らす、できるかな?)

見た目は置いておいて、ベルをつかみ、振ったときに音を鳴らしたい。(振ったときに音を鳴らしたいよね!)

const se = $.audio("Handbell4A");
$.onGrab((isGrab, isLeftHand) => {
    if (isGrab) {
        if (isLeftHand) {
            $.log("grabbed by left hand.");
        } else {
            $.log("grabbed by right hand.");
        }
        se.play();
    }
});

$.onUpdate(deltaTime => {
    let t = $.state.time ?? 0;
    t += deltaTime;
    if (t > 10) {
        var pos = $.getPosition();
        $.log("pos: " + pos);
        t -= 10;
    }
    $.state.time = t;
});


座標は取れましたが、なんとなくベルを鳴らした感が出ませんでした。
そこで、①加速して②止めた、時に音を鳴らすようにしてみました。
加速の閾値と止めた閾値はそれぞれ僕の好みです。

const se = $.audio("Handbell");

$.onStart(() => {
    $.state.grabbing = false;
    $.state.accelerating = false;
    $.state.hasStopped = true;
});

$.onGrab((isGrab, isLeftHand) => {
    $.state.grabbing = isGrab;
    if ($.state.grabbing) {
        if (isLeftHand) {
            $.log("grabbed by left hand.");
        } else {
            $.log("grabbed by right hand.");
        }
    }
});

$.onUpdate(deltaTime => {
    if (!$.state.grabbing) { 
        // 何も掴んでいない場合は何もしない
        return;
    }
    const thresholdHigh = 0.5;
    const thresholdLow = 0.001;
    var velocityLength = $.velocity.length();
    // $.log("velocityLength: " + velocityLength);
    $.state.accelerating |= velocityLength > thresholdHigh;
    $.state.hasStopped = velocityLength <= thresholdLow;
    if ($.state.accelerating && $.state.hasStopped) {
        se.play();
        $.state.accelerating = false;
    }
});

試行錯誤の記録4(世界観)

空間データとベイク

体育館データを配布いただいているので感謝して利用します
テクスチャーの貼り方わからないので、添付されたpngを真心こめてオブジェクトにD&Dしました。
ライトベイクの基本を学ぼう屋根のあるワールドはスマホで入ると真っ暗になるので、事前にライトベイクしておきます

スクリーン

ギフトコールバック API

まったく当初の目的とは違いますが、使ってみましょうか。→PlayerHandleをテスト時に取得できない(N敗)
→質問してみました。 https://discord.com/channels/682526731311251636/1321144309164019753/1321144309164019753
結論として、僕がstateからとれる"プロパティ"をプロキシーとして理解せず代入していたために動いていませんでした。

$.onStart(() => {
    $.state.giftingPlayerUserIds = [];
});

$.onGiftSent((gifts) => {
    $.log("onGiftSent called");

    let currentSenderPlayerUserIds = []
    for (let i = 0; i < gifts.length; i++) {
        let player = gifts[i].sender;
        $.log("push player:" + player);
        if (player != null) {
            currentSenderPlayerUserIds.push(player.userId);
            $.log("push player.Userid:" + player.userId);
        }
    }
    $.state.giftingPlayerUserIds = currentSenderPlayerUserIds;
    $.log("currentSenderPlayerUserIds:" + currentSenderPlayerUserIds);
});

const text = $.subNode("Text");
const initializedText = "";
const theManText = "この人がやりました\n↓";

$.onUpdate(deltaTime => {
    let time = $.state.time ?? 0;
    time += deltaTime;
    const interval = 5;
    if (time > interval) {
        time = 0;
        let currentText = initializedText;
        let _giftingPlayerUserIds = $.state.giftingPlayerUserIds;
        _giftingPlayerUserIds.forEach(playerUserId => {
            $.log("collected player:" + playerUserId);
        });
        let giftingPlayerUserId = _giftingPlayerUserIds.shift();
        const playerHandles = $.getPlayersNear(new Vector3(), Infinity);
        playerHandles.forEach(playerHandle => {
            $.log("### playerHandle.userId:" + playerHandle.userId);
            $.log("### giftingPlayerUserId:" + giftingPlayerUserId);
            if (playerHandle != null && playerHandle.exists() && playerHandle.userId === giftingPlayerUserId) {
                let position = playerHandle.getPosition();
                const shift = new Vector3(0, 2, 0);
                position.add(shift);
                $.setPosition(position);
                currentText = theManText;

            }
        });
        text.setText(currentText);
        $.state.giftingPlayerUserIds = _giftingPlayerUserIds;
     }

    $.state.time = time;
});

これらを添付して幸せになるかわかりませんが、半年後の自分のために添付します。


ギフトを行ったプレイヤーの位置に、テキストを表示するための機能です。
テキストの位置を移動するためにMovable itemコンポーネントを設定しています。
動作確認ではコメントにスラッシュコマンドを送ります。
空白が入っていると失敗します(N敗)。Smithさんからの回答にありますが、コマンドの値は適切な値でないと失敗します(N敗)

/dummygift コマンドの userId には、 /users コマンドで取得できる ID を指定してください。
/users の出力結果はコンソールの、アイテムの方ではなく全ての方に出力されます。
存在しない ID を指定すると null を返します。
null はゴーストが投げた場合に相当します。
userID を省略した場合は、コマンドを実行したユーザーが投げたことになります。

nullは正常な用途として利用される(N敗)。PlayerHandleのログ表示を行うとPlayerHandleテキストが表示される(アドレスが表示されない)(N敗)。これらのつらいトラブルがありました。。。

テストとフィードバック

  • ワールド公開前のテストプレイ
    * Winで基本的に確認していますが、Androidでも見た目や動作性を確認しました
  • 自分自身と他の人の視点でチェック
    • 適宜イベントを開催して、フレンドとともに音の発火や、発火のタイミングについて確認しました。
    • まぁ個人が解決できる領域ではなさそう、、、
  • どう反映したか、どこで妥協したか
    • "箱"というのを前面に押しているので、いまさらドラムやハンドベルのモデリングをしたところでしかたないですねぇ。。。(クリスマスにまにあわなかった。1敗)

おわりに

  • 感謝の言葉
  • 当初のゴールからだいぶ異なるところに来てしまいました。しかし2025年からいろいろ企画があり、このワールドに時間をさけられていないのが現状です。
  • この軌跡が少しでも次につながれば本望です。

Discussion