【VRChat】PlayerDataで音量を保存する:次に来ても自分の設定のまま

作成: 2026-09-08最終更新: 2026-09-09

BGM音量のスライダーをPlayerDataに保存して、次回の訪問で復元します。復元を待たずに読むと保存が消える理由と、その避け方を実際に手を動かして確かめます。

BGMが少し大きかったので、ワールドのスライダーを下げて遊びました。楽しかったので翌日また訪れると、また大音量で迎えられます。

音量、字幕のオンオフ、チュートリアルを見たかどうか。こういう 自分だけの設定 は、同期変数では残せません。同期はいまこの部屋にいる人へ伝えるための仕組みで、部屋を出れば消えるからです。

この記事では、スライダーで決めた音量が次回も残る ようにします。

同じ人が2回目に訪れたとき、スライダーが前回の位置で始まる

この記事でわかること

  • 同期変数とPlayerDataの使い分け
  • 復元を待ってから読む理由
  • 書くのは自分の分だけ、読むときは誰の分かを渡すこと
  • 保存を連発しないための工夫

ネットワーク同期入門ワールド内にUIを置く を試した状態から始めます。

Sponsored


黒板は消える、ロッカーは残る

同期の話で、同期変数は黒板 というたとえを使いました。いま部屋にいる人が読める場所です。部屋を出れば、その黒板とはお別れになります。

PlayerData は、そのワールドに置かれた自分のロッカー です。中身は自分だけのもので、次に来たときもそのまま残っています。

黒板は部屋を出ると消え、ロッカーは次回まで残る

音量で並べると、違いがはっきりします。

やりたいこと使うもの
部屋のBGMを、いる人全員でいっせいに止める同期変数
自分だけBGMを小さくして、次回も小さいままPlayerData

保存できるのは です。真偽、数値、文字列が基本で、Vector3Color も扱えます。

保存 できない ものもはっきりしています。AudioSourceやGameObjectそのもの、マテリアル、シーンにある物への参照。これらは保存されません。

保存されるのは値だけで、シーンにある物そのものは保存されない

保存するのは「音量は0.2だった」という数字だけで、スライダーもAudioSourceも保存しません。次回、その数字をいまシーンにある物へ書き戻します。

保存はワールドごと、プレイヤーごと です。同じアカウントでも、別のワールドからは見えません。逆に、同じワールドなら別のインスタンスでも同じデータが読めます。

Sponsored

復元を待たないと、保存が消える

PlayerDataでいちばん多い事故が、ここです。

入室した直後は、保存データがまだ届いていません。 この状態で読むと、保存した値ではなく初期値(floatなら 0)が返ってきます。

復元前に読んだ0を書き戻すと、本物の保存が消える

問題は、その 0 をスライダーに入れてしまうことです。スライダーが動けば「音量が変わった」と判断して保存します。こうして本物の 0.2 が 0 で上書きされ、消えます。

読むのが早いこと自体は害がありません。危ないのは、初期値を本物だと思って書き戻すこと です。

これを避けるための合図が OnPlayerRestored です。

public override void OnPlayerRestored(VRCPlayerApi player)

「このプレイヤーの保存データを取り出したので、もう読めます」という知らせです。呼ばれてから読めば、正しい値が返ってきます。

このイベントには2つの注意点があります。

他の人の分でも呼ばれます。 誰かが入室するたび、その人の復元でも呼ばれます。自分のBGM音量を他人の入室で書き換えないよう、player.isLocal で自分の分だけを見ます。

書くのと読むのは形が違います。 ここは最初とまどうところです。

PlayerData.SetFloat("bgm_volume", 0.2f);                          // 書く:自分の分だけ
PlayerData.TryGetFloat(player, "bgm_volume", out float saved);    // 読む:誰の分かを渡す

書けるのは自分のロッカーだけなので、書く側にプレイヤーの指定はありません。読む側は他の人のロッカーも覗けるので、誰の分かを渡します。ランキング表示などは、この読み方でできています。

