土鍋で雑多煮

UnityでXR・ゲーム開発をしています。学んだことや備忘録、趣味の記録などを書いていきます。

MENU

【MRUK最新版】MRUKで部屋の情報を取得し加工する【Quest3】

はじめに

どうも、土鍋です。

この記事はAizu Advent Calendar 2024の10日目の記事です。

一年前くらいに下の記事を書いたのですが、どうやら2024/5/16リリースのMeta XR SDK v65でだいぶ仕様が変わっていたので新しい記事を書きました。

donabenabe.hatenablog.com

Meta XR SimulatorでMR空間をシミュレーションする

プレイボタン左のボタンからSimulatorを有効化し、XR環境をシミュレーションできます。

毎回実環境でビルドしてテストするのは効率が悪いので、Synthetic Environment Serverからパススルーで見える現実空間をシミュレートできるようになります。

Tools Prefab

MRUKやそれに付随するツール群を使うことで部屋のデータにアクセスできます。

Prefab群は Packages > Meta MR Utility Kit > Core > Tools にあります。

Scene上にMRUK Prefabを追加するだけでScene API関連のものが勝手に動作します。 そのため特にこれから先の作業が必要ない場合(Raycastするなど)はこれだけで大丈夫です。

MR環境構築の具体的な内容は以下をご覧ください。

https://developers.meta.com/horizon/documentation/unity/unity-mr-utility-kit-gs?locale=ja_JP

AnchorPrefabSpawner

https://developers.meta.com/horizon/reference/mruk/v68/class_meta_x_r_m_r_utility_kit_anchor_prefab_spawner/?locale=ja_JP

AnchorPrefabSpawner Prefabを追加することで、部屋の壁や家具に対応して特定のPrefabを生成させることができるようになります。
これは前のバージョンでOVRSceneManagerとされてたものです。

Prefabs To Spawnで生成したいもののPrefabを設定できます。

デフォルトの状態そのままを実行すると下のように部屋のレイアウトに合わせてPrefabが生成されているのが確認できます。

RoomModel

RoomModel Prefabは基本的にはAnchorPrefabSpawner Prefabと一緒です。
適用されているPrefabが半透明なので現実空間が見やすいといった感じです。

Labelsを切り替えることでそのPrefabの適用先を変更できます。

EffectMesh

https://developers.meta.com/horizon/reference/mruk/v69/class_meta_x_r_m_r_utility_kit_effect_mesh

EffectMeshは特定のマテリアルやコライダーを指定したLabelに適用するものです。

GlobalMeshを可視化するにはLabelsにGlobalMeshを指定してあげることで見えるようになります。

Collidersのチャックボックスにチェックをいれるとメッシュにコライダーをつけることができます。

RoomGuardian

通常のQuestのガーディアンのように物理空間のガーディアンを表示できます。

参考

donabenabe.hatenablog.com

https://developers.meta.com/horizon/reference/mruk/v68/class_meta_x_r_m_r_utility_kit_anchor_prefab_spawner

https://developers.meta.com/horizon/reference/mruk/v69/class_meta_x_r_m_r_utility_kit_effect_mesh

https://developers.meta.com/horizon/downloads/package/meta-xr-mr-utility-kit-upm/65.0

UnityECSで都市開発シミュレーションゲームを作る【その4】~建物を建築する~

はじめに

どうも、土鍋です。

これは土鍋ひとり Advent Calendar 2024の2日目の記事です。

前回は土鍋さんの【ECS&DI】Unity ECSでVContainerによるDIをやってみる - 土鍋で雑多煮でした。

さて、ECSで都市開発シミュレーションゲームを作るシリーズ4回目はいよいよ建築できるようにしようと思います。
といっても、建物選んでInstantiateするだけですが、まーーECSの仕様がむずくって手こずりました。

建築物選択メニューUI

ECSとUI Toolkit

今回のゲーム開発ではなるべくUnityの新しい要素に触れるようにしようという思想の元、開発しているので、UIに関してもUI Toolkitを導入しました。

UIを構築する

建築したい建物を選ぶメニューをどうしようかな~と思ったんですが、やっぱグリッドビューが一番視認性がいいし情報量が多くていいかなと思い実装しました。
ただここで発生した問題がUI Toolkitにグリッドビューがないという問題でした。(uGUIにはあるのに!)

ということで自力で実装して別記事にまとめたので、ご覧ください。

donabenabe.hatenablog.com

