Fix: 초기 연결 실패 시에도 자동 재시도 (간헐적 NatNetError_Network 대응)

기존 AutoReconnect는 첫 프레임 수신 후 끊긴 경우(케이스 C)에만 작동했음.
이제 케이스 A(Connect 자체 throw)와 케이스 B(Connect 성공했으나 첫 프레임 미수신)도
백오프 재시도로 자동 복구함.

- InitialConnectWithRetry 코루틴: 최대 10회, 1→2→3→5→10초 백오프
- CleanupClient 헬퍼: 케이스 B 재시도 시 m_client 안전 정리
- AutoReconnect=true이면 OnEnable이 InitialConnectWithRetry로 분기
- AutoReconnect=false이면 기존 동작 유지 (1회 시도, 실패 시 종료)

효과: Motive 소켓 바인딩 지연, Unity가 Motive보다 먼저 켜짐, 짧은 네트워크
블립 등 일시적 원인의 간헐적 연결 실패가 사용자 개입 없이 자동 복구됨.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
qsxft258@gmail.com 2026-05-12 22:37:49 +09:00
parent 3b642416af
commit f5b6690aee

View File

@ -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.")] [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; public bool SkipDataDescriptions = false;
[Tooltip("스트리밍이 끊어졌을 때 최대 5회까지 자동으로 재연결을 시도합니다.")] [Tooltip("초기 연결 실패 시 최대 10회 백오프 재시도, 스트리밍이 끊어졌을 때 최대 5회까지 자동으로 재연결을 시도합니다.")]
public bool AutoReconnect = true; public bool AutoReconnect = true;
[Tooltip("Changes to the version of Natnet used by the server")] [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). // dev.ChannelNames is a fixed-size 32 array (NatNet C# wrapper limit).
// Plugin can register devices with > 32 channels (Motive stores them all), // Plugin can register devices with > 32 channels (Motive stores them all),
// but only the first 32 names survive the wrapper marshaling. // but only the first 32 names survive the wrapper marshaling.
// Clamp ChannelCount to the marshaled name array's actual length to avoid // IMPORTANT: ChannelCount is preserved as the full count reported by Motive
// IndexOutOfRangeException on the loop below. // (could be > 32, e.g. iFacialMocap primary = 54). Downstream consumers
int actualNameCount = (dev.ChannelNames != null) // 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) ? Math.Min(dev.ChannelCount, dev.ChannelNames.Length)
: 0; : 0;
@ -1572,11 +1575,11 @@ public class OptitrackStreamingClient : MonoBehaviour
SerialNumber = dev.SerialNo, SerialNumber = dev.SerialNo,
DeviceType = dev.DeviceType, DeviceType = dev.DeviceType,
ChannelDataType = dev.ChannelDataType, ChannelDataType = dev.ChannelDataType,
ChannelCount = actualNameCount, ChannelCount = dev.ChannelCount,
ChannelNames = new List<string>(actualNameCount), ChannelNames = new List<string>(marshalNameCount),
}; };
for (int i = 0; i < actualNameCount; ++i) for (int i = 0; i < marshalNameCount; ++i)
{ {
deviceDef.ChannelNames.Add(dev.ChannelNames[i]); deviceDef.ChannelNames.Add(dev.ChannelNames[i]);
} }
@ -1643,7 +1646,81 @@ public class OptitrackStreamingClient : MonoBehaviour
void OnEnable() void OnEnable()
{ {
m_receivedFrameSinceConnect = false; m_receivedFrameSinceConnect = false;
StartCoroutine( ConnectCoroutine() ); // AutoReconnect가 켜져있으면 초기 연결 실패 시에도 자동 재시도
if (AutoReconnect)
StartCoroutine( InitialConnectWithRetry() );
else
StartCoroutine( ConnectCoroutine() );
}
/// <summary>
/// 초기 연결을 백오프와 함께 자동 재시도합니다.
/// 처리하는 케이스:
/// A) NatNet_Client_Connect 자체가 throw (NatNetError_Network 등) — Motive PC 미응답, 방화벽, 잘못된 IP 등
/// B) Connect는 성공했으나 첫 프레임이 영영 안 옴 — Motive Streaming 미활성화, Multicast 라우팅 차단 등
/// 케이스 C(연결 후 프레임 끊김)는 기존 CheckConnectionHealth+ReconnectCoroutine에서 처리됨.
/// </summary>
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);
}
}
/// <summary>
/// m_client와 관련 코루틴/이벤트를 안전하게 정리합니다.
/// InitialConnectWithRetry의 케이스 B 재시도 시 사용.
/// </summary>
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() private System.Collections.IEnumerator ConnectCoroutine()