Closed13

VRChatのワールドのグローバル対応についてのログ

ぷちぶーけぷちぶーけ

【前提】
Useでドーナツを発射するGrub可能なギミックを制作
GrubできるCube自体はグローバルに対応済み
Udon Sharpでコーディング
CopilotのGPT-4oを使用

【目的】
発射されるドーナツのグローバル対応を目指す

【ゴール】
ドーナツを食べさせてお化けパンプキンを大きくさせるワールドの実装
ハロウィンでみんなハッピー

ぷちぶーけぷちぶーけ

同期の実装をしたところ部分的にグローバル同期が実現できたが、同期ズレが発生するように。
※クライアント数2でbuild and testを実施

現時点の成長するかぼちゃに実装しているコードは以下の通り

using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using UdonSharp;
using VRC.SDK3.Components;

/// <summary>
/// 特定のプレハブのオブジェクトが触れると、アタッチされているオブジェクトが大きくなります。
/// 一定の大きさに達したら、それ以上は大きくなりません
/// 最大の大きさに達したら、目が光っているオブジェクトに差し替わります
/// </summary>
[UdonBehaviourSyncMode(BehaviourSyncMode.Continuous)]
public class PumpkinPumper : UdonSharpBehaviour
{
    // スケール変更の倍率
    public Vector3 scaleMultiplier = new Vector3(1.05f, 1.05f, 1.05f);

    // 衝突対象のタグ名
    public string targetTag = "BakeryItem";

    // ミルクのタグ名
    public string milkTag = "Milkobj";

    // 最大スケール
    public Vector3 maxScale = new Vector3(30.0f, 30.0f, 30.0f);

    // 縮小倍率
    public Vector3 shrinkMultiplier = new Vector3(0.9f, 0.9f, 0.9f);

    // 目が光るオブジェクトのプレハブ
    public GameObject glowingPrefab;

    // 最小スケール
    public Vector3 minScale = new Vector3(3.0f, 3.0f, 3.0f);

    [UdonSynced] private Vector3 syncedScale = new Vector3(3.0f, 3.0f, 3.0f);

    void Start()
    {

    }

    void Update()
    {
    }

    private void OnTriggerEnter(Collider other)
    {
        Debug.Log("object: " + other.gameObject.name);
        // 衝突したオブジェクトが特定のタグを持っているか確認
        if (other.gameObject.name.Contains(targetTag))
        {
            Debug.Log("Pumpkin ate: " + other.gameObject.name);
            Vector3 newScale = Vector3.Scale(transform.localScale, scaleMultiplier);
            UpdateScale(newScale); // スケール変更を同期
            Destroy(other.gameObject); // 衝突したオブジェクトを削除
        }
        else if (other.gameObject.name.Contains(milkTag))
        {
            Debug.Log("Pumpkin drank milk: " + other.gameObject.name);
            Vector3 newScale = Vector3.Scale(transform.localScale, shrinkMultiplier);

            // 最小スケールを下回らないように制限
            if (newScale.x >= minScale.x && newScale.y >= minScale.y && newScale.z >= minScale.z)
            {
                UpdateScale(newScale); // スケール変更を同期
            }

            Destroy(other.gameObject); // ミルクオブジェクトを削除
            return; // ミルクの効果を優先して処理を終了
        }

        // 現在のスケールを取得
        Vector3 currentScale = transform.localScale;

        // 最大スケールに達しているか確認
        if (currentScale.x < maxScale.x && currentScale.y < maxScale.y && currentScale.z < maxScale.z)
        {
            // スケールを変更
            transform.localScale = Vector3.Scale(transform.localScale, scaleMultiplier);
            Debug.Log("New Scale: " + transform.localScale);

            // 再度最大スケールに達しているか確認
            if (transform.localScale.x >= maxScale.x || transform.localScale.y >= maxScale.y || transform.localScale.z >= maxScale.z)
            {
                // 目が光るオブジェクトに差し替え
                if (glowingPrefab != null)
                {
                    GameObject glowingObject = Instantiate(glowingPrefab, transform.position, transform.rotation);
                    glowingObject.transform.localScale = transform.localScale; // 元のスケールを適用
                    Destroy(gameObject);
                }
            }
        }
    }