ClickEventの設定

上記の記事でグリッドビューの作成とそれにDataをBindingするとこまで実装したので、プラスアルファでどの建物を選択したかの情報をPlayerStatusHolderに渡しています。

public class ConstructMenu : MonoBehaviour
{
    [SerializeField] 
    private VisualTreeAsset buildingElement; // グリッド要素テンプレート
    
    [SerializeField] 
    private BuildingList buildingList; // 追加したいUI要素のList保持ScriptableObject
    
    private void Start()
    {
        var root = GetComponent<UIDocument>().rootVisualElement;

        foreach (var building in buildingList.buildings)
        {
            var buildingTemplate = buildingElement.Instantiate(); // テンプレートの生成
            buildingTemplate.Q<VisualElement>("thumbnail").dataSource = building; // データのバインド
            root.Q<VisualElement>("Grid").contentContainer.Add(buildingTemplate); // グリッドに要素追加
            buildingTemplate.RegisterCallback<ClickEvent, BuildingData>(Clicked, building); // クリックイベントの登録
        }
    }

    private void Clicked(ClickEvent evt, BuildingData data)
    {
        Debug.Log("Click: " + data.buildingName);
        PlayerStatusHolder.I.NowSelectConstructBuildingID = data.buildingID; // PlayerStatusHolderにIDを渡す
    }
}

Raycastによる建物配置場所の決定

Entity情報の取得

通常の画面クリックからのRaycastではECSのEntity情報は取得できません。
そのため、ECS用物理シミュレーションパッケージのPhysicsを使います。
実装方法は別記事にまとめたのでこちらをご覧ください。

donabenabe.hatenablog.com

UI上ではRaycastをブロック

このRaycastはUI上も構わず貫通してRayを飛ばすので対策が必要になってきます。
これも別記事に書いたので、そちらを参照してください。

donabenabe.hatenablog.com

Entityの生成

さてここまででようやく建物選択と配置のためのRaycastが実装できたので、最後は建物のEntityを生成する必要があります。

PrefabのBake

建物のPrefabを作ったのは良いのですが、これはGameObjectなのでそのままECSでは使えません。

そのためBakeを行う必要があるのですが、ランタイムで生成時にBakeは行えません。
(自分はその手法を発見できなかったのでご存じの方いらっしゃいましたらご教授いただきたいです)

建物の種類すべてにBakeを行うために、適当なMonoBehaviorクラスにListなどで全Prefabを保持させて、再生時に一括Bakeさせました。

普通にGameObjectのListからGetEntityでEntityをAddしてやればいいだけかと思いきや
ArgumentException: srcEntity is not a valid entity
InvalidOperationException: Baking error: Attempt to add duplicate component
のようなエラーが出てしまい、苦労しましたが、以下の記事を参考にさせていただき、書き直したところなんとか動きました。

qiita.com

[System.Serializable]
public struct PrefabEntityComponent : IComponentData // 中身なし → Tag的扱い
{
}

public struct PrefabElement : IBufferElementData
{
    public Entity prefabEntity;
}
public class PrefabBaker : MonoBehaviour
{
    [SerializeField]
    private List<GameObject> prefabs;

    class Baker : Baker<PrefabBaker>
    {
        public override void Bake(PrefabBaker authoring)
        {
            var entity = GetEntity(TransformUsageFlags.Dynamic);
            var sample = new PrefabEntityComponent();
            var buffer = AddBuffer<PrefabElement>(entity);
            foreach (var prefab in authoring.prefabs)
            {
                buffer.Add(new PrefabElement
                {
                    prefabEntity = GetEntity(prefab, TransformUsageFlags.Dynamic)
                });
            }
            AddComponent(entity, sample);
        }
    }
}

画像のようにインスペクターからBakeしたいPrefabを指定してあげることで、そのPrefab群を自動的にBakeするようになりました。

Entityの生成

あとは以下のようなコードをクリック時に実行してあげればクリックした場所に生成されます。

