diff --git a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs index 3dd0acdbd..0b6b6a4e6 100644 --- a/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs +++ b/Assets/External/OptiTrack Unity Plugin/OptiTrack/Scripts/OptitrackStreamingClient.cs @@ -349,7 +349,7 @@ public class OptitrackStreamingClient : MonoBehaviour [Tooltip("Skips getting data descriptions. Skeletons will not work with this feature turned on, but it will reduce network usage with a large number of rigid bodies.")] public bool SkipDataDescriptions = false; - [Tooltip("스트리밍이 끊어졌을 때 최대 5회까지 자동으로 재연결을 시도합니다.")] + [Tooltip("초기 연결 실패 시 최대 10회 백오프 재시도, 스트리밍이 끊어졌을 때도 최대 5회까지 자동으로 재연결을 시도합니다.")] public bool AutoReconnect = true; [Tooltip("Changes to the version of Natnet used by the server")] @@ -1559,9 +1559,12 @@ public class OptitrackStreamingClient : MonoBehaviour // dev.ChannelNames is a fixed-size 32 array (NatNet C# wrapper limit). // Plugin can register devices with > 32 channels (Motive stores them all), // but only the first 32 names survive the wrapper marshaling. - // Clamp ChannelCount to the marshaled name array's actual length to avoid - // IndexOutOfRangeException on the loop below. - int actualNameCount = (dev.ChannelNames != null) + // IMPORTANT: ChannelCount is preserved as the full count reported by Motive + // (could be > 32, e.g. iFacialMocap primary = 54). Downstream consumers + // need this to match the wire frame's nCh (which may also be > 32 if Motive + // doesn't truncate broadcast). ChannelNames is clamped to the marshaled + // array length to avoid IndexOutOfRangeException. + int marshalNameCount = (dev.ChannelNames != null) ? Math.Min(dev.ChannelCount, dev.ChannelNames.Length) : 0; @@ -1572,11 +1575,11 @@ public class OptitrackStreamingClient : MonoBehaviour SerialNumber = dev.SerialNo, DeviceType = dev.DeviceType, ChannelDataType = dev.ChannelDataType, - ChannelCount = actualNameCount, - ChannelNames = new List(actualNameCount), + ChannelCount = dev.ChannelCount, + ChannelNames = new List(marshalNameCount), }; - for (int i = 0; i < actualNameCount; ++i) + for (int i = 0; i < marshalNameCount; ++i) { deviceDef.ChannelNames.Add(dev.ChannelNames[i]); } @@ -1643,7 +1646,81 @@ public class OptitrackStreamingClient : MonoBehaviour void OnEnable() { m_receivedFrameSinceConnect = false; - StartCoroutine( ConnectCoroutine() ); + // AutoReconnect가 켜져있으면 초기 연결 실패 시에도 자동 재시도 + if (AutoReconnect) + StartCoroutine( InitialConnectWithRetry() ); + else + StartCoroutine( ConnectCoroutine() ); + } + + /// + /// 초기 연결을 백오프와 함께 자동 재시도합니다. + /// 처리하는 케이스: + /// A) NatNet_Client_Connect 자체가 throw (NatNetError_Network 등) — Motive PC 미응답, 방화벽, 잘못된 IP 등 + /// B) Connect는 성공했으나 첫 프레임이 영영 안 옴 — Motive Streaming 미활성화, Multicast 라우팅 차단 등 + /// 케이스 C(연결 후 프레임 끊김)는 기존 CheckConnectionHealth+ReconnectCoroutine에서 처리됨. + /// + private System.Collections.IEnumerator InitialConnectWithRetry() + { + const int maxAttempts = 10; + // 백오프: 1, 2, 3, 5, 10, 10, ... 초 (총 약 60초간 시도) + float[] retryDelays = { 1f, 2f, 3f, 5f, 10f }; + + for (int attempt = 1; attempt <= maxAttempts; attempt++) + { + // ConnectCoroutine 실행 + yield return StartCoroutine(ConnectCoroutine()); + + // 케이스 A: m_client가 null이면 Connect() 자체가 실패한 것 + if (m_client != null) + { + // 케이스 B 확인: Connect는 성공 — 첫 프레임 도착까지 5초 대기 + float deadline = Time.realtimeSinceStartup + 5.0f; + while (!m_receivedFrameSinceConnect && Time.realtimeSinceStartup < deadline) + yield return null; + + if (m_receivedFrameSinceConnect) + { + if (attempt > 1) + Debug.Log(string.Format("{0}: 초기 연결 성공 (시도 {1}/{2})", GetType().FullName, attempt, maxAttempts), this); + yield break; // 성공 + } + + // 케이스 B: 연결됐지만 프레임 미수신 — 클라이언트 정리 후 재시도 + Debug.LogWarning(string.Format("{0}: 시도 {1}/{2} — 연결됐으나 프레임 미수신. 정리 후 재시도.", GetType().FullName, attempt, maxAttempts), this); + CleanupClient(); + } + + if (attempt == maxAttempts) + { + Debug.LogError(string.Format("{0}: {1}회 초기 연결 시도 모두 실패. 인스펙터에서 'Reconnect' 또는 컴포넌트 토글로 수동 재시도하세요.", GetType().FullName, maxAttempts), this); + yield break; + } + + float delay = retryDelays[Mathf.Min(attempt - 1, retryDelays.Length - 1)]; + Debug.Log(string.Format("{0}: 초기 연결 시도 {1}/{2} 실패 — {3}초 후 재시도", GetType().FullName, attempt, maxAttempts, delay), this); + yield return new WaitForSeconds(delay); + } + } + + /// + /// m_client와 관련 코루틴/이벤트를 안전하게 정리합니다. + /// InitialConnectWithRetry의 케이스 B 재시도 시 사용. + /// + private void CleanupClient() + { + if (m_connectionHealthCoroutine != null) + { + StopCoroutine(m_connectionHealthCoroutine); + m_connectionHealthCoroutine = null; + } + if (m_client != null) + { + try { m_client.NativeFrameReceived -= OnNatNetFrameReceived; } catch (Exception) { } + try { m_client.Disconnect(); } catch (Exception) { } + try { m_client.Dispose(); } catch (Exception) { } + m_client = null; + } } private System.Collections.IEnumerator ConnectCoroutine()