本日はHoloLens枠です。
〇HoloLensアドベントカレンダー2023とは?
HoloLensアドベントカレンダーとは毎年12月に私の師である、がちもとさんを中心に開催されているアドベントカレンダー企画になります。
〇iPhoneからHoloLensアプリの関数を実行する
1日目はDebugLogを飛ばしました。
本来であれば一方的にHoloLensからiPhoneへデータを飛ばしているだけなのでUDPをしようするほうがよさそうですが前回はTCPのプロトコルを使用していました。
せっかくなので今回はiPhoneからHoloLensの操作を行っていきます。
今回は例としてiPhone側でボタンを押したら対応するUnityEventが発火するということをやっていきます。
昨日のコードではClient側からServer側にデータを送信していました。
今回はTCPのプロトコルを採用しているため2つのデバイスの接続が確立した場合Server側のデバイスからClient側のデバイスにデータを送信することも可能です。
これを行うためにServer.cs
に次のメソッドを追加します。
public void SendDataToClient(string message) { try { if (_connectedTcpClient != null) { NetworkStream clientStream = _connectedTcpClient.GetStream(); byte[] messageBytes = Encoding.ASCII.GetBytes(message);//送信するデータをバイトに変換 // Add header and send data byte[] dataToSend = new byte[messageBytes.Length + 6]; Encoding.ASCII.GetBytes("FUN").CopyTo(dataToSend, 0); messageBytes.CopyTo(dataToSend, 3); clientStream.Write(dataToSend, 0, dataToSend.Length); Debug.Log($"Server sent message to client: {message}"); } else { Debug.LogWarning("No connected client to send data to."); } } catch (Exception e) { Debug.LogError($"Error sending data from server: {e.Message}"); } }
ここで引数に渡したString型のデータがClient側に送信される形になります。
Client側でFUNのヘッダーを検知してUnityEventsを発火させるには次のように実装します。
using UnityEngine.Events; ・・・ public UnityEvent onReceivedEvent; private void ProcessReceivedData(byte[] data, int length) { string header = Encoding.ASCII.GetString(data, 0, 3); // Only the part related to processing received data has been kept if (header == "LOG") { // Extract the log message from the received data string logMessage = Encoding.ASCII.GetString(data, 13, length - 13); } if(header == "FUN") { string functionMessage = Encoding.ASCII.GetString(data, 13, length - 13); MainThreadDispatcher.Enqueue(() => _access = true); MainThreadDispatcher.Execute(); } else { Debug.Log($"Received unknown data with header: {header}"); } }
以上でコア部分の実装が完了しました。
今回のアプローチとしてはメッセージをClientからDebugLogを送信したのと同じようにServer側からメッセージを送信し、メッセージの内容で任意の処理を実行するという感じになります。
注意点としてサブスレッドからメインスレッドの処理へアクセスすることはできません。
このため今回はbool値を変更し、Update関数で検知してメインスレッドの処理に落とし込むアプローチをとりました。(もっと良い方法ありそうですが…)
この際にはMainThredDispatcherを使用しています。
MainThreadDispatcher.Enqueue(() => _access = true);
MainThreadDispatcherクラスは今回は以下のような性的クラスを作成しています。
これは前述の通りサブスレッドからメインスレッドの処理にアクセスできず、場合によってはクラッシュしてしまうので良しなにするためのものです。
public static class MainThreadDispatcher { private static Queue<Action> _actions = new Queue<Action>(); private static readonly object _lockObject = new object(); public static void Enqueue(Action action) { lock (_lockObject) { _actions.Enqueue(action); } } public static void Execute() { lock (_lockObject) { while (_actions.Count > 0) { Action action = _actions.Dequeue(); action?.Invoke(); } } } }
lock
ステートメントはその名の通り、ロックが保持されている間はほかスレッドからブロックされ、ロックが解放されるまでの間待機します。
これによってスレッドアクセスを同期するために使用されています。
このアクセスの仕組みの影響で筆者自身が未熟であるだけなのですが何度も実機でクラッシュさせてしまいました。
〇実機で確認
前回同様iPhoneのデザリングの機能を使用して同一のネットワークに接続しています。
iPhone側でボタンを押すとHoloLens側のCubeが表示/日表示されることが確認できました。
〇コード全文
〇サーバー側
using System; using System.Collections; using System.Collections.Generic; using System.Net; using System.Net.Sockets; using System.Text; using TMPro; using UnityEngine; using System.Threading; public class Server : MonoBehaviour { private TcpListener _tcpListener; private Thread _tcpListenerThread; private TcpClient _connectedTcpClient; public TextMeshProUGUI logText; private readonly Queue<string> _logQueue = new Queue<string>(); private readonly object _lockObject = new object(); [Tooltip("Port number")] public int _port = 9991; // Default is 9991 private void Start() { _tcpListenerThread = new Thread(new ThreadStart(ListenForIncomingRequests)); _tcpListenerThread.IsBackground = true; _tcpListenerThread.Start(); // Start a coroutine to process logs from the main thread StartCoroutine(ProcessLogQueue()); } private void Update() { // Process logs in the main thread MainThreadDispatcher.Execute(); } private void ListenForIncomingRequests() { try { _tcpListener = new TcpListener(IPAddress.Any, _port); _tcpListener.Start(); Debug.Log($"Server is listening on port {_port}"); while (true) { _connectedTcpClient = _tcpListener.AcceptTcpClient(); Thread clientThread = new Thread(new ParameterizedThreadStart(HandleClientComm)); clientThread.Start(_connectedTcpClient); } } catch (Exception e) { Debug.LogError($"Error: {e.Message}"); } } private void HandleClientComm(object clientObj) { TcpClient tcpClient = (TcpClient)clientObj; NetworkStream clientStream = tcpClient.GetStream(); byte[] message = new byte[4096]; int bytesRead; while (true) { bytesRead = 0; try { bytesRead = clientStream.Read(message, 0, 4096); } catch (Exception e) { Debug.LogError($"Error reading from client: {e.Message}"); break; } if (bytesRead == 0) break; ProcessReceivedData(message, bytesRead); } tcpClient.Close(); } private void ProcessReceivedData(byte[] data, int length) { string header = Encoding.ASCII.GetString(data, 0, 3); Debug.Log($"Received header: {header}"); if (header == "LOG") { string logMessage = Encoding.ASCII.GetString(data, 3, length - 3); Debug.Log($"Received log message: {logMessage}"); // Add log message to the queue for processing in the main thread lock (_lockObject) { _logQueue.Enqueue(logMessage); } } else { Debug.Log($"Received unknown data with header: {header}"); } } public void SendDataToClient(string message) { try { if (_connectedTcpClient != null) { NetworkStream clientStream = _connectedTcpClient.GetStream(); byte[] messageBytes = Encoding.ASCII.GetBytes(message); // Add header and send data byte[] dataToSend = new byte[messageBytes.Length + 6]; Encoding.ASCII.GetBytes("FUN").CopyTo(dataToSend, 0); messageBytes.CopyTo(dataToSend, 3); clientStream.Write(dataToSend, 0, dataToSend.Length); Debug.Log($"Server sent message to client: {message}"); } else { Debug.LogWarning("No connected client to send data to."); } } catch (Exception e) { Debug.LogError($"Error sending data from server: {e.Message}"); } } private IEnumerator ProcessLogQueue() { while (true) { yield return null; // Wait for the next frame // Process logs in the main thread lock (_lockObject) { while (_logQueue.Count > 0) { string logMessage = _logQueue.Dequeue(); AppendLogText(logMessage); } } } } private void AppendLogText(string message) { // Append the new log message to the existing log text using StringBuilder StringBuilder sb = new StringBuilder(logText.text); sb.AppendLine(message); logText.text = sb.ToString(); } private void OnDestroy() { if (_tcpListener != null) { _tcpListener.Stop(); } } } // Helper class to dispatch actions to the main thread public static class MainThreadDispatcher { private static Queue<Action> _actions = new Queue<Action>(); private static readonly object _lockObject = new object(); public static void Enqueue(Action action) { lock (_lockObject) { _actions.Enqueue(action); } } public static void Execute() { lock (_lockObject) { while (_actions.Count > 0) { Action action = _actions.Dequeue(); action?.Invoke(); } } } }
〇クライアント側
using System; using System.Net.Sockets; using System.Text; using System.Threading; using JetBrains.Annotations; using UnityEditor; using UnityEngine; using UnityEngine.Events; public class Client : MonoBehaviour { [CanBeNull] private TcpClient _client; private NetworkStream _stream; [Tooltip("Port number")] public int _port = 9991; // Default is 9991 [Tooltip("Address of the server")] public string _ipAddress = "localhost"; MultiPlayEventManager multiPlayEvent; bool _access = false; public GameObject gameObject; public void StartConnection() { multiPlayEvent = this.gameObject.GetComponent<MultiPlayEventManager>(); _client = new TcpClient(_ipAddress, _port); _stream = _client.GetStream(); // Start a new thread to listen for incoming messages new Thread(() => { var responseBytes = new byte[536870912]; try { while (true) { var bytesRead = _stream.Read(responseBytes, 0, responseBytes.Length); if (bytesRead == 0) break; // Process received data based on header ProcessReceivedData(responseBytes, bytesRead); } } catch (Exception ex) { Debug.LogError($"Exception in StartConnection thread: {ex.Message}"); // Handle the exception as needed } finally { // Clean up resources if necessary _client.Close(); } }).Start(); } private void ProcessReceivedData(byte[] data, int length) { string header = Encoding.ASCII.GetString(data, 0, 3); Debug.Log("62==="+header); Debug.Log("test"); // Only the part related to processing received data has been kept if (header == "LOG") { // Extract the log message from the received data string logMessage = Encoding.ASCII.GetString(data, 13, length - 13); Debug.Log($"Received log message: {logMessage}"); } if(header == "FUN") { string functionMessage = Encoding.ASCII.GetString(data, 13, length - 13); Debug.Log("functionMessage"); MainThreadDispatcher.Enqueue(() => _access = true); MainThreadDispatcher.Execute(); } else { Debug.Log($"Received unknown data with header: {header}"); } } void Start() { StartConnection(); } private readonly object _lockObject = new object(); void Update() { if (_access) { Debug.Log("OKKKK"); lock (_lockObject) { _access = false; } gameObject.SetActive(!gameObject.activeSelf); Debug.Log("OKKKK!!!!!!!"); //MainThreadDispatcher.Enqueue(() => multiPlayEvent.isReceiveEvent = true); } } public void RequestDebugLog(string logMessage) { byte[] logBytes = Encoding.ASCII.GetBytes(logMessage); byte[] dataToSend = new byte[logBytes.Length + 13]; Encoding.ASCII.GetBytes("LOG").CopyTo(dataToSend, 0); logBytes.CopyTo(dataToSend, 3); // 修正: 3からコピーするように変更 _stream.Write(dataToSend, 0, dataToSend.Length); } private void OnDestroy() { // Unity エディターモードでのみ実行されるコード #if UNITY_EDITOR if (!EditorApplication.isPlayingOrWillChangePlaymode) { // エディターモードのクリーンアップ処理 EditorApplication.ExitPlaymode(); return; } #endif // 実行モード時のクリーンアップ処理 _stream.Close(); _client.Close(); } }