if (physics.CastRay(input, out var hit))
{
    var name = this.EntityManager.GetName(hit.Entity);

    if (name == "Plane") // ここ名前でやってるのよくないので変えます
    {
        foreach (var buffer in SystemAPI.Query<DynamicBuffer<PrefabElement>>().WithAll<PrefabEntityComponent>())
        {
            for (int i = 0; i < buffer.Length; i++)
            {
                var entity = buffer[i].prefabEntity;
                if (PlayerStatusHolder.I.NowSelectConstructBuildingID == BuildingLookup[entity].BuildingID) 
                {
                    var buildingTransform = SystemAPI.GetComponentRW<LocalTransform>(entity);
                    buildingTransform.ValueRW.Position = new float3(hit.Position.x, hit.Position.y + buildingTransform.ValueRW.Scale/2, hit.Position.z); // 生成場所の決定
                    EntityManager.Instantiate(entity); // Entityの生成
                }
            }
        }
    }
}

完成

まとめ

MonoBehaviorなら簡単なこともかなり脳みそ使いますね…。
ただ、今回の実装でだいぶECSの気持ちが分かってきました。

参考

qiita.com

wgn-obs.shop-pro.jp

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

docs.unity3d.com

qiita.com

www.f-sp.com

www.f-sp.com

【ECS&DI】Unity ECSでVContainerによるDIをやってみる

はじめに

どうも、土鍋です。

これは土鍋ひとり Advent Calendar 2024の1日目の記事です。

現在、ECSでゲームを作ってみているのですが、そのプロジェクトでDIって導入できないのかなーと思い調べたところ、VContainerが対応してくれているようなので、やってみました。

DefaultWorldにおけるDI

テスト用コード

DIで扱えるようにするにはSystemBase継承でSystemを構築する必要があります。
ISystemでは。現状できないようです。(そもそもMonoBehaviorから扱えないので)

public partial class SystemA : SystemBase
{
    protected override void OnUpdate() { }

    public void TestMessage()
    {
        Debug.Log("SystemA");
    }
}
public partial class SystemB : SystemBase
{
    [Inject]
    private SystemA _systemA;
    
    protected override void OnUpdate()
    {
        if (_systemA == null) return;
        _systemA.TestMessage();
    }
}

通常時のDIと同様に[Inject]のAttributeをつけるだけでそこに流し込まれます。

LifetimeScope

LifetimeScopeもMonoBehaviorのときと同様、以下のように簡単に記述できます。

このLifetimeScopeはECS管理ではなくMonoBehaviorのGameObjectにアタッチしてください。

public class TestLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterSystemFromDefaultWorld<SystemA>();
        builder.RegisterSystemFromDefaultWorld<SystemB>();
    }
}

上記は以下のように書き換えることができます。

builder.UseDefaultWorld(systems =>
{
    systems.Add<SystemA>();
    systems.Add<SystemB>();
});

CustomWorldにおけるDI

自分で作ったWorldに関してDIを行うこともできます。

これによってWorld自体もVContainerに管理させることができるようです。

テスト用コード

public partial class SystemC : SystemBase
{
    readonly ServiceA serviceA;
    
    public SystemC(ServiceA serviceA)
    {
        this.serviceA = serviceA;
    }
    
    protected override void OnUpdate()
    {
        serviceA.TestMessage();
    }
}
public class ServiceA
{
    public void TestMessage()
    {
        Debug.Log("ServiceA");
    }
}

このような通常のクラスをSystemに流すように扱うことももちろんできる。

LifetimeScope

以下のように記述することで、Worldの生成から登録まで行っています。

builder.RegisterNewWorld("MyWorld", Lifetime.Scoped);
        
builder.RegisterSystemIntoWorld<SystemC>("MyWorld");
        
builder.Register<ServiceA>(Lifetime.Singleton);

参考

vcontainer.hadashikick.jp

hadashia.hatenablog.com

【BLE&Unity】Blutooth搭載マイコンからBLEでWindows上のUnityと通信する

はじめに

どうも、土鍋です。

今回はBluetoothデバイスとUnity間で通信を行ってみました。
ただ、UnityでのBluetoothに関してはOS固有の問題が多く、そのままのUnity上だけでBLEを完結させることはできませんでした。これに関しては様々記事を探しましたが今まで完璧にスマートな解決しているものがないので、現状はないのだと思います。
モバイル向けには以下のようなアセットがありました。

Bluetooth LE for iOS, tvOS and Android | ネットワーク | Unity Asset Store

で、今回はどのように解決したかと言いますと、

単純にPythonでBLE用のコードを動かし、UnityとローカルでTCPして送る形にしました。 まるで解決してないじゃないかではありますが、まあいったんこれでいこうということになりました。

なので、Unityで書いたのはTCP部分だけなので、この記事はほとんどnRF52とWindows間のBLEに関する解説がほとんどです。

