606 lines
22 KiB
C#
606 lines
22 KiB
C#
using UnityEngine;
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Streamingle;
|
|
using Newtonsoft.Json;
|
|
using NiloToon.NiloToonURP;
|
|
using Streamingle.Effects;
|
|
#if MAGICACLOTH2
|
|
using MagicaCloth2;
|
|
#endif
|
|
|
|
public class AvatarOutfitController : MonoBehaviour, IController
|
|
{
|
|
#region Classes
|
|
[System.Serializable]
|
|
public class AvatarData
|
|
{
|
|
[Header("Avatar Settings")]
|
|
public string avatarName = "New Avatar";
|
|
|
|
[Header("Outfit Settings")]
|
|
public OutfitData[] outfits = new OutfitData[0];
|
|
|
|
[SerializeField] private int currentOutfitIndex = 0;
|
|
|
|
[System.NonSerialized] public bool isTransforming;
|
|
|
|
public int CurrentOutfitIndex => currentOutfitIndex;
|
|
public OutfitData CurrentOutfit => outfits != null && outfits.Length > currentOutfitIndex ? outfits[currentOutfitIndex] : null;
|
|
|
|
public AvatarData(string name)
|
|
{
|
|
avatarName = name;
|
|
outfits = new OutfitData[0];
|
|
}
|
|
|
|
public void SetOutfit(int outfitIndex)
|
|
{
|
|
if (outfits == null || outfitIndex < 0 || outfitIndex >= outfits.Length)
|
|
{
|
|
Debug.LogWarning($"[AvatarData] 잘못된 의상 인덱스: {outfitIndex}");
|
|
return;
|
|
}
|
|
|
|
if (CurrentOutfit != null)
|
|
CurrentOutfit.RemoveOutfit();
|
|
|
|
currentOutfitIndex = outfitIndex;
|
|
|
|
if (CurrentOutfit != null)
|
|
{
|
|
CurrentOutfit.ApplyOutfit();
|
|
Debug.Log($"[AvatarData] {avatarName} 의상 변경: {CurrentOutfit.outfitName}");
|
|
}
|
|
}
|
|
|
|
public override string ToString() => avatarName;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class TransformEffectSettings
|
|
{
|
|
[Tooltip("변신 연출 사용 여부. 끄면 VFX/SFX/컬러 플래시 없이 즉시 교체만 수행.")]
|
|
public bool enabled = true;
|
|
|
|
[Tooltip("공유 프리셋 (VFX 프리팹, SFX 클립, 컬러 플래시 파라미터). Assets > Create > Streamingle > Transform Effect Preset.")]
|
|
public TransformEffectPreset preset;
|
|
|
|
[Header("Scene References (프리셋과 별도)")]
|
|
[Tooltip("VFX 스폰 기준 Transform. 비우면 새 의상의 첫 clothingObject 위치 사용.")]
|
|
public Transform vfxAnchor;
|
|
|
|
[Tooltip("SFX 재생용 AudioSource. 비어있거나 프리셋에 클립이 없으면 SFX 생략.")]
|
|
public AudioSource audioSource;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class OutfitData
|
|
{
|
|
[Header("Outfit Info")]
|
|
public string outfitName = "New Outfit";
|
|
|
|
[Header("Clothing GameObjects")]
|
|
[Tooltip("이 의상을 입을 때 표시할 오브젝트들. NiloToonPerCharacterRenderController 가 붙어있으면 renderCharacter=true 로 토글, 없으면 SetActive(true).")]
|
|
public GameObject[] clothingObjects = new GameObject[0];
|
|
|
|
[Tooltip("이 의상을 입을 때 숨길 오브젝트들. NiloToon 있으면 renderCharacter=false, 없으면 SetActive(false).")]
|
|
public GameObject[] hideObjects = new GameObject[0];
|
|
|
|
[Header("Transform Effect (이 의상으로 변경할 때 재생)")]
|
|
public TransformEffectSettings transformEffect = new TransformEffectSettings();
|
|
|
|
// NiloToon renderCharacter 경로는 GameObject 를 Active 로 유지하기 때문에
|
|
// 하위 MagicaCloth / SkinnedMeshRenderer 가 계속 시뮬+스키닝을 돈다.
|
|
// 1회 수집 후 Apply/Remove 에서 .enabled 로 함께 토글.
|
|
[NonSerialized] private SkinnedMeshRenderer[][] _clothingSmrCache;
|
|
[NonSerialized] private SkinnedMeshRenderer[][] _hideSmrCache;
|
|
#if MAGICACLOTH2
|
|
[NonSerialized] private MagicaCloth[][] _clothingClothCache;
|
|
[NonSerialized] private MagicaCloth[][] _hideClothCache;
|
|
#endif
|
|
[NonSerialized] private bool _simCacheBuilt;
|
|
|
|
public void BuildSimulationCache()
|
|
{
|
|
if (_simCacheBuilt) return;
|
|
_clothingSmrCache = CollectChildren<SkinnedMeshRenderer>(clothingObjects);
|
|
_hideSmrCache = CollectChildren<SkinnedMeshRenderer>(hideObjects);
|
|
#if MAGICACLOTH2
|
|
_clothingClothCache = CollectChildren<MagicaCloth>(clothingObjects);
|
|
_hideClothCache = CollectChildren<MagicaCloth>(hideObjects);
|
|
#endif
|
|
_simCacheBuilt = true;
|
|
}
|
|
|
|
public void ApplyOutfit()
|
|
{
|
|
LogToggle("Apply", clothingObjects, true, hideObjects, false);
|
|
ApplyArrayState(clothingObjects, true, clothingSide: true);
|
|
ApplyArrayState(hideObjects, false, clothingSide: false);
|
|
}
|
|
|
|
public void RemoveOutfit()
|
|
{
|
|
LogToggle("Remove", clothingObjects, false, hideObjects, true);
|
|
ApplyArrayState(clothingObjects, false, clothingSide: true);
|
|
ApplyArrayState(hideObjects, true, clothingSide: false);
|
|
}
|
|
|
|
void ApplyArrayState(GameObject[] objs, bool value, bool clothingSide)
|
|
{
|
|
if (objs == null) return;
|
|
var smrCache = clothingSide ? _clothingSmrCache : _hideSmrCache;
|
|
#if MAGICACLOTH2
|
|
var clothCache = clothingSide ? _clothingClothCache : _hideClothCache;
|
|
#endif
|
|
for (int i = 0; i < objs.Length; i++)
|
|
{
|
|
SetRenderOrActive(objs[i], value);
|
|
if (smrCache != null && i < smrCache.Length)
|
|
SetRenderersEnabled(smrCache[i], value);
|
|
#if MAGICACLOTH2
|
|
if (clothCache != null && i < clothCache.Length)
|
|
SetBehavioursEnabled(clothCache[i], value);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
void LogToggle(string op, GameObject[] a, bool av, GameObject[] b, bool bv)
|
|
{
|
|
var aNames = a == null || a.Length == 0 ? "∅" : string.Join(",", a.Select(o => o != null ? o.name : "null"));
|
|
var bNames = b == null || b.Length == 0 ? "∅" : string.Join(",", b.Select(o => o != null ? o.name : "null"));
|
|
Debug.Log($"[Outfit:{outfitName}] {op} — clothing→{av}: [{aNames}], hide→{bv}: [{bNames}]");
|
|
}
|
|
|
|
/// <summary>clothingObjects 중 첫 번째 NiloToonPerCharacterRenderController. 컬러 플래시 / VFX 앵커 fallback 용.</summary>
|
|
public NiloToonPerCharacterRenderController GetPrimaryRenderController()
|
|
{
|
|
if (clothingObjects == null) return null;
|
|
foreach (var obj in clothingObjects)
|
|
{
|
|
if (obj == null) continue;
|
|
var rc = obj.GetComponent<NiloToonPerCharacterRenderController>();
|
|
if (rc != null) return rc;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static void SetRenderOrActive(GameObject obj, bool value)
|
|
{
|
|
if (obj == null) return;
|
|
var rc = obj.GetComponent<NiloToonPerCharacterRenderController>();
|
|
if (rc != null)
|
|
rc.renderCharacter = value;
|
|
else
|
|
obj.SetActive(value);
|
|
}
|
|
|
|
static T[][] CollectChildren<T>(GameObject[] objs) where T : Component
|
|
{
|
|
if (objs == null) return Array.Empty<T[]>();
|
|
var arr = new T[objs.Length][];
|
|
for (int i = 0; i < objs.Length; i++)
|
|
arr[i] = objs[i] != null
|
|
? objs[i].GetComponentsInChildren<T>(true)
|
|
: Array.Empty<T>();
|
|
return arr;
|
|
}
|
|
|
|
static void SetBehavioursEnabled<T>(T[] behaviours, bool value) where T : Behaviour
|
|
{
|
|
if (behaviours == null) return;
|
|
foreach (var b in behaviours)
|
|
{
|
|
if (b != null && b.enabled != value)
|
|
b.enabled = value;
|
|
}
|
|
}
|
|
|
|
static void SetRenderersEnabled<T>(T[] renderers, bool value) where T : Renderer
|
|
{
|
|
if (renderers == null) return;
|
|
foreach (var r in renderers)
|
|
{
|
|
if (r != null && r.enabled != value)
|
|
r.enabled = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Events
|
|
public delegate void AvatarOutfitChangedEventHandler(AvatarData avatar, OutfitData oldOutfit, OutfitData newOutfit);
|
|
public event AvatarOutfitChangedEventHandler OnAvatarOutfitChanged;
|
|
|
|
/// <summary>UI 등 상태 구독자용 통합 이벤트. 의상 스왑이 실제로 완료된 시점에 발생 (변신 연출 중에는 아직 발생하지 않음).</summary>
|
|
public event System.Action OnStateChanged;
|
|
#endregion
|
|
|
|
#region Fields
|
|
[SerializeField] public List<AvatarData> avatars = new List<AvatarData>();
|
|
|
|
[Header("Avatar Control Settings")]
|
|
[SerializeField] private bool autoFindAvatars = false;
|
|
[SerializeField] private string avatarTag = "Avatar";
|
|
|
|
[Tooltip("Awake 시 각 아바타의 currentOutfit 을 적용해 초기 표시 상태를 정렬.")]
|
|
[SerializeField] private bool syncOutfitStateOnAwake = true;
|
|
|
|
private AvatarData currentAvatar;
|
|
private StreamDeckServerManager streamDeckManager;
|
|
#endregion
|
|
|
|
#region Properties
|
|
public AvatarData CurrentAvatar => currentAvatar;
|
|
public int CurrentAvatarIndex => avatars.IndexOf(currentAvatar);
|
|
public bool IsAnyTransforming => avatars.Any(a => a != null && a.isTransforming);
|
|
public bool IsAvatarTransforming(int avatarIndex) =>
|
|
avatarIndex >= 0 && avatarIndex < avatars.Count && avatars[avatarIndex] != null && avatars[avatarIndex].isTransforming;
|
|
#endregion
|
|
|
|
#region Unity Messages
|
|
private void Awake()
|
|
{
|
|
InitializeAvatars();
|
|
|
|
streamDeckManager = FindObjectOfType<StreamDeckServerManager>();
|
|
if (streamDeckManager == null)
|
|
{
|
|
Debug.LogWarning("[AvatarOutfitController] StreamDeckServerManager를 찾을 수 없습니다. 스트림덱 연동이 비활성화됩니다.");
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Initialization
|
|
public void InitializeAvatars()
|
|
{
|
|
if (avatars == null)
|
|
avatars = new List<AvatarData>();
|
|
|
|
if (autoFindAvatars && avatars.Count == 0)
|
|
{
|
|
var avatarObjects = GameObject.FindGameObjectsWithTag(avatarTag);
|
|
foreach (var avatarObj in avatarObjects)
|
|
avatars.Add(new AvatarData(avatarObj.name));
|
|
Debug.Log($"[AvatarOutfitController] {avatars.Count}개의 아바타를 자동으로 찾았습니다.");
|
|
}
|
|
|
|
avatars.RemoveAll(avatar => avatar == null);
|
|
|
|
if (avatars.Count > 0 && currentAvatar == null)
|
|
currentAvatar = avatars[0];
|
|
|
|
BuildAllSimulationCaches();
|
|
|
|
if (syncOutfitStateOnAwake)
|
|
SyncInitialOutfitState();
|
|
|
|
Debug.Log($"[AvatarOutfitController] 총 {avatars.Count}개의 아바타가 등록되었습니다.");
|
|
}
|
|
|
|
private void BuildAllSimulationCaches()
|
|
{
|
|
foreach (var avatar in avatars)
|
|
{
|
|
if (avatar?.outfits == null) continue;
|
|
foreach (var outfit in avatar.outfits)
|
|
outfit?.BuildSimulationCache();
|
|
}
|
|
}
|
|
|
|
/// <summary>모든 아바타의 모든 의상을 먼저 숨긴 뒤, 각 아바타의 currentOutfit 만 표시. 초기 씬 상태 정리용.</summary>
|
|
public void SyncInitialOutfitState()
|
|
{
|
|
// Pass 1: 모든 의상 제거 (아바타 간 교차 hideObjects 간섭 방지)
|
|
foreach (var avatar in avatars)
|
|
{
|
|
if (avatar == null || avatar.outfits == null) continue;
|
|
foreach (var outfit in avatar.outfits)
|
|
{
|
|
if (outfit != null) outfit.RemoveOutfit();
|
|
}
|
|
}
|
|
|
|
// Pass 2: 각 아바타의 현재 의상만 적용
|
|
foreach (var avatar in avatars)
|
|
{
|
|
if (avatar == null) continue;
|
|
if (avatar.CurrentOutfit != null)
|
|
avatar.CurrentOutfit.ApplyOutfit();
|
|
}
|
|
}
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
public void Set(int index)
|
|
{
|
|
if (avatars.Count > 0)
|
|
SetAvatarOutfit(0, index);
|
|
else
|
|
Debug.LogWarning("[AvatarOutfitController] 설정할 아바타가 없습니다.");
|
|
}
|
|
|
|
public void SetAvatarOutfit(int avatarIndex, int outfitIndex)
|
|
{
|
|
if (avatarIndex < 0 || avatarIndex >= avatars.Count)
|
|
{
|
|
Debug.LogWarning($"[AvatarOutfitController] 잘못된 아바타 인덱스: {avatarIndex}");
|
|
return;
|
|
}
|
|
|
|
var avatar = avatars[avatarIndex];
|
|
if (avatar.outfits == null || outfitIndex < 0 || outfitIndex >= avatar.outfits.Length)
|
|
{
|
|
Debug.LogWarning($"[AvatarOutfitController] 잘못된 의상 인덱스: {outfitIndex}");
|
|
return;
|
|
}
|
|
if (avatar.CurrentOutfitIndex == outfitIndex) return;
|
|
|
|
if (avatar.isTransforming)
|
|
{
|
|
Debug.LogWarning($"[AvatarOutfitController] '{avatar.avatarName}' 이 이미 변신 중입니다.");
|
|
return;
|
|
}
|
|
|
|
var oldOutfit = avatar.CurrentOutfit;
|
|
var newOutfit = avatar.outfits[outfitIndex];
|
|
var effect = newOutfit.transformEffect;
|
|
|
|
if (effect == null || !effect.enabled)
|
|
{
|
|
// 즉시 스왑
|
|
avatar.SetOutfit(outfitIndex);
|
|
OnAvatarOutfitChanged?.Invoke(avatar, oldOutfit, avatar.CurrentOutfit);
|
|
NotifyAvatarOutfitChanged(avatar);
|
|
return;
|
|
}
|
|
|
|
StartCoroutine(TransformRoutine(avatar, outfitIndex, oldOutfit, newOutfit, effect));
|
|
}
|
|
|
|
public void AddAvatar(string avatarName)
|
|
{
|
|
var newAvatar = new AvatarData(avatarName);
|
|
avatars.Add(newAvatar);
|
|
NotifyAvatarOutfitChanged(newAvatar);
|
|
Debug.Log($"[AvatarOutfitController] 아바타 추가: {avatarName}");
|
|
}
|
|
|
|
public void RemoveAvatar(int index)
|
|
{
|
|
if (index < 0 || index >= avatars.Count) return;
|
|
|
|
var removedAvatar = avatars[index];
|
|
avatars.RemoveAt(index);
|
|
|
|
if (removedAvatar == currentAvatar)
|
|
currentAvatar = avatars.Count > 0 ? avatars[0] : null;
|
|
|
|
Debug.Log($"[AvatarOutfitController] 아바타 제거: {removedAvatar.avatarName}");
|
|
}
|
|
#endregion
|
|
|
|
#region Outfit Transformation
|
|
private IEnumerator TransformRoutine(AvatarData avatar, int outfitIndex,
|
|
OutfitData oldOutfit, OutfitData newOutfit, TransformEffectSettings s)
|
|
{
|
|
avatar.isTransforming = true;
|
|
|
|
var preset = s.preset;
|
|
if (preset == null)
|
|
{
|
|
Debug.LogWarning($"[AvatarOutfitController] '{newOutfit.outfitName}' 의 TransformEffectPreset 이 비어있어 즉시 교체합니다.");
|
|
avatar.SetOutfit(outfitIndex);
|
|
OnAvatarOutfitChanged?.Invoke(avatar, oldOutfit, avatar.CurrentOutfit);
|
|
NotifyAvatarOutfitChanged(avatar);
|
|
avatar.isTransforming = false;
|
|
yield break;
|
|
}
|
|
|
|
var fromRc = oldOutfit != null ? oldOutfit.GetPrimaryRenderController() : null;
|
|
var toRc = newOutfit.GetPrimaryRenderController();
|
|
|
|
SpawnTransformVfx(s, preset, newOutfit);
|
|
PlayTransformSfx(s, preset);
|
|
|
|
// Phase 1: 현재 의상을 flashColor 로 덮음
|
|
if (preset.useColorFlash && fromRc != null)
|
|
{
|
|
if (preset.flashInDuration > 0f)
|
|
yield return LerpFlash(fromRc, preset, 0f, preset.flashIntensity, preset.flashInDuration);
|
|
else
|
|
ApplyFlash(fromRc, preset, preset.flashIntensity);
|
|
}
|
|
|
|
// 새 의상을 flashColor 상태로 프라임해둔 뒤 스왑
|
|
if (preset.useColorFlash && toRc != null)
|
|
ApplyFlash(toRc, preset, preset.flashIntensity);
|
|
|
|
avatar.SetOutfit(outfitIndex);
|
|
|
|
// 이전 NiloToon 은 더 이상 렌더되지 않지만 컬러 상태는 원복 (다음 변경을 위해)
|
|
if (preset.useColorFlash && fromRc != null)
|
|
ApplyFlash(fromRc, preset, 0f);
|
|
|
|
OnAvatarOutfitChanged?.Invoke(avatar, oldOutfit, avatar.CurrentOutfit);
|
|
NotifyAvatarOutfitChanged(avatar);
|
|
|
|
// Phase 2: 새 의상이 flashColor 에서 원색으로 복귀
|
|
if (preset.useColorFlash && toRc != null)
|
|
{
|
|
if (preset.flashOutDuration > 0f)
|
|
yield return LerpFlash(toRc, preset, preset.flashIntensity, 0f, preset.flashOutDuration);
|
|
else
|
|
ApplyFlash(toRc, preset, 0f);
|
|
}
|
|
|
|
avatar.isTransforming = false;
|
|
Debug.Log($"[AvatarOutfitController] 의상 변신 완료: {avatar.avatarName} → {newOutfit.outfitName}");
|
|
}
|
|
|
|
private void SpawnTransformVfx(TransformEffectSettings s, TransformEffectPreset preset, OutfitData incoming)
|
|
{
|
|
if (preset.vfxPrefab == null) return;
|
|
Transform anchor = s.vfxAnchor;
|
|
if (anchor == null)
|
|
{
|
|
var rc = incoming.GetPrimaryRenderController();
|
|
if (rc != null) anchor = rc.transform;
|
|
}
|
|
if (anchor == null) return;
|
|
var vfx = Instantiate(preset.vfxPrefab, anchor.position, anchor.rotation);
|
|
if (preset.vfxLifetime > 0f)
|
|
Destroy(vfx, preset.vfxLifetime);
|
|
}
|
|
|
|
private void PlayTransformSfx(TransformEffectSettings s, TransformEffectPreset preset)
|
|
{
|
|
if (s.audioSource == null || preset.sfxClip == null) return;
|
|
s.audioSource.PlayOneShot(preset.sfxClip, preset.sfxVolume);
|
|
}
|
|
|
|
private void ApplyFlash(NiloToonPerCharacterRenderController c, TransformEffectPreset preset, float t)
|
|
{
|
|
c.perCharacterLerpColor = preset.flashColor;
|
|
c.perCharacterLerpUsage = t;
|
|
}
|
|
|
|
private IEnumerator LerpFlash(NiloToonPerCharacterRenderController c, TransformEffectPreset preset, float from, float to, float duration)
|
|
{
|
|
c.perCharacterLerpColor = preset.flashColor;
|
|
float elapsed = 0f;
|
|
while (elapsed < duration)
|
|
{
|
|
elapsed += Time.deltaTime;
|
|
float k = Mathf.Clamp01(elapsed / duration);
|
|
float eased = k * k * (3f - 2f * k);
|
|
ApplyFlash(c, preset, Mathf.Lerp(from, to, eased));
|
|
yield return null;
|
|
}
|
|
ApplyFlash(c, preset, to);
|
|
}
|
|
#endregion
|
|
|
|
#region StreamDeck Integration
|
|
private void NotifyAvatarOutfitChanged(AvatarData avatar)
|
|
{
|
|
OnStateChanged?.Invoke();
|
|
if (streamDeckManager != null)
|
|
streamDeckManager.NotifyAvatarOutfitChanged();
|
|
}
|
|
|
|
public AvatarOutfitListData GetAvatarOutfitListData()
|
|
{
|
|
return new AvatarOutfitListData
|
|
{
|
|
avatar_count = avatars.Count,
|
|
avatars = avatars.Select((a, i) => new AvatarPresetData
|
|
{
|
|
index = i,
|
|
name = a.avatarName,
|
|
current_outfit_index = a.CurrentOutfitIndex,
|
|
current_outfit_name = a.CurrentOutfit?.outfitName ?? "없음",
|
|
outfits = a.outfits?.Select((o, oi) => new OutfitPresetData
|
|
{
|
|
index = oi,
|
|
name = o.outfitName
|
|
}).ToArray() ?? new OutfitPresetData[0],
|
|
hotkey = "스트림덱 전용"
|
|
}).ToArray(),
|
|
current_avatar_index = CurrentAvatarIndex
|
|
};
|
|
}
|
|
|
|
public AvatarOutfitStateData GetCurrentAvatarOutfitState()
|
|
{
|
|
if (currentAvatar == null) return null;
|
|
|
|
return new AvatarOutfitStateData
|
|
{
|
|
current_avatar_index = CurrentAvatarIndex,
|
|
avatar_name = currentAvatar.avatarName,
|
|
current_outfit_index = currentAvatar.CurrentOutfitIndex,
|
|
current_outfit_name = currentAvatar.CurrentOutfit?.outfitName ?? "없음",
|
|
total_avatars = avatars.Count
|
|
};
|
|
}
|
|
|
|
public string GetAvatarOutfitListJson()
|
|
{
|
|
return JsonConvert.SerializeObject(GetAvatarOutfitListData());
|
|
}
|
|
|
|
public string GetAvatarOutfitStateJson()
|
|
{
|
|
return JsonConvert.SerializeObject(GetCurrentAvatarOutfitState());
|
|
}
|
|
#endregion
|
|
|
|
#region Data Classes
|
|
[System.Serializable]
|
|
public class OutfitPresetData
|
|
{
|
|
public int index;
|
|
public string name;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class AvatarPresetData
|
|
{
|
|
public int index;
|
|
public string name;
|
|
public int current_outfit_index;
|
|
public string current_outfit_name;
|
|
public OutfitPresetData[] outfits;
|
|
public string hotkey;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class AvatarOutfitListData
|
|
{
|
|
public int avatar_count;
|
|
public AvatarPresetData[] avatars;
|
|
public int current_avatar_index;
|
|
}
|
|
|
|
[System.Serializable]
|
|
public class AvatarOutfitStateData
|
|
{
|
|
public int current_avatar_index;
|
|
public string avatar_name;
|
|
public int current_outfit_index;
|
|
public string current_outfit_name;
|
|
public int total_avatars;
|
|
}
|
|
#endregion
|
|
|
|
#region IController Implementation
|
|
public string GetControllerId() => "avatar_outfit_controller";
|
|
public string GetControllerName() => "Avatar Outfit Controller";
|
|
public object GetControllerData() => GetAvatarOutfitListData();
|
|
|
|
public void ExecuteAction(string actionId, object parameters)
|
|
{
|
|
switch (actionId)
|
|
{
|
|
case "set_avatar_outfit":
|
|
if (parameters is Dictionary<string, object> setParams
|
|
&& setParams.ContainsKey("avatar_index") && setParams.ContainsKey("outfit_index"))
|
|
{
|
|
int avatarIndex = Convert.ToInt32(setParams["avatar_index"]);
|
|
int outfitIndex = Convert.ToInt32(setParams["outfit_index"]);
|
|
SetAvatarOutfit(avatarIndex, outfitIndex);
|
|
}
|
|
break;
|
|
default:
|
|
Debug.LogWarning($"[AvatarOutfitController] 알 수 없는 액션: {actionId}");
|
|
break;
|
|
}
|
|
}
|
|
#endregion
|
|
}
|