実践:音量を覚えるスライダー

スライダーでBGM音量を変えられて、次に来たときもその音量で始まる仕掛けを作ります。

1. BGMとスライダーを置く

床のあるシーンに、次を置きます。

名前作り方と設定
BgmCreate Empty に Audio Source を追加。Play On Awake と Loop をオン、Spatial Blend は 0
SettingCanvasUI Canvas(World Space)。(0, 1.5, 2)、Scale (0.003, 0.003, 0.003)
VolumeSliderSettingCanvas の子に UI Slider
PersistenceRootCreate Empty。(0, 0, 0)
スライダーのパネルとBGMの配置

Bgm の Audio Source には、ループできる曲を入れます。

Canvasには ワールド内にUIを置く と同じく、VRC Ui Shape を追加して、Graphic Raycasterが付いていることを確認します。これがないとVRChat内でスライダーを触れません。

VolumeSlider は Min Value 0、Max Value 1、Value 0.5 にしておきます。

2. コードを書く

Assets/ScriptsBgmVolumeSetting を作ります。

using UdonSharp;
using UnityEngine;
using UnityEngine.UI;
using VRC.SDK3.Persistence;
using VRC.SDKBase;

[UdonBehaviourSyncMode(BehaviourSyncMode.None)]
public class BgmVolumeSetting : UdonSharpBehaviour
{
    [SerializeField] private AudioSource bgm;
    [SerializeField] private Slider volumeSlider;
    [SerializeField] private float defaultVolume = 0.5f;

    private const string VolumeKey = "bgm_volume";

    private bool restored;      // 復元が終わるまでは保存しない
    private bool savePending;   // 保存の予約が入っているか

    private void Start()
    {
        // 復元が来るまでは、作者が決めた音量で鳴らしておく
        Apply(defaultVolume);
    }

    // 自分の保存データが読める状態になったら呼ばれる
    public override void OnPlayerRestored(VRCPlayerApi player)
    {
        if (!player.isLocal) return;   // 他の人の復元では何もしない

        float volume = defaultVolume;
        if (PlayerData.TryGetFloat(player, VolumeKey, out float saved))
        {
            volume = saved;            // 前回の値があった
        }
        Apply(volume);

        restored = true;               // ここから先は保存してよい
    }

    // スライダーの On Value Changed からつなぐ
    public void OnVolumeChanged()
    {
        if (volumeSlider == null) return;

        // 音はすぐ変える。耳で確かめられるように
        if (bgm != null) bgm.volume = volumeSlider.value;

        if (!restored) return;

        // 動かしている間は予約を1つだけにして、0.5秒後にまとめて保存する
        if (!savePending)
        {
            savePending = true;
            SendCustomEventDelayedSeconds(nameof(SaveVolume), 0.5f);
        }
    }

    public void SaveVolume()
    {
        savePending = false;
        if (volumeSlider == null) return;

        PlayerData.SetFloat(VolumeKey, volumeSlider.value);
        Debug.Log("[BgmVolume] 保存しました: " + volumeSlider.value);
    }

    private void Apply(float volume)
    {
        if (bgm != null) bgm.volume = volume;
        if (volumeSlider != null) volumeSlider.value = volume;
    }
}

読むときのポイントが3つあります。

restored を立てるのは Apply() のあと です。Apply() はスライダーを動かすので OnVolumeChanged() が呼ばれますが、その時点ではまだ restoredfalse なので保存されません。復元した値をそのまま保存し直す無駄を防いでいます。

保存は0.5秒に1回までにしています。 スライダーをドラッグすると OnVolumeChanged() は毎フレーム呼ばれます。そのたびに保存すると、クラウドへの書き込みが数百回になります。予約を1つだけ持つようにして、動かし終わった0.5秒後に1回書きます。この待たせ方は 時間差で処理を呼ぶ と同じ仕組みです。