コードは半分くらいChatGPTで、動かなかった部分を一部修正を加えて動くようにしました。

ペリフェラル

Bluetooth搭載マイコン

BLE通信にはBlutooth対応SoC「nRF52840」を搭載したマイコンを使用しました。

akizukidenshi.com

このマイコンはAdafruit nRF52 Bootloaderが組み込まれているのでArduinoIDEにて開発が可能です。

ボードマネージャーの追加

ArduinoIDEのAdafruit nRF52アドオン追加用URLが最近変更されたようなのでここにメモっときます。

https://adafruit.github.io/arduino-board-index/package_adafruit_index.json

これをPreferencesのAdditional Boards Manager URLsに追加してください。

これによってnRF52のボードに書き込めるようになります。

コード

テスト用に1秒おきに「Hello」というメッセージを飛ばすだけのコードです。

#include <Arduino.h>
#include <bluefruit.h>

// Define UUID
#define USER_SERVICE_UUID " 生成したUUID "
#define WRITE_CHARACTERISTIC_UUID " 生成したUUID "
#define NOTIFY_CHARACTERISTIC_UUID " 生成したUUID "

#define DEVICE_NAME " BLEデバイス "

// BLE Service and Characteristics
BLEService userService = BLEService(USER_SERVICE_UUID);
BLECharacteristic notifyChar = BLECharacteristic(NOTIFY_CHARACTERISTIC_UUID);

// Connection handle for notifications
uint16_t connectionHandle = BLE_CONN_HANDLE_INVALID;

void connectionCallback(uint16_t conn_handle) {
    connectionHandle = conn_handle; // Save connection handle
    Serial.println("Connected");
}

void disconnectionCallback(uint16_t conn_handle, uint8_t reason) {
    connectionHandle = BLE_CONN_HANDLE_INVALID; // Invalidate connection handle
    Serial.println("Disconnected");
    Bluefruit.Advertising.start(); // Restart advertising
}

void sendHello() {
    if (connectionHandle != BLE_CONN_HANDLE_INVALID) {
        const char* message = "Hello";
        notifyChar.notify(connectionHandle, (uint8_t*)message, strlen(message));
        Serial.println("Sent: Hello");
    }
}

void setup() {
  Serial.begin(115200);
  while ( !Serial ) delay(10);
  Serial.println("Start BLE Test");

  // Initialize serial and BLE
  Bluefruit.begin();
  Bluefruit.setTxPower(4);

  // Set BLE device name
  Bluefruit.setName(DEVICE_NAME);

  // Initialize the service and characteristics
  userService.begin();

  notifyChar.setProperties(CHR_PROPS_NOTIFY);
  notifyChar.setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS);
  notifyChar.begin();

  // Set connection and disconnection callbacks
  Bluefruit.Periph.setConnectCallback(connectionCallback);
  Bluefruit.Periph.setDisconnectCallback(disconnectionCallback);

  // Start advertising
  Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);
  Bluefruit.Advertising.addTxPower();
  Bluefruit.Advertising.addName();

  Bluefruit.Advertising.addService(userService);

  Bluefruit.Advertising.restartOnDisconnect(true);
  Bluefruit.Advertising.setInterval(32, 244);
  Bluefruit.Advertising.start();
  Bluefruit.Advertising.setFastTimeout(30);

  Serial.println("BLE device is ready and advertising!");
}

void loop() {
  static unsigned long lastSendTime = 0;
  unsigned long currentTime = millis();

  // Check if 1 second has passed
  if (currentTime - lastSendTime >= 1000) {
      lastSendTime = currentTime;
      sendHello(); // Send "Hello" notification
  }
}

解説

BLEの準備

UUIDの生成は以下のようなサイトやそれぞれの環境で生成することでゲットしてください。

uuid.doratool.com

BLEService userService = BLEService(USER_SERVICE_UUID);
BLECharacteristic writeChar = BLECharacteristic(WRITE_CHARACTERISTIC_UUID);
BLECharacteristic notifyChar = BLECharacteristic(NOTIFY_CHARACTERISTIC_UUID);

この部分でServiceとCharacteristicにUUIDを登録しています。

Characteristicの設定

userService.begin();

setProperties

setProperties(CHR_PROPS_WRITE)
CharacteristicのプロパティをWriteに設定します

setProperties(CHR_PROPS_NOTIFY)
CharacteristicのプロパティをNotifyに設定します

setPermission