    private void UpdateScale(Vector3 newScale)
    {
        // オーナーシップを確認または変更
        if (!Networking.IsOwner(gameObject))
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
            Debug.Log("Ownership transferred to: " + Networking.LocalPlayer.displayName);
        }

        // スケールを更新し、同期変数に反映
        transform.localScale = newScale;
        syncedScale = newScale;
        Debug.Log("Requesting serialization for scale: " + newScale);
        RequestSerialization();
    }

    public override void OnDeserialization()
    {
        // 同期されたスケールをローカルに反映
        Debug.Log("Deserialization received scale: " + syncedScale);
        transform.localScale = syncedScale;
    }
}

ぷちぶーけぷちぶーけ

RequestSerializationが無限ループを起こす構造になっていたため、修正。
この修正で同期が正常になった。

using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using UdonSharp;
using VRC.SDK3.Components;

/// <summary>
/// 特定のプレハブのオブジェクトが触れると、アタッチされているオブジェクトが大きくなります。
/// 一定の大きさに達したら、それ以上は大きくなりません
/// 最大の大きさに達したら、目が光っているオブジェクトに差し替わります
/// </summary>
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class PumpkinPumper : UdonSharpBehaviour
{
    // スケール変更の倍率
    public Vector3 scaleMultiplier = new Vector3(1.05f, 1.05f, 1.05f);

    // 衝突対象のタグ名
    public string targetTag = "BakeryItem";

    // ミルクのタグ名
    public string milkTag = "Milkobj";

    // 最大スケール
    public Vector3 maxScale = new Vector3(30.0f, 30.0f, 30.0f);

    // 縮小倍率
    public Vector3 shrinkMultiplier = new Vector3(0.9f, 0.9f, 0.9f);

    // 目が光るオブジェクトのプレハブ
    public GameObject glowingPrefab;

    // 最小スケール
    public Vector3 minScale = new Vector3(3.0f, 3.0f, 3.0f);

    // 同期変数:かぼちゃの大きさ
    [UdonSynced] private Vector3 syncedScale = new Vector3(3.0f, 3.0f, 3.0f);

    void Start()
    {

    }

    void Update()
    {
    }

    private void OnTriggerEnter(Collider other)
    {
        Debug.Log("object: " + other.gameObject.name);
        // 衝突したオブジェクトが特定のタグを持っているか確認
        if (other.gameObject.name.Contains(targetTag))
        {
            Debug.Log("Pumpkin ate: " + other.gameObject.name);
            Vector3 newScale = Vector3.Scale(transform.localScale, scaleMultiplier);
            UpdateScale(newScale); // スケール変更を同期
            Destroy(other.gameObject); // 衝突したオブジェクトを削除
        }
        else if (other.gameObject.name.Contains(milkTag))
        {
            Debug.Log("Pumpkin drank milk: " + other.gameObject.name);
            Vector3 newScale = Vector3.Scale(transform.localScale, shrinkMultiplier);

            // 最小スケールを下回らないように制限
            if (newScale.x >= minScale.x && newScale.y >= minScale.y && newScale.z >= minScale.z)
            {
                UpdateScale(newScale); // スケール変更を同期
            }

            Destroy(other.gameObject); // ミルクオブジェクトを削除
            return; // ミルクの効果を優先して処理を終了
        }

        // 現在のスケールを取得
        Vector3 currentScale = transform.localScale;

        // 最大スケールに達しているか確認
        if (currentScale.x < maxScale.x && currentScale.y < maxScale.y && currentScale.z < maxScale.z)
        {
            // スケールを変更
            Vector3 newScale = Vector3.Scale(transform.localScale, scaleMultiplier);
            UpdateScale(newScale);

            // 再度最大スケールに達しているか確認
            if (transform.localScale.x >= maxScale.x || transform.localScale.y >= maxScale.y || transform.localScale.z >= maxScale.z)
            {
                // 目が光るオブジェクトに差し替え
                if (glowingPrefab != null)
                {
                    GameObject glowingObject = Instantiate(glowingPrefab, transform.position, transform.rotation);
                    glowingObject.transform.localScale = transform.localScale; // 元のスケールを適用
                    Destroy(gameObject);
                }
            }
        }
    }

    private void UpdateScale(Vector3 newScale)
    {
        // オーナーシップを確認または変更
        if (!Networking.IsOwner(gameObject))
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
            Debug.Log("Ownership transferred to: " + Networking.LocalPlayer.displayName);
        }

        // スケールを更新し、同期変数に反映
        transform.localScale = newScale;
        syncedScale = newScale;

        // 少し待機してから同期リクエストを行う
        SendCustomEventDelayedFrames(nameof(RequestSync), 1);
    }

    public void RequestSync()
    {
        Debug.Log("Requesting serialization for scale: " + syncedScale);
        RequestSerialization();
    }

    public override void OnDeserialization()
    {
        // 同期されたスケールをローカルに反映
        Debug.Log("Deserialization received scale: " + syncedScale);
        transform.localScale = syncedScale;
    }
}

