using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace KindRetargeting.Remote
{
///
/// 리타게팅 원격 제어 컨트롤러
/// HTTP 서버와 WebSocket 서버를 관리하고 메시지를 처리합니다.
///
public class RetargetingRemoteController : MonoBehaviour
{
[Header("서버 설정")]
[SerializeField] private int httpPort = 8080;
[SerializeField] private int wsPort = 8081;
[SerializeField] private bool autoStart = true;
[Header("캐릭터 등록")]
[SerializeField] private List registeredCharacters = new List();
private RetargetingHTTPServer httpServer;
private RetargetingWebSocketServer wsServer;
private Queue mainThreadActions = new Queue();
public bool IsRunning => httpServer?.IsRunning == true && wsServer?.IsRunning == true;
public int HttpPort { get => httpPort; set => httpPort = value; }
public int WsPort { get => wsPort; set => wsPort = value; }
public bool AutoStart { get => autoStart; set => autoStart = value; }
// 손 포즈 프리셋 목록
private static readonly string[] handPosePresets = new string[]
{
"가위", "바위", "보", "브이", "검지", "초기화"
};
private void Start()
{
if (autoStart)
{
StartServer();
}
}
private void OnDestroy()
{
StopServer();
}
private void Update()
{
// 메인 스레드 액션 처리
lock (mainThreadActions)
{
while (mainThreadActions.Count > 0)
{
var action = mainThreadActions.Dequeue();
try
{
action?.Invoke();
}
catch (Exception ex)
{
Debug.LogError($"[RetargetingRemote] 메인 스레드 액션 오류: {ex.Message}");
}
}
}
}
public void StartServer()
{
if (IsRunning) return;
httpServer = new RetargetingHTTPServer(httpPort, wsPort);
wsServer = new RetargetingWebSocketServer(wsPort);
wsServer.OnMessageReceived += OnWebSocketMessage;
httpServer.Start();
wsServer.Start();
Debug.Log($"[RetargetingRemote] 서버 시작됨 - HTTP: {httpPort}, WS: {wsPort}");
}
public void StopServer()
{
wsServer?.Stop();
httpServer?.Stop();
if (wsServer != null)
{
wsServer.OnMessageReceived -= OnWebSocketMessage;
}
Debug.Log("[RetargetingRemote] 서버 중지됨");
}
private void OnWebSocketMessage(string message)
{
EnqueueMainThread(() => ProcessMessage(message));
}
private void EnqueueMainThread(Action action)
{
lock (mainThreadActions)
{
mainThreadActions.Enqueue(action);
}
}
private void ProcessMessage(string message)
{
try
{
var json = JObject.Parse(message);
string action = json["action"]?.ToString();
switch (action)
{
case "refresh":
SendCharacterList();
SendHandPosePresets();
break;
case "getCharacterData":
{
string charId = json["characterId"]?.ToString();
SendCharacterData(charId);
}
break;
case "updateValueRealtime":
{
string charId = json["characterId"]?.ToString();
string property = json["property"]?.ToString();
float value = json["value"]?.Value() ?? 0f;
UpdateValue(charId, property, value);
}
break;
case "setHandPosePreset":
{
string charId = json["characterId"]?.ToString();
string property = json["property"]?.ToString();
string presetName = json["stringValue"]?.ToString();
ApplyHandPosePreset(charId, property, presetName);
}
break;
case "calibrateIPose":
{
string charId = json["characterId"]?.ToString();
CalibrateIPose(charId);
}
break;
case "resetCalibration":
{
string charId = json["characterId"]?.ToString();
ResetCalibration(charId);
}
break;
case "calibrateMingleOpen":
{
string charId = json["characterId"]?.ToString();
CalibrateMingleOpen(charId);
}
break;
case "calibrateMingleClose":
{
string charId = json["characterId"]?.ToString();
CalibrateMingleClose(charId);
}
break;
case "calibrateHeadForward":
{
string charId = json["characterId"]?.ToString();
CalibrateHeadForward(charId);
}
break;
default:
Debug.LogWarning($"[RetargetingRemote] 알 수 없는 액션: {action}");
break;
}
}
catch (Exception ex)
{
Debug.LogError($"[RetargetingRemote] 메시지 처리 오류: {ex.Message}");
}
}
private void SendCharacterList()
{
var charIds = new List();
foreach (var script in registeredCharacters)
{
if (script != null)
{
charIds.Add(script.gameObject.name);
}
}
var response = new
{
type = "characterList",
characters = charIds
};
wsServer?.Broadcast(JsonConvert.SerializeObject(response));
}
private void SendHandPosePresets()
{
var response = new
{
type = "handPosePresets",
presets = handPosePresets
};
wsServer?.Broadcast(JsonConvert.SerializeObject(response));
}
private void SendCharacterData(string characterId)
{
var script = FindCharacter(characterId);
if (script == null)
{
SendStatus(false, $"캐릭터를 찾을 수 없습니다: {characterId}");
return;
}
var limbWeight = script.GetComponent();
var handPose = script.GetComponent();
var data = new Dictionary
{
// 힙 위치 보정 (로컬)
{ "hipsVertical", GetPrivateField(script, "hipsOffsetY") },
{ "hipsForward", GetPrivateField(script, "hipsOffsetZ") },
{ "hipsHorizontal", GetPrivateField(script, "hipsOffsetX") },
// 무릎 위치 조정
{ "kneeFrontBackWeight", GetPrivateField(script, "kneeFrontBackWeight") },
{ "kneeInOutWeight", GetPrivateField(script, "kneeInOutWeight") },
// 발 IK 위치 조정
{ "feetForwardBackward", GetPrivateField(script, "footFrontBackOffset") },
{ "feetNarrow", GetPrivateField(script, "footInOutOffset") },
// 바닥 높이
{ "floorHeight", script.floorHeight },
// 아바타 크기
{ "avatarScale", GetPrivateField(script, "avatarScale") },
// 머리 크기
{ "headScale", script.GetHeadScale() },
// 머리 회전 오프셋
{ "headRotationOffsetX", script.headRotationOffsetX },
{ "headRotationOffsetY", script.headRotationOffsetY },
{ "headRotationOffsetZ", script.headRotationOffsetZ },
// 손가락 복제 모드
{ "fingerCopyMode", (int)GetPrivateField(script, "fingerCopyMode") },
// 모션 설정
{ "useMotionFilter", GetPrivateField(script, "useMotionFilter") },
{ "filterBufferSize", GetPrivateField(script, "filterBufferSize") },
{ "useBodyRoughMotion", GetPrivateField(script, "useBodyRoughMotion") },
{ "bodyRoughness", GetPrivateField(script, "bodyRoughness") },
// 캘리브레이션 상태
{ "hasCalibrationData", script.HasCachedSettings() }
};
// LimbWeightController 데이터
if (limbWeight != null)
{
data["limbMinDistance"] = limbWeight.minDistance;
data["limbMaxDistance"] = limbWeight.maxDistance;
data["weightSmoothSpeed"] = limbWeight.weightSmoothSpeed;
data["hipsMinDistance"] = limbWeight.hipsMinDistance;
data["hipsMaxDistance"] = limbWeight.hipsMaxDistance;
data["groundHipsMinHeight"] = limbWeight.groundHipsMinHeight;
data["groundHipsMaxHeight"] = limbWeight.groundHipsMaxHeight;
data["footHeightMinThreshold"] = limbWeight.footHeightMinThreshold;
data["footHeightMaxThreshold"] = limbWeight.footHeightMaxThreshold;
data["chairSeatHeightOffset"] = limbWeight.chairSeatHeightOffset;
}
// FingerShapedController 데이터
if (handPose != null)
{
data["handPoseEnabled"] = handPose.enabled;
data["leftHandEnabled"] = handPose.leftHandEnabled;
data["rightHandEnabled"] = handPose.rightHandEnabled;
}
else
{
data["handPoseEnabled"] = false;
data["leftHandEnabled"] = false;
data["rightHandEnabled"] = false;
}
var response = new
{
type = "characterData",
characterId = characterId,
data = data
};
wsServer?.Broadcast(JsonConvert.SerializeObject(response));
}
private void UpdateValue(string characterId, string property, float value)
{
var script = FindCharacter(characterId);
if (script == null) return;
var limbWeight = script.GetComponent();
var handPose = script.GetComponent();
switch (property)
{
// 힙 위치 보정
case "hipsVertical":
SetPrivateField(script, "hipsOffsetY", value);
break;
case "hipsForward":
SetPrivateField(script, "hipsOffsetZ", value);
break;
case "hipsHorizontal":
SetPrivateField(script, "hipsOffsetX", value);
break;
// 무릎 위치 조정
case "kneeFrontBackWeight":
SetPrivateField(script, "kneeFrontBackWeight", value);
break;
case "kneeInOutWeight":
SetPrivateField(script, "kneeInOutWeight", value);
break;
// 발 IK 위치 조정
case "feetForwardBackward":
SetPrivateField(script, "footFrontBackOffset", value);
break;
case "feetNarrow":
SetPrivateField(script, "footInOutOffset", value);
break;
// 바닥 높이
case "floorHeight":
script.floorHeight = value;
break;
// 아바타 크기
case "avatarScale":
SetPrivateField(script, "avatarScale", value);
break;
// 머리 크기
case "headScale":
script.SetHeadScale(value);
break;
// 머리 회전 오프셋
case "headRotationOffsetX":
script.headRotationOffsetX = value;
break;
case "headRotationOffsetY":
script.headRotationOffsetY = value;
break;
case "headRotationOffsetZ":
script.headRotationOffsetZ = value;
break;
// 손가락 복제 모드
case "fingerCopyMode":
SetPrivateField(script, "fingerCopyMode", (EnumsList.FingerCopyMode)(int)value);
break;
// 모션 설정
case "useMotionFilter":
SetPrivateField(script, "useMotionFilter", value > 0.5f);
break;
case "filterBufferSize":
SetPrivateField(script, "filterBufferSize", (int)value);
break;
case "useBodyRoughMotion":
SetPrivateField(script, "useBodyRoughMotion", value > 0.5f);
break;
case "bodyRoughness":
SetPrivateField(script, "bodyRoughness", value);
break;
// LimbWeightController 속성
case "limbMinDistance":
if (limbWeight != null) limbWeight.minDistance = value;
break;
case "limbMaxDistance":
if (limbWeight != null) limbWeight.maxDistance = value;
break;
case "weightSmoothSpeed":
if (limbWeight != null) limbWeight.weightSmoothSpeed = value;
break;
case "hipsMinDistance":
if (limbWeight != null) limbWeight.hipsMinDistance = value;
break;
case "hipsMaxDistance":
if (limbWeight != null) limbWeight.hipsMaxDistance = value;
break;
case "groundHipsMinHeight":
if (limbWeight != null) limbWeight.groundHipsMinHeight = value;
break;
case "groundHipsMaxHeight":
if (limbWeight != null) limbWeight.groundHipsMaxHeight = value;
break;
case "footHeightMinThreshold":
if (limbWeight != null) limbWeight.footHeightMinThreshold = value;
break;
case "footHeightMaxThreshold":
if (limbWeight != null) limbWeight.footHeightMaxThreshold = value;
break;
case "chairSeatHeightOffset":
if (limbWeight != null) limbWeight.chairSeatHeightOffset = value;
break;
// FingerShapedController 속성
case "handPoseEnabled":
if (handPose != null)
handPose.enabled = value > 0.5f;
break;
case "leftHandEnabled":
if (handPose != null)
{
handPose.leftHandEnabled = value > 0.5f;
if (handPose.leftHandEnabled)
handPose.enabled = true;
}
break;
case "rightHandEnabled":
if (handPose != null)
{
handPose.rightHandEnabled = value > 0.5f;
if (handPose.rightHandEnabled)
handPose.enabled = true;
}
break;
default:
Debug.LogWarning($"[RetargetingRemote] 알 수 없는 속성: {property}");
break;
}
}
private void ApplyHandPosePreset(string characterId, string property, string presetName)
{
var script = FindCharacter(characterId);
if (script == null) return;
var handPose = script.GetComponent();
if (handPose == null) return;
// 스크립트 자동 활성화
handPose.enabled = true;
// 해당 손 활성화
if (property == "leftHandPreset")
handPose.leftHandEnabled = true;
else if (property == "rightHandPreset")
handPose.rightHandEnabled = true;
// 프리셋 적용
float thumb = 0, index = 0, middle = 0, ring = 0, pinky = 0, spread = 0;
switch (presetName)
{
case "가위":
thumb = 1f; index = 1f; middle = -1f; ring = -1f; pinky = -1f; spread = 0.3f;
break;
case "바위":
thumb = -1f; index = -1f; middle = -1f; ring = -1f; pinky = -1f; spread = 0f;
break;
case "보":
thumb = 1f; index = 1f; middle = 1f; ring = 1f; pinky = 1f; spread = 1f;
break;
case "브이":
thumb = -1f; index = 1f; middle = 1f; ring = -1f; pinky = -1f; spread = 1f;
break;
case "검지":
thumb = -1f; index = 1f; middle = -1f; ring = -1f; pinky = -1f; spread = 0f;
break;
case "초기화":
thumb = 0.8f; index = 0.8f; middle = 0.8f; ring = 0.8f; pinky = 0.8f; spread = 0.8f;
break;
}
if (property == "leftHandPreset" && handPose.leftHandEnabled)
{
handPose.leftThumbCurl = thumb;
handPose.leftIndexCurl = index;
handPose.leftMiddleCurl = middle;
handPose.leftRingCurl = ring;
handPose.leftPinkyCurl = pinky;
handPose.leftSpreadFingers = spread;
}
else if (property == "rightHandPreset" && handPose.rightHandEnabled)
{
handPose.rightThumbCurl = thumb;
handPose.rightIndexCurl = index;
handPose.rightMiddleCurl = middle;
handPose.rightRingCurl = ring;
handPose.rightPinkyCurl = pinky;
handPose.rightSpreadFingers = spread;
}
SendStatus(true, $"{presetName} 프리셋 적용됨");
}
private void CalibrateIPose(string characterId)
{
var script = FindCharacter(characterId);
if (script == null) return;
script.I_PoseCalibration();
script.SaveSettings(); // 캘리브레이션 후 설정 저장
SendCharacterData(characterId);
SendStatus(true, "I-포즈 캘리브레이션 완료");
}
private void ResetCalibration(string characterId)
{
var script = FindCharacter(characterId);
if (script == null) return;
script.ResetPoseAndCache();
SendCharacterData(characterId);
SendStatus(true, "캘리브레이션 초기화됨");
}
private void CalibrateMingleOpen(string characterId)
{
var script = FindCharacter(characterId);
if (script == null) return;
script.CalibrateMingleOpen();
script.SaveSettings();
SendCharacterData(characterId);
SendStatus(true, "Mingle 펼침 캘리브레이션 완료");
}
private void CalibrateMingleClose(string characterId)
{
var script = FindCharacter(characterId);
if (script == null) return;
script.CalibrateMingleClose();
script.SaveSettings();
SendCharacterData(characterId);
SendStatus(true, "Mingle 모음 캘리브레이션 완료");
}
private void CalibrateHeadForward(string characterId)
{
var script = FindCharacter(characterId);
if (script == null) return;
script.CalibrateHeadToForward();
script.SaveSettings();
SendCharacterData(characterId);
SendStatus(true, "정면 캘리브레이션 완료");
}
private CustomRetargetingScript FindCharacter(string characterId)
{
foreach (var script in registeredCharacters)
{
if (script != null && script.gameObject.name == characterId)
{
return script;
}
}
return null;
}
private void SendStatus(bool success, string message)
{
var response = new
{
type = "status",
success = success,
message = message
};
wsServer?.Broadcast(JsonConvert.SerializeObject(response));
}
#region Reflection Helpers
private T GetPrivateField(object obj, string fieldName)
{
try
{
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
if (field != null)
{
return (T)field.GetValue(obj);
}
}
catch (Exception) { }
return default(T);
}
private void SetPrivateField(object obj, string fieldName, T value)
{
try
{
var field = obj.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public);
if (field != null)
{
field.SetValue(obj, value);
}
}
catch (Exception ex)
{
Debug.LogError($"[RetargetingRemote] 필드 설정 오류 ({fieldName}): {ex.Message}");
}
}
#endregion
#region Public API
///
/// 캐릭터 등록
///
public void RegisterCharacter(CustomRetargetingScript character)
{
if (!registeredCharacters.Contains(character))
{
registeredCharacters.Add(character);
}
}
///
/// 캐릭터 등록 해제
///
public void UnregisterCharacter(CustomRetargetingScript character)
{
registeredCharacters.Remove(character);
}
#endregion
}
}