setPermission(SECMODE_OPEN, SECMODE_NO_ACCESS)

1つ目の引数がReadの権限で、2つ目の引数がWriteの権限です。

callbackの設定

Bluefruit.Periph.setConnectCallback → 接続時のコールバックの設定

Bluefruit.Periph.setDisconnectCallback → 切断時のコールバックの設定

advertising

addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE);

常時、アドバタイジングパケットを発信するモードです。

セントラル

BLEデバイス見つけて、メッセージを受け取るWindows側のコードです。

Bleakのインストール

PythonのBLE通信にはBleakというライブラリを使用するのが良さそうです。 pipコマンドでインストールを行ってください。

pip install bleak

コード

Blutoothデバイスのスキャンを行って、目的のデバイスを見つけたら接続を開始します。

import asyncio
import socket
from bleak import BleakClient, BleakScanner

# BLE UUIDs
USER_SERVICE_UUID = " 生成したUUID "
NOTIFY_CHARACTERISTIC_UUID = " 生成したUUID "
DEVICE_NAME = " BLEデバイス "

# TCP settings
TCP_HOST = "127.0.0.1"  # Unity側で待機するIPアドレス (ローカルホスト)
TCP_PORT = 12345         # Unityで設定するポート番号

# Global TCP connection
tcp_client = None

# Callback for BLE notifications
def notification_handler(sender, data):
    message = data.decode("utf-8")
    print(f"Received from BLE: {message}")
    
    # Send data to Unity via TCP
    if tcp_client:
        try:
            tcp_client.sendall(message.encode("utf-8"))
            print(f"Sent to Unity: {message}")
        except Exception as e:
            print(f"Error sending data to Unity: {e}")

async def main():
    # Set up TCP connection to Unity
    global tcp_client
    try:
        tcp_client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        tcp_client.connect((TCP_HOST, TCP_PORT))
        print(f"Connected to Unity at {TCP_HOST}:{TCP_PORT}")
    except Exception as e:
        print(f"Failed to connect to Unity: {e}")
        return

    # Scan for BLE devices
    print("Scanning for BLE devices...")
    devices = await BleakScanner.discover()
    target_device = None

    for device in devices:
        print(f"Found device: {device.name} - {device.address}")
        if device.name == DEVICE_NAME:
            target_device = device
            break

    if not target_device:
        print(f"Device {DEVICE_NAME} not found. Exiting.")
        return

    print(f"Connecting to {DEVICE_NAME} ({target_device.address})...")

    async with BleakClient(target_device.address) as client:
        # Wait for services to load
        services = await client.get_services()

        # Debug: Print all services and characteristics
        print("Available services and characteristics:")
        for service in services:
            print(f"- Service: {service.uuid}")
            for char in service.characteristics:
                print(f"  - Characteristic: {char.uuid}")

        # Check if the target service and characteristic are available
        if USER_SERVICE_UUID not in [s.uuid for s in services]:
            print(f"Service {USER_SERVICE_UUID} not found on the device.")
            return

        if NOTIFY_CHARACTERISTIC_UUID not in [c.uuid for s in services for c in s.characteristics]:
            print(f"Characteristic {NOTIFY_CHARACTERISTIC_UUID} not found on the device.")
            return

        print(f"Connected to {DEVICE_NAME}. Subscribing to notifications...")

        # Set notification callback
        await client.start_notify(NOTIFY_CHARACTERISTIC_UUID, notification_handler)

        print("Listening for notifications. Press Ctrl+C to exit.")
        try:
            while True:
                await asyncio.sleep(1)
        except KeyboardInterrupt:
            print("Stopping notifications...")
            await client.stop_notify(NOTIFY_CHARACTERISTIC_UUID)
            print("Disconnected.")
        finally:
            tcp_client.close()

if __name__ == "__main__":
    asyncio.run(main())

解説

スキャン

BleakScanner.discover() でBLEデバイスのスキャンを行って、接続するデバイスを探します。

接続するServiceとCharacteristicを探す

client.get_services() でサービスを取得します。
service.characteristics でそのServiceのCharacteristicを取得できます。

Notifyを待つ

client.start_notify(UUID, 実行するメソッド) でデバイスからNotifyが送られてくるのを待ちます。

Unityでメッセージを受け取る

こちらは単純にTCPサーバーを立てているだけです

コード

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using UnityEngine;

public class TCPTest : MonoBehaviour
{
    private TcpListener server;
    private bool isRunning = true;