ぷちぶーけぷちぶーけ

かぼちゃの目が光るかどうかも同期させた版はこちら

using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using UdonSharp;
using VRC.SDK3.Components;

/// <summary>
/// 特定のプレハブのオブジェクトが触れると、アタッチされているオブジェクトが大きくなります。
/// 一定の大きさに達したら、それ以上は大きくなりません
/// 最大の大きさに達したら、目が光っているオブジェクトに差し替わります
/// </summary>
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class PumpkinPumper : UdonSharpBehaviour
{
    // スケール変更の倍率
    public Vector3 scaleMultiplier = new Vector3(1.05f, 1.05f, 1.05f);

    // 衝突対象のタグ名
    public string targetTag = "BakeryItem";

    // ミルクのタグ名
    public string milkTag = "Milkobj";

    // 最大スケール
    public Vector3 maxScale = new Vector3(30.0f, 30.0f, 30.0f);

    // 縮小倍率
    public Vector3 shrinkMultiplier = new Vector3(0.9f, 0.9f, 0.9f);

    // 目が光るオブジェクトのプレハブ
    public GameObject glowingPrefab;

    // 最小スケール
    public Vector3 minScale = new Vector3(3.0f, 3.0f, 3.0f);

    // 同期変数:かぼちゃの大きさ
    [UdonSynced] private Vector3 syncedScale = new Vector3(3.0f, 3.0f, 3.0f);

    // 同期変数:目が光っている状態
    [UdonSynced] private bool isGlowing = false;

    void Start()
    {

    }

    void Update()
    {
    }

    private void OnTriggerEnter(Collider other)
    {
        Debug.Log("object: " + other.gameObject.name);
        // 衝突したオブジェクトが特定のタグを持っているか確認
        if (other.gameObject.name.Contains(targetTag))
        {
            Debug.Log("Pumpkin ate: " + other.gameObject.name);
            Vector3 newScale = Vector3.Scale(transform.localScale, scaleMultiplier);
            UpdateScale(newScale); // スケール変更を同期
            Destroy(other.gameObject); // 衝突したオブジェクトを削除
        }
        else if (other.gameObject.name.Contains(milkTag))
        {
            Debug.Log("Pumpkin drank milk: " + other.gameObject.name);
            Vector3 newScale = Vector3.Scale(transform.localScale, shrinkMultiplier);

            // 最小スケールを下回らないように制限
            if (newScale.x >= minScale.x && newScale.y >= minScale.y && newScale.z >= minScale.z)
            {
                UpdateScale(newScale); // スケール変更を同期
            }

            Destroy(other.gameObject); // ミルクオブジェクトを削除
            return; // ミルクの効果を優先して処理を終了
        }

        // 現在のスケールを取得
        Vector3 currentScale = transform.localScale;

        // 最大スケールに達しているか確認
        if (currentScale.x < maxScale.x && currentScale.y < maxScale.y && currentScale.z < maxScale.z)
        {
            // スケールを変更
            Vector3 newScale = Vector3.Scale(transform.localScale, scaleMultiplier);
            UpdateScale(newScale);

            // 再度最大スケールに達しているか確認
            if (transform.localScale.x >= maxScale.x || transform.localScale.y >= maxScale.y || transform.localScale.z >= maxScale.z)
            {
                // 目が光るオブジェクトに差し替え
                if (glowingPrefab != null && !isGlowing)
                {
                    isGlowing = true; // 状態を更新
                    RequestSerialization(); // 同期リクエスト

                    GameObject glowingObject = Instantiate(glowingPrefab, transform.position, transform.rotation);
                    glowingObject.transform.localScale = transform.localScale; // 元のスケールを適用
                    Destroy(gameObject);
                }
            }
        }
    }

    private void UpdateScale(Vector3 newScale)
    {
        // オーナーシップを確認または変更
        if (!Networking.IsOwner(gameObject))
        {
            Networking.SetOwner(Networking.LocalPlayer, gameObject);
            Debug.Log("Ownership transferred to: " + Networking.LocalPlayer.displayName);
        }

        // スケールを更新し、同期変数に反映
        transform.localScale = newScale;
        syncedScale = newScale;

        // 少し待機してから同期リクエストを行う
        SendCustomEventDelayedFrames(nameof(RequestSync), 1);
    }

    public void RequestSync()
    {
        Debug.Log("Requesting serialization for scale: " + syncedScale);
        RequestSerialization();
    }

    public override void OnDeserialization()
    {
        // 同期されたスケールをローカルに反映
        Debug.Log("Deserialization received scale: " + syncedScale);
        transform.localScale = syncedScale;

        // 目が光るオブジェクトの状態を反映
        if (isGlowing && glowingPrefab != null)
        {
            GameObject glowingObject = Instantiate(glowingPrefab, transform.position, transform.rotation);
            glowingObject.transform.localScale = transform.localScale; // 元のスケールを適用
            Destroy(gameObject);
        }
    }
}
ぷちぶーけぷちぶーけ

