Change Parts3

첫번째,  Change Parts에서는 리소스 설정 및 복장을 착용하는 간단한 코드,
두번째, Change Parts2에서는 enum을 하나로 합치되, 카테고리 필드를 추가해서 착용형인지 메쉬형인지 구분하는 방식입니다.

이번에 사용할 방식은, 인터페이스/추상화 활용 방식입니다.
PartCategory를 사용하지 않고, 인터페이스 기반으로 공통 처리하는 방법입니다.

실행화면입니다.






주요 클래스의 특징은 다음과 같습니다.

IEquippablePart:
장착 가능한 아이템의 추상 인터페이스입니다. Equip() 메서드를 통해 CharacterCustomizer에게 자신을 어떻게 장착할지 명령합니다.

PartData(Abstract):
모든 파츠의 기본 데이터를 담는 ScriptableObject입니다. UI용 이름과 아이콘을 가집니다.

MeshPartData:
단순 메쉬 교체형(귀, 머리 등) 파츠입니다. sharedMesh를 교체하는 로직을 가집니다.

WearablePartData:
의상형(상체, 하체 등) 파츠입니다. 프리팹을 생성하고 본(Bone) 정보를 동기화하는 로직을 가집니다.

CharacterCustomizer:
실제 캐릭터의 SkinnedMeshRenderer들을 관리하고, 실질적인 메쉬 할당 및 인스턴스화를 수행하는 Context 역할입니다.

이 구조는 새로운 장착 타입(예: 이펙트 파츠, 무기 등)이 필요하면 IEquippablePart를 상속받는 클래스만 추가하면 CharacterCustomizer의 기존 코드를 거의 수정하지 않고 확장할 수 있습니다.


ScriptableObject [CreateAssetMenu(menuName = "Character/Part/Mesh")]로 ear 생성


ScriptableObject [CreateAssetMenu(menuName = "Character/Part/Wearable")]로 upper 생성


PartType.cs
public enum PartType
{
    // Wearable
    Upper,
    Pants,
    Gloves,
    Shoes,

    // Mesh
    Ear,
    Hair,
    Face,
    Tail
}

PartData.cs
using UnityEngine;

public abstract class PartData : ScriptableObject
{
    [Header("Common Info")]
    public string partName;   // 파츠 이름
    public Sprite icon;       // UI 아이콘
}

IEquippablePart.cs
public interface IEquippablePart
{
    PartType Type { get; }
    void Equip(CharacterCustomizer customizer);
}

MeshPartData.cs
using UnityEngine;

[CreateAssetMenu(menuName = "Character/Part/Mesh")]
public class MeshPartData : PartData, IEquippablePart
{
    public Mesh mesh;
    public PartType type; // Ear, Hair, Face, Tail 등

    public PartType Type => type;

    public void Equip(CharacterCustomizer customizer)
    {
        customizer.EquipMesh(this);
    }
}

WearablePartData.cs
using UnityEngine;

[CreateAssetMenu(menuName = "Character/Part/Wearable")]
public class WearablePartData : PartData, IEquippablePart
{
    public SkinnedMeshRenderer prefab;
    // Upper, Pants, Gloves, Shoes 등
    public PartType type; // 인스펙터에서 지정

    public PartType Type => type; // 인터페이스 구현

    public void Equip(CharacterCustomizer customizer)
    {
        customizer.EquipWearable(this);
    }
}

CharacterCustomizer.cs
using System.Collections.Generic;
using UnityEngine;

public class CharacterCustomizer : MonoBehaviour
{
    [Header("Base Character")]
    [SerializeField] private SkinnedMeshRenderer baseMeshRenderer;

    [Header("Mesh Swap Renderers")]
    [SerializeField] private SkinnedMeshRenderer earRenderer;
    [SerializeField] private SkinnedMeshRenderer hairRenderer;
    [SerializeField] private SkinnedMeshRenderer faceRenderer;
    [SerializeField] private SkinnedMeshRenderer tailRenderer;

    // 현재 장착된 착용형 파츠 관리
    private Dictionary<PartType, SkinnedMeshRenderer> equippedWearables = new();

    // 공통 Equip 메서드
    public void Equip(IEquippablePart part)
    {
        if (part == null)
            return;
        part.Equip(this);
    }

    #region 내부 장착 로직 (Wearable / Mesh)

    // 착용형 파츠 장착
    public void EquipWearable(WearablePartData partData)
    {
        if (partData == null || partData.prefab == null)
            return;

        if (equippedWearables.TryGetValue(partData.Type, out var oldPart))
        {
            Destroy(oldPart.gameObject);
        }

        var newPart = Instantiate(partData.prefab, transform);
        newPart.rootBone = baseMeshRenderer.rootBone;
        newPart.bones = baseMeshRenderer.bones;

        equippedWearables[partData.Type] = newPart;
    }

    // 메쉬 교체형 파츠 장착
    public void EquipMesh(MeshPartData partData)
    {
        if (partData == null || partData.mesh == null)
            return;

        SkinnedMeshRenderer targetRenderer = partData.Type switch
        {
            PartType.Ear => earRenderer,
            PartType.Hair => hairRenderer,
            PartType.Face => faceRenderer,
            PartType.Tail => tailRenderer,
            _ => null
        };

        if (targetRenderer != null)
        {
            targetRenderer.sharedMesh = partData.mesh;
        }
    }

    #endregion
}

UIEquipButton.cs
using UnityEngine;
using UnityEngine.UI;

public class UIEquipButton : MonoBehaviour
{
    [Header("Target Customizer")]
    [SerializeField] private CharacterCustomizer customizer;

    [Header("Part to Equip: wear")]
    [SerializeField] private PartData upperPartData;

    [Header("Part to Equip: attach")]
    [SerializeField] private PartData earPartData;

    [Header("UI Button")]
    [SerializeField] private Button equipButton;

    private void Awake()
    {
        if (equipButton != null)
        {
            equipButton.onClick.AddListener(OnEquipClicked);
        }
    }

    private void OnEquipClicked()
    {
        IEquippablePart equippablePart = upperPartData as IEquippablePart;
        if (customizer != null && equippablePart != null)
            customizer.Equip(equippablePart);

        equippablePart = earPartData as IEquippablePart;
        if (customizer != null && equippablePart != null)
            customizer.Equip(equippablePart);
    }
}

소스:
PartType.cs
PartData.cs
IEquippablePart.cs
MeshPartData.cs
WearablePartData.cs
CharacterCustomizer.cs
UIEquipButton.cs