    void Start()
    {
        StartServer();
    }

    void OnDisable()
    {
        isRunning = false;
        server?.Stop();
    }

    void StartServer()
    {
        try
        {
            server = new TcpListener(IPAddress.Any, 12345); // ポート12345で待ち受け
            server.Start();
            Debug.Log("Server started");

            server.BeginAcceptTcpClient(OnClientConnected, null);
        }
        catch (Exception ex)
        {
            Debug.LogError($"Error starting server: {ex.Message}");
        }
    }

    void OnClientConnected(IAsyncResult result)
    {
        if (!isRunning) return;

        TcpClient client = server.EndAcceptTcpClient(result);
        Debug.Log("Client connected");

        server.BeginAcceptTcpClient(OnClientConnected, null);

        // 非同期でデータ受信を開始
        NetworkStream stream = client.GetStream();
        byte[] buffer = new byte[1024];
        stream.BeginRead(buffer, 0, buffer.Length, OnDataReceived, new Tuple<NetworkStream, byte[]>(stream, buffer));
    }

    void OnDataReceived(IAsyncResult result)
    {
        var state = (Tuple<NetworkStream, byte[]>)result.AsyncState;
        NetworkStream stream = state.Item1;
        byte[] buffer = state.Item2;

        int bytesRead = stream.EndRead(result);
        if (bytesRead > 0)
        {
            string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
            Debug.Log($"Received: {message}");

            // 再度データ受信を待機
            stream.BeginRead(buffer, 0, buffer.Length, OnDataReceived, state);
        }
        else
        {
            stream.Close();
        }
    }
}

Unityにメッセージが来た!

まとめ

うーん。本当はHololens上とかQuest上のアプリケーションでやりたいからUnityで全部書きたいんだよなぁ…

NuGet For UnityでPureC#のWindows.Devices使えるんじゃね!?って思ったんですが、だめだったので、今回の手法に落ち着きました。

どなたかUnityで完結する書き方があったらご教授いただきたいです。

参考

www.mkbtm.jp

www.mkbtm.jp

qiita.com

qiita.com

www.musen-connect.co.jp

【WebGL】WebGL(UnityRoom)でマルチプレイをする【Photon Fusion】

はじめに

どうも、土鍋です。

WebGL(UnityRoom)だと、マルチプレイゲームをどのように作るのか気になったのでやってみました。
結論から言うと、Photon FusionがデフォルトでWebGLにも対応しており、注意点を除いてほとんど詰まる所はありませんでした。

(記事としてはあっさりになってしまった…)
(Netcode for GameObjectsでやってる記事は少なかったから今度はそっちをやるかも…?)

やったこと

今回は特に特殊なことはやっていないので、非常にわかりやすい参考記事の方を参考にしてください。

コンテンツ作成

zenn.dev

おおむねこちらのチュートリアルを参考にさせていただきました。

注意点

たまにPrefabの登録に失敗しているケースがあるので、うまくマルチプレイが動かないときはNetworkPrefabInspectorを確認します。

ビルド設定

PlayerSettings > Player > Publishing Settings > Compression Format をGzipに変更してください。

UnityRoomへのアップロード

今回のバージョンはUnity6なのでUnity6000を設定

Gzipでビルドすると、Buildというフォルダ内に右のようなデータがあるのでそれを、UnityRoomの対応した場所にアップロードしてあげれば完了です。

ブラウザタブを2つ開いて同じリンクを踏んであげれば、自動的にマルチプレイが始まります。

注意点

WebGLのマルチプレイで注意が必要なのは、Editorでの実行とWebGLビルド間ではマッチングしないという点です。
そのため、WebGLビルドしてUnityRoomにアップロードして、デバッグするのが確実です。

参考

zenn.dev

qiita.com

【UI Toolkit】UI ToolkitのUI上にカーソルがあるときにRaycastをブロックする

はじめに

どうも、土鍋です。

通常のPhysics.RaycastはUIがあるかを検知してくれません。そのため、UIがあっても貫通して後ろのオブジェクトにRayが当たってしまいます。
従来のuGUIでもそうですが、uGUIはこの記事のようにEventSystemでブロックすることが可能です。

しかし、UI ToolkitはEventSystemを使用しないのでこの方法が使えません。
そこで今回はUI ToolkitでもRaycastをブロックできるようにします。

現状の状態

現状のRaycastではこのようにUI上でクリックしても貫通してその裏のオブジェクトの情報を取ってきてしまいます。