【注意】
VRCのUdonではタグが使えないっぽいので、アセットの名前の前方一致検査によって擬似的なタグ分けを実現している

ぷちぶーけぷちぶーけ

ドーナツ発射スクリプトはこちら

using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
using UdonSharp;
using VRC.SDK3.Components;

/// <summary>
/// 握って使うことで、アイテム(ドーナツなど)を前に発射するスクリプト。
/// 長押し時間で発射するアイテムの大きさが変わります。
/// </summary>
[UdonBehaviourSyncMode(BehaviourSyncMode.Manual)]
public class BakerySpawner : UdonSharpBehaviour
{
    [Header("発射設定")]
    [Tooltip("発射するアイテムのプレハブ配列。0番目にドーナツを設定してください。")]
    public GameObject[] bakeryPrefabs;
    [Tooltip("アイテムが発射される場所。銃口のように、発射位置を調整したい場合に設定します。")]
    public Transform muzzlePoint;
    [Tooltip("アイテムの基本発射速度。")]
    public float shootSpeed = 10f;

    [Header("スケール設定")]
    [Tooltip("最小スケール。")]
    public float minScale = 5f;
    [Tooltip("最大スケール。")]
    public float maxScale = 30f;
    [Tooltip("最大スケールに達するまでの長押し時間(秒)。")]
    public float maxHoldDuration = 2.0f;

    [Header("見た目設定")]
    [Tooltip("この発射装置(持つオブジェクト)の通常時の色。")]
    public Color spawnerColor = Color.white;
    [Tooltip("チャージが最大になったときの色。")]
    public Color chargingColor = Color.red;

    // --- 内部変数 ---
    private bool isHoldingUse = false;
    private float useButtonDownTime = 0f;
    private Renderer spawnerRenderer;

    [UdonSynced] private Vector3 syncedPosition;
    [UdonSynced] private Quaternion syncedRotation;
    [UdonSynced] private float syncedScale;

    void Start()
    {
        spawnerRenderer = GetComponent<Renderer>();
        if (spawnerRenderer != null)
        {
            spawnerRenderer.material.color = spawnerColor;
        }
    }

    void Update()
    {
        // Useボタンを長押ししている間、チャージ状態を色で表現する
        if (isHoldingUse && spawnerRenderer != null)
        {
            float holdDuration = Time.time - useButtonDownTime;
            float chargeRatio = Mathf.Clamp01(holdDuration / maxHoldDuration);
            spawnerRenderer.material.color = Color.Lerp(spawnerColor, chargingColor, chargeRatio);
        }
    }

