Refactor : PropLocationController를 Serializable 모듈로 전환

- PropLocationController: MonoBehaviour → [Serializable] 클래스
- Start() → Initialize(Animator), GetComponent 제거
- CRS에서 propLocation 필드로 소유 및 초기화
- RetargetingControlWindow: GetComponent → script.propLocation 직접 접근
- PropLocationControllerEditor 삭제 (MonoBehaviour 아니므로 불필요)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
user 2026-03-07 23:03:06 +09:00
parent 52d6960710
commit 5c65185a61
5 changed files with 19 additions and 208 deletions

View File

@ -98,6 +98,9 @@ namespace KindRetargeting
[Header("어깨 보정")]
[SerializeField] public ShoulderCorrectionFunction shoulderCorrection = new ShoulderCorrectionFunction();
[Header("프랍 부착")]
[SerializeField] public PropLocationController propLocation = new PropLocationController();
[Header("아바타 크기 조정")]
[SerializeField, Range(0.1f, 3f)] private float avatarScale = 1f;
private float previousScale = 1f;
@ -325,6 +328,10 @@ namespace KindRetargeting
// 어깨 보정 모듈 초기화
if (targetAnimator != null)
shoulderCorrection.Initialize(targetAnimator);
// 프랍 부착 모듈 초기화
if (targetAnimator != null)
propLocation.Initialize(targetAnimator);
}
/// <summary>

View File

@ -1,167 +0,0 @@
using UnityEngine;
using UnityEditor;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
namespace KindRetargeting
{
[CustomEditor(typeof(PropLocationController))]
public class PropLocationControllerEditor : BaseRetargetingEditor
{
private const string CommonUssPath = "Assets/Scripts/Streamingle/StreamingleControl/Editor/UXML/StreamingleCommon.uss";
private VisualElement offsetContainer;
private VisualElement propListContainer;
private PropLocationController controller;
public override VisualElement CreateInspectorGUI()
{
controller = target as PropLocationController;
if (controller == null) return new VisualElement();
var root = new VisualElement();
var commonUss = AssetDatabase.LoadAssetAtPath<StyleSheet>(CommonUssPath);
if (commonUss != null) root.styleSheets.Add(commonUss);
// 기본 프로퍼티
var iterator = serializedObject.GetIterator();
iterator.NextVisible(true); // skip m_Script
while (iterator.NextVisible(false))
{
var field = new PropertyField(iterator.Copy());
root.Add(field);
}
// 오프셋 조정 섹션
root.Add(new Label("오프셋 조정") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
offsetContainer = new VisualElement();
root.Add(offsetContainer);
// 부착된 프랍 목록 섹션
root.Add(new Label("부착된 프랍 목록") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
propListContainer = new VisualElement();
root.Add(propListContainer);
// 프랍 위치 이동 버튼
root.Add(new Label("프랍 위치 이동") { style = { unityFontStyleAndWeight = FontStyle.Bold, marginTop = 10 } });
var btnRow = new VisualElement { style = { flexDirection = FlexDirection.Row, marginTop = 4 } };
var headBtn = new Button(() => controller.MoveToHead()) { text = "머리로 이동" };
headBtn.style.flexGrow = 1; headBtn.style.height = 30; headBtn.style.marginRight = 2;
btnRow.Add(headBtn);
var leftBtn = new Button(() => controller.MoveToLeftHand()) { text = "왼손으로 이동" };
leftBtn.style.flexGrow = 1; leftBtn.style.height = 30; leftBtn.style.marginRight = 2;
btnRow.Add(leftBtn);
var rightBtn = new Button(() => controller.MoveToRightHand()) { text = "오른손으로 이동" };
rightBtn.style.flexGrow = 1; rightBtn.style.height = 30;
btnRow.Add(rightBtn);
root.Add(btnRow);
var detachBtn = new Button(() =>
{
if (Selection.activeGameObject != null)
Undo.RecordObject(Selection.activeGameObject.transform, "Detach Prop");
controller.DetachProp();
}) { text = "프랍 해제" };
detachBtn.style.height = 30;
detachBtn.style.marginTop = 4;
root.Add(detachBtn);
// 주기적으로 동적 UI 갱신
root.schedule.Execute(() => RebuildDynamicUI()).Every(500);
RebuildDynamicUI();
return root;
}
private void RebuildDynamicUI()
{
if (controller == null) return;
RebuildOffsets();
RebuildPropLists();
}
private void RebuildOffsets()
{
if (offsetContainer == null) return;
offsetContainer.Clear();
BuildOffsetSection(offsetContainer, "왼손 오프셋", controller.GetLeftHandOffset());
BuildOffsetSection(offsetContainer, "오른손 오프셋", controller.GetRightHandOffset());
BuildOffsetSection(offsetContainer, "머리 오프셋", controller.GetHeadOffset());
}
private void BuildOffsetSection(VisualElement parent, string label, Transform offset)
{
if (offset == null) return;
var foldout = new Foldout { text = label, value = true };
var posField = new Vector3Field("위치") { value = offset.localPosition };
posField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(offset, $"Update {label}");
offset.localPosition = evt.newValue;
EditorUtility.SetDirty(offset);
});
foldout.Add(posField);
var rotField = new Vector3Field("회전") { value = offset.localRotation.eulerAngles };
rotField.RegisterValueChangedCallback(evt =>
{
Undo.RecordObject(offset, $"Update {label}");
offset.localRotation = Quaternion.Euler(evt.newValue);
EditorUtility.SetDirty(offset);
});
foldout.Add(rotField);
parent.Add(foldout);
}
private void RebuildPropLists()
{
if (propListContainer == null) return;
propListContainer.Clear();
BuildPropListSection(propListContainer, "머리 프랍", controller.GetHeadProps());
BuildPropListSection(propListContainer, "왼손 프랍", controller.GetLeftHandProps());
BuildPropListSection(propListContainer, "오른손 프랍", controller.GetRightHandProps());
}
private void BuildPropListSection(VisualElement parent, string title, GameObject[] propList)
{
var box = new VisualElement();
box.style.backgroundColor = new Color(0, 0, 0, 0.1f);
box.style.borderTopLeftRadius = box.style.borderTopRightRadius =
box.style.borderBottomLeftRadius = box.style.borderBottomRightRadius = 4;
box.style.paddingTop = box.style.paddingBottom =
box.style.paddingLeft = box.style.paddingRight = 4;
box.style.marginBottom = 4;
box.Add(new Label(title) { style = { unityFontStyleAndWeight = FontStyle.Bold } });
if (propList.Length > 0)
{
foreach (var prop in propList)
{
if (prop == null) continue;
var row = new VisualElement { style = { flexDirection = FlexDirection.Row, alignItems = Align.Center, marginTop = 2 } };
row.Add(new Label(prop.name) { style = { flexGrow = 1 } });
var selectBtn = new Button(() => Selection.activeGameObject = prop) { text = "선택" };
selectBtn.style.width = 60;
row.Add(selectBtn);
box.Add(row);
}
}
else
{
box.Add(new Label("부착된 프랍 없음") { style = { color = new Color(0.6f, 0.6f, 0.6f) } });
}
parent.Add(box);
}
}
}