コードはこんな感じのをupdateで実行しています。

if (Input.GetMouseButtonDown(0)) //マウスがクリックされたら
{
    var ray = camera.ScreenPointToRay(Input.mousePosition);

    foreach (RaycastHit hit in Physics.RaycastAll(ray))
    {
        Debug.Log(hit.transform.name);
    }
}

実装していく

UI上にカーソルがあるかのEventCallbackを登録する

どのタイミングでブロックするかを設定したいので、UI上にカーソルが入ったときと出たときのEventCallbackを登録していきます。

private void Start()
{
    var root = GetComponent<UIDocument>().rootVisualElement;
        
    root.RegisterCallback<MouseOverEvent>(Enter);
    root.RegisterCallback<MouseOutEvent>(Exit);
}

VisualElementに対してRegisterCallbackでマウスの状態に合わせてイベントを発火するように設定しています。
ここでは

  • MouseOverEvent → マウスがUI上に入った瞬間
  • MouseOutEvent → マウスがUI上から出た瞬間

にそれぞれEnterとExitのメソッドを実行するようにしています。

ブロック状態の保持

UIの上にカーソルが来た際にブロック状態を変化させることができれば良いので、boolでそれを保持できるようにします。
今回はシングルトンでブロック状態を保持するクラスを作りました。
これを適当なクラスでnewしてあげれば、どこからでもブロックするか否かを設定・参照できます。

public class PlayerStatusHolder
{
    public static PlayerStatusHolder I;
    public bool isBlockClickRaycast;
    
    public PlayerStatusHolder()
    {
        I = this;
        isBlockClickRaycast = false;
    }
}

RegisterCallbackで設定していたメソッドの中でboolの値を変えれば、ブロック状態を変化させることができます。

private void Enter(MouseOverEvent evt)
{
    PlayerStatusHolder.I.isBlockClickRaycast = true;
}

private void Exit(MouseOutEvent evt)
{
    PlayerStatusHolder.I.isBlockClickRaycast = false;
}

Raycastをブロックする

あとはこれをRaycast処理の前で監視して、trueだったらRaycast処理を実行しないようにすれば、UIによるRaycastブロックができます。

Update()
{
    if(PlayerStatusHolder.I.isBlockClickRaycast) return;

    ~Raycast処理~
}

完成

参考

qiita.com

light11.hatenadiary.com

【UI Toolkit】デフォルトにないGridViewを実装する

はじめに

どうも、土鍋です。

UI Toolkitを使っていてuGUIにあったグリッドレイアウトができない!という問題があったので、今回は自力で実装します。

uxmlを書いて解決する記事もあったのですが、UI Builderだけで解決できました。

実装方法

UI Builder

Grid

ScrollViewのunity-content-container配下にGrid用のVisualElementを追加します。
実際の要素はこのGridVisualElement配下に配置します。

次にGridVisualElementを選択し、FlexのDirectionとWrapを画像のように変えます。
またGrowも1にしてください。
これでひとまずグリッドレイアウトっぽくなります。

コンテンツ

グリッドに配置するコンテンツのuxmlを作成します。

もし、すでにGrid以下にコンテンツを作っていたら、右クリックしてCreate Templateでuxmlを生成できます。

コンテンツの中身は画像表示だけですが、サイズを固定したかったので以下のようにしました。

Data Binding

用意されているListViewなどであれば簡単にデータソースやテンプレートを設定できるのですが、今回のものは自作しているので自力でBindしてあげる必要があります。

public class ConstructMenu : MonoBehaviour
{
    [SerializeField] 
    private VisualTreeAsset buildingElement;
    
    [SerializeField] 
    private BuildingList buildingList;
    
    private void Start()
    {
        var root = GetComponent<UIDocument>().rootVisualElement;

        foreach (var building in buildingList.buildings)
        {
            var buildingTemplate = buildingElement.Instantiate();
            Debug.Log(building.name);
            buildingTemplate.dataSource = building;
            root.Q<VisualElement>("Grid").contentContainer.Add(buildingTemplate);
        }
    }
}

コンテンツのuxmlをインスペクターからセットするにはVisualTreeAssetを使う必要があります。

これはあくまでテンプレートなので、Instantiate()で生成してGridに追加しています。

参考

https://discussions.unity.com/t/ui-toolkit-items-scrollable-grid-using-a-scrollview/899308/6

light11.hatenadiary.com