キーは文字列で自分が決めます。 "bgm_volume" のように分かる名前にして、const で1か所にまとめておくと打ち間違いが減ります。書いた型と読む型もそろえてください。

3. つなぐ

PersistenceRoot に Udon Behaviour を追加し、BgmVolumeSetting を指定します。BgmとVolume Sliderをドラッグします。

VolumeSlider の Inspector で、いちばん下の On Value Changed (Single)+ を押します。

指定するもの
オブジェクトPersistenceRoot
関数UdonBehaviour → SendCustomEvent
引数OnVolumeChanged
スライダーのOn Value Changedから、メソッド名を文字で指定する

関数の一覧から選ぶのは SendCustomEvent で、呼びたいメソッド名は文字列で入力します。ここのつづりが違うと、静かに何も起きません。

4. 本当に残るかを確かめる

まずBuild & Testで、動きだけを確かめます。

順序操作期待する結果
1入るスライダーが 0.5、BGMがその音量で鳴る
20.2 まで下げる音がすぐ小さくなる
3手を離して1秒待つConsoleに「保存しました: 0.2」が 1回だけ 出る
4ドラッグし続けるログが毎フレームではなく、間隔をあけて出る

3番目と4番目で、保存が連発していないことを確かめます。

次回まで残るかどうかは、アップロードしたワールドで確かめます。 テスト用の保存データはクライアントの中だけにあり、閉じると消えるためです。

Community Labsに出す前でも、自分だけが入れる状態でアップロードして、退出してから入り直せば確認できます。スライダーが 0.2 の位置で始まれば成功です。

Sponsored

うまくいかないときは

  • いつも初期値で始まるStart() で読んでいます。OnPlayerRestored を待ちます
  • 設定が保存されない → キー名のつづりが書くときと読むときで違っていないか確認します
  • 他の人が入ると自分の音量が変わるplayer.isLocal の判定が抜けています
  • スライダーを動かしても何も起きない → On Value Changed の設定か、CanvasのVRC Ui Shapeを確認します
  • Build & Testで消える → テスト用のデータはクライアントを閉じると消えます。アップロードして確かめます
  • 保存ログが大量に出る → 予約の仕組みが効いていません。savePending の判定を確認します

おまけ:先に知っておくと良いこと

  • 初回とゼロを見分ける: TryGetFloatfalse を返したときが初回訪問です。0 が返ってきたのなら、それは「ミュートで保存した」という意味になります。この2つを混同しないでください
  • キーは増やしすぎない: 設定が10個あるなら10個のキーで構いませんが、名前の付け方を決めておかないと自分で分からなくなります。bgm_volume subtitle_on のように、そろえた形にします
  • 同期とは独立している: このスクリプトの同期モードは None ですが、PlayerDataは問題なく使えます。個々のUdon Behaviourの同期設定とは別の仕組みです
  • 他の人の分も読める: 読む側にプレイヤーを渡せるので、全員の記録を集めて掲示板に出すこともできます。ランキングのあるワールドはこれを使っています
  • 使いどころはいろいろ: BGM音量、ミラーの初期状態、字幕の言語、チュートリアルを見たか、前回いた部屋。「他の人と合わせる必要がない設定」はすべて対象です

まとめ

PlayerDataは、そのワールドに置かれた自分のロッカーです。

  • 同期変数は部屋を出ると消える。PlayerDataは次回まで残る
  • 読む前に OnPlayerRestored を待つ。待たずに読んだ初期値を書き戻すと保存が消える
  • 書くのは自分の分だけ。読むときは誰の分かを渡す
  • 保存は連発しない。動かし終わってから1回書く

保存する前の問いかけは、「これは他の人と合わせる値か、自分だけの値か」 です。自分だけならPlayerDataです。

各自が自分の状態を持つ仕掛けなら PlayerObjectで各自の状態を持つ、決まった数の物を貸し出すなら VRCObjectPool入門 へ進んでください。

VRChat このセクションのノート63