View File

@ -1,11 +0,0 @@
fileFormatVersion: 2
guid: a49ee1ae55b970e4c8ca00ccae5d6f97
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -148,8 +148,7 @@ public class RetargetingControlWindow : EditorWindow
s.gameObject.AddComponent<LimbWeightController>();
if (s.GetComponent<FingerShapedController>() == null)
s.gameObject.AddComponent<FingerShapedController>();
if (s.GetComponent<PropLocationController>() == null)
s.gameObject.AddComponent<PropLocationController>();
// PropLocationController는 CRS 내부 모듈로 이동됨
EditorUtility.SetDirty(s.gameObject);
}
@ -606,19 +605,7 @@ public class RetargetingControlWindow : EditorWindow
private VisualElement BuildPropSection(CustomRetargetingScript script)
{
var foldout = new Foldout { text = "프랍 설정", value = false };
var propController = script.GetComponent<PropLocationController>();
if (propController == null)
{
foldout.Add(new HelpBox("PropLocationController가 없습니다.", HelpBoxMessageType.Warning));
var addBtn = new Button(() =>
{
script.gameObject.AddComponent<PropLocationController>();
EditorUtility.SetDirty(script.gameObject);
RebuildCharacterPanels();
}) { text = "PropLocationController 추가" };
foldout.Add(addBtn);
return foldout;
}
var propController = script.propLocation;
var dynamicContainer = new VisualElement();
foldout.Add(dynamicContainer);

View File

@ -4,7 +4,8 @@ using UniHumanoid;
namespace KindRetargeting
{
public class PropLocationController : MonoBehaviour
[System.Serializable]
public class PropLocationController
{
// 캐시된 타겟과 오프셋 Transform
[System.Serializable]
@ -18,9 +19,9 @@ namespace KindRetargeting
[SerializeField] private TargetOffset rightHandTargetOffset;
[SerializeField] private TargetOffset headTargetOffset;
private void Start()
public void Initialize(Animator animator)
{
CreateTargets();
CreateTargets(animator);
}
public void SetTPose(Animator animator)
@ -31,7 +32,6 @@ namespace KindRetargeting
Avatar avatar = animator.avatar;
Transform transform = animator.transform;
// HumanPoseClip에 저장된 T-포즈 데이터를 로드하여 적용
var humanPoseClip = Resources.Load<HumanPoseClip>(HumanPoseClip.TPoseResourcePath);
if (humanPoseClip != null)
{
@ -43,9 +43,9 @@ namespace KindRetargeting
Debug.LogWarning("T-Pose 데이터가 존재하지 않습니다.");
}
}
private void CreateTargets()
private void CreateTargets(Animator animator)
{
Animator animator = GetComponent<Animator>();
SetTPose(animator);
// 왼손 타겟 및 오프셋 설정
@ -54,7 +54,7 @@ namespace KindRetargeting
{
leftHandTargetOffset = new TargetOffset();
GameObject leftTarget = new GameObject("Left_Hand_Target");
leftTarget.transform.parent = leftHandBone; // 왼손 본에 직접 부모 설정
leftTarget.transform.parent = leftHandBone;
leftHandTargetOffset.target = leftTarget.transform;
leftTarget.transform.position = leftHandBone.position + new Vector3(-0.039f, -0.022f, 0f);
leftTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
@ -63,7 +63,6 @@ namespace KindRetargeting
leftOffset.transform.parent = leftTarget.transform;
leftHandTargetOffset.offset = leftOffset.transform;
// 로컬 포지션과 로테이션 설정
leftHandTargetOffset.offset.localPosition = Vector3.zero;
leftHandTargetOffset.offset.localRotation = Quaternion.identity;
}
@ -74,7 +73,7 @@ namespace KindRetargeting
{
rightHandTargetOffset = new TargetOffset();
GameObject rightTarget = new GameObject("Right_Hand_Target");
rightTarget.transform.parent = rightHandBone; // 오른손 본에 직접 부모 설정
rightTarget.transform.parent = rightHandBone;
rightHandTargetOffset.target = rightTarget.transform;
rightTarget.transform.position = rightHandBone.position + new Vector3(0.039f, -0.022f, 0f);
rightTarget.transform.rotation = Quaternion.Euler(90f, 0f, 0f);
@ -83,7 +82,6 @@ namespace KindRetargeting
rightOffset.transform.parent = rightTarget.transform;
rightHandTargetOffset.offset = rightOffset.transform;
// 로컬 포지션과 로테이션 설정
rightHandTargetOffset.offset.localPosition = Vector3.zero;
rightHandTargetOffset.offset.localRotation = Quaternion.identity;
}
@ -94,7 +92,7 @@ namespace KindRetargeting
{
headTargetOffset = new TargetOffset();
GameObject headTarget = new GameObject("Head_Target");
headTarget.transform.parent = headBone; // 머리 본에 직접 부모 설정
headTarget.transform.parent = headBone;
headTargetOffset.target = headTarget.transform;
headTarget.transform.position = headBone.position + new Vector3(0f, 0.16f, 0f);
headTarget.transform.rotation = Quaternion.Euler(0f, 0f, 0f);
@ -103,7 +101,6 @@ namespace KindRetargeting
headOffset.transform.parent = headTarget.transform;
headTargetOffset.offset = headOffset.transform;
// 기본 오프셋 설정
headTargetOffset.offset.localPosition = Vector3.zero;
headTargetOffset.offset.localRotation = Quaternion.identity;
}
@ -172,7 +169,6 @@ namespace KindRetargeting
}
}
// 에디터에서 사용할 메서드들
#if UNITY_EDITOR
public void MoveToHead()
{
@ -195,7 +191,6 @@ namespace KindRetargeting
}
#endif
// 오프셋 getter 메서드들 추가
public Transform GetLeftHandOffset()
{
return leftHandTargetOffset?.offset;
@ -239,4 +234,4 @@ namespace KindRetargeting
return children.ToArray();
}
}
}
}