    /// <summary>
    /// このオブジェクトを握った状態でUseボタンを押し始めたときに呼び出されます。
    /// </summary>
    public override void OnPickupUseDown()
    {
        isHoldingUse = true;
        useButtonDownTime = Time.time;
    }

    /// <summary>
    /// このオブジェクトを握った状態でUseボタンを離したときに呼び出されます。
    /// </summary>
    public override void OnPickupUseUp()
    {
        if (!isHoldingUse) return;

        isHoldingUse = false;

        // 長押し時間からスケールを計算
        float holdDuration = Time.time - useButtonDownTime;
        float chargeRatio = Mathf.Clamp01(holdDuration / maxHoldDuration);
        float targetScale = Mathf.Lerp(minScale, maxScale, chargeRatio);

        // 計算したスケールでアイテムを発射し、グローバルサービスに登録
        ShootAndRegisterItem(0, targetScale);

        // 発射装置の色を元に戻す
        if (spawnerRenderer != null)
        {
            spawnerRenderer.material.color = spawnerColor;
        }
    }

    /// <summary>
    /// 指定されたインデックスのアイテムを、指定されたスケールで発射し、グローバルサービスに登録します。
    /// </summary>
    public void ShootAndRegisterItem(int itemIndex, float scale)
    {
        ShootItem(itemIndex, scale);
    }

    /// <summary>
    /// 指定されたインデックスのアイテムを、指定されたスケールで発射します。
    /// </summary>
    public GameObject ShootItem(int itemIndex, float scale)
    {
        if (bakeryPrefabs == null || bakeryPrefabs.Length == 0 || bakeryPrefabs[itemIndex] == null)
        {
            Debug.LogError("Bakery Prefabが正しく設定されていません。");
            return null;
        }

        VRCPlayerApi localPlayer = Networking.LocalPlayer;
        if (localPlayer == null) return null;

        VRCPlayerApi.TrackingData headData = localPlayer.GetTrackingData(VRCPlayerApi.TrackingDataType.Head);
        syncedPosition = (muzzlePoint != null) ? muzzlePoint.position : transform.position;
        syncedRotation = headData.rotation;
        syncedScale = scale;

        // --- 同期リクエストを送信 ---
        RequestSerialization();

        // --- プレハブを生成 ---
        GameObject newItem = (GameObject)Instantiate(bakeryPrefabs[itemIndex], syncedPosition, syncedRotation);
        newItem.transform.localScale = Vector3.one * syncedScale;

        // --- オーナーシップを設定 ---
        Networking.SetOwner(localPlayer, newItem);

        Rigidbody itemRigidbody = newItem.GetComponent<Rigidbody>();
        if (itemRigidbody != null)
        {
            itemRigidbody.mass = scale;
            itemRigidbody.velocity = headData.rotation * Vector3.forward * shootSpeed;
        }
        else
        {
            Debug.LogWarning($"発射されたアイテム '{newItem.name}' にRigidbodyコンポーネントがありません。");
        }

        return newItem;
    }

    public override void OnDeserialization()
    {
        VRCPlayerApi ownerPlayer = Networking.GetOwner(gameObject);
        if (ownerPlayer == null) return;

        // --- プレハブを生成 ---
        GameObject newItem = (GameObject)Instantiate(bakeryPrefabs[0], ownerPlayer.GetPosition(), ownerPlayer.GetRotation());
        newItem.transform.localScale = Vector3.one * syncedScale; // 同期されたスケールを適用
        Rigidbody itemRigidbody = newItem.GetComponent<Rigidbody>();
        if (itemRigidbody != null)
        {
            itemRigidbody.mass = syncedScale; // スケールに基づいた質量を設定
            itemRigidbody.velocity = newItem.transform.rotation * Vector3.forward * shootSpeed;
        }
        Debug.Log("Donut spawned at synced owner's position, rotation, and scale.");
    }
}

ぷちぶーけぷちぶーけ

Audioの再生にはAudioSourceを再生されるオブジェクトに適用する必要がある

ぷちぶーけぷちぶーけ

public化しようとしたところ「Object reference not set to an instance of an object」にてバリデーションエラー。

複雑化しており、対応を断念
次回のワールド作成にて配慮する

このスクラップは8日前にクローズされました