From f4af0495d321217303876e30b6d127ad1a28592c Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 12 Aug 2025 12:21:47 +0900 Subject: [PATCH 01/19] =?UTF-8?q?docs:=20live2d=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=20=EC=84=A4=EA=B3=84=20=EB=AC=B8=EC=84=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Docs/Component_Analysis_Document.md | 329 ++++++++++++++++++ .../Docs/Component_Analysis_Document.md.meta | 7 + Assets/Docs/Live2D_Service_Design.md | 165 +++++++++ Assets/Docs/Live2D_Service_Design.md.meta | 7 + Assets/Docs/diagrams.meta | 8 + Assets/Docs/diagrams/live2d_architecture.mmd | 10 + .../diagrams/live2d_architecture.mmd.meta | 7 + Assets/Docs/diagrams/live2d_architecture.png | Bin 0 -> 16564 bytes .../diagrams/live2d_architecture.png.meta | 156 +++++++++ Assets/Docs/diagrams/live2d_architecture.svg | 1 + .../diagrams/live2d_architecture.svg.meta | 7 + Assets/Docs/diagrams/live2d_sequence.mmd | 21 ++ Assets/Docs/diagrams/live2d_sequence.mmd.meta | 7 + Assets/Docs/diagrams/live2d_sequence.png | Bin 0 -> 23552 bytes Assets/Docs/diagrams/live2d_sequence.png.meta | 156 +++++++++ Assets/Docs/diagrams/live2d_sequence.svg | 1 + Assets/Docs/diagrams/live2d_sequence.svg.meta | 7 + Assets/Docs/diagrams/live2d_sequence@2x.png | Bin 0 -> 40758 bytes .../Docs/diagrams/live2d_sequence@2x.png.meta | 156 +++++++++ Assets/Tests/Sences/Live2D.unity | 218 ++++++++++++ Assets/Tests/Sences/Live2D.unity.meta | 7 + ProjectSettings/ProjectSettings.asset | 4 +- 22 files changed, 1272 insertions(+), 2 deletions(-) create mode 100644 Assets/Docs/Component_Analysis_Document.md create mode 100644 Assets/Docs/Component_Analysis_Document.md.meta create mode 100644 Assets/Docs/Live2D_Service_Design.md create mode 100644 Assets/Docs/Live2D_Service_Design.md.meta create mode 100644 Assets/Docs/diagrams.meta create mode 100644 Assets/Docs/diagrams/live2d_architecture.mmd create mode 100644 Assets/Docs/diagrams/live2d_architecture.mmd.meta create mode 100644 Assets/Docs/diagrams/live2d_architecture.png create mode 100644 Assets/Docs/diagrams/live2d_architecture.png.meta create mode 100644 Assets/Docs/diagrams/live2d_architecture.svg create mode 100644 Assets/Docs/diagrams/live2d_architecture.svg.meta create mode 100644 Assets/Docs/diagrams/live2d_sequence.mmd create mode 100644 Assets/Docs/diagrams/live2d_sequence.mmd.meta create mode 100644 Assets/Docs/diagrams/live2d_sequence.png create mode 100644 Assets/Docs/diagrams/live2d_sequence.png.meta create mode 100644 Assets/Docs/diagrams/live2d_sequence.svg create mode 100644 Assets/Docs/diagrams/live2d_sequence.svg.meta create mode 100644 Assets/Docs/diagrams/live2d_sequence@2x.png create mode 100644 Assets/Docs/diagrams/live2d_sequence@2x.png.meta create mode 100644 Assets/Tests/Sences/Live2D.unity create mode 100644 Assets/Tests/Sences/Live2D.unity.meta diff --git a/Assets/Docs/Component_Analysis_Document.md b/Assets/Docs/Component_Analysis_Document.md new file mode 100644 index 0000000..e4d7718 --- /dev/null +++ b/Assets/Docs/Component_Analysis_Document.md @@ -0,0 +1,329 @@ +# 컴포넌트 분석 문서 + +## 개요 +이 문서는 ProjectVG-Client의 핵심 컴포넌트들을 분석하고 정리한 문서입니다. Live2D 캐릭터 시스템의 입력 처리, 시선 추적, 터치 인터랙션, 그리고 시스템 관리에 대한 구조를 설명합니다. + +## 1. ScreenTapManager - 입력 처리 시스템 + +### 1.1 개요 +`ScreenTapManager`는 플랫폼별 입력(터치/마우스)을 통합 관리하는 싱글톤 클래스입니다. + +### 1.2 주요 인터페이스 + +#### IInputProvider +```csharp +public interface IInputProvider +{ + bool TryGetPosition(out Vector3 position); +} +``` +- 현재 입력 위치를 반환하는 인터페이스 +- 터치/마우스 입력을 통합 처리 + +#### IInputUpProvider +```csharp +public interface IInputUpProvider +{ + bool TryGetPosition(out Vector3 position); +} +``` +- 터치 종료 시점의 위치만 반환하는 인터페이스 +- Raycast 이벤트 처리에 사용 + +### 1.3 구현 클래스 + +#### DefaultInputProvider +- **플랫폼별 입력 처리**: iOS/Android는 터치, 데스크톱은 마우스 +- **UI 무시 기능**: "IgnoreLookAt" 태그가 달린 UI 클릭 시 입력 무시 +- **EventSystem 통합**: Unity UI 시스템과 연동하여 UI 오버레이 감지 + +#### DefaultInputUpProvider +- **터치 종료 감지**: 터치/마우스 버튼 해제 시점만 감지 +- **Raycast 이벤트**: 터치 종료 시점에만 이벤트 발생 + +### 1.4 주요 기능 + +#### LookAt 기능 +```csharp +public bool TryGetLookDirection(out Vector3 lookDir) +``` +- 스크린 좌표를 Live2D 모델이 사용할 방향 벡터로 변환 +- 뷰포트 좌표계로 정규화하여 [-1,1] 범위로 변환 + +#### Raycast 기능 +```csharp +public bool TryGetTapUpPosition(out CubismRaycastHit[] hitResults) +``` +- 터치 종료 시점에만 Raycast 수행 +- Live2D 모델의 특정 영역 터치 감지 + +## 2. SystemManager - 시스템 통합 관리 + +### 2.1 개요 +Live2D 캐릭터 시스템의 전체적인 초기화와 관리를 담당하는 싱글톤 클래스입니다. + +### 2.2 주요 구성 요소 +- **CubismLookTarget**: 시선 추적 타겟 +- **Camera**: 메인 카메라 참조 +- **ModelConfig**: 모델 설정 데이터 +- **AudioSource**: 음성 입력 소스 +- **Button**: 표정 변경 버튼 + +### 2.3 초기화 프로세스 + +#### Start() 메서드 +1. `ScreenTapManager` 초기화 +2. `AudioManager` 초기화 +3. 모델 초기화 (`ModelInit`) + +#### ModelInit() 메서드 +1. 기존 모델 제거 +2. 새 모델 인스턴스 생성 +3. LookAt 설정 +4. LipSync 설정 +5. Raycast 설정 + +### 2.4 설정 메서드들 + +#### SetLockAt() +```csharp +private void SetLockAt(ModelConfig modelConfig) +{ + var lookController = _currentModel.GetComponent(); + lookController.Target = cubismLookTarget.gameObject; + lookController.Damping = modelConfig.LockAtDamping; + cubismLookTarget.Initialize(modelConfig); +} +``` + +#### SetLipSync() +```csharp +private void SetLipSync(ModelConfig modelConfig) +{ + var mouthController = _currentModel.GetComponent(); + mouthController.AudioInput = voiceSource; + mouthController.Gain = modelConfig.Gain; + mouthController.Smoothing = modelConfig.Smoothing; +} +``` + +#### SetRayCast() +```csharp +private void SetRayCast(ModelConfig modelConfig) +{ + var hitHandler = _currentModel.GetComponent(); + hitHandler.Initialize(); + expressionChangeBtn.onClick.AddListener(hitHandler.ExpressionChange_Btn); +} +``` + +## 3. CubismLookTarget - 시선 추적 타겟 + +### 3.1 개요 +Live2D 모델의 시선 추적을 위한 타겟 클래스로, `ICubismLookTarget` 인터페이스를 구현합니다. + +### 3.2 주요 기능 + +#### Initialize() +```csharp +public void Initialize(ModelConfig modelConfig) +{ + _modelConfig = modelConfig; +} +``` +- 모델 설정 데이터를 저장 + +#### GetPosition() +```csharp +public Vector3 GetPosition() +{ + if (!ScreenTapManager.Instance.TryGetLookDirection(out var lookDir)) + return Vector3.zero; + return lookDir * _modelConfig.LookSensitivity; +} +``` +- `ScreenTapManager`에서 입력 방향을 받아서 민감도 적용 +- 시선 추적 활성화 여부 확인 + +#### IsActive() +```csharp +public bool IsActive() +{ + return _modelConfig.IsLockAtActive; +} +``` +- 모델 설정에 따른 시선 추적 활성화 상태 반환 + +## 4. CubismHitHandler - 터치 인터랙션 처리 + +### 4.1 개요 +Live2D 모델의 특정 영역 터치를 감지하고 반응을 처리하는 클래스입니다. + +### 4.2 주요 구성 요소 +- **CubismRaycaster**: 터치 감지를 위한 레이캐스터 +- **CubismExpressionController**: 표정 변경을 위한 컨트롤러 + +### 4.3 초기화 +```csharp +public void Initialize() +{ + _raycaster = GetComponent(); + _expressionController = GetComponent(); + ScreenTapManager.Instance.SetRaycaster(_raycaster); +} +``` + +### 4.4 터치 처리 + +#### Update() 메서드 +```csharp +private void Update() +{ + if (ScreenTapManager.Instance.TryGetTapUpPosition(out var hits)) + { + foreach (var hit in hits) + { + if(hit.Drawable is null) continue; + HandleHit(hit.Drawable.name); + } + } +} +``` + +#### HandleHit() 메서드 +```csharp +private void HandleHit(string drawableName) +{ + switch (drawableName) + { + case "HitAreaHead": + Debug.Log("머리 터치 → 표정 변경 or 모션 재생"); + ExpressionChange(); + break; + case "HitAreaBody": + Debug.Log("몸통 터치 → 다른 반응"); + break; + } +} +``` + +### 4.5 표정 변경 기능 + +#### ExpressionChange() +```csharp +private void ExpressionChange() +{ + _expressionController.CurrentExpressionIndex = + GetNextExpressionIndex(_expressionController.CurrentExpressionIndex, 0, + _expressionController.ExpressionsList.CubismExpressionObjects.Length); +} +``` + +#### GetNextExpressionIndex() +```csharp +private int GetNextExpressionIndex(int current, int min, int max) +{ + return ((current - min + 1) % (max - min + 1)) + min; +} +``` + +## 5. ModelConfig - 모델 설정 데이터 + +### 5.1 개요 +ScriptableObject 기반의 모델 설정 데이터 클래스로, Live2D 모델의 다양한 설정을 관리합니다. + +### 5.2 주요 설정 카테고리 + +#### 모델 정보 +- **modelName**: 모델 식별용 이름 +- **modelDescription**: 모델 설명 +- **thumbnail**: 썸네일 이미지 + +#### 시선 설정 +- **lookSensitivity**: 시선 추적 민감도 (0-30) +- **lockAtDamping**: 시선 추적 반응 속도 (0-5) +- **isLockAtActive**: 시선 추적 활성화 여부 + +#### 립싱크 설정 +- **gain**: 음량 증폭 배율 (1-10) +- **smoothing**: 입 움직임 부드러움 (0-1) + +#### 모델 프리팹 +- **modelPrefab**: 실제 모델 프리팹 + +### 5.3 사용 예시 +```csharp +// natoriConfig.asset 예시 +modelName: Natori +modelDescription: "이지적인 집사" +lookSensitivity: 5 +lockAtDamping: 0.15 +isLockAtActive: true +gain: 10 +smoothing: 1 +``` + +## 6. 연관 클래스들 + +### 6.1 GameManager +- 게임 전체의 초기화와 관리 +- WebSocket, Session, HTTP API 클라이언트 관리 +- 시스템 간 의존성 설정 + +### 6.2 AudioManager +- 음성, BGM, SFX 소스 관리 +- 현재는 기본 구조만 구현된 상태 + +### 6.3 Live2D 프레임워크 클래스들 +- **CubismLookController**: 시선 추적 컨트롤러 +- **CubismAudioMouthInput**: 립싱크 입력 처리 +- **CubismRaycaster**: 터치 감지 레이캐스터 +- **CubismExpressionController**: 표정 변경 컨트롤러 + +## 7. 시스템 아키텍처 + +### 7.1 데이터 흐름 +``` +사용자 입력 → ScreenTapManager → CubismLookTarget → CubismLookController → Live2D 모델 +터치 종료 → ScreenTapManager → CubismHitHandler → CubismExpressionController → 표정 변경 +``` + +### 7.2 의존성 구조 +``` +SystemManager +├── ScreenTapManager (싱글톤) +├── AudioManager (싱글톤) +├── CubismLookTarget +├── ModelConfig (ScriptableObject) +└── CubismHitHandler +``` + +### 7.3 초기화 순서 +1. `SystemManager.Start()` 호출 +2. `ScreenTapManager.Initialize()` - 카메라 설정 +3. `AudioManager.Initialize()` - 오디오 시스템 초기화 +4. `ModelInit()` - 모델 생성 및 설정 +5. 각 컴포넌트별 초기화 메서드 호출 + +## 8. 확장성 및 개선 사항 + +### 8.1 현재 구조의 장점 +- **모듈화**: 각 기능이 독립적인 클래스로 분리 +- **설정 기반**: ScriptableObject를 통한 데이터 관리 +- **플랫폼 호환성**: 터치/마우스 입력 통합 처리 +- **확장성**: 인터페이스 기반 설계로 새로운 기능 추가 용이 + +### 8.2 개선 가능한 부분 +- **에러 처리**: 예외 상황에 대한 처리 부족 +- **성능 최적화**: Update() 메서드의 최적화 필요 +- **설정 검증**: ModelConfig의 유효성 검사 추가 +- **로깅 시스템**: 디버깅을 위한 로깅 시스템 구축 + +## 9. 결론 + +이 시스템은 Live2D 캐릭터와의 상호작용을 위한 잘 구조화된 아키텍처를 제공합니다. 입력 처리, 시선 추적, 터치 인터랙션, 그리고 설정 관리가 체계적으로 분리되어 있어 유지보수성과 확장성이 우수합니다. 특히 ScriptableObject를 활용한 설정 관리와 인터페이스 기반 설계는 코드의 재사용성과 테스트 용이성을 높여줍니다. + + + + + diff --git a/Assets/Docs/Component_Analysis_Document.md.meta b/Assets/Docs/Component_Analysis_Document.md.meta new file mode 100644 index 0000000..7ca46b0 --- /dev/null +++ b/Assets/Docs/Component_Analysis_Document.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 310a102fb70ae1348bc89929d0f94431 +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/Live2D_Service_Design.md b/Assets/Docs/Live2D_Service_Design.md new file mode 100644 index 0000000..5b39578 --- /dev/null +++ b/Assets/Docs/Live2D_Service_Design.md @@ -0,0 +1,165 @@ +## Live2D 연동 Service 설계 (Emotion/Action 중심) + +### 목적 +- **캐릭터 반응**: 서버로부터 수신한 텍스트, 음성, 감정(Emotion), 행동(Action) 정보를 바탕으로 Live2D 캐릭터의 표정/모션/상태를 변경한다. +- **모듈화/확장성**: 감정/행동 처리를 각각 모듈로 분리하여 맵핑과 우선순위 정책을 유연하게 확장한다. +- **일관된 상태관리**: 음성 재생, 감정 지속시간, 행동 트리거 등 비동기 이벤트를 단일 상태 모델에서 일관되게 관리한다. + +### 범위 +- 포함: 감정/행동 파이프라인 설계, 상태관리, Live2D SDK 연동 포인트, 데이터 매핑 규칙, 테스트 전략, 단계별 구현 로드맵 +- 제외: 정교한 액션 모션 구현(더미 처리), 고급 물리/파라미터 튜닝, 에디터 툴링(UI) + +### 용어 및 데이터 모델 +- **Emotion**: 캐릭터의 표정/감정 상태를 표현. 예: Neutral, Happy, Sad, Angry, Surprised 등 +- **Action**: 캐릭터의 몸짓/행동 트리거. 예: Nod, ShakeHead, WaveHand 등(현재 더미) +- **ChatResponse.Metadata**: 서버가 감정/행동 정보를 담아 보내는 확장 필드. 제안 키: + - `emotion`: string (예: "happy") + - `emotion_intensity`: float 0~1 + - `emotion_duration_ms`: int + - `action`: string (예: "wave_hand") + - `action_args`: object + +### 전체 아키텍처 개요 +- **ChatManager**: 서버 왕복, 메시지 큐 및 음성/버블 표시. 수신 메시지를 감정/행동 파이프라인으로 라우팅. +- **Live2DCharacterManager**: 캐릭터 상태의 단일 진입점. Emotion/Action 컨트롤러를 보유하고 우선순위/병행 정책 적용. +- **EmotionController**: 감정 → Live2D Expression 맵핑/블렌딩/지속시간/해제 관리. +- **ActionController**: 행동 → Live2D Motion/Parameter 트리거. 현재 더미. +- **VoiceManager(기존)**: 음성 재생 및 완료 이벤트 발생. +- **ChatBubbleManager(기존)**: 대화 버블 표시. + +```mermaid +sequenceDiagram +participant U as User +participant CM as ChatManager +participant API as ApiService +participant L2D as Live2DCharacterManager +participant EMO as EmotionController +participant ACT as ActionController +participant VO as VoiceManager + +U->>CM: 사용자 입력 전송 +CM->>API: 요청(텍스트, 캐릭터/유저 ID) +API-->>CM: 응답(text, audio, metadata{emotion, action}) +CM->>CM: ChatMessage 변환 및 큐 처리 +CM->>VO: 음성 재생 +CM->>L2D: ApplyReaction(metadata) +L2D->>EMO: SetEmotion(emotion, intensity, duration) +L2D->>ACT: TriggerAction(action) +VO-->>L2D: OnVoiceFinished() +L2D->>EMO: Release/Restore 상태 갱신 +``` + +이미지 버전: `Assets/Docs/diagrams/live2d_sequence.svg` / `Assets/Docs/diagrams/live2d_sequence.png` +![Live2D Sequence](diagrams/live2d_sequence.svg) + +### 상태 관리 설계 +- **CharacterStateModel** 제안 필드 + - `currentEmotion`, `pendingEmotion`, `emotionIntensity`, `emotionExpireAt` + - `currentAction`, `actionPlaying`, `actionExpireAt` + - `isVoicePlaying` +- **우선순위(초안)** + - Action > Voice Gating > Emotion 순으로 적용. 예: 강한 액션은 진행 중 감정을 일시 덮어씀. + - Voice 재생 중 과격한 표정 전환은 완화(블렌드 타임 증가) 또는 대기열로 보관. +- **시간/블렌딩** + - 감정은 `duration` 동안 유지 후 `Neutral`로 복귀. 블렌드 인/아웃 시간 설정. + - 액션은 모션 길이 동안 `actionPlaying=true`, 종료 시 이전 감정 복원. + +### Live2D SDK 연동 포인트 +- Expression: `CubismExpressionController.CurrentExpressionIndex` 또는 이름 기반 선택. 감정 → Expression 이름/인덱스 맵 필요. +- Motion: 추후 `CubismMotionController` 또는 모션 그룹 기반 트리거. 현재는 더미 처리(로그/플래그). +- Parameter(선택): 입모양/깜박임 등은 차후 `Voice` 파형 기반 드라이브(범위 외). + +### Live2D 모델 관리자 / 파라미터 컨트롤러 설계 +- **Live2DModelManager** + - 역할: 모델 로드/언로드, 프리로드, 활성 모델 전환, 표시/비표시, 컴포넌트 접근 + - 고려: 풀링, 프리로딩, 비동기 로드, 메모리 관리, 품질/성능(텍스처 압축/샘플링, MipMap, LOD) +- **Live2DParameterController** + - 역할: 파라미터 직접 제어(Set/Blend/Reset), 프리셋 적용(감정/액션의 파라미터 세트), 업데이트 루프에서 블렌딩 처리 + - 고려: 충돌해결(우선순위/레이어), 블렌드 커브, 최대/최소 클램프, 이름→ID 매핑 캐시 +- **Live2DCharacterManager** + - 역할: 상위 조정자. 모델 관리자/파라미터 컨트롤러를 묶어 감정/행동 반응을 일관되게 적용 + - 정책: Action > Voice > Emotion 우선순위, 지속시간/블렌드 관리, 구성 자산 기반 매핑 + +```mermaid +graph TD + CM[ChatManager] --> LCM[Live2DCharacterManager] + LCM --> LMM[Live2DModelManager] + LCM --> LPC[Live2DParameterController] + LMM -->|Active Model| L2D[Live2D Components] + LPC -->|Parameters/Expressions| L2D +``` + +이미지 버전: `Assets/Docs/diagrams/live2d_architecture.svg` / `Assets/Docs/diagrams/live2d_architecture.png` +![Live2D Architecture](diagrams/live2d_architecture.svg) + +### 모듈 API 초안 (스켈레톤 중심) +```csharp +/** 캐릭터 전반의 상태를 단일 진입점에서 관리한다. */ +public interface ILive2DCharacterManager +{ + void Initialize(); + void ApplyReaction(EmotionData emotionData, ActionData actionData); + void OnVoiceStarted(); + void OnVoiceFinished(); +} + +/** 감정 → Expression 맵핑과 블렌딩을 담당한다. */ +public interface IEmotionController +{ + void Initialize(); + void SetEmotion(string emotion, float intensity, int durationMs); + void ClearEmotion(); +} + +/** 행동 → 모션/파라미터 트리거를 담당한다. */ +public interface IActionController +{ + void Initialize(); + void TriggerAction(string action, object args = null); +} + +/** 감정/행동 데이터 전달을 위한 단순 DTO. */ +public struct EmotionData { public string Emotion; public float Intensity; public int DurationMs; } +public struct ActionData { public string Action; public object Args; } +``` + +### 메시지 매핑 규칙 +- ChatResponse → ChatMessage 변환 시 `metadata`에서 감정/행동 추출. +- 누락/알 수 없는 값은 안전한 기본값 적용: + - emotion 미지정: `neutral` + - intensity 누락: `0.5f` + - duration 누락: `2000ms` + - action 미지정: 처리 없음 + +### 에러 처리 및 로깅 +- 알 수 없는 emotion/action은 경고 로그와 함께 무시. 매핑 테이블에 기록하여 추후 보강. +- Live2D 컴포넌트 결여 시 초기화 단계에서 명시적 오류. + +### 테스트 전략 +- 단위 테스트 + - Emotion 맵핑: 입력 emotion → 예상 Expression 식별자 + - 상태 머신: 음성 재생 중 감정 대기/복원, 액션 우선 적용 +- 통합 테스트 + - ChatManager 이벤트 플로우에서 ApplyReaction 호출 여부 + - 음성 종료 이벤트 후 상태 정상 복원 + +### 구성/데이터 자산 +- `Live2DCharacterConfig`(ScriptableObject 제안) + - `Emotion → ExpressionKey` 사전 + - `Action → MotionKey` 사전(더미 가능) + - 블렌드/지속 기본값, 우선순위 정책 + +### 단계별 로드맵(점진 구현) +1) 스켈레톤 클래스/인터페이스 추가 및 DI 연결 +2) EmotionController 최소 맵핑(Neutral/Happy 등)과 블렌드 기본값 +3) ChatManager → Live2DCharacterManager 라우팅 연결, Voice 이벤트 연동 +4) ActionController 더미 트리거(로그/플래그) 및 상태 복원 +5) 구성 자산(SO) 도입, 맵 테이블 외부화 +6) 모션 컨트롤러 연동 및 액션 일부 실제 재생 + +### 오픈 이슈 +- 라이브2D 모션 자산命名/그룹 규칙 확정 필요 +- 감정 우선순위 정책의 UX 적정값(블렌드/지속) 튜닝 필요 +- 서버 메타데이터 스키마 확정 및 계약 문서화 필요 + + diff --git a/Assets/Docs/Live2D_Service_Design.md.meta b/Assets/Docs/Live2D_Service_Design.md.meta new file mode 100644 index 0000000..6f63b46 --- /dev/null +++ b/Assets/Docs/Live2D_Service_Design.md.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: ab727606d752cc940ad0601903444ffd +TextScriptImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/diagrams.meta b/Assets/Docs/diagrams.meta new file mode 100644 index 0000000..5613773 --- /dev/null +++ b/Assets/Docs/diagrams.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: fcfec176b97236845967e6d6d0f6282d +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/diagrams/live2d_architecture.mmd b/Assets/Docs/diagrams/live2d_architecture.mmd new file mode 100644 index 0000000..b5dddca --- /dev/null +++ b/Assets/Docs/diagrams/live2d_architecture.mmd @@ -0,0 +1,10 @@ +graph TD + CM[ChatManager] --> LCM[Live2DCharacterManager] + LCM --> LMM[Live2DModelManager] + LCM --> LPC[Live2DParameterController] + LCM --> EMO[EmotionController] + LCM --> ACT[ActionController] + LMM -->|Active Model| L2D[Live2D Components] + LPC -->|Parameters/Expressions| L2D + + diff --git a/Assets/Docs/diagrams/live2d_architecture.mmd.meta b/Assets/Docs/diagrams/live2d_architecture.mmd.meta new file mode 100644 index 0000000..df84f53 --- /dev/null +++ b/Assets/Docs/diagrams/live2d_architecture.mmd.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 42e5237b6af6d3a44b03d71fe6499a05 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/diagrams/live2d_architecture.png b/Assets/Docs/diagrams/live2d_architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..126887266a659365300b22f3866f614f866ca35f GIT binary patch literal 16564 zcmcJ%bySsIyEnQ(P`X1pq@@G|Bo*oI?hffL2}K&Dq(Rz6cbC#q(hbrLE^HLu@qKHwJxdGYMWWmK8qy5FM~%kVMYrPLoqwVU;r1WZL!CS(O+O;u+kD1o4b-6#1KT zSdw2(o0$1Ju%rznIK*F???mw`xjv zuiH6iI}&B}nL$HT@b6xyizxTyvv2TC5u&Y!{jP|=NcbdM; z;IadEgZrpM69lqO&57`uTDy4A`+UH(;8IJ|*?C&Hto%kW3nSXp6mLjDXm0+nMuh8o8-kz_Ka>}~$#uk@Man$)y9CdhrR~XsdV7-69g!JV>f^BV)>ba`H#2ytG*y`?+u}@`$ub#e=JTb z(51KPCsfeU#uwx_5$l}EhpXJaWE|}))+LR8wR^L{Y`zv!%bRYS%^JR=V92CCAk9fJ zy)(3SI9V>pxeFwcm^h-G-YETzZ7T5;-+?cyVC=P!&8*8zaE4G&qIh=^p~m9EXIjV* z-#_9WxUwy_ZAef(x+o0ZbqX&V>LmR6O=7ftcb`a;tYC9cIyGBU!(MgcFgZ?0!o=Z# zVQ<>T%_eDaU$#137Ja^OQ99C4t^C`@^F+E7EW7!zu*4q5sRM**Y4L9BeGRT^oVaT` zK9gn*KHP-WmKqF*81bhK2sKXP4Uq6}(q}d4XLV`@PrkMc4{dDrhxMCs#n1GuWPETC zUs{2gnyVBONZJWJW6b?{My}UlVBHX}TW}_buU|QzA*|=oK5(F4(_^llP0+X79VBYe z{wd0FEf^UR2&^cG_IW91;+$+Z>Ljv4BJUJtk9AKq#=^H^^@OUbkTCP&54P?4U0m;M z4tqSFGmo2#i|btN)e_8bQmsrgt5JX*tz_-m`zD><#h2<|M4@96H?RDIi&ali!UDr_ zt!E+p!d(fbY1{OZC)4s4!wsVS`3E^x=;TYt>(6`EEmPM$!=d#R@y(wDacs~16su&} z6?2PaD~NAWx8|=#%X=%rXa6YcL+X(so|vw{jbyxQM%0uN7ne`X;m5H-g>0e2O`LXn z69cV)9T5^tu*Hc0EdDYGEDZwr{RCJc1QJLEwh*-Uz{J4E|KLYl8g~Q;&ZL7ORaFDF ztbqj6{&5RM$Kz1bK;uv?W{5nqgTw~fRW9Xz;AF_qYggCPXR$)$M7)m8qh{+rG<&6F z43Ht0xLZ#A`Q-=@<7sjMx1ENLJ9~5U{K*i=Yw1)zMicl%Ro@sR27Zpu$PnV?WpF2i z;Mf8WMxF2G=Jqjkz5g+!1|yUZ{v6z~QRmY!GOpLKLOA#T*Fzy8@PR&({%`It@H1_~ zKt4MHL<|Q=<|Dxyod5OZzl!O#vzbp2CP3+%gU?z?ApO3`EU||d(be6vTSHyXS;Xebx zL5!giz0ZEQ%06nR5ACY*y*P@9K!ffh2I2%!f+ZqzwEp?g8%@?_5Fa1^{(2%?)_?%= z5evS0WV`9oxpx1S%a*Ry*=h?pAD`w)353@>AfN@}RCyjRpYPA$tN8id%5k*OxxWId zhwp?q9X-AMLjCZn)5g|T!~iP9xL#b^00LPo(0i0;A3*WBIpX(;@%%&+_Oci4{#=D< z6N5Y3cZ)JEk6l^vKL|&ZBJ@u`co_JiBZoQ+%F72X<#*IVAk9dgKWLX zaDoh(J=c^f^L0v{)OcOlG9}6aq<%{If7T5C&AL8Ij3%ZXtS8SY6htj^)g%FLP>oUI z=*%|$D!CG4aId#)=#*o|B{GJsjPU3@XNWR#WPi0`=R+|s1ce_6H-(Tbx}NJ%pR0b` zv_~iY%cssXj=yx}T5D9oHSB)BirK~>iN>#)Cr|Mv#h)+hmDD=HFXg>&Icj3Yglr*$ z4*tlL7yph-n&|M?be7183X{&trPjOY#-LMcGlcT0ryhuVs3irBeSw;UTA7v!ZVDeAF!TGAe_Slr7d?Sm*i|0a0@8qz$2s+rO}j+Gf=zfz-USST zDv|Boul<^OUDF57Bj0{rbIA-jIIJhHo1eF)p%q+7ESyqJuZbM!BVzNBtnkF-YW7IB ziW=u8->oy4ez~?wlIbQaStUA@Yo@a$+P>G(QiKDsBnCzx&!DA|p^lFmC-1I2obDAA z)~{-yIDhxAP!r)5v;HckBLC6yX*SYCx@6(I6?FqO-jdhiiC<_>&yAMT$~VF9>SVHK zY^EIq+vH=QubU-rWkQ3rxH(;tD~*w%Jq@iYQ48;? z4?mHAkfL+^y4Q$uud<>KVw^;urbSrZmy9=4`OZ?$6UtS3oNre|YtNRm6(NnDxbV#J z4ZB_T7oI7$VpCfl+X((wsC5cLFd;Ly74an*I!0PNTX%tRQZ)Zw8d_S3w5U??X^)`9 z1jp`gPf5c4RVEqOZO3Y!M3p}@JEk;@)}VfO)6U}-M5l52YiyG#tSzgt+FgcXX>`tr zi@SN~iAA6*FUeq*%^ICHB=*y@zV)seDVL?@D@&rna;~<44<0TqeF1m*e8{}2!Yk2L zk98eV%;EK8B$xrDoiG7I;?)VC+#jTp!KAnYS+v+aWJF_eoE)`fTk&1b8 zB^!pGe|;4$UMQ5WD2gA&iRUcIq9sLDpAb4~@)d3#CM2*bd{1u&d6jN955Sd-a}POmgY>__foK^I)0BE+5WoE zE<)Xfwy9X!jMaH~CWq**TSwU;)4qI8@r&f6tLu!kUk>zjvb%eH)pZL!C;F$+{iZIM z8%@U|5_ikZ3qz){aa^Lq)`Q08rZaJM27l@#vXM%nd9GxwRNZ1EfYi@roHNSB=oxAM zNlVf=Gs&rUazardVPRed2qmufBi8%R)z|M!7PSf$N1fd{1~drI)pa|o*Rs=O6-)BJ z*k-2o>;E8aCBlPkj=&fShdP^j#}akRRr_q8I)AXWZ~NWS!3@_VQc0=&av>-$rFE@! z(z>7_NNq+$$O=nU)78e!iPS@iBNBYRm@LJHgeBh8$-hCh!$CjQ=-~5hi6*#mp`|U5 zEpwW&bl7J|s$jeRW}vB^Zz-$&NF}DeI*R!POe`lNENX<3!HQ$%(%?Ioa_|ZF2MEr8 zMEWDiWcNSmqmOxyyKnYyX(sos$YES)M8axGlwZBh4(CLAuD`jbn?9;!3sepP!6>k^ zs>$~xh3oB$AMBm2aBqvlRQ&^OWag3rf+Q?|{r_^t>CATw^nQ(l)Uxz`^Jmg?t@7DJ z57Te2o&#aH#cEu?88R!_dG0Zh%p( z_1H#|+Y)JFu188T5u2`gm3CTMnvajqr%#{o@bJ3-)cTy;!aP%l9z1UlSj|DE;M%k5 zBp)W;*4AcP6!-LlW`!YJ@*p=icaP|%i?_BmND|&!`y5pDP20~i+>7w@&#t@dju$A2 z-qjWr6?JqNMJb!U*KPBuyDXXvd4vSA!9d@a0s=l@?Il_j9-f}QjQKza{RWz4y6!9f z7s9Y^{IzV=WDIm&kWJabTk*w@g$BnKwZvh|o3%&|Fj*g8-=!;=TY4HAlk>ePkrp8Y zF)~E_PPgc&sM*86?d|PZS(L{%k~8H734@!vyStG@Z1IDe7b^iBzS;_V@d$;_xwu@0 z(_VtK0cN=4#NXV~vh-$e!LftzLBGkRz|%94iG@YB`2=B$9CRyg%87@R)6g*ETc{Ec z(4o(6)ZnnR))T2!rkj+MMF04i{C%J@AraA+6nNt|Xx3^?|5&(o?KK|$gWdu4t5M%`0O)( zOucCj8dUBv3fkK9wfmdn3qz(D*~mA=O-)TjMSTtpp`oEIzB%xHTS;csp*1*!c%lZO z5+PlDw1X8rjlaIW#>2uBJO%vzzs=I-EB9`@?!nul_DLN=Zqbo}MNoBy9Ucq6aNG zJOERXl$1Q`cz9o++T;aWYg#s4o8O@y48G2@fAr|lT0Rr3=n@^bAU{9MbqF6HzlWa? zAOD$qRV5A$=No1GZKFtkiM2!&m=Z1Cvp9WrC28r;1LkYladB~~$>aj<_t*P1R-^B) zHj}|_fM#Q3qpC$<`;K>EuEt9A{&Ib2XlQY9aX6h%&Oqgta}iVAECnG6N$Tg%l@3eI zjg5`@`E)PjR_m}`F@sQpLwtD@=os?%XDU_#9`3M0&FUg&%?QZISU;fWfQ`-;_WgUd zJ25aY07eqM9*fN{XFBtT^$=49+&b&&={YzY0q?*Vz=GLo(DA;lq(lw}DSdcrMAg~Z z87Q5p7oM8Bm>6O?5_%0v5GtZVt4Nk0-cqag>CJJ+@!?@~csMS6jj^$@#7`#wgx|g9 zN*{)I?dW(6`gL`6eQ_n+d3|#;n(@l@@4;M3%F`FGR`1?lcjtJI{r>&?>U6uMxf!m9 zgS3qS->nQ*%V{{jlQ+M=Yfg#o&dv=ALU@54o{X(1FHi7f(rHQ%s$DicQKY0-Hl3fJ z&zSAU#m%j@7eUlqUtb>!WCv!YBqQT}GQh~^u*l>tajK!EmCXNn``!Y$4Kp({R8&+M z8JTc)gp{|BX&^zUmZ7r-be7K=dLoJ87G%&oxnxE_MHS24O8o*@I+(Fqk)njSxX{%` zf|AIUxj0feH=h0oSj_d=F7S=-7V6pf_=t&vT0P&seVdt?x%{TMriPoBm-oes7i?_y zU{ur3X~$GKAx*$JbFbeQe_UQRyx7)(J0hc`-*9zu!e?;vft0o z&hm3|R=RGF+}`>@2T?7$y1FtA4?KK*eQ!YE=-x=o$Y6cU?00Sc1{~)YFG7^Mv5z9C z#AKwUwL9MXzk64DqxW-teZ8QZz_PTV;UX_qM8JJ-vfh5d#l^)bDA4=xWOLB6rm4K# z;pXq5pvQrwrE2`!|`mD85p2ZQ-SFf7+;FY zzt&~*XQrU%8mVG@qsL*|EVBF6$!5$yZEQ@%^F3a`n~!KDDDpH8PEM6AEiE-Q6T)3p zB_+=p7<%t$7QkG>X5M!{NqIX2nzNbrbq9k?oo`C4XL+8;NjbSaS( z^YRR8_V$Nc!|6I-G?)lflOF~Bnwsiy&X{|*Tw{ejSiK{QK?zpPn+_MEDaRxj;Much zeD?Feyi1gsR8>_8(2(16oef_br}cFsoP))I2nKY?!^1;TO3F8&@EJ2RD)D=Le(=rC z&Thr`xQ*)Jvd`Pw`*M05+1lFrTU8Zo$#Z3?$<@llghnwscw(wVOHe>yegWR>dG&xE zfD=84cVIA8H8nys-!GN?XFUQ&%@mX@CVu6TO=3|uJLN5{i{MF%A%rJ@47xl1uY z(UHWA(F{FW?y4haGh1vX3j6z|Ve6&9gMrq_gn}~$j648XCh(C!gLU_GsT)(|8EWP- zl9J9BT}NdM(1$uf!xp`z_ITqDMx~&j0J1l%e>bN~jIk<%7{ttkfsGx>5li*{GN+|w z!L}}?8#T2F5Y5}n`mkCleo(YN+~0tu!0QKK^YhD*k9t9MW7x9AZ8v_( z_HH$ls#McXR`%z7ogLr1+0XIu!6hG&Fc1@X%8fcepnAVMPXB`L{QMjqD%vWbP$*c! zQ~eKumAYjCx9e0ETyv+Rq8;V~ctEF&`M9`FKsYjL_iL)G#91MHir77C-msvjs|!Ly zv-|#ZOaG<|v;M5B{q^LOwjf z&YkOwLJ9uk`0&>eXyHBB1QvHTPCK9t9Kh2}Pcz(;laa|ODUJ5`&sLf|637Ne&d(zow@>KsYa%O8fGq-s5l{OrW5kU^Sg$m0iEtZ8w^4dA81O288~Ca^$TNZmW^o znSi?)(Ypyz;Xj>^RA&%tj)CC@xb7YtEVg>rh~@NyuvTgEVX|P@7e&6t#?9^Q_Ucr# zIgh|Ukt?{=%~gj zcCxOMoRpH%!Ordmhz-Otpt4D_n~$)CzN)j#liZ_oM5fgW?gxIa|l8WSao7t z91X+u+1c5*Z%k0R4>&NeSPyr1LPA0r1C)<3o-6*I@7^6Sie80JhPj(P@AK#T6QDOH(ET(eoRlL~kUhxB4f3t{ou1DaiGmyygqRCpQ=r~dP>wG4 zD;%G3ZGAo4|E~@dJxF=t*RMZQT1Izk#2ufTaZtX2{H^w%rr+xILRdJ{B<0}Xoj%1f z2U20`)ltiF`|ZgfaLys2p;i_a_VcyC928WEk#i2<;s;T9Jyw+6odzcjBo%fs>$SRN zlOcgTY3b?SS)u{=en6OC*z|sFyFEdXpBSH>w&YEJ_}kDSAtfbx(*F!N-pd^Zq|S%i z(Fb@|2P6v+n4E(NB~jsk``i1wn*w&E{O;4%xpxls_QQjNgQKG$&L=nLe*Mtt#t7ma zmH*k8RklA7E-vhHJ@#QoN>sPm4LT|DT?{tUaeoei0Shy899XCS!~O9?xjIX>*BUAm z9U*)Ps56+l2yoRvPNyd)oTJ@jhzX9s>L;OU0r#ib4UQ}A?)#ehe;y+atvcQAX?0wm zR|G_dBjY^nxSpzrp%ji7H4feZx&y3A2bebZs8B*$8jJg@HwZyCxj8vGU@I>oIodB_ zu>n!xq(nx9NTRbZ!LpsHRluUn^mz9-uJlq`vm1+0C$uF1ixkT)62BH1*H2XnF zSon^w;|g;ePx>1ax(hAJif=~Ke(iCz2;7bk4-Xv>x;V%^jQr21CMPG2?oX0I99B(! z_5z6^c2wl1w65+PY>^i@-T<#bR9n%0&BD!%KLZPfm_7Zp?F}sn{0cHo*R!25EhB&T zplur@h9b{pZz-IQ0hgfxKsO-QK&fH$$~ayiKO~Bj*khr7+SAjs>(;z-yTu325%JAL z1Js;xUNjLoIW$p$uRk}wzq`IZ-!CXfB+3BBy42|W7QEyI_riw_D_@$?;Ez8|q4Ymf zftEm=nmrD6LHwT7=nTduCZ3s@VRR?NY2|fX_5-Id5F(-IaRBMU1qYW0kO}0MBX;hB z-f?kpRkaY%71h-zf%SvG2G3Vp3?jYx+>B5yiDNi2JbpEL)t+7HQN~_!zYs;pn>$Ht7~F>+|X+k8TvdJ`4E_|67I-dXFE`ztpxqE+L+w_ts>>UY$u)t+!{1lP8w=pFNo+s%dbZhF%*J{vh=uRIyGDWIEu|q zu?AqILqqbks!RlG-&YvMqe^z>&6kJ1fPMq`3A6Mc&_fwhL^&> zd(&GNr(|2fS{weGD3V^--(P*w6pD`e?S{&zaQ5l1uCM_;ncn{94W2(elg5YPl93hXZTGHF@Sa$3s(iZnYnDPCO>nQ);btP#7v9EI@ z*!qVgSWd=!S0>gPd+{0rG$*p@g6NfRmNPqAS15jW@(A&Joc7MXUDym-w5DFlPN#N9 zb=~2ha*K@?V5$rnSbmk5r--&ew5=XSk$FGwWwA58WAK=PHbR%YU7hIFlaMz!;W3fa zr0BOE$*j?@_k+DLrTVX)bIAtlFs(-ZG7i?7TqEHL4-o;yCV)qCfU?GazzN{v-vJ2l z<-~-&t<|$py%GH+ZG54FFv>L;Q)2SB=Aa+?)HIf(fRDB2Pz%ltQ@uhY`ao8cB|7 zTYOD?mTk9hU!B<2a$}(e5o`#{|9ers2A||#Mft_i0^>Roh4S>-1^cb~qd9g)NP6IF zT(5-O_-+2I68VW;Au0U2#?OM#UxZ$5Fl&}quwHe%*|)cukJ>mq)NKA$0*+aaoPW>P zWvh3_I)a4wacnwL3BM0U$a1w*X1>K{P8*rUT-AhCyjw^ja`Wa6i#V9IV*>W6SMcbM zuOpuMSQWiySr8bG+ni^FH8}KL0*aGydE@YZwuZh6(=*!mBBuUn5!kB)s39nG z7k6)00~B3(NtY4NahvM&VvMNzp*cMBkRH$Ut+(>jj}lx{UOUo!cblDfyR(~gPKuM75~D{N zoi7c8Wzgqu|FO>$r>oT?sq3AdT{{r^^+iRG(VKTwzeeycT4C23Ui8R;VY+Y5sA4~q zZ!Fh}QPW|J+o!EWZsOdyO<+}c|Kcds)5FH=TFhYkdd(1zs(39!H!S)pkA#l}x1_S&SF`I=WKG6_b=V)iyrMI!-+9EpHVY0J!Bjj4}MUV87V zrQNy^jS3zXs%{-W+;<#%tsoCNqQhVZz7054I?dUnO3&RXgAWSFyL|3Uj<+R#D+uM8 z`n6^%zAyJ*T2e1xAJAU&u}~0DRVX%*O~CG0la7`ctvoO>@zT6F^P3vCo>V9O`EbM>Q1yA=D&y{e$-9UE(%lF0Z?Tr^A%T7g8xm(pTUQ}@+HdJZD27;JD81gO} zJu)wGaR=0&OqFjh{5x8b(KeCf)_LQ^nzGEpWpPs|pjNMgoO{GuLmLo48Q!5LkWg5u zgG`%C)K<9o>e7^{CW)LERh%fZP-c^KX3*Jwv$F7zdYjTa^-N!}Exgp!s^eOwZj9T_ z`adBM*v9A;Wa<;1)q=pm9)>8CK{daBAlIyft9zdRL$S=dh^B5yvCJSWqvRAKWFA78 zL`q8=UlPINm!RXfSK^+(l>G9XozT%tlUTw1`lZd}lqnY%i{#cil#Z4b04JN|hH-?N z{0-WY%`RH9+=)EJ{4v`sFMH!%079cxVbN`UKk9@ov3|x)H4)uaRO_5ojSfFf*Yg_oF8gh2xKSx0%LeLduQz z3@@0CM(T@K;=Vk@K6u#(jN10EyCdT(Zrs*=mbnS=Lsuc1u_M`zXpLd>E6CMfe^+3s z=aV{%@pL_X#PX|j`sBU8{ou@!MKKrKGc5CtqrJ3~+FoYcKPErB9M!zdL`Iz}26HVP zJ4W?_cNOw<3la&5pu-BSHyC@6SB!X*MGXz|Le@-m%g4?l*)ao-8saw03G>}NdXpJ^ zs57`WazEE}mRjy5ki2}^b#7|&p$;m9YI!PZ-2*b;?AOA#N%C$O_7rJfy;91Nwo7T2 zQi)^8`(kURcrD_Mmg+@QHe5K*C`ap)K#Zob#PH09rN^v;l`)ptai2jhFKD2IszVH5 zHw?=|J}keY0xI6wWZ2?!;mLUwCA1Lo>f8|LQFzjbmJ8wbiN*&BuAbw6c1J%mXBv=GOi3RUKvQX2qF6tA^XfU`a| z&~sx`-?ob5$C6H@b;aU4wa=u}F7iY5nzrVOPvwaksS6yaQA1R)Ci6=l6;$*iR1V&* zWRjCfcL`{wO7vQW63%2KRTLI`2wp9^2hYuz$Pd0rFpKhZW0A$)_I%$GqjQ~P_RCA5mGqN8gU_)v zjg{vHz-v24{Q zbWlWmxJ{N0Bs$dD4C-@)l9#QX%REtQk^vzxHjESs()V?0&`>?7HzSGrvU6~d7N(l$w4&f9h@eF~pXVCs0d-k66?iuq_3 zletza8HSr8Mu_di zpE77xRuIA-SPc=I(tp?QulUl=-gG(*Y3UBvhDiF7xvA8z?oE;0b3D&bpl2`kiy8lp zpcc9ula%6XpCJnvs^!P4U3iYzFv%wUuvW0zSUY3TYspMxsq-zY-}Dp^VH6I+8+X;u z<8^Rh;0VnY8kq5JSM;3y_{+h4gtX~Y-s!vTGlll?q|BaGq3?_0Blj{e3WL-N8c(fP zixdU2_N+6^SP_o1Y6s&){^2v-y{I4O$MGKJEgj!%OmC~bGW1MO9T<#jY7V&#`05go z3~73tnHd|YpY4(o;nML%h+-*N#tN^0yTsU5P5n7Q5cMA+h}kF2)QTyk6BmuHtX6dz zr29PF*ao>UjQS_mfVkM+yVN~JL`=zdZ<+H$S^$mu)ru|L~HFBy^ zPfnQWV|hMhY2ZG18Jjb!z%UbeWSE9Dpdmq;=lgrvTR3xfCG)6EChP*Um*GW&uzYKI zpgSfd=lpd9%tX7XLOwHV-RCXH3-+M3ffhZYJrg_sAcY4nNpm&uaE*=*;!V1_z3}GC z@^Mx!0San!q(9W)9zo^FGR3(TF_qLRE1B=pRvitC8vBMFL6y$+%LH>j1MzG5Mr2a{ zqE@dpR;AncXOT??^PVUF@VL4YHEdmF(}seJfAt-G23{)_Z~Ap&UKy*Ito+F7ql+$Q z2lYXXJ?)vw$m1|f@m{>qu-N8!4$6zsQh!|qf%Ttr?H*8Lp9yL4a)v#J?4zj&%+3aA zmMq6le~Wx(V|Verge2RyGG=mBCnMWGffCXI_SHU?^{k8OPYlA4o<$kqeIK0{7LTc@ z-jj4tN~#TfKGq64R?ezZO3!09i4yr+tgqo*9esXKWm<4LTl}1vso)xI>OGs9@()$| zqVp?l<6uE58b(;t17q!H@{2NyWvN$%L&_QS9w!QgtXt2|kJfV}?JAY(*j6+MZQSUQ zOJkne`y+E71+?4#Ay;o&`$6mO9lpg$VyJZ`a2uSQBWb3X_5IZFD_mOgUrg<*-e~Lz zG(w+W$te6dwcI$}{bPUoLanTf*z#BIQGtxPH-%Dgxy(m^1|4^Vl&^NAl#5)uwYt%> z@v!*_6B1i_Z#5#DD_#_Qx!>upQcz;W?yO%NY4?;U)W+-JW%x`Mg5$9#YR^I5)YTn6 z=UI1AfAwg!N(|bI^gAn~{K)INC|qR>fkGE5V!6;H`n8Op@c0zlhf=NVGri(IH*);2 z2_j=qzenf3IC*y9BM1aV07YIk!Gcgr{^=#i-~)f-Z5Tzv8LZU8|7VU0v0(#|&Fo7; zr=g0>5phGHqAA`@o68B#YZ+uSl9Ya?I=>qhT&{nvrAP#=9Nkt!$TpTx6HKw)bsX7h zpPBdL2!tYbjO}ZE8Z!>oG@8K~i?CVd2Xzszhr_3N-_!G4)wKVxJby0eve6$wII(1x zLu2cFncE-OzPd>15rq+p>=Jo8SrKrzpM7^=#g!_*Cl+kr)UM;?YSxbl;$sHgyN6wV%oP@>3-i-Rx0)Y5<^hZ{$Y0!Q7n05=ejaO3zb@&Pco z#{w7#0Dl3PBpYDhK{&Ax@xzk|8@*2p0dEZl7u0LZ^cobwV-RC3f!#PMN>JEL0EMHS za>KS}0GnQ3dV%uSKcdK}uC4~frf2SimA_Jlx6aPoz)B&3;W#N7@EQy#-~xC_+5m;s z+G-RKx-BCA8B-iZo)VPrK{*kS!a9GP-(9R!fYKd6r2knyJ`JaLC953|DZB_&K0iMX zKqEL`5OVo1zJiIYM~iT$UMr{+fdYrAnc21jJ{&>@;QnG+Kv0X%`5u5pCp8wM0d8*8 z;-SC}P?-KwRCIK`I@=`p42EI=O}7ycNUI=@FWA|SK;;`CU9Jt`#){u)jNiRu{WEN- ziGhX1u2=sqi|AMlj`P!l*GMr26bSI~x%v5-z=|uQqM|;Agh+@ZougtvI>kP9@~D7{ z($3C~5XE$m_Z}z;%6(ytgG(}H{1!#Nj4_56g}bHeh-A4d@{i0EW3s^vF%D@=mUU)y z^s<@^r~e~NDbE&#i>8Fh&d%5R`iqwqWsLfD4Oz2?Y@N|4!$gqRJXDAYzy4?{obGN3 zQr20WHZHqXZMp3`m0r5H3KOT>A9{7ASpB6e`KJj=Rv__m54TcaVPi{4N-ExxzqV5w zUT&42F`APq-qSZ+pil4d0YuNVtvAMaX-a(fFR22o9y|J_h92EVOISCkoTj?YJfo#` zSQeU5e>+UE-1GCQEAF!4ax@PUu`cTmX(BA+*7xYJgzxLN>FIJ?#KiC; z6%}0jt>b{dHEl-?`ro+M|q0F-V{??pgLv8~<&7`?7co zOR52OTU+78RGCR1(~IYBrRD4iVIMzz()>9jSSZ}{1WBW&{hSWQWqHv~D?~AsN zpBilcw(B$IP2;z$=o)EgG2&w%NieGJVR?7(p&r&BoJ*bTWKz=Z?I|;5DUjS{Q_jCCY>e?GA z*!_1}a6dLo34bi7x%n-fh<8OsKYn)Q*1Ds8S#e1MDyCFqL|V+$6h$tWijjAb`%I2} z&Z_lTPxAhr;WI5xsE_^3rN_=G!w6=@T`DZIap7(Dt?00Ctin5rGKH7Er&!)>)*^2Sc1IPrf+vpzPc2kCV@xE+n;O*uKHWRr#wK z#3j6s1JwAr;g_C-!%R(+JDZ-rGk8*?@16D*gNCdt4u4iRSW0L;DuUJ zrKj{{BHmB7R_h%cE%=r?BoC2%$e;+E(_7+OxNa>v2l^Hn!x!0c?9Mi{oW}NyRwp93 zdZrH_x2IyXaN-cLhcDZqaBFKR%dn?SSbTjx_xC9SOq{3Q!tws3YG-G5_MmL8&uC6z zoogb$bwg3ikPBOb;+5_h8U<8Kr10RVj%9R`v}r0M7$AXv-ZMpOkkqRY3o#K$IG>=XFVc$U;mgj zVRTp6hn?4|LR75!vaH^7nMPunoz~>7hm_XC>s$NW{#^%;69!IfmcZWQ_Z7>yo*dNP z%lqN|B^S!p{JL(=K6xtwSO3yBrTNx7b#34@ zdF7%S#*yh$Fsypb%*1vk zMuePo{!YJJp%QnhSea3H{Mg}Bs7m)5EIEEu$Zcq;u!wI(&zk?ezH@2$QOepSOomMX zKGmzhK6{J3!efKZ4N-&Go+}xRoYgYY?uhF<&_=6w zQzy}arA2YIBH`=NQr{vcNhhf8zF|Ffv^0`#L6z!)KZ3UVLFvKsN5%nLiY(aRZ{Msh zoO9dyVh+o^3!vx9Ta65~SWE5IEuWZf`m3JJRb)62c^o#M%36S}q8(K`PBY*sr-tQz`ag>$$Nk zkLyRhF>z_hhat-s_hov4g$R0#xvv~N(@=94iPr1N{sR+$pisQI+iVdQ{yzK6b$_}H zgwmG#V8c}TPmY}_g2B{Cs~Yo9o;(3j2Ogh~_NTf@mfpCd23A&9PL;f1cmkoJ{T8|r ztR^on|1S_G_)j!R@bK)cHlSl^YisN4leb3^L5%OkuqT#UoYy5mw2_w|;&IpyqAYwx zs4grIAQS-JfpJ$?(P2Y?bqE{>(VDYgeRpp!yKe0ci0J@+`_fE=!VZ)e;93B7l}lzf zNCCGpsJp+GtY?W!z#E_exq5l=GcY`MjHVuOA)}x$s5T$yb_{z23G{yM^(X=GxeQAf zb^zXZaeiJ`TT4ermtRn@bk&QZ;mQJdazAiK0gLN&d$hu+<2^iwnZgkY_HLwt7#9Zn zU`@2Dsi~a;v=pjd+(#9l4}ku!0GC!aFrc8KdhiUm@QjO_XzyeO*#$lTln=nP;dmb4 z6B+u!dH_RBY0t%Y1Y~n07@q(yg4oCpAgl&}=Lj)Yq~;t(Et%2kNfJ$-#j z>y(@vfG+f~$r0cUCrH6@GCPp0z53Y66AA}nK|VD(sa~hs2Itld2F;xTVqL)f)iy|V zOmsB&bV0`>G@Qdtg@p`xV=0-LcLz12%jf6cX}l(9W*z_zX|D6PX*(;SnF!c84pJcm zP*oDgtcQk&HAk-WW~BCvtO8!`)Ud2!VWrATgjik!$Jc-2gn6n23n|A7bZk#rv}&(YA|a zUp;^u0`A#(fm{Lm69^mCd0)oL{kTW0%hyol>^mY}SC8@Zw z(y3klEl8BNP!JQkMF2q!^u!@7?0<2z1i)9oTbW#0G1Ahy09YJ+E-XoCD~T9jiSpmq zI0CNZ3=BZ}I+*qT+D89Kl?Bk2sQmtX2fG0~n^0Oe1*!RO%pm44fP(Y9dv|4e-gyJ1F-U{_V#g~aNg1b z5SUjMV9}LTR6GEtgZ38-9io|RBuL=yTjMk(K&@c(;K#(kz!?K@`Te=k17Vkp8VLo( zU0=X$A4kV+d>B9w8|)V}m6a>)7kI&i7f9gMlOUUW@3S$frP299h4eEYVF5WmpYtg# z(aYbDF51_;4cCH!o-PB9&BvqTCsuYE78Vw8<{(a_PsQOm@{1bBkGL&pF?y?vup96~}05j{I#@6@54Jlnm!I}g6+s(W`e zr!URdjP*hCf1z~7sldy_!^6ufxj{`E`T<-K>sk$qj7(2Tx|ny^6nUmJ(CP35qRsTD z4m${^f$L<|pyu4(`EvTzuD`$TXK?KZg2EI9jQoFeyX)VswfvWJ>VLg7HqmN2R#i~< zbfLp)wCZ9ro@R&!wHHQ|MxwY*T<+L zX|gf@0?lmvEEpnV*s5$Xh5BF_G^a5a7<7rt`OJ+Eyn>Y`;{U z&h?Bb1vhjO9{<*Fu;GcP?U6c)E69%<7AW_X1gO|6@2FTZ-^Z63!U7&@(2+IL)(O2u zwMOEbA<=K^<)A#B%pk6-`@q419qgAa^^ zIB{FZT0Xjf7_{88U4gCm$NV}>^xpOb>TbaCwb+2+&Ggf9swo&JE-CpI*Wr6k9$TxBV(IaAD zLI~;gS?gXgaKN$gG1_qb85g+w@|MKRZ-Nd7TxQky%MSjcNIi>Bz0Pe<$G5jZeTATDlh!-JzlTEAHL&;3)wDTe%Q z<%NwW2}u>a$<|m2tL{^Y*iV5QRK!UU3j?*+pWYLm0zGdOCFyWfVy2~JE7RRJTd&P~ z2zI}ZTT}N8I6owAZoJsEUlNahf^|L+-X@;V{DVP_=v2Q70MM1{ntCcvUwUpLKwgLa z_^i(q$L*azp;LEL-Jj|*lu+oHV9YWDOZ&OKL3u#@Z>* z0!$SrC)B2Wbx_z$6PHM>8B0qN^sRgY>yOt zwL}3q5@`SPTc-cL9|D&LJA*le_@}*Lpaj)fx_jKBKmaO3K%St;

Active Model

Parameters/Expressions

ChatManager

Live2DCharacterManager

Live2DModelManager

Live2DParameterController

EmotionController

ActionController

Live2D Components

\ No newline at end of file diff --git a/Assets/Docs/diagrams/live2d_architecture.svg.meta b/Assets/Docs/diagrams/live2d_architecture.svg.meta new file mode 100644 index 0000000..4e5d73a --- /dev/null +++ b/Assets/Docs/diagrams/live2d_architecture.svg.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 7c9a7be09b8901c419630f98fa918250 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/diagrams/live2d_sequence.mmd b/Assets/Docs/diagrams/live2d_sequence.mmd new file mode 100644 index 0000000..b98fa18 --- /dev/null +++ b/Assets/Docs/diagrams/live2d_sequence.mmd @@ -0,0 +1,21 @@ +sequenceDiagram +participant U as User +participant CM as ChatManager +participant API as ApiService +participant L2D as Live2DCharacterManager +participant EMO as EmotionController +participant ACT as ActionController +participant VO as VoiceManager + +U->>CM: 사용자 입력 전송 +CM->>API: 요청(텍스트, 캐릭터/유저 ID) +API-->>CM: 응답(text, audio, metadata{emotion, action}) +CM->>CM: ChatMessage 변환 및 큐 처리 +CM->>VO: 음성 재생 +CM->>L2D: ApplyReaction(metadata) +L2D->>EMO: SetEmotion(emotion, intensity, duration) +L2D->>ACT: TriggerAction(action) +VO-->>L2D: OnVoiceFinished() +L2D->>EMO: Release/Restore 상태 갱신 + + diff --git a/Assets/Docs/diagrams/live2d_sequence.mmd.meta b/Assets/Docs/diagrams/live2d_sequence.mmd.meta new file mode 100644 index 0000000..559fe93 --- /dev/null +++ b/Assets/Docs/diagrams/live2d_sequence.mmd.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: f0cb5cd19b9e0b84a81fb773e73e4f55 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/diagrams/live2d_sequence.png b/Assets/Docs/diagrams/live2d_sequence.png new file mode 100644 index 0000000000000000000000000000000000000000..970dd95c28bf6a50bcc99f46dc4868dfcb0ed7bd GIT binary patch literal 23552 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

* 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 z?hgP|rKuhC#GrO^VoiceManagerActionControllerEmotionControllerLive2DCharacterManagerApiServiceChatManagerUserVoiceManagerActionControllerEmotionControllerLive2DCharacterManagerApiServiceChatManagerUser사용자 입력 전송요청(텍스트, 캐릭터/유저 ID)응답(text, audio, metadata{emotion, action})ChatMessage 변환 및 큐 처리음성 재생ApplyReaction(metadata)SetEmotion(emotion, intensity, duration)TriggerAction(action)OnVoiceFinished()Release/Restore 상태 갱신 \ No newline at end of file diff --git a/Assets/Docs/diagrams/live2d_sequence.svg.meta b/Assets/Docs/diagrams/live2d_sequence.svg.meta new file mode 100644 index 0000000..3f840c0 --- /dev/null +++ b/Assets/Docs/diagrams/live2d_sequence.svg.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 1c2e02197b6cd3547a0ecf2732bda685 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/diagrams/live2d_sequence@2x.png b/Assets/Docs/diagrams/live2d_sequence@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..aff2e7cabc08c478902c3a429bfa2186cc241606 GIT binary patch literal 40758 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≷;$%PHMIegVjig>fccQpg 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 Date: Tue, 12 Aug 2025 12:36:45 +0900 Subject: [PATCH 02/19] =?UTF-8?q?feat:=20live2D=EB=A5=BC=20=EC=9C=84?= =?UTF-8?q?=ED=95=9C=20=EB=A9=94=EB=8B=88=EC=A0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/Live2d Sence.unity | 263 ++++++++++++++++++ Assets/App/Scenes/Live2d Sence.unity.meta | 7 + .../Character/Script/ILive2DModelApplier.cs | 12 + .../Script/ILive2DModelApplier.cs.meta | 2 + .../Character/Script/ILive2DModelManager.cs | 37 +++ .../Script/ILive2DModelManager.cs.meta | 2 + .../Character/Script/Live2DCharacterConfig.cs | 35 +++ .../Script/Live2DCharacterConfig.cs.meta | 2 + .../Character/Script/Live2DModelApplier.cs | 15 + .../Script/Live2DModelApplier.cs.meta | 2 + .../Character/Script/Live2DModelManager.cs | 67 +++++ .../Script/Live2DModelManager.cs.meta | 2 + .../Character/Script/Live2DModelRegistry.cs | 34 +++ .../Script/Live2DModelRegistry.cs.meta | 2 + 14 files changed, 482 insertions(+) create mode 100644 Assets/App/Scenes/Live2d Sence.unity create mode 100644 Assets/App/Scenes/Live2d Sence.unity.meta create mode 100644 Assets/Domain/Character/Script/ILive2DModelApplier.cs create mode 100644 Assets/Domain/Character/Script/ILive2DModelApplier.cs.meta create mode 100644 Assets/Domain/Character/Script/ILive2DModelManager.cs create mode 100644 Assets/Domain/Character/Script/ILive2DModelManager.cs.meta create mode 100644 Assets/Domain/Character/Script/Live2DCharacterConfig.cs create mode 100644 Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta create mode 100644 Assets/Domain/Character/Script/Live2DModelApplier.cs create mode 100644 Assets/Domain/Character/Script/Live2DModelApplier.cs.meta create mode 100644 Assets/Domain/Character/Script/Live2DModelManager.cs create mode 100644 Assets/Domain/Character/Script/Live2DModelManager.cs.meta create mode 100644 Assets/Domain/Character/Script/Live2DModelRegistry.cs create mode 100644 Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta diff --git a/Assets/App/Scenes/Live2d Sence.unity b/Assets/App/Scenes/Live2d Sence.unity new file mode 100644 index 0000000..5e14456 --- /dev/null +++ b/Assets/App/Scenes/Live2d Sence.unity @@ -0,0 +1,263 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!29 &1 +OcclusionCullingSettings: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_OcclusionBakeSettings: + smallestOccluder: 5 + smallestHole: 0.25 + backfaceThreshold: 100 + m_SceneGUID: 00000000000000000000000000000000 + m_OcclusionCullingData: {fileID: 0} +--- !u!104 &2 +RenderSettings: + m_ObjectHideFlags: 0 + serializedVersion: 10 + m_Fog: 0 + m_FogColor: {r: 0.5, g: 0.5, b: 0.5, a: 1} + m_FogMode: 3 + m_FogDensity: 0.01 + m_LinearFogStart: 0 + m_LinearFogEnd: 300 + m_AmbientSkyColor: {r: 0.212, g: 0.227, b: 0.259, a: 1} + m_AmbientEquatorColor: {r: 0.114, g: 0.125, b: 0.133, a: 1} + m_AmbientGroundColor: {r: 0.047, g: 0.043, b: 0.035, a: 1} + m_AmbientIntensity: 1 + m_AmbientMode: 3 + m_SubtractiveShadowColor: {r: 0.42, g: 0.478, b: 0.627, a: 1} + m_SkyboxMaterial: {fileID: 0} + m_HaloStrength: 0.5 + m_FlareStrength: 1 + m_FlareFadeSpeed: 3 + m_HaloTexture: {fileID: 0} + m_SpotCookie: {fileID: 10001, guid: 0000000000000000e000000000000000, type: 0} + m_DefaultReflectionMode: 0 + m_DefaultReflectionResolution: 128 + m_ReflectionBounces: 1 + m_ReflectionIntensity: 1 + m_CustomReflection: {fileID: 0} + m_Sun: {fileID: 0} + m_UseRadianceAmbientProbe: 0 +--- !u!157 &3 +LightmapSettings: + m_ObjectHideFlags: 0 + serializedVersion: 13 + m_BakeOnSceneLoad: 0 + m_GISettings: + serializedVersion: 2 + m_BounceScale: 1 + m_IndirectOutputScale: 1 + m_AlbedoBoost: 1 + m_EnvironmentLightingMode: 0 + m_EnableBakedLightmaps: 0 + m_EnableRealtimeLightmaps: 0 + m_LightmapEditorSettings: + serializedVersion: 12 + m_Resolution: 2 + m_BakeResolution: 40 + m_AtlasSize: 1024 + m_AO: 0 + m_AOMaxDistance: 1 + m_CompAOExponent: 1 + m_CompAOExponentDirect: 0 + m_ExtractAmbientOcclusion: 0 + m_Padding: 2 + m_LightmapParameters: {fileID: 0} + m_LightmapsBakeMode: 1 + m_TextureCompression: 1 + m_ReflectionCompression: 2 + m_MixedBakeMode: 2 + m_BakeBackend: 1 + m_PVRSampling: 1 + m_PVRDirectSampleCount: 32 + m_PVRSampleCount: 512 + m_PVRBounces: 2 + m_PVREnvironmentSampleCount: 256 + m_PVREnvironmentReferencePointCount: 2048 + m_PVRFilteringMode: 1 + m_PVRDenoiserTypeDirect: 1 + m_PVRDenoiserTypeIndirect: 1 + m_PVRDenoiserTypeAO: 1 + m_PVRFilterTypeDirect: 0 + m_PVRFilterTypeIndirect: 0 + m_PVRFilterTypeAO: 0 + m_PVREnvironmentMIS: 1 + m_PVRCulling: 1 + m_PVRFilteringGaussRadiusDirect: 1 + m_PVRFilteringGaussRadiusIndirect: 1 + m_PVRFilteringGaussRadiusAO: 1 + m_PVRFilteringAtrousPositionSigmaDirect: 0.5 + m_PVRFilteringAtrousPositionSigmaIndirect: 2 + m_PVRFilteringAtrousPositionSigmaAO: 1 + m_ExportTrainingData: 0 + m_TrainingDataDestination: TrainingData + m_LightProbeSampleCountMultiplier: 4 + m_LightingDataAsset: {fileID: 20201, guid: 0000000000000000f000000000000000, type: 0} + m_LightingSettings: {fileID: 0} +--- !u!196 &4 +NavMeshSettings: + serializedVersion: 2 + m_ObjectHideFlags: 0 + m_BuildSettings: + serializedVersion: 3 + agentTypeID: 0 + agentRadius: 0.5 + agentHeight: 2 + agentSlope: 45 + agentClimb: 0.4 + ledgeDropHeight: 0 + maxJumpAcrossDistance: 0 + minRegionArea: 2 + manualCellSize: 0 + cellSize: 0.16666667 + manualTileSize: 0 + tileSize: 256 + buildHeightMesh: 0 + maxJobWorkers: 0 + preserveTilesOutsideBounds: 0 + debug: + m_Flags: 0 + m_NavMeshData: {fileID: 0} +--- !u!1 &893152814 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 893152817} + - component: {fileID: 893152816} + - component: {fileID: 893152815} + - component: {fileID: 893152818} + m_Layer: 0 + m_Name: Main Camera + m_TagString: MainCamera + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!81 &893152815 +AudioListener: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 893152814} + m_Enabled: 1 +--- !u!20 &893152816 +Camera: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 893152814} + m_Enabled: 1 + serializedVersion: 2 + m_ClearFlags: 1 + m_BackGroundColor: {r: 0.19215687, g: 0.3019608, b: 0.4745098, a: 0} + m_projectionMatrixMode: 1 + m_GateFitMode: 2 + m_FOVAxisMode: 0 + m_Iso: 200 + m_ShutterSpeed: 0.005 + m_Aperture: 16 + m_FocusDistance: 10 + m_FocalLength: 50 + m_BladeCount: 5 + m_Curvature: {x: 2, y: 11} + m_BarrelClipping: 0.25 + m_Anamorphism: 0 + m_SensorSize: {x: 36, y: 24} + m_LensShift: {x: 0, y: 0} + m_NormalizedViewPortRect: + serializedVersion: 2 + x: 0 + y: 0 + width: 1 + height: 1 + near clip plane: 0.3 + far clip plane: 1000 + field of view: 60 + orthographic: 1 + orthographic size: 5 + m_Depth: -1 + m_CullingMask: + serializedVersion: 2 + m_Bits: 4294967295 + m_RenderingPath: -1 + m_TargetTexture: {fileID: 0} + m_TargetDisplay: 0 + m_TargetEye: 3 + m_HDR: 1 + m_AllowMSAA: 1 + m_AllowDynamicResolution: 0 + m_ForceIntoRT: 0 + m_OcclusionCulling: 1 + m_StereoConvergence: 10 + m_StereoSeparation: 0.022 +--- !u!4 &893152817 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 893152814} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: -10} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &893152818 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 893152814} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a79441f348de89743a2939f4d699eac1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_RenderShadows: 1 + m_RequiresDepthTextureOption: 2 + m_RequiresOpaqueTextureOption: 2 + m_CameraType: 0 + m_Cameras: [] + m_RendererIndex: -1 + m_VolumeLayerMask: + serializedVersion: 2 + m_Bits: 1 + m_VolumeTrigger: {fileID: 0} + m_VolumeFrameworkUpdateModeOption: 2 + m_RenderPostProcessing: 0 + m_Antialiasing: 0 + m_AntialiasingQuality: 2 + m_StopNaN: 0 + m_Dithering: 0 + m_ClearDepth: 1 + m_AllowXRRendering: 1 + m_AllowHDROutput: 1 + m_UseScreenCoordOverride: 0 + m_ScreenSizeOverride: {x: 0, y: 0, z: 0, w: 0} + m_ScreenCoordScaleBias: {x: 0, y: 0, z: 0, w: 0} + m_RequiresDepthTexture: 0 + m_RequiresColorTexture: 0 + m_Version: 2 + m_TaaSettings: + m_Quality: 3 + m_FrameInfluence: 0.1 + m_JitterScale: 1 + m_MipBias: 0 + m_VarianceClampScale: 0.9 + m_ContrastAdaptiveSharpening: 0 +--- !u!1660057539 &9223372036854775807 +SceneRoots: + m_ObjectHideFlags: 0 + m_Roots: + - {fileID: 893152817} diff --git a/Assets/App/Scenes/Live2d Sence.unity.meta b/Assets/App/Scenes/Live2d Sence.unity.meta new file mode 100644 index 0000000..1e774b7 --- /dev/null +++ b/Assets/App/Scenes/Live2d Sence.unity.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 834aa5ca0d5c48149b8c17792ca7ae9a +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Domain/Character/Script/ILive2DModelApplier.cs b/Assets/Domain/Character/Script/ILive2DModelApplier.cs new file mode 100644 index 0000000..3a3754d --- /dev/null +++ b/Assets/Domain/Character/Script/ILive2DModelApplier.cs @@ -0,0 +1,12 @@ +using UnityEngine; + +namespace ProjectVG.Domain.Character.Service +{ + public interface ILive2DModelApplier + { + /** 활성 모델과 구성에 대해 LookAt, LipSync, 썸네일 등 시각 설정을 적용한다. */ + void Apply(GameObject activeModel, ModelConfig modelConfig); + } +} + + diff --git a/Assets/Domain/Character/Script/ILive2DModelApplier.cs.meta b/Assets/Domain/Character/Script/ILive2DModelApplier.cs.meta new file mode 100644 index 0000000..29399d9 --- /dev/null +++ b/Assets/Domain/Character/Script/ILive2DModelApplier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2234415ca6f22a14e9e1db4b6ca01091 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/ILive2DModelManager.cs b/Assets/Domain/Character/Script/ILive2DModelManager.cs new file mode 100644 index 0000000..1815a0c --- /dev/null +++ b/Assets/Domain/Character/Script/ILive2DModelManager.cs @@ -0,0 +1,37 @@ +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Domain.Character.Service +{ + public interface ILive2DModelManager + { + /** Live2D 모델 관리를 위한 초기 설정을 수행한다. */ + void Initialize(ProjectVG.Domain.Character.Live2D.Model.Live2DModelRegistry modelRegistry, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig defaultCharacterConfig); + + /** 지정한 캐릭터 모델을 비동기 로드한다. */ + UniTask LoadModelAsync(string characterId); + + /** 현재 활성 모델을 언로드한다. */ + void UnloadActiveModel(); + + /** 지정한 캐릭터 ID의 모델을 활성 모델로 전환한다. */ + void SetActiveModel(string characterId); + + /** 현재 활성 모델 GameObject를 반환한다. */ + GameObject GetActiveModel(); + + /** 지정한 캐릭터 ID의 모델이 로드되어 있는지 반환한다. */ + bool HasModel(string characterId); + + /** 지정한 캐릭터 ID의 모델을 사전 로드한다. */ + UniTask PreloadModelAsync(string characterId); + + /** 활성 모델의 가시성을 설정한다. */ + void SetVisibility(bool isVisible); + + /** 활성 모델에 캐릭터별 ModelConfig를 적용한다. */ + void ApplyModelConfig(ModelConfig modelConfig); + } +} + + diff --git a/Assets/Domain/Character/Script/ILive2DModelManager.cs.meta b/Assets/Domain/Character/Script/ILive2DModelManager.cs.meta new file mode 100644 index 0000000..32095f3 --- /dev/null +++ b/Assets/Domain/Character/Script/ILive2DModelManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a7acebd7cbed2fd499cd205d2797e302 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Live2DCharacterConfig.cs b/Assets/Domain/Character/Script/Live2DCharacterConfig.cs new file mode 100644 index 0000000..34e6055 --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DCharacterConfig.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace ProjectVG.Domain.Character.Live2D.Model +{ + [CreateAssetMenu(fileName = "Live2DCharacterConfig", menuName = "ProjectVG/Live2D/CharacterConfig", order = 100)] + public class Live2DCharacterConfig : ScriptableObject + { + [Serializable] + public class EmotionMapping + { + public string emotionKey; + public string expressionName; + public float defaultIntensity = 0.5f; + public int defaultDurationMs = 2000; + } + + [Serializable] + public class ActionMapping + { + public string actionKey; + public string motionGroup; + public string motionName; + } + + public string characterId; + public GameObject characterPrefab; + public ModelConfig modelConfig; + public List emotionMappings = new List(); + public List actionMappings = new List(); + } +} + + diff --git a/Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta b/Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta new file mode 100644 index 0000000..c90ec6d --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8513455b30303c0408dfdf193b413316 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Live2DModelApplier.cs b/Assets/Domain/Character/Script/Live2DModelApplier.cs new file mode 100644 index 0000000..fd1b508 --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DModelApplier.cs @@ -0,0 +1,15 @@ +using UnityEngine; + +namespace ProjectVG.Domain.Character.Service +{ + public class Live2DModelApplier : MonoBehaviour, ILive2DModelApplier + { + [SerializeField] private bool _autoApplyOnEnable = true; + + public void Apply(GameObject activeModel, ModelConfig modelConfig) + { + } + } +} + + diff --git a/Assets/Domain/Character/Script/Live2DModelApplier.cs.meta b/Assets/Domain/Character/Script/Live2DModelApplier.cs.meta new file mode 100644 index 0000000..132d5ba --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DModelApplier.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 599c8846d73d1244a80788addacb4285 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Live2DModelManager.cs b/Assets/Domain/Character/Script/Live2DModelManager.cs new file mode 100644 index 0000000..f9bc715 --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DModelManager.cs @@ -0,0 +1,67 @@ +using System.Collections.Generic; +using UnityEngine; +using Cysharp.Threading.Tasks; + +namespace ProjectVG.Domain.Character.Service +{ + public class Live2DModelManager : MonoBehaviour, ILive2DModelManager + { + [SerializeField] private Transform _modelRoot; + [SerializeField] private ProjectVG.Domain.Character.Live2D.Model.Live2DModelRegistry _modelRegistry; + [SerializeField] private ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig _defaultCharacterConfig; + + private readonly Dictionary _characterIdToInstance = new Dictionary(); + private string _activeCharacterId; + + /** Live2D 모델 관리를 위한 초기 설정을 수행한다. */ + public void Initialize(ProjectVG.Domain.Character.Live2D.Model.Live2DModelRegistry modelRegistry, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig defaultCharacterConfig) + { + } + + /** 지정한 캐릭터 모델을 비동기 로드한다. */ + public UniTask LoadModelAsync(string characterId) + { + return default; + } + + /** 현재 활성 모델을 언로드한다. */ + public void UnloadActiveModel() + { + } + + /** 지정한 캐릭터 ID의 모델을 활성 모델로 전환한다. */ + public void SetActiveModel(string characterId) + { + } + + /** 현재 활성 모델 GameObject를 반환한다. */ + public GameObject GetActiveModel() + { + return null; + } + + /** 지정한 캐릭터 ID의 모델이 로드되어 있는지 반환한다. */ + public bool HasModel(string characterId) + { + return false; + } + + /** 지정한 캐릭터 ID의 모델을 사전 로드한다. */ + public UniTask PreloadModelAsync(string characterId) + { + return default; + } + + /** 활성 모델의 가시성을 설정한다. */ + public void SetVisibility(bool isVisible) + { + } + + /** 활성 모델에 캐릭터별 ModelConfig를 적용한다. */ + public void ApplyModelConfig(ModelConfig modelConfig) + { + } + } +} + + diff --git a/Assets/Domain/Character/Script/Live2DModelManager.cs.meta b/Assets/Domain/Character/Script/Live2DModelManager.cs.meta new file mode 100644 index 0000000..58c5d74 --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DModelManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: bbd7bfb36017db743b3b7cab74183970 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Live2DModelRegistry.cs b/Assets/Domain/Character/Script/Live2DModelRegistry.cs new file mode 100644 index 0000000..afcee32 --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DModelRegistry.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using UnityEngine; + +namespace ProjectVG.Domain.Character.Live2D.Model +{ + [CreateAssetMenu(fileName = "Live2DModelRegistry", menuName = "ProjectVG/Live2D/ModelRegistry", order = 101)] + public class Live2DModelRegistry : ScriptableObject + { + [System.Serializable] + public class Entry + { + public string characterId; + public Live2DCharacterConfig characterConfig; + } + + [SerializeField] private List _entries = new List(); + + public bool TryGetConfig(string characterId, out Live2DCharacterConfig config) + { + foreach (var e in _entries) + { + if (e != null && e.characterId == characterId) + { + config = e.characterConfig; + return config != null; + } + } + config = null; + return false; + } + } +} + + diff --git a/Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta b/Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta new file mode 100644 index 0000000..8f700e4 --- /dev/null +++ b/Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7a9942477d782f94baa8697ff6d415ca \ No newline at end of file From 5b3d798cedd478153b053b4f02723e3be0768efd Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 12 Aug 2025 14:48:56 +0900 Subject: [PATCH 03/19] =?UTF-8?q?feat:=20=EB=AA=A8=EB=8D=B8=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/Live2d Sence.unity | 94 ++++++++ .../Character/Script/ILive2DModelApplier.cs | 2 +- .../Character/Script/ILive2DModelManager.cs | 4 +- .../Character/Script/Live2DCharacterConfig.cs | 96 ++++++++- .../Character/Script/Live2DModelApplier.cs | 2 +- .../Character/Script/Live2DModelManager.cs | 204 ++++++++++++++++-- 6 files changed, 376 insertions(+), 26 deletions(-) diff --git a/Assets/App/Scenes/Live2d Sence.unity b/Assets/App/Scenes/Live2d Sence.unity index 5e14456..8d4fc2d 100644 --- a/Assets/App/Scenes/Live2d Sence.unity +++ b/Assets/App/Scenes/Live2d Sence.unity @@ -256,8 +256,102 @@ MonoBehaviour: m_MipBias: 0 m_VarianceClampScale: 0.9 m_ContrastAdaptiveSharpening: 0 +--- !u!1 &1607417702 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1607417703} + m_Layer: 0 + m_Name: CharacterRoot + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1607417703 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1607417702} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &1916112489 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1916112490} + - component: {fileID: 1916112492} + - component: {fileID: 1916112491} + m_Layer: 0 + m_Name: Live2D Manager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1916112490 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916112489} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1916112491 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916112489} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 599c8846d73d1244a80788addacb4285, type: 3} + m_Name: + m_EditorClassIdentifier: + _autoApplyOnEnable: 1 +--- !u!114 &1916112492 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916112489} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: bbd7bfb36017db743b3b7cab74183970, type: 3} + m_Name: + m_EditorClassIdentifier: + _modelRoot: {fileID: 1607417703} + _modelRegistry: {fileID: 11400000, guid: faf582e392fb4334aa0f4dd41df09e0d, type: 2} + _defaultCharacterConfig: {fileID: 11400000, guid: 85f44d149f02e394597f11878de45759, type: 2} --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 m_Roots: - {fileID: 893152817} + - {fileID: 1916112490} + - {fileID: 1607417703} diff --git a/Assets/Domain/Character/Script/ILive2DModelApplier.cs b/Assets/Domain/Character/Script/ILive2DModelApplier.cs index 3a3754d..d621fdb 100644 --- a/Assets/Domain/Character/Script/ILive2DModelApplier.cs +++ b/Assets/Domain/Character/Script/ILive2DModelApplier.cs @@ -5,7 +5,7 @@ namespace ProjectVG.Domain.Character.Service public interface ILive2DModelApplier { /** 활성 모델과 구성에 대해 LookAt, LipSync, 썸네일 등 시각 설정을 적용한다. */ - void Apply(GameObject activeModel, ModelConfig modelConfig); + void Apply(GameObject activeModel, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig characterConfig); } } diff --git a/Assets/Domain/Character/Script/ILive2DModelManager.cs b/Assets/Domain/Character/Script/ILive2DModelManager.cs index 1815a0c..b85593a 100644 --- a/Assets/Domain/Character/Script/ILive2DModelManager.cs +++ b/Assets/Domain/Character/Script/ILive2DModelManager.cs @@ -29,8 +29,8 @@ public interface ILive2DModelManager /** 활성 모델의 가시성을 설정한다. */ void SetVisibility(bool isVisible); - /** 활성 모델에 캐릭터별 ModelConfig를 적용한다. */ - void ApplyModelConfig(ModelConfig modelConfig); + /** 활성 모델에 캐릭터별 Live2DCharacterConfig를 적용한다. */ + void ApplyCharacterConfig(ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig characterConfig); } } diff --git a/Assets/Domain/Character/Script/Live2DCharacterConfig.cs b/Assets/Domain/Character/Script/Live2DCharacterConfig.cs index 34e6055..cd3b030 100644 --- a/Assets/Domain/Character/Script/Live2DCharacterConfig.cs +++ b/Assets/Domain/Character/Script/Live2DCharacterConfig.cs @@ -10,25 +10,111 @@ public class Live2DCharacterConfig : ScriptableObject [Serializable] public class EmotionMapping { + [Header("감정 설정")] + [Tooltip("감정 키입니다. 서버에서 전송되는 감정 값과 일치해야 합니다.")] public string emotionKey; + + [Tooltip("Live2D Expression 이름입니다. 모델의 표정 파일명과 일치해야 합니다.")] public string expressionName; + + [Header("기본값")] + [Tooltip("감정의 기본 강도입니다. (0.0 ~ 1.0)")] + [Range(0f, 1f)] public float defaultIntensity = 0.5f; + + [Tooltip("감정의 기본 지속시간입니다. (밀리초)")] + [Range(500, 10000)] public int defaultDurationMs = 2000; } [Serializable] public class ActionMapping { + [Header("행동 설정")] + [Tooltip("행동 키입니다. 서버에서 전송되는 행동 값과 일치해야 합니다.")] public string actionKey; + + [Tooltip("Live2D 모션 그룹 이름입니다.")] public string motionGroup; + + [Tooltip("Live2D 모션 파일 이름입니다.")] public string motionName; } - public string characterId; - public GameObject characterPrefab; - public ModelConfig modelConfig; - public List emotionMappings = new List(); - public List actionMappings = new List(); + [Header("[ 캐릭터 기본정보 ]")] + [Space(5)] + [Tooltip("캐릭터 고유 ID")] + [SerializeField] private string characterId; + + [Tooltip("캐릭터 이름")] + [SerializeField] private string characterName; + + [Tooltip("Live2D 캐릭터 프리팹 (Cubism 모델 포함)")] + [SerializeField] private GameObject characterPrefab; + + + [Tooltip("캐릭터 썸네일 이미지")] + [SerializeField] private Texture2D thumbnail; + + [Tooltip("캐릭터 설명")] + [SerializeField, Multiline] private string characterDescription; + + [Space(5)] + [Header("────────────────────────────────────────")] + [Header("[ 행동/표정 ]")] + [Space(2)] + [Tooltip("감정 → Live2D Expression 매핑")] + [SerializeField] private List emotionMappings = new List(); + + [Tooltip("행동 → Live2D Motion 매핑")] + [SerializeField] private List actionMappings = new List(); + + [Space(5)] + [Header("────────────────────────────────────────")] + [Header("[ 시선 설정 ]")] + [Space(2)] + + [Tooltip("시선 추적 사용 여부")] + [SerializeField] private bool isLockAtActive = true; + + [Tooltip("시선 민감도 (값이 클수록 회전이 커짐)")] + [Range(0f, 30f)] + [SerializeField] private float lookSensitivity = 1.0f; + + [Tooltip("시선 반응 속도 (값이 작을수록 빠름)")] + [Range(0f, 5f)] + [SerializeField] private float lockAtDamping = 0.0f; + + [Space(5)] + [Header("────────────────────────────────────────")] + [Header("[ 립싱크 설정 ]")] + [Space(2)] + + [Tooltip("음량 배수 (1 = 기본)")] + [Range(1f, 10f)] + [SerializeField] private float gain = 1f; + + [Tooltip("입 움직임 부드러움 (값이 클수록 부드럽지만 부하 증가)")] + [Range(0f, 1f)] + [SerializeField] private float smoothing = 1f; + + // 캐릭터 기본정보 + public string CharacterId => characterId; + public string CharacterName => characterName; + public GameObject CharacterPrefab => characterPrefab; + public Texture2D Thumbnail => thumbnail; + public string CharacterDescription => characterDescription; + + // 행동/표정 + public List EmotionMappings => emotionMappings; + public List ActionMappings => actionMappings; + + // 세부 설정 + public bool IsLockAtActive => isLockAtActive; + public float LookSensitivity => lookSensitivity; + public float LockAtDamping => lockAtDamping; + public float Gain => gain; + public float Smoothing => smoothing; } } diff --git a/Assets/Domain/Character/Script/Live2DModelApplier.cs b/Assets/Domain/Character/Script/Live2DModelApplier.cs index fd1b508..c748130 100644 --- a/Assets/Domain/Character/Script/Live2DModelApplier.cs +++ b/Assets/Domain/Character/Script/Live2DModelApplier.cs @@ -6,7 +6,7 @@ public class Live2DModelApplier : MonoBehaviour, ILive2DModelApplier { [SerializeField] private bool _autoApplyOnEnable = true; - public void Apply(GameObject activeModel, ModelConfig modelConfig) + public void Apply(GameObject activeModel, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig characterConfig) { } } diff --git a/Assets/Domain/Character/Script/Live2DModelManager.cs b/Assets/Domain/Character/Script/Live2DModelManager.cs index f9bc715..7cd157b 100644 --- a/Assets/Domain/Character/Script/Live2DModelManager.cs +++ b/Assets/Domain/Character/Script/Live2DModelManager.cs @@ -1,65 +1,235 @@ using System.Collections.Generic; using UnityEngine; using Cysharp.Threading.Tasks; +using ProjectVG.Domain.Character.Live2D.Model; +using UnityEngine.UI; +using Live2D.Cubism.Framework.LookAt; +using Live2D.Cubism.Framework.MouthMovement; +#if UNITY_EDITOR +using UnityEditor; +#endif namespace ProjectVG.Domain.Character.Service { public class Live2DModelManager : MonoBehaviour, ILive2DModelManager { [SerializeField] private Transform _modelRoot; - [SerializeField] private ProjectVG.Domain.Character.Live2D.Model.Live2DModelRegistry _modelRegistry; - [SerializeField] private ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig _defaultCharacterConfig; + [SerializeField] private Live2DModelRegistry _modelRegistry; + [SerializeField] private Live2DCharacterConfig _defaultCharacterConfig; + [SerializeField] private CubismLookTarget _cubismLookTarget; + [SerializeField] private AudioSource _voiceSource; + [SerializeField] private Button _expressionChangeBtn; private readonly Dictionary _characterIdToInstance = new Dictionary(); private string _activeCharacterId; - /** Live2D 모델 관리를 위한 초기 설정을 수행한다. */ - public void Initialize(ProjectVG.Domain.Character.Live2D.Model.Live2DModelRegistry modelRegistry, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig defaultCharacterConfig) + ///

+ /// Live2D 모델 관리를 위한 초기 설정을 수행한다. + /// + public void Initialize(Live2DModelRegistry modelRegistry, Live2DCharacterConfig defaultCharacterConfig) { + _modelRegistry = modelRegistry; + _defaultCharacterConfig = defaultCharacterConfig; + if (_modelRoot == null) + { + _modelRoot = transform; + } } - /** 지정한 캐릭터 모델을 비동기 로드한다. */ + /// + /// 지정한 캐릭터 모델을 비동기 로드한다. + /// public UniTask LoadModelAsync(string characterId) { - return default; + if (string.IsNullOrEmpty(characterId)) + { + return UniTask.FromResult(null); + } + if (_characterIdToInstance.TryGetValue(characterId, out var existing)) + { + return UniTask.FromResult(existing); + } + Live2DCharacterConfig config = null; + if (_modelRegistry != null) + { + _modelRegistry.TryGetConfig(characterId, out config); + } + if (config == null) + { + config = _defaultCharacterConfig; + } + if (config == null || config.CharacterPrefab == null) + { + return UniTask.FromResult(null); + } + var instance = Instantiate(config.CharacterPrefab, _modelRoot != null ? _modelRoot : transform); + instance.name = characterId; + instance.SetActive(false); + _characterIdToInstance[characterId] = instance; + return UniTask.FromResult(instance); } - /** 현재 활성 모델을 언로드한다. */ + /// + /// 현재 활성 모델을 언로드한다. + /// public void UnloadActiveModel() { + if (string.IsNullOrEmpty(_activeCharacterId)) + { + return; + } + if (_characterIdToInstance.TryGetValue(_activeCharacterId, out var go) && go != null) + { + Destroy(go); + } + _characterIdToInstance.Remove(_activeCharacterId); + _activeCharacterId = null; } - /** 지정한 캐릭터 ID의 모델을 활성 모델로 전환한다. */ + /// + /// 지정한 캐릭터 ID의 모델을 활성 모델로 전환한다. + /// public void SetActiveModel(string characterId) { + if (string.IsNullOrEmpty(characterId)) + { + return; + } + if (!_characterIdToInstance.TryGetValue(characterId, out var target)) + { + return; + } + foreach (var kv in _characterIdToInstance) + { + if (kv.Value != null) + { + kv.Value.SetActive(false); + } + } + target.SetActive(true); + _activeCharacterId = characterId; } - /** 현재 활성 모델 GameObject를 반환한다. */ + /// + /// 현재 활성 모델 GameObject를 반환한다. + /// public GameObject GetActiveModel() { - return null; + if (string.IsNullOrEmpty(_activeCharacterId)) + { + return null; + } + _characterIdToInstance.TryGetValue(_activeCharacterId, out var go); + return go; } - /** 지정한 캐릭터 ID의 모델이 로드되어 있는지 반환한다. */ + /// + /// 지정한 캐릭터 ID의 모델이 로드되어 있는지 반환한다. + /// public bool HasModel(string characterId) { - return false; + return !string.IsNullOrEmpty(characterId) && _characterIdToInstance.ContainsKey(characterId); } - /** 지정한 캐릭터 ID의 모델을 사전 로드한다. */ + /// + /// 지정한 캐릭터 ID의 모델을 사전 로드한다. + /// public UniTask PreloadModelAsync(string characterId) { - return default; + return LoadModelAsync(characterId).AsUniTask(); } - /** 활성 모델의 가시성을 설정한다. */ + /// + /// 활성 모델의 가시성을 설정한다. + /// public void SetVisibility(bool isVisible) { + var active = GetActiveModel(); + if (active == null) + { + return; + } + active.SetActive(isVisible); } - /** 활성 모델에 캐릭터별 ModelConfig를 적용한다. */ - public void ApplyModelConfig(ModelConfig modelConfig) + /// + /// 활성 모델에 캐릭터별 Live2DCharacterConfig를 적용한다. + /// + public void ApplyCharacterConfig(Live2DCharacterConfig characterConfig) { + var active = GetActiveModel(); + if (active == null || characterConfig == null) + { + return; + } + + var lookController = active.GetComponent(); + if (lookController != null && _cubismLookTarget != null) + { + lookController.Target = _cubismLookTarget.gameObject; + lookController.Damping = characterConfig.LockAtDamping; + _cubismLookTarget.Initialize(ToModelConfigProxy(characterConfig)); + } + + var mouthInput = active.GetComponent(); + if (mouthInput != null && _voiceSource != null) + { + mouthInput.AudioInput = _voiceSource; + mouthInput.Gain = characterConfig.Gain; + mouthInput.Smoothing = characterConfig.Smoothing; + } + + var hitHandler = active.GetComponent(); + if (hitHandler != null) + { + hitHandler.Initialize(); + if (_expressionChangeBtn != null) + { + _expressionChangeBtn.onClick.RemoveAllListeners(); + _expressionChangeBtn.onClick.AddListener(hitHandler.ExpressionChange_Btn); + } + } + } + + /// + /// Live2DCharacterConfig를 ModelConfig로 임시 변환한다. (CubismLookTarget.Initialize 호환성) + /// + private ModelConfig ToModelConfigProxy(Live2DCharacterConfig cfg) + { + // ModelConfig의 필드가 private이므로 리플렉션을 사용하여 설정 + var proxy = ScriptableObject.CreateInstance(); + + // 리플렉션을 사용하여 private 필드에 값 설정 + var type = typeof(ModelConfig); + + var modelNameField = type.GetField("modelName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + modelNameField?.SetValue(proxy, cfg.CharacterName); + + var modelDescriptionField = type.GetField("modelDescription", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + modelDescriptionField?.SetValue(proxy, cfg.CharacterDescription); + + var thumbnailField = type.GetField("thumbnail", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + thumbnailField?.SetValue(proxy, cfg.Thumbnail); + + var lookSensitivityField = type.GetField("lookSensitivity", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + lookSensitivityField?.SetValue(proxy, cfg.LookSensitivity); + + var lockAtDampingField = type.GetField("lockAtDamping", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + lockAtDampingField?.SetValue(proxy, cfg.LockAtDamping); + + var isLockAtActiveField = type.GetField("isLockAtActive", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + isLockAtActiveField?.SetValue(proxy, cfg.IsLockAtActive); + + var gainField = type.GetField("gain", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + gainField?.SetValue(proxy, cfg.Gain); + + var smoothingField = type.GetField("smoothing", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + smoothingField?.SetValue(proxy, cfg.Smoothing); + + var modelPrefabField = type.GetField("modelPrefab", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + modelPrefabField?.SetValue(proxy, cfg.CharacterPrefab); + + return proxy; } } } From 49d44857a2b8d2923c0aa1a96d170ed0b139ad8b Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 12 Aug 2025 15:17:38 +0900 Subject: [PATCH 04/19] =?UTF-8?q?refactory:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Domain/Character/Script/Component.meta | 8 ++++++++ .../Character/Script/{ => Component}/CubismHitHandler.cs | 0 .../Script/{ => Component}/CubismHitHandler.cs.meta | 0 .../Character/Script/{ => Component}/CubismLookTarget.cs | 0 .../Script/{ => Component}/CubismLookTarget.cs.meta | 0 Assets/Domain/Character/Script/Config.meta | 8 ++++++++ .../Script/{ => Config}/Live2DCharacterConfig.cs | 0 .../Character/Script/Config/Live2DCharacterConfig.cs.meta | 2 ++ .../Character/Script/{ => Config}/Live2DModelRegistry.cs | 0 .../Character/Script/Config/Live2DModelRegistry.cs.meta | 2 ++ Assets/Domain/Character/Script/Controller.meta | 8 ++++++++ .../Script/{ => Controller}/ILive2DModelApplier.cs | 0 .../Script/{ => Controller}/ILive2DModelApplier.cs.meta | 0 .../Script/{ => Controller}/Live2DModelApplier.cs | 0 .../Script/{ => Controller}/Live2DModelApplier.cs.meta | 0 .../Domain/Character/Script/Live2DCharacterConfig.cs.meta | 2 -- .../Domain/Character/Script/Live2DModelRegistry.cs.meta | 2 -- Assets/Domain/Character/Script/Manager.meta | 8 ++++++++ .../Character/Script/{ => Manager}/ILive2DModelManager.cs | 0 .../Script/{ => Manager}/ILive2DModelManager.cs.meta | 0 .../Character/Script/{ => Manager}/Live2DModelManager.cs | 0 .../Script/{ => Manager}/Live2DModelManager.cs.meta | 0 Assets/Domain/Character/Script/Test.meta | 8 ++++++++ Assets/Domain/Character/Script/{ => Test}/TestVoice.cs | 0 .../Domain/Character/Script/{ => Test}/TestVoice.cs.meta | 0 25 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 Assets/Domain/Character/Script/Component.meta rename Assets/Domain/Character/Script/{ => Component}/CubismHitHandler.cs (100%) rename Assets/Domain/Character/Script/{ => Component}/CubismHitHandler.cs.meta (100%) rename Assets/Domain/Character/Script/{ => Component}/CubismLookTarget.cs (100%) rename Assets/Domain/Character/Script/{ => Component}/CubismLookTarget.cs.meta (100%) create mode 100644 Assets/Domain/Character/Script/Config.meta rename Assets/Domain/Character/Script/{ => Config}/Live2DCharacterConfig.cs (100%) create mode 100644 Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta rename Assets/Domain/Character/Script/{ => Config}/Live2DModelRegistry.cs (100%) create mode 100644 Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs.meta create mode 100644 Assets/Domain/Character/Script/Controller.meta rename Assets/Domain/Character/Script/{ => Controller}/ILive2DModelApplier.cs (100%) rename Assets/Domain/Character/Script/{ => Controller}/ILive2DModelApplier.cs.meta (100%) rename Assets/Domain/Character/Script/{ => Controller}/Live2DModelApplier.cs (100%) rename Assets/Domain/Character/Script/{ => Controller}/Live2DModelApplier.cs.meta (100%) delete mode 100644 Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta delete mode 100644 Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta create mode 100644 Assets/Domain/Character/Script/Manager.meta rename Assets/Domain/Character/Script/{ => Manager}/ILive2DModelManager.cs (100%) rename Assets/Domain/Character/Script/{ => Manager}/ILive2DModelManager.cs.meta (100%) rename Assets/Domain/Character/Script/{ => Manager}/Live2DModelManager.cs (100%) rename Assets/Domain/Character/Script/{ => Manager}/Live2DModelManager.cs.meta (100%) create mode 100644 Assets/Domain/Character/Script/Test.meta rename Assets/Domain/Character/Script/{ => Test}/TestVoice.cs (100%) rename Assets/Domain/Character/Script/{ => Test}/TestVoice.cs.meta (100%) diff --git a/Assets/Domain/Character/Script/Component.meta b/Assets/Domain/Character/Script/Component.meta new file mode 100644 index 0000000..06915c6 --- /dev/null +++ b/Assets/Domain/Character/Script/Component.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 7fb0bddb7904cff4d85d4afa552339be +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Domain/Character/Script/CubismHitHandler.cs b/Assets/Domain/Character/Script/Component/CubismHitHandler.cs similarity index 100% rename from Assets/Domain/Character/Script/CubismHitHandler.cs rename to Assets/Domain/Character/Script/Component/CubismHitHandler.cs diff --git a/Assets/Domain/Character/Script/CubismHitHandler.cs.meta b/Assets/Domain/Character/Script/Component/CubismHitHandler.cs.meta similarity index 100% rename from Assets/Domain/Character/Script/CubismHitHandler.cs.meta rename to Assets/Domain/Character/Script/Component/CubismHitHandler.cs.meta diff --git a/Assets/Domain/Character/Script/CubismLookTarget.cs b/Assets/Domain/Character/Script/Component/CubismLookTarget.cs similarity index 100% rename from Assets/Domain/Character/Script/CubismLookTarget.cs rename to Assets/Domain/Character/Script/Component/CubismLookTarget.cs diff --git a/Assets/Domain/Character/Script/CubismLookTarget.cs.meta b/Assets/Domain/Character/Script/Component/CubismLookTarget.cs.meta similarity index 100% rename from Assets/Domain/Character/Script/CubismLookTarget.cs.meta rename to Assets/Domain/Character/Script/Component/CubismLookTarget.cs.meta diff --git a/Assets/Domain/Character/Script/Config.meta b/Assets/Domain/Character/Script/Config.meta new file mode 100644 index 0000000..ea4a79f --- /dev/null +++ b/Assets/Domain/Character/Script/Config.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 9b920a8a8e1a9c146bd2843d87eb15be +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Domain/Character/Script/Live2DCharacterConfig.cs b/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs similarity index 100% rename from Assets/Domain/Character/Script/Live2DCharacterConfig.cs rename to Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs diff --git a/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta b/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta new file mode 100644 index 0000000..a059ad0 --- /dev/null +++ b/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2a91fff055b553542acd1857fd9bbf0f \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Live2DModelRegistry.cs b/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs similarity index 100% rename from Assets/Domain/Character/Script/Live2DModelRegistry.cs rename to Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs diff --git a/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs.meta b/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs.meta new file mode 100644 index 0000000..bd51e18 --- /dev/null +++ b/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a663c59d4d7aa174dbef2ec581104a3d \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Controller.meta b/Assets/Domain/Character/Script/Controller.meta new file mode 100644 index 0000000..830fe1c --- /dev/null +++ b/Assets/Domain/Character/Script/Controller.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 37c695efe237c2a47888f4ce37fa681e +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Domain/Character/Script/ILive2DModelApplier.cs b/Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs similarity index 100% rename from Assets/Domain/Character/Script/ILive2DModelApplier.cs rename to Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs diff --git a/Assets/Domain/Character/Script/ILive2DModelApplier.cs.meta b/Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs.meta similarity index 100% rename from Assets/Domain/Character/Script/ILive2DModelApplier.cs.meta rename to Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs.meta diff --git a/Assets/Domain/Character/Script/Live2DModelApplier.cs b/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs similarity index 100% rename from Assets/Domain/Character/Script/Live2DModelApplier.cs rename to Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs diff --git a/Assets/Domain/Character/Script/Live2DModelApplier.cs.meta b/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs.meta similarity index 100% rename from Assets/Domain/Character/Script/Live2DModelApplier.cs.meta rename to Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs.meta diff --git a/Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta b/Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta deleted file mode 100644 index c90ec6d..0000000 --- a/Assets/Domain/Character/Script/Live2DCharacterConfig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 8513455b30303c0408dfdf193b413316 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta b/Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta deleted file mode 100644 index 8f700e4..0000000 --- a/Assets/Domain/Character/Script/Live2DModelRegistry.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 7a9942477d782f94baa8697ff6d415ca \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Manager.meta b/Assets/Domain/Character/Script/Manager.meta new file mode 100644 index 0000000..d2debce --- /dev/null +++ b/Assets/Domain/Character/Script/Manager.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a07cad638f33a9e448551011b2f085bf +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Domain/Character/Script/ILive2DModelManager.cs b/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs similarity index 100% rename from Assets/Domain/Character/Script/ILive2DModelManager.cs rename to Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs diff --git a/Assets/Domain/Character/Script/ILive2DModelManager.cs.meta b/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs.meta similarity index 100% rename from Assets/Domain/Character/Script/ILive2DModelManager.cs.meta rename to Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs.meta diff --git a/Assets/Domain/Character/Script/Live2DModelManager.cs b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs similarity index 100% rename from Assets/Domain/Character/Script/Live2DModelManager.cs rename to Assets/Domain/Character/Script/Manager/Live2DModelManager.cs diff --git a/Assets/Domain/Character/Script/Live2DModelManager.cs.meta b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs.meta similarity index 100% rename from Assets/Domain/Character/Script/Live2DModelManager.cs.meta rename to Assets/Domain/Character/Script/Manager/Live2DModelManager.cs.meta diff --git a/Assets/Domain/Character/Script/Test.meta b/Assets/Domain/Character/Script/Test.meta new file mode 100644 index 0000000..793641e --- /dev/null +++ b/Assets/Domain/Character/Script/Test.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c762a71a10e06714a96c237780ce3100 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Domain/Character/Script/TestVoice.cs b/Assets/Domain/Character/Script/Test/TestVoice.cs similarity index 100% rename from Assets/Domain/Character/Script/TestVoice.cs rename to Assets/Domain/Character/Script/Test/TestVoice.cs diff --git a/Assets/Domain/Character/Script/TestVoice.cs.meta b/Assets/Domain/Character/Script/Test/TestVoice.cs.meta similarity index 100% rename from Assets/Domain/Character/Script/TestVoice.cs.meta rename to Assets/Domain/Character/Script/Test/TestVoice.cs.meta From a5c545690f36d414f049bd7422f639f714a4287f Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 12 Aug 2025 20:52:52 +0900 Subject: [PATCH 05/19] =?UTF-8?q?feat:=20live2d=20=EB=AA=A8=EB=8D=B8=20?= =?UTF-8?q?=EB=A1=9C=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/Live2d Sence.unity | 541 +++++++++++++++++- Assets/Core/Input/ScreenTapManager.cs | 19 + Assets/Core/Loading/LoadingManager.cs | 24 +- Assets/Core/Managers/GameManager.cs.meta | 2 - Assets/Core/Managers/Sample.meta | 8 + .../Sample/SampleSystemManager.cs} | 2 +- .../Sample/SampleSystemManager.cs.meta} | 0 .../{GameManager.cs => SystemManager.cs} | 16 +- Assets/Core/Managers/SystemManager.cs.meta | 2 + .../Script/Component/CubismHitHandler.cs | 15 +- .../Script/Manager/ILive2DModelManager.cs | 37 -- .../Manager/ILive2DModelManager.cs.meta | 2 - .../Script/Manager/Live2DModelManager.cs | 383 +++++++++---- .../Character/Script/Test/Live2DModelTest.cs | 170 ++++++ .../Script/Test/Live2DModelTest.cs.meta | 2 + Assets/Resources/Character.meta | 8 + .../Character/Live2DModelRegistry.asset | 17 + .../Character/Live2DModelRegistry.asset.meta | 8 + Assets/Resources/Character/Model.meta | 8 + .../Character/Model/CharacterZero.asset | 33 ++ .../Character/Model/CharacterZero.asset.meta | 8 + 21 files changed, 1116 insertions(+), 189 deletions(-) delete mode 100644 Assets/Core/Managers/GameManager.cs.meta create mode 100644 Assets/Core/Managers/Sample.meta rename Assets/Core/{SystemManager.cs => Managers/Sample/SampleSystemManager.cs} (96%) rename Assets/Core/{SystemManager.cs.meta => Managers/Sample/SampleSystemManager.cs.meta} (100%) rename Assets/Core/Managers/{GameManager.cs => SystemManager.cs} (89%) create mode 100644 Assets/Core/Managers/SystemManager.cs.meta delete mode 100644 Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs delete mode 100644 Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs.meta create mode 100644 Assets/Domain/Character/Script/Test/Live2DModelTest.cs create mode 100644 Assets/Domain/Character/Script/Test/Live2DModelTest.cs.meta create mode 100644 Assets/Resources/Character.meta create mode 100644 Assets/Resources/Character/Live2DModelRegistry.asset create mode 100644 Assets/Resources/Character/Live2DModelRegistry.asset.meta create mode 100644 Assets/Resources/Character/Model.meta create mode 100644 Assets/Resources/Character/Model/CharacterZero.asset create mode 100644 Assets/Resources/Character/Model/CharacterZero.asset.meta diff --git a/Assets/App/Scenes/Live2d Sence.unity b/Assets/App/Scenes/Live2d Sence.unity index 8d4fc2d..70a0dc7 100644 --- a/Assets/App/Scenes/Live2d Sence.unity +++ b/Assets/App/Scenes/Live2d Sence.unity @@ -119,6 +119,50 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1 &194973787 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 194973789} + - component: {fileID: 194973788} + m_Layer: 0 + m_Name: GameObject (1) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &194973788 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 194973787} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 220500989a5e4164381615ebdf1a6e33, type: 3} + m_Name: + m_EditorClassIdentifier: +--- !u!4 &194973789 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 194973787} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0.08988479, y: -0.3071759, z: 0.044769116} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1 &893152814 GameObject: m_ObjectHideFlags: 0 @@ -256,6 +300,365 @@ MonoBehaviour: m_MipBias: 0 m_VarianceClampScale: 0.9 m_ContrastAdaptiveSharpening: 0 +--- !u!1 &1211836786 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1211836790} + - component: {fileID: 1211836789} + - component: {fileID: 1211836788} + - component: {fileID: 1211836787} + m_Layer: 5 + m_Name: Canvas + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &1211836787 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1211836786} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: dc42784cf147c0c48a680349fa168899, type: 3} + m_Name: + m_EditorClassIdentifier: + m_IgnoreReversedGraphics: 1 + m_BlockingObjects: 0 + m_BlockingMask: + serializedVersion: 2 + m_Bits: 4294967295 +--- !u!114 &1211836788 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1211836786} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 0cd44c1031e13a943bb63640046fad76, type: 3} + m_Name: + m_EditorClassIdentifier: + m_UiScaleMode: 0 + m_ReferencePixelsPerUnit: 100 + m_ScaleFactor: 1 + m_ReferenceResolution: {x: 800, y: 600} + m_ScreenMatchMode: 0 + m_MatchWidthOrHeight: 0 + m_PhysicalUnit: 3 + m_FallbackScreenDPI: 96 + m_DefaultSpriteDPI: 96 + m_DynamicPixelsPerUnit: 1 + m_PresetInfoIsWorld: 0 +--- !u!223 &1211836789 +Canvas: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1211836786} + m_Enabled: 1 + serializedVersion: 3 + m_RenderMode: 0 + m_Camera: {fileID: 0} + m_PlaneDistance: 100 + m_PixelPerfect: 0 + m_ReceivesEvents: 1 + m_OverrideSorting: 0 + m_OverridePixelPerfect: 0 + m_SortingBucketNormalizedSize: 0 + m_VertexColorAlwaysGammaSpace: 0 + m_AdditionalShaderChannelsFlag: 25 + m_UpdateRectTransformForStandalone: 0 + m_SortingLayerID: 0 + m_SortingOrder: 0 + m_TargetDisplay: 0 +--- !u!224 &1211836790 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1211836786} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 0, y: 0, z: 0} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1594706922} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 0, y: 0} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0, y: 0} +--- !u!1 &1303424300 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1303424301} + - component: {fileID: 1303424303} + - component: {fileID: 1303424302} + m_Layer: 5 + m_Name: Text (TMP) + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1303424301 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1303424300} + m_LocalRotation: {x: -0, y: -0, z: -0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 1594706922} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0, y: 0} + m_AnchorMax: {x: 1, y: 1} + m_AnchoredPosition: {x: 0, y: 0} + m_SizeDelta: {x: 0, y: 0} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1303424302 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1303424300} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: f4688fdb7df04437aeb418b961361dc5, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_text: Button + m_isRightToLeft: 0 + m_fontAsset: {fileID: 11400000, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_sharedMaterial: {fileID: 2180264, guid: 8f586378b4e144a9851e7b34d9b748ee, type: 2} + m_fontSharedMaterials: [] + m_fontMaterial: {fileID: 0} + m_fontMaterials: [] + m_fontColor32: + serializedVersion: 2 + rgba: 4281479730 + m_fontColor: {r: 0.19607843, g: 0.19607843, b: 0.19607843, a: 1} + m_enableVertexGradient: 0 + m_colorMode: 3 + m_fontColorGradient: + topLeft: {r: 1, g: 1, b: 1, a: 1} + topRight: {r: 1, g: 1, b: 1, a: 1} + bottomLeft: {r: 1, g: 1, b: 1, a: 1} + bottomRight: {r: 1, g: 1, b: 1, a: 1} + m_fontColorGradientPreset: {fileID: 0} + m_spriteAsset: {fileID: 0} + m_tintAllSprites: 0 + m_StyleSheet: {fileID: 0} + m_TextStyleHashCode: -1183493901 + m_overrideHtmlColors: 0 + m_faceColor: + serializedVersion: 2 + rgba: 4294967295 + m_fontSize: 24 + m_fontSizeBase: 24 + m_fontWeight: 400 + m_enableAutoSizing: 0 + m_fontSizeMin: 18 + m_fontSizeMax: 72 + m_fontStyle: 0 + m_HorizontalAlignment: 2 + m_VerticalAlignment: 512 + m_textAlignment: 65535 + m_characterSpacing: 0 + m_wordSpacing: 0 + m_lineSpacing: 0 + m_lineSpacingMax: 0 + m_paragraphSpacing: 0 + m_charWidthMaxAdj: 0 + m_TextWrappingMode: 1 + m_wordWrappingRatios: 0.4 + m_overflowMode: 0 + m_linkedTextComponent: {fileID: 0} + parentLinkedComponent: {fileID: 0} + m_enableKerning: 0 + m_ActiveFontFeatures: 6e72656b + m_enableExtraPadding: 0 + checkPaddingRequired: 0 + m_isRichText: 1 + m_EmojiFallbackSupport: 1 + m_parseCtrlCharacters: 1 + m_isOrthographic: 1 + m_isCullingEnabled: 0 + m_horizontalMapping: 0 + m_verticalMapping: 0 + m_uvLineOffset: 0 + m_geometrySortingOrder: 0 + m_IsTextObjectScaleStatic: 0 + m_VertexBufferAutoSizeReduction: 0 + m_useMaxVisibleDescender: 1 + m_pageToDisplay: 1 + m_margin: {x: 0, y: 0, z: 0, w: 0} + m_isUsingLegacyAnimationComponent: 0 + m_isVolumetricText: 0 + m_hasFontAssetChanged: 0 + m_baseMaterial: {fileID: 0} + m_maskOffset: {x: 0, y: 0, z: 0, w: 0} +--- !u!222 &1303424303 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1303424300} + m_CullTransparentMesh: 1 +--- !u!1 &1594706921 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1594706922} + - component: {fileID: 1594706925} + - component: {fileID: 1594706924} + - component: {fileID: 1594706923} + m_Layer: 5 + m_Name: Button + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!224 &1594706922 +RectTransform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1594706921} + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 1303424301} + m_Father: {fileID: 1211836790} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} + m_AnchorMin: {x: 0.5, y: 0.5} + m_AnchorMax: {x: 0.5, y: 0.5} + m_AnchoredPosition: {x: -640, y: -1465} + m_SizeDelta: {x: 160, y: 30} + m_Pivot: {x: 0.5, y: 0.5} +--- !u!114 &1594706923 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1594706921} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Navigation: + m_Mode: 3 + m_WrapAround: 0 + m_SelectOnUp: {fileID: 0} + m_SelectOnDown: {fileID: 0} + m_SelectOnLeft: {fileID: 0} + m_SelectOnRight: {fileID: 0} + m_Transition: 1 + m_Colors: + m_NormalColor: {r: 1, g: 1, b: 1, a: 1} + m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1} + m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1} + m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608} + m_ColorMultiplier: 1 + m_FadeDuration: 0.1 + m_SpriteState: + m_HighlightedSprite: {fileID: 0} + m_PressedSprite: {fileID: 0} + m_SelectedSprite: {fileID: 0} + m_DisabledSprite: {fileID: 0} + m_AnimationTriggers: + m_NormalTrigger: Normal + m_HighlightedTrigger: Highlighted + m_PressedTrigger: Pressed + m_SelectedTrigger: Selected + m_DisabledTrigger: Disabled + m_Interactable: 1 + m_TargetGraphic: {fileID: 1594706924} + m_OnClick: + m_PersistentCalls: + m_Calls: [] +--- !u!114 &1594706924 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1594706921} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3} + m_Name: + m_EditorClassIdentifier: + m_Material: {fileID: 0} + m_Color: {r: 1, g: 1, b: 1, a: 1} + m_RaycastTarget: 1 + m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0} + m_Maskable: 1 + m_OnCullStateChanged: + m_PersistentCalls: + m_Calls: [] + m_Sprite: {fileID: 10905, guid: 0000000000000000f000000000000000, type: 0} + m_Type: 1 + m_PreserveAspect: 0 + m_FillCenter: 1 + m_FillMethod: 4 + m_FillAmount: 1 + m_FillClockwise: 1 + m_FillOrigin: 0 + m_UseSpriteMesh: 0 + m_PixelsPerUnitMultiplier: 1 +--- !u!222 &1594706925 +CanvasRenderer: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1594706921} + m_CullTransparentMesh: 1 --- !u!1 &1607417702 GameObject: m_ObjectHideFlags: 0 @@ -346,8 +749,138 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _modelRoot: {fileID: 1607417703} - _modelRegistry: {fileID: 11400000, guid: faf582e392fb4334aa0f4dd41df09e0d, type: 2} - _defaultCharacterConfig: {fileID: 11400000, guid: 85f44d149f02e394597f11878de45759, type: 2} + _modelRegistry: {fileID: 11400000, guid: cf68d9632ab0d2c42a02de12beecadff, type: 2} +--- !u!1 &2028963604 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2028963606} + - component: {fileID: 2028963605} + m_Layer: 0 + m_Name: GameObject + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &2028963605 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2028963604} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 7fb1442983837c140abae42a85733fe6, type: 3} + m_Name: + m_EditorClassIdentifier: + _testCharacterId: zero + _loadCharacterBtn: {fileID: 1594706923} + _activateCharacterBtn: {fileID: 0} + _unloadCharacterBtn: {fileID: 0} + _unloadAllBtn: {fileID: 0} + _changeVisibilityBtn: {fileID: 0} + _characterIdInput: {fileID: 0} + _statusText: {fileID: 0} +--- !u!4 &2028963606 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2028963604} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &2140621153 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 2140621156} + - component: {fileID: 2140621155} + - component: {fileID: 2140621154} + m_Layer: 0 + m_Name: EventSystem + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!114 &2140621154 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2140621153} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 01614664b831546d2ae94a42149d80ac, type: 3} + m_Name: + m_EditorClassIdentifier: + m_SendPointerHoverToParent: 1 + m_MoveRepeatDelay: 0.5 + m_MoveRepeatRate: 0.1 + m_XRTrackingOrigin: {fileID: 0} + m_ActionsAsset: {fileID: -944628639613478452, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_PointAction: {fileID: -1654692200621890270, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MoveAction: {fileID: -8784545083839296357, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_SubmitAction: {fileID: 392368643174621059, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_CancelAction: {fileID: 7727032971491509709, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_LeftClickAction: {fileID: 3001919216989983466, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_MiddleClickAction: {fileID: -2185481485913320682, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_RightClickAction: {fileID: -4090225696740746782, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_ScrollWheelAction: {fileID: 6240969308177333660, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDevicePositionAction: {fileID: 6564999863303420839, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_TrackedDeviceOrientationAction: {fileID: 7970375526676320489, guid: ca9f5fa95ffab41fb9a615ab714db018, type: 3} + m_DeselectOnBackgroundClick: 1 + m_PointerBehavior: 0 + m_CursorLockBehavior: 0 + m_ScrollDeltaPerTick: 6 +--- !u!114 &2140621155 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2140621153} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 76c392e42b5098c458856cdf6ecaaaa1, type: 3} + m_Name: + m_EditorClassIdentifier: + m_FirstSelected: {fileID: 0} + m_sendNavigationEvents: 1 + m_DragThreshold: 10 +--- !u!4 &2140621156 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 2140621153} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} --- !u!1660057539 &9223372036854775807 SceneRoots: m_ObjectHideFlags: 0 @@ -355,3 +888,7 @@ SceneRoots: - {fileID: 893152817} - {fileID: 1916112490} - {fileID: 1607417703} + - {fileID: 2028963606} + - {fileID: 1211836790} + - {fileID: 2140621156} + - {fileID: 194973789} diff --git a/Assets/Core/Input/ScreenTapManager.cs b/Assets/Core/Input/ScreenTapManager.cs index 82f8262..13ee1de 100644 --- a/Assets/Core/Input/ScreenTapManager.cs +++ b/Assets/Core/Input/ScreenTapManager.cs @@ -144,6 +144,25 @@ public bool TryGetTapUpPosition(out CubismRaycastHit[] hitResults) { hitResults = null; + // 필수 컴포넌트 null 체크 + if (_camera == null) + { + Debug.LogWarning("[ScreenTapManager] Camera가 null입니다. Initialize()를 호출해주세요."); + return false; + } + + if (_raycaster == null) + { + Debug.LogWarning("[ScreenTapManager] CubismRaycaster가 null입니다. SetRaycaster()를 호출해주세요."); + return false; + } + + if (_inputUpProvider == null) + { + Debug.LogWarning("[ScreenTapManager] InputUpProvider가 null입니다. Initialize()를 호출해주세요."); + return false; + } + // 손 뗀 시점이 아니면 false if (!_inputUpProvider.TryGetPosition(out var screenPosition)) { diff --git a/Assets/Core/Loading/LoadingManager.cs b/Assets/Core/Loading/LoadingManager.cs index fa78e23..64fd55a 100644 --- a/Assets/Core/Loading/LoadingManager.cs +++ b/Assets/Core/Loading/LoadingManager.cs @@ -56,9 +56,9 @@ private void OnDestroy() public void StartInitialization() { - if (GameManager.Instance != null) + if (SystemManager.Instance != null) { - GameManager.Instance.InitializeGame(); + SystemManager.Instance.InitializeGame(); } else { @@ -92,9 +92,9 @@ public async void StartGame() { await _loadingUI.FadeOut(); } - if (GameManager.Instance != null) + if (SystemManager.Instance != null) { - await GameManager.Instance.TransitionToMainSceneAsync(); + await SystemManager.Instance.TransitionToMainSceneAsync(); } } @@ -104,11 +104,11 @@ public async void StartGame() private void SetupEventListeners() { - if (GameManager.Instance != null) + if (SystemManager.Instance != null) { - GameManager.Instance.OnGameInitialized += OnGameInitialized; - GameManager.Instance.OnInitializationError += OnInitializationError; - var initializationManager = GameManager.Instance.GetComponent(); + SystemManager.Instance.OnGameInitialized += OnGameInitialized; + SystemManager.Instance.OnInitializationError += OnInitializationError; + var initializationManager = SystemManager.Instance.GetComponent(); if (initializationManager != null) { initializationManager.OnProgressUpdated += OnProgressUpdated; @@ -118,11 +118,11 @@ private void SetupEventListeners() private void RemoveEventListeners() { - if (GameManager.Instance != null) + if (SystemManager.Instance != null) { - GameManager.Instance.OnGameInitialized -= OnGameInitialized; - GameManager.Instance.OnInitializationError -= OnInitializationError; - var initializationManager = GameManager.Instance.GetComponent(); + SystemManager.Instance.OnGameInitialized -= OnGameInitialized; + SystemManager.Instance.OnInitializationError -= OnInitializationError; + var initializationManager = SystemManager.Instance.GetComponent(); if (initializationManager != null) { initializationManager.OnProgressUpdated -= OnProgressUpdated; diff --git a/Assets/Core/Managers/GameManager.cs.meta b/Assets/Core/Managers/GameManager.cs.meta deleted file mode 100644 index e566be2..0000000 --- a/Assets/Core/Managers/GameManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: f2343a97be172e748a6501089d9e3c0a \ No newline at end of file diff --git a/Assets/Core/Managers/Sample.meta b/Assets/Core/Managers/Sample.meta new file mode 100644 index 0000000..2ba03b8 --- /dev/null +++ b/Assets/Core/Managers/Sample.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 1be1e8e01487ecc41a2a3dcfa61ade15 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Core/SystemManager.cs b/Assets/Core/Managers/Sample/SampleSystemManager.cs similarity index 96% rename from Assets/Core/SystemManager.cs rename to Assets/Core/Managers/Sample/SampleSystemManager.cs index 2236490..76880c7 100644 --- a/Assets/Core/SystemManager.cs +++ b/Assets/Core/Managers/Sample/SampleSystemManager.cs @@ -3,7 +3,7 @@ using UnityEngine; using UnityEngine.UI; -public class SystemManager : Singleton +public class SampleSystemManager : Singleton { [SerializeField] private CubismLookTarget cubismLookTarget = null; [SerializeField] private Camera mCamera = null; diff --git a/Assets/Core/SystemManager.cs.meta b/Assets/Core/Managers/Sample/SampleSystemManager.cs.meta similarity index 100% rename from Assets/Core/SystemManager.cs.meta rename to Assets/Core/Managers/Sample/SampleSystemManager.cs.meta diff --git a/Assets/Core/Managers/GameManager.cs b/Assets/Core/Managers/SystemManager.cs similarity index 89% rename from Assets/Core/Managers/GameManager.cs rename to Assets/Core/Managers/SystemManager.cs index 48163d8..8fed2b2 100644 --- a/Assets/Core/Managers/GameManager.cs +++ b/Assets/Core/Managers/SystemManager.cs @@ -8,7 +8,7 @@ namespace ProjectVG.Core.Managers { - public class GameManager : Singleton + public class SystemManager : Singleton { [Header("Core Managers")] [SerializeField] private InitializationManager _initializationManager; @@ -76,7 +76,7 @@ public async UniTask InitializeGameAsync() { if (_initializationManager == null) { - Debug.LogError("[GameManager] InitializationManager가 설정되지 않았습니다."); + Debug.LogError("[SystemManager] InitializationManager가 설정되지 않았습니다."); return; } @@ -97,20 +97,20 @@ public async UniTask InitializeGameAsync() await _initializationManager.InitializeAsync(); if (IsInitialized) { - Debug.Log("[GameManager] 게임 시스템 준비 완료"); + Debug.Log("[SystemManager] 게임 시스템 준비 완료"); } } public void Shutdown() { _managerRegistry?.ShutdownAllManagers(); - Debug.Log("[GameManager] 시스템 종료 완료"); + Debug.Log("[SystemManager] 시스템 종료 완료"); } [ContextMenu("Log Manager Status")] public void LogManagerStatus() { - Debug.Log($"[GameManager] Initialized: {IsInitialized}"); + Debug.Log($"[SystemManager] Initialized: {IsInitialized}"); _managerRegistry?.LogManagerStatus(); } @@ -119,11 +119,11 @@ public async UniTask TransitionToMainSceneAsync() try { await UnityEngine.SceneManagement.SceneManager.LoadSceneAsync("MainSence"); - Debug.Log("[GameManager] MainScene 전환 완료"); + Debug.Log("[SystemManager] MainScene 전환 완료"); } catch (System.Exception ex) { - Debug.LogError($"[GameManager] 씬 전환 실패: {ex.Message}"); + Debug.LogError($"[SystemManager] 씬 전환 실패: {ex.Message}"); } } @@ -168,7 +168,7 @@ private void SetupManagerReferences() } else { - Debug.LogError("[GameManager] 필수 매니저가 설정되지 않았습니다."); + Debug.LogError("[SystemManager] 필수 매니저가 설정되지 않았습니다."); } } } diff --git a/Assets/Core/Managers/SystemManager.cs.meta b/Assets/Core/Managers/SystemManager.cs.meta new file mode 100644 index 0000000..14acbab --- /dev/null +++ b/Assets/Core/Managers/SystemManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: c31e1b9082df088489ede1181874cde4 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Component/CubismHitHandler.cs b/Assets/Domain/Character/Script/Component/CubismHitHandler.cs index fe9ece9..8b066ba 100644 --- a/Assets/Domain/Character/Script/Component/CubismHitHandler.cs +++ b/Assets/Domain/Character/Script/Component/CubismHitHandler.cs @@ -13,11 +13,24 @@ public void Initialize() { _raycaster = GetComponent(); _expressionController = GetComponent(); - ScreenTapManager.Instance.SetRaycaster(_raycaster); + + if (_raycaster != null && ScreenTapManager.Instance != null) + { + ScreenTapManager.Instance.SetRaycaster(_raycaster); + } + else + { + Debug.LogWarning("[CubismHitHandler] Raycaster 또는 ScreenTapManager가 null입니다."); + } } private void Update() { + if (ScreenTapManager.Instance == null) + { + return; + } + if (ScreenTapManager.Instance.TryGetTapUpPosition(out var hits)) { foreach (var hit in hits) diff --git a/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs b/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs deleted file mode 100644 index b85593a..0000000 --- a/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs +++ /dev/null @@ -1,37 +0,0 @@ -using UnityEngine; -using Cysharp.Threading.Tasks; - -namespace ProjectVG.Domain.Character.Service -{ - public interface ILive2DModelManager - { - /** Live2D 모델 관리를 위한 초기 설정을 수행한다. */ - void Initialize(ProjectVG.Domain.Character.Live2D.Model.Live2DModelRegistry modelRegistry, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig defaultCharacterConfig); - - /** 지정한 캐릭터 모델을 비동기 로드한다. */ - UniTask LoadModelAsync(string characterId); - - /** 현재 활성 모델을 언로드한다. */ - void UnloadActiveModel(); - - /** 지정한 캐릭터 ID의 모델을 활성 모델로 전환한다. */ - void SetActiveModel(string characterId); - - /** 현재 활성 모델 GameObject를 반환한다. */ - GameObject GetActiveModel(); - - /** 지정한 캐릭터 ID의 모델이 로드되어 있는지 반환한다. */ - bool HasModel(string characterId); - - /** 지정한 캐릭터 ID의 모델을 사전 로드한다. */ - UniTask PreloadModelAsync(string characterId); - - /** 활성 모델의 가시성을 설정한다. */ - void SetVisibility(bool isVisible); - - /** 활성 모델에 캐릭터별 Live2DCharacterConfig를 적용한다. */ - void ApplyCharacterConfig(ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig characterConfig); - } -} - - diff --git a/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs.meta b/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs.meta deleted file mode 100644 index 32095f3..0000000 --- a/Assets/Domain/Character/Script/Manager/ILive2DModelManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: a7acebd7cbed2fd499cd205d2797e302 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs index 7cd157b..0722b7a 100644 --- a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs +++ b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs @@ -2,235 +2,370 @@ using UnityEngine; using Cysharp.Threading.Tasks; using ProjectVG.Domain.Character.Live2D.Model; -using UnityEngine.UI; using Live2D.Cubism.Framework.LookAt; using Live2D.Cubism.Framework.MouthMovement; -#if UNITY_EDITOR -using UnityEditor; -#endif namespace ProjectVG.Domain.Character.Service { - public class Live2DModelManager : MonoBehaviour, ILive2DModelManager + public class Live2DModelManager : Singleton { + [Header("설정")] [SerializeField] private Transform _modelRoot; [SerializeField] private Live2DModelRegistry _modelRegistry; - [SerializeField] private Live2DCharacterConfig _defaultCharacterConfig; - [SerializeField] private CubismLookTarget _cubismLookTarget; - [SerializeField] private AudioSource _voiceSource; - [SerializeField] private Button _expressionChangeBtn; private readonly Dictionary _characterIdToInstance = new Dictionary(); private string _activeCharacterId; + #region Public Methods + /// - /// Live2D 모델 관리를 위한 초기 설정을 수행한다. + /// 캐릭터 모델을 로드하고 설정을 적용한다. /// - public void Initialize(Live2DModelRegistry modelRegistry, Live2DCharacterConfig defaultCharacterConfig) + public GameObject LoadCharacter(string characterId, bool activateImmediately = false) { - _modelRegistry = modelRegistry; - _defaultCharacterConfig = defaultCharacterConfig; - if (_modelRoot == null) + if (string.IsNullOrEmpty(characterId)) + { + Debug.LogWarning("[Live2DModelManager] 캐릭터 ID가 null입니다."); + return null; + } + + // 이미 로드된 모델이 있는지 확인 + if (_characterIdToInstance.TryGetValue(characterId, out var existing)) + { + Debug.Log($"[Live2DModelManager] 캐릭터 '{characterId}'가 이미 로드되어 있습니다."); + if (activateImmediately) + { + ActivateCharacter(characterId); + } + return existing; + } + + // 설정 가져오기 + var config = GetCharacterConfig(characterId); + if (config == null) + { + Debug.LogError($"[Live2DModelManager] 캐릭터 '{characterId}'의 설정을 찾을 수 없습니다."); + return null; + } + + // 모델 인스턴스 생성 + var instance = CreateModelInstance(config, characterId); + if (instance == null) + { + return null; + } + + // 설정 적용 + ApplyCharacterConfig(instance, config); + + // 딕셔너리에 저장 + _characterIdToInstance[characterId] = instance; + + // 즉시 활성화 옵션 + if (activateImmediately) { - _modelRoot = transform; + ActivateCharacter(characterId); } + + Debug.Log($"[Live2DModelManager] 캐릭터 '{characterId}' 로드 완료"); + return instance; } /// - /// 지정한 캐릭터 모델을 비동기 로드한다. + /// 캐릭터 모델을 비동기로 로드하고 설정을 적용한다. /// - public UniTask LoadModelAsync(string characterId) + public async UniTask LoadCharacterAsync(string characterId, bool activateImmediately = false) { if (string.IsNullOrEmpty(characterId)) { - return UniTask.FromResult(null); + Debug.LogWarning("[Live2DModelManager] 캐릭터 ID가 null입니다."); + return null; } + + // 이미 로드된 모델이 있는지 확인 if (_characterIdToInstance.TryGetValue(characterId, out var existing)) { - return UniTask.FromResult(existing); - } - Live2DCharacterConfig config = null; - if (_modelRegistry != null) - { - _modelRegistry.TryGetConfig(characterId, out config); + Debug.Log($"[Live2DModelManager] 캐릭터 '{characterId}'가 이미 로드되어 있습니다."); + if (activateImmediately) + { + ActivateCharacter(characterId); + } + return existing; } + + // 설정 가져오기 + var config = GetCharacterConfig(characterId); if (config == null) { - config = _defaultCharacterConfig; + Debug.LogError($"[Live2DModelManager] 캐릭터 '{characterId}'의 설정을 찾을 수 없습니다."); + return null; } - if (config == null || config.CharacterPrefab == null) + + // 모델 인스턴스 생성 (비동기) + var instance = await CreateModelInstanceAsync(config, characterId); + if (instance == null) { - return UniTask.FromResult(null); + return null; } - var instance = Instantiate(config.CharacterPrefab, _modelRoot != null ? _modelRoot : transform); - instance.name = characterId; - instance.SetActive(false); + + // 설정 적용 + ApplyCharacterConfig(instance, config); + + // 딕셔너리에 저장 _characterIdToInstance[characterId] = instance; - return UniTask.FromResult(instance); + + // 즉시 활성화 옵션 + if (activateImmediately) + { + ActivateCharacter(characterId); + } + + Debug.Log($"[Live2DModelManager] 캐릭터 '{characterId}' 비동기 로드 완료"); + return instance; } /// - /// 현재 활성 모델을 언로드한다. + /// 캐릭터를 활성화한다. /// - public void UnloadActiveModel() + public void ActivateCharacter(string characterId) { - if (string.IsNullOrEmpty(_activeCharacterId)) + if (!_characterIdToInstance.TryGetValue(characterId, out var target)) { + Debug.LogWarning($"[Live2DModelManager] 캐릭터 '{characterId}'가 로드되지 않았습니다."); return; } - if (_characterIdToInstance.TryGetValue(_activeCharacterId, out var go) && go != null) + + // 모든 모델 비활성화 + DeactivateAllModels(); + + // 대상 모델 활성화 + target.SetActive(true); + _activeCharacterId = characterId; + + Debug.Log($"[Live2DModelManager] 캐릭터 '{characterId}' 활성화"); + } + + /// + /// 현재 활성 캐릭터를 반환한다. + /// + public GameObject GetActiveCharacter() + { + if (string.IsNullOrEmpty(_activeCharacterId)) { - Destroy(go); + return null; } - _characterIdToInstance.Remove(_activeCharacterId); - _activeCharacterId = null; + + _characterIdToInstance.TryGetValue(_activeCharacterId, out var character); + return character; } /// - /// 지정한 캐릭터 ID의 모델을 활성 모델로 전환한다. + /// 캐릭터가 로드되어 있는지 확인한다. /// - public void SetActiveModel(string characterId) + public bool HasCharacter(string characterId) { - if (string.IsNullOrEmpty(characterId)) + return !string.IsNullOrEmpty(characterId) && _characterIdToInstance.ContainsKey(characterId); + } + + /// + /// 캐릭터를 언로드한다. + /// + public void UnloadCharacter(string characterId) + { + if (!_characterIdToInstance.TryGetValue(characterId, out var instance)) { return; } - if (!_characterIdToInstance.TryGetValue(characterId, out var target)) + + if (instance != null) { - return; + Destroy(instance); } - foreach (var kv in _characterIdToInstance) + + _characterIdToInstance.Remove(characterId); + + // 현재 활성 캐릭터였다면 활성 ID 초기화 + if (_activeCharacterId == characterId) { - if (kv.Value != null) + _activeCharacterId = null; + } + + Debug.Log($"[Live2DModelManager] 캐릭터 '{characterId}' 언로드 완료"); + } + + /// + /// 모든 캐릭터를 언로드한다. + /// + public void UnloadAllCharacters() + { + foreach (var kvp in _characterIdToInstance) + { + if (kvp.Value != null) { - kv.Value.SetActive(false); + Destroy(kvp.Value); } } - target.SetActive(true); - _activeCharacterId = characterId; + + _characterIdToInstance.Clear(); + _activeCharacterId = null; + + Debug.Log("[Live2DModelManager] 모든 캐릭터 언로드 완료"); } /// - /// 현재 활성 모델 GameObject를 반환한다. + /// 활성 캐릭터의 가시성을 설정한다. /// - public GameObject GetActiveModel() + public void SetCharacterVisibility(bool isVisible) { - if (string.IsNullOrEmpty(_activeCharacterId)) + var active = GetActiveCharacter(); + if (active == null) { - return null; + Debug.LogWarning("[Live2DModelManager] 활성 캐릭터가 없습니다."); + return; } - _characterIdToInstance.TryGetValue(_activeCharacterId, out var go); - return go; + + active.SetActive(isVisible); } + #endregion + + #region Private Methods + /// - /// 지정한 캐릭터 ID의 모델이 로드되어 있는지 반환한다. + /// 캐릭터 설정을 가져온다. /// - public bool HasModel(string characterId) + private Live2DCharacterConfig GetCharacterConfig(string characterId) { - return !string.IsNullOrEmpty(characterId) && _characterIdToInstance.ContainsKey(characterId); + // 레지스트리에서 먼저 찾기 + if (_modelRegistry != null && _modelRegistry.TryGetConfig(characterId, out var config)) + { + return config; + } + + // 설정을 찾을 수 없음 + Debug.LogError($"[Live2DModelManager] 캐릭터 '{characterId}'의 설정을 찾을 수 없습니다. 레지스트리를 확인해주세요."); + return null; } /// - /// 지정한 캐릭터 ID의 모델을 사전 로드한다. + /// 모델 인스턴스를 생성한다. /// - public UniTask PreloadModelAsync(string characterId) + private GameObject CreateModelInstance(Live2DCharacterConfig config, string characterId) { - return LoadModelAsync(characterId).AsUniTask(); + if (config.CharacterPrefab == null) + { + Debug.LogError($"[Live2DModelManager] 캐릭터 '{characterId}'의 프리팹이 null입니다."); + return null; + } + + var parent = _modelRoot != null ? _modelRoot : transform; + var instance = Instantiate(config.CharacterPrefab, parent); + instance.name = characterId; + instance.SetActive(false); + + return instance; } /// - /// 활성 모델의 가시성을 설정한다. + /// 모델 인스턴스를 비동기로 생성한다. /// - public void SetVisibility(bool isVisible) + private async UniTask CreateModelInstanceAsync(Live2DCharacterConfig config, string characterId) { - var active = GetActiveModel(); - if (active == null) + if (config.CharacterPrefab == null) { - return; + Debug.LogError($"[Live2DModelManager] 캐릭터 '{characterId}'의 프리팹이 null입니다."); + return null; } - active.SetActive(isVisible); + + var parent = _modelRoot != null ? _modelRoot : transform; + var instance = Instantiate(config.CharacterPrefab, parent); + instance.name = characterId; + instance.SetActive(false); + + // 비동기 작업을 위한 지연 (필요시) + await UniTask.Yield(); + + return instance; } /// - /// 활성 모델에 캐릭터별 Live2DCharacterConfig를 적용한다. + /// 캐릭터 설정을 적용한다. /// - public void ApplyCharacterConfig(Live2DCharacterConfig characterConfig) + private void ApplyCharacterConfig(GameObject character, Live2DCharacterConfig config) { - var active = GetActiveModel(); - if (active == null || characterConfig == null) + if (character == null || config == null) { return; } - var lookController = active.GetComponent(); - if (lookController != null && _cubismLookTarget != null) + ApplyLookAtSettings(character, config); + ApplyLipSyncSettings(character, config); + InitializeHitHandler(character); + } + + /// + /// 시선 추적 설정을 적용한다. + /// + private void ApplyLookAtSettings(GameObject character, Live2DCharacterConfig config) + { + var lookController = character.GetComponent(); + if (lookController == null) { - lookController.Target = _cubismLookTarget.gameObject; - lookController.Damping = characterConfig.LockAtDamping; - _cubismLookTarget.Initialize(ToModelConfigProxy(characterConfig)); + Debug.LogWarning($"[Live2DModelManager] CubismLookController를 찾을 수 없습니다: {character.name}"); + return; } - var mouthInput = active.GetComponent(); - if (mouthInput != null && _voiceSource != null) + lookController.Target = null; // TODO: 시선 타겟 설정 필요 + lookController.Damping = config.LockAtDamping; + } + + /// + /// 립싱크 설정을 적용한다. + /// + private void ApplyLipSyncSettings(GameObject character, Live2DCharacterConfig config) + { + var mouthInput = character.GetComponent(); + if (mouthInput == null) { - mouthInput.AudioInput = _voiceSource; - mouthInput.Gain = characterConfig.Gain; - mouthInput.Smoothing = characterConfig.Smoothing; + Debug.LogWarning($"[Live2DModelManager] CubismAudioMouthInput을 찾을 수 없습니다: {character.name}"); + return; } - var hitHandler = active.GetComponent(); - if (hitHandler != null) + mouthInput.AudioInput = null; // TODO: 오디오 소스 설정 필요 + mouthInput.Gain = config.Gain; + mouthInput.Smoothing = config.Smoothing; + } + + /// + /// 터치 핸들러를 초기화한다. + /// + private void InitializeHitHandler(GameObject character) + { + var hitHandler = character.GetComponent(); + if (hitHandler == null) { - hitHandler.Initialize(); - if (_expressionChangeBtn != null) - { - _expressionChangeBtn.onClick.RemoveAllListeners(); - _expressionChangeBtn.onClick.AddListener(hitHandler.ExpressionChange_Btn); - } + Debug.LogWarning($"[Live2DModelManager] CubismHitHandler를 찾을 수 없습니다: {character.name}"); + return; } + + hitHandler.Initialize(); } /// - /// Live2DCharacterConfig를 ModelConfig로 임시 변환한다. (CubismLookTarget.Initialize 호환성) + /// 모든 모델을 비활성화한다. /// - private ModelConfig ToModelConfigProxy(Live2DCharacterConfig cfg) + private void DeactivateAllModels() { - // ModelConfig의 필드가 private이므로 리플렉션을 사용하여 설정 - var proxy = ScriptableObject.CreateInstance(); - - // 리플렉션을 사용하여 private 필드에 값 설정 - var type = typeof(ModelConfig); - - var modelNameField = type.GetField("modelName", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - modelNameField?.SetValue(proxy, cfg.CharacterName); - - var modelDescriptionField = type.GetField("modelDescription", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - modelDescriptionField?.SetValue(proxy, cfg.CharacterDescription); - - var thumbnailField = type.GetField("thumbnail", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - thumbnailField?.SetValue(proxy, cfg.Thumbnail); - - var lookSensitivityField = type.GetField("lookSensitivity", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - lookSensitivityField?.SetValue(proxy, cfg.LookSensitivity); - - var lockAtDampingField = type.GetField("lockAtDamping", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - lockAtDampingField?.SetValue(proxy, cfg.LockAtDamping); - - var isLockAtActiveField = type.GetField("isLockAtActive", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - isLockAtActiveField?.SetValue(proxy, cfg.IsLockAtActive); - - var gainField = type.GetField("gain", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - gainField?.SetValue(proxy, cfg.Gain); - - var smoothingField = type.GetField("smoothing", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - smoothingField?.SetValue(proxy, cfg.Smoothing); - - var modelPrefabField = type.GetField("modelPrefab", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - modelPrefabField?.SetValue(proxy, cfg.CharacterPrefab); - - return proxy; + foreach (var kvp in _characterIdToInstance) + { + if (kvp.Value != null) + { + kvp.Value.SetActive(false); + } + } } + + #endregion } } diff --git a/Assets/Domain/Character/Script/Test/Live2DModelTest.cs b/Assets/Domain/Character/Script/Test/Live2DModelTest.cs new file mode 100644 index 0000000..399f383 --- /dev/null +++ b/Assets/Domain/Character/Script/Test/Live2DModelTest.cs @@ -0,0 +1,170 @@ +using UnityEngine; +using UnityEngine.UI; +using ProjectVG.Domain.Character.Service; +using ProjectVG.Domain.Character.Live2D.Model; + +namespace ProjectVG.Domain.Character.Test +{ + /// + /// Live2DModelManager 테스트를 위한 컴포넌트 + /// + public class Live2DModelTest : MonoBehaviour + { + [Header("테스트 설정")] + [SerializeField] private string _testCharacterId = "test_character"; + + [Header("UI 요소")] + [SerializeField] private Button _loadCharacterBtn; + [SerializeField] private Button _activateCharacterBtn; + [SerializeField] private Button _unloadCharacterBtn; + [SerializeField] private Button _unloadAllBtn; + [SerializeField] private Button _changeVisibilityBtn; + [SerializeField] private InputField _characterIdInput; + + [Header("테스트 결과")] + [SerializeField] private Text _statusText; + + private bool _isCharacterVisible = true; + + private void Start() + { + SetupUI(); + } + + /// + /// UI 요소들을 설정한다. + /// + private void SetupUI() + { + if (_loadCharacterBtn != null) + { + _loadCharacterBtn.onClick.RemoveAllListeners(); + _loadCharacterBtn.onClick.AddListener(OnLoadCharacterClicked); + } + + if (_activateCharacterBtn != null) + { + _activateCharacterBtn.onClick.RemoveAllListeners(); + _activateCharacterBtn.onClick.AddListener(OnActivateCharacterClicked); + } + + if (_unloadCharacterBtn != null) + { + _unloadCharacterBtn.onClick.RemoveAllListeners(); + _unloadCharacterBtn.onClick.AddListener(OnUnloadCharacterClicked); + } + + if (_unloadAllBtn != null) + { + _unloadAllBtn.onClick.RemoveAllListeners(); + _unloadAllBtn.onClick.AddListener(OnUnloadAllClicked); + } + + if (_changeVisibilityBtn != null) + { + _changeVisibilityBtn.onClick.RemoveAllListeners(); + _changeVisibilityBtn.onClick.AddListener(OnChangeVisibilityClicked); + } + + if (_characterIdInput != null) + { + _characterIdInput.text = _testCharacterId; + _characterIdInput.onValueChanged.AddListener(OnCharacterIdChanged); + } + + UpdateStatus("테스트 준비 완료"); + } + + /// + /// 캐릭터 로드 버튼 클릭 시 호출된다. + /// + private async void OnLoadCharacterClicked() + { + UpdateStatus($"캐릭터 로드 시작: {_testCharacterId}"); + + var character = await Live2DModelManager.Instance.LoadCharacterAsync(_testCharacterId, activateImmediately: true); + if (character != null) + { + UpdateStatus($"캐릭터 로드 및 활성화 완료: {_testCharacterId}"); + } + else + { + UpdateStatus($"캐릭터 로드 실패: {_testCharacterId}"); + } + } + + /// + /// 캐릭터 활성화 버튼 클릭 시 호출된다. + /// + private void OnActivateCharacterClicked() + { + Live2DModelManager.Instance.ActivateCharacter(_testCharacterId); + UpdateStatus($"캐릭터 활성화: {_testCharacterId}"); + } + + /// + /// 캐릭터 언로드 버튼 클릭 시 호출된다. + /// + private void OnUnloadCharacterClicked() + { + Live2DModelManager.Instance.UnloadCharacter(_testCharacterId); + UpdateStatus($"캐릭터 언로드 완료: {_testCharacterId}"); + } + + /// + /// 모든 캐릭터 언로드 버튼 클릭 시 호출된다. + /// + private void OnUnloadAllClicked() + { + Live2DModelManager.Instance.UnloadAllCharacters(); + UpdateStatus("모든 캐릭터 언로드 완료"); + } + + /// + /// 가시성 변경 버튼 클릭 시 호출된다. + /// + private void OnChangeVisibilityClicked() + { + _isCharacterVisible = !_isCharacterVisible; + Live2DModelManager.Instance.SetCharacterVisibility(_isCharacterVisible); + UpdateStatus($"캐릭터 가시성 변경: {(_isCharacterVisible ? "보임" : "숨김")}"); + } + + /// + /// 캐릭터 ID 입력 변경 시 호출된다. + /// + private void OnCharacterIdChanged(string newId) + { + _testCharacterId = newId; + } + + /// + /// 상태 텍스트를 업데이트한다. + /// + private void UpdateStatus(string message) + { + Debug.Log($"[Live2DModelTest] {message}"); + + if (_statusText != null) + { + _statusText.text = message; + } + } + + /// + /// 현재 상태 정보를 출력한다. + /// + [ContextMenu("현재 상태 출력")] + private void PrintCurrentStatus() + { + var activeCharacter = Live2DModelManager.Instance.GetActiveCharacter(); + var hasCharacter = Live2DModelManager.Instance.HasCharacter(_testCharacterId); + + Debug.Log($"[Live2DModelTest] 현재 상태:"); + Debug.Log($" - 테스트 캐릭터 ID: {_testCharacterId}"); + Debug.Log($" - 캐릭터 로드 여부: {hasCharacter}"); + Debug.Log($" - 활성 캐릭터: {(activeCharacter != null ? activeCharacter.name : "없음")}"); + } + } +} + diff --git a/Assets/Domain/Character/Script/Test/Live2DModelTest.cs.meta b/Assets/Domain/Character/Script/Test/Live2DModelTest.cs.meta new file mode 100644 index 0000000..d21d899 --- /dev/null +++ b/Assets/Domain/Character/Script/Test/Live2DModelTest.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7fb1442983837c140abae42a85733fe6 \ No newline at end of file diff --git a/Assets/Resources/Character.meta b/Assets/Resources/Character.meta new file mode 100644 index 0000000..892336f --- /dev/null +++ b/Assets/Resources/Character.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 33c3ee053842bbb4a8f0c00ad6fbf92a +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Character/Live2DModelRegistry.asset b/Assets/Resources/Character/Live2DModelRegistry.asset new file mode 100644 index 0000000..72bdee3 --- /dev/null +++ b/Assets/Resources/Character/Live2DModelRegistry.asset @@ -0,0 +1,17 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: a663c59d4d7aa174dbef2ec581104a3d, type: 3} + m_Name: Live2DModelRegistry + m_EditorClassIdentifier: + _entries: + - characterId: zero + characterConfig: {fileID: 11400000, guid: 4c6d1f5cb9556f24c843f3e9fe14d49e, type: 2} diff --git a/Assets/Resources/Character/Live2DModelRegistry.asset.meta b/Assets/Resources/Character/Live2DModelRegistry.asset.meta new file mode 100644 index 0000000..516a966 --- /dev/null +++ b/Assets/Resources/Character/Live2DModelRegistry.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: cf68d9632ab0d2c42a02de12beecadff +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Character/Model.meta b/Assets/Resources/Character/Model.meta new file mode 100644 index 0000000..cea515c --- /dev/null +++ b/Assets/Resources/Character/Model.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c4f92c17fdb8d804492c23f2446382db +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Resources/Character/Model/CharacterZero.asset b/Assets/Resources/Character/Model/CharacterZero.asset new file mode 100644 index 0000000..09aed52 --- /dev/null +++ b/Assets/Resources/Character/Model/CharacterZero.asset @@ -0,0 +1,33 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 2a91fff055b553542acd1857fd9bbf0f, type: 3} + m_Name: CharacterZero + m_EditorClassIdentifier: + characterId: zero + characterName: "\uC81C\uB85C" + characterPrefab: {fileID: 6181935751025943507, guid: 43e77a085e1072e4dbc6393f20643f3b, type: 3} + thumbnail: {fileID: 2800000, guid: 7ee051ec4bca36046934826113d64c32, type: 3} + characterDescription: "\uCE90\uB9AD\uD130 \uC81C\uB85C" + emotionMappings: + - emotionKey: + expressionName: + defaultIntensity: 0 + defaultDurationMs: 0 + actionMappings: + - actionKey: + motionGroup: + motionName: + isLockAtActive: 1 + lookSensitivity: 1 + lockAtDamping: 0 + gain: 1 + smoothing: 1 diff --git a/Assets/Resources/Character/Model/CharacterZero.asset.meta b/Assets/Resources/Character/Model/CharacterZero.asset.meta new file mode 100644 index 0000000..9d69fe5 --- /dev/null +++ b/Assets/Resources/Character/Model/CharacterZero.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 4c6d1f5cb9556f24c843f3e9fe14d49e +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: From e50adbfdf1caad7da91d7887eb32e7ec2050b76d Mon Sep 17 00:00:00 2001 From: WooSH Date: Tue, 12 Aug 2025 21:46:23 +0900 Subject: [PATCH 06/19] =?UTF-8?q?feat:=20live2d=20=EB=A7=A4=EB=8B=88?= =?UTF-8?q?=EC=A0=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/Live2d Sence.unity | 21 +++- Assets/Core/Input/ScreenTapManager.cs | 28 +++++- Assets/Core/Managers/SystemManager.cs | 96 +++++++++++++++++++ .../Script/Manager/Live2DModelManager.cs | 5 + .../Character/Script/Test/Live2DModelTest.cs | 10 ++ 5 files changed, 157 insertions(+), 3 deletions(-) diff --git a/Assets/App/Scenes/Live2d Sence.unity b/Assets/App/Scenes/Live2d Sence.unity index 70a0dc7..c5700d2 100644 --- a/Assets/App/Scenes/Live2d Sence.unity +++ b/Assets/App/Scenes/Live2d Sence.unity @@ -157,7 +157,7 @@ Transform: m_GameObject: {fileID: 194973787} serializedVersion: 2 m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0.08988479, y: -0.3071759, z: 0.044769116} + m_LocalPosition: {x: 0, y: 0, z: 0} m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: [] @@ -701,6 +701,7 @@ GameObject: - component: {fileID: 1916112490} - component: {fileID: 1916112492} - component: {fileID: 1916112491} + - component: {fileID: 1916112493} m_Layer: 0 m_Name: Live2D Manager m_TagString: Untagged @@ -750,6 +751,24 @@ MonoBehaviour: m_EditorClassIdentifier: _modelRoot: {fileID: 1607417703} _modelRegistry: {fileID: 11400000, guid: cf68d9632ab0d2c42a02de12beecadff, type: 2} +--- !u!114 &1916112493 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1916112489} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c31e1b9082df088489ede1181874cde4, type: 3} + m_Name: + m_EditorClassIdentifier: + _initializationManager: {fileID: 0} + _managerRegistry: {fileID: 0} + _dependencyManager: {fileID: 0} + _autoInitializeOnStart: 1 + _createManagersIfNotExist: 1 + _camera: {fileID: 0} --- !u!1 &2028963604 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Core/Input/ScreenTapManager.cs b/Assets/Core/Input/ScreenTapManager.cs index 13ee1de..f137903 100644 --- a/Assets/Core/Input/ScreenTapManager.cs +++ b/Assets/Core/Input/ScreenTapManager.cs @@ -102,14 +102,38 @@ public class ScreenTapManager : Singleton public void Initialize(Camera cam) { - if (_camera == null) - _camera = cam; + _camera = cam; // 항상 업데이트 if(_inputProvider == null) _inputProvider = new DefaultInputProvider(); if (_inputUpProvider == null) _inputUpProvider = new DefaultInputUpProvider(); } + /// + /// 씬 전환 시 Camera를 업데이트한다. + /// + public void UpdateCamera(Camera newCamera) + { + _camera = newCamera; + Debug.Log($"[ScreenTapManager] Camera 업데이트: {(newCamera != null ? newCamera.name : "null")}"); + } + + /// + /// 현재 Main Camera로 Camera를 업데이트한다. + /// + public void UpdateToMainCamera() + { + var mainCamera = Camera.main; + if (mainCamera != null) + { + UpdateCamera(mainCamera); + } + else + { + Debug.LogWarning("[ScreenTapManager] Main Camera를 찾을 수 없습니다."); + } + } + #region LockAt public bool TryGetLookDirection(out Vector3 lookDir) { diff --git a/Assets/Core/Managers/SystemManager.cs b/Assets/Core/Managers/SystemManager.cs index 8fed2b2..695c29b 100644 --- a/Assets/Core/Managers/SystemManager.cs +++ b/Assets/Core/Managers/SystemManager.cs @@ -18,6 +18,10 @@ public class SystemManager : Singleton [Header("Settings")] [SerializeField] private bool _autoInitializeOnStart = true; [SerializeField] private bool _createManagersIfNotExist = true; + [SerializeField] private bool _autoUpdateCameraOnSceneChange = true; + + [Header("Camera Settings")] + [SerializeField] private Camera _camera; private bool _initializationKickoffDone = false; @@ -52,6 +56,15 @@ protected override void Awake() InitializeGame(); } } + + private void Start() + { + // 씬 전환 이벤트 구독 + if (_autoUpdateCameraOnSceneChange) + { + UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; + } + } private void OnDestroy() { @@ -59,8 +72,62 @@ private void OnDestroy() { return; } + + // 이벤트 구독 해제 + if (_autoUpdateCameraOnSceneChange) + { + UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded; + } + Shutdown(); } + + /// + /// 씬이 로드될 때 호출된다. + /// + private void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode) + { + Debug.Log($"[SystemManager] 씬 로드됨: {scene.name}"); + UpdateCamera(); + } + + /// + /// 현재 씬의 Main Camera로 Camera를 업데이트한다. + /// + public void UpdateCamera() + { + var mainCamera = Camera.main; + if (mainCamera != null) + { + _camera = mainCamera; + Debug.Log($"[SystemManager] Camera 업데이트: {mainCamera.name}"); + + // ScreenTapManager에 Camera 주입 + if (ScreenTapManager.Instance != null) + { + ScreenTapManager.Instance.UpdateCamera(_camera); + } + } + else + { + Debug.LogWarning("[SystemManager] Main Camera를 찾을 수 없습니다."); + } + } + + /// + /// 수동으로 Camera를 설정한다. + /// + public void SetCamera(Camera camera) + { + _camera = camera; + Debug.Log($"[SystemManager] Camera 수동 설정: {(camera != null ? camera.name : "null")}"); + + // ScreenTapManager에 Camera 주입 + if (ScreenTapManager.Instance != null) + { + ScreenTapManager.Instance.UpdateCamera(_camera); + } + } public async void InitializeGame() { @@ -69,6 +136,14 @@ public async void InitializeGame() return; } _initializationKickoffDone = true; + + // Camera 업데이트 및 ScreenTapManager 초기화 + UpdateCamera(); + if (_camera != null) + { + ScreenTapManager.Instance.Initialize(_camera); + } + await InitializeGameAsync(); } @@ -111,9 +186,30 @@ public void Shutdown() public void LogManagerStatus() { Debug.Log($"[SystemManager] Initialized: {IsInitialized}"); + Debug.Log($"[SystemManager] Current Camera: {(_camera != null ? _camera.name : "null")}"); _managerRegistry?.LogManagerStatus(); } + [ContextMenu("Update Camera")] + public void UpdateCameraFromContextMenu() + { + UpdateCamera(); + } + + [ContextMenu("Set Main Camera")] + public void SetMainCameraFromContextMenu() + { + var mainCamera = Camera.main; + if (mainCamera != null) + { + SetCamera(mainCamera); + } + else + { + Debug.LogWarning("[SystemManager] Main Camera를 찾을 수 없습니다."); + } + } + public async UniTask TransitionToMainSceneAsync() { try diff --git a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs index 0722b7a..2d741be 100644 --- a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs +++ b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs @@ -16,6 +16,11 @@ public class Live2DModelManager : Singleton private readonly Dictionary _characterIdToInstance = new Dictionary(); private string _activeCharacterId; + private void Awake() + { + base.Awake(); + } + #region Public Methods /// diff --git a/Assets/Domain/Character/Script/Test/Live2DModelTest.cs b/Assets/Domain/Character/Script/Test/Live2DModelTest.cs index 399f383..94be95e 100644 --- a/Assets/Domain/Character/Script/Test/Live2DModelTest.cs +++ b/Assets/Domain/Character/Script/Test/Live2DModelTest.cs @@ -29,6 +29,16 @@ public class Live2DModelTest : MonoBehaviour private void Start() { SetupUI(); + + // Live2DModelManager 초기화 확인 + if (Live2DModelManager.Instance != null) + { + UpdateStatus("Live2DModelManager 준비 완료"); + } + else + { + UpdateStatus("Live2DModelManager 초기화 실패"); + } } /// From 69a4a6c85e72e71479a33d663d6bb7b0bd52ef30 Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 08:50:37 +0900 Subject: [PATCH 07/19] =?UTF-8?q?feat:=20=EC=8A=A4=ED=81=AC=EB=A6=B0?= =?UTF-8?q?=ED=85=9D=20=EB=94=94=EB=B2=84=EA=B7=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/Live2d Sence.unity | 5 + Assets/Core/Input/ScreenTapManager.cs | 128 +++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/Assets/App/Scenes/Live2d Sence.unity b/Assets/App/Scenes/Live2d Sence.unity index c5700d2..449d304 100644 --- a/Assets/App/Scenes/Live2d Sence.unity +++ b/Assets/App/Scenes/Live2d Sence.unity @@ -148,6 +148,10 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 220500989a5e4164381615ebdf1a6e33, type: 3} m_Name: m_EditorClassIdentifier: + _enableDebugLog: 1 + _enableDebugRay: 1 + _debugRayColor: {r: 1, g: 0, b: 0, a: 1} + _debugRayDuration: 2 --- !u!4 &194973789 Transform: m_ObjectHideFlags: 0 @@ -768,6 +772,7 @@ MonoBehaviour: _dependencyManager: {fileID: 0} _autoInitializeOnStart: 1 _createManagersIfNotExist: 1 + _autoUpdateCameraOnSceneChange: 1 _camera: {fileID: 0} --- !u!1 &2028963604 GameObject: diff --git a/Assets/Core/Input/ScreenTapManager.cs b/Assets/Core/Input/ScreenTapManager.cs index f137903..b50b85d 100644 --- a/Assets/Core/Input/ScreenTapManager.cs +++ b/Assets/Core/Input/ScreenTapManager.cs @@ -6,6 +6,7 @@ public interface IInputProvider { bool TryGetPosition(out Vector3 position); + bool IsUIBlockingInput(); } public interface IInputUpProvider @@ -41,8 +42,13 @@ public bool TryGetPosition(out Vector3 position) return false; } + public bool IsUIBlockingInput() + { + return IsPointerOverIgnoredUI(); + } + /// - /// "IgnoreLookAt" 태그가 달린 UI 클릭 여부 체크 + /// UI 클릭 여부 체크 /// private bool IsPointerOverIgnoredUI() { @@ -62,8 +68,14 @@ private bool IsPointerOverIgnoredUI() foreach (var result in results) { - if (result.gameObject.CompareTag("IgnoreLookAt")) + // UI 요소인지 확인 + if (result.gameObject.layer == LayerMask.NameToLayer("UI") || + result.gameObject.GetComponent() != null || + result.gameObject.GetComponent() != null || + result.gameObject.GetComponent() != null) + { return true; + } } return false; } @@ -100,15 +112,100 @@ public class ScreenTapManager : Singleton private CubismRaycaster _raycaster = null; + [Header("디버그 설정")] + [SerializeField] private bool _enableDebugLog = false; + [SerializeField] private bool _enableDebugRay = false; + [SerializeField] private Color _debugRayColor = Color.red; + [SerializeField] private float _debugRayDuration = 2f; + + private Vector3 _lastTapPosition = Vector3.zero; + private bool _hasTapped = false; + public void Initialize(Camera cam) { - _camera = cam; // 항상 업데이트 + _camera = cam; if(_inputProvider == null) _inputProvider = new DefaultInputProvider(); if (_inputUpProvider == null) _inputUpProvider = new DefaultInputUpProvider(); } + private void Update() + { + UpdateTapDebug(); + } + + private void UpdateTapDebug() + { + if (_inputProvider.TryGetPosition(out var currentPosition)) + { + if (!_hasTapped) + { + _lastTapPosition = currentPosition; + _hasTapped = true; + LogTapDebug(currentPosition, "탭 시작"); + DrawDebugRay(currentPosition); + } + } + else + { + if (_hasTapped && _enableDebugLog && _inputProvider is DefaultInputProvider defaultProvider) + { + if (defaultProvider.IsUIBlockingInput()) + { + Debug.Log("[ScreenTapManager] UI가 입력을 차단했습니다."); + } + } + _hasTapped = false; + } + } + + private void LogTapDebug(Vector3 screenPosition, string action) + { + if (!_enableDebugLog) return; + + var worldPosition = _camera != null ? _camera.ScreenToWorldPoint(screenPosition) : Vector3.zero; + var viewportPosition = _camera != null ? _camera.ScreenToViewportPoint(screenPosition) : Vector3.zero; + + Debug.Log($"[ScreenTapManager] {action} - 스크린: {screenPosition}, 월드: {worldPosition}, 뷰포트: {viewportPosition}"); + } + + private void DrawDebugRay(Vector3 screenPosition) + { + if (!_enableDebugRay || _camera == null) return; + + var ray = _camera.ScreenPointToRay(screenPosition); + Debug.DrawRay(ray.origin, ray.direction * 100f, _debugRayColor, _debugRayDuration); + } + + public Vector3 GetLastTapPosition() + { + return _lastTapPosition; + } + + public bool HasTapped() + { + return _hasTapped; + } + + public void SetDebugEnabled(bool enableLog, bool enableRay) + { + _enableDebugLog = enableLog; + _enableDebugRay = enableRay; + } + + private void OnDrawGizmos() + { + if (!_enableDebugRay || _camera == null || !_hasTapped) return; + + Gizmos.color = _debugRayColor; + var worldPosition = _camera.ScreenToWorldPoint(_lastTapPosition); + Gizmos.DrawWireSphere(worldPosition, 0.1f); + + var ray = _camera.ScreenPointToRay(_lastTapPosition); + Gizmos.DrawRay(ray.origin, ray.direction * 10f); + } + /// /// 씬 전환 시 Camera를 업데이트한다. /// @@ -140,6 +237,10 @@ public bool TryGetLookDirection(out Vector3 lookDir) if (_inputProvider.TryGetPosition(out var screenPos)) { lookDir = ConvertScreenToLookDirection(screenPos); + if (_enableDebugLog) + { + Debug.Log($"[ScreenTapManager] LookAt 방향: {lookDir}"); + } return true; } lookDir = Vector3.zero; @@ -193,6 +294,9 @@ public bool TryGetTapUpPosition(out CubismRaycastHit[] hitResults) return false; } + LogTapDebug(screenPosition, "탭 종료"); + DrawDebugRay(screenPosition); + var results = new CubismRaycastHit[4]; var ray = _camera.ScreenPointToRay(screenPosition); var hitCount = _raycaster.Raycast(ray, results); @@ -200,9 +304,27 @@ public bool TryGetTapUpPosition(out CubismRaycastHit[] hitResults) if (hitCount > 0) { hitResults = results; + if (_enableDebugLog) + { + Debug.Log($"[ScreenTapManager] Raycast 히트: {hitCount}개 오브젝트"); + for (int i = 0; i < hitCount; i++) + { + var drawable = results[i].Drawable; + var worldPos = results[i].WorldPosition; + var localPos = results[i].LocalPosition; + var distance = results[i].Distance; + + Debug.Log($"[ScreenTapManager] 히트 {i}: Drawable={drawable.name}, 월드위치={worldPos}, 로컬위치={localPos}, 거리={distance:F2}"); + } + } return true; } + if (_enableDebugLog) + { + Debug.Log("[ScreenTapManager] Raycast 히트 없음"); + } + return false; } #endregion From 3bee3a806e9bd8a9752c41ab7bfde9d77f380b72 Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 09:03:22 +0900 Subject: [PATCH 08/19] =?UTF-8?q?feat:=20live2d=20=EC=BA=90=EB=A6=AD?= =?UTF-8?q?=ED=84=B0=20=EC=A0=9C=EC=96=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Script/Controller/ActionController.cs | 20 +++++++++++++ .../Controller/ActionController.cs.meta | 2 ++ .../Character/Script/Controller/DTOs.cs | 10 +++++++ .../Character/Script/Controller/DTOs.cs.meta | 2 ++ .../Script/Controller/EmotionController.cs | 24 ++++++++++++++++ .../Controller/EmotionController.cs.meta | 2 ++ .../Script/Controller/IActionController.cs | 13 +++++++++ .../Controller/IActionController.cs.meta | 2 ++ .../Script/Controller/IEmotionController.cs | 14 ++++++++++ .../Controller/IEmotionController.cs.meta | 2 ++ .../Controller/Live2DParameterController.cs | 24 ++++++++++++++++ .../Live2DParameterController.cs.meta | 2 ++ .../Script/Manager/ILive2DCharacterManager.cs | 17 +++++++++++ .../Manager/ILive2DCharacterManager.cs.meta | 2 ++ .../Script/Manager/Live2DCharacterManager.cs | 28 +++++++++++++++++++ .../Manager/Live2DCharacterManager.cs.meta | 2 ++ 16 files changed, 166 insertions(+) create mode 100644 Assets/Domain/Character/Script/Controller/ActionController.cs create mode 100644 Assets/Domain/Character/Script/Controller/ActionController.cs.meta create mode 100644 Assets/Domain/Character/Script/Controller/DTOs.cs create mode 100644 Assets/Domain/Character/Script/Controller/DTOs.cs.meta create mode 100644 Assets/Domain/Character/Script/Controller/EmotionController.cs create mode 100644 Assets/Domain/Character/Script/Controller/EmotionController.cs.meta create mode 100644 Assets/Domain/Character/Script/Controller/IActionController.cs create mode 100644 Assets/Domain/Character/Script/Controller/IActionController.cs.meta create mode 100644 Assets/Domain/Character/Script/Controller/IEmotionController.cs create mode 100644 Assets/Domain/Character/Script/Controller/IEmotionController.cs.meta create mode 100644 Assets/Domain/Character/Script/Controller/Live2DParameterController.cs create mode 100644 Assets/Domain/Character/Script/Controller/Live2DParameterController.cs.meta create mode 100644 Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs create mode 100644 Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs.meta create mode 100644 Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs create mode 100644 Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs.meta diff --git a/Assets/Domain/Character/Script/Controller/ActionController.cs b/Assets/Domain/Character/Script/Controller/ActionController.cs new file mode 100644 index 0000000..f87b4c2 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/ActionController.cs @@ -0,0 +1,20 @@ +using UnityEngine; + +namespace ProjectVG.Domain.Character.Service +{ + /// + /// 행동 → 모션/파라미터 트리거를 구현하는 컨트롤러의 스켈레톤. + /// + public class ActionController : MonoBehaviour, IActionController + { + public void Initialize() + { + } + + public void TriggerAction(string action, object args = null) + { + } + } +} + + diff --git a/Assets/Domain/Character/Script/Controller/ActionController.cs.meta b/Assets/Domain/Character/Script/Controller/ActionController.cs.meta new file mode 100644 index 0000000..1cc9337 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/ActionController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7ea94bb5086c84841b65fdc5cf3c652b \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Controller/DTOs.cs b/Assets/Domain/Character/Script/Controller/DTOs.cs new file mode 100644 index 0000000..4816e2d --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/DTOs.cs @@ -0,0 +1,10 @@ +namespace ProjectVG.Domain.Character.Service +{ + /// + /// 감정/행동 데이터 전달을 위한 단순 DTO. + /// + public struct EmotionData { public string Emotion; public float Intensity; public int DurationMs; } + public struct ActionData { public string Action; public object Args; } +} + + diff --git a/Assets/Domain/Character/Script/Controller/DTOs.cs.meta b/Assets/Domain/Character/Script/Controller/DTOs.cs.meta new file mode 100644 index 0000000..266eeca --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/DTOs.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 91104183254896440a378f856ed66e43 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Controller/EmotionController.cs b/Assets/Domain/Character/Script/Controller/EmotionController.cs new file mode 100644 index 0000000..16ef56a --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/EmotionController.cs @@ -0,0 +1,24 @@ +using UnityEngine; + +namespace ProjectVG.Domain.Character.Service +{ + /// + /// 감정 → Expression 맵핑과 블렌딩을 구현하는 컨트롤러의 스켈레톤. + /// + public class EmotionController : MonoBehaviour, IEmotionController + { + public void Initialize() + { + } + + public void SetEmotion(string emotion, float intensity, int durationMs) + { + } + + public void ClearEmotion() + { + } + } +} + + diff --git a/Assets/Domain/Character/Script/Controller/EmotionController.cs.meta b/Assets/Domain/Character/Script/Controller/EmotionController.cs.meta new file mode 100644 index 0000000..af39ad2 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/EmotionController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: d1977c0cf1b32534daaeb00c3f777fe8 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Controller/IActionController.cs b/Assets/Domain/Character/Script/Controller/IActionController.cs new file mode 100644 index 0000000..df24f71 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/IActionController.cs @@ -0,0 +1,13 @@ +namespace ProjectVG.Domain.Character.Service +{ + /// + /// 행동 → 모션/파라미터 트리거를 담당한다. + /// + public interface IActionController + { + void Initialize(); + void TriggerAction(string action, object args = null); + } +} + + diff --git a/Assets/Domain/Character/Script/Controller/IActionController.cs.meta b/Assets/Domain/Character/Script/Controller/IActionController.cs.meta new file mode 100644 index 0000000..a8816b8 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/IActionController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2d7f3153340a3c444996d299a6f29f3a \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Controller/IEmotionController.cs b/Assets/Domain/Character/Script/Controller/IEmotionController.cs new file mode 100644 index 0000000..3232beb --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/IEmotionController.cs @@ -0,0 +1,14 @@ +namespace ProjectVG.Domain.Character.Service +{ + /// + /// 감정 → Expression 맵핑과 블렌딩을 담당한다. + /// + public interface IEmotionController + { + void Initialize(); + void SetEmotion(string emotion, float intensity, int durationMs); + void ClearEmotion(); + } +} + + diff --git a/Assets/Domain/Character/Script/Controller/IEmotionController.cs.meta b/Assets/Domain/Character/Script/Controller/IEmotionController.cs.meta new file mode 100644 index 0000000..b4e8673 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/IEmotionController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 7ef1b6519703d4244af92ace857a98ce \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Controller/Live2DParameterController.cs b/Assets/Domain/Character/Script/Controller/Live2DParameterController.cs new file mode 100644 index 0000000..a0877c1 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/Live2DParameterController.cs @@ -0,0 +1,24 @@ +using UnityEngine; + +namespace ProjectVG.Domain.Character.Service +{ + /// + /// Live2D 파라미터를 직접 제어하거나 프리셋을 적용하는 컨트롤러의 스켈레톤. + /// + public class Live2DParameterController : MonoBehaviour + { + public void Initialize() + { + } + + public void ApplyPreset(string presetKey) + { + } + + public void SetParameter(string parameterId, float value) + { + } + } +} + + diff --git a/Assets/Domain/Character/Script/Controller/Live2DParameterController.cs.meta b/Assets/Domain/Character/Script/Controller/Live2DParameterController.cs.meta new file mode 100644 index 0000000..6e1f953 --- /dev/null +++ b/Assets/Domain/Character/Script/Controller/Live2DParameterController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 2c84ef8b52117614e806503794e80e87 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs b/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs new file mode 100644 index 0000000..ae3bd0b --- /dev/null +++ b/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs @@ -0,0 +1,17 @@ +using UnityEngine; + +namespace ProjectVG.Domain.Character.Service +{ + /// + /// 캐릭터 전반의 상태를 단일 진입점에서 관리한다. + /// + public interface ILive2DCharacterManager + { + void Initialize(); + void ApplyReaction(EmotionData emotionData, ActionData actionData); + void OnVoiceStarted(); + void OnVoiceFinished(); + } +} + + diff --git a/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs.meta b/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs.meta new file mode 100644 index 0000000..3fd378f --- /dev/null +++ b/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: ea75b23f7c651a24c9e0055ae07e8234 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs b/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs new file mode 100644 index 0000000..6e42b44 --- /dev/null +++ b/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs @@ -0,0 +1,28 @@ +using UnityEngine; + +namespace ProjectVG.Domain.Character.Service +{ + /// + /// 상위 조정자. 모델 관리자/파라미터 컨트롤러를 묶어 감정/행동 반응을 일관되게 적용한다. + /// + public class Live2DCharacterManager : MonoBehaviour, ILive2DCharacterManager + { + public void Initialize() + { + } + + public void ApplyReaction(EmotionData emotionData, ActionData actionData) + { + } + + public void OnVoiceStarted() + { + } + + public void OnVoiceFinished() + { + } + } +} + + diff --git a/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs.meta b/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs.meta new file mode 100644 index 0000000..c06ba09 --- /dev/null +++ b/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: aeee24d07701ad747a92d82afe08caa6 \ No newline at end of file From c5074ad0755664afb62326c6806fa88b145a75ca Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 09:11:26 +0900 Subject: [PATCH 09/19] =?UTF-8?q?feat:=20=EB=84=A4=EC=9D=B4=EB=B0=8D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20Character->Model?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Docs/Live2D_Service_Design.md | 6 +++--- .../Script/Config/Live2DCharacterConfig.cs.meta | 2 -- ...Live2DCharacterConfig.cs => Live2DModelConfig.cs} | 9 +++++++-- .../Script/Config/Live2DModelConfig.cs.meta | 2 ++ .../Character/Script/Config/Live2DModelRegistry.cs | 4 ++-- .../Script/Controller/ILive2DModelApplier.cs | 2 +- .../Script/Controller/Live2DModelApplier.cs | 2 +- .../Script/Manager/ILive2DCharacterManager.cs | 4 ++-- .../Script/Manager/Live2DCharacterManager.cs | 2 +- .../Character/Script/Manager/Live2DModelManager.cs | 12 ++++++------ 10 files changed, 25 insertions(+), 20 deletions(-) delete mode 100644 Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta rename Assets/Domain/Character/Script/Config/{Live2DCharacterConfig.cs => Live2DModelConfig.cs} (92%) create mode 100644 Assets/Domain/Character/Script/Config/Live2DModelConfig.cs.meta diff --git a/Assets/Docs/Live2D_Service_Design.md b/Assets/Docs/Live2D_Service_Design.md index 5b39578..a09f8e4 100644 --- a/Assets/Docs/Live2D_Service_Design.md +++ b/Assets/Docs/Live2D_Service_Design.md @@ -94,8 +94,8 @@ graph TD ### 모듈 API 초안 (스켈레톤 중심) ```csharp -/** 캐릭터 전반의 상태를 단일 진입점에서 관리한다. */ -public interface ILive2DCharacterManager +/** 모델 전반의 상태를 단일 진입점에서 관리한다. */ +public interface ILive2DModelManagerFacade { void Initialize(); void ApplyReaction(EmotionData emotionData, ActionData actionData); @@ -144,7 +144,7 @@ public struct ActionData { public string Action; public object Args; } - 음성 종료 이벤트 후 상태 정상 복원 ### 구성/데이터 자산 -- `Live2DCharacterConfig`(ScriptableObject 제안) +- `Live2DModelConfig`(ScriptableObject 제안) - `Emotion → ExpressionKey` 사전 - `Action → MotionKey` 사전(더미 가능) - 블렌드/지속 기본값, 우선순위 정책 diff --git a/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta b/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta deleted file mode 100644 index a059ad0..0000000 --- a/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2a91fff055b553542acd1857fd9bbf0f \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs b/Assets/Domain/Character/Script/Config/Live2DModelConfig.cs similarity index 92% rename from Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs rename to Assets/Domain/Character/Script/Config/Live2DModelConfig.cs index cd3b030..4c1c54b 100644 --- a/Assets/Domain/Character/Script/Config/Live2DCharacterConfig.cs +++ b/Assets/Domain/Character/Script/Config/Live2DModelConfig.cs @@ -4,8 +4,8 @@ namespace ProjectVG.Domain.Character.Live2D.Model { - [CreateAssetMenu(fileName = "Live2DCharacterConfig", menuName = "ProjectVG/Live2D/CharacterConfig", order = 100)] - public class Live2DCharacterConfig : ScriptableObject + [CreateAssetMenu(fileName = "Live2DModelConfig", menuName = "ProjectVG/Live2D/ModelConfig", order = 100)] + public class Live2DModelConfig : ScriptableObject { [Serializable] public class EmotionMapping @@ -105,6 +105,11 @@ public class ActionMapping public Texture2D Thumbnail => thumbnail; public string CharacterDescription => characterDescription; + public string ModelId => characterId; + public string ModelName => characterName; + public GameObject ModelPrefab => characterPrefab; + public string ModelDescription => characterDescription; + // 행동/표정 public List EmotionMappings => emotionMappings; public List ActionMappings => actionMappings; diff --git a/Assets/Domain/Character/Script/Config/Live2DModelConfig.cs.meta b/Assets/Domain/Character/Script/Config/Live2DModelConfig.cs.meta new file mode 100644 index 0000000..998fcf6 --- /dev/null +++ b/Assets/Domain/Character/Script/Config/Live2DModelConfig.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f22d39f6e7c71524d911cb4b658d7fd9 \ No newline at end of file diff --git a/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs b/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs index afcee32..3569b94 100644 --- a/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs +++ b/Assets/Domain/Character/Script/Config/Live2DModelRegistry.cs @@ -10,12 +10,12 @@ public class Live2DModelRegistry : ScriptableObject public class Entry { public string characterId; - public Live2DCharacterConfig characterConfig; + public Live2DModelConfig characterConfig; } [SerializeField] private List _entries = new List(); - public bool TryGetConfig(string characterId, out Live2DCharacterConfig config) + public bool TryGetConfig(string characterId, out Live2DModelConfig config) { foreach (var e in _entries) { diff --git a/Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs b/Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs index d621fdb..6420840 100644 --- a/Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs +++ b/Assets/Domain/Character/Script/Controller/ILive2DModelApplier.cs @@ -5,7 +5,7 @@ namespace ProjectVG.Domain.Character.Service public interface ILive2DModelApplier { /** 활성 모델과 구성에 대해 LookAt, LipSync, 썸네일 등 시각 설정을 적용한다. */ - void Apply(GameObject activeModel, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig characterConfig); + void Apply(GameObject activeModel, ProjectVG.Domain.Character.Live2D.Model.Live2DModelConfig characterConfig); } } diff --git a/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs b/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs index c748130..b99adac 100644 --- a/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs +++ b/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs @@ -6,7 +6,7 @@ public class Live2DModelApplier : MonoBehaviour, ILive2DModelApplier { [SerializeField] private bool _autoApplyOnEnable = true; - public void Apply(GameObject activeModel, ProjectVG.Domain.Character.Live2D.Model.Live2DCharacterConfig characterConfig) + public void Apply(GameObject activeModel, ProjectVG.Domain.Character.Live2D.Model.Live2DModelConfig characterConfig) { } } diff --git a/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs b/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs index ae3bd0b..9e3f518 100644 --- a/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs +++ b/Assets/Domain/Character/Script/Manager/ILive2DCharacterManager.cs @@ -3,9 +3,9 @@ namespace ProjectVG.Domain.Character.Service { /// - /// 캐릭터 전반의 상태를 단일 진입점에서 관리한다. + /// 모델 전반의 상태를 단일 진입점에서 관리한다. /// - public interface ILive2DCharacterManager + public interface ILive2DModelManagerFacade { void Initialize(); void ApplyReaction(EmotionData emotionData, ActionData actionData); diff --git a/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs b/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs index 6e42b44..f11edc4 100644 --- a/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs +++ b/Assets/Domain/Character/Script/Manager/Live2DCharacterManager.cs @@ -5,7 +5,7 @@ namespace ProjectVG.Domain.Character.Service /// /// 상위 조정자. 모델 관리자/파라미터 컨트롤러를 묶어 감정/행동 반응을 일관되게 적용한다. /// - public class Live2DCharacterManager : MonoBehaviour, ILive2DCharacterManager + public class Live2DModelManagerFacade : MonoBehaviour, ILive2DModelManagerFacade { public void Initialize() { diff --git a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs index 2d741be..167b889 100644 --- a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs +++ b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs @@ -239,7 +239,7 @@ public void SetCharacterVisibility(bool isVisible) /// /// 캐릭터 설정을 가져온다. /// - private Live2DCharacterConfig GetCharacterConfig(string characterId) + private Live2DModelConfig GetCharacterConfig(string characterId) { // 레지스트리에서 먼저 찾기 if (_modelRegistry != null && _modelRegistry.TryGetConfig(characterId, out var config)) @@ -255,7 +255,7 @@ private Live2DCharacterConfig GetCharacterConfig(string characterId) /// /// 모델 인스턴스를 생성한다. /// - private GameObject CreateModelInstance(Live2DCharacterConfig config, string characterId) + private GameObject CreateModelInstance(Live2DModelConfig config, string characterId) { if (config.CharacterPrefab == null) { @@ -274,7 +274,7 @@ private GameObject CreateModelInstance(Live2DCharacterConfig config, string char /// /// 모델 인스턴스를 비동기로 생성한다. /// - private async UniTask CreateModelInstanceAsync(Live2DCharacterConfig config, string characterId) + private async UniTask CreateModelInstanceAsync(Live2DModelConfig config, string characterId) { if (config.CharacterPrefab == null) { @@ -296,7 +296,7 @@ private async UniTask CreateModelInstanceAsync(Live2DCharacterConfig /// /// 캐릭터 설정을 적용한다. /// - private void ApplyCharacterConfig(GameObject character, Live2DCharacterConfig config) + private void ApplyCharacterConfig(GameObject character, Live2DModelConfig config) { if (character == null || config == null) { @@ -311,7 +311,7 @@ private void ApplyCharacterConfig(GameObject character, Live2DCharacterConfig co /// /// 시선 추적 설정을 적용한다. /// - private void ApplyLookAtSettings(GameObject character, Live2DCharacterConfig config) + private void ApplyLookAtSettings(GameObject character, Live2DModelConfig config) { var lookController = character.GetComponent(); if (lookController == null) @@ -327,7 +327,7 @@ private void ApplyLookAtSettings(GameObject character, Live2DCharacterConfig con /// /// 립싱크 설정을 적용한다. /// - private void ApplyLipSyncSettings(GameObject character, Live2DCharacterConfig config) + private void ApplyLipSyncSettings(GameObject character, Live2DModelConfig config) { var mouthInput = character.GetComponent(); if (mouthInput == null) From 71562c21477df24f0500279bb161a7763d39b334 Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 09:29:41 +0900 Subject: [PATCH 10/19] =?UTF-8?q?chore(settings):=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=9E=90=EC=82=B0=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Readme.asset | 40 -- Assets/Readme.asset.meta | 8 - Assets/{Core/Chat.meta => Samples.meta} | 2 +- .../Extensions.meta => Samples/Core.meta} | 2 +- .../Core/Managers.meta} | 2 +- .../Core/Managers}/SampleSystemManager.cs | 0 .../Managers}/SampleSystemManager.cs.meta | 0 .../{ => Settings}/Adaptive Performance.meta | 0 .../AdaptivePerformanceGeneralSettings.asset | 0 ...ptivePerformanceGeneralSettings.asset.meta | 0 .../Adaptive Performance/Provider.meta | 0 .../Google Android Provider Loader.asset | 0 .../Google Android Provider Loader.asset.meta | 0 .../Samsung Android Provider Loader.asset | 0 ...Samsung Android Provider Loader.asset.meta | 0 .../Adaptive Performance/Settings.meta | 0 .../Google Android Provider Settings.asset | 0 ...oogle Android Provider Settings.asset.meta | 0 .../Samsung Android Provider Settings.asset | 0 ...msung Android Provider Settings.asset.meta | 0 .../Simulator Provider Settings.asset | 0 .../Simulator Provider Settings.asset.meta | 0 .../Sample.meta => Settings/InputSystem.meta} | 2 +- .../InputSystem_Actions.inputactions | 0 .../InputSystem_Actions.inputactions.meta | 0 Assets/Settings/Rendering.meta | 8 + Assets/Settings/Rendering/URP.meta | 8 + .../Rendering/URP}/DefaultVolumeProfile.asset | 0 .../URP}/DefaultVolumeProfile.asset.meta | 0 ...niversalRenderPipelineGlobalSettings.asset | 0 ...salRenderPipelineGlobalSettings.asset.meta | 0 Assets/TutorialInfo.meta | 8 - Assets/TutorialInfo/Icons.meta | 9 - Assets/TutorialInfo/Icons/Help_Icon.png | Bin 18108 -> 0 bytes Assets/TutorialInfo/Icons/Help_Icon.png.meta | 128 ---- Assets/TutorialInfo/Icons/Mobile 2D.png | Bin 4950 -> 0 bytes Assets/TutorialInfo/Icons/Mobile 2D.png.meta | 122 ---- Assets/TutorialInfo/Layout.wlt | 654 ------------------ Assets/TutorialInfo/Layout.wlt.meta | 8 - Assets/TutorialInfo/Scripts.meta | 9 - Assets/TutorialInfo/Scripts/Editor.meta | 9 - .../Scripts/Editor/ReadmeEditor.cs | 242 ------- .../Scripts/Editor/ReadmeEditor.cs.meta | 12 - Assets/TutorialInfo/Scripts/Readme.cs | 16 - Assets/TutorialInfo/Scripts/Readme.cs.meta | 12 - 45 files changed, 20 insertions(+), 1281 deletions(-) delete mode 100644 Assets/Readme.asset delete mode 100644 Assets/Readme.asset.meta rename Assets/{Core/Chat.meta => Samples.meta} (77%) rename Assets/{Core/Extensions.meta => Samples/Core.meta} (77%) rename Assets/{Core/Localization.meta => Samples/Core/Managers.meta} (77%) rename Assets/{Core/Managers/Sample => Samples/Core/Managers}/SampleSystemManager.cs (100%) rename Assets/{Core/Managers/Sample => Samples/Core/Managers}/SampleSystemManager.cs.meta (100%) rename Assets/{ => Settings}/Adaptive Performance.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset (100%) rename Assets/{ => Settings}/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/Provider.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/Provider/Google Android Provider Loader.asset (100%) rename Assets/{ => Settings}/Adaptive Performance/Provider/Google Android Provider Loader.asset.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/Provider/Samsung Android Provider Loader.asset (100%) rename Assets/{ => Settings}/Adaptive Performance/Provider/Samsung Android Provider Loader.asset.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/Settings.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/Settings/Google Android Provider Settings.asset (100%) rename Assets/{ => Settings}/Adaptive Performance/Settings/Google Android Provider Settings.asset.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/Settings/Samsung Android Provider Settings.asset (100%) rename Assets/{ => Settings}/Adaptive Performance/Settings/Samsung Android Provider Settings.asset.meta (100%) rename Assets/{ => Settings}/Adaptive Performance/Settings/Simulator Provider Settings.asset (100%) rename Assets/{ => Settings}/Adaptive Performance/Settings/Simulator Provider Settings.asset.meta (100%) rename Assets/{Core/Managers/Sample.meta => Settings/InputSystem.meta} (77%) rename Assets/{ => Settings/InputSystem}/InputSystem_Actions.inputactions (100%) rename Assets/{ => Settings/InputSystem}/InputSystem_Actions.inputactions.meta (100%) create mode 100644 Assets/Settings/Rendering.meta create mode 100644 Assets/Settings/Rendering/URP.meta rename Assets/{ => Settings/Rendering/URP}/DefaultVolumeProfile.asset (100%) rename Assets/{ => Settings/Rendering/URP}/DefaultVolumeProfile.asset.meta (100%) rename Assets/{ => Settings/Rendering/URP}/UniversalRenderPipelineGlobalSettings.asset (100%) rename Assets/{ => Settings/Rendering/URP}/UniversalRenderPipelineGlobalSettings.asset.meta (100%) delete mode 100644 Assets/TutorialInfo.meta delete mode 100644 Assets/TutorialInfo/Icons.meta delete mode 100644 Assets/TutorialInfo/Icons/Help_Icon.png delete mode 100644 Assets/TutorialInfo/Icons/Help_Icon.png.meta delete mode 100644 Assets/TutorialInfo/Icons/Mobile 2D.png delete mode 100644 Assets/TutorialInfo/Icons/Mobile 2D.png.meta delete mode 100644 Assets/TutorialInfo/Layout.wlt delete mode 100644 Assets/TutorialInfo/Layout.wlt.meta delete mode 100644 Assets/TutorialInfo/Scripts.meta delete mode 100644 Assets/TutorialInfo/Scripts/Editor.meta delete mode 100644 Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs delete mode 100644 Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta delete mode 100644 Assets/TutorialInfo/Scripts/Readme.cs delete mode 100644 Assets/TutorialInfo/Scripts/Readme.cs.meta diff --git a/Assets/Readme.asset b/Assets/Readme.asset deleted file mode 100644 index c5d79e4..0000000 --- a/Assets/Readme.asset +++ /dev/null @@ -1,40 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &11400000 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: fcf7219bab7fe46a1ad266029b2fee19, type: 3} - m_Name: Readme - m_EditorClassIdentifier: - icon: {fileID: 2800000, guid: eda43ba821d75d046a45209bde150047, type: 3} - title: Universal Mobile 2D Template - sections: - - heading: Welcome to the Universal Mobile 2D Template - text: This template sets up the right Project settings for developing a 2D game - on mobile. Also it includes some of the recommended packages for developing - on mobile. - linkText: - url: - - heading: Forums iOS - text: - linkText: Get answers and support - url: https://discussions.unity.com/tag/iOS - - heading: Forums Android - text: - linkText: Get answers and support - url: https://discussions.unity.com/tag/Android - - heading: Bugs - text: - linkText: Report any bugs - url: https://unity3d.com/unity/qa/bug-reporting - - heading: Template feedback - text: - linkText: Share your feedback on this template with us - url: https://unitysoftware.co1.qualtrics.com/jfe/form/SV_b8GWOIYxi4l6PDE?templatename=mobile2d - loadedLayout: 1 diff --git a/Assets/Readme.asset.meta b/Assets/Readme.asset.meta deleted file mode 100644 index fce2d34..0000000 --- a/Assets/Readme.asset.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 7f2c382dc2c22446db59030979f6e495 -NativeFormatImporter: - externalObjects: {} - mainObjectFileID: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Core/Chat.meta b/Assets/Samples.meta similarity index 77% rename from Assets/Core/Chat.meta rename to Assets/Samples.meta index 1c75036..1790a84 100644 --- a/Assets/Core/Chat.meta +++ b/Assets/Samples.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: a51c1117b42a1cb449577dbfe0758bd5 +guid: 7e93090ffb9d8aa47ab8da6804c4f7ca folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Core/Extensions.meta b/Assets/Samples/Core.meta similarity index 77% rename from Assets/Core/Extensions.meta rename to Assets/Samples/Core.meta index 69f76fe..9210e88 100644 --- a/Assets/Core/Extensions.meta +++ b/Assets/Samples/Core.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 69143743cfba359419e75dee89edbd2b +guid: 25ed4b2e8a2f9fc48af8e926e17a17c0 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Core/Localization.meta b/Assets/Samples/Core/Managers.meta similarity index 77% rename from Assets/Core/Localization.meta rename to Assets/Samples/Core/Managers.meta index 3942246..ce6405e 100644 --- a/Assets/Core/Localization.meta +++ b/Assets/Samples/Core/Managers.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 8c038861992a06046889d384bb65a294 +guid: bd11cb6c95a02a94faa52c02ded0b378 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/Core/Managers/Sample/SampleSystemManager.cs b/Assets/Samples/Core/Managers/SampleSystemManager.cs similarity index 100% rename from Assets/Core/Managers/Sample/SampleSystemManager.cs rename to Assets/Samples/Core/Managers/SampleSystemManager.cs diff --git a/Assets/Core/Managers/Sample/SampleSystemManager.cs.meta b/Assets/Samples/Core/Managers/SampleSystemManager.cs.meta similarity index 100% rename from Assets/Core/Managers/Sample/SampleSystemManager.cs.meta rename to Assets/Samples/Core/Managers/SampleSystemManager.cs.meta diff --git a/Assets/Adaptive Performance.meta b/Assets/Settings/Adaptive Performance.meta similarity index 100% rename from Assets/Adaptive Performance.meta rename to Assets/Settings/Adaptive Performance.meta diff --git a/Assets/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset b/Assets/Settings/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset similarity index 100% rename from Assets/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset rename to Assets/Settings/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset diff --git a/Assets/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset.meta b/Assets/Settings/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset.meta similarity index 100% rename from Assets/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset.meta rename to Assets/Settings/Adaptive Performance/AdaptivePerformanceGeneralSettings.asset.meta diff --git a/Assets/Adaptive Performance/Provider.meta b/Assets/Settings/Adaptive Performance/Provider.meta similarity index 100% rename from Assets/Adaptive Performance/Provider.meta rename to Assets/Settings/Adaptive Performance/Provider.meta diff --git a/Assets/Adaptive Performance/Provider/Google Android Provider Loader.asset b/Assets/Settings/Adaptive Performance/Provider/Google Android Provider Loader.asset similarity index 100% rename from Assets/Adaptive Performance/Provider/Google Android Provider Loader.asset rename to Assets/Settings/Adaptive Performance/Provider/Google Android Provider Loader.asset diff --git a/Assets/Adaptive Performance/Provider/Google Android Provider Loader.asset.meta b/Assets/Settings/Adaptive Performance/Provider/Google Android Provider Loader.asset.meta similarity index 100% rename from Assets/Adaptive Performance/Provider/Google Android Provider Loader.asset.meta rename to Assets/Settings/Adaptive Performance/Provider/Google Android Provider Loader.asset.meta diff --git a/Assets/Adaptive Performance/Provider/Samsung Android Provider Loader.asset b/Assets/Settings/Adaptive Performance/Provider/Samsung Android Provider Loader.asset similarity index 100% rename from Assets/Adaptive Performance/Provider/Samsung Android Provider Loader.asset rename to Assets/Settings/Adaptive Performance/Provider/Samsung Android Provider Loader.asset diff --git a/Assets/Adaptive Performance/Provider/Samsung Android Provider Loader.asset.meta b/Assets/Settings/Adaptive Performance/Provider/Samsung Android Provider Loader.asset.meta similarity index 100% rename from Assets/Adaptive Performance/Provider/Samsung Android Provider Loader.asset.meta rename to Assets/Settings/Adaptive Performance/Provider/Samsung Android Provider Loader.asset.meta diff --git a/Assets/Adaptive Performance/Settings.meta b/Assets/Settings/Adaptive Performance/Settings.meta similarity index 100% rename from Assets/Adaptive Performance/Settings.meta rename to Assets/Settings/Adaptive Performance/Settings.meta diff --git a/Assets/Adaptive Performance/Settings/Google Android Provider Settings.asset b/Assets/Settings/Adaptive Performance/Settings/Google Android Provider Settings.asset similarity index 100% rename from Assets/Adaptive Performance/Settings/Google Android Provider Settings.asset rename to Assets/Settings/Adaptive Performance/Settings/Google Android Provider Settings.asset diff --git a/Assets/Adaptive Performance/Settings/Google Android Provider Settings.asset.meta b/Assets/Settings/Adaptive Performance/Settings/Google Android Provider Settings.asset.meta similarity index 100% rename from Assets/Adaptive Performance/Settings/Google Android Provider Settings.asset.meta rename to Assets/Settings/Adaptive Performance/Settings/Google Android Provider Settings.asset.meta diff --git a/Assets/Adaptive Performance/Settings/Samsung Android Provider Settings.asset b/Assets/Settings/Adaptive Performance/Settings/Samsung Android Provider Settings.asset similarity index 100% rename from Assets/Adaptive Performance/Settings/Samsung Android Provider Settings.asset rename to Assets/Settings/Adaptive Performance/Settings/Samsung Android Provider Settings.asset diff --git a/Assets/Adaptive Performance/Settings/Samsung Android Provider Settings.asset.meta b/Assets/Settings/Adaptive Performance/Settings/Samsung Android Provider Settings.asset.meta similarity index 100% rename from Assets/Adaptive Performance/Settings/Samsung Android Provider Settings.asset.meta rename to Assets/Settings/Adaptive Performance/Settings/Samsung Android Provider Settings.asset.meta diff --git a/Assets/Adaptive Performance/Settings/Simulator Provider Settings.asset b/Assets/Settings/Adaptive Performance/Settings/Simulator Provider Settings.asset similarity index 100% rename from Assets/Adaptive Performance/Settings/Simulator Provider Settings.asset rename to Assets/Settings/Adaptive Performance/Settings/Simulator Provider Settings.asset diff --git a/Assets/Adaptive Performance/Settings/Simulator Provider Settings.asset.meta b/Assets/Settings/Adaptive Performance/Settings/Simulator Provider Settings.asset.meta similarity index 100% rename from Assets/Adaptive Performance/Settings/Simulator Provider Settings.asset.meta rename to Assets/Settings/Adaptive Performance/Settings/Simulator Provider Settings.asset.meta diff --git a/Assets/Core/Managers/Sample.meta b/Assets/Settings/InputSystem.meta similarity index 77% rename from Assets/Core/Managers/Sample.meta rename to Assets/Settings/InputSystem.meta index 2ba03b8..4f37ba1 100644 --- a/Assets/Core/Managers/Sample.meta +++ b/Assets/Settings/InputSystem.meta @@ -1,5 +1,5 @@ fileFormatVersion: 2 -guid: 1be1e8e01487ecc41a2a3dcfa61ade15 +guid: 4be52ae04c666284f931a36a6c925982 folderAsset: yes DefaultImporter: externalObjects: {} diff --git a/Assets/InputSystem_Actions.inputactions b/Assets/Settings/InputSystem/InputSystem_Actions.inputactions similarity index 100% rename from Assets/InputSystem_Actions.inputactions rename to Assets/Settings/InputSystem/InputSystem_Actions.inputactions diff --git a/Assets/InputSystem_Actions.inputactions.meta b/Assets/Settings/InputSystem/InputSystem_Actions.inputactions.meta similarity index 100% rename from Assets/InputSystem_Actions.inputactions.meta rename to Assets/Settings/InputSystem/InputSystem_Actions.inputactions.meta diff --git a/Assets/Settings/Rendering.meta b/Assets/Settings/Rendering.meta new file mode 100644 index 0000000..f7f8b65 --- /dev/null +++ b/Assets/Settings/Rendering.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0c372d94848e2054d95e3b1d15f36322 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Settings/Rendering/URP.meta b/Assets/Settings/Rendering/URP.meta new file mode 100644 index 0000000..3babd86 --- /dev/null +++ b/Assets/Settings/Rendering/URP.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: efbc1660010436348b8f5427f8190913 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/DefaultVolumeProfile.asset b/Assets/Settings/Rendering/URP/DefaultVolumeProfile.asset similarity index 100% rename from Assets/DefaultVolumeProfile.asset rename to Assets/Settings/Rendering/URP/DefaultVolumeProfile.asset diff --git a/Assets/DefaultVolumeProfile.asset.meta b/Assets/Settings/Rendering/URP/DefaultVolumeProfile.asset.meta similarity index 100% rename from Assets/DefaultVolumeProfile.asset.meta rename to Assets/Settings/Rendering/URP/DefaultVolumeProfile.asset.meta diff --git a/Assets/UniversalRenderPipelineGlobalSettings.asset b/Assets/Settings/Rendering/URP/UniversalRenderPipelineGlobalSettings.asset similarity index 100% rename from Assets/UniversalRenderPipelineGlobalSettings.asset rename to Assets/Settings/Rendering/URP/UniversalRenderPipelineGlobalSettings.asset diff --git a/Assets/UniversalRenderPipelineGlobalSettings.asset.meta b/Assets/Settings/Rendering/URP/UniversalRenderPipelineGlobalSettings.asset.meta similarity index 100% rename from Assets/UniversalRenderPipelineGlobalSettings.asset.meta rename to Assets/Settings/Rendering/URP/UniversalRenderPipelineGlobalSettings.asset.meta diff --git a/Assets/TutorialInfo.meta b/Assets/TutorialInfo.meta deleted file mode 100644 index 539a977..0000000 --- a/Assets/TutorialInfo.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 37844c66aae9c48458b0dbb739dc59f0 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Icons.meta b/Assets/TutorialInfo/Icons.meta deleted file mode 100644 index 1d19fb9..0000000 --- a/Assets/TutorialInfo/Icons.meta +++ /dev/null @@ -1,9 +0,0 @@ -fileFormatVersion: 2 -guid: 8a0c9218a650547d98138cd835033977 -folderAsset: yes -timeCreated: 1484670163 -licenseType: Store -DefaultImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Icons/Help_Icon.png b/Assets/TutorialInfo/Icons/Help_Icon.png deleted file mode 100644 index 91fa21586231dbb04e158442e6f87dc6c60efbb5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18108 zcmeI4c~leE9>*s^WogkapwenPMyYs8m}JOK2m(<-2(ru4#mUSBq9KzeVYBub)LOSL zXnpXg*izh{J1%{;wiTsTpKbLOp`KE;l-G(~o?^w;s;zG(0dfh!q08wz@0`gwAwTZ? ze&2iV=icAFcjk{-o|-ZvFyMgz2!a9=6SV2{uZa2jjiCR|=Ku5@{o`**$hASxUH3Cz z9#pe#EClhC#`yTu)I!Qm*$OEOoERSuTdb79Xd)q~tZheS!_21CprwZ%oOZM>+pnYD zGC3{P)OvVTV*Uy^XIuR09f|R4n%-oy=x8}SA;EIr zVd}=oQ(suxee3mSYL})hjoqGg@au##SHJr1{>WtB)X5*FA8b`Y%ho75Ci68<-V2?2 zrR1EkOMam#(>RB3DyLlVxZAikCvRSoR{@MTYl%OPeng@;fKF0f!=GzD}$HB7yokZ*@mk(A3Hl1Y5GE# zJZpvN#{jcz&KGYT&)$}Le$mY%J9fQk?OlF&^ELz~yRnaz6j0j+-i+x_SAeN~R&e5g9@syQ>r4ceAE{;^d za%F^As#Ktoa2OSdWr#?Lh$TXiSS3=bL}Iw-BhUoUmfA`fRO#C3J=M|wMGFe;c8dx@ zN=r*4N+TmEYXKrwDwT)`MNm{odkAgiW;<3UG}|V+M0$0!qz$(kEp{Vih8bN=Y>H0#wwzsIrYoyISt%OFB$rQey<0=`MD+vQNP?oN2R{aA&b+xEf&=B9pOy zKU}5#y9YMMD;1y_S_&^_R*YCPPTJ`C{VU;VTuVAZ;YLE`W)mq^5=s#&6G}0eL?{-M za-l*Y!-NV_EFtwu`YMk8w*K6yy=nqi87ONJMl&tK3P{9aE>I)xLN`$+2X#7CqSKf*|yRGE1o$w0A?UuKGKEMX z$3#LYDiaGaRALZfdV^Sq69xs2%Slv%V!c&(kr_}`f{~daz8?OdumRyohBmGL<&2p!e%fUEsIERK{K=|NwlopcaJ1yBRDW})qA~wokFU726 zYyq8lL)lGSs{O+}<-O9Y`!bk(1ReFM64@g|Eml)`| z9x0+H`)?kGfqUNnZWtKguG;ZJ%v?Yc8l<;py#;^b$l;W^gtY#eB%*ghdQY=6+o-`x z6~UOrVlv_wlaNS>ndmv+_AT|Woq~bI*41-B<4^1>L0XN!W}X7A?85~ApY{27XXeoM zJ1#T(_8KO?ohPyWdtUE8I&hEdIcsQLvvWF=Q_Pmhe&VE;5&FJIT>E4-Y*w> z)QO(+-=OD|p&jL1O$Eff)kXc@ql&8*FEZ}hddC644V+BW%x$5HKI_o?u5(G#h}Fn| zGC<9FEr+=!Fcmw`Byv=~MS=TPvnXQ(?e%WSl3=<~9(F&@Q06m#$D zx&_OipVyEYnN%7nRU@9oo)M|WGSZZzHPUwhHs;hNQS`)kUho>2J6Irap(oIlHo%h= z=fJp1>E|}|BO|2mnGrB%VCMb7w!24vurMgcRRYprwK-g@h(HGp7f6HE=5Vnh0v$M9 zAPrWV!^MgSbl`A-G+1p87b_yrfx`vTV6{11tcXAd4i`v+)#h-qA_5&aTp$fro5RJ5 z2z20Zfizfc4i_sT(1F7R(qOeYT&##d2M!lVgVpA6u_6K;I9wnNR-41ciU@SzaDg;f zZ4MVJBG7@u1=3)(Ib5uWKnD&NNQ2epaIqo+9XMPd4OW}O#fk`Y;BbL7SZxj$Dr0XDPekcSv7DLcax9Q)D5R~o*@q%+8NL2+vW2iHE55ELKK5>cK z*vzuFmYSr5%(St`7HaOBzXhIKJ#)$7u|cb(ttWrnw8wA3kGmIz`30TR-pnglwR7&8 zjJmZUq0O7pk1T7xlJ=l)K;6r`$JK{~qnnK4&Ni|pq*F7!bAcs9(Ru#r&kH*jbbeSK zdj03W5N{BUj>wYwuO{)cU(39e=TOg?_|nJ)zN0Hb8iyzI7dH;;Iwo@PJ_}qo;+u-^ z{4Q@?S#a0xQHm+ehoI7mR~oC&*PO=5Mq8u5WEZSiO|HCL`}*5^a(qm~ANsa&UFyTqw~o{($d>9nI~lAGxvg@#|;KEzfX7O?`g!Wu5Q#aXZ2V zkrRU*m8GZcqgJ8c+}IwxA>=sk1)rq6eAwSo7`(8~5L58&imIbN-<{k4{I;sO4Y}6t zebWOiQI92Q`GsSe>K}>&i{CjYud zyf^p(M|S9v?Nk3stZWExtl3g?;zS|2eCyh^s|*=XK=4UjFsk(3Tq@kJ3+8V~SA;ZquS^See=S0&$?-M*jS?lXUq&qdyX zZ-R5H2BM15!(4$-ps8rIMH7L~UKSw(SYZ=hCs|*L?;1KBg;4f!>)H{_YsX zBi)63M`J^Z&%YX`cSJ3@z0rbbZO1!(@*MAHjjL6^{z?9k*0)DP*UmNt-2Gn8El1bs z@gIGVH|nj;{_Fmoz2$at@}Af<^V+WXaHz#rY31jCU+c&(P@1p2qkDOCd4!>R&vO~- z9j||y^d2?gwt3rz<<;5dZOQi@*nw)>-&k>Knd6WT6t;eU3sHEjl6f^s;? F{tL_Hfk*%V diff --git a/Assets/TutorialInfo/Icons/Help_Icon.png.meta b/Assets/TutorialInfo/Icons/Help_Icon.png.meta deleted file mode 100644 index 86cc13a..0000000 --- a/Assets/TutorialInfo/Icons/Help_Icon.png.meta +++ /dev/null @@ -1,128 +0,0 @@ -fileFormatVersion: 2 -guid: 9266273b8f123004195741f969177dda -TextureImporter: - fileIDToRecycleName: {} - externalObjects: {} - serializedVersion: 5 - mipmaps: - mipMapMode: 0 - enableMipMap: 0 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - isReadable: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: -1 - aniso: 1 - mipBias: -1 - wrapU: 1 - wrapV: 1 - wrapW: -1 - nPOTScale: 0 - lightmap: 0 - compressionQuality: 50 - spriteMode: 0 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 1 - spriteTessellationDetail: -1 - textureType: 2 - textureShape: 1 - singleChannelComponent: 0 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - platformSettings: - - serializedVersion: 2 - buildTarget: DefaultTexturePlatform - maxTextureSize: 8192 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: Standalone - maxTextureSize: 8192 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: iPhone - maxTextureSize: 8192 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: Android - maxTextureSize: 8192 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - - serializedVersion: 2 - buildTarget: Windows Store Apps - maxTextureSize: 8192 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - physicsShape: [] - bones: [] - spriteID: - vertices: [] - indices: - edges: [] - weights: [] - spritePackingTag: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Icons/Mobile 2D.png b/Assets/TutorialInfo/Icons/Mobile 2D.png deleted file mode 100644 index 8d8c67a9607ff50c0c1b81b122ae9576b5a22901..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4950 zcmeHLS6EYPumzEzK#-%-0tiS~q#95_dJDayfOG*vH^75x7;CCfj^jl_fQ$3qTK5nX6wLhO7bVP??>hy>MG{O++0^g#de(D` z&)V#f$D^yD2S&77_nd~CNcVT2EI$dTuvh`TGSe33UU{|H6n1&{oNL6UULm+3i}{~z z6yMRK>`rX$G5;k=180F`KXmy98wJH>pV{7e3JN>rToA?6vlvPWZb~=>h1OFD(D465 z|K~{<>Uwy*U1r}s(9pGiY3{qE4vISqH-vetV@BEc{qj70$IiN{*UDP1rn7L|x`0t; zdrK#3k!G_3ra_Y9(+sY16z`S5@Z68WElNwiN?KWe^c@Fz@@oa4pxF_ z<|Q=V+P*`<0V3prit`99MW1i^&vwV>1Q`omRC8I@)$2wU*(gErQ&pA2kOYwK*RvU)gw_+34~axk71`_)9*qX6o1V{uS0fL56P&QFB> zr^mrvJ+%=%hZ9I|yVna^lezWWb~GBfAm}X{aDXF^M$tyc=|6Q+flCcaO+n)` z%?Bh79JlzTmrNLCaKE`~KV3Gu3SY0zG+ZmtIrPdoN6ofz%05_yX081UFkzY!W!;@p zpNOJDg*3OPjwuH_s2=E9IdMuTrlS$AwQ~IheYZt#+0Sl~Jlxj)_h>p6PyeS7!WUEM z;Q8_S=l+nZg(IX(BBXZ5OwNJC7jZH+Gw{T&g)d*$p_Au(vE~)&4-hJj9ft>t zF?2F%5)?pjG<;Blt>tlIxD>?mrKq}B~1jbNq@^J!qPmWzos(YARigYTN;BTs2|VX9{%1f##P<~(ZN7RNuM=-;9*&mOFKDpEF9 zks?KhEqlKsAYt2{i5m!P9=}Sxw&C=?`_rMt%6pXn)u2=b+iGy*rV5w?I0@dRke1o2 zP^Vqm+fuk8rq^z6*&cNN)aHb>uh|hD0z5xs>J@89=r;BF`-g1wuB^5Pq#u^EeU_&9 z$wypTOG&FmdsoM2lJcNksF|dsXbwuKH+iyE@bBu{R$x;_Y&irfzpql#*?8@a^x+A& zBD45deeQ9!e_dtvvI6!r>Dm!#-}b_=U6cmtsq?d=Z&L$uqpADIxSKM$AclVj(l%#K zgpd=OHIm6rgOrNY}c~}hG&(cp9q7_Xn{LY*oKtuS{bxHqHmEo_#OsPhckqWmqE69UwXJv~-)(&P&|rL=-Qm7z#L%V^Br$U>yAT-PT05BYLSV=c?^r~<;x4&W9Vlek*&8u1*5&tW1P^SvGYv&(-iILn<7x-U% z8Z+$SQO0ugJEGB)VOOCEGO^NUB+7i1R%E(z1s)4U7pg2+fnh&H3GKANPIs4dU~+b( zWLrex&By~K_IQHm^%La2nJn%VevP?~_Yo&;Ymd@;SY8jUjA~EUV;Z0HCaDp~b>1;a z-fM4uSfi+bz86#tBOQDNtyrm{LNtIYnhby0Q&(`I9Zo1oARrkZpM3xL;3V2n{;1IV zYv>4kkofgm*6I3&j^u~F!B4nOJi`Qw<7~(aO7*fgxwb=$7D6+n!RR(BobXCa*W*4{ zKTa00#?W7e(9qJEQ;+Nc3mIBCIK$^>rWcPQ&f7;XWDQnVhAU)Ly3%viel=lwhcj@r zRJ*xVSedjepb)isG4w4Pc!Ey5fz$GuyQC6W_xH&NN;F9KQm>a!nW2Qy=1LtA<3}-A zF*wc#Yut;X2Q6uFg$L7F1TOEcUbro$%OW4W=e^UBcIVz8E00j916}LW$&#OBqcViX z`UNGH<+F%gpOR7cGD=l8U7AEMh6901ybjR|Cj)D3Gjdwdc>Rdh`1RhnAzfDXU_}|y zL!MUUtW=g&JvcgwiRs0Ie}Q2jm|;dyQz|HU<+Z_X`n90dR(eyT)lyM4?=r6w?vMzT zSWZ3-A_SN5h@SegeuShY*x}+!^p(}J@dn{cvM1}Cg=Z2B#pz+Pu01dQ;&W8>im?O} z2!aoyl%LV;+g#yDZ}fITB9KJaTfKY8LSN*Y7~*m_jH$M2PQDH8oVcy#Vwk z$1SET-MaBp@x+qULuinK?BeQ4t3{VmyKVf)%F&yw@%*@Uhk$K;*LzcUVSQ)6QgamZ z7?y(UDJ7Va6uj;VB#%P{ps(~_iq@wM!Yh}Ix-)t)ph8K~u_Lptf<72tcdsd>Np|G= z<=OSHK%v@64#~g(4}-<-bIBT`N>gmZGZ{z3&FeG=U2jIv3D{8P{C(x9=K}{YLrvMs zG>crv0x5k4%}4OcIdbJ9Pz-aL@Hk*joJQe3DgCyw9|C!ZJq@4k$@5sg@x$sFU&|6Y z_E?S_@g5YEjADo~3{Qg<5WUUt=eitJBJm|y=(wP;Yda-KU zjSvcsU0%$kzvxbNGPp74m9nB*r}KJVlf+Ubw+|gNqL)l!q$3)XtpYPl;BGGa&3+$~ z{oZ}XpDKD@vILLFf4!URQH+u(@u*Q97N7gUVbdaifN(H4a_XK}L?#W4H4WR1XS~IL z=8pKdN26Bs#y{^S@+6&K96mFP9j1MV~x19FIJl~sZxIqos zBj+)dTV(nLC={stjFnVlHN?~dk#0^^1exmeB6_fti(W@4!#os+SUZL)I20V4Ol9UC zN-VbhoSkR!gx=l8IO?Gw1U@-aaK5L3lk9GJ7sUSr!w zbjwRYW|QvK5V+eV8?Uvl&X*4gxTV)-lUce>{qY2^`hOKp8aaR|n1i*+@*W<#1TE`U z$ZYIIp9+&wN#{y+?0q{Y8Tlg;KYZO`s2h$0XOcT$@p;tF6HD_1@uhvimG=+Xn7Uij z;fzJL!3{js)z!gvtqc)k+35?K@q$;Q4QwjERM~%aB4Il+@Js|H!P>)s;u)iWU|9@K z*ToAio{0td92UF^kgsaeF*3P+{G6paeAD=4ogwCPMiJ0ZsyOsS-<^6%TVYo^)g-(J zx`xz*yYYg-6PTIR+(X#ye^~SWzVWgbjXE-VHAusof8mN)y4AP%duqu%^3|4ikt6vV zE&P)PU=}e;ABz+dRlPpOp|PLtcHFPDJ$n1HP)=Kpfuxt7>Eg#XHNmwqTbGn)jdp0x@sOS5hq9wgix znZ6@+vlhZokrh^CU_liM8;VZ|es3L)-XC_O^pYD1-m4caLIDHtFYVZjpYSY2|LwJcjB37<2s!=Us>p0^(H3>P zfBagx^-G1mvYq?${@WE6b*K9~1>Ib`et8N+d&ReNI7vr%E(*R6Q7zxT^D-%!Z1|G``FF#CvaEKI)_IQRUp|y?6Bbr z2u8;tR{4`+pOI=rtMah1HLSoAE)ZcNudDF2fAWWQcM)o7308N;N}};K1nCJKmjNR z5?S5aTBE1u2!*P4l|`D5v1=ifwj#>Q*%%HH`BSp)OQai^CI4}N;PsL~@415=)xbNFk)OSb+x2TDYn zx#kL?4kFT=ljyE5nGX@YKX*;sYRmL7{=7X+yrA*l-<%xhqljE`yIHABuI{m;qutv! zhPOuycW1osRzHZBEq79YdNVcPhVz<@xdo`1?s%gc{adiB_m~FUgkrSHy|KhF(s&#` zrK~+DFkA)zwZIO1!gRef^|Y z=72rf-A|6RfS>AVp&$vdPJB9FOga&DXC@`mHwV1BTSJ`?NkvwcB3Kk$nhvj}u{92u z6A$acp9W}D-hwc@XANg^RZdx3P9tG(sAG3o^EZ@yh^18{El`vT7!&!4zQbT3gu^h3C8n>su7rzQp&VDufg8`Vv!#~MO#+^8~|0bFbX=?s8v&@`cz|9(E?hYrYk@X_m?lYDD^8S;i z+9^R5h`f+``{%cc(fBLJ;lCw`Ekn)Nlp`3(i%jl3 z)%5;%XF(mC%0fu-ga~<#E}8UDZs3@tv=^Wz>woV8AhZT_XV-@b=qZ}|TcTpviDE_= WLdvAoAP#soMgddTS1VVs3;Q2jy99;+ diff --git a/Assets/TutorialInfo/Icons/Mobile 2D.png.meta b/Assets/TutorialInfo/Icons/Mobile 2D.png.meta deleted file mode 100644 index 89301f3..0000000 --- a/Assets/TutorialInfo/Icons/Mobile 2D.png.meta +++ /dev/null @@ -1,122 +0,0 @@ -fileFormatVersion: 2 -guid: eda43ba821d75d046a45209bde150047 -TextureImporter: - internalIDToNameTable: [] - externalObjects: {} - serializedVersion: 11 - mipmaps: - mipMapMode: 0 - enableMipMap: 1 - sRGBTexture: 1 - linearTexture: 0 - fadeOut: 0 - borderMipMap: 0 - mipMapsPreserveCoverage: 0 - alphaTestReferenceValue: 0.5 - mipMapFadeDistanceStart: 1 - mipMapFadeDistanceEnd: 3 - bumpmap: - convertToNormalMap: 0 - externalNormalMap: 0 - heightScale: 0.25 - normalMapFilter: 0 - isReadable: 0 - streamingMipmaps: 0 - streamingMipmapsPriority: 0 - vTOnly: 0 - ignoreMasterTextureLimit: 0 - grayScaleToAlpha: 0 - generateCubemap: 6 - cubemapConvolution: 0 - seamlessCubemap: 0 - textureFormat: 1 - maxTextureSize: 2048 - textureSettings: - serializedVersion: 2 - filterMode: 1 - aniso: 1 - mipBias: 0 - wrapU: 1 - wrapV: 1 - wrapW: 1 - nPOTScale: 0 - lightmap: 0 - compressionQuality: 50 - spriteMode: 1 - spriteExtrude: 1 - spriteMeshType: 1 - alignment: 0 - spritePivot: {x: 0.5, y: 0.5} - spritePixelsToUnits: 100 - spriteBorder: {x: 0, y: 0, z: 0, w: 0} - spriteGenerateFallbackPhysicsShape: 1 - alphaUsage: 1 - alphaIsTransparency: 0 - spriteTessellationDetail: -1 - textureType: 0 - textureShape: 1 - singleChannelComponent: 0 - flipbookRows: 1 - flipbookColumns: 1 - maxTextureSizeSet: 0 - compressionQualitySet: 0 - textureFormatSet: 0 - ignorePngGamma: 0 - applyGammaDecoding: 0 - platformSettings: - - serializedVersion: 3 - buildTarget: DefaultTexturePlatform - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 - buildTarget: Standalone - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - - serializedVersion: 3 - buildTarget: Server - maxTextureSize: 2048 - resizeAlgorithm: 0 - textureFormat: -1 - textureCompression: 1 - compressionQuality: 50 - crunchedCompression: 0 - allowsAlphaSplitting: 0 - overridden: 0 - androidETC2FallbackOverride: 0 - forceMaximumCompressionQuality_BC6H_BC7: 0 - spriteSheet: - serializedVersion: 2 - sprites: [] - outline: [] - physicsShape: [] - bones: [] - spriteID: 5e97eb03825dee720800000000000000 - internalID: 0 - vertices: [] - indices: - edges: [] - weights: [] - secondaryTextures: [] - nameFileIdTable: {} - spritePackingTag: - pSDRemoveMatte: 0 - pSDShowRemoveMatteOption: 0 - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Layout.wlt b/Assets/TutorialInfo/Layout.wlt deleted file mode 100644 index 7b50a25..0000000 --- a/Assets/TutorialInfo/Layout.wlt +++ /dev/null @@ -1,654 +0,0 @@ -%YAML 1.1 -%TAG !u! tag:unity3d.com,2011: ---- !u!114 &1 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12004, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_PixelRect: - serializedVersion: 2 - x: 0 - y: 45 - width: 1666 - height: 958 - m_ShowMode: 4 - m_Title: - m_RootView: {fileID: 6} - m_MinSize: {x: 950, y: 542} - m_MaxSize: {x: 10000, y: 10000} ---- !u!114 &2 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: [] - m_Position: - serializedVersion: 2 - x: 0 - y: 466 - width: 290 - height: 442 - m_MinSize: {x: 234, y: 271} - m_MaxSize: {x: 10004, y: 10021} - m_ActualView: {fileID: 14} - m_Panes: - - {fileID: 14} - m_Selected: 0 - m_LastSelected: 0 ---- !u!114 &3 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: - - {fileID: 4} - - {fileID: 2} - m_Position: - serializedVersion: 2 - x: 973 - y: 0 - width: 290 - height: 908 - m_MinSize: {x: 234, y: 492} - m_MaxSize: {x: 10004, y: 14042} - vertical: 1 - controlID: 226 ---- !u!114 &4 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: [] - m_Position: - serializedVersion: 2 - x: 0 - y: 0 - width: 290 - height: 466 - m_MinSize: {x: 204, y: 221} - m_MaxSize: {x: 4004, y: 4021} - m_ActualView: {fileID: 17} - m_Panes: - - {fileID: 17} - m_Selected: 0 - m_LastSelected: 0 ---- !u!114 &5 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: [] - m_Position: - serializedVersion: 2 - x: 0 - y: 466 - width: 973 - height: 442 - m_MinSize: {x: 202, y: 221} - m_MaxSize: {x: 4002, y: 4021} - m_ActualView: {fileID: 15} - m_Panes: - - {fileID: 15} - m_Selected: 0 - m_LastSelected: 0 ---- !u!114 &6 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12008, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: - - {fileID: 7} - - {fileID: 8} - - {fileID: 9} - m_Position: - serializedVersion: 2 - x: 0 - y: 0 - width: 1666 - height: 958 - m_MinSize: {x: 950, y: 542} - m_MaxSize: {x: 10000, y: 10000} ---- !u!114 &7 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12011, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: [] - m_Position: - serializedVersion: 2 - x: 0 - y: 0 - width: 1666 - height: 30 - m_MinSize: {x: 0, y: 0} - m_MaxSize: {x: 0, y: 0} - m_LastLoadedLayoutName: Tutorial ---- !u!114 &8 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: - - {fileID: 10} - - {fileID: 3} - - {fileID: 11} - m_Position: - serializedVersion: 2 - x: 0 - y: 30 - width: 1666 - height: 908 - m_MinSize: {x: 713, y: 492} - m_MaxSize: {x: 18008, y: 14042} - vertical: 0 - controlID: 74 ---- !u!114 &9 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12042, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: [] - m_Position: - serializedVersion: 2 - x: 0 - y: 938 - width: 1666 - height: 20 - m_MinSize: {x: 0, y: 0} - m_MaxSize: {x: 0, y: 0} ---- !u!114 &10 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12010, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: - - {fileID: 12} - - {fileID: 5} - m_Position: - serializedVersion: 2 - x: 0 - y: 0 - width: 973 - height: 908 - m_MinSize: {x: 202, y: 442} - m_MaxSize: {x: 4002, y: 8042} - vertical: 1 - controlID: 75 ---- !u!114 &11 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: [] - m_Position: - serializedVersion: 2 - x: 1263 - y: 0 - width: 403 - height: 908 - m_MinSize: {x: 277, y: 71} - m_MaxSize: {x: 4002, y: 4021} - m_ActualView: {fileID: 13} - m_Panes: - - {fileID: 13} - m_Selected: 0 - m_LastSelected: 0 ---- !u!114 &12 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12006, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_Children: [] - m_Position: - serializedVersion: 2 - x: 0 - y: 0 - width: 973 - height: 466 - m_MinSize: {x: 202, y: 221} - m_MaxSize: {x: 4002, y: 4021} - m_ActualView: {fileID: 16} - m_Panes: - - {fileID: 16} - m_Selected: 0 - m_LastSelected: 0 ---- !u!114 &13 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12019, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_AutoRepaintOnSceneChange: 0 - m_MinSize: {x: 275, y: 50} - m_MaxSize: {x: 4000, y: 4000} - m_TitleContent: - m_Text: Inspector - m_Image: {fileID: -6905738622615590433, guid: 0000000000000000d000000000000000, - type: 0} - m_Tooltip: - m_DepthBufferBits: 0 - m_Pos: - serializedVersion: 2 - x: 2 - y: 19 - width: 401 - height: 887 - m_ScrollPosition: {x: 0, y: 0} - m_InspectorMode: 0 - m_PreviewResizer: - m_CachedPref: -160 - m_ControlHash: -371814159 - m_PrefName: Preview_InspectorPreview - m_PreviewWindow: {fileID: 0} ---- !u!114 &14 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12014, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_AutoRepaintOnSceneChange: 0 - m_MinSize: {x: 230, y: 250} - m_MaxSize: {x: 10000, y: 10000} - m_TitleContent: - m_Text: Project - m_Image: {fileID: -7501376956915960154, guid: 0000000000000000d000000000000000, - type: 0} - m_Tooltip: - m_DepthBufferBits: 0 - m_Pos: - serializedVersion: 2 - x: 2 - y: 19 - width: 286 - height: 421 - m_SearchFilter: - m_NameFilter: - m_ClassNames: [] - m_AssetLabels: [] - m_AssetBundleNames: [] - m_VersionControlStates: [] - m_ReferencingInstanceIDs: - m_ScenePaths: [] - m_ShowAllHits: 0 - m_SearchArea: 0 - m_Folders: - - Assets - m_ViewMode: 0 - m_StartGridSize: 64 - m_LastFolders: - - Assets - m_LastFoldersGridSize: -1 - m_LastProjectPath: /Users/danielbrauer/Unity Projects/New Unity Project 47 - m_IsLocked: 0 - m_FolderTreeState: - scrollPos: {x: 0, y: 0} - m_SelectedIDs: ee240000 - m_LastClickedID: 9454 - m_ExpandedIDs: ee24000000ca9a3bffffff7f - m_RenameOverlay: - m_UserAcceptedRename: 0 - m_Name: - m_OriginalName: - m_EditFieldRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 0 - height: 0 - m_UserData: 0 - m_IsWaitingForDelay: 0 - m_IsRenaming: 0 - m_OriginalEventType: 11 - m_IsRenamingFilename: 1 - m_ClientGUIView: {fileID: 0} - m_SearchString: - m_CreateAssetUtility: - m_EndAction: {fileID: 0} - m_InstanceID: 0 - m_Path: - m_Icon: {fileID: 0} - m_ResourceFile: - m_AssetTreeState: - scrollPos: {x: 0, y: 0} - m_SelectedIDs: 68fbffff - m_LastClickedID: 0 - m_ExpandedIDs: ee240000 - m_RenameOverlay: - m_UserAcceptedRename: 0 - m_Name: - m_OriginalName: - m_EditFieldRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 0 - height: 0 - m_UserData: 0 - m_IsWaitingForDelay: 0 - m_IsRenaming: 0 - m_OriginalEventType: 11 - m_IsRenamingFilename: 1 - m_ClientGUIView: {fileID: 0} - m_SearchString: - m_CreateAssetUtility: - m_EndAction: {fileID: 0} - m_InstanceID: 0 - m_Path: - m_Icon: {fileID: 0} - m_ResourceFile: - m_ListAreaState: - m_SelectedInstanceIDs: 68fbffff - m_LastClickedInstanceID: -1176 - m_HadKeyboardFocusLastEvent: 0 - m_ExpandedInstanceIDs: c6230000 - m_RenameOverlay: - m_UserAcceptedRename: 0 - m_Name: - m_OriginalName: - m_EditFieldRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 0 - height: 0 - m_UserData: 0 - m_IsWaitingForDelay: 0 - m_IsRenaming: 0 - m_OriginalEventType: 11 - m_IsRenamingFilename: 1 - m_ClientGUIView: {fileID: 0} - m_CreateAssetUtility: - m_EndAction: {fileID: 0} - m_InstanceID: 0 - m_Path: - m_Icon: {fileID: 0} - m_ResourceFile: - m_NewAssetIndexInList: -1 - m_ScrollPosition: {x: 0, y: 0} - m_GridSize: 64 - m_DirectoriesAreaWidth: 110 ---- !u!114 &15 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12015, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_AutoRepaintOnSceneChange: 1 - m_MinSize: {x: 200, y: 200} - m_MaxSize: {x: 4000, y: 4000} - m_TitleContent: - m_Text: Game - m_Image: {fileID: -2087823869225018852, guid: 0000000000000000d000000000000000, - type: 0} - m_Tooltip: - m_DepthBufferBits: 32 - m_Pos: - serializedVersion: 2 - x: 0 - y: 19 - width: 971 - height: 421 - m_MaximizeOnPlay: 0 - m_Gizmos: 0 - m_Stats: 0 - m_SelectedSizes: 00000000000000000000000000000000000000000000000000000000000000000000000000000000 - m_TargetDisplay: 0 - m_ZoomArea: - m_HRangeLocked: 0 - m_VRangeLocked: 0 - m_HBaseRangeMin: -242.75 - m_HBaseRangeMax: 242.75 - m_VBaseRangeMin: -101 - m_VBaseRangeMax: 101 - m_HAllowExceedBaseRangeMin: 1 - m_HAllowExceedBaseRangeMax: 1 - m_VAllowExceedBaseRangeMin: 1 - m_VAllowExceedBaseRangeMax: 1 - m_ScaleWithWindow: 0 - m_HSlider: 0 - m_VSlider: 0 - m_IgnoreScrollWheelUntilClicked: 0 - m_EnableMouseInput: 1 - m_EnableSliderZoom: 0 - m_UniformScale: 1 - m_UpDirection: 1 - m_DrawArea: - serializedVersion: 2 - x: 0 - y: 17 - width: 971 - height: 404 - m_Scale: {x: 2, y: 2} - m_Translation: {x: 485.5, y: 202} - m_MarginLeft: 0 - m_MarginRight: 0 - m_MarginTop: 0 - m_MarginBottom: 0 - m_LastShownAreaInsideMargins: - serializedVersion: 2 - x: -242.75 - y: -101 - width: 485.5 - height: 202 - m_MinimalGUI: 1 - m_defaultScale: 2 - m_TargetTexture: {fileID: 0} - m_CurrentColorSpace: 0 - m_LastWindowPixelSize: {x: 1942, y: 842} - m_ClearInEditMode: 1 - m_NoCameraWarning: 1 - m_LowResolutionForAspectRatios: 01000000000100000100 ---- !u!114 &16 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12013, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_AutoRepaintOnSceneChange: 1 - m_MinSize: {x: 200, y: 200} - m_MaxSize: {x: 4000, y: 4000} - m_TitleContent: - m_Text: Scene - m_Image: {fileID: 2318424515335265636, guid: 0000000000000000d000000000000000, - type: 0} - m_Tooltip: - m_DepthBufferBits: 32 - m_Pos: - serializedVersion: 2 - x: 0 - y: 19 - width: 971 - height: 445 - m_SceneLighting: 1 - lastFramingTime: 0 - m_2DMode: 0 - m_isRotationLocked: 0 - m_AudioPlay: 0 - m_Position: - m_Target: {x: 0, y: 0, z: 0} - speed: 2 - m_Value: {x: 0, y: 0, z: 0} - m_RenderMode: 0 - m_ValidateTrueMetals: 0 - m_SceneViewState: - showFog: 1 - showMaterialUpdate: 0 - showSkybox: 1 - showFlares: 1 - showImageEffects: 1 - grid: - xGrid: - m_Target: 0 - speed: 2 - m_Value: 0 - yGrid: - m_Target: 1 - speed: 2 - m_Value: 1 - zGrid: - m_Target: 0 - speed: 2 - m_Value: 0 - m_Rotation: - m_Target: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} - speed: 2 - m_Value: {x: -0.08717229, y: 0.89959055, z: -0.21045254, w: -0.3726226} - m_Size: - m_Target: 10 - speed: 2 - m_Value: 10 - m_Ortho: - m_Target: 0 - speed: 2 - m_Value: 0 - m_LastSceneViewRotation: {x: 0, y: 0, z: 0, w: 0} - m_LastSceneViewOrtho: 0 - m_ReplacementShader: {fileID: 0} - m_ReplacementString: - m_LastLockedObject: {fileID: 0} - m_ViewIsLockedToObject: 0 ---- !u!114 &17 -MonoBehaviour: - m_ObjectHideFlags: 52 - m_PrefabParentObject: {fileID: 0} - m_PrefabInternal: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 1 - m_Script: {fileID: 12061, guid: 0000000000000000e000000000000000, type: 0} - m_Name: - m_EditorClassIdentifier: - m_AutoRepaintOnSceneChange: 0 - m_MinSize: {x: 200, y: 200} - m_MaxSize: {x: 4000, y: 4000} - m_TitleContent: - m_Text: Hierarchy - m_Image: {fileID: -590624980919486359, guid: 0000000000000000d000000000000000, - type: 0} - m_Tooltip: - m_DepthBufferBits: 0 - m_Pos: - serializedVersion: 2 - x: 2 - y: 19 - width: 286 - height: 445 - m_TreeViewState: - scrollPos: {x: 0, y: 0} - m_SelectedIDs: 68fbffff - m_LastClickedID: -1176 - m_ExpandedIDs: 7efbffff00000000 - m_RenameOverlay: - m_UserAcceptedRename: 0 - m_Name: - m_OriginalName: - m_EditFieldRect: - serializedVersion: 2 - x: 0 - y: 0 - width: 0 - height: 0 - m_UserData: 0 - m_IsWaitingForDelay: 0 - m_IsRenaming: 0 - m_OriginalEventType: 11 - m_IsRenamingFilename: 0 - m_ClientGUIView: {fileID: 0} - m_SearchString: - m_ExpandedScenes: - - - m_CurrenRootInstanceID: 0 - m_Locked: 0 - m_CurrentSortingName: TransformSorting diff --git a/Assets/TutorialInfo/Layout.wlt.meta b/Assets/TutorialInfo/Layout.wlt.meta deleted file mode 100644 index 92800c6..0000000 --- a/Assets/TutorialInfo/Layout.wlt.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 96e98bffd05413f489cd9851fc862d1f -timeCreated: 1487337779 -licenseType: Store -DefaultImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts.meta b/Assets/TutorialInfo/Scripts.meta deleted file mode 100644 index 02da605..0000000 --- a/Assets/TutorialInfo/Scripts.meta +++ /dev/null @@ -1,9 +0,0 @@ -fileFormatVersion: 2 -guid: 5a9bcd70e6a4b4b05badaa72e827d8e0 -folderAsset: yes -timeCreated: 1475835190 -licenseType: Store -DefaultImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts/Editor.meta b/Assets/TutorialInfo/Scripts/Editor.meta deleted file mode 100644 index f59f099..0000000 --- a/Assets/TutorialInfo/Scripts/Editor.meta +++ /dev/null @@ -1,9 +0,0 @@ -fileFormatVersion: 2 -guid: 3ad9b87dffba344c89909c6d1b1c17e1 -folderAsset: yes -timeCreated: 1475593892 -licenseType: Store -DefaultImporter: - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs b/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs deleted file mode 100644 index ad55eca..0000000 --- a/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs +++ /dev/null @@ -1,242 +0,0 @@ -using System.Collections; -using System.Collections.Generic; -using UnityEngine; -using UnityEditor; -using System; -using System.IO; -using System.Reflection; - -[CustomEditor(typeof(Readme))] -[InitializeOnLoad] -public class ReadmeEditor : Editor -{ - static string s_ShowedReadmeSessionStateName = "ReadmeEditor.showedReadme"; - - static string s_ReadmeSourceDirectory = "Assets/TutorialInfo"; - - const float k_Space = 16f; - - static ReadmeEditor() - { - EditorApplication.delayCall += SelectReadmeAutomatically; - } - - static void RemoveTutorial() - { - if (EditorUtility.DisplayDialog("Remove Readme Assets", - - $"All contents under {s_ReadmeSourceDirectory} will be removed, are you sure you want to proceed?", - "Proceed", - "Cancel")) - { - if (Directory.Exists(s_ReadmeSourceDirectory)) - { - FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory); - FileUtil.DeleteFileOrDirectory(s_ReadmeSourceDirectory + ".meta"); - } - else - { - Debug.Log($"Could not find the Readme folder at {s_ReadmeSourceDirectory}"); - } - - var readmeAsset = SelectReadme(); - if (readmeAsset != null) - { - var path = AssetDatabase.GetAssetPath(readmeAsset); - FileUtil.DeleteFileOrDirectory(path + ".meta"); - FileUtil.DeleteFileOrDirectory(path); - } - - AssetDatabase.Refresh(); - } - } - - static void SelectReadmeAutomatically() - { - if (!SessionState.GetBool(s_ShowedReadmeSessionStateName, false)) - { - var readme = SelectReadme(); - SessionState.SetBool(s_ShowedReadmeSessionStateName, true); - - if (readme && !readme.loadedLayout) - { - LoadLayout(); - readme.loadedLayout = true; - } - } - } - - static void LoadLayout() - { - var assembly = typeof(EditorApplication).Assembly; - var windowLayoutType = assembly.GetType("UnityEditor.WindowLayout", true); - var method = windowLayoutType.GetMethod("LoadWindowLayout", BindingFlags.Public | BindingFlags.Static); - method.Invoke(null, new object[] { Path.Combine(Application.dataPath, "TutorialInfo/Layout.wlt"), false }); - } - - static Readme SelectReadme() - { - var ids = AssetDatabase.FindAssets("Readme t:Readme"); - if (ids.Length == 1) - { - var readmeObject = AssetDatabase.LoadMainAssetAtPath(AssetDatabase.GUIDToAssetPath(ids[0])); - - Selection.objects = new UnityEngine.Object[] { readmeObject }; - - return (Readme)readmeObject; - } - else - { - Debug.Log("Couldn't find a readme"); - return null; - } - } - - protected override void OnHeaderGUI() - { - var readme = (Readme)target; - Init(); - - var iconWidth = Mathf.Min(EditorGUIUtility.currentViewWidth / 3f - 20f, 128f); - - GUILayout.BeginHorizontal("In BigTitle"); - { - if (readme.icon != null) - { - GUILayout.Space(k_Space); - GUILayout.Label(readme.icon, GUILayout.Width(iconWidth), GUILayout.Height(iconWidth)); - } - GUILayout.Space(k_Space); - GUILayout.BeginVertical(); - { - - GUILayout.FlexibleSpace(); - GUILayout.Label(readme.title, TitleStyle); - GUILayout.FlexibleSpace(); - } - GUILayout.EndVertical(); - GUILayout.FlexibleSpace(); - } - GUILayout.EndHorizontal(); - } - - public override void OnInspectorGUI() - { - var readme = (Readme)target; - Init(); - - foreach (var section in readme.sections) - { - if (!string.IsNullOrEmpty(section.heading)) - { - GUILayout.Label(section.heading, HeadingStyle); - } - - if (!string.IsNullOrEmpty(section.text)) - { - GUILayout.Label(section.text, BodyStyle); - } - - if (!string.IsNullOrEmpty(section.linkText)) - { - if (LinkLabel(new GUIContent(section.linkText))) - { - Application.OpenURL(section.url); - } - } - - GUILayout.Space(k_Space); - } - - if (GUILayout.Button("Remove Readme Assets", ButtonStyle)) - { - RemoveTutorial(); - } - } - - bool m_Initialized; - - GUIStyle LinkStyle - { - get { return m_LinkStyle; } - } - - [SerializeField] - GUIStyle m_LinkStyle; - - GUIStyle TitleStyle - { - get { return m_TitleStyle; } - } - - [SerializeField] - GUIStyle m_TitleStyle; - - GUIStyle HeadingStyle - { - get { return m_HeadingStyle; } - } - - [SerializeField] - GUIStyle m_HeadingStyle; - - GUIStyle BodyStyle - { - get { return m_BodyStyle; } - } - - [SerializeField] - GUIStyle m_BodyStyle; - - GUIStyle ButtonStyle - { - get { return m_ButtonStyle; } - } - - [SerializeField] - GUIStyle m_ButtonStyle; - - void Init() - { - if (m_Initialized) - return; - m_BodyStyle = new GUIStyle(EditorStyles.label); - m_BodyStyle.wordWrap = true; - m_BodyStyle.fontSize = 14; - m_BodyStyle.richText = true; - - m_TitleStyle = new GUIStyle(m_BodyStyle); - m_TitleStyle.fontSize = 26; - - m_HeadingStyle = new GUIStyle(m_BodyStyle); - m_HeadingStyle.fontStyle = FontStyle.Bold; - m_HeadingStyle.fontSize = 18; - - m_LinkStyle = new GUIStyle(m_BodyStyle); - m_LinkStyle.wordWrap = false; - - // Match selection color which works nicely for both light and dark skins - m_LinkStyle.normal.textColor = new Color(0x00 / 255f, 0x78 / 255f, 0xDA / 255f, 1f); - m_LinkStyle.stretchWidth = false; - - m_ButtonStyle = new GUIStyle(EditorStyles.miniButton); - m_ButtonStyle.fontStyle = FontStyle.Bold; - - m_Initialized = true; - } - - bool LinkLabel(GUIContent label, params GUILayoutOption[] options) - { - var position = GUILayoutUtility.GetRect(label, LinkStyle, options); - - Handles.BeginGUI(); - Handles.color = LinkStyle.normal.textColor; - Handles.DrawLine(new Vector3(position.xMin, position.yMax), new Vector3(position.xMax, position.yMax)); - Handles.color = Color.white; - Handles.EndGUI(); - - EditorGUIUtility.AddCursorRect(position, MouseCursor.Link); - - return GUI.Button(position, label, LinkStyle); - } -} diff --git a/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta b/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta deleted file mode 100644 index f038618..0000000 --- a/Assets/TutorialInfo/Scripts/Editor/ReadmeEditor.cs.meta +++ /dev/null @@ -1,12 +0,0 @@ -fileFormatVersion: 2 -guid: 476cc7d7cd9874016adc216baab94a0a -timeCreated: 1484146680 -licenseType: Store -MonoImporter: - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/TutorialInfo/Scripts/Readme.cs b/Assets/TutorialInfo/Scripts/Readme.cs deleted file mode 100644 index 95f6269..0000000 --- a/Assets/TutorialInfo/Scripts/Readme.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using UnityEngine; - -public class Readme : ScriptableObject -{ - public Texture2D icon; - public string title; - public Section[] sections; - public bool loadedLayout; - - [Serializable] - public class Section - { - public string heading, text, linkText, url; - } -} diff --git a/Assets/TutorialInfo/Scripts/Readme.cs.meta b/Assets/TutorialInfo/Scripts/Readme.cs.meta deleted file mode 100644 index 935153f..0000000 --- a/Assets/TutorialInfo/Scripts/Readme.cs.meta +++ /dev/null @@ -1,12 +0,0 @@ -fileFormatVersion: 2 -guid: fcf7219bab7fe46a1ad266029b2fee19 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: - - icon: {instanceID: 0} - executionOrder: 0 - icon: {fileID: 2800000, guid: a186f8a87ca4f4d3aa864638ad5dfb65, type: 3} - userData: - assetBundleName: - assetBundleVariant: From 1fa1ded8a1b5a463462406c51a5406c3d8db142d Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 10:04:43 +0900 Subject: [PATCH 11/19] =?UTF-8?q?docs:=20=EC=9C=A0=EB=8B=88=ED=8B=B0=20?= =?UTF-8?q?=EB=AC=B8=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._Manager_Checklist.md.meta => Design.meta} | 5 +- .../{ => Design}/Chat_System_Design_v2.md | 0 .../Chat_System_Design_v2.md.meta | 0 .../Component_Analysis_Document.md | 0 .../Component_Analysis_Document.md.meta | 0 .../{ => Design}/DialogueBubble_Design.md | 0 .../DialogueBubble_Design.md.meta | 0 .../Initial_Loading_System_Design.md | 0 .../Initial_Loading_System_Design.md.meta | 0 .../Initial_Startup_System_Design.md | 0 .../Initial_Startup_System_Design.md.meta | 0 .../{ => Design}/Live2D_Service_Design.md | 0 .../Live2D_Service_Design.md.meta | 0 Assets/Docs/Guides.meta | 8 + Assets/Docs/Guides/Manager_System_Guide.md | 110 +++++++ .../{ => Guides}/Manager_System_Guide.md.meta | 0 .../Docs/Guides/ProjectVG_Structure_Guide.md | 169 ++++++++++ .../ProjectVG_Structure_Guide.md.meta | 0 .../Docs/Guides/Unity_Naming_Conventions.md | 62 ++++ .../Unity_Naming_Conventions.md.meta | 0 Assets/Docs/Manager_System_Guide.md | 305 ------------------ Assets/Docs/New_Manager_Checklist.md | 198 ------------ Assets/Docs/ProjectVG_Structure_Guide.md | 100 ------ Assets/Docs/Unity_Naming_Conventions.md | 236 -------------- 24 files changed, 352 insertions(+), 841 deletions(-) rename Assets/Docs/{New_Manager_Checklist.md.meta => Design.meta} (57%) rename Assets/Docs/{ => Design}/Chat_System_Design_v2.md (100%) rename Assets/Docs/{ => Design}/Chat_System_Design_v2.md.meta (100%) rename Assets/Docs/{ => Design}/Component_Analysis_Document.md (100%) rename Assets/Docs/{ => Design}/Component_Analysis_Document.md.meta (100%) rename Assets/Docs/{ => Design}/DialogueBubble_Design.md (100%) rename Assets/Docs/{ => Design}/DialogueBubble_Design.md.meta (100%) rename Assets/Docs/{ => Design}/Initial_Loading_System_Design.md (100%) rename Assets/Docs/{ => Design}/Initial_Loading_System_Design.md.meta (100%) rename Assets/Docs/{ => Design}/Initial_Startup_System_Design.md (100%) rename Assets/Docs/{ => Design}/Initial_Startup_System_Design.md.meta (100%) rename Assets/Docs/{ => Design}/Live2D_Service_Design.md (100%) rename Assets/Docs/{ => Design}/Live2D_Service_Design.md.meta (100%) create mode 100644 Assets/Docs/Guides.meta create mode 100644 Assets/Docs/Guides/Manager_System_Guide.md rename Assets/Docs/{ => Guides}/Manager_System_Guide.md.meta (100%) create mode 100644 Assets/Docs/Guides/ProjectVG_Structure_Guide.md rename Assets/Docs/{ => Guides}/ProjectVG_Structure_Guide.md.meta (100%) create mode 100644 Assets/Docs/Guides/Unity_Naming_Conventions.md rename Assets/Docs/{ => Guides}/Unity_Naming_Conventions.md.meta (100%) delete mode 100644 Assets/Docs/Manager_System_Guide.md delete mode 100644 Assets/Docs/New_Manager_Checklist.md delete mode 100644 Assets/Docs/ProjectVG_Structure_Guide.md delete mode 100644 Assets/Docs/Unity_Naming_Conventions.md diff --git a/Assets/Docs/New_Manager_Checklist.md.meta b/Assets/Docs/Design.meta similarity index 57% rename from Assets/Docs/New_Manager_Checklist.md.meta rename to Assets/Docs/Design.meta index c297355..995e092 100644 --- a/Assets/Docs/New_Manager_Checklist.md.meta +++ b/Assets/Docs/Design.meta @@ -1,6 +1,7 @@ fileFormatVersion: 2 -guid: 7f33f526f09614c49839275f4953bd57 -TextScriptImporter: +guid: fe2e2a2211f3e084a82545a6c3395cb6 +folderAsset: yes +DefaultImporter: externalObjects: {} userData: assetBundleName: diff --git a/Assets/Docs/Chat_System_Design_v2.md b/Assets/Docs/Design/Chat_System_Design_v2.md similarity index 100% rename from Assets/Docs/Chat_System_Design_v2.md rename to Assets/Docs/Design/Chat_System_Design_v2.md diff --git a/Assets/Docs/Chat_System_Design_v2.md.meta b/Assets/Docs/Design/Chat_System_Design_v2.md.meta similarity index 100% rename from Assets/Docs/Chat_System_Design_v2.md.meta rename to Assets/Docs/Design/Chat_System_Design_v2.md.meta diff --git a/Assets/Docs/Component_Analysis_Document.md b/Assets/Docs/Design/Component_Analysis_Document.md similarity index 100% rename from Assets/Docs/Component_Analysis_Document.md rename to Assets/Docs/Design/Component_Analysis_Document.md diff --git a/Assets/Docs/Component_Analysis_Document.md.meta b/Assets/Docs/Design/Component_Analysis_Document.md.meta similarity index 100% rename from Assets/Docs/Component_Analysis_Document.md.meta rename to Assets/Docs/Design/Component_Analysis_Document.md.meta diff --git a/Assets/Docs/DialogueBubble_Design.md b/Assets/Docs/Design/DialogueBubble_Design.md similarity index 100% rename from Assets/Docs/DialogueBubble_Design.md rename to Assets/Docs/Design/DialogueBubble_Design.md diff --git a/Assets/Docs/DialogueBubble_Design.md.meta b/Assets/Docs/Design/DialogueBubble_Design.md.meta similarity index 100% rename from Assets/Docs/DialogueBubble_Design.md.meta rename to Assets/Docs/Design/DialogueBubble_Design.md.meta diff --git a/Assets/Docs/Initial_Loading_System_Design.md b/Assets/Docs/Design/Initial_Loading_System_Design.md similarity index 100% rename from Assets/Docs/Initial_Loading_System_Design.md rename to Assets/Docs/Design/Initial_Loading_System_Design.md diff --git a/Assets/Docs/Initial_Loading_System_Design.md.meta b/Assets/Docs/Design/Initial_Loading_System_Design.md.meta similarity index 100% rename from Assets/Docs/Initial_Loading_System_Design.md.meta rename to Assets/Docs/Design/Initial_Loading_System_Design.md.meta diff --git a/Assets/Docs/Initial_Startup_System_Design.md b/Assets/Docs/Design/Initial_Startup_System_Design.md similarity index 100% rename from Assets/Docs/Initial_Startup_System_Design.md rename to Assets/Docs/Design/Initial_Startup_System_Design.md diff --git a/Assets/Docs/Initial_Startup_System_Design.md.meta b/Assets/Docs/Design/Initial_Startup_System_Design.md.meta similarity index 100% rename from Assets/Docs/Initial_Startup_System_Design.md.meta rename to Assets/Docs/Design/Initial_Startup_System_Design.md.meta diff --git a/Assets/Docs/Live2D_Service_Design.md b/Assets/Docs/Design/Live2D_Service_Design.md similarity index 100% rename from Assets/Docs/Live2D_Service_Design.md rename to Assets/Docs/Design/Live2D_Service_Design.md diff --git a/Assets/Docs/Live2D_Service_Design.md.meta b/Assets/Docs/Design/Live2D_Service_Design.md.meta similarity index 100% rename from Assets/Docs/Live2D_Service_Design.md.meta rename to Assets/Docs/Design/Live2D_Service_Design.md.meta diff --git a/Assets/Docs/Guides.meta b/Assets/Docs/Guides.meta new file mode 100644 index 0000000..f6f479a --- /dev/null +++ b/Assets/Docs/Guides.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: d86ffccbebb5ad441aea196e0d5d3199 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Docs/Guides/Manager_System_Guide.md b/Assets/Docs/Guides/Manager_System_Guide.md new file mode 100644 index 0000000..6f1be25 --- /dev/null +++ b/Assets/Docs/Guides/Manager_System_Guide.md @@ -0,0 +1,110 @@ +# 매니저 시스템 가이드 (소규모 팀용) + +간단/소규모 기준: 인스펙터 연결 + Initialize 패턴 + +## 규칙(요약) +| 항목 | 규칙 | 예시 | +|---|---|---| +| 의존성 연결 | SerializedField 인스펙터 참조 | [SerializeField] private AudioManager _audio; | +| 동적 생성 | 비활성 Instantiate → 값 세팅 → Initialize → 활성화 | go.SetActive(false); comp.Initialize(...); go.SetActive(true); | +| 수명주기 | Awake/OnEnable에서 외부 의존 사용 금지, Initialize 이후 사용 | Initialize 호출 순서 보장 | +| 구성 루트 | Bootstrapper 1개, DontDestroyOnLoad | GameBootstrapper | +| 설정 자산 | ScriptableObject 참조 | AppEnvironmentConfig | + +## 패턴 1) 정적 의존성(인스펙터 연결) +```csharp +using UnityEngine; + +public sealed class GameBootstrapper : MonoBehaviour +{ + [SerializeField] private AudioManager _audioManager; + [SerializeField] private UIManager _uiManager; + + private void Awake() + { + _audioManager.Initialize(); + _uiManager.Initialize(); + } +} + +public sealed class UIManager : MonoBehaviour +{ + [SerializeField] private AudioManager _audioManager; + + /// + /// 매니저 초기화 + /// + public void Initialize() { } +} +``` + +포인트 +- SerializedField로 동일 씬/프리팹 내 의존 연결 +- Awake에서 Initialize 순차 호출(간단, 명확) + +## 패턴 2) 동적 생성(Initialize 세팅) +```csharp +using UnityEngine; + +public sealed class EnemyFactory +{ + public Enemy Spawn(Enemy prefab, Vector3 pos, EnemyStats stats) + { + var go = Object.Instantiate(prefab.gameObject, pos, Quaternion.identity); + go.SetActive(false); + var enemy = go.GetComponent(); + enemy.Initialize(stats); + go.SetActive(true); + return enemy; + } +} + +public sealed class Enemy : MonoBehaviour +{ + /// + /// 런타임 파라미터 주입 + /// + public void Initialize(EnemyStats stats) { } +} +``` + +포인트 +- Awake/OnEnable 이전 값 주입 보장 +- 프리팹 바리언트로 기본값, Initialize로 런타임 값만 주입 + +## 패턴 3) 경량 서비스 조회(선택) +```csharp +public static class ServiceLocator +{ + private static readonly Dictionary _map = new(); + public static void Register(T svc) => _map[typeof(T)] = svc; + public static T Get() => (T)_map[typeof(T)]; +} + +public sealed class GameBootstrapper : MonoBehaviour +{ + [SerializeField] private AudioManager _audio; + private void Awake() + { + ServiceLocator.Register(_audio); + _audio.Initialize(); + } +} +``` + +포인트 +- 작은 규모에서만 사용, 과도한 DI 지양 +- 1회 조회 후 캐싱 권장 + +## 실전 체크리스트 +- [ ] 인스펙터 참조 누락 없음(씬/프리팹) +- [ ] Awake에서 Initialize 순서 정의(상위→하위) +- [ ] 동적 객체는 비활성 Instantiate → Initialize 후 활성화 +- [ ] ScriptableObject로 설정/리소스 참조 외부화 +- [ ] FindObjectOfType 매 프레임 호출 금지(초기 1회 캐싱만) + +## 안티 패턴 +- 글로벌 싱글톤 남용(하드 의존) +- Awake에서 외부 매니저 즉시 호출(주입 순서 깨짐) +- Resources 남용(필요 최소만) + diff --git a/Assets/Docs/Manager_System_Guide.md.meta b/Assets/Docs/Guides/Manager_System_Guide.md.meta similarity index 100% rename from Assets/Docs/Manager_System_Guide.md.meta rename to Assets/Docs/Guides/Manager_System_Guide.md.meta diff --git a/Assets/Docs/Guides/ProjectVG_Structure_Guide.md b/Assets/Docs/Guides/ProjectVG_Structure_Guide.md new file mode 100644 index 0000000..d1285e4 --- /dev/null +++ b/Assets/Docs/Guides/ProjectVG_Structure_Guide.md @@ -0,0 +1,169 @@ + +# ProjectVG Unity 프로젝트 구조 가이드 + +```text +Assets/ +├─ App/ - 앱 엔트리, 전역 부트스트랩 +│ └─ Scenes/ - 앱/메인/로딩 씬 +├─ Core/ - 공통 런타임 모듈 +│ ├─ Input/ - 입력 매니저/라우팅(ScreenTapManager 등) +│ ├─ Audio/ - 오디오 매니저/레코더/보이스 +│ ├─ Loading/ - 로딩/씬 전환 유틸 +│ ├─ Managers/ - 초기화/DI/레지스트리(System/Dependency) +│ └─ Utils/ - 범용 유틸/싱글톤 +├─ Domain/ - 기능(도메인)별 코드/자산 +│ ├─ Character/ +│ │ ├─ Model/ - Live2D 자산(.moc3/.json/Prefab/Motions/Expressions) +│ │ ├─ View/ - 캐릭터 프리팹/뷰 스크립트 +│ │ ├─ Script/ - 컨트롤러/매니저/컴포넌트/Config SO +│ │ └─ Animation/ - 타임라인/모션 클립 +│ └─ Chat/ +│ ├─ Script/ - 채팅 로직/서비스 연동 +│ ├─ View/ - UI 프리팹/스크립트 +│ └─ Model/ - 데이터 모델/DTO +├─ Infrastructure/ - 외부 연동/저장/네트워크 +│ ├─ Network/ - Http/WS 클라이언트, Services, DTOs, Configs +│ ├─ Data/ - 로컬 저장 래퍼(SaveData/PlayerPrefs) +│ ├─ Bridge/ - 네이티브/외부 SDK 브리지 +│ └─ Config/ - 실행환경 설정 SO(AppEnvironmentConfig 등) +├─ UI/ - 공용 UI 리소스 +│ ├─ Panels/ - 화면 단위 패널 프리팹 +│ ├─ Prefabs/ - 공용 UI 프리팹 +│ ├─ Scripts/ - UI 상호작용 스크립트 +│ ├─ Transitions/ - 전환 효과(페이드/이동 등) +│ └─ Fonts/ - 폰트/머티리얼 +├─ Resources/ - Resources.Load 대상(필요 최소) +│ ├─ App/ - 앱 공용 리소스 +│ └─ Character/ - 레지스트리/모델 등록 SO +├─ Settings/ - 프로젝트 설정 자산 +│ ├─ Rendering/ +│ │ └─ URP/ - URP 에셋/글로벌 설정/Renderer2D +│ ├─ InputSystem/ - .inputactions +│ └─ Adaptive Performance/ - 프로바이더/프로필 자산 +├─ Scenes/ - 공용/샘플 씬 +├─ Plugins/ - 외부 SDK/라이브러리 +│ ├─ Live2D/ - Cubism SDK +│ └─ TextMesh Pro/ - TMP 패키지 리소스 +├─ Editor/ - 에디터 전용 스크립트 +├─ Tests/ - 테스트 +│ ├─ Editor/ - 에디터 테스트 +│ └─ Runtime/ - 플레이모드/유닛 테스트 +├─ Docs/ - 문서 +│ ├─ Design/ - 설계/다이어그램 +│ └─ Guides/ - 가이드/규칙 +└─ Samples/ - 샘플 코드/리소스 + └─ Core/ + └─ Managers/ - 샘플 매니저 스크립트 +``` + +```mermaid +graph TD + A[Assets] --> APP[App] + A --> CORE[Core] + A --> DOMAIN[Domain] + A --> INFRA[Infrastructure] + A --> UI[UI] + A --> RES[Resources] + A --> SETT[Settings] + A --> SCENES[Scenes] + A --> PLUG[Plugins] + A --> EDITOR[Editor] + A --> TESTS[Tests] + A --> DOCS[Docs] + A --> SAMPLES[Samples] + + CORE --> CORE_Input[Input] + CORE --> CORE_Audio[Audio] + CORE --> CORE_Loading[Loading] + CORE --> CORE_Managers[Managers] + CORE --> CORE_Utils[Utils] + + DOMAIN --> D_Character[Character] + D_Character --> D_Model[Model] + D_Character --> D_View[View] + D_Character --> D_Script[Script] + D_Character --> D_Anim[Animation] + + INFRA --> Nw[Network] + INFRA --> Data[Data] + INFRA --> Bridge[Bridge] + INFRA --> Cfg[Config] + + SETT --> URP[Rendering/URP] + SETT --> InputSys[InputSystem] + SETT --> AP[AdaptivePerformance] + + DOCS --> Design[Design] + DOCS --> Guides[Guides] +``` + +## 최상위 디렉토리 +| 경로 | 용도 | 예시 | +|---|---|---| +| Assets/App | 엔트리/앱 전역 흐름 | App.cs, App/Scenes | +| Assets/Core | 공통 기능(엔진 독립) | Audio, Input, Loading, Managers, Utils | +| Assets/Domain | 기능(도메인)별 코드/자산 | Character, Chat, Popup | +| Assets/Infrastructure | 외부 연동/저장/네트워크 | Network, Data, Bridge, Config | +| Assets/UI | 공용 UI 리소스 | Panels, Prefabs, Scripts, Transitions, Fonts | +| Assets/Resources | 런타임 Resources.Load 대상 | Config assets, Registries | +| Assets/Settings | 프로젝트 설정 자산 | Rendering/URP, InputSystem, AdaptivePerformance | +| Assets/Scenes | 공용 씬(샘플/공통) | SampleScene, DevScene | +| Assets/Plugins | 외부 SDK/라이브러리 | Live2D, TextMesh Pro | +| Assets/Editor | 에디터 전용 코드 | Inspectors, MenuItems | +| Assets/Tests | 테스트 | Editor, Runtime | +| Assets/Docs | 문서 | Design, Guides, diagrams | +| Assets/Samples | 샘플 코드/리소스 | Core/Managers/SampleSystemManager.cs | + +## Core 하위 +| 경로 | 용도 | 비고 | +|---|---|---| +| Core/Input | 입력 처리 | ScreenTapManager | +| Core/Audio | 오디오 공통 | AudioManager, VoiceManager | +| Core/Loading | 로딩/씬 전환 | LoadingManager, SceneTransitionManager | +| Core/Managers | 초기화/DI/레지스트리 | InitializationManager, DependencyManager | +| Core/Utils | 범용 유틸 | Singleton 등 | + +## Domain 하위(패턴) +| 경로 | 용도 | 예시 | +|---|---|---| +| Domain//Model | 모델 자산(겉모습) | Live2D .moc3/.json, Prefab, Motions | +| Domain//View | 뷰/프리팹 | UI/3D 프리팹 | +| Domain//Script | 로직/컨트롤러 | Manager, Controller, Component, Config | +| Domain//Animation | 타임라인/클립 | Playables, AnimClips | + +## Infrastructure 하위 +| 경로 | 용도 | 예시 | +|---|---|---| +| Infrastructure/Network | API/WS/HTTP | Services, DTOs, Configs | +| Infrastructure/Data | 로컬 저장 | SaveData, PlayerPrefs 래퍼 | +| Infrastructure/Bridge | 네이티브/외부 SDK 브리지 | Android/iOS glue | +| Infrastructure/Config | 실행환경 설정 | AppEnvironmentConfig | + +## Settings vs Plugins +| 항목 | 내용 | +|---|---| +| Settings | 프로젝트 설정 자산(.asset, 프로필). 예: URP, InputSystem, Adaptive Performance | +| Plugins | 외부 기능 코드/리소스. 예: Live2D Cubism, TMP | + +## 배치 규칙 +- 스크립트: 기능 기준 배치(Domain/Core/Infrastructure/UI) +- 모델 자산: Domain//Model// +- 설정 자산: Assets/Settings/** +- Resources: 런타임 강제 로드 대상만 배치(남용 금지) +- Addressables: 도입 시 Resources 대체, 키 규칙 Domain/Category/Name +- 샘플: Assets/Samples/** + +## 씬 규칙 +- 공용/샘플 씬: Assets/Scenes/** +- 기능 전용 씬: Domain//View 또는 Tests/Runtime/** +- 네이밍: PascalCase + 역할(Main/Loading/Dev/Sample) + +## Addressables 키 규칙 +- Domain/Category/Name +- 예: UI/Panels/PanelChat, Characters/Natori/Model + +## 체크리스트 +- 폴더/네임스페이스/파일명/타입명 일치 +- Settings 자산 중앙화(URP/InputSystem/AdaptivePerformance) +- Model 자산 폴더 표준화(Character/Model/) +- 샘플/테스트 자산 분리(Samples, Tests) diff --git a/Assets/Docs/ProjectVG_Structure_Guide.md.meta b/Assets/Docs/Guides/ProjectVG_Structure_Guide.md.meta similarity index 100% rename from Assets/Docs/ProjectVG_Structure_Guide.md.meta rename to Assets/Docs/Guides/ProjectVG_Structure_Guide.md.meta diff --git a/Assets/Docs/Guides/Unity_Naming_Conventions.md b/Assets/Docs/Guides/Unity_Naming_Conventions.md new file mode 100644 index 0000000..f3f1d91 --- /dev/null +++ b/Assets/Docs/Guides/Unity_Naming_Conventions.md @@ -0,0 +1,62 @@ +# Unity/C# 네이밍 컨벤션 (ProjectVG) + +## C# 식별자 +| 대상 | 규칙 | 예시 | +|---|---|---| +| 클래스/구조체/열거형/속성/메서드 | PascalCase | ChatManager, MessageType, LoadConfig | +| 인터페이스 | IPascalCase | IChatService | +| 상수(const) | UPPER_SNAKE_CASE | DEFAULT_TIMEOUT_MS | +| 비공개 필드 | _camelCase | _sessionId | +| 직렬화 비공개 필드 | _camelCase | _gain | +| 매개변수/지역변수 | camelCase | messageText | +| 제네릭 매개변수 | TName | TItem, TResponse | + +## C# 패턴 +| 항목 | 규칙 | 예시 | +|---|---|---| +| 불리언 | Is/Has/Can/Should 접두사 | IsConnected, HasData | +| 이벤트 이름 | OnXxx / XxxChanged | OnMessageReceived, VolumeChanged | +| 비동기 메서드 | Async 접미사, ct 매개변수 | LoadAsync(CancellationToken ct) | +| 네임스페이스 | 폴더 구조 반영, 루트 ProjectVG | ProjectVG.Domain.Chat | +| 파일명 | 공개 루트 타입명과 동일 | ChatManager.cs | + +## Unity 클래스 +| 범주 | 접미/접두 | 예시 | 용도 | +|---|---|---|---| +| MonoBehaviour | Controller | PlayerController | 기능 제어 | +| | Manager | AudioManager | 전역 수명/상태 | +| | Service | ChatService | 외부/비즈 로직 | +| | View/Presenter | ChatView | UI 표시/중개 | +| | Bootstrapper/Installer | GameBootstrapper | 초기화/조립 | +| ScriptableObject | Config/Settings/Profile/Definition/Registry/Database | Live2DModelConfig | 데이터/설정 | +| 기타 | Handler/Factory/Pool | WebSocketHandler | 책임 명확화 | + +공개 API: 명령형 동사 사용(Initialize/Apply/Load 등) + +## Unity 자산 +| 타입 | 규칙 | 예시 | +|---|---|---| +| 씬 | PascalCase, 역할 접미 허용(Main/Boot/Loading/Sample) | MainScene, LoadingScene | +| 프리팹 | PascalCase, UI 루트 접두사 Panel/Dialog/HUD | PanelChat, DialogConfirm | +| SO 에셋 | 타입명 기반 + 키 | NetworkConfig_Prod | +| Addressables | Domain/Category/Name | UI/Panels/PanelChat | +| 폴더 | PascalCase 단수형 | Domain/Character/Model | + +## UI 위젯 +| 위치 | 규칙 | 예시 | +|---|---|---| +| Hierarchy 이름 | 접두사 사용 Panel/Btn/Img/Txt/Input/Scroll/Toggle/Slider/Dropdown | PanelChat, BtnSend | +| 코드 필드명 | 의미 중심 camelCase | sendButton, titleText | + +## 예시(요약) +- MonoBehaviour: ScreenTapManager, Live2DModelManagerFacade +- ScriptableObject: Live2DModelConfig, AppEnvironmentConfig +- Prefab: PanelChat, ChatBubbleUI, AudioInputView +- Scene: MainScene, Live2DScene + +## 금지/주의 +- 범용어 남용: Data/Util/Helper/Manager(무의미 사용) +- 약어 조합: Cfg/Ctrllr/Svc +- 파일명 ≠ 타입명 불일치 +- 부정형 이벤트/플래그: NotReady → 긍정형 ShouldWait/IsReady + diff --git a/Assets/Docs/Unity_Naming_Conventions.md.meta b/Assets/Docs/Guides/Unity_Naming_Conventions.md.meta similarity index 100% rename from Assets/Docs/Unity_Naming_Conventions.md.meta rename to Assets/Docs/Guides/Unity_Naming_Conventions.md.meta diff --git a/Assets/Docs/Manager_System_Guide.md b/Assets/Docs/Manager_System_Guide.md deleted file mode 100644 index deab96a..0000000 --- a/Assets/Docs/Manager_System_Guide.md +++ /dev/null @@ -1,305 +0,0 @@ -# 매니저 시스템 사용 가이드 - -## 개요 - -리팩토링된 매니저 시스템은 **단일 책임 원칙**에 따라 GameManager의 복잡성을 분산시켜 관리하기 쉽게 만들었습니다. - -## 🏗️ 시스템 구조 - -``` -GameManager (퍼사드/조율자) -├── InitializationManager (초기화 전담) -├── ManagerRegistry (매니저 생성/관리) -└── DependencyManager (의존성 주입) -``` - -### 각 매니저의 역할 - -| 매니저 | 책임 | 주요 기능 | -|--------|------|-----------| -| **GameManager** | 전체 조율, 퍼사드 | 간단한 인터페이스 제공, 이벤트 중계 | -| **InitializationManager** | 초기화 과정 관리 | 단계별 초기화, 진행률 추적, 이벤트 발생 | -| **ManagerRegistry** | 매니저 생명주기 관리 | 매니저 생성/등록/해제, 상태 조회 | -| **DependencyManager** | 의존성 주입 | 서비스 등록, 의존성 해결 | - -## 📖 기본 사용법 - -### 1. 기존 코드 - 변경 없음! - -```csharp -// 여전히 이렇게 사용 가능 -GameManager.Instance.InitializeGameAsync(); -GameManager.Instance.OnGameInitialized += OnGameReady; - -// 매니저 접근도 동일 -var webSocket = GameManager.Instance.WebSocketManager; -var session = GameManager.Instance.SessionManager; -``` - -### 2. 새로운 기능들 - -```csharp -// 상세한 초기화 상태 조회 -var status = GameManager.Instance.GetInitializationStatus(); -Debug.Log($"현재 단계: {status.CurrentPhase}"); -Debug.Log($"진행률: {status.Progress * 100}%"); - -// 매니저 준비 상태 확인 -if (GameManager.Instance.AreManagersReady()) -{ - // 모든 매니저가 준비됨 -} -``` - -## 🔧 새로운 매니저 추가하기 - -### Step 1: 매니저 클래스 생성 - -```csharp -using UnityEngine; -using ProjectVG.Core.Managers; - -public class AudioManager : MonoBehaviour, IManager -{ - [Header("Audio Settings")] - [SerializeField] private float _masterVolume = 1.0f; - - public float MasterVolume => _masterVolume; - - public void Initialize() - { - Debug.Log("[AudioManager] 초기화 완료"); - } - - public void Shutdown() - { - Debug.Log("[AudioManager] 종료 처리"); - } - - public void PlaySound(string soundName) - { - // 사운드 재생 로직 - } -} -``` - -### Step 2: ManagerRegistry에 추가 - -```csharp -// ManagerRegistry.cs에 추가 -[Header("Manager References")] -[SerializeField] private AudioManager _audioManager; // 👈 추가 - -public AudioManager AudioManager => _audioManager; // 👈 추가 - -public void InitializeAllManagers() -{ - InitializeWebSocketManager(); - InitializeSessionManager(); - InitializeHttpApiClient(); - InitializeAudioManager(); // 👈 추가 -} - -private void InitializeAudioManager() // 👈 새 메서드 -{ - if (_audioManager == null && _createManagersIfNotExist) - { - var audioObj = new GameObject("AudioManager"); - audioObj.transform.SetParent(transform); - _audioManager = audioObj.AddComponent(); - } - - if (_audioManager != null) - { - _managers.Add(_audioManager); - _audioManager.Initialize(); - Debug.Log("[ManagerRegistry] AudioManager 초기화 완료"); - } -} -``` - -### Step 3: GameManager에 접근자 추가 - -```csharp -// GameManager.cs에 추가 -public AudioManager AudioManager => _managerRegistry?.AudioManager; -``` - -### Step 4: (선택사항) 의존성 주입 설정 - -```csharp -// DependencyManager.cs에 추가 (필요한 경우) -private void RegisterServices(ManagerRegistry managerRegistry) -{ - // 기존 코드... - - if (managerRegistry.AudioManager != null) - { - _container.Register(managerRegistry.AudioManager); - Debug.Log("[DependencyManager] AudioManager 등록 완료"); - } -} -``` - -## 🎯 실제 사용 예시 - -### 새로운 AudioManager 사용 - -```csharp -public class GameController : MonoBehaviour -{ - private void Start() - { - // GameManager 초기화 완료 대기 - GameManager.Instance.OnGameInitialized += OnGameReady; - } - - private void OnGameReady() - { - // AudioManager 사용 - var audioManager = GameManager.Instance.AudioManager; - audioManager?.PlaySound("background_music"); - - Debug.Log($"Audio Volume: {audioManager.MasterVolume}"); - } -} -``` - -## 🚀 고급 사용법 - -### 1. 커스텀 초기화 단계 추가 - -새로운 매니저가 특별한 초기화 과정이 필요한 경우: - -```csharp -// InitializationPhase enum에 추가 -public enum InitializationPhase -{ - NotStarted, - InitializingManagers, - ConnectingToServer, - LoadingResources, - InitializingAudio, // 👈 새 단계 추가 - Completed -} - -// InitializationManager.cs에서 단계 추가 -private async UniTask InitializeAsync() -{ - // 기존 단계들... - - SetPhase(InitializationPhase.InitializingAudio); - await InitializeAudioAsync(); // 새 단계 - UpdateProgress(0.9f); - - SetPhase(InitializationPhase.Completed); - // ... -} -``` - -### 2. 매니저간 의존성 처리 - -```csharp -public class UIManager : MonoBehaviour, IManager -{ - [Inject] private AudioManager _audioManager; // 의존성 주입 - - public void ShowMenu() - { - _audioManager?.PlaySound("menu_open"); - // UI 표시 로직 - } -} -``` - -## 📋 체크리스트 - -새로운 매니저를 추가할 때 확인하세요: - -- [ ] `IManager` 인터페이스 구현 -- [ ] `ManagerRegistry`에 참조 추가 -- [ ] `ManagerRegistry.InitializeAllManagers()`에 초기화 메서드 추가 -- [ ] `GameManager`에 접근자 추가 -- [ ] (필요시) `DependencyManager`에 의존성 등록 -- [ ] (필요시) 새로운 초기화 단계 추가 -- [ ] 매니저 상태 로깅에 추가 - -## 🔍 디버깅 팁 - -### 1. 매니저 상태 확인 - -```csharp -// Inspector에서 또는 코드에서 호출 -GameManager.Instance.LogManagerStatus(); -``` - -### 2. 초기화 진행 상황 모니터링 - -```csharp -GameManager.Instance.OnPhaseChanged += (phase) => { - Debug.Log($"초기화 단계: {phase}"); -}; - -GameManager.Instance.OnProgressChanged += (progress) => { - Debug.Log($"진행률: {progress * 100:F1}%"); -}; -``` - -### 3. 일반적인 문제들 - -| 문제 | 원인 | 해결책 | -|------|------|--------| -| "Manager가 null" | 초기화 전에 접근 | `OnGameInitialized` 이벤트 대기 | -| "의존성 주입 실패" | 서비스 미등록 | `DependencyManager`에 등록 확인 | -| "초기화 실패" | 매니저 생성 실패 | `_createManagersIfNotExist` 설정 확인 | - -## 🎨 베스트 프랙티스 - -### 1. 매니저 설계 원칙 - -✅ **해야 할 것** -- 단일 책임 원칙 준수 -- `IManager` 인터페이스 구현 -- 명확한 Initialize/Shutdown 구현 -- 의존성 주입 활용 - -❌ **하지 말아야 할 것** -- 다른 매니저에 직접 의존 -- GameManager에 복잡한 로직 추가 -- Singleton 남용 - -### 2. 네이밍 컨벤션 - -```csharp -// 좋은 예 -AudioManager, NetworkManager, UIManager - -// 나쁜 예 -Manager, SoundSystem, AudioController -``` - -### 3. 이벤트 사용 - -```csharp -// 매니저에서 이벤트 발생 -public class AudioManager : MonoBehaviour, IManager -{ - public event Action OnSoundPlayed; - - public void PlaySound(string soundName) - { - // 재생 로직 - OnSoundPlayed?.Invoke(soundName); - } -} -``` - -## 📚 추가 자료 - -- [Unity 싱글톤 패턴 가이드](./Unity_Singleton_Guide.md) -- [의존성 주입 사용법](./Dependency_Injection_Guide.md) -- [이벤트 시스템 가이드](./Event_System_Guide.md) - ---- - -> 💡 **팁**: 새로운 매니저를 추가하기 전에 기존 매니저로 해결할 수 있는지 먼저 검토해보세요. 너무 많은 매니저는 오히려 복잡성을 증가시킬 수 있습니다. diff --git a/Assets/Docs/New_Manager_Checklist.md b/Assets/Docs/New_Manager_Checklist.md deleted file mode 100644 index 4c75460..0000000 --- a/Assets/Docs/New_Manager_Checklist.md +++ /dev/null @@ -1,198 +0,0 @@ -# 새로운 매니저 추가 체크리스트 - -## 📋 필수 체크리스트 - -새로운 매니저를 추가할 때 다음 항목들을 순서대로 확인하세요: - -### ✅ 1. 매니저 클래스 생성 - -- [ ] `IManager` 인터페이스 구현 -- [ ] `Initialize()` 메서드 구현 -- [ ] `Shutdown()` 메서드 구현 -- [ ] 필요한 이벤트 정의 -- [ ] 적절한 네임스페이스 사용 (`ProjectVG.Core.Managers`) - -```csharp -public class YourManager : MonoBehaviour, IManager -{ - public void Initialize() { /* 구현 */ } - public void Shutdown() { /* 구현 */ } -} -``` - -### ✅ 2. ManagerRegistry 수정 - -- [ ] Header에 SerializeField 변수 추가 -- [ ] Public 프로퍼티 추가 -- [ ] `InitializeAllManagers()`에 초기화 메서드 호출 추가 -- [ ] Private 초기화 메서드 구현 - -```csharp -[SerializeField] private YourManager _yourManager; -public YourManager YourManager => _yourManager; - -public void InitializeAllManagers() -{ - // 기존 초기화들... - InitializeYourManager(); // 추가 -} - -private void InitializeYourManager() { /* 구현 */ } -``` - -### ✅ 3. GameManager 수정 - -- [ ] Public 접근자 프로퍼티 추가 - -```csharp -public YourManager YourManager => _managerRegistry?.YourManager; -``` - -### ✅ 4. (선택사항) 의존성 주입 설정 - -- [ ] `DependencyManager.RegisterServices()`에 등록 추가 -- [ ] `DependencyManager.InjectDependencies()`에 주입 추가 (필요시) - -### ✅ 5. 테스트 및 검증 - -- [ ] 컴파일 에러 없음 확인 -- [ ] 런타임에서 매니저 생성 확인 -- [ ] 초기화 로그 확인 -- [ ] `GameManager.Instance.LogManagerStatus()` 실행하여 상태 확인 - -## 🚀 고급 옵션 체크리스트 - -### ✅ 커스텀 초기화 단계 (필요시) - -- [ ] `InitializationPhase` enum에 새 단계 추가 -- [ ] `InitializationManager.InitializeAsync()`에 새 단계 추가 -- [ ] 진행률 계산 업데이트 - -### ✅ 매니저간 의존성 (필요시) - -- [ ] `[Inject]` 어트리뷰트 사용 -- [ ] `DependencyManager`에서 의존성 등록 -- [ ] 초기화 순서 고려 - -### ✅ 이벤트 시스템 (추천) - -- [ ] 매니저에서 적절한 이벤트 발생 -- [ ] 이벤트 구독/해제 예시 작성 -- [ ] 메모리 누수 방지 (이벤트 해제) - -## 📝 실제 예시: AudioManager - -```csharp -// 1. 매니저 클래스 -public class AudioManager : MonoBehaviour, IManager -{ - public void Initialize() { /* 오디오 시스템 초기화 */ } - public void Shutdown() { /* 리소스 정리 */ } - public void PlaySound(string soundName) { /* 사운드 재생 */ } -} - -// 2. ManagerRegistry 추가 -[SerializeField] private AudioManager _audioManager; -public AudioManager AudioManager => _audioManager; - -// 3. GameManager 접근자 -public AudioManager AudioManager => _managerRegistry?.AudioManager; - -// 4. 사용 예시 -GameManager.Instance.AudioManager?.PlaySound("button_click"); -``` - -## 🐛 일반적인 실수들 - -### ❌ 하지 말아야 할 것들 - -- **GameManager에 복잡한 로직 추가** - ```csharp - // 나쁜 예 - public void PlaySound(string name) { /* 복잡한 로직 */ } - - // 좋은 예 - public AudioManager AudioManager => _managerRegistry?.AudioManager; - ``` - -- **다른 매니저에 직접 의존** - ```csharp - // 나쁜 예 - public class UIManager : MonoBehaviour - { - private AudioManager _audioManager; // 직접 참조 - } - - // 좋은 예 - public class UIManager : MonoBehaviour - { - [Inject] private AudioManager _audioManager; // 의존성 주입 - } - ``` - -- **초기화 순서 무시** - ```csharp - // 나쁜 예 - void Start() - { - GameManager.Instance.AudioManager.PlaySound("start"); // 초기화 전 호출 - } - - // 좋은 예 - void Start() - { - GameManager.Instance.OnGameInitialized += () => { - GameManager.Instance.AudioManager.PlaySound("start"); - }; - } - ``` - -## 🔍 디버깅 가이드 - -### 매니저가 null인 경우 - -1. **초기화 전에 접근했는지 확인** - ```csharp - GameManager.Instance.OnGameInitialized += () => { - // 여기서 매니저 사용 - }; - ``` - -2. **ManagerRegistry에 제대로 등록했는지 확인** - ```csharp - // Inspector에서 매니저 참조가 설정되었는지 확인 - ``` - -3. **초기화 메서드가 호출되는지 확인** - ```csharp - // 로그를 통해 InitializeYourManager()가 호출되는지 확인 - ``` - -### 의존성 주입이 안 되는 경우 - -1. **서비스가 등록되었는지 확인** - ```csharp - // DependencyManager.RegisterServices()에서 등록 확인 - ``` - -2. **InjectDependencies가 호출되었는지 확인** - ```csharp - // DependencyManager.InjectDependencies()에서 주입 확인 - ``` - -## 📊 성능 고려사항 - -- **매니저 수 제한**: 너무 많은 매니저는 복잡성 증가 -- **초기화 시간**: 복잡한 초기화는 LoadingResources 단계에서 -- **메모리 사용량**: 불필요한 리소스 로딩 방지 -- **이벤트 정리**: OnDestroy에서 이벤트 구독 해제 - -## 📚 참고 자료 - -- [Manager System Guide](./Manager_System_Guide.md) -- [Unity Singleton Pattern](./Unity_Singleton_Guide.md) -- [Dependency Injection Guide](./Dependency_Injection_Guide.md) - ---- - -> 💡 **꿀팁**: 새로운 매니저를 추가하기 전에 기존 매니저로 해결할 수 있는지 검토해보세요. AudioManager에 UI 사운드 기능을 추가하는 것이 UISoundManager를 새로 만드는 것보다 나을 수 있습니다. diff --git a/Assets/Docs/ProjectVG_Structure_Guide.md b/Assets/Docs/ProjectVG_Structure_Guide.md deleted file mode 100644 index a9473e4..0000000 --- a/Assets/Docs/ProjectVG_Structure_Guide.md +++ /dev/null @@ -1,100 +0,0 @@ - -# 📁 ProjectVG Unity 클라이언트 구조 가이드 - -이 문서는 `Assets/` 디렉토리 기준의 Unity 클라이언트 프로젝트 구조 및 디렉토리 용도/명명 규칙을 설명합니다. - ---- - -## 🗂 전체 구조 - -``` -Assets/ -├── App/ # 진입점, 전체 앱 설정 -│ ├── Scenes/ # 메인 씬, 로딩 씬 등 -│ └── App.cs # 앱 초기화/엔트리 진입 - -├── Core/ # 전역 공통 기능 -│ ├── Input/ # 입력 처리 (Touch, Mouse, Key 등) -│ ├── Audio/ # BGM, SFX, 오디오 매니저 -│ ├── Localization/ # 다국어 지원 -│ ├── Time/ # 시간 유틸 (타이머, 딜레이 등) -│ ├── Extensions/ # Unity, LINQ, System 확장 메서드 -│ └── Utils/ # 범용 유틸리티 클래스 - -├── Infrastructure/ # 외부 서비스, 저장소, 네트워크 -│ ├── Data/ # 로컬/클라우드 저장소 (SaveData, PlayerPrefs) -│ ├── Network/ # 서버 API 호출, Response DTO -│ └── Bridge/ # Native, 외부 SDK 연동 (예: Android, iOS 기능) - -├── Domain/ # 도메인(기능)별 묶음 -│ ├── Character/ -│ │ ├── Model/ # Live2D 모델 파일들 (.moc3, .json 등) -│ │ ├── View/ # 캐릭터 Prefab, UI 뷰 -│ │ ├── Script/ # 제어 스크립트 (Controller, Motion) -│ │ └── Animation/ # 타임라인, 모션 시퀀스 관리 -│ ├── Chat/ -│ │ ├── Script/ -│ │ ├── View/ -│ │ └── Model/ -│ ├── Popup/ -│ │ └── System/ # 팝업 매니저, 팝업 큐 -│ │ └── Instances/ # 실제 팝업 프리팹들 -│ └── System/ # 게임 로직 컨트롤러, FSM 등 (선택) - -├── UI/ # UI 공통 요소 -│ ├── Prefabs/ # 공용 UI 프리팹 (버튼, 아이콘, 툴팁 등) -│ ├── Panels/ # HUD, 메인패널 등 -│ ├── Scripts/ # UI 상호작용 스크립트 -│ ├── Transitions/ # UI 전환 효과 (페이드, 이동 등) -│ └── Fonts/ # UI 폰트 - -├── Resources/ # Resources.Load() 로드 대상 -│ ├── Configs/ # Json 기반 설정파일 -│ └── AddressablesDummy/ # Addressable 사용 안 할 때 대체 - -├── Addressables/ # 어드레서블 관리용 분리 리소스 (선택) -│ ├── UI/ -│ ├── Characters/ -│ └── Scenes/ - -├── Plugins/ # 외부 라이브러리/Live2D SDK -│ └── Live2D/ -│ └── CubismSdkForUnity/ - -├── Editor/ # 커스텀 에디터 코드 (.cs만 가능) -│ └── Inspectors/ -│ └── PropertyDrawers/ -│ └── MenuItems/ - -├── Tests/ # 테스트 -│ ├── Editor/ # Editor Test -│ └── Runtime/ # PlayMode Test, 유닛 테스트 - -├── Art/ # 디자인 원본 (PSD, AI 등) - 버전 관리 제외 가능 - -└── Docs/ # 문서 (설계, 흐름도, README 등) -``` - ---- - -## 📌 네이밍 가이드 요약 - -- 디렉토리 및 클래스명: **PascalCase** -- 변수 및 메서드: **camelCase** -- JSON 및 설정파일: **snake_case** -- Addressable 키: `/` 구분, 예: `UI/PanelChat` -- UI 오브젝트 접두어: `Panel`, `Btn`, `Txt`, `Img`, `Group` 등 - ---- - -## ✅ 활용 예시 - -- `Domain/Character/Model/` → Live2D `.moc3`, `.json`, 모션 설정 -- `Core/Audio/AudioManager.cs` → SFX, BGM 전역 제어 -- `Infrastructure/Network/ApiClient.cs` → REST API 통신 처리 -- `UI/Panels/PanelMain.prefab` → 메인 화면 UI -- `Tests/Runtime/CharacterMotionTest.cs` → 캐릭터 모션 유닛 테스트 - ---- - -> 이 문서는 신규 팀원이 구조를 빠르게 이해하고 규칙대로 작업할 수 있도록 작성된 **구조 및 명명 가이드라인**입니다. diff --git a/Assets/Docs/Unity_Naming_Conventions.md b/Assets/Docs/Unity_Naming_Conventions.md deleted file mode 100644 index 273fb6c..0000000 --- a/Assets/Docs/Unity_Naming_Conventions.md +++ /dev/null @@ -1,236 +0,0 @@ -# Unity/C# 네이밍 컨벤션 가이드 - -이 문서는 ProjectVG Unity 클라이언트의 코드 네이밍 규칙과 컨벤션을 정의합니다. - ---- - -## 기본 C# 컨벤션 - -### 네이밍 스타일 -- **클래스명, 메서드명, 프로퍼티명**: `PascalCase` -- **변수명, 매개변수명**: `camelCase` -- **상수명**: `UPPER_SNAKE_CASE` -- **인터페이스명**: `IPascalCase` (I 접두사) - -### 접근 제한자 규칙 -- **private 필드**: `_camelCase` (언더스코어 접두사) -- **public 필드**: `camelCase` -- **프로퍼티**: `PascalCase` -- **메서드**: `PascalCase` - -### 예시 -```csharp -public class ChatManager -{ - private string _sessionId; // private 필드 - public string characterId; // public 필드 - public string SessionId { get; set; } // 프로퍼티 - - private void InitializeSession() // private 메서드 - { - // 구현 - } - - public void SendMessage(string message) // public 메서드 - { - // 구현 - } -} -``` - ---- - -## Unity 클래스 네이밍 컨벤션 - -| 역할 | 접미어 또는 접두어 | 예시 | 설명 | -|------|-------------------|------|------| -| **MonoBehaviour** | Controller, Manager, Behaviour, System 등 | PlayerController, GameManager, CameraBehaviour | 씬에 붙는 실행 스크립트 | -| **데이터 객체** (Plain C# Class) | Data, Info, Model, Config, State 등 | PlayerData, LevelInfo, GameConfig | 직렬화 또는 로직 없는 순수 데이터 | -| **싱글톤 서비스** | Service, Manager, System | AudioService, InputManager, SaveSystem | 전역 기능 담당 클래스 | -| **인터페이스** | I 접두어 | IMoveable, IDamageable | 일반 C# 인터페이스 명명 규칙 동일 | -| **UI 컴포넌트** | UI, Panel, View, Dialog | MainMenuUI, SettingsPanel, GameOverDialog | UI Prefab용 MonoBehaviour | -| **이벤트/메시지 객체** | Event, Message | GameStartEvent, PlayerDeathMessage | EventBus 또는 Observer 용 메시지 구조체 | -| **스크립터블 오브젝트** | SO, Config, Asset, Definition | WeaponConfig, LevelDefinition | ScriptableObject 파생 클래스 | -| **테스트/디버깅** | Debug, Tester, Sample, Fake | PlayerDebug, SoundTester, FakeEnemyAI | 테스트, 샘플 용도 클래스 | - ---- - -## Unity 특화 컨벤션 - -### 클래스명 접미사 규칙 - -| 역할 | 접미사 | 예시 | 설명 | -|------|--------|------|------| -| 매니저/컨트롤러 | `Manager` | `ChatManager`, `AudioManager` | 전체 시스템 관리 | -| 서비스 | `Service` | `ChatApiService`, `DataService` | 외부 서비스 연동 | -| 컨트롤러 | `Controller` | `PlayerController`, `UIController` | 특정 기능 제어 | -| 핸들러 | `Handler` | `WebSocketHandler`, `EventHandler` | 이벤트/메시지 처리 | -| 팩토리 | `Factory` | `UIFactory`, `ObjectFactory` | 객체 생성 | -| 풀 | `Pool` | `ObjectPool`, `AudioPool` | 객체 재사용 | -| 뷰 | `View` | `ChatView`, `CharacterView` | UI 표시 담당 | -| 모델 | `Model` | `ChatModel`, `UserModel` | 데이터 구조 | -| DTO | `Request`/`Response` | `ChatRequest`, `LoginResponse` | 데이터 전송 객체 | -| 인터페이스 | `I` + 기능명 | `INetworkClient`, `IAudioPlayer` | 인터페이스 | - -### 메서드명 접두사 규칙 - -| 기능 | 접두사 | 예시 | 설명 | -|------|--------|------|------| -| 초기화 | `Initialize` | `InitializeChat()` | 초기 설정 | -| 설정 | `Setup` | `SetupUI()` | 구성 설정 | -| 시작 | `Start` | `StartSession()` | 프로세스 시작 | -| 중지 | `Stop` | `StopAudio()` | 프로세스 중지 | -| 정리 | `Cleanup` | `CleanupResources()` | 리소스 정리 | -| 업데이트 | `Update` | `UpdatePosition()` | 상태 업데이트 | -| 처리 | `Process` | `ProcessMessage()` | 데이터 처리 | -| 검증 | `Validate` | `ValidateInput()` | 입력 검증 | -| 변환 | `Convert` | `ConvertToJson()` | 형식 변환 | -| 로드 | `Load` | `LoadConfig()` | 데이터 로드 | -| 저장 | `Save` | `SaveData()` | 데이터 저장 | -| 전송 | `Send` | `SendMessage()` | 네트워크 전송 | -| 수신 | `Receive` | `ReceiveResponse()` | 네트워크 수신 | - -### 변수명 접두사 규칙 - -| 타입 | 접두사 | 예시 | 설명 | -|------|--------|------|------| -| GameObject | `go` | `goPlayer` | 게임 오브젝트 | -| Transform | `tr` | `trPlayer` | 트랜스폼 | -| Component | `comp` | `compAudio` | 컴포넌트 | -| UI 요소 | `ui` | `uiButton` | UI 오브젝트 | -| Text | `txt` | `txtMessage` | 텍스트 컴포넌트 | -| Image | `img` | `imgAvatar` | 이미지 컴포넌트 | -| Button | `btn` | `btnSend` | 버튼 컴포넌트 | -| InputField | `input` | `inputMessage` | 입력 필드 | -| AudioSource | `audio` | `audioBGM` | 오디오 소스 | -| Camera | `cam` | `camMain` | 카메라 | -| Rigidbody | `rb` | `rbPlayer` | 리지드바디 | -| Collider | `col` | `colPlayer` | 콜라이더 | - -### UI 오브젝트 네이밍 - -| UI 요소 | 접두사 | 예시 | 설명 | -|---------|--------|------|------| -| Panel | `Panel` | `PanelChat`, `PanelMain` | 패널 | -| Button | `Btn` | `BtnSend`, `BtnClose` | 버튼 | -| Text | `Txt` | `TxtMessage`, `TxtTitle` | 텍스트 | -| Image | `Img` | `ImgAvatar`, `ImgBackground` | 이미지 | -| InputField | `Input` | `InputMessage`, `InputName` | 입력 필드 | -| ScrollView | `Scroll` | `ScrollChat`, `ScrollList` | 스크롤 뷰 | -| Toggle | `Toggle` | `ToggleSound`, `ToggleMusic` | 토글 | -| Slider | `Slider` | `SliderVolume`, `SliderProgress` | 슬라이더 | -| Dropdown | `Dropdown` | `DropdownLanguage` | 드롭다운 | - ---- - -## 프로젝트별 특화 규칙 - -### Domain 클래스 네이밍 -``` -Domain/Character/ -├── Script/ -│ ├── CharacterController.cs # 캐릭터 제어 -│ ├── CharacterAnimation.cs # 애니메이션 관리 -│ └── CharacterState.cs # 상태 관리 -├── View/ -│ ├── CharacterView.cs # 뷰 로직 -│ └── CharacterUI.cs # UI 관련 -└── Model/ - └── CharacterData.cs # 데이터 모델 -``` - -### Infrastructure 클래스 네이밍 -``` -Infrastructure/Network/ -├── Services/ -│ ├── ChatApiService.cs # API 서비스 -│ └── WebSocketService.cs # 웹소켓 서비스 -├── DTOs/ -│ ├── ChatRequest.cs # 요청 DTO -│ └── ChatResponse.cs # 응답 DTO -└── Handlers/ - └── MessageHandler.cs # 메시지 처리 -``` - -### Core 클래스 네이밍 -``` -Core/ -├── Audio/ -│ ├── AudioManager.cs # 오디오 매니저 -│ └── VoicePlayer.cs # 음성 재생 -├── Input/ -│ ├── InputManager.cs # 입력 매니저 -│ └── TouchHandler.cs # 터치 처리 -└── Utils/ - ├── JsonHelper.cs # JSON 유틸 - └── TimeHelper.cs # 시간 유틸 -``` - ---- - -## 코딩 스타일 가이드 - -### 메서드 구조 -```csharp -public class ChatManager : MonoBehaviour -{ - // 1. SerializeField (Inspector 노출) - [SerializeField] private ChatUI _chatUI; - [SerializeField] private AudioManager _audioManager; - - // 2. private 필드 - private string _sessionId; - private bool _isInitialized; - - // 3. public 프로퍼티 - public bool IsConnected { get; private set; } - - // 4. Unity 생명주기 메서드 - private void Awake() - { - InitializeComponents(); - } - - private void Start() - { - InitializeChat(); - } - - // 5. public 메서드 - public void SendMessage(string message) - { - ValidateInput(message); - ProcessMessage(message); - } - - // 6. private 메서드 - private void InitializeComponents() - { - // 구현 - } - - // 7. 이벤트 핸들러 - private void OnMessageReceived(ChatResponse response) - { - // 구현 - } -} -``` - -### 네임스페이스 규칙 -```csharp -namespace ProjectVG.Domain.Chat -{ - public class ChatManager { } -} - -namespace ProjectVG.Infrastructure.Network -{ - public class ChatApiService { } -} - -namespace ProjectVG.Core.Audio -{ - public class AudioManager { } -} -``` \ No newline at end of file From d580e48b3eb4736436471af118ad6c14d7d846a3 Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 11:13:43 +0900 Subject: [PATCH 12/19] =?UTF-8?q?refactoy:=20=EB=B3=B5=EC=9E=A1=ED=95=9C?= =?UTF-8?q?=20Manager=20=EA=B5=AC=EC=A1=B0=EB=A5=BC=20=EA=B0=84=EC=86=8C?= =?UTF-8?q?=ED=99=94=ED=95=98=EC=97=AC=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/MainSence.unity | 40 +++-- Assets/App/Scenes/StartSence.unity | 115 +++---------- Assets/Core/Attributes.meta | 8 - Assets/Core/Attributes/InjectAttribute.cs | 17 -- .../Core/Attributes/InjectAttribute.cs.meta | 2 - Assets/Core/DI.meta | 8 - Assets/Core/DI/DIContainer.cs | 96 ----------- Assets/Core/DI/DIContainer.cs.meta | 2 - Assets/Core/Loading/LoadingManager.cs | 62 +++++-- Assets/Core/Managers/DependencyManager.cs | 75 --------- .../Core/Managers/DependencyManager.cs.meta | 2 - Assets/Core/Managers/InitializationManager.cs | 155 ------------------ .../Managers/InitializationManager.cs.meta | 2 - Assets/Core/Managers/ManagerRegistry.cs | 125 -------------- Assets/Core/Managers/ManagerRegistry.cs.meta | 2 - Assets/Core/Managers/README_GameManager.md | 129 --------------- .../Core/Managers/README_GameManager.md.meta | 7 - Assets/Core/Managers/SystemManager.cs | 126 ++++++-------- .../Network/Http/HttpApiClient.cs | 145 +++++----------- .../Network/Services/SessionManager.cs | 34 ++-- .../Network/WebSocket/WebSocketManager.cs | 55 ++++--- 21 files changed, 227 insertions(+), 980 deletions(-) delete mode 100644 Assets/Core/Attributes.meta delete mode 100644 Assets/Core/Attributes/InjectAttribute.cs delete mode 100644 Assets/Core/Attributes/InjectAttribute.cs.meta delete mode 100644 Assets/Core/DI.meta delete mode 100644 Assets/Core/DI/DIContainer.cs delete mode 100644 Assets/Core/DI/DIContainer.cs.meta delete mode 100644 Assets/Core/Managers/DependencyManager.cs delete mode 100644 Assets/Core/Managers/DependencyManager.cs.meta delete mode 100644 Assets/Core/Managers/InitializationManager.cs delete mode 100644 Assets/Core/Managers/InitializationManager.cs.meta delete mode 100644 Assets/Core/Managers/ManagerRegistry.cs delete mode 100644 Assets/Core/Managers/ManagerRegistry.cs.meta delete mode 100644 Assets/Core/Managers/README_GameManager.md delete mode 100644 Assets/Core/Managers/README_GameManager.md.meta diff --git a/Assets/App/Scenes/MainSence.unity b/Assets/App/Scenes/MainSence.unity index 2d854cc..5d1bde5 100644 --- a/Assets/App/Scenes/MainSence.unity +++ b/Assets/App/Scenes/MainSence.unity @@ -379,6 +379,7 @@ MonoBehaviour: _btnVoice: {fileID: 1411584568} _btnVoiceStop: {fileID: 948060135} _txtVoiceStatus: {fileID: 1273342020} + _progressBar: {fileID: 0} _maxRecordingTime: 30 _voiceStatusRecording: "\uB179\uC74C \uC911..." _voiceStatusProcessing: "\uC74C\uC131\uC744 \uD14D\uC2A4\uD2B8\uB85C \uBCC0\uD658 @@ -689,7 +690,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1104447313} - - component: {fileID: 1104447312} + - component: {fileID: 1104447314} m_Layer: 0 m_Name: GameManager m_TagString: Untagged @@ -697,23 +698,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &1104447312 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 1104447311} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f2343a97be172e748a6501089d9e3c0a, type: 3} - m_Name: - m_EditorClassIdentifier: - _initializationManager: {fileID: 0} - _managerRegistry: {fileID: 0} - _dependencyManager: {fileID: 0} - _autoInitializeOnStart: 1 - _createManagersIfNotExist: 1 --- !u!4 &1104447313 Transform: m_ObjectHideFlags: 0 @@ -730,6 +714,26 @@ Transform: - {fileID: 1087467995} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1104447314 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1104447311} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c31e1b9082df088489ede1181874cde4, type: 3} + m_Name: + m_EditorClassIdentifier: + _webSocketManager: {fileID: 0} + _sessionManager: {fileID: 0} + _httpApiClient: {fileID: 0} + _audioManager: {fileID: 0} + _autoInitializeOnStart: 1 + _createManagersIfNotExist: 1 + _autoUpdateCameraOnSceneChange: 1 + _camera: {fileID: 0} --- !u!1 &1145326583 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/App/Scenes/StartSence.unity b/Assets/App/Scenes/StartSence.unity index 81246aa..373ae08 100644 --- a/Assets/App/Scenes/StartSence.unity +++ b/Assets/App/Scenes/StartSence.unity @@ -128,7 +128,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 177257299} - - component: {fileID: 177257298} + - component: {fileID: 177257300} m_Layer: 0 m_Name: GameSystem m_TagString: Untagged @@ -136,23 +136,6 @@ GameObject: m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &177257298 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 177257297} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f2343a97be172e748a6501089d9e3c0a, type: 3} - m_Name: - m_EditorClassIdentifier: - _initializationManager: {fileID: 0} - _managerRegistry: {fileID: 0} - _dependencyManager: {fileID: 0} - _autoInitializeOnStart: 1 - _createManagersIfNotExist: 1 --- !u!4 &177257299 Transform: m_ObjectHideFlags: 0 @@ -166,10 +149,30 @@ Transform: m_LocalScale: {x: 1, y: 1, z: 1} m_ConstrainProportionsScale: 0 m_Children: - - {fileID: 573203869} - {fileID: 1015393258} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &177257300 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 177257297} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: c31e1b9082df088489ede1181874cde4, type: 3} + m_Name: + m_EditorClassIdentifier: + _webSocketManager: {fileID: 0} + _sessionManager: {fileID: 0} + _httpApiClient: {fileID: 0} + _audioManager: {fileID: 0} + _loadingManager: {fileID: 0} + _autoInitializeOnStart: 1 + _createManagersIfNotExist: 1 + _autoUpdateCameraOnSceneChange: 1 + _camera: {fileID: 0} --- !u!1 &193680551 GameObject: m_ObjectHideFlags: 0 @@ -414,78 +417,6 @@ CanvasRenderer: m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 359102246} m_CullTransparentMesh: 1 ---- !u!1 &573203868 -GameObject: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - serializedVersion: 6 - m_Component: - - component: {fileID: 573203869} - - component: {fileID: 573203872} - - component: {fileID: 573203871} - - component: {fileID: 573203870} - m_Layer: 0 - m_Name: Network Managers - m_TagString: Untagged - m_Icon: {fileID: 0} - m_NavMeshLayer: 0 - m_StaticEditorFlags: 0 - m_IsActive: 1 ---- !u!4 &573203869 -Transform: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 573203868} - serializedVersion: 2 - m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} - m_LocalPosition: {x: 0, y: 0, z: 0} - m_LocalScale: {x: 1, y: 1, z: 1} - m_ConstrainProportionsScale: 0 - m_Children: [] - m_Father: {fileID: 177257299} - m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} ---- !u!114 &573203870 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 573203868} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 8e1977f0484f5f84684a077ad63409b5, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &573203871 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 573203868} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 00fbb8efd091db646bc0653b6295222f, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &573203872 -MonoBehaviour: - m_ObjectHideFlags: 0 - m_CorrespondingSourceObject: {fileID: 0} - m_PrefabInstance: {fileID: 0} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 573203868} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: f0048fe563a65f94a90a49760ba27126, type: 3} - m_Name: - m_EditorClassIdentifier: - _currentSessionId: - _isInitialized: 0 --- !u!1 &648783742 GameObject: m_ObjectHideFlags: 0 @@ -827,6 +758,8 @@ MonoBehaviour: m_Name: m_EditorClassIdentifier: _loadingUI: {fileID: 193680555} + _autoProgressMax: 0.9 + _autoProgressSpeed: 0.25 --- !u!4 &915844785 Transform: m_ObjectHideFlags: 0 diff --git a/Assets/Core/Attributes.meta b/Assets/Core/Attributes.meta deleted file mode 100644 index d84cb55..0000000 --- a/Assets/Core/Attributes.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: 9c42baf40e460a24baa55ced9fdb5b7a -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Core/Attributes/InjectAttribute.cs b/Assets/Core/Attributes/InjectAttribute.cs deleted file mode 100644 index 743c9a3..0000000 --- a/Assets/Core/Attributes/InjectAttribute.cs +++ /dev/null @@ -1,17 +0,0 @@ -using UnityEngine; - -namespace ProjectVG.Core.Attributes -{ - /// - /// 의존성 주입을 위한 커스텀 어트리뷰트 - /// - public class InjectAttribute : PropertyAttribute - { - public string DependencyName { get; } - - public InjectAttribute(string dependencyName = "") - { - DependencyName = dependencyName; - } - } -} \ No newline at end of file diff --git a/Assets/Core/Attributes/InjectAttribute.cs.meta b/Assets/Core/Attributes/InjectAttribute.cs.meta deleted file mode 100644 index f186bc4..0000000 --- a/Assets/Core/Attributes/InjectAttribute.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 2aa07c71a3e7eb2429aeca8359d31418 \ No newline at end of file diff --git a/Assets/Core/DI.meta b/Assets/Core/DI.meta deleted file mode 100644 index 6686f9d..0000000 --- a/Assets/Core/DI.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: a528030241c95284cb4188eeb31d5685 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Core/DI/DIContainer.cs b/Assets/Core/DI/DIContainer.cs deleted file mode 100644 index b8f9a95..0000000 --- a/Assets/Core/DI/DIContainer.cs +++ /dev/null @@ -1,96 +0,0 @@ -using System; -using System.Collections.Generic; -using UnityEngine; -using ProjectVG.Core.Attributes; - -namespace ProjectVG.Core.DI -{ - public class DIContainer : Singleton - { - private readonly Dictionary _services = new Dictionary(); - - #region Unity Lifecycle - - protected override void Awake() - { - base.Awake(); - } - - #endregion - - #region Public Methods - - public void Register(T service) - { - _services[typeof(T)] = service; - } - - public void Unregister() - { - _services.Remove(typeof(T)); - } - - public T Get() - { - if (_services.TryGetValue(typeof(T), out var service)) - { - return (T)service; - } - return default(T); - } - - public void InjectDependencies(MonoBehaviour component) - { - var type = component.GetType(); - var fields = type.GetFields(System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - - Debug.Log($"[DIContainer] {component.GetType().Name}에 의존성 주입 시작"); - - foreach (var field in fields) - { - var injectAttribute = field.GetCustomAttributes(typeof(InjectAttribute), true); - if (injectAttribute.Length > 0) - { - var serviceType = field.FieldType; - var service = GetService(serviceType); - if (service != null) - { - field.SetValue(component, service); - Debug.Log($"[DIContainer] 의존성 주입 완료: {component.GetType().Name}.{field.Name} <- {serviceType.Name}"); - - // 주입 후 검증 - var injectedValue = field.GetValue(component); - if (injectedValue != null) - { - Debug.Log($"[DIContainer] 주입 검증 성공: {field.Name}에 {serviceType.Name} 인스턴스가 정상적으로 설정됨"); - } - else - { - Debug.LogError($"[DIContainer] 주입 검증 실패: {field.Name}이 여전히 null임"); - } - } - else - { - Debug.LogWarning($"[DIContainer] 의존성 주입 실패: {serviceType.Name} 서비스를 찾을 수 없습니다."); - Debug.LogWarning($"[DIContainer] 등록된 서비스 목록: {string.Join(", ", _services.Keys)}"); - } - } - } - } - - #endregion - - #region Private Methods - - private object GetService(Type serviceType) - { - if (_services.TryGetValue(serviceType, out var service)) - { - return service; - } - return null; - } - - #endregion - } -} \ No newline at end of file diff --git a/Assets/Core/DI/DIContainer.cs.meta b/Assets/Core/DI/DIContainer.cs.meta deleted file mode 100644 index 5e6e2f3..0000000 --- a/Assets/Core/DI/DIContainer.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: d45e4b5cd956cd74ead23af47fbec602 \ No newline at end of file diff --git a/Assets/Core/Loading/LoadingManager.cs b/Assets/Core/Loading/LoadingManager.cs index 64fd55a..d92f6b2 100644 --- a/Assets/Core/Loading/LoadingManager.cs +++ b/Assets/Core/Loading/LoadingManager.cs @@ -35,6 +35,9 @@ public class LoadingManager : Singleton private TaskInfo _currentTask; private bool _gameStarted; + [SerializeField] private float _autoProgressMax = 0.9f; + [SerializeField] private float _autoProgressSpeed = 0.25f; + private Coroutine _autoProgressCoroutine; public event Action OnInitializationFailed; @@ -54,11 +57,22 @@ private void OnDestroy() #region Public Methods + /// + /// 로딩 UI 시작 및 자동 진행 시작 + /// + public void BeginLoadingUI() + { + UpdateTask("INITIALIZATION", "시스템 초기화 중...", 0.05f); + StartAutoProgress(); + } + public void StartInitialization() { if (SystemManager.Instance != null) { - SystemManager.Instance.InitializeGame(); + UpdateTask("INITIALIZATION", "시스템 초기화 중...", 0.05f); + StartAutoProgress(); + SystemManager.Instance.InitializeGame(); } else { @@ -108,11 +122,6 @@ private void SetupEventListeners() { SystemManager.Instance.OnGameInitialized += OnGameInitialized; SystemManager.Instance.OnInitializationError += OnInitializationError; - var initializationManager = SystemManager.Instance.GetComponent(); - if (initializationManager != null) - { - initializationManager.OnProgressUpdated += OnProgressUpdated; - } } } @@ -122,11 +131,6 @@ private void RemoveEventListeners() { SystemManager.Instance.OnGameInitialized -= OnGameInitialized; SystemManager.Instance.OnInitializationError -= OnInitializationError; - var initializationManager = SystemManager.Instance.GetComponent(); - if (initializationManager != null) - { - initializationManager.OnProgressUpdated -= OnProgressUpdated; - } } } @@ -134,6 +138,8 @@ private void OnGameInitialized() { if (!_gameStarted) { + StopAutoProgress(); + UpdateTask("INITIALIZATION", "완료", 1f); StartGame(); } } @@ -144,10 +150,36 @@ private void OnInitializationError(string error) OnInitializationFailed?.Invoke(error); } - private void OnProgressUpdated(string taskName, string description, float progress) - { - UpdateTask(taskName, description, progress); - } + /// + /// 자동 진행 바 업데이트 시작 + /// + private void StartAutoProgress() + { + if (_autoProgressCoroutine != null) return; + _autoProgressCoroutine = StartCoroutine(AutoProgressRoutine()); + } + + /// + /// 자동 진행 바 중지 + /// + private void StopAutoProgress() + { + if (_autoProgressCoroutine == null) return; + StopCoroutine(_autoProgressCoroutine); + _autoProgressCoroutine = null; + } + + private System.Collections.IEnumerator AutoProgressRoutine() + { + float p = Mathf.Clamp01(_currentTask.progress); + while (p < _autoProgressMax && !_gameStarted) + { + p += Time.deltaTime * _autoProgressSpeed; + UpdateTask("INITIALIZATION", "시스템 초기화 중...", Mathf.Min(p, _autoProgressMax)); + yield return null; + } + _autoProgressCoroutine = null; + } #endregion } diff --git a/Assets/Core/Managers/DependencyManager.cs b/Assets/Core/Managers/DependencyManager.cs deleted file mode 100644 index 4d26426..0000000 --- a/Assets/Core/Managers/DependencyManager.cs +++ /dev/null @@ -1,75 +0,0 @@ -using UnityEngine; -using ProjectVG.Core.DI; -using ProjectVG.Infrastructure.Network.Services; -using ProjectVG.Infrastructure.Network.WebSocket; -using ProjectVG.Infrastructure.Network.Http; - - namespace ProjectVG.Core.Managers - { - /// - /// 의존성 주입을 전담하는 매니저 - /// - public class DependencyManager : MonoBehaviour - { - private DIContainer _container; - - private void Awake() - { - _container = DIContainer.Instance; - } - - public void SetupDependencies(ManagerRegistry managerRegistry) - { - if (managerRegistry == null) - { - Debug.LogError("[DependencyManager] ManagerRegistry가 null입니다."); - return; - } - - RegisterServices(managerRegistry); - InjectDependencies(managerRegistry); - Debug.Log("[DependencyManager] DI 완료"); - } - - public void RegisterService(T service) - { - _container.Register(service); - } - - public T GetService() - { - return _container.Get(); - } - - private void RegisterServices(ManagerRegistry managerRegistry) - { - if (managerRegistry.SessionManager != null) - { - _container.Register(managerRegistry.SessionManager); - } - - if (managerRegistry.WebSocketManager != null) - { - _container.Register(managerRegistry.WebSocketManager); - } - - if (managerRegistry.HttpApiClient != null) - { - _container.Register(managerRegistry.HttpApiClient); - } - } - - private void InjectDependencies(ManagerRegistry managerRegistry) - { - if (managerRegistry.SessionManager != null) - { - _container.InjectDependencies(managerRegistry.SessionManager); - } - - if (managerRegistry.HttpApiClient != null) - { - _container.InjectDependencies(managerRegistry.HttpApiClient); - } - } - } - } diff --git a/Assets/Core/Managers/DependencyManager.cs.meta b/Assets/Core/Managers/DependencyManager.cs.meta deleted file mode 100644 index 904a8c2..0000000 --- a/Assets/Core/Managers/DependencyManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 4170d4d59f89094499434d69bfe8b704 \ No newline at end of file diff --git a/Assets/Core/Managers/InitializationManager.cs b/Assets/Core/Managers/InitializationManager.cs deleted file mode 100644 index 37700ca..0000000 --- a/Assets/Core/Managers/InitializationManager.cs +++ /dev/null @@ -1,155 +0,0 @@ -using UnityEngine; -using System; -using Cysharp.Threading.Tasks; - -namespace ProjectVG.Core.Managers -{ - public enum InitializationPhase - { - NotStarted, - InitializingManagers, - ConnectingToServer, - LoadingResources, - Completed - } - - /** - * 초기화 과정을 전담하는 매니저 - * 단계별 초기화, 진행률 추적, 이벤트 발생을 담당 - */ - public class InitializationManager : MonoBehaviour - { - private InitializationPhase _currentPhase = InitializationPhase.NotStarted; - private bool _isInitialized = false; - private bool _isInitializing = false; - - public InitializationPhase CurrentPhase => _currentPhase; - public bool IsInitialized => _isInitialized; - public bool IsInitializing => _isInitializing; - - public event Action OnInitializationCompleted; - public event Action OnInitializationError; - public event Action OnProgressUpdated; - - private ManagerRegistry _managerRegistry; - private DependencyManager _dependencyManager; - - #region Public Methods - - public void Initialize(ManagerRegistry managerRegistry, DependencyManager dependencyManager) - { - _managerRegistry = managerRegistry; - _dependencyManager = dependencyManager; - } - - public async UniTask InitializeAsync() - { - if (_isInitialized) - { - return; - } - - if (_isInitializing) - { - while (_isInitializing && !_isInitialized) - { - await UniTask.Yield(); - } - return; - } - - _isInitializing = true; - - try - { - _currentPhase = InitializationPhase.InitializingManagers; - await InitializeManagersAsync(); - - _currentPhase = InitializationPhase.ConnectingToServer; - await ConnectToServerAsync(); - - _currentPhase = InitializationPhase.LoadingResources; - await LoadResourcesAsync(); - - _currentPhase = InitializationPhase.Completed; - UpdateLoadingProgress("INITIALIZATION", "게임 준비 완료", 1.0f); - - _isInitialized = true; - Debug.Log("[InitializationManager] 초기화 완료"); - OnInitializationCompleted?.Invoke(); - } - catch (Exception ex) - { - string error = $"[InitializationManager] 초기화 실패: {ex.Message}"; - Debug.LogError(error); - OnInitializationError?.Invoke(error); - } - finally - { - _isInitializing = false; - } - } - - #endregion - - #region Private Methods - - private async UniTask InitializeManagersAsync() - { - if (_managerRegistry == null) - throw new InvalidOperationException("ManagerRegistry가 설정되지 않았습니다."); - - UpdateLoadingProgress("INITIALIZATION", "시스템 매니저 초기화", 0.05f); - - _managerRegistry.InitializeAllManagers(); - - UpdateLoadingProgress("INITIALIZATION", "매니저 등록", 0.12f); - - _dependencyManager?.SetupDependencies(_managerRegistry); - - UpdateLoadingProgress("INITIALIZATION", "의존성 주입", 0.20f); - - if (_managerRegistry.SessionManager != null) - { - _managerRegistry.SessionManager.Initialize(); - } - } - - private async UniTask ConnectToServerAsync() - { - if (_managerRegistry == null) - throw new InvalidOperationException("ManagerRegistry가 설정되지 않았습니다."); - - var sessionManager = _managerRegistry.SessionManager; - if (sessionManager == null) - throw new InvalidOperationException("SessionManager가 초기화되지 않았습니다."); - - UpdateLoadingProgress("INITIALIZATION", "네트워크 연결", 0.30f); - UpdateLoadingProgress("INITIALIZATION", "세션 생성", 0.45f); - - bool connected = await sessionManager.EnsureConnectionAsync(); - if (!connected) - { - throw new InvalidOperationException("세션 연결에 실패했습니다."); - } - - UpdateLoadingProgress("INITIALIZATION", "세션 완료", 0.60f); - } - - private async UniTask LoadResourcesAsync() - { - UpdateLoadingProgress("INITIALIZATION", "리소스 스캔", 0.65f); - await UniTask.Delay(100); - UpdateLoadingProgress("INITIALIZATION", "필수 에셋 로딩", 0.75f); - await UniTask.Delay(100); - UpdateLoadingProgress("INITIALIZATION", "리소스 로딩 완료", 0.90f); - } - - private void UpdateLoadingProgress(string taskName, string description, float progress) - { - OnProgressUpdated?.Invoke(taskName, description, progress); - } - - #endregion - } -} diff --git a/Assets/Core/Managers/InitializationManager.cs.meta b/Assets/Core/Managers/InitializationManager.cs.meta deleted file mode 100644 index dfba111..0000000 --- a/Assets/Core/Managers/InitializationManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 64867cef2ba0d4849a0a93a3253d7c5d \ No newline at end of file diff --git a/Assets/Core/Managers/ManagerRegistry.cs b/Assets/Core/Managers/ManagerRegistry.cs deleted file mode 100644 index 5b807ee..0000000 --- a/Assets/Core/Managers/ManagerRegistry.cs +++ /dev/null @@ -1,125 +0,0 @@ -using UnityEngine; -using System; -using System.Collections.Generic; -using ProjectVG.Infrastructure.Network.WebSocket; -using ProjectVG.Infrastructure.Network.Services; -using ProjectVG.Infrastructure.Network.Http; - -namespace ProjectVG.Core.Managers -{ - /** - * 매니저들의 등록, 생성, 생명주기를 관리하는 매니저 - */ - public class ManagerRegistry : MonoBehaviour - { - [Header("Manager References")] - [SerializeField] private WebSocketManager _webSocketManager; - [SerializeField] private SessionManager _sessionManager; - [SerializeField] private HttpApiClient _httpApiClient; - [SerializeField] private AudioManager _audioManager; - - [Header("Settings")] - [SerializeField] private bool _createManagersIfNotExist = true; - - private readonly List _managers = new List(); - - public WebSocketManager WebSocketManager => _webSocketManager; - public SessionManager SessionManager => _sessionManager; - public HttpApiClient HttpApiClient => _httpApiClient; - public AudioManager AudioManager => _audioManager; - - #region Public Methods - - public void InitializeAllManagers() - { - InitializeWebSocketManager(); - InitializeHttpApiClient(); - InitializeSessionManager(); - } - - public bool AreManagersReady() - { - return _webSocketManager != null && - _sessionManager != null && - _httpApiClient != null; - } - - public bool IsSessionConnected() - { - return _sessionManager != null && _sessionManager.IsSessionConnected; - } - - public void ShutdownAllManagers() - { - for (int i = _managers.Count - 1; i >= 0; i--) - { - try - { - _managers[i]?.Shutdown(); - } - catch (Exception ex) - { - Debug.LogError($"[ManagerRegistry] 매니저 종료 오류: {ex.Message}"); - } - } - _managers.Clear(); - Debug.Log("[ManagerRegistry] 모든 매니저 종료 완료"); - } - - public void LogManagerStatus() - { - Debug.Log($"Managers Ready: {(AreManagersReady() ? "Yes" : "No")}, Session: {(IsSessionConnected() ? "Connected" : "Disconnected")}"); - } - - #endregion - - #region Private Methods - - private void InitializeWebSocketManager() - { - if (_webSocketManager == null && _createManagersIfNotExist) - { - var webSocketObj = new GameObject("WebSocketManager"); - webSocketObj.transform.SetParent(transform); - _webSocketManager = webSocketObj.AddComponent(); - } - if (_webSocketManager == null) - { - throw new InvalidOperationException("WebSocketManager를 초기화할 수 없습니다."); - } - _managers.Add(_webSocketManager); - } - - private void InitializeSessionManager() - { - if (_sessionManager == null && _createManagersIfNotExist) - { - var sessionObj = new GameObject("SessionManager"); - sessionObj.transform.SetParent(transform); - _sessionManager = sessionObj.AddComponent(); - } - if (_sessionManager == null) - { - throw new InvalidOperationException("SessionManager를 초기화할 수 없습니다."); - } - _managers.Add(_sessionManager); - } - - private void InitializeHttpApiClient() - { - if (_httpApiClient == null && _createManagersIfNotExist) - { - var httpObj = new GameObject("HttpApiClient"); - httpObj.transform.SetParent(transform); - _httpApiClient = httpObj.AddComponent(); - } - if (_httpApiClient == null) - { - throw new InvalidOperationException("HttpApiClient를 초기화할 수 없습니다."); - } - _managers.Add(_httpApiClient); - } - - #endregion - } -} diff --git a/Assets/Core/Managers/ManagerRegistry.cs.meta b/Assets/Core/Managers/ManagerRegistry.cs.meta deleted file mode 100644 index c5cdea9..0000000 --- a/Assets/Core/Managers/ManagerRegistry.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: e9adb6dc961927c489cf26eefd75c62c \ No newline at end of file diff --git a/Assets/Core/Managers/README_GameManager.md b/Assets/Core/Managers/README_GameManager.md deleted file mode 100644 index e478f73..0000000 --- a/Assets/Core/Managers/README_GameManager.md +++ /dev/null @@ -1,129 +0,0 @@ -# GameManager 패턴 사용법 - -## 개요 -GameManager는 Unity에서 가장 권장되는 매니저 초기화 패턴입니다. 게임의 핵심 매니저들(WebSocketManager, SessionManager, HttpApiClient)의 생명주기를 관리합니다. - -## Unity에서 가장 권장되는 이유 - -### 1. **명확한 책임 분리** -- GameManager: 매니저들의 생명주기 관리 -- 각 매니저: 자신의 도메인 로직만 담당 - -### 2. **의존성 관리** -- 매니저들 간의 의존성을 명확하게 관리 -- 초기화 순서 보장 (WebSocketManager → SessionManager → HttpApiClient) - -### 3. **확장성** -- 새로운 매니저 추가가 용이 -- 설정 변경이 간단 - -### 4. **디버깅 용이성** -- Inspector에서 매니저 상태 확인 가능 -- ContextMenu를 통한 상태 로그 출력 - -## 설정 방법 - -### 1. GameManager 프리팹 생성 -1. 빈 GameObject 생성 -2. GameManager 컴포넌트 추가 -3. 프리팹으로 저장 - -### 2. 씬에 배치 -- 첫 번째 씬에 GameManager 프리팹 배치 -- DontDestroyOnLoad로 설정되어 씬 전환 시에도 유지 - -### 3. 설정 옵션 -GameManager Inspector에서 설정 가능: -- **Auto Initialize On Start**: Start() 시 자동 초기화 여부 -- **Create Managers If Not Exist**: 매니저가 없을 때 자동 생성 여부 -- **Manager References**: 각 매니저의 참조 (선택사항) - -## 사용법 - -### 자동 초기화 (기본) -```csharp -// GameManager가 Start()에서 자동으로 초기화 -// 별도 코드 작성 불필요 -``` - -### 수동 초기화 -```csharp -// GameManager 참조 -var gameManager = GameManager.Instance; - -// 수동 초기화 -gameManager.InitializeGame(); - -// 초기화 완료 이벤트 구독 -gameManager.OnGameInitialized += () => { - Debug.Log("게임 초기화 완료!"); -}; - -// 초기화 에러 이벤트 구독 -gameManager.OnInitializationError += (error) => { - Debug.LogError($"초기화 실패: {error}"); -}; -``` - -### 매니저 접근 -```csharp -// GameManager를 통한 접근 -var webSocket = GameManager.Instance.WebSocketManager; -var session = GameManager.Instance.SessionManager; -var httpClient = GameManager.Instance.HttpApiClient; - -// 또는 직접 접근 (싱글톤) -var webSocket = WebSocketManager.Instance; -var session = SessionManager.Instance; -var httpClient = HttpApiClient.Instance; -``` - -### 상태 확인 -```csharp -// 에디터에서 -// GameManager 우클릭 → Log Manager Status - -// 코드에서 -if (GameManager.Instance.AreManagersReady()) -{ - // 모든 매니저가 준비됨 -} -``` - -## 장점 - -### 1. **Unity 커뮤니티 표준** -- 대부분의 Unity 프로젝트에서 사용 -- 개발자들이 익숙한 패턴 - -### 2. **Inspector 지원** -- 매니저 참조를 Inspector에서 설정 가능 -- 실시간 상태 확인 가능 - -### 3. **명확한 생명주기** -- 초기화 순서 보장 -- 종료 시 역순으로 정리 - -### 4. **에러 처리** -- 초기화 실패 시 명확한 에러 메시지 -- 이벤트를 통한 에러 처리 - -## 다른 방법들과의 비교 - -| 방법 | 장점 | 단점 | 권장도 | -|------|------|------|--------| -| **GameManager 패턴** | 표준, 명확, 확장성 | 약간의 보일러플레이트 | ⭐⭐⭐⭐⭐ | -| ScriptableObject 초기화 | 자동화, 설정 관리 | 복잡, Resources 의존 | ⭐⭐⭐ | -| 별도 컴포넌트 | 간단, 직관적 | 실수 가능성, 관리 어려움 | ⭐⭐ | - -## 결론 - -**GameManager 패턴**이 Unity에서 가장 권장되는 방식입니다. 이유: - -1. **Unity 커뮤니티 표준**: 대부분의 프로덕션 프로젝트에서 사용 -2. **명확한 책임 분리**: 각 매니저의 역할이 명확 -3. **확장성**: 새로운 매니저 추가가 용이 -4. **디버깅 용이성**: Inspector에서 상태 확인 가능 -5. **에러 처리**: 명확한 에러 메시지와 처리 - -이 패턴을 사용하면 프로젝트의 유지보수성과 확장성이 크게 향상됩니다. \ No newline at end of file diff --git a/Assets/Core/Managers/README_GameManager.md.meta b/Assets/Core/Managers/README_GameManager.md.meta deleted file mode 100644 index c24bedb..0000000 --- a/Assets/Core/Managers/README_GameManager.md.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: eb3280819ad3dc247b57af8efe6c9ef6 -TextScriptImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Core/Managers/SystemManager.cs b/Assets/Core/Managers/SystemManager.cs index 695c29b..b2b604b 100644 --- a/Assets/Core/Managers/SystemManager.cs +++ b/Assets/Core/Managers/SystemManager.cs @@ -3,17 +3,20 @@ using ProjectVG.Infrastructure.Network.WebSocket; using ProjectVG.Infrastructure.Network.Services; using ProjectVG.Infrastructure.Network.Http; -using ProjectVG.Core.DI; using Cysharp.Threading.Tasks; +using ProjectVG.Core.Loading; namespace ProjectVG.Core.Managers { public class SystemManager : Singleton { [Header("Core Managers")] - [SerializeField] private InitializationManager _initializationManager; - [SerializeField] private ManagerRegistry _managerRegistry; - [SerializeField] private DependencyManager _dependencyManager; + [SerializeField] private WebSocketManager _webSocketManager; + [SerializeField] private SessionManager _sessionManager; + [SerializeField] private HttpApiClient _httpApiClient; + [SerializeField] private AudioManager _audioManager; + [SerializeField] private LoadingManager _loadingManager; + [Header("Settings")] [SerializeField] private bool _autoInitializeOnStart = true; @@ -24,22 +27,15 @@ public class SystemManager : Singleton [SerializeField] private Camera _camera; private bool _initializationKickoffDone = false; - - public bool IsInitialized => _initializationManager?.IsInitialized ?? false; - - public WebSocketManager WebSocketManager => _managerRegistry?.WebSocketManager; - public SessionManager SessionManager => _managerRegistry?.SessionManager; - - public event Action OnGameInitialized - { - add => _initializationManager.OnInitializationCompleted += value; - remove => _initializationManager.OnInitializationCompleted -= value; - } - public event Action OnInitializationError - { - add => _initializationManager.OnInitializationError += value; - remove => _initializationManager.OnInitializationError -= value; - } + public bool IsInitialized { get; private set; } + + public WebSocketManager WebSocketManager => _webSocketManager; + public SessionManager SessionManager => _sessionManager; + public AudioManager AudioManager => _audioManager; + public LoadingManager LoadingManager => _loadingManager; + + public event Action OnGameInitialized; + public event Action OnInitializationError; protected override void Awake() { @@ -59,7 +55,6 @@ protected override void Awake() private void Start() { - // 씬 전환 이벤트 구독 if (_autoUpdateCameraOnSceneChange) { UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; @@ -73,7 +68,6 @@ private void OnDestroy() return; } - // 이벤트 구독 해제 if (_autoUpdateCameraOnSceneChange) { UnityEngine.SceneManagement.SceneManager.sceneLoaded -= OnSceneLoaded; @@ -131,7 +125,7 @@ public void SetCamera(Camera camera) public async void InitializeGame() { - if (_initializationKickoffDone && (IsInitialized || (_initializationManager?.IsInitializing ?? false))) + if (_initializationKickoffDone && IsInitialized) { return; } @@ -149,36 +143,26 @@ public async void InitializeGame() public async UniTask InitializeGameAsync() { - if (_initializationManager == null) - { - Debug.LogError("[SystemManager] InitializationManager가 설정되지 않았습니다."); - return; - } - - if (_initializationManager.IsInitialized) - { - return; - } - - if (_initializationManager.IsInitializing) + try { - while (_initializationManager.IsInitializing && !_initializationManager.IsInitialized) - { - await UniTask.Yield(); - } - return; + await InitializeManagersAsync(); + IsInitialized = true; + Debug.Log("[SystemManager] 게임 시스템 준비 완료"); + OnGameInitialized?.Invoke(); } - - await _initializationManager.InitializeAsync(); - if (IsInitialized) + catch (Exception ex) { - Debug.Log("[SystemManager] 게임 시스템 준비 완료"); + IsInitialized = false; + Debug.LogError($"[SystemManager] 초기화 실패: {ex.Message}"); + OnInitializationError?.Invoke(ex.Message); } } public void Shutdown() { - _managerRegistry?.ShutdownAllManagers(); + try { _httpApiClient?.Shutdown(); } catch {} + try { _sessionManager?.Shutdown(); } catch {} + try { _webSocketManager?.Shutdown(); } catch {} Debug.Log("[SystemManager] 시스템 종료 완료"); } @@ -187,7 +171,7 @@ public void LogManagerStatus() { Debug.Log($"[SystemManager] Initialized: {IsInitialized}"); Debug.Log($"[SystemManager] Current Camera: {(_camera != null ? _camera.name : "null")}"); - _managerRegistry?.LogManagerStatus(); + Debug.Log($"[SystemManager] WS: {(WebSocketManager != null ? "OK" : "null")}, Session: {(SessionManager != null ? "OK" : "null")}, Audio: {(AudioManager != null ? "OK" : "null")}, Loading: {(LoadingManager != null ? "OK" : "null")}"); } [ContextMenu("Update Camera")] @@ -226,51 +210,39 @@ public async UniTask TransitionToMainSceneAsync() private void InitializeComponents() { CreateManagersIfNotExist(); - SetupManagerReferences(); } private void CreateManagersIfNotExist() { if (_createManagersIfNotExist) { - if (_initializationManager == null) - { - var initObj = new GameObject("InitializationManager"); - initObj.transform.SetParent(transform); - _initializationManager = initObj.AddComponent(); - } - - if (_managerRegistry == null) - { - var registryObj = new GameObject("ManagerRegistry"); - registryObj.transform.SetParent(transform); - _managerRegistry = registryObj.AddComponent(); - } - - if (_dependencyManager == null) - { - var depObj = new GameObject("DependencyManager"); - depObj.transform.SetParent(transform); - _dependencyManager = depObj.AddComponent(); - } + _webSocketManager = WebSocketManager.Instance; + _sessionManager = SessionManager.Instance; + _httpApiClient = HttpApiClient.Instance; + _audioManager = AudioManager.Instance; + _loadingManager = LoadingManager.Instance; } } - - private void SetupManagerReferences() + + private async UniTask InitializeManagersAsync() { - if (_initializationManager != null && _managerRegistry != null && _dependencyManager != null) + if (_webSocketManager == null || _sessionManager == null || _httpApiClient == null) { - _initializationManager.Initialize(_managerRegistry, _dependencyManager); + throw new InvalidOperationException("필수 매니저 인스턴스를 찾을 수 없습니다."); } - else + _loadingManager?.BeginLoadingUI(); + _audioManager?.Initialize(); + _webSocketManager.Initialize(); + _sessionManager.Initialize(_webSocketManager); + _httpApiClient.Initialize(_sessionManager); + + bool connected = await _sessionManager.EnsureConnectionAsync(); + if (!connected) { - Debug.LogError("[SystemManager] 필수 매니저가 설정되지 않았습니다."); + throw new InvalidOperationException("세션 연결 실패"); } } } - public interface IManager - { - void Shutdown(); - } + } \ No newline at end of file diff --git a/Assets/Infrastructure/Network/Http/HttpApiClient.cs b/Assets/Infrastructure/Network/Http/HttpApiClient.cs index f7af97d..d9028e9 100644 --- a/Assets/Infrastructure/Network/Http/HttpApiClient.cs +++ b/Assets/Infrastructure/Network/Http/HttpApiClient.cs @@ -9,12 +9,10 @@ using ProjectVG.Infrastructure.Network.DTOs.Chat; using ProjectVG.Infrastructure.Network.Services; using Newtonsoft.Json; -using ProjectVG.Core.Managers; -using ProjectVG.Core.Attributes; namespace ProjectVG.Infrastructure.Network.Http { - public class HttpApiClient : Singleton, IManager + public class HttpApiClient : Singleton { [Header("API Configuration")] @@ -24,14 +22,14 @@ public class HttpApiClient : Singleton, IManager private readonly Dictionary defaultHeaders = new Dictionary(); private CancellationTokenSource cancellationTokenSource; - [Inject] private SessionManager _sessionManager; + private SessionManager _sessionManager; + public bool IsInitialized { get; private set; } #region Unity Lifecycle protected override void Awake() { base.Awake(); - Initialize(); } private void OnDestroy() @@ -43,11 +41,31 @@ private void OnDestroy() #region Public Methods + /// + /// 초기화 실행 + /// + public void Initialize(SessionManager sessionManager) + { + _sessionManager = sessionManager; + cancellationTokenSource?.Cancel(); + cancellationTokenSource?.Dispose(); + cancellationTokenSource = new CancellationTokenSource(); + ApplyNetworkConfig(); + SetupDefaultHeaders(); + IsInitialized = true; + } + + /// + /// 기본 헤더 추가 + /// public void AddDefaultHeader(string key, string value) { defaultHeaders[key] = value; } + /// + /// 인증 토큰 설정 + /// public void SetAuthToken(string token) { AddDefaultHeader(AUTHORIZATION_HEADER, $"{BEARER_PREFIX}{token}"); @@ -82,7 +100,15 @@ public async UniTask DeleteAsync(string endpoint, Dictionary UploadFileAsync(string endpoint, byte[] fileData, string fileName, string fieldName = "file", Dictionary headers = null, CancellationToken cancellationToken = default) { var url = GetFullUrl(endpoint); - return await SendFileRequestAsync(url, fileData, fileName, fieldName, headers, cancellationToken); + var formData = new Dictionary + { + { fieldName, fileData } + }; + var fileNames = new Dictionary + { + { fieldName, fileName } + }; + return await SendFormDataRequestAsync(url, formData, fileNames, headers, cancellationToken); } public async UniTask PostFormDataAsync(string endpoint, Dictionary formData, Dictionary headers = null, CancellationToken cancellationToken = default) @@ -115,22 +141,19 @@ public async UniTask PostFormDataAsync(string endpoint, Dictionary(url, formData, fileNames, headers, cancellationToken); } + /// + /// 종료 처리 + /// public void Shutdown() { cancellationTokenSource?.Cancel(); cancellationTokenSource?.Dispose(); + IsInitialized = false; } #endregion #region Private Methods - - private void Initialize() - { - cancellationTokenSource = new CancellationTokenSource(); - ApplyNetworkConfig(); - SetupDefaultHeaders(); - } private void ApplyNetworkConfig() { @@ -178,9 +201,7 @@ private string SerializeData(object data, bool requiresSession = false) return jsonData; } - private void LogRequestDetails(string method, string url, string jsonData) - { - } + private async UniTask SendJsonRequestAsync(string url, string method, string jsonData, Dictionary headers, CancellationToken cancellationToken) { @@ -217,80 +238,9 @@ private async UniTask SendJsonRequestAsync(string url, string method, stri throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 요청 실패", 0, "최대 재시도 횟수 초과"); } - private async UniTask SendRequestAsync(string url, string method, string jsonData, Dictionary headers, CancellationToken cancellationToken) - { - var combinedCancellationToken = CreateCombinedCancellationToken(cancellationToken); - - for (int attempt = 0; attempt <= NetworkConfig.MaxRetryCount; attempt++) - { - try - { - using var request = CreateRequest(url, method, jsonData, headers); - - var operation = request.SendWebRequest(); - await operation.WithCancellation(combinedCancellationToken); - - if (request.result == UnityWebRequest.Result.Success) - { - return ParseResponse(request); - } - else - { - await HandleRequestFailure(request, attempt, combinedCancellationToken); - } - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (ex is not ApiException) - { - await HandleRequestException(ex, attempt, combinedCancellationToken); - } - } - - throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 요청 실패", 0, "최대 재시도 횟수 초과"); - } - - private async UniTask SendFileRequestAsync(string url, byte[] fileData, string fileName, string fieldName, Dictionary headers, CancellationToken cancellationToken) - { - var combinedCancellationToken = CreateCombinedCancellationToken(cancellationToken); - - for (int attempt = 0; attempt <= NetworkConfig.MaxRetryCount; attempt++) - { - try - { - var form = new WWWForm(); - form.AddBinaryData(fieldName, fileData, fileName); - - using var request = UnityWebRequest.Post(url, form); - SetupRequest(request, headers); - request.timeout = (int)NetworkConfig.HttpTimeout; - - var operation = request.SendWebRequest(); - await operation.WithCancellation(combinedCancellationToken); + - if (request.result == UnityWebRequest.Result.Success) - { - return ParseResponse(request); - } - else - { - await HandleFileUploadFailure(request, attempt, combinedCancellationToken); - } - } - catch (OperationCanceledException) - { - throw; - } - catch (Exception ex) when (ex is not ApiException) - { - await HandleFileUploadException(ex, attempt, combinedCancellationToken); - } - } - - throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, "최대 재시도 횟수 초과"); - } + @@ -407,22 +357,7 @@ private async UniTask HandleFileUploadException(Exception ex, int attempt, Cance throw new ApiException($"{NetworkConfig.MaxRetryCount + 1}번 시도 후 파일 업로드 실패", 0, ex.Message); } - private UnityWebRequest CreateRequest(string url, string method, string jsonData, Dictionary headers) - { - var request = new UnityWebRequest(url, method); - - if (!string.IsNullOrEmpty(jsonData)) - { - var bodyRaw = Encoding.UTF8.GetBytes(jsonData); - request.uploadHandler = new UploadHandlerRaw(bodyRaw); - } - - request.downloadHandler = new DownloadHandlerBuffer(); - SetupRequest(request, headers); - request.timeout = (int)NetworkConfig.HttpTimeout; - - return request; - } + private UnityWebRequest CreateJsonRequest(string url, string method, string jsonData, Dictionary headers) { diff --git a/Assets/Infrastructure/Network/Services/SessionManager.cs b/Assets/Infrastructure/Network/Services/SessionManager.cs index 3d3f598..5963943 100644 --- a/Assets/Infrastructure/Network/Services/SessionManager.cs +++ b/Assets/Infrastructure/Network/Services/SessionManager.cs @@ -2,8 +2,6 @@ using System; using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.WebSocket; -using ProjectVG.Core.Managers; -using ProjectVG.Core.Attributes; namespace ProjectVG.Infrastructure.Network.Services { @@ -11,13 +9,13 @@ namespace ProjectVG.Infrastructure.Network.Services /// 새로운 이벤트 기반 SessionManager /// WebSocketManager의 연결/해제 상태를 모니터링하고 세션 ID를 관리 /// - public class SessionManager : Singleton, IManager + public class SessionManager : Singleton { [Header("Session Info")] [SerializeField] private string _currentSessionId = ""; [SerializeField] private bool _isInitialized = false; - [Inject] private WebSocketManager _webSocketManager; + private WebSocketManager _webSocketManager; // 공개 속성 public string SessionId => _currentSessionId; @@ -38,11 +36,6 @@ protected override void Awake() base.Awake(); } - private void Start() - { - // DI 완료 후 ManagerRegistry에서 Initialize 호출됨 - } - private void OnDestroy() { Shutdown(); @@ -53,7 +46,7 @@ private void OnDestroy() #region Public Methods /// - /// 세션 ID를 요청합니다. 연결되지 않았다면 연결을 시도하고 기다립니다. + /// 세션 ID 요청 /// public async UniTask GetSessionIdAsync() { @@ -78,7 +71,7 @@ public async UniTask GetSessionIdAsync() } /// - /// 세션 연결을 보장합니다. 이미 연결되어 있으면 즉시 반환하고, 그렇지 않으면 연결을 시도합니다. + /// 세션 연결 보장 /// public async UniTask EnsureConnectionAsync() { @@ -101,7 +94,7 @@ public async UniTask EnsureConnectionAsync() } /// - /// 새로운 연결 요청 로직 - 폴링 방식 + /// 연결 요청 /// private async UniTask RequestConnectionAsync() { @@ -146,7 +139,7 @@ private async UniTask RequestConnectionAsync() } /// - /// 세션 연결 완료를 폴링으로 대기 + /// 세션 연결 완료 대기 /// private async UniTask WaitForSessionConnection() { @@ -199,21 +192,24 @@ public void EndSession() #region Private Methods - 초기화 및 이벤트 핸들링 - public void Initialize() + /// + /// 초기화 실행 + /// + public void Initialize(WebSocketManager webSocketManager) { try { + _webSocketManager = webSocketManager; Debug.Log("[SessionManager] 초기화 시작"); - + if (_webSocketManager == null) { - Debug.LogError("[SessionManager] WebSocketManager가 DI로 주입되지 않았습니다."); + Debug.LogError("[SessionManager] WebSocketManager가 null입니다."); return; } - - // 이벤트 구독 + SubscribeToWebSocketEvents(); - + _isInitialized = true; Debug.Log("[SessionManager] 초기화 완료"); } diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs index 372dddc..4b84e17 100644 --- a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -5,16 +5,13 @@ using Cysharp.Threading.Tasks; using ProjectVG.Infrastructure.Network.Configs; using ProjectVG.Infrastructure.Network.DTOs.Chat; -using ProjectVG.Infrastructure.Network.Services; using ProjectVG.Domain.Chat.Model; -using ProjectVG.Core.Managers; -using ProjectVG.Core.Attributes; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace ProjectVG.Infrastructure.Network.WebSocket { - public class WebSocketManager : Singleton, IManager + public class WebSocketManager : Singleton { private INativeWebSocket _nativeWebSocket; private CancellationTokenSource _cancellationTokenSource; @@ -56,7 +53,6 @@ public class WebSocketManager : Singleton, IManager protected override void Awake() { base.Awake(); - Initialize(); } private void OnDestroy() @@ -67,7 +63,24 @@ private void OnDestroy() #endregion #region Public Methods + + /// + /// 웹소켓 매니저 초기화 + /// + public void Initialize() + { + if (_cancellationTokenSource != null) + { + return; + } + _cancellationTokenSource = new CancellationTokenSource(); + InitializeNativeWebSocket(); + StartConnectionMonitoring(); + } + /// + /// 서버와 웹소켓 연결 시도 + /// public async UniTask ConnectAsync(string sessionId = null, CancellationToken cancellationToken = default) { if (_isConnected || _isConnecting) @@ -122,6 +135,9 @@ public async UniTask ConnectAsync(string sessionId = null, CancellationTok } } + /// + /// 웹소켓 연결 해제 + /// public async UniTask DisconnectAsync() { if (!_isConnected) @@ -140,29 +156,25 @@ public async UniTask DisconnectAsync() OnDisconnected?.Invoke(); } - public void SetAutoReconnect(bool enabled) - { - _autoReconnect = enabled; - } - - public void SetReconnectSettings(int maxAttempts, float delay, float maxDelay = 60f, bool useExponentialBackoff = true) - { - _maxReconnectAttempts = maxAttempts; - _reconnectDelay = delay; - _maxReconnectDelay = maxDelay; - _useExponentialBackoff = useExponentialBackoff; - } - + /// + /// 웹소켓 메시지 전송 + /// public async UniTask SendMessageAsync(string type, string data) { throw new NotImplementedException(); } + /// + /// 연결 상태 로깅 + /// public void LogConnectionStatus() { Debug.Log($"[WebSocket] 연결 상태: {(_isConnected ? "연결됨" : "연결안됨")}, 연결 중: {(_isConnecting ? "예" : "아니오")}, 재연결 시도: {_reconnectAttempts}/{_maxReconnectAttempts}"); } + /// + /// 매니저 종료 및 리소스 정리 + /// public void Shutdown() { if (_isShutdown) @@ -187,13 +199,6 @@ public void Shutdown() #region Private Methods - private void Initialize() - { - _cancellationTokenSource = new CancellationTokenSource(); - InitializeNativeWebSocket(); - StartConnectionMonitoring(); - } - private void InitializeNativeWebSocket() { _nativeWebSocket = WebSocketFactory.CreateWebSocket(); From 9d2f222aa988a24f51017c3e0400a490a5ca9e17 Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 12:02:25 +0900 Subject: [PATCH 13/19] =?UTF-8?q?style:=20=EB=AA=85=EC=B9=AD=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20Game=20->=20App=20or=20System?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Game보다는 App이나 System에 가깝다 --- Assets/App/Scenes/MainSence.unity | 2 +- .../Core/DebugConsole/README_DebugConsole.md | 10 ++-- Assets/Core/Loading/LoadingManager.cs | 30 +++++----- Assets/Core/Loading/LoadingUI.cs | 2 +- Assets/Core/Managers/SystemManager.cs | 14 ++--- .../Design/Component_Analysis_Document.md | 4 +- .../Design/Initial_Loading_System_Design.md | 56 +++++++++---------- .../Design/Initial_Startup_System_Design.md | 56 +++++++++---------- 8 files changed, 87 insertions(+), 87 deletions(-) diff --git a/Assets/App/Scenes/MainSence.unity b/Assets/App/Scenes/MainSence.unity index 5d1bde5..bb67bcb 100644 --- a/Assets/App/Scenes/MainSence.unity +++ b/Assets/App/Scenes/MainSence.unity @@ -692,7 +692,7 @@ GameObject: - component: {fileID: 1104447313} - component: {fileID: 1104447314} m_Layer: 0 - m_Name: GameManager + m_Name: SystemManager m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 diff --git a/Assets/Core/DebugConsole/README_DebugConsole.md b/Assets/Core/DebugConsole/README_DebugConsole.md index 62816e9..4008856 100644 --- a/Assets/Core/DebugConsole/README_DebugConsole.md +++ b/Assets/Core/DebugConsole/README_DebugConsole.md @@ -1,7 +1,7 @@ -# 인게임 디버그 콘솔 사용 가이드 +# 인앱 디버그 콘솔 사용 가이드 ## 개요 -인게임에서 Debug.Log 메시지를 실시간으로 확인할 수 있는 관리자 콘솔입니다. +앱 실행 중 Debug.Log 메시지를 실시간으로 확인할 수 있는 관리자 콘솔입니다. ## 주요 기능 - **실시간 로그 표시**: 모든 Debug.Log 메시지를 실시간으로 확인 @@ -41,7 +41,7 @@ LogEntryPrefab (GameObject) ``` ### 3. 컴포넌트 설정 -1. **InGameDebugConsole** 스크립트를 ConsolePanel에 추가 +1. **InAppDebugConsole** 스크립트를 ConsolePanel에 추가 2. **LogEntryPrefab** 스크립트를 로그 엔트리 프리팹에 추가 3. **DebugConsoleSettings** ScriptableObject 생성: - Project 창에서 우클릭 → Create → ProjectVG → Debug Console Settings @@ -103,7 +103,7 @@ LogEntryPrefab (GameObject) ```csharp // 디버그 콘솔 참조 -InGameDebugConsole debugConsole = FindObjectOfType(); +InAppDebugConsole debugConsole = FindObjectOfType(); // 특정 키워드로 필터링 debugConsole.SetFilter("ChatManager"); @@ -151,5 +151,5 @@ debugConsole.ToggleConsole(); 2. 민감한 정보가 로그에 포함되지 않도록 주의 3. 모바일에서는 3개 손가락 동시 터치로 콘솔 제어 (설정에서 변경 가능) 4. 오브젝트 풀링 사용 시 Pool Size를 적절히 설정하여 메모리 사용량 조절 -5. 백그라운드 로깅 사용 시 게임 성능에 미치는 영향을 모니터링 +5. 백그라운드 로깅 사용 시 앱 성능에 미치는 영향을 모니터링 6. 성능이 중요한 경우 `InitializePoolOnStart`를 false로 설정하여 지연 초기화 사용 \ No newline at end of file diff --git a/Assets/Core/Loading/LoadingManager.cs b/Assets/Core/Loading/LoadingManager.cs index d92f6b2..968dda7 100644 --- a/Assets/Core/Loading/LoadingManager.cs +++ b/Assets/Core/Loading/LoadingManager.cs @@ -26,7 +26,7 @@ public TaskInfo(string name, string description, float progressValue) /// /// 로딩 과정을 전담 관리하는 싱글톤 매니저 - /// 실제 초기화는 GameManager가 담당하고, 이 클래스는 UI와 사용자 피드백에 집중 + /// 실제 초기화는 SystemManager가 담당하고, 이 클래스는 UI와 사용자 피드백에 집중 /// public class LoadingManager : Singleton { @@ -34,7 +34,7 @@ public class LoadingManager : Singleton [SerializeField] private LoadingUI _loadingUI; private TaskInfo _currentTask; - private bool _gameStarted; + private bool _appStarted; [SerializeField] private float _autoProgressMax = 0.9f; [SerializeField] private float _autoProgressSpeed = 0.25f; private Coroutine _autoProgressCoroutine; @@ -72,11 +72,11 @@ public void StartInitialization() { UpdateTask("INITIALIZATION", "시스템 초기화 중...", 0.05f); StartAutoProgress(); - SystemManager.Instance.InitializeGame(); + SystemManager.Instance.Initialize(); } else { - Debug.LogError("[LoadingManager] GameManager가 없습니다."); + Debug.LogError("[LoadingManager] SystemManager가 없습니다."); } } @@ -91,17 +91,17 @@ public void UpdateTask(string taskName, string description, float progress) _loadingUI.UpdateTask(_currentTask); } - if (!_gameStarted && _currentTask.progress >= 1f) + if (!_appStarted && _currentTask.progress >= 1f) { - StartGame(); + StartApp(); } } - public async void StartGame() + public async void StartApp() { - if (_gameStarted) + if (_appStarted) return; - _gameStarted = true; + _appStarted = true; if (_loadingUI != null) { await _loadingUI.FadeOut(); @@ -120,7 +120,7 @@ private void SetupEventListeners() { if (SystemManager.Instance != null) { - SystemManager.Instance.OnGameInitialized += OnGameInitialized; + SystemManager.Instance.OnAppInitialized += OnAppInitialized; SystemManager.Instance.OnInitializationError += OnInitializationError; } } @@ -129,18 +129,18 @@ private void RemoveEventListeners() { if (SystemManager.Instance != null) { - SystemManager.Instance.OnGameInitialized -= OnGameInitialized; + SystemManager.Instance.OnAppInitialized -= OnAppInitialized; SystemManager.Instance.OnInitializationError -= OnInitializationError; } } - private void OnGameInitialized() + private void OnAppInitialized() { - if (!_gameStarted) + if (!_appStarted) { StopAutoProgress(); UpdateTask("INITIALIZATION", "완료", 1f); - StartGame(); + StartApp(); } } @@ -172,7 +172,7 @@ private void StopAutoProgress() private System.Collections.IEnumerator AutoProgressRoutine() { float p = Mathf.Clamp01(_currentTask.progress); - while (p < _autoProgressMax && !_gameStarted) + while (p < _autoProgressMax && !_appStarted) { p += Time.deltaTime * _autoProgressSpeed; UpdateTask("INITIALIZATION", "시스템 초기화 중...", Mathf.Min(p, _autoProgressMax)); diff --git a/Assets/Core/Loading/LoadingUI.cs b/Assets/Core/Loading/LoadingUI.cs index aaf4d44..24a0feb 100644 --- a/Assets/Core/Loading/LoadingUI.cs +++ b/Assets/Core/Loading/LoadingUI.cs @@ -111,7 +111,7 @@ private void InitializeUI() } // 초기 상태 메시지 - UpdateStatus("게임 시작 준비 중..."); + UpdateStatus("앱 시작 준비 중..."); } /// diff --git a/Assets/Core/Managers/SystemManager.cs b/Assets/Core/Managers/SystemManager.cs index b2b604b..624e736 100644 --- a/Assets/Core/Managers/SystemManager.cs +++ b/Assets/Core/Managers/SystemManager.cs @@ -34,7 +34,7 @@ public class SystemManager : Singleton public AudioManager AudioManager => _audioManager; public LoadingManager LoadingManager => _loadingManager; - public event Action OnGameInitialized; + public event Action OnAppInitialized; public event Action OnInitializationError; protected override void Awake() @@ -49,7 +49,7 @@ protected override void Awake() if (_autoInitializeOnStart && !_initializationKickoffDone && !IsInitialized) { _initializationKickoffDone = true; - InitializeGame(); + Initialize(); } } @@ -123,7 +123,7 @@ public void SetCamera(Camera camera) } } - public async void InitializeGame() + public async void Initialize() { if (_initializationKickoffDone && IsInitialized) { @@ -138,17 +138,17 @@ public async void InitializeGame() ScreenTapManager.Instance.Initialize(_camera); } - await InitializeGameAsync(); + await InitializeAppAsync(); } - public async UniTask InitializeGameAsync() + public async UniTask InitializeAppAsync() { try { await InitializeManagersAsync(); IsInitialized = true; - Debug.Log("[SystemManager] 게임 시스템 준비 완료"); - OnGameInitialized?.Invoke(); + Debug.Log("[SystemManager] 앱 시스템 준비 완료"); + OnAppInitialized?.Invoke(); } catch (Exception ex) { diff --git a/Assets/Docs/Design/Component_Analysis_Document.md b/Assets/Docs/Design/Component_Analysis_Document.md index e4d7718..482042e 100644 --- a/Assets/Docs/Design/Component_Analysis_Document.md +++ b/Assets/Docs/Design/Component_Analysis_Document.md @@ -265,8 +265,8 @@ smoothing: 1 ## 6. 연관 클래스들 -### 6.1 GameManager -- 게임 전체의 초기화와 관리 +### 6.1 SystemManager +- 앱 전체의 초기화와 관리 - WebSocket, Session, HTTP API 클라이언트 관리 - 시스템 간 의존성 설정 diff --git a/Assets/Docs/Design/Initial_Loading_System_Design.md b/Assets/Docs/Design/Initial_Loading_System_Design.md index b5b9fb8..eb54480 100644 --- a/Assets/Docs/Design/Initial_Loading_System_Design.md +++ b/Assets/Docs/Design/Initial_Loading_System_Design.md @@ -26,29 +26,29 @@ - 로딩 중 상태 표시 - 각 단계별 진행률 표시 -- 준비 완료 시 "게임 시작" 버튼 활성화 +- 준비 완료 시 "앱 시작" 버튼 활성화 - 페이드 인/아웃 효과로 씬 전환 ## 아키텍처 설계 -### 선택된 접근 방식: 이벤트 기반 시스템 (GameManager 중심) +### 선택된 접근 방식: 이벤트 기반 시스템 (SystemManager 중심) **선택 이유:** -- GameManager가 모든 씬에서 지속되는 싱글톤 +- SystemManager가 모든 씬에서 지속되는 싱글톤 - StartupManager는 Start 씬에서만 존재 - 이벤트 구독을 통한 효율적인 상태 관리 -- 기존 GameManager 시스템과의 완벽한 통합 +- 기존 SystemManager 시스템과의 완벽한 통합 ### 시스템 구성요소 ``` -GameManager (DontDestroyOnLoad) +SystemManager (DontDestroyOnLoad) ├── InitializationPhase 이벤트 발생 ├── Progress 이벤트 발생 └── 완료/에러 이벤트 발생 ↓ (이벤트 구독) LoadingManager (Start 씬 전용) -├── GameManager 이벤트 구독 +├── SystemManager 이벤트 구독 ├── LoadingUI 제어 └── SceneTransitionManager 호출 ↓ @@ -58,14 +58,14 @@ SceneTransitionManager (싱글톤) ### 이벤트 기반 동작 흐름 -1. **GameManager**: 초기화 진행하며 이벤트 발생 -2. **LoadingManager**: GameManager 이벤트 구독하여 UI 업데이트 +1. **SystemManager**: 초기화 진행하며 이벤트 발생 +2. **LoadingManager**: SystemManager 이벤트 구독하여 UI 업데이트 3. **LoadingUI**: 진행 상황 표시 및 사용자 상호작용 4. **SceneTransitionManager**: 씬 전환 관리 ## 구현된 클래스 -### 1. GameManager (확장됨) +### 1. SystemManager (확장됨) ```csharp public enum InitializationPhase @@ -77,16 +77,16 @@ public enum InitializationPhase Completed } -public class GameManager : Singleton +public class SystemManager : Singleton { // 이벤트 public event Action OnPhaseChanged; public event Action OnProgressChanged; - public event Action OnGameInitialized; + public event Action OnAppInitialized; public event Action OnInitializationError; // 비동기 초기화 - public async UniTask InitializeGameAsync(); + public async UniTask InitializeAppAsync(); // 상태 조회 public InitializationStatus GetInitializationStatus(); @@ -104,24 +104,24 @@ public class GameManager : Singleton ```csharp public class LoadingManager : MonoBehaviour { - // GameManager 이벤트 구독 - private void SubscribeToGameManager(); + // SystemManager 이벤트 구독 + private void SubscribeToSystemManager(); // 초기화 시작 public async void StartInitialization(); - // 게임 시작 (씬 전환) - public async void StartGame(); + // 앱 시작 (씬 전환) + public async void StartApp(); // 이벤트 핸들러 private void OnPhaseChanged(InitializationPhase phase); private void OnProgressChanged(float progress); - private void OnGameInitialized(); + private void OnAppInitialized(); } ``` **주요 기능:** -- GameManager 이벤트 구독 관리 +- SystemManager 이벤트 구독 관리 - LoadingUI와 연동 - 초기화 완료 시 씬 전환 처리 @@ -176,9 +176,9 @@ public class SceneTransitionManager : Singleton ``` 1. StartScene 로드 -2. LoadingManager 생성 및 GameManager 이벤트 구독 +2. LoadingManager 생성 및 SystemManager 이벤트 구독 3. LoadingManager.StartInitialization() 호출 -4. GameManager.InitializeGameAsync() 실행 +4. SystemManager.InitializeAppAsync() 실행 ├── Phase: InitializingManagers (0% → 40%) ├── Phase: ConnectingToServer (40% → 80%) └── Phase: LoadingResources (80% → 100%) @@ -189,7 +189,7 @@ public class SceneTransitionManager : Singleton ### 2. 이벤트 흐름 ``` -GameManager LoadingManager LoadingUI +SystemManager LoadingManager LoadingUI | | | |──OnPhaseChanged──────────────▶| | | |──UpdatePhase──────────▶| @@ -197,14 +197,14 @@ GameManager LoadingManager LoadingUI |──OnProgressChanged───────────▶| | | |──UpdateProgress───────▶| | | | - |──OnGameInitialized───────────▶| | + |──OnAppInitialized────────────▶| | | |──ShowStartButton──────▶| ``` ## 주요 개선사항 ### 1. 이벤트 기반 아키텍처 -- **분리된 관심사**: GameManager는 초기화, LoadingManager는 UI 관리 +- **분리된 관심사**: SystemManager는 초기화, LoadingManager는 UI 관리 - **유연한 확장**: 새로운 구독자 추가 용이 - **생명주기 독립성**: Start 씬 전용 컴포넌트와 전역 싱글톤 분리 @@ -224,7 +224,7 @@ GameManager LoadingManager LoadingUI Assets/ ├── Core/ │ ├── Managers/ -│ │ └── GameManager.cs (확장됨) +│ │ └── SystemManager.cs (확장됨) │ └── Loading/ │ ├── LoadingManager.cs │ ├── LoadingUI.cs @@ -245,9 +245,9 @@ Assets/ 3. UI 요소들 (ProgressBar, StatusText, StartButton 등) 연결 4. 다음 씬 이름 설정 -### 2. GameManager 설정 +### 2. SystemManager 설정 -- 기존 GameManager 설정 그대로 사용 +- 기존 SystemManager 설정 그대로 사용 - 자동 초기화 옵션 유지 또는 LoadingManager에서 수동 호출 ### 3. 씬 전환 설정 @@ -258,7 +258,7 @@ Assets/ ## 고려사항 ### 1. 성능 최적화 -- GameManager 이벤트는 Start 씬에서만 구독 +- SystemManager 이벤트는 Start 씬에서만 구독 - 메모리 누수 방지를 위한 적절한 구독 해제 - 불필요한 업데이트 최소화 @@ -274,4 +274,4 @@ Assets/ ## 결론 -이벤트 기반 아키텍처를 통해 GameManager의 전역적 특성과 LoadingManager의 씬 전용 특성을 효과적으로 분리했습니다. 이를 통해 유지보수성과 확장성을 높이면서도 사용자에게 명확한 초기화 진행 상황을 제공할 수 있습니다. +이벤트 기반 아키텍처를 통해 SystemManager의 전역적 특성과 LoadingManager의 씬 전용 특성을 효과적으로 분리했습니다. 이를 통해 유지보수성과 확장성을 높이면서도 사용자에게 명확한 초기화 진행 상황을 제공할 수 있습니다. diff --git a/Assets/Docs/Design/Initial_Startup_System_Design.md b/Assets/Docs/Design/Initial_Startup_System_Design.md index b5b9fb8..eb54480 100644 --- a/Assets/Docs/Design/Initial_Startup_System_Design.md +++ b/Assets/Docs/Design/Initial_Startup_System_Design.md @@ -26,29 +26,29 @@ - 로딩 중 상태 표시 - 각 단계별 진행률 표시 -- 준비 완료 시 "게임 시작" 버튼 활성화 +- 준비 완료 시 "앱 시작" 버튼 활성화 - 페이드 인/아웃 효과로 씬 전환 ## 아키텍처 설계 -### 선택된 접근 방식: 이벤트 기반 시스템 (GameManager 중심) +### 선택된 접근 방식: 이벤트 기반 시스템 (SystemManager 중심) **선택 이유:** -- GameManager가 모든 씬에서 지속되는 싱글톤 +- SystemManager가 모든 씬에서 지속되는 싱글톤 - StartupManager는 Start 씬에서만 존재 - 이벤트 구독을 통한 효율적인 상태 관리 -- 기존 GameManager 시스템과의 완벽한 통합 +- 기존 SystemManager 시스템과의 완벽한 통합 ### 시스템 구성요소 ``` -GameManager (DontDestroyOnLoad) +SystemManager (DontDestroyOnLoad) ├── InitializationPhase 이벤트 발생 ├── Progress 이벤트 발생 └── 완료/에러 이벤트 발생 ↓ (이벤트 구독) LoadingManager (Start 씬 전용) -├── GameManager 이벤트 구독 +├── SystemManager 이벤트 구독 ├── LoadingUI 제어 └── SceneTransitionManager 호출 ↓ @@ -58,14 +58,14 @@ SceneTransitionManager (싱글톤) ### 이벤트 기반 동작 흐름 -1. **GameManager**: 초기화 진행하며 이벤트 발생 -2. **LoadingManager**: GameManager 이벤트 구독하여 UI 업데이트 +1. **SystemManager**: 초기화 진행하며 이벤트 발생 +2. **LoadingManager**: SystemManager 이벤트 구독하여 UI 업데이트 3. **LoadingUI**: 진행 상황 표시 및 사용자 상호작용 4. **SceneTransitionManager**: 씬 전환 관리 ## 구현된 클래스 -### 1. GameManager (확장됨) +### 1. SystemManager (확장됨) ```csharp public enum InitializationPhase @@ -77,16 +77,16 @@ public enum InitializationPhase Completed } -public class GameManager : Singleton +public class SystemManager : Singleton { // 이벤트 public event Action OnPhaseChanged; public event Action OnProgressChanged; - public event Action OnGameInitialized; + public event Action OnAppInitialized; public event Action OnInitializationError; // 비동기 초기화 - public async UniTask InitializeGameAsync(); + public async UniTask InitializeAppAsync(); // 상태 조회 public InitializationStatus GetInitializationStatus(); @@ -104,24 +104,24 @@ public class GameManager : Singleton ```csharp public class LoadingManager : MonoBehaviour { - // GameManager 이벤트 구독 - private void SubscribeToGameManager(); + // SystemManager 이벤트 구독 + private void SubscribeToSystemManager(); // 초기화 시작 public async void StartInitialization(); - // 게임 시작 (씬 전환) - public async void StartGame(); + // 앱 시작 (씬 전환) + public async void StartApp(); // 이벤트 핸들러 private void OnPhaseChanged(InitializationPhase phase); private void OnProgressChanged(float progress); - private void OnGameInitialized(); + private void OnAppInitialized(); } ``` **주요 기능:** -- GameManager 이벤트 구독 관리 +- SystemManager 이벤트 구독 관리 - LoadingUI와 연동 - 초기화 완료 시 씬 전환 처리 @@ -176,9 +176,9 @@ public class SceneTransitionManager : Singleton ``` 1. StartScene 로드 -2. LoadingManager 생성 및 GameManager 이벤트 구독 +2. LoadingManager 생성 및 SystemManager 이벤트 구독 3. LoadingManager.StartInitialization() 호출 -4. GameManager.InitializeGameAsync() 실행 +4. SystemManager.InitializeAppAsync() 실행 ├── Phase: InitializingManagers (0% → 40%) ├── Phase: ConnectingToServer (40% → 80%) └── Phase: LoadingResources (80% → 100%) @@ -189,7 +189,7 @@ public class SceneTransitionManager : Singleton ### 2. 이벤트 흐름 ``` -GameManager LoadingManager LoadingUI +SystemManager LoadingManager LoadingUI | | | |──OnPhaseChanged──────────────▶| | | |──UpdatePhase──────────▶| @@ -197,14 +197,14 @@ GameManager LoadingManager LoadingUI |──OnProgressChanged───────────▶| | | |──UpdateProgress───────▶| | | | - |──OnGameInitialized───────────▶| | + |──OnAppInitialized────────────▶| | | |──ShowStartButton──────▶| ``` ## 주요 개선사항 ### 1. 이벤트 기반 아키텍처 -- **분리된 관심사**: GameManager는 초기화, LoadingManager는 UI 관리 +- **분리된 관심사**: SystemManager는 초기화, LoadingManager는 UI 관리 - **유연한 확장**: 새로운 구독자 추가 용이 - **생명주기 독립성**: Start 씬 전용 컴포넌트와 전역 싱글톤 분리 @@ -224,7 +224,7 @@ GameManager LoadingManager LoadingUI Assets/ ├── Core/ │ ├── Managers/ -│ │ └── GameManager.cs (확장됨) +│ │ └── SystemManager.cs (확장됨) │ └── Loading/ │ ├── LoadingManager.cs │ ├── LoadingUI.cs @@ -245,9 +245,9 @@ Assets/ 3. UI 요소들 (ProgressBar, StatusText, StartButton 등) 연결 4. 다음 씬 이름 설정 -### 2. GameManager 설정 +### 2. SystemManager 설정 -- 기존 GameManager 설정 그대로 사용 +- 기존 SystemManager 설정 그대로 사용 - 자동 초기화 옵션 유지 또는 LoadingManager에서 수동 호출 ### 3. 씬 전환 설정 @@ -258,7 +258,7 @@ Assets/ ## 고려사항 ### 1. 성능 최적화 -- GameManager 이벤트는 Start 씬에서만 구독 +- SystemManager 이벤트는 Start 씬에서만 구독 - 메모리 누수 방지를 위한 적절한 구독 해제 - 불필요한 업데이트 최소화 @@ -274,4 +274,4 @@ Assets/ ## 결론 -이벤트 기반 아키텍처를 통해 GameManager의 전역적 특성과 LoadingManager의 씬 전용 특성을 효과적으로 분리했습니다. 이를 통해 유지보수성과 확장성을 높이면서도 사용자에게 명확한 초기화 진행 상황을 제공할 수 있습니다. +이벤트 기반 아키텍처를 통해 SystemManager의 전역적 특성과 LoadingManager의 씬 전용 특성을 효과적으로 분리했습니다. 이를 통해 유지보수성과 확장성을 높이면서도 사용자에게 명확한 초기화 진행 상황을 제공할 수 있습니다. From be91caaffb8cada419aec5896a68e6d8d88d563c Mon Sep 17 00:00:00 2001 From: WooSH Date: Wed, 13 Aug 2025 23:09:06 +0900 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20chat=EA=B3=BC=20Voice=20Manager?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/App/Scenes/MainSence.unity | 397 +++++++++++++++--- Assets/Core/Audio/AudioControllerCore.cs | 78 ++++ Assets/Core/Audio/AudioControllerCore.cs.meta | 2 + Assets/Core/Audio/AudioManager.cs | 329 ++++++++++++++- Assets/Core/Audio/BGMController.cs | 53 +++ Assets/Core/Audio/BGMController.cs.meta | 2 + Assets/Core/Audio/IAudioController.cs | 15 + Assets/Core/Audio/IAudioController.cs.meta | 2 + Assets/Core/Audio/NewAudioMixer.mixer | 209 +++++++++ .../Audio/NewAudioMixer.mixer.meta} | 6 +- Assets/Core/Audio/SFXController.cs | 132 ++++++ Assets/Core/Audio/SFXController.cs.meta | 2 + Assets/Core/Audio/UIController.cs | 123 ++++++ Assets/Core/Audio/UIController.cs.meta | 2 + .../{VoiceManager.cs => VoiceController.cs} | 141 ++++--- Assets/Core/Audio/VoiceController.cs.meta | 2 + Assets/Core/Audio/VoiceManager.cs.meta | 2 - Assets/Core/Managers/SystemManager.cs | 4 + .../Domain/Chat/Service/ChatBubbleManager.cs | 253 ----------- .../Chat/Service/ChatBubbleManager.cs.meta | 2 - Assets/Domain/Chat/Service/ChatManager.cs | 96 +++-- Assets/Domain/Chat/View/ChatBubblePanel.cs | 255 +++++++++++ .../Domain/Chat/View/ChatBubblePanel.cs.meta | 3 + Assets/Domain/Chat/View/ChatBubbleUI.cs | 6 +- .../Core/Managers/SampleSystemManager.cs | 1 + Assets/UI/Prefabs/ChatView.prefab | 23 + Assets/Voice.mixer | 69 +++ Assets/Voice.mixer.meta | 8 + 28 files changed, 1804 insertions(+), 413 deletions(-) create mode 100644 Assets/Core/Audio/AudioControllerCore.cs create mode 100644 Assets/Core/Audio/AudioControllerCore.cs.meta create mode 100644 Assets/Core/Audio/BGMController.cs create mode 100644 Assets/Core/Audio/BGMController.cs.meta create mode 100644 Assets/Core/Audio/IAudioController.cs create mode 100644 Assets/Core/Audio/IAudioController.cs.meta create mode 100644 Assets/Core/Audio/NewAudioMixer.mixer rename Assets/{Art.meta => Core/Audio/NewAudioMixer.mixer.meta} (52%) create mode 100644 Assets/Core/Audio/SFXController.cs create mode 100644 Assets/Core/Audio/SFXController.cs.meta create mode 100644 Assets/Core/Audio/UIController.cs create mode 100644 Assets/Core/Audio/UIController.cs.meta rename Assets/Core/Audio/{VoiceManager.cs => VoiceController.cs} (52%) create mode 100644 Assets/Core/Audio/VoiceController.cs.meta delete mode 100644 Assets/Core/Audio/VoiceManager.cs.meta delete mode 100644 Assets/Domain/Chat/Service/ChatBubbleManager.cs delete mode 100644 Assets/Domain/Chat/Service/ChatBubbleManager.cs.meta create mode 100644 Assets/Domain/Chat/View/ChatBubblePanel.cs create mode 100644 Assets/Domain/Chat/View/ChatBubblePanel.cs.meta create mode 100644 Assets/Voice.mixer create mode 100644 Assets/Voice.mixer.meta diff --git a/Assets/App/Scenes/MainSence.unity b/Assets/App/Scenes/MainSence.unity index bb67bcb..3f2933f 100644 --- a/Assets/App/Scenes/MainSence.unity +++ b/Assets/App/Scenes/MainSence.unity @@ -423,6 +423,53 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 2925110611670761417, guid: 4ad5b82186255a54aafec3328e7a1737, type: 3} m_PrefabInstance: {fileID: 1348947105} m_PrefabAsset: {fileID: 0} +--- !u!1 &416852388 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 416852389} + - component: {fileID: 416852390} + m_Layer: 0 + m_Name: VoiceController + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &416852389 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 416852388} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 622824879} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &416852390 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 416852388} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3032c507d42ef0740bf28ca870b8fb21, type: 3} + m_Name: + m_EditorClassIdentifier: + _audioSource: {fileID: 0} + _audioMixerGroup: {fileID: 0} + _autoPlay: 1 --- !u!114 &577588901 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 2620430970088811223, guid: cd9756d05ff7ef847b3abde3e768a611, type: 3} @@ -434,7 +481,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!1 &829067250 +--- !u!1 &622824876 GameObject: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} @@ -442,38 +489,218 @@ GameObject: m_PrefabAsset: {fileID: 0} serializedVersion: 6 m_Component: - - component: {fileID: 829067253} - - component: {fileID: 829067252} - - component: {fileID: 829067251} + - component: {fileID: 622824879} + - component: {fileID: 622824877} + - component: {fileID: 622824878} m_Layer: 0 - m_Name: ChatManager + m_Name: Audio Manager m_TagString: Untagged m_Icon: {fileID: 0} m_NavMeshLayer: 0 m_StaticEditorFlags: 0 m_IsActive: 1 ---- !u!114 &829067251 +--- !u!114 &622824877 MonoBehaviour: m_ObjectHideFlags: 0 m_CorrespondingSourceObject: {fileID: 0} m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 829067250} + m_GameObject: {fileID: 622824876} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: bebe894334d166c44b4b4e1c755d03ae, type: 3} + m_Script: {fileID: 11500000, guid: 5a710e93b242df6468dad9017ee5bc47, type: 3} m_Name: m_EditorClassIdentifier: - _scrollRect: {fileID: 1636555668} - _gridLayoutGroup: {fileID: 1863899999} - _contentSizeFitter: {fileID: 1863899998} - _chatBubblePrefab: {fileID: 3575337311992857127, guid: 7151716ef12e2424d866e4db41b1f6f9, type: 3} - _bubbleContainer: {fileID: 1863899997} - _enableQueueAnimation: 1 - _queueAnimationDelay: 0.1 - _maxBubbles: 20 - _autoCleanup: 1 - _cleanupThreshold: 15 + _bgmController: {fileID: 709008645} + _voiceController: {fileID: 416852390} + _sfxController: {fileID: 1145687257} + _uiController: {fileID: 920517456} + _audioMixer: {fileID: 24100000, guid: 5661f475a706360419d3419e026775a1, type: 2} + _masterGroup: {fileID: 24300002, guid: 5661f475a706360419d3419e026775a1, type: 2} + _bgmGroup: {fileID: 3421493782693753967, guid: 5661f475a706360419d3419e026775a1, type: 2} + _sfxGroup: {fileID: -7380437828635221995, guid: 5661f475a706360419d3419e026775a1, type: 2} + _uiGroup: {fileID: 4821037570395430284, guid: 5661f475a706360419d3419e026775a1, type: 2} + _voiceGroup: {fileID: 5131122028680581099, guid: 5661f475a706360419d3419e026775a1, type: 2} + _masterVolume: 1 +--- !u!82 &622824878 +AudioSource: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 622824876} + m_Enabled: 1 + serializedVersion: 4 + OutputAudioMixerGroup: {fileID: 0} + m_audioClip: {fileID: 0} + m_Resource: {fileID: 0} + m_PlayOnAwake: 1 + m_Volume: 1 + m_Pitch: 1 + Loop: 0 + Mute: 0 + Spatialize: 0 + SpatializePostEffects: 0 + Priority: 128 + DopplerLevel: 1 + MinDistance: 1 + MaxDistance: 500 + Pan2D: 0 + rolloffMode: 0 + BypassEffects: 0 + BypassListenerEffects: 0 + BypassReverbZones: 0 + rolloffCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + - serializedVersion: 3 + time: 1 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + panLevelCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + spreadCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 0 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 + reverbZoneMixCustomCurve: + serializedVersion: 2 + m_Curve: + - serializedVersion: 3 + time: 0 + value: 1 + inSlope: 0 + outSlope: 0 + tangentMode: 0 + weightedMode: 0 + inWeight: 0.33333334 + outWeight: 0.33333334 + m_PreInfinity: 2 + m_PostInfinity: 2 + m_RotationOrder: 4 +--- !u!4 &622824879 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 622824876} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: + - {fileID: 709008644} + - {fileID: 416852389} + - {fileID: 1145687256} + - {fileID: 920517455} + m_Father: {fileID: 0} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &709008643 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 709008644} + - component: {fileID: 709008645} + m_Layer: 0 + m_Name: BGMController + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &709008644 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 709008643} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 622824879} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &709008645 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 709008643} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 6b0432b7302b2834f916352bcb369cd2, type: 3} + m_Name: + m_EditorClassIdentifier: + _audioSource: {fileID: 0} + _audioMixerGroup: {fileID: 0} +--- !u!1 &829067250 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 829067253} + - component: {fileID: 829067252} + m_Layer: 0 + m_Name: ChatManager + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 --- !u!114 &829067252 MonoBehaviour: m_ObjectHideFlags: 0 @@ -486,9 +713,7 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2b142d4545b9a4f43841c4893e72c04f, type: 3} m_Name: m_EditorClassIdentifier: - _webSocketManager: {fileID: 0} - _voiceManager: {fileID: 0} - _chatBubbleManager: {fileID: 829067251} + _chatBubblePanel: {fileID: 2017944365} _characterId: 44444444-4444-4444-4444-444444444444 _userId: bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb _enableMessageQueue: 1 @@ -508,6 +733,53 @@ Transform: m_Children: [] m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!1 &920517454 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 920517455} + - component: {fileID: 920517456} + m_Layer: 0 + m_Name: UIController + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &920517455 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 920517454} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 622824879} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &920517456 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 920517454} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 547f477965608824f9b820652903230a, type: 3} + m_Name: + m_EditorClassIdentifier: + _audioSource: {fileID: 0} + _audioMixerGroup: {fileID: 0} + _poolSize: 5 --- !u!114 &948060135 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 5682896619326521631, guid: 4e9594e3adccfc04793a3b9f79181ebd, type: 3} @@ -730,6 +1002,8 @@ MonoBehaviour: _sessionManager: {fileID: 0} _httpApiClient: {fileID: 0} _audioManager: {fileID: 0} + _loadingManager: {fileID: 0} + _chatManager: {fileID: 829067252} _autoInitializeOnStart: 1 _createManagersIfNotExist: 1 _autoUpdateCameraOnSceneChange: 1 @@ -839,6 +1113,53 @@ RectTransform: m_AnchoredPosition: {x: 0, y: 0} m_SizeDelta: {x: 0, y: 0} m_Pivot: {x: 0, y: 0} +--- !u!1 &1145687255 +GameObject: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + serializedVersion: 6 + m_Component: + - component: {fileID: 1145687256} + - component: {fileID: 1145687257} + m_Layer: 0 + m_Name: SFXController + m_TagString: Untagged + m_Icon: {fileID: 0} + m_NavMeshLayer: 0 + m_StaticEditorFlags: 0 + m_IsActive: 1 +--- !u!4 &1145687256 +Transform: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1145687255} + serializedVersion: 2 + m_LocalRotation: {x: 0, y: 0, z: 0, w: 1} + m_LocalPosition: {x: 0, y: 0, z: 0} + m_LocalScale: {x: 1, y: 1, z: 1} + m_ConstrainProportionsScale: 0 + m_Children: [] + m_Father: {fileID: 622824879} + m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1145687257 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1145687255} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 54c04a926f0c6b44fa67847d6c530907, type: 3} + m_Name: + m_EditorClassIdentifier: + _audioSource: {fileID: 0} + _audioMixerGroup: {fileID: 0} + _poolSize: 10 --- !u!114 &1273342020 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 8225492411684342768, guid: 4e9594e3adccfc04793a3b9f79181ebd, type: 3} @@ -1097,17 +1418,6 @@ RectTransform: m_CorrespondingSourceObject: {fileID: 1193639241046649271, guid: 290ac1b848f75584f9077b5dd03337f6, type: 3} m_PrefabInstance: {fileID: 1636555666} m_PrefabAsset: {fileID: 0} ---- !u!114 &1636555668 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 900044648925520396, guid: 290ac1b848f75584f9077b5dd03337f6, type: 3} - m_PrefabInstance: {fileID: 1636555666} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 1aa08ab6e0800fa44ae55d278d1423e3, type: 3} - m_Name: - m_EditorClassIdentifier: --- !u!114 &1695405118 stripped MonoBehaviour: m_CorrespondingSourceObject: {fileID: 3254242542015821905, guid: cd9756d05ff7ef847b3abde3e768a611, type: 3} @@ -1119,31 +1429,15 @@ MonoBehaviour: m_Script: {fileID: 11500000, guid: 2da0c512f12947e489f739169773d7ca, type: 3} m_Name: m_EditorClassIdentifier: ---- !u!224 &1863899997 stripped -RectTransform: - m_CorrespondingSourceObject: {fileID: 5196526590166986169, guid: 290ac1b848f75584f9077b5dd03337f6, type: 3} - m_PrefabInstance: {fileID: 1636555666} - m_PrefabAsset: {fileID: 0} ---- !u!114 &1863899998 stripped -MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 2327760123492423530, guid: 290ac1b848f75584f9077b5dd03337f6, type: 3} - m_PrefabInstance: {fileID: 1636555666} - m_PrefabAsset: {fileID: 0} - m_GameObject: {fileID: 0} - m_Enabled: 1 - m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 3245ec927659c4140ac4f8d17403cc18, type: 3} - m_Name: - m_EditorClassIdentifier: ---- !u!114 &1863899999 stripped +--- !u!114 &2017944365 stripped MonoBehaviour: - m_CorrespondingSourceObject: {fileID: 6992329501726019127, guid: 290ac1b848f75584f9077b5dd03337f6, type: 3} + m_CorrespondingSourceObject: {fileID: 7731201516897828228, guid: 290ac1b848f75584f9077b5dd03337f6, type: 3} m_PrefabInstance: {fileID: 1636555666} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 0} m_Enabled: 1 m_EditorHideFlags: 0 - m_Script: {fileID: 11500000, guid: 8a8695521f0d02e499659fee002a26c2, type: 3} + m_Script: {fileID: 11500000, guid: 30700e2214ca700469760eef5b0069ea, type: 3} m_Name: m_EditorClassIdentifier: --- !u!224 &2076648994 stripped @@ -1258,3 +1552,4 @@ SceneRoots: - {fileID: 829067253} - {fileID: 133449156} - {fileID: 332900996} + - {fileID: 622824879} diff --git a/Assets/Core/Audio/AudioControllerCore.cs b/Assets/Core/Audio/AudioControllerCore.cs new file mode 100644 index 0000000..a6c46c4 --- /dev/null +++ b/Assets/Core/Audio/AudioControllerCore.cs @@ -0,0 +1,78 @@ +#nullable enable +using UnityEngine; +using UnityEngine.Audio; + +namespace ProjectVG.Core.Audio +{ + public class AudioControllerCore : MonoBehaviour + { + [SerializeField] protected AudioSource _audioSource; + [SerializeField] protected AudioMixerGroup _audioMixerGroup; + + protected float _volume = 1f; + protected string _volumeParameterName = ""; + + public virtual void Initialize(string volumeParameterName) + { + _volumeParameterName = volumeParameterName; + + // AudioSource가 없으면 자동 생성 + if (_audioSource == null) + { + _audioSource = gameObject.AddComponent(); + _audioSource.playOnAwake = false; + _audioSource.loop = false; + _audioSource.volume = _volume; + } + + // AudioMixerGroup이 설정되지 않은 경우 AudioManager에서 자동 할당 + if (_audioMixerGroup == null) + { + _audioMixerGroup = GetAudioMixerGroupFromManager(); + } + + if (_audioSource != null && _audioMixerGroup != null) + { + _audioSource.outputAudioMixerGroup = _audioMixerGroup; + } + } + + protected virtual AudioMixerGroup? GetAudioMixerGroupFromManager() + { + // 하위 클래스에서 오버라이드하여 적절한 그룹 반환 + return null; + } + + public virtual void SetVolume(float volume) + { + _volume = Mathf.Clamp01(volume); + + if (_audioMixerGroup != null && !string.IsNullOrEmpty(_volumeParameterName)) + { + float dbValue = _volume > 0 ? 20f * Mathf.Log10(_volume) : -80f; + _audioMixerGroup.audioMixer.SetFloat(_volumeParameterName, dbValue); + } + } + + public virtual void Stop() + { + if (_audioSource != null) + { + _audioSource.Stop(); + } + } + + public virtual bool IsPlaying() + { + return _audioSource != null && _audioSource.isPlaying; + } + + public virtual float GetVolume() + { + return _volume; + } + + public AudioSource GetAudioSource() => _audioSource; + public AudioMixerGroup GetAudioMixerGroup() => _audioMixerGroup; + } +} diff --git a/Assets/Core/Audio/AudioControllerCore.cs.meta b/Assets/Core/Audio/AudioControllerCore.cs.meta new file mode 100644 index 0000000..783245d --- /dev/null +++ b/Assets/Core/Audio/AudioControllerCore.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8b7ab8e5a4fd02b448fdc3f34d05ea54 \ No newline at end of file diff --git a/Assets/Core/Audio/AudioManager.cs b/Assets/Core/Audio/AudioManager.cs index eb4c761..1c0ba0c 100644 --- a/Assets/Core/Audio/AudioManager.cs +++ b/Assets/Core/Audio/AudioManager.cs @@ -1,12 +1,327 @@ +#nullable enable +using System; using UnityEngine; using UnityEngine.Audio; -using UnityEngine.Serialization; +using Cysharp.Threading.Tasks; +using ProjectVG.Domain.Chat.Model; -public class AudioManager : Singleton +namespace ProjectVG.Core.Audio { - [SerializeField] private AudioSource voiceSource; - [SerializeField] private AudioSource bgmSource; - [SerializeField] private AudioSource sfxSource; - - public void Initialize() { } + public class AudioManager : Singleton + { + [Header("Audio Controllers")] + [SerializeField] private BGMController? _bgmController; + [SerializeField] private VoiceController? _voiceController; + [SerializeField] private SFXController? _sfxController; + [SerializeField] private UIController? _uiController; + + [Header("Audio Mixer")] + [SerializeField] private AudioMixer? _audioMixer; + + [Header("Audio Mixer Groups")] + [SerializeField] public AudioMixerGroup? _masterGroup; + [SerializeField] public AudioMixerGroup? _bgmGroup; + [SerializeField] public AudioMixerGroup? _sfxGroup; + [SerializeField] public AudioMixerGroup? _uiGroup; + [SerializeField] public AudioMixerGroup? _voiceGroup; + + [Header("Volume Settings")] + [SerializeField] private float _masterVolume = 1f; + + private bool _isInitialized = false; + + public bool IsInitialized => _isInitialized; + public float MasterVolume => _masterVolume; + public float BgmVolume => _bgmController?.GetVolume() ?? 1f; + public float SfxVolume => _sfxController?.GetVolume() ?? 1f; + public float UiVolume => _uiController?.GetVolume() ?? 1f; + public float VoiceVolume => _voiceController?.GetVolume() ?? 1f; + + public event Action? OnMasterVolumeChanged; + public event Action? OnBgmVolumeChanged; + public event Action? OnSfxVolumeChanged; + public event Action? OnUiVolumeChanged; + public event Action? OnVoiceVolumeChanged; + + #region Unity Lifecycle + + protected override void Awake() + { + base.Awake(); + } + + private void Start() + { + Initialize(); + } + + private void OnDestroy() + { + Cleanup(); + } + + #endregion + + #region Public Methods + + public void Initialize() + { + if (_isInitialized) return; + + try + { + InitializeControllers(); + LoadVolumeSettings(); + + _isInitialized = true; + Debug.Log("[AudioManager] 초기화 완료"); + } + catch (Exception ex) + { + Debug.LogError($"[AudioManager] 초기화 실패: {ex.Message}"); + } + } + + public void PlayBGM(AudioClip? clip, bool loop = true) + { + _bgmController?.PlayBGM(clip, loop); + } + + public void StopBGM() + { + _bgmController?.StopBGM(); + } + + public void PlayVoice(AudioClip? clip) + { + _voiceController?.PlayVoice(clip); + } + + public void PlayVoice(VoiceData voiceData) + { + _voiceController?.PlayVoice(voiceData); + } + + public async UniTask PlayVoiceAsync(VoiceData voiceData) + { + if (_voiceController != null) + { + await _voiceController.PlayVoiceAsync(voiceData); + } + } + + public void StopVoice() + { + _voiceController?.StopVoice(); + } + + public void PauseVoice() + { + _voiceController?.PauseVoice(); + } + + public void ResumeVoice() + { + _voiceController?.ResumeVoice(); + } + + public void PlaySFX(AudioClip? clip, Vector3? position = null) + { + _sfxController?.PlaySFX(clip, position); + } + + public void PlayUI(AudioClip? clip) + { + _uiController?.PlayUI(clip); + } + + public void SetMasterVolume(float volume) + { + _masterVolume = Mathf.Clamp01(volume); + + if (_audioMixer != null) + { + float dbValue = _masterVolume > 0 ? 20f * Mathf.Log10(_masterVolume) : -80f; + _audioMixer.SetFloat("MasterVolume", dbValue); + } + + OnMasterVolumeChanged?.Invoke(_masterVolume); + SaveVolumeSettings(); + } + + public void SetBGMVolume(float volume) + { + _bgmController?.SetVolume(volume); + OnBgmVolumeChanged?.Invoke(volume); + SaveVolumeSettings(); + } + + public void SetSFXVolume(float volume) + { + _sfxController?.SetVolume(volume); + OnSfxVolumeChanged?.Invoke(volume); + SaveVolumeSettings(); + } + + public void SetUIVolume(float volume) + { + _uiController?.SetVolume(volume); + OnUiVolumeChanged?.Invoke(volume); + SaveVolumeSettings(); + } + + public void SetVoiceVolume(float volume) + { + _voiceController?.SetVolume(volume); + OnVoiceVolumeChanged?.Invoke(volume); + SaveVolumeSettings(); + } + + public void SaveVolumeSettings() + { + PlayerPrefs.SetFloat("Audio_MasterVolume", _masterVolume); + PlayerPrefs.SetFloat("Audio_BGMVolume", BgmVolume); + PlayerPrefs.SetFloat("Audio_SFXVolume", SfxVolume); + PlayerPrefs.SetFloat("Audio_UIVolume", UiVolume); + PlayerPrefs.SetFloat("Audio_VoiceVolume", VoiceVolume); + PlayerPrefs.Save(); + } + + public void LoadVolumeSettings() + { + _masterVolume = PlayerPrefs.GetFloat("Audio_MasterVolume", 1f); + float bgmVolume = PlayerPrefs.GetFloat("Audio_BGMVolume", 1f); + float sfxVolume = PlayerPrefs.GetFloat("Audio_SFXVolume", 1f); + float uiVolume = PlayerPrefs.GetFloat("Audio_UIVolume", 1f); + float voiceVolume = PlayerPrefs.GetFloat("Audio_VoiceVolume", 1f); + + SetMasterVolume(_masterVolume); + SetBGMVolume(bgmVolume); + SetSFXVolume(sfxVolume); + SetUIVolume(uiVolume); + SetVoiceVolume(voiceVolume); + } + + public void ResetVolumeSettings() + { + SetMasterVolume(1f); + SetBGMVolume(1f); + SetSFXVolume(1f); + SetUIVolume(1f); + SetVoiceVolume(1f); + } + + public void StopAllAudio() + { + _bgmController?.Stop(); + _voiceController?.Stop(); + _sfxController?.Stop(); + _uiController?.Stop(); + } + + public bool IsBGMPlaying() + { + return _bgmController?.IsPlaying() ?? false; + } + + public bool IsVoicePlaying() + { + return _voiceController?.IsPlaying() ?? false; + } + + public int GetActiveSFXCount() + { + // SFXController에서 활성 소스 개수 반환 메서드 추가 필요 + return 0; + } + + public int GetActiveUICount() + { + // UIController에서 활성 소스 개수 반환 메서드 추가 필요 + return 0; + } + + #endregion + + #region Private Methods + + private void InitializeControllers() + { + SetupAudioMixerGroups(); + + _bgmController?.Initialize(); + _voiceController?.Initialize(); + _sfxController?.Initialize(); + _uiController?.Initialize(); + } + + private void SetupAudioMixerGroups() + { + if (_audioMixer == null) + { + Debug.LogWarning("[AudioManager] AudioMixer가 설정되지 않았습니다!"); + return; + } + + // 그룹이 설정되지 않은 경우 동적으로 생성 + if (_masterGroup == null) + _masterGroup = CreateOrFindGroup("Master"); + if (_bgmGroup == null) + _bgmGroup = CreateOrFindGroup("BGM"); + if (_sfxGroup == null) + _sfxGroup = CreateOrFindGroup("SFX"); + if (_uiGroup == null) + _uiGroup = CreateOrFindGroup("UI"); + if (_voiceGroup == null) + _voiceGroup = CreateOrFindGroup("Voice"); + + // 각 컨트롤러에 그룹 할당 + AssignGroupToController(_bgmController, _bgmGroup); + AssignGroupToController(_voiceController, _voiceGroup); + AssignGroupToController(_sfxController, _sfxGroup); + AssignGroupToController(_uiController, _uiGroup); + } + + private AudioMixerGroup? CreateOrFindGroup(string groupName) + { + if (_audioMixer == null) return null; + + // 기존 그룹 찾기 + AudioMixerGroup[] groups = _audioMixer.FindMatchingGroups(groupName); + if (groups.Length > 0) + { + Debug.Log($"[AudioManager] 기존 그룹 사용: {groupName}"); + return groups[0]; + } + + // 그룹이 없으면 생성 (Unity 에디터에서만 가능) + Debug.LogWarning($"[AudioManager] 그룹 '{groupName}'을 찾을 수 없습니다. AudioMixer에서 수동으로 생성해주세요."); + return null; + } + + private void AssignGroupToController(AudioControllerCore? controller, AudioMixerGroup? group) + { + if (controller == null || group == null) return; + + // AudioControllerCore의 _audioMixerGroup 필드에 할당 + var audioSource = controller.GetAudioSource(); + if (audioSource != null) + { + audioSource.outputAudioMixerGroup = group; + Debug.Log($"[AudioManager] {controller.GetType().Name}에 {group.name} 그룹 할당"); + } + } + + private void Cleanup() + { + _bgmController?.Stop(); + _voiceController?.Stop(); + _sfxController?.Stop(); + _uiController?.Stop(); + + Debug.Log("[AudioManager] 정리 완료"); + } + + #endregion + } } diff --git a/Assets/Core/Audio/BGMController.cs b/Assets/Core/Audio/BGMController.cs new file mode 100644 index 0000000..4fec16d --- /dev/null +++ b/Assets/Core/Audio/BGMController.cs @@ -0,0 +1,53 @@ +#nullable enable +using UnityEngine; +using UnityEngine.Audio; + +namespace ProjectVG.Core.Audio +{ + public class BGMController : AudioControllerCore, IAudioController + { + public string GetControllerName() => "BGM"; + + public void Initialize() + { + base.Initialize("BGMVolume"); + } + + protected override AudioMixerGroup? GetAudioMixerGroupFromManager() + { + return AudioManager.Instance._bgmGroup; + } + + public void PlayBGM(AudioClip? clip, bool loop = true) + { + if (_audioSource == null) + { + Debug.LogWarning("[BGMController] AudioSource가 설정되지 않았습니다!"); + return; + } + + if (clip == null) + { + Debug.LogWarning("[BGMController] BGM 클립이 null입니다!"); + return; + } + + _audioSource.clip = clip; + _audioSource.loop = loop; + _audioSource.Play(); + + Debug.Log($"[BGMController] BGM 재생 시작: {clip.name}"); + } + + public void StopBGM() + { + Stop(); + Debug.Log("[BGMController] BGM 재생 중지"); + } + + public AudioClip? GetCurrentClip() + { + return _audioSource?.clip; + } + } +} diff --git a/Assets/Core/Audio/BGMController.cs.meta b/Assets/Core/Audio/BGMController.cs.meta new file mode 100644 index 0000000..b468143 --- /dev/null +++ b/Assets/Core/Audio/BGMController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 6b0432b7302b2834f916352bcb369cd2 \ No newline at end of file diff --git a/Assets/Core/Audio/IAudioController.cs b/Assets/Core/Audio/IAudioController.cs new file mode 100644 index 0000000..ff7274b --- /dev/null +++ b/Assets/Core/Audio/IAudioController.cs @@ -0,0 +1,15 @@ +#nullable enable +using UnityEngine; + +namespace ProjectVG.Core.Audio +{ + public interface IAudioController + { + void Initialize(); + void SetVolume(float volume); + void Stop(); + bool IsPlaying(); + float GetVolume(); + string GetControllerName(); + } +} diff --git a/Assets/Core/Audio/IAudioController.cs.meta b/Assets/Core/Audio/IAudioController.cs.meta new file mode 100644 index 0000000..d72acc5 --- /dev/null +++ b/Assets/Core/Audio/IAudioController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: f638fbd296a39e04ab1e48c5432c5c7c \ No newline at end of file diff --git a/Assets/Core/Audio/NewAudioMixer.mixer b/Assets/Core/Audio/NewAudioMixer.mixer new file mode 100644 index 0000000..d1bfeb0 --- /dev/null +++ b/Assets/Core/Audio/NewAudioMixer.mixer @@ -0,0 +1,209 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!243 &-7380437828635221995 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: SFX + m_AudioMixer: {fileID: 24100000} + m_GroupID: 603178d5b1e127d429609f70b6dfaf79 + m_Children: [] + m_Volume: a14a87239cbc80d4ea3cc25c732ce286 + m_Pitch: 92aa377384123554fbf27fe9a957eeda + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: 4715039332668670441} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!244 &-6908518189966514031 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 1f54a9280deea2a419a46fe74ebc4362 + m_EffectName: Attenuation + m_MixLevel: bc8ed2f555610cf4abeae1663db50a9b + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 +--- !u!244 &-5248543927617172087 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 7bc61aa520f02174f9e4ed6ec4caca15 + m_EffectName: Attenuation + m_MixLevel: 52e4d977f715cbf4f8bf6a64c5e2462a + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 +--- !u!241 &24100000 +AudioMixerController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: NewAudioMixer + m_OutputGroup: {fileID: 0} + m_MasterGroup: {fileID: 24300002} + m_Snapshots: + - {fileID: 24500006} + m_StartSnapshot: {fileID: 24500006} + m_SuspendThreshold: -80 + m_EnableSuspend: 1 + m_UpdateMode: 0 + m_ExposedParameters: [] + m_AudioMixerGroupViews: + - guids: + - 78fb5b14484baf24d8e876ea9cd4e22a + - 1bf7479df396fed488843a11e4f16671 + - 1c2c7c658d2c3e647adee42f03d62f41 + - 603178d5b1e127d429609f70b6dfaf79 + - 323361331de8fbf46b26000797bc1794 + name: View + m_CurrentViewIndex: 0 + m_TargetSnapshot: {fileID: 24500006} +--- !u!243 &24300002 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Master + m_AudioMixer: {fileID: 24100000} + m_GroupID: 78fb5b14484baf24d8e876ea9cd4e22a + m_Children: + - {fileID: 3421493782693753967} + - {fileID: 5131122028680581099} + - {fileID: -7380437828635221995} + - {fileID: 4821037570395430284} + m_Volume: d683a197e89cb63419aa87d5e9ffd201 + m_Pitch: 57058f4e3f8db9b45bf44904130fd94e + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: 24400004} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!244 &24400004 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 5b1fad1651498f64e896a200c3460279 + m_EffectName: Attenuation + m_MixLevel: 5634d4ae45b4e3b46a1238a75adf6559 + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 +--- !u!245 &24500006 +AudioMixerSnapshotController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Snapshot + m_AudioMixer: {fileID: 24100000} + m_SnapshotID: 72350169c21947e4eab158741fc20486 + m_FloatValues: + d683a197e89cb63419aa87d5e9ffd201: 0.13552584 + m_TransitionOverrides: {} +--- !u!243 &3421493782693753967 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: BGM + m_AudioMixer: {fileID: 24100000} + m_GroupID: 1bf7479df396fed488843a11e4f16671 + m_Children: [] + m_Volume: e16781d3daa6c0e45adbff52097c39f1 + m_Pitch: ba3dd39f4f9857f4da2a595243444152 + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: -5248543927617172087} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!244 &4715039332668670441 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 9663680121f2eec4da8b34409c293da5 + m_EffectName: Attenuation + m_MixLevel: 0acb42f468b80d949b814f51086d2244 + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 +--- !u!243 &4821037570395430284 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: UI + m_AudioMixer: {fileID: 24100000} + m_GroupID: 323361331de8fbf46b26000797bc1794 + m_Children: [] + m_Volume: 5b2b28ee812512d479114f20e49b7c3e + m_Pitch: defdbe6e61c7b9742a79170f86807b5f + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: -6908518189966514031} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!243 &5131122028680581099 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Voice + m_AudioMixer: {fileID: 24100000} + m_GroupID: 1c2c7c658d2c3e647adee42f03d62f41 + m_Children: [] + m_Volume: ebec21cb4c0d05b4bae9b518144b691f + m_Pitch: 137240dd54422f64a9ea7cfc5414197c + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: 8918031699990245807} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!244 &8918031699990245807 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 4a3aa06ad866be94dba9d6c66dbb5bde + m_EffectName: Attenuation + m_MixLevel: 837f9927fc6868841a1f2bfd3338aac1 + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 diff --git a/Assets/Art.meta b/Assets/Core/Audio/NewAudioMixer.mixer.meta similarity index 52% rename from Assets/Art.meta rename to Assets/Core/Audio/NewAudioMixer.mixer.meta index a0fc4a5..17f1ffa 100644 --- a/Assets/Art.meta +++ b/Assets/Core/Audio/NewAudioMixer.mixer.meta @@ -1,8 +1,8 @@ fileFormatVersion: 2 -guid: d25bcdd67766fb24691112602b8b8bb8 -folderAsset: yes -DefaultImporter: +guid: 5661f475a706360419d3419e026775a1 +NativeFormatImporter: externalObjects: {} + mainObjectFileID: 24100000 userData: assetBundleName: assetBundleVariant: diff --git a/Assets/Core/Audio/SFXController.cs b/Assets/Core/Audio/SFXController.cs new file mode 100644 index 0000000..ad581ff --- /dev/null +++ b/Assets/Core/Audio/SFXController.cs @@ -0,0 +1,132 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Audio; + +namespace ProjectVG.Core.Audio +{ + public class SFXController : AudioControllerCore, IAudioController + { + [Header("SFX Settings")] + [SerializeField] private int _poolSize = 10; + + private Queue _pool = new Queue(); + private List _activeSources = new List(); + + public string GetControllerName() => "SFX"; + + public void Initialize() + { + base.Initialize("SFXVolume"); + CreatePool(); + } + + protected override AudioMixerGroup? GetAudioMixerGroupFromManager() + { + return AudioManager.Instance._sfxGroup; + } + + public void Stop() + { + foreach (var source in _activeSources.ToArray()) + { + if (source != null) + { + source.Stop(); + ReturnToPool(source); + } + } + } + + public bool IsPlaying() + { + return _activeSources.Count > 0; + } + + public void PlaySFX(AudioClip? clip, Vector3? position = null) + { + if (clip == null) + { + Debug.LogWarning("[SFXController] SFX 클립이 null입니다!"); + return; + } + + AudioSource? audioSource = GetPooledSource(); + if (audioSource == null) + { + Debug.LogWarning("[SFXController] 사용 가능한 AudioSource가 없습니다!"); + return; + } + + audioSource.clip = clip; + audioSource.loop = false; + + if (position.HasValue) + { + audioSource.transform.position = position.Value; + audioSource.spatialBlend = 1f; + } + else + { + audioSource.spatialBlend = 0f; + } + + audioSource.Play(); + + StartCoroutine(ReturnToPoolWhenFinished(audioSource)); + + Debug.Log($"[SFXController] SFX 재생: {clip.name}"); + } + + private void CreatePool() + { + for (int i = 0; i < _poolSize; i++) + { + GameObject poolObject = new GameObject($"PooledSFXSource_{i}"); + poolObject.transform.SetParent(transform); + + AudioSource audioSource = poolObject.AddComponent(); + audioSource.playOnAwake = false; + audioSource.outputAudioMixerGroup = GetAudioMixerGroupFromManager(); + + _pool.Enqueue(audioSource); + } + + Debug.Log($"[SFXController] SFX AudioSource 풀 생성 완료: {_poolSize}개"); + } + + private AudioSource? GetPooledSource() + { + if (_pool.Count > 0) + { + AudioSource audioSource = _pool.Dequeue(); + _activeSources.Add(audioSource); + return audioSource; + } + + return null; + } + + private void ReturnToPool(AudioSource audioSource) + { + audioSource.Stop(); + audioSource.clip = null; + audioSource.volume = 1f; + audioSource.pitch = 1f; + + _activeSources.Remove(audioSource); + _pool.Enqueue(audioSource); + } + + private IEnumerator ReturnToPoolWhenFinished(AudioSource audioSource) + { + while (audioSource.isPlaying) + { + yield return null; + } + + ReturnToPool(audioSource); + } + } +} diff --git a/Assets/Core/Audio/SFXController.cs.meta b/Assets/Core/Audio/SFXController.cs.meta new file mode 100644 index 0000000..80ae809 --- /dev/null +++ b/Assets/Core/Audio/SFXController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 54c04a926f0c6b44fa67847d6c530907 \ No newline at end of file diff --git a/Assets/Core/Audio/UIController.cs b/Assets/Core/Audio/UIController.cs new file mode 100644 index 0000000..3b4b9d2 --- /dev/null +++ b/Assets/Core/Audio/UIController.cs @@ -0,0 +1,123 @@ +#nullable enable +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Audio; + +namespace ProjectVG.Core.Audio +{ + public class UIController : AudioControllerCore, IAudioController + { + [Header("UI Settings")] + [SerializeField] private int _poolSize = 5; + + private Queue _pool = new Queue(); + private List _activeSources = new List(); + + public string GetControllerName() => "UI"; + + public void Initialize() + { + base.Initialize("UIVolume"); + CreatePool(); + } + + protected override AudioMixerGroup? GetAudioMixerGroupFromManager() + { + return AudioManager.Instance._uiGroup; + } + + public void Stop() + { + foreach (var source in _activeSources.ToArray()) + { + if (source != null) + { + source.Stop(); + ReturnToPool(source); + } + } + } + + public bool IsPlaying() + { + return _activeSources.Count > 0; + } + + public void PlayUI(AudioClip? clip) + { + if (clip == null) + { + Debug.LogWarning("[UIController] UI 클립이 null입니다!"); + return; + } + + AudioSource? audioSource = GetPooledSource(); + if (audioSource == null) + { + Debug.LogWarning("[UIController] 사용 가능한 AudioSource가 없습니다!"); + return; + } + + audioSource.clip = clip; + audioSource.loop = false; + audioSource.spatialBlend = 0f; + + audioSource.Play(); + + StartCoroutine(ReturnToPoolWhenFinished(audioSource)); + + Debug.Log($"[UIController] UI 효과음 재생: {clip.name}"); + } + + private void CreatePool() + { + for (int i = 0; i < _poolSize; i++) + { + GameObject poolObject = new GameObject($"PooledUISource_{i}"); + poolObject.transform.SetParent(transform); + + AudioSource audioSource = poolObject.AddComponent(); + audioSource.playOnAwake = false; + audioSource.outputAudioMixerGroup = GetAudioMixerGroupFromManager(); + + _pool.Enqueue(audioSource); + } + + Debug.Log($"[UIController] UI AudioSource 풀 생성 완료: {_poolSize}개"); + } + + private AudioSource? GetPooledSource() + { + if (_pool.Count > 0) + { + AudioSource audioSource = _pool.Dequeue(); + _activeSources.Add(audioSource); + return audioSource; + } + + return null; + } + + private void ReturnToPool(AudioSource audioSource) + { + audioSource.Stop(); + audioSource.clip = null; + audioSource.volume = 1f; + audioSource.pitch = 1f; + + _activeSources.Remove(audioSource); + _pool.Enqueue(audioSource); + } + + private IEnumerator ReturnToPoolWhenFinished(AudioSource audioSource) + { + while (audioSource.isPlaying) + { + yield return null; + } + + ReturnToPool(audioSource); + } + } +} diff --git a/Assets/Core/Audio/UIController.cs.meta b/Assets/Core/Audio/UIController.cs.meta new file mode 100644 index 0000000..839f71c --- /dev/null +++ b/Assets/Core/Audio/UIController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 547f477965608824f9b820652903230a \ No newline at end of file diff --git a/Assets/Core/Audio/VoiceManager.cs b/Assets/Core/Audio/VoiceController.cs similarity index 52% rename from Assets/Core/Audio/VoiceManager.cs rename to Assets/Core/Audio/VoiceController.cs index 087c8ae..a4ab009 100644 --- a/Assets/Core/Audio/VoiceManager.cs +++ b/Assets/Core/Audio/VoiceController.cs @@ -1,26 +1,23 @@ #nullable enable using System; using UnityEngine; +using UnityEngine.Audio; using ProjectVG.Domain.Chat.Model; using Cysharp.Threading.Tasks; namespace ProjectVG.Core.Audio { - public class VoiceManager : Singleton + public class VoiceController : AudioControllerCore, IAudioController { - [Header("Voice Audio Source")] - [SerializeField] private AudioSource _voiceSource; - [Header("Voice Settings")] - [SerializeField] private float _volume = 1.0f; [SerializeField] private bool _autoPlay = true; private VoiceData? _currentVoice; private bool _isPlaying = false; - public bool IsPlaying => _isPlaying; - public float Volume => _volume; + public string GetControllerName() => "Voice"; public VoiceData? CurrentVoice => _currentVoice; + public bool AutoPlay => _autoPlay; public event Action? OnVoiceFinished; public event Action? OnVoiceStarted; @@ -28,15 +25,9 @@ public class VoiceManager : Singleton #region Unity Lifecycle - protected override void Awake() - { - base.Awake(); - Initialize(); - } - private void Update() { - if (_isPlaying && !_voiceSource.isPlaying && _voiceSource.clip != null) + if (_isPlaying && !_audioSource.isPlaying && _audioSource.clip != null) { _isPlaying = false; OnVoiceFinished?.Invoke(); @@ -50,13 +41,71 @@ private void OnDestroy() #endregion + #region IAudioController Implementation + + public void Initialize() + { + base.Initialize("VoiceVolume"); + SetVolume(_volume); + } + + protected override AudioMixerGroup? GetAudioMixerGroupFromManager() + { + return AudioManager.Instance._voiceGroup; + } + + public override void SetVolume(float volume) + { + base.SetVolume(volume); + + if (_audioSource != null) + { + _audioSource.volume = _volume; + } + } + + public override void Stop() + { + StopVoice(); + } + + public override bool IsPlaying() + { + return _isPlaying; + } + + #endregion + #region Public Methods + public void PlayVoice(AudioClip? clip) + { + if (_audioSource == null) + { + Debug.LogWarning("[VoiceController] AudioSource가 설정되지 않았습니다!"); + return; + } + + if (clip == null) + { + Debug.LogWarning("[VoiceController] Voice 클립이 null입니다!"); + return; + } + + PrepareAudioSource(); + + _audioSource.clip = clip; + _audioSource.Play(); + _isPlaying = true; + + Debug.Log($"[VoiceController] Voice 재생 시작: {clip.name}"); + } + public async void PlayVoice(VoiceData voiceData) { if (voiceData == null || !voiceData.IsPlayable()) { - Debug.LogWarning("[VoiceManager] 재생할 수 있는 VoiceData가 없습니다."); + Debug.LogWarning("[VoiceController] 재생할 수 있는 VoiceData가 없습니다."); return; } @@ -65,12 +114,12 @@ public async void PlayVoice(VoiceData voiceData) await UniTask.Delay(50); _currentVoice = voiceData; - _voiceSource.clip = voiceData.AudioClip; - _voiceSource.volume = _volume; + _audioSource.clip = voiceData.AudioClip; + _audioSource.volume = _volume; if (_autoPlay) { - _voiceSource.Play(); + _audioSource.Play(); _isPlaying = true; OnVoiceStarted?.Invoke(voiceData); } @@ -80,7 +129,7 @@ public async UniTask PlayVoiceAsync(VoiceData voiceData) { if (voiceData == null || !voiceData.IsPlayable()) { - Debug.LogWarning("[VoiceManager] 재생할 수 있는 VoiceData가 없습니다."); + Debug.LogWarning("[VoiceController] 재생할 수 있는 VoiceData가 없습니다."); return; } @@ -89,12 +138,12 @@ public async UniTask PlayVoiceAsync(VoiceData voiceData) await UniTask.Delay(50); _currentVoice = voiceData; - _voiceSource.clip = voiceData.AudioClip; - _voiceSource.volume = _volume; + _audioSource.clip = voiceData.AudioClip; + _audioSource.volume = _volume; if (_autoPlay) { - _voiceSource.Play(); + _audioSource.Play(); _isPlaying = true; OnVoiceStarted?.Invoke(voiceData); @@ -104,9 +153,9 @@ public async UniTask PlayVoiceAsync(VoiceData voiceData) public void StopVoice() { - if (_voiceSource.isPlaying) + if (_audioSource != null && _audioSource.isPlaying) { - _voiceSource.Stop(); + _audioSource.Stop(); _isPlaying = false; OnVoiceStopped?.Invoke(); } @@ -114,65 +163,47 @@ public void StopVoice() public void PauseVoice() { - if (_voiceSource.isPlaying) + if (_audioSource != null && _audioSource.isPlaying) { - _voiceSource.Pause(); + _audioSource.Pause(); _isPlaying = false; } } public void ResumeVoice() { - if (_voiceSource.clip != null && !_voiceSource.isPlaying) + if (_audioSource != null && _audioSource.clip != null && !_audioSource.isPlaying) { - _voiceSource.UnPause(); + _audioSource.UnPause(); _isPlaying = true; } } - public void SetVolume(float volume) + public void SetAutoPlay(bool autoPlay) { - _volume = Mathf.Clamp01(volume); - - if (_voiceSource != null) - { - _voiceSource.volume = _volume; - } + _autoPlay = autoPlay; } - public void SetAutoPlay(bool autoPlay) + public AudioClip? GetCurrentClip() { - _autoPlay = autoPlay; + return _audioSource?.clip; } #endregion #region Private Methods - private void Initialize() - { - if (_voiceSource == null) - { - _voiceSource = gameObject.AddComponent(); - _voiceSource.playOnAwake = false; - _voiceSource.loop = false; - _voiceSource.volume = _volume; - } - - SetVolume(_volume); - } - private void PrepareAudioSource() { - if (_voiceSource == null) return; + if (_audioSource == null) return; - if (_voiceSource.isPlaying) + if (_audioSource.isPlaying) { - _voiceSource.Stop(); + _audioSource.Stop(); } - _voiceSource.volume = _volume; - _voiceSource.clip = null; + _audioSource.volume = _volume; + _audioSource.clip = null; } #endregion diff --git a/Assets/Core/Audio/VoiceController.cs.meta b/Assets/Core/Audio/VoiceController.cs.meta new file mode 100644 index 0000000..b294186 --- /dev/null +++ b/Assets/Core/Audio/VoiceController.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3032c507d42ef0740bf28ca870b8fb21 \ No newline at end of file diff --git a/Assets/Core/Audio/VoiceManager.cs.meta b/Assets/Core/Audio/VoiceManager.cs.meta deleted file mode 100644 index 759350d..0000000 --- a/Assets/Core/Audio/VoiceManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 6c790fc11dc6c5c4eaa90ee37fd0f53b \ No newline at end of file diff --git a/Assets/Core/Managers/SystemManager.cs b/Assets/Core/Managers/SystemManager.cs index 624e736..f7a2301 100644 --- a/Assets/Core/Managers/SystemManager.cs +++ b/Assets/Core/Managers/SystemManager.cs @@ -1,3 +1,4 @@ +#nullable enable using UnityEngine; using System; using ProjectVG.Infrastructure.Network.WebSocket; @@ -5,6 +6,8 @@ using ProjectVG.Infrastructure.Network.Http; using Cysharp.Threading.Tasks; using ProjectVG.Core.Loading; +using ProjectVG.Core.Audio; +using ProjectVG.Domain.Chat.Service; namespace ProjectVG.Core.Managers { @@ -16,6 +19,7 @@ public class SystemManager : Singleton [SerializeField] private HttpApiClient _httpApiClient; [SerializeField] private AudioManager _audioManager; [SerializeField] private LoadingManager _loadingManager; + [SerializeField] private ChatManager? _chatManager; [Header("Settings")] diff --git a/Assets/Domain/Chat/Service/ChatBubbleManager.cs b/Assets/Domain/Chat/Service/ChatBubbleManager.cs deleted file mode 100644 index a5bdded..0000000 --- a/Assets/Domain/Chat/Service/ChatBubbleManager.cs +++ /dev/null @@ -1,253 +0,0 @@ -#nullable enable -using System; -using System.Collections.Generic; -using UnityEngine; -using UnityEngine.UI; -using ProjectVG.Domain.Chat.Model; -using ProjectVG.Domain.Chat.View; - -namespace ProjectVG.Domain.Chat.Service -{ - public class ChatBubbleManager : MonoBehaviour - { - [Header("UI Components")] - [SerializeField] private ScrollRect? _scrollRect; - [SerializeField] private GridLayoutGroup? _gridLayoutGroup; - [SerializeField] private ContentSizeFitter? _contentSizeFitter; - - [Header("Bubble Settings")] - [SerializeField] private GameObject? _chatBubblePrefab; - [SerializeField] private Transform? _bubbleContainer; - - [Header("Queue Animation Settings")] - [SerializeField] private bool _enableQueueAnimation = true; - [SerializeField] private float _queueAnimationDelay = 0.1f; - - [Header("Performance Settings")] - [SerializeField] private int _maxBubbles = 20; - [SerializeField] private bool _autoCleanup = true; - [SerializeField] private int _cleanupThreshold = 15; - - private List _activeBubbles = new List(); - - public int ActiveBubbleCount => _activeBubbles.Count; - - public event Action? OnBubbleCreated; - public event Action? OnBubbleDestroyed; - public event Action? OnAllBubblesCleared; - - #region Unity Lifecycle - - private void Awake() - { - Initialize(); - } - - private void OnDestroy() - { - ClearAllBubbles(); - } - - #endregion - - #region Public Methods - - public void CreateBubble(Actor actor, string text, float displayTime = -1f) - { - if (_chatBubblePrefab == null || _bubbleContainer == null) - { - Debug.LogError("[ChatBubbleManager] ChatBubblePrefab 또는 BubbleContainer가 설정되지 않았습니다!"); - return; - } - - try - { - GameObject? bubbleObject = Instantiate(_chatBubblePrefab, _bubbleContainer); - ChatBubbleUI? bubbleUI = bubbleObject?.GetComponent(); - - if (bubbleUI == null) - { - Debug.LogError("[ChatBubbleManager] ChatBubbleUI 컴포넌트를 찾을 수 없습니다!"); - if (bubbleObject != null) - Destroy(bubbleObject); - return; - } - - SetupCanvasGroup(bubbleObject); - - if (_activeBubbles.Count >= _maxBubbles) - { - Debug.LogWarning($"[ChatBubbleManager] 최대 버블 수({_maxBubbles})에 도달했습니다. 가장 오래된 버블을 제거합니다."); - RemoveOldestBubble(); - } - - if (_autoCleanup && _activeBubbles.Count >= _cleanupThreshold) - { - CleanupOldBubbles(); - } - - if (_enableQueueAnimation && _activeBubbles.Count > 0) - { - StartQueueAnimationForExistingBubbles(); - } - - bubbleUI.Initialize(actor, text, displayTime, this); - - bubbleUI.OnBubbleDestroyed += OnBubbleDestroyed; - bubbleUI.OnToastAnimationComplete += OnBubbleToastAnimationComplete; - - _activeBubbles.Add(bubbleUI); - - OnBubbleCreated?.Invoke(bubbleUI); - } - catch (Exception ex) - { - Debug.LogError($"[ChatBubbleManager] 버블 생성 실패: {ex.Message}"); - } - } - - public void RemoveBubble(ChatBubbleUI? bubble) - { - if (bubble == null) return; - - try - { - bubble.OnBubbleDestroyed -= OnBubbleDestroyed; - bubble.OnToastAnimationComplete -= OnBubbleToastAnimationComplete; - - _activeBubbles.Remove(bubble); - - OnBubbleDestroyed?.Invoke(bubble); - } - catch (Exception ex) - { - Debug.LogError($"[ChatBubbleManager] 버블 제거 실패: {ex.Message}"); - } - } - - public void ClearAllBubbles() - { - try - { - foreach (var bubble in _activeBubbles.ToArray()) - { - if (bubble != null) - { - bubble.OnBubbleDestroyed -= OnBubbleDestroyed; - bubble.OnToastAnimationComplete -= OnBubbleToastAnimationComplete; - Destroy(bubble.gameObject); - } - } - - _activeBubbles.Clear(); - - OnAllBubblesCleared?.Invoke(); - } - catch (Exception ex) - { - Debug.LogError($"[ChatBubbleManager] 모든 버블 제거 실패: {ex.Message}"); - } - } - - #endregion - - #region Private Methods - - private void Initialize() - { - if (_bubbleContainer == null) - { - Debug.LogError("[ChatBubbleManager] BubbleContainer가 설정되지 않았습니다! 인스펙터에서 설정해주세요."); - return; - } - - if (_chatBubblePrefab == null) - { - Debug.LogError("[ChatBubbleManager] ChatBubblePrefab이 설정되지 않았습니다!"); - return; - } - - Debug.Log("[ChatBubbleManager] 초기화 완료"); - } - - private void StartQueueAnimationForExistingBubbles() - { - Canvas.ForceUpdateCanvases(); - - for (int i = 0; i < _activeBubbles.Count; i++) - { - var bubble = _activeBubbles[i]; - if (bubble != null && bubble.IsToastAnimationComplete) - { - float delay = i * _queueAnimationDelay; - StartCoroutine(QueueAnimationWithDelay(bubble, delay)); - } - } - } - - private System.Collections.IEnumerator QueueAnimationWithDelay(ChatBubbleUI? bubble, float delay) - { - yield return new WaitForSeconds(delay); - - if (bubble != null) - { - bubble.StartQueueSlideAnimation(); - } - } - - private void OnBubbleToastAnimationComplete(ChatBubbleUI? bubble) - { - if (bubble == null) return; - - - if (_scrollRect != null) - { - Canvas.ForceUpdateCanvases(); - _scrollRect.verticalNormalizedPosition = 0f; - } - } - - private void RemoveOldestBubble() - { - if (_activeBubbles.Count > 0) - { - var oldestBubble = _activeBubbles[0]; - RemoveBubble(oldestBubble); - if (oldestBubble != null) - { - Destroy(oldestBubble.gameObject); - } - } - } - - private void CleanupOldBubbles() - { - int bubblesToRemove = _activeBubbles.Count - _cleanupThreshold; - for (int i = 0; i < bubblesToRemove && i < _activeBubbles.Count; i++) - { - var bubble = _activeBubbles[0]; - RemoveBubble(bubble); - if (bubble != null) - { - Destroy(bubble.gameObject); - } - } - } - - private void SetupCanvasGroup(GameObject? bubbleObject) - { - if (bubbleObject == null) return; - - CanvasGroup? canvasGroup = bubbleObject.GetComponent(); - if (canvasGroup == null) - { - canvasGroup = bubbleObject.AddComponent(); - Debug.Log($"[ChatBubbleManager] ChatBubble에 CanvasGroup이 자동으로 추가되었습니다: {bubbleObject.name}"); - } - - canvasGroup.alpha = 0f; - } - - #endregion - } -} \ No newline at end of file diff --git a/Assets/Domain/Chat/Service/ChatBubbleManager.cs.meta b/Assets/Domain/Chat/Service/ChatBubbleManager.cs.meta deleted file mode 100644 index 0a7b58b..0000000 --- a/Assets/Domain/Chat/Service/ChatBubbleManager.cs.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: bebe894334d166c44b4b4e1c755d03ae \ No newline at end of file diff --git a/Assets/Domain/Chat/Service/ChatManager.cs b/Assets/Domain/Chat/Service/ChatManager.cs index 172f287..e7bfc9a 100644 --- a/Assets/Domain/Chat/Service/ChatManager.cs +++ b/Assets/Domain/Chat/Service/ChatManager.cs @@ -9,15 +9,16 @@ using ProjectVG.Infrastructure.Network.Services; using ProjectVG.Infrastructure.Network.DTOs.Chat; using ProjectVG.Domain.Chat.Service; +using ProjectVG.Domain.Chat.View; +using ProjectVG.Core.Audio; namespace ProjectVG.Domain.Chat.Service { - public class ChatManager : Singleton + public class ChatManager : MonoBehaviour { [Header("Components")] - [SerializeField] private WebSocketManager _webSocketManager; - [SerializeField] private VoiceManager _voiceManager; - [SerializeField] private ChatBubbleManager _chatBubbleManager; + [SerializeField] private ChatBubblePanel? _chatBubblePanel; + [Header("Chat Settings")] [SerializeField] private string _characterId = "44444444-4444-4444-4444-444444444444"; @@ -31,6 +32,9 @@ public class ChatManager : Singleton private bool _isInitialized = false; private bool _isProcessing = false; + private WebSocketManager? _webSocketManager; + private AudioManager? _audioManager; + private readonly Queue _messageQueue = new Queue(); private readonly object _queueLock = new object(); @@ -43,13 +47,44 @@ public class ChatManager : Singleton #region Unity Lifecycle - protected override void Awake() + private static ChatManager? _instance; + public static ChatManager Instance { - base.Awake(); + get + { + if (_instance == null) + { + _instance = FindAnyObjectByType(); + } + return _instance; + } + } + + private void Awake() + { + if (_instance != null && _instance != this) + { + Destroy(gameObject); + return; + } + + _instance = this; } private void Start() { + // UI 컴포넌트들이 모두 생성된 후 초기화 + StartCoroutine(InitializeWhenReady()); + } + + private System.Collections.IEnumerator InitializeWhenReady() + { + // ChatBubblePanel이 준비될 때까지 대기 + while (_chatBubblePanel == null) + { + yield return new WaitForEndOfFrame(); + } + Initialize(); } @@ -59,11 +94,6 @@ private void OnDestroy() { _webSocketManager.OnChatMessageReceived -= HandleChatMessageReceived; } - - if (_voiceManager != null) - { - _voiceManager.OnVoiceFinished -= OnVoiceFinished; - } } #endregion @@ -77,25 +107,15 @@ public void Initialize() try { - if (_webSocketManager == null) - _webSocketManager = WebSocketManager.Instance; - - if (_voiceManager == null) - _voiceManager = VoiceManager.Instance; - - if (_chatBubbleManager == null) - _chatBubbleManager = FindObjectOfType(); + // 기본 매니저들 초기화 + _webSocketManager = WebSocketManager.Instance; + _audioManager = AudioManager.Instance; if (_webSocketManager != null) { _webSocketManager.OnChatMessageReceived += HandleChatMessageReceived; } - if (_voiceManager != null) - { - _voiceManager.OnVoiceFinished += OnVoiceFinished; - } - _isInitialized = true; _isConnected = true; Debug.Log("[ChatManager] 초기화 완료"); @@ -114,9 +134,9 @@ public async void SendUserMessage(string message) try { - if (_chatBubbleManager != null) + if (_chatBubblePanel != null) { - _chatBubbleManager.CreateBubble(Actor.User, message); + _chatBubblePanel.CreateBubble(Actor.User, message); } var chatService = ApiServiceManager.Instance.Chat; @@ -174,6 +194,10 @@ public void ClearMessageQueue() } } + + + + #endregion #region Private Methods @@ -224,14 +248,14 @@ private void ProcessMessageImmediately(ChatMessage chatMessage) OnChatMessageReceived?.Invoke(chatMessage); // 캐릭터 메시지를 버블로 표시 - if (_chatBubbleManager != null && !string.IsNullOrEmpty(chatMessage.Text)) + if (_chatBubblePanel != null && !string.IsNullOrEmpty(chatMessage.Text)) { - _chatBubbleManager.CreateBubble(Actor.Character, chatMessage.Text); + _chatBubblePanel.CreateBubble(Actor.Character, chatMessage.Text); } - if (chatMessage.VoiceData != null && _voiceManager != null) + if (chatMessage.VoiceData != null && _audioManager != null) { - _voiceManager.PlayVoice(chatMessage.VoiceData); + _audioManager.PlayVoice(chatMessage.VoiceData); } } catch (Exception ex) @@ -247,14 +271,14 @@ private async UniTask ProcessMessageImmediatelyAsync(ChatMessage chatMessage) { OnChatMessageReceived?.Invoke(chatMessage); - if (_chatBubbleManager != null && !string.IsNullOrEmpty(chatMessage.Text)) + if (_chatBubblePanel != null && !string.IsNullOrEmpty(chatMessage.Text)) { - _chatBubbleManager.CreateBubble(Actor.Character, chatMessage.Text); + _chatBubblePanel.CreateBubble(Actor.Character, chatMessage.Text); } - if (chatMessage.VoiceData != null && _voiceManager != null) + if (chatMessage.VoiceData != null && _audioManager != null) { - await _voiceManager.PlayVoiceAsync(chatMessage.VoiceData); + await _audioManager.PlayVoiceAsync(chatMessage.VoiceData); } } catch (Exception ex) @@ -281,9 +305,7 @@ private bool ValidateUserInput(string message) return true; } - private void OnVoiceFinished() - { - } + private void HandleChatMessageReceived(ChatMessage chatMessage) { diff --git a/Assets/Domain/Chat/View/ChatBubblePanel.cs b/Assets/Domain/Chat/View/ChatBubblePanel.cs new file mode 100644 index 0000000..66fc3af --- /dev/null +++ b/Assets/Domain/Chat/View/ChatBubblePanel.cs @@ -0,0 +1,255 @@ +#nullable enable +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.UI; +using ProjectVG.Domain.Chat.Model; +using ProjectVG.Domain.Chat.View; + +namespace ProjectVG.Domain.Chat.View +{ + public class ChatBubblePanel : MonoBehaviour + { + [Header("UI Components")] + [SerializeField] private ScrollRect? _scrollRect; + [SerializeField] private GridLayoutGroup? _gridLayoutGroup; + [SerializeField] private ContentSizeFitter? _contentSizeFitter; + + [Header("Bubble Settings")] + [SerializeField] private GameObject? _chatBubblePrefab; + [SerializeField] private Transform? _bubbleContainer; + + [Header("Queue Animation Settings")] + [SerializeField] private bool _enableQueueAnimation = true; + [SerializeField] private float _queueAnimationDelay = 0.1f; + + [Header("Performance Settings")] + [SerializeField] private int _maxBubbles = 20; + [SerializeField] private bool _autoCleanup = true; + [SerializeField] private int _cleanupThreshold = 15; + + private List _activeBubbles = new List(); + + public int ActiveBubbleCount => _activeBubbles.Count; + + public event Action? OnBubbleCreated; + public event Action? OnBubbleDestroyed; + public event Action? OnAllBubblesCleared; + + #region Unity Lifecycle + + private void Awake() + { + Initialize(); + } + + private void OnDestroy() + { + ClearAllBubbles(); + } + + #endregion + + #region Public Methods + + public void CreateBubble(Actor actor, string text, float displayTime = -1f) + { + if (_chatBubblePrefab == null || _bubbleContainer == null) + { + Debug.LogError("[ChatBubblePanel] ChatBubblePrefab 또는 BubbleContainer가 설정되지 않았습니다!"); + return; + } + + try + { + GameObject? bubbleObject = Instantiate(_chatBubblePrefab, _bubbleContainer); + ChatBubbleUI? bubbleUI = bubbleObject?.GetComponent(); + + if (bubbleUI == null) + { + Debug.LogError("[ChatBubblePanel] ChatBubbleUI 컴포넌트를 찾을 수 없습니다!"); + if (bubbleObject != null) + Destroy(bubbleObject); + return; + } + + SetupCanvasGroup(bubbleObject); + + if (_activeBubbles.Count >= _maxBubbles) + { + Debug.LogWarning($"[ChatBubblePanel] 최대 버블 수({_maxBubbles})에 도달했습니다. 가장 오래된 버블을 제거합니다."); + RemoveOldestBubble(); + } + + if (_autoCleanup && _activeBubbles.Count >= _cleanupThreshold) + { + CleanupOldBubbles(); + } + + if (_enableQueueAnimation && _activeBubbles.Count > 0) + { + StartQueueAnimationForExistingBubbles(); + } + + bubbleUI.Initialize(actor, text, displayTime, this); + + bubbleUI.OnBubbleDestroyed += OnBubbleDestroyed; + bubbleUI.OnToastAnimationComplete += OnBubbleToastAnimationComplete; + + _activeBubbles.Add(bubbleUI); + + OnBubbleCreated?.Invoke(bubbleUI); + } + catch (Exception ex) + { + Debug.LogError($"[ChatBubblePanel] 버블 생성 실패: {ex.Message}"); + } + } + + public void RemoveBubble(ChatBubbleUI? bubble) + { + if (bubble == null) return; + + try + { + bubble.OnBubbleDestroyed -= OnBubbleDestroyed; + bubble.OnToastAnimationComplete -= OnBubbleToastAnimationComplete; + + _activeBubbles.Remove(bubble); + + OnBubbleDestroyed?.Invoke(bubble); + } + catch (Exception ex) + { + Debug.LogError($"[ChatBubblePanel] 버블 제거 실패: {ex.Message}"); + } + } + + public void ClearAllBubbles() + { + try + { + foreach (var bubble in _activeBubbles.ToArray()) + { + if (bubble != null) + { + bubble.OnBubbleDestroyed -= OnBubbleDestroyed; + bubble.OnToastAnimationComplete -= OnBubbleToastAnimationComplete; + Destroy(bubble.gameObject); + } + } + + _activeBubbles.Clear(); + + OnAllBubblesCleared?.Invoke(); + } + catch (Exception ex) + { + Debug.LogError($"[ChatBubblePanel] 모든 버블 제거 실패: {ex.Message}"); + } + } + + #endregion + + #region Private Methods + + private void Initialize() + { + if (_bubbleContainer == null) + { + Debug.LogError("[ChatBubblePanel] BubbleContainer가 설정되지 않았습니다! 인스펙터에서 설정해주세요."); + return; + } + + if (_chatBubblePrefab == null) + { + Debug.LogError("[ChatBubblePanel] ChatBubblePrefab이 설정되지 않았습니다!"); + return; + } + + Debug.Log("[ChatBubblePanel] 초기화 완료"); + } + + private void StartQueueAnimationForExistingBubbles() + { + Canvas.ForceUpdateCanvases(); + + for (int i = 0; i < _activeBubbles.Count; i++) + { + var bubble = _activeBubbles[i]; + if (bubble != null && bubble.IsToastAnimationComplete) + { + float delay = i * _queueAnimationDelay; + StartCoroutine(QueueAnimationWithDelay(bubble, delay)); + } + } + } + + private System.Collections.IEnumerator QueueAnimationWithDelay(ChatBubbleUI? bubble, float delay) + { + yield return new WaitForSeconds(delay); + + if (bubble != null) + { + bubble.StartQueueSlideAnimation(); + } + } + + private void OnBubbleToastAnimationComplete(ChatBubbleUI? bubble) + { + if (bubble == null) return; + + + if (_scrollRect != null) + { + Canvas.ForceUpdateCanvases(); + _scrollRect.verticalNormalizedPosition = 0f; + } + } + + private void RemoveOldestBubble() + { + if (_activeBubbles.Count > 0) + { + var oldestBubble = _activeBubbles[0]; + RemoveBubble(oldestBubble); + if (oldestBubble != null) + { + Destroy(oldestBubble.gameObject); + } + } + } + + private void CleanupOldBubbles() + { + int bubblesToRemove = _activeBubbles.Count - _cleanupThreshold; + for (int i = 0; i < bubblesToRemove && i < _activeBubbles.Count; i++) + { + var bubble = _activeBubbles[0]; + RemoveBubble(bubble); + if (bubble != null) + { + Destroy(bubble.gameObject); + } + } + } + + private void SetupCanvasGroup(GameObject? bubbleObject) + { + if (bubbleObject == null) return; + + CanvasGroup? canvasGroup = bubbleObject.GetComponent(); + if (canvasGroup == null) + { + canvasGroup = bubbleObject.AddComponent(); + Debug.Log($"[ChatBubblePanel] ChatBubble에 CanvasGroup이 자동으로 추가되었습니다: {bubbleObject.name}"); + } + + canvasGroup.alpha = 0f; + } + + #endregion + } +} + + diff --git a/Assets/Domain/Chat/View/ChatBubblePanel.cs.meta b/Assets/Domain/Chat/View/ChatBubblePanel.cs.meta new file mode 100644 index 0000000..1573451 --- /dev/null +++ b/Assets/Domain/Chat/View/ChatBubblePanel.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 30700e2214ca700469760eef5b0069ea + diff --git a/Assets/Domain/Chat/View/ChatBubbleUI.cs b/Assets/Domain/Chat/View/ChatBubbleUI.cs index bee3aff..6ea921f 100644 --- a/Assets/Domain/Chat/View/ChatBubbleUI.cs +++ b/Assets/Domain/Chat/View/ChatBubbleUI.cs @@ -5,7 +5,7 @@ using UnityEngine.UI; using TMPro; using ProjectVG.Domain.Chat.Model; -using ProjectVG.Domain.Chat.Service; + namespace ProjectVG.Domain.Chat.View { @@ -67,7 +67,7 @@ public enum EasingType private Coroutine? _typingCoroutine; private Coroutine? _animationCoroutine; - private ChatBubbleManager? _manager; + private ChatBubblePanel? _manager; // 애니메이션 관련 변수들 private Vector3 _originalPosition; @@ -110,7 +110,7 @@ private void OnDestroy() #region Public Methods - public void Initialize(Actor actor, string text, float displayTime, ChatBubbleManager? manager = null) + public void Initialize(Actor actor, string text, float displayTime, ChatBubblePanel? manager = null) { _actor = actor; _fullText = text; diff --git a/Assets/Samples/Core/Managers/SampleSystemManager.cs b/Assets/Samples/Core/Managers/SampleSystemManager.cs index 76880c7..3a49e09 100644 --- a/Assets/Samples/Core/Managers/SampleSystemManager.cs +++ b/Assets/Samples/Core/Managers/SampleSystemManager.cs @@ -2,6 +2,7 @@ using Live2D.Cubism.Framework.MouthMovement; using UnityEngine; using UnityEngine.UI; +using ProjectVG.Core.Audio; public class SampleSystemManager : Singleton { diff --git a/Assets/UI/Prefabs/ChatView.prefab b/Assets/UI/Prefabs/ChatView.prefab index 70a9993..042972b 100644 --- a/Assets/UI/Prefabs/ChatView.prefab +++ b/Assets/UI/Prefabs/ChatView.prefab @@ -11,6 +11,7 @@ GameObject: - component: {fileID: 1193639241046649271} - component: {fileID: 5443803679294381057} - component: {fileID: 900044648925520396} + - component: {fileID: 7731201516897828228} m_Layer: 5 m_Name: ChatView m_TagString: Untagged @@ -76,6 +77,28 @@ MonoBehaviour: m_OnValueChanged: m_PersistentCalls: m_Calls: [] +--- !u!114 &7731201516897828228 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1198648032041697651} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 30700e2214ca700469760eef5b0069ea, type: 3} + m_Name: + m_EditorClassIdentifier: + _scrollRect: {fileID: 900044648925520396} + _gridLayoutGroup: {fileID: 6992329501726019127} + _contentSizeFitter: {fileID: 2327760123492423530} + _chatBubblePrefab: {fileID: 3575337311992857127, guid: 7151716ef12e2424d866e4db41b1f6f9, type: 3} + _bubbleContainer: {fileID: 5196526590166986169} + _enableQueueAnimation: 1 + _queueAnimationDelay: 0.1 + _maxBubbles: 20 + _autoCleanup: 1 + _cleanupThreshold: 15 --- !u!1 &1644047271142599422 GameObject: m_ObjectHideFlags: 0 diff --git a/Assets/Voice.mixer b/Assets/Voice.mixer new file mode 100644 index 0000000..84960c3 --- /dev/null +++ b/Assets/Voice.mixer @@ -0,0 +1,69 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!241 &24100000 +AudioMixerController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Voice + m_OutputGroup: {fileID: 0} + m_MasterGroup: {fileID: 24300002} + m_Snapshots: + - {fileID: 24500006} + m_StartSnapshot: {fileID: 24500006} + m_SuspendThreshold: -80 + m_EnableSuspend: 1 + m_UpdateMode: 0 + m_ExposedParameters: [] + m_AudioMixerGroupViews: + - guids: + - 0f47c3379c5b66e458d9740a30fc267a + name: View + m_CurrentViewIndex: 0 + m_TargetSnapshot: {fileID: 24500006} +--- !u!243 &24300002 +AudioMixerGroupController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Master + m_AudioMixer: {fileID: 24100000} + m_GroupID: 0f47c3379c5b66e458d9740a30fc267a + m_Children: [] + m_Volume: 583a709ad5a7180498f0cda7cd1091b2 + m_Pitch: 7ea55f842e0513b43902be8341087012 + m_Send: 00000000000000000000000000000000 + m_Effects: + - {fileID: 24400004} + m_UserColorIndex: 0 + m_Mute: 0 + m_Solo: 0 + m_BypassEffects: 0 +--- !u!244 &24400004 +AudioMixerEffectController: + m_ObjectHideFlags: 3 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: + m_EffectID: 9f0119b8c97725546ac7b269861f6c67 + m_EffectName: Attenuation + m_MixLevel: b67c869ef3e747e4fad8fd66b2f91bf9 + m_Parameters: [] + m_SendTarget: {fileID: 0} + m_EnableWetMix: 0 + m_Bypass: 0 +--- !u!245 &24500006 +AudioMixerSnapshotController: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_Name: Snapshot + m_AudioMixer: {fileID: 24100000} + m_SnapshotID: 028d43bb053f091429fb3985cb191113 + m_FloatValues: + 583a709ad5a7180498f0cda7cd1091b2: 2.850226 + m_TransitionOverrides: {} diff --git a/Assets/Voice.mixer.meta b/Assets/Voice.mixer.meta new file mode 100644 index 0000000..3f7bc74 --- /dev/null +++ b/Assets/Voice.mixer.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 3bf46f7daa3260847aa1b964ad2b01a7 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 24100000 + userData: + assetBundleName: + assetBundleVariant: From 8352b4ce53be129219aed8a257bc365b6f29be4b Mon Sep 17 00:00:00 2001 From: WooSH Date: Thu, 14 Aug 2025 08:28:59 +0900 Subject: [PATCH 15/19] =?UTF-8?q?fix:=20AudioManager=20=EB=B0=8F=20Session?= =?UTF-8?q?Manager=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20=EC=95=88=EC=A0=95=EC=84=B1=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 주요 수정사항 #### AudioManager 개선 - LoadVolumeSettings에서 무한 재귀 호출 위험 방지 - _isLoadingSettings 플래그 추가로 로딩 중 저장 방지 - SaveVolumeSettings에서 로딩 중일 때 early return 처리 #### AudioController 코루틴 안정성 개선 - UIController 및 SFXController의 ReturnToPoolWhenFinished 코루틴에 null 체크 추가 - AudioSource 파괴 시 NullReferenceException 방지 - while 루프와 ReturnToPool 호출 시 null 검증 #### SessionManager 이벤트 처리 개선 - WebSocketManager null 처리 시 OnSessionError 이벤트 발생 추가 - Initialize, EnsureConnectionAsync, RequestConnectionAsync 메서드에서 이벤트 발생 - 예외 발생 시에도 OnSessionError 이벤트 발생 #### ChatManager 초기화 안정성 개선 - InitializeWhenReady 코루틴에 타임아웃 추가 (5초) - ChatBubblePanel null 상태에서 무한 대기 방지 - 타임아웃 시 에러 로그 출력 후 yield break 처리 - System.Collections 네임스페이스 추가로 IEnumerator 타입 오류 해결 #### SystemManager 예외 처리 개선 - Initialize async void 메서드에 try-catch 블록 추가 - 예외 발생 시 OnInitializationError 이벤트 발생 #### Singleton import 누락 수정 - AudioManager, SystemManager에 ProjectVG.Core.Utils 네임스페이스 추가 - Singleton 클래스 참조 오류 해결 ### 기술적 개선사항 - async void 메서드의 예외 처리 강화 - 코루틴에서 null 체크 및 타임아웃 처리 - 이벤트 기반 에러 처리 일관성 확보 - 메모리 누수 및 무한 루프 방지 --- Assets/Core/Audio/AudioManager.cs | 8 +++++ Assets/Core/Audio/SFXController.cs | 7 +++-- Assets/Core/Audio/UIController.cs | 7 +++-- Assets/Core/Managers/SystemManager.cs | 29 ++++++++++++------- Assets/Domain/Chat/Service/ChatManager.cs | 19 ++++++++---- .../Network/Services/SessionManager.cs | 4 +++ 6 files changed, 55 insertions(+), 19 deletions(-) diff --git a/Assets/Core/Audio/AudioManager.cs b/Assets/Core/Audio/AudioManager.cs index 1c0ba0c..f617ed3 100644 --- a/Assets/Core/Audio/AudioManager.cs +++ b/Assets/Core/Audio/AudioManager.cs @@ -4,6 +4,7 @@ using UnityEngine.Audio; using Cysharp.Threading.Tasks; using ProjectVG.Domain.Chat.Model; +using ProjectVG.Core.Utils; namespace ProjectVG.Core.Audio { @@ -29,6 +30,7 @@ public class AudioManager : Singleton [SerializeField] private float _masterVolume = 1f; private bool _isInitialized = false; + private bool _isLoadingSettings = false; public bool IsInitialized => _isInitialized; public float MasterVolume => _masterVolume; @@ -179,6 +181,8 @@ public void SetVoiceVolume(float volume) public void SaveVolumeSettings() { + if (_isLoadingSettings) return; + PlayerPrefs.SetFloat("Audio_MasterVolume", _masterVolume); PlayerPrefs.SetFloat("Audio_BGMVolume", BgmVolume); PlayerPrefs.SetFloat("Audio_SFXVolume", SfxVolume); @@ -189,6 +193,8 @@ public void SaveVolumeSettings() public void LoadVolumeSettings() { + _isLoadingSettings = true; + _masterVolume = PlayerPrefs.GetFloat("Audio_MasterVolume", 1f); float bgmVolume = PlayerPrefs.GetFloat("Audio_BGMVolume", 1f); float sfxVolume = PlayerPrefs.GetFloat("Audio_SFXVolume", 1f); @@ -200,6 +206,8 @@ public void LoadVolumeSettings() SetSFXVolume(sfxVolume); SetUIVolume(uiVolume); SetVoiceVolume(voiceVolume); + + _isLoadingSettings = false; } public void ResetVolumeSettings() diff --git a/Assets/Core/Audio/SFXController.cs b/Assets/Core/Audio/SFXController.cs index ad581ff..09db666 100644 --- a/Assets/Core/Audio/SFXController.cs +++ b/Assets/Core/Audio/SFXController.cs @@ -121,12 +121,15 @@ private void ReturnToPool(AudioSource audioSource) private IEnumerator ReturnToPoolWhenFinished(AudioSource audioSource) { - while (audioSource.isPlaying) + while (audioSource != null && audioSource.isPlaying) { yield return null; } - ReturnToPool(audioSource); + if (audioSource != null) + { + ReturnToPool(audioSource); + } } } } diff --git a/Assets/Core/Audio/UIController.cs b/Assets/Core/Audio/UIController.cs index 3b4b9d2..1d1b8f6 100644 --- a/Assets/Core/Audio/UIController.cs +++ b/Assets/Core/Audio/UIController.cs @@ -112,12 +112,15 @@ private void ReturnToPool(AudioSource audioSource) private IEnumerator ReturnToPoolWhenFinished(AudioSource audioSource) { - while (audioSource.isPlaying) + while (audioSource != null && audioSource.isPlaying) { yield return null; } - ReturnToPool(audioSource); + if (audioSource != null) + { + ReturnToPool(audioSource); + } } } } diff --git a/Assets/Core/Managers/SystemManager.cs b/Assets/Core/Managers/SystemManager.cs index f7a2301..67bd118 100644 --- a/Assets/Core/Managers/SystemManager.cs +++ b/Assets/Core/Managers/SystemManager.cs @@ -8,6 +8,7 @@ using ProjectVG.Core.Loading; using ProjectVG.Core.Audio; using ProjectVG.Domain.Chat.Service; +using ProjectVG.Core.Utils; namespace ProjectVG.Core.Managers { @@ -129,20 +130,28 @@ public void SetCamera(Camera camera) public async void Initialize() { - if (_initializationKickoffDone && IsInitialized) + try { - return; - } - _initializationKickoffDone = true; + if (_initializationKickoffDone && IsInitialized) + { + return; + } + _initializationKickoffDone = true; - // Camera 업데이트 및 ScreenTapManager 초기화 - UpdateCamera(); - if (_camera != null) + // Camera 업데이트 및 ScreenTapManager 초기화 + UpdateCamera(); + if (_camera != null) + { + ScreenTapManager.Instance.Initialize(_camera); + } + + await InitializeAppAsync(); + } + catch (Exception ex) { - ScreenTapManager.Instance.Initialize(_camera); + Debug.LogError($"[SystemManager] Initialize 중 오류 발생: {ex.Message}"); + OnInitializationError?.Invoke(ex.Message); } - - await InitializeAppAsync(); } public async UniTask InitializeAppAsync() diff --git a/Assets/Domain/Chat/Service/ChatManager.cs b/Assets/Domain/Chat/Service/ChatManager.cs index e7bfc9a..c34deb3 100644 --- a/Assets/Domain/Chat/Service/ChatManager.cs +++ b/Assets/Domain/Chat/Service/ChatManager.cs @@ -1,5 +1,6 @@ #nullable enable using System; +using System.Collections; using System.Collections.Generic; using UnityEngine; using Cysharp.Threading.Tasks; @@ -7,10 +8,8 @@ using ProjectVG.Domain.Chat.Model; using ProjectVG.Infrastructure.Network.WebSocket; using ProjectVG.Infrastructure.Network.Services; -using ProjectVG.Infrastructure.Network.DTOs.Chat; -using ProjectVG.Domain.Chat.Service; using ProjectVG.Domain.Chat.View; -using ProjectVG.Core.Audio; + namespace ProjectVG.Domain.Chat.Service { @@ -77,12 +76,22 @@ private void Start() StartCoroutine(InitializeWhenReady()); } - private System.Collections.IEnumerator InitializeWhenReady() + private IEnumerator InitializeWhenReady() { + float timeout = 5f; + float elapsedTime = 0f; + // ChatBubblePanel이 준비될 때까지 대기 - while (_chatBubblePanel == null) + while (_chatBubblePanel == null && elapsedTime < timeout) { yield return new WaitForEndOfFrame(); + elapsedTime += Time.deltaTime; + } + + if (_chatBubblePanel == null) + { + Debug.LogError("[ChatManager] ChatBubblePanel 초기화 타임아웃"); + yield break; } Initialize(); diff --git a/Assets/Infrastructure/Network/Services/SessionManager.cs b/Assets/Infrastructure/Network/Services/SessionManager.cs index 5963943..7d4b339 100644 --- a/Assets/Infrastructure/Network/Services/SessionManager.cs +++ b/Assets/Infrastructure/Network/Services/SessionManager.cs @@ -87,6 +87,7 @@ public async UniTask EnsureConnectionAsync() if (_webSocketManager == null) { Debug.LogError("[SessionManager] WebSocketManager가 DI로 주입되지 않았습니다. DependencyManager 설정을 확인하세요."); + OnSessionError?.Invoke("WebSocketManager가 null입니다."); return false; } @@ -104,6 +105,7 @@ private async UniTask RequestConnectionAsync() if (_webSocketManager == null) { Debug.LogError("[SessionManager] WebSocketManager가 DI로 주입되지 않았습니다."); + OnSessionError?.Invoke("WebSocketManager가 null입니다."); return false; } @@ -205,6 +207,7 @@ public void Initialize(WebSocketManager webSocketManager) if (_webSocketManager == null) { Debug.LogError("[SessionManager] WebSocketManager가 null입니다."); + OnSessionError?.Invoke("WebSocketManager가 null입니다."); return; } @@ -217,6 +220,7 @@ public void Initialize(WebSocketManager webSocketManager) { Debug.LogError($"[SessionManager] 초기화 실패: {ex.Message}"); Debug.LogError($"[SessionManager] 스택 트레이스: {ex.StackTrace}"); + OnSessionError?.Invoke($"초기화 실패: {ex.Message}"); } } From 2232309badd12d525896367010a246955bd11038 Mon Sep 17 00:00:00 2001 From: WooSH Date: Thu, 14 Aug 2025 08:56:24 +0900 Subject: [PATCH 16/19] =?UTF-8?q?fix:=20=EC=98=A4=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=95=84=ED=82=A4=ED=85=8D?= =?UTF-8?q?=EC=B2=98=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=BB=B4=ED=8C=8C?= =?UTF-8?q?=EC=9D=BC=20=EA=B2=BD=EA=B3=A0=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 주요 변경사항 ### 1. 오디오 시스템 아키텍처 개선 - **AudioManager 리팩토링**: 단일 AudioManager에서 계층적 구조로 변경 - BGMController, SFXController, UIController, VoiceController로 분리 - AudioControllerCore 베이스 클래스와 IAudioController 인터페이스 도입 - 각 컨트롤러별 전용 AudioMixerGroup 지원 - **오브젝트 풀링 시스템**: SFX 및 UI 사운드용 AudioSource 풀링 구현 - 동적 AudioSource 생성 및 관리 - 동시 재생 지원으로 성능 최적화 - **VoiceController 통합**: 기존 VoiceController 기능을 새로운 아키텍처에 통합 - VoiceData, UniTask, 이벤트 시스템 유지 - PauseVoice/ResumeVoice 기능 보존 ### 2. ChatManager 개선 - **Singleton 패턴 최적화**: DontDestroyOnLoad 제거로 초기화 순서 문제 해결 - **UI 참조 안정화**: _chatBubblePanel null 참조 문제 해결 - InitializeWhenReady 코루틴에 타임아웃 추가 - 초기화 순서 안정화 ### 3. 파일 구조 정리 - **ChatBubblePanel 이동**: Service → View 디렉토리로 이동 - **임시 파일 제거**: UIAudioPlayer, UIAudioEventSystem, UISoundButton 삭제 ### 4. 컴파일 경고 및 오류 해결 - **Nullable Reference Types**: 모든 파일에 #nullable enable 적용 - **CS8600/CS8601/CS8602/CS8603 경고**: null 체크 및 nullable 타입 선언 - **CS8618 경고**: 필드 초기화 및 nullable 선언 - **CS0114 경고**: override 키워드 추가 (Awake 메서드) - **CS0414 경고**: 사용하지 않는 필드 제거 - **CS4014/CS1998 경고**: async 메서드 최적화 - **CS0305/CS1622 오류**: 코루틴 문법 수정 ### 5. VoiceInputView 간소화 - **UI 요소 제거**: 진행률 표시 및 상태 텍스트 제거 - **불필요한 이벤트 제거**: OnRecordingProgress 이벤트 구독 해제 ### 6. 시스템 안정성 개선 - **예외 처리 강화**: async void 메서드에 try-catch 추가 - **이벤트 일관성**: SessionManager에서 WebSocketManager null 처리 개선 - **무한 루프 방지**: 코루틴에 타임아웃 및 안전장치 추가 ## 기술적 세부사항 - Unity AudioMixer 기반 볼륨 제어 시스템 - PlayerPrefs를 통한 설정 저장/로드 - Cysharp.Threading.Tasks (UniTask) 활용 - 메모리 누수 방지를 위한 AudioClip 정리 로직 ## 영향받는 파일 - Core/Audio/: AudioManager, AudioControllerCore, BGMController, SFXController, UIController, VoiceController - Domain/Chat/: ChatManager, ChatBubblePanel, VoiceInputView, ChatBubbleUI - Core/Managers/: SystemManager - Infrastructure/: SessionManager, WebSocketManager, STTService - 기타: AudioRecorder, LoadingManager, Live2DModelManager 등 --- Assets/Core/Audio/AudioControllerCore.cs | 14 +++-- Assets/Core/Audio/AudioRecorder.cs | 6 +- Assets/Core/Audio/SFXController.cs | 4 +- Assets/Core/Audio/UIController.cs | 4 +- Assets/Core/Audio/VoiceController.cs | 20 ++++--- .../DebugConsole/GameDebugConsoleManager.cs | 3 +- Assets/Core/DebugConsole/LogEntryPrefab.cs | 1 + Assets/Core/Loading/LoadingManager.cs | 3 +- Assets/Core/Managers/SystemManager.cs | 24 ++++---- .../Script/Controller/Live2DModelApplier.cs | 2 - .../Script/Manager/Live2DModelManager.cs | 2 +- Assets/Domain/Chat/View/ChatBubbleUI.cs | 5 +- Assets/Domain/Chat/View/VoiceInputView.cs | 58 +------------------ .../Config/AppEnvironmentConfig.cs | 9 +++ .../Network/Services/STTService.cs | 12 +--- .../Network/WebSocket/WebSocketManager.cs | 4 +- 16 files changed, 64 insertions(+), 107 deletions(-) diff --git a/Assets/Core/Audio/AudioControllerCore.cs b/Assets/Core/Audio/AudioControllerCore.cs index a6c46c4..ac38814 100644 --- a/Assets/Core/Audio/AudioControllerCore.cs +++ b/Assets/Core/Audio/AudioControllerCore.cs @@ -6,8 +6,8 @@ namespace ProjectVG.Core.Audio { public class AudioControllerCore : MonoBehaviour { - [SerializeField] protected AudioSource _audioSource; - [SerializeField] protected AudioMixerGroup _audioMixerGroup; + [SerializeField] protected AudioSource? _audioSource; + [SerializeField] protected AudioMixerGroup? _audioMixerGroup; protected float _volume = 1f; protected string _volumeParameterName = ""; @@ -28,7 +28,11 @@ public virtual void Initialize(string volumeParameterName) // AudioMixerGroup이 설정되지 않은 경우 AudioManager에서 자동 할당 if (_audioMixerGroup == null) { - _audioMixerGroup = GetAudioMixerGroupFromManager(); + var group = GetAudioMixerGroupFromManager(); + if (group != null) + { + _audioMixerGroup = group; + } } if (_audioSource != null && _audioMixerGroup != null) @@ -72,7 +76,7 @@ public virtual float GetVolume() return _volume; } - public AudioSource GetAudioSource() => _audioSource; - public AudioMixerGroup GetAudioMixerGroup() => _audioMixerGroup; + public AudioSource? GetAudioSource() => _audioSource; + public AudioMixerGroup? GetAudioMixerGroup() => _audioMixerGroup; } } diff --git a/Assets/Core/Audio/AudioRecorder.cs b/Assets/Core/Audio/AudioRecorder.cs index 6e26ac0..bb40579 100644 --- a/Assets/Core/Audio/AudioRecorder.cs +++ b/Assets/Core/Audio/AudioRecorder.cs @@ -14,7 +14,6 @@ public class AudioRecorder : Singleton { [Header("Recording Settings")] [SerializeField] private int _sampleRate = 44100; - [SerializeField] private int _channels = 1; [SerializeField] private int _maxRecordingLength = 30; // 최대 녹음 시간 (초) [Header("Audio Processing")] @@ -99,8 +98,7 @@ public bool StartRecording() _isRecording = true; _recordingStartTime = Time.time; - // 최대 녹음 시간만큼 버퍼 할당 - _recordingClip = Microphone.Start(_currentDevice ?? string.Empty, false, _maxRecordingLength, _sampleRate); + _recordingClip = Microphone.Start(_currentDevice != null ? _currentDevice : string.Empty, false, _maxRecordingLength, _sampleRate); Debug.Log($"[AudioRecorder] 음성 녹음 시작됨 (최대 {_maxRecordingLength}초, {_sampleRate}Hz)"); OnRecordingStarted?.Invoke(); @@ -134,7 +132,7 @@ public bool StartRecording() _recordingEndTime = Time.time; float actualRecordingDuration = _recordingEndTime - _recordingStartTime; - Microphone.End(_currentDevice ?? string.Empty); + Microphone.End(_currentDevice != null ? _currentDevice : string.Empty); if (_recordingClip != null) { diff --git a/Assets/Core/Audio/SFXController.cs b/Assets/Core/Audio/SFXController.cs index 09db666..c1a8bf4 100644 --- a/Assets/Core/Audio/SFXController.cs +++ b/Assets/Core/Audio/SFXController.cs @@ -27,7 +27,7 @@ public void Initialize() return AudioManager.Instance._sfxGroup; } - public void Stop() + public override void Stop() { foreach (var source in _activeSources.ToArray()) { @@ -39,7 +39,7 @@ public void Stop() } } - public bool IsPlaying() + public override bool IsPlaying() { return _activeSources.Count > 0; } diff --git a/Assets/Core/Audio/UIController.cs b/Assets/Core/Audio/UIController.cs index 1d1b8f6..01b47d0 100644 --- a/Assets/Core/Audio/UIController.cs +++ b/Assets/Core/Audio/UIController.cs @@ -27,7 +27,7 @@ public void Initialize() return AudioManager.Instance._uiGroup; } - public void Stop() + public override void Stop() { foreach (var source in _activeSources.ToArray()) { @@ -39,7 +39,7 @@ public void Stop() } } - public bool IsPlaying() + public override bool IsPlaying() { return _activeSources.Count > 0; } diff --git a/Assets/Core/Audio/VoiceController.cs b/Assets/Core/Audio/VoiceController.cs index a4ab009..19d9003 100644 --- a/Assets/Core/Audio/VoiceController.cs +++ b/Assets/Core/Audio/VoiceController.cs @@ -27,7 +27,7 @@ public class VoiceController : AudioControllerCore, IAudioController private void Update() { - if (_isPlaying && !_audioSource.isPlaying && _audioSource.clip != null) + if (_isPlaying && _audioSource != null && !_audioSource.isPlaying && _audioSource.clip != null) { _isPlaying = false; OnVoiceFinished?.Invoke(); @@ -114,10 +114,13 @@ public async void PlayVoice(VoiceData voiceData) await UniTask.Delay(50); _currentVoice = voiceData; - _audioSource.clip = voiceData.AudioClip; - _audioSource.volume = _volume; + if (_audioSource != null) + { + _audioSource.clip = voiceData.AudioClip; + _audioSource.volume = _volume; + } - if (_autoPlay) + if (_autoPlay && _audioSource != null) { _audioSource.Play(); _isPlaying = true; @@ -138,10 +141,13 @@ public async UniTask PlayVoiceAsync(VoiceData voiceData) await UniTask.Delay(50); _currentVoice = voiceData; - _audioSource.clip = voiceData.AudioClip; - _audioSource.volume = _volume; + if (_audioSource != null) + { + _audioSource.clip = voiceData.AudioClip; + _audioSource.volume = _volume; + } - if (_autoPlay) + if (_autoPlay && _audioSource != null) { _audioSource.Play(); _isPlaying = true; diff --git a/Assets/Core/DebugConsole/GameDebugConsoleManager.cs b/Assets/Core/DebugConsole/GameDebugConsoleManager.cs index 23b0591..77aeb98 100644 --- a/Assets/Core/DebugConsole/GameDebugConsoleManager.cs +++ b/Assets/Core/DebugConsole/GameDebugConsoleManager.cs @@ -1,3 +1,4 @@ +#nullable enable using UnityEngine; using UnityEngine.UI; using TMPro; @@ -285,7 +286,7 @@ private GameObject GetPooledObject() return Instantiate(_logEntryPrefab, _logContentParent); } - return null; + return null!; } private void ReturnToPool(GameObject obj) diff --git a/Assets/Core/DebugConsole/LogEntryPrefab.cs b/Assets/Core/DebugConsole/LogEntryPrefab.cs index d9dd4f0..ff80ce3 100644 --- a/Assets/Core/DebugConsole/LogEntryPrefab.cs +++ b/Assets/Core/DebugConsole/LogEntryPrefab.cs @@ -1,3 +1,4 @@ +#nullable enable using UnityEngine; using TMPro; diff --git a/Assets/Core/Loading/LoadingManager.cs b/Assets/Core/Loading/LoadingManager.cs index 968dda7..cfc1ec4 100644 --- a/Assets/Core/Loading/LoadingManager.cs +++ b/Assets/Core/Loading/LoadingManager.cs @@ -43,8 +43,9 @@ public class LoadingManager : Singleton #region Unity Lifecycle - private void Awake() + protected override void Awake() { + base.Awake(); SetupEventListeners(); } diff --git a/Assets/Core/Managers/SystemManager.cs b/Assets/Core/Managers/SystemManager.cs index 67bd118..2d76b8c 100644 --- a/Assets/Core/Managers/SystemManager.cs +++ b/Assets/Core/Managers/SystemManager.cs @@ -15,11 +15,11 @@ namespace ProjectVG.Core.Managers public class SystemManager : Singleton { [Header("Core Managers")] - [SerializeField] private WebSocketManager _webSocketManager; - [SerializeField] private SessionManager _sessionManager; - [SerializeField] private HttpApiClient _httpApiClient; - [SerializeField] private AudioManager _audioManager; - [SerializeField] private LoadingManager _loadingManager; + [SerializeField] private WebSocketManager? _webSocketManager; + [SerializeField] private SessionManager? _sessionManager; + [SerializeField] private HttpApiClient? _httpApiClient; + [SerializeField] private AudioManager? _audioManager; + [SerializeField] private LoadingManager? _loadingManager; [SerializeField] private ChatManager? _chatManager; @@ -29,18 +29,18 @@ public class SystemManager : Singleton [SerializeField] private bool _autoUpdateCameraOnSceneChange = true; [Header("Camera Settings")] - [SerializeField] private Camera _camera; + [SerializeField] private Camera? _camera; private bool _initializationKickoffDone = false; public bool IsInitialized { get; private set; } - public WebSocketManager WebSocketManager => _webSocketManager; - public SessionManager SessionManager => _sessionManager; - public AudioManager AudioManager => _audioManager; - public LoadingManager LoadingManager => _loadingManager; + public WebSocketManager? WebSocketManager => _webSocketManager; + public SessionManager? SessionManager => _sessionManager; + public AudioManager? AudioManager => _audioManager; + public LoadingManager? LoadingManager => _loadingManager; - public event Action OnAppInitialized; - public event Action OnInitializationError; + public event Action? OnAppInitialized; + public event Action? OnInitializationError; protected override void Awake() { diff --git a/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs b/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs index b99adac..43dd699 100644 --- a/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs +++ b/Assets/Domain/Character/Script/Controller/Live2DModelApplier.cs @@ -4,8 +4,6 @@ namespace ProjectVG.Domain.Character.Service { public class Live2DModelApplier : MonoBehaviour, ILive2DModelApplier { - [SerializeField] private bool _autoApplyOnEnable = true; - public void Apply(GameObject activeModel, ProjectVG.Domain.Character.Live2D.Model.Live2DModelConfig characterConfig) { } diff --git a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs index 167b889..8e390b0 100644 --- a/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs +++ b/Assets/Domain/Character/Script/Manager/Live2DModelManager.cs @@ -16,7 +16,7 @@ public class Live2DModelManager : Singleton private readonly Dictionary _characterIdToInstance = new Dictionary(); private string _activeCharacterId; - private void Awake() + protected override void Awake() { base.Awake(); } diff --git a/Assets/Domain/Chat/View/ChatBubbleUI.cs b/Assets/Domain/Chat/View/ChatBubbleUI.cs index 6ea921f..6429084 100644 --- a/Assets/Domain/Chat/View/ChatBubbleUI.cs +++ b/Assets/Domain/Chat/View/ChatBubbleUI.cs @@ -23,7 +23,6 @@ public class ChatBubbleUI : MonoBehaviour private CanvasGroup? _canvasGroup; [Header("Animation Settings")] - [SerializeField] private float _slideInDuration = 0.25f; [SerializeField] private float _slideOutDuration = 0.03f; [SerializeField] private float _typingSpeed = 0.025f; [SerializeField] private bool _enableAutoDestroy = true; @@ -34,7 +33,7 @@ public class ChatBubbleUI : MonoBehaviour [SerializeField] private float _toastBounceHeight = 20f; [SerializeField] private float _toastBounceScale = 1.1f; [SerializeField] private float _queueSlideDuration = 0.2f; - [SerializeField] private float _queueSlideDistance = 10f; + [SerializeField] private bool _enableBounceEffect = true; [SerializeField] private bool _enableScaleEffect = true; [SerializeField] private EasingType _bounceEasing = EasingType.Bounce; @@ -60,7 +59,6 @@ public enum EasingType private string _fullText = string.Empty; private float _displayTime; - private bool _isInitialized = false; private bool _isAnimating = false; private bool _isTyping = false; private float _typingProgress = 0f; @@ -135,7 +133,6 @@ public void Initialize(Actor actor, string text, float displayTime, ChatBubblePa } } - _isInitialized = true; OnBubbleCreated?.Invoke(this); } diff --git a/Assets/Domain/Chat/View/VoiceInputView.cs b/Assets/Domain/Chat/View/VoiceInputView.cs index 9a4baba..1bbe81b 100644 --- a/Assets/Domain/Chat/View/VoiceInputView.cs +++ b/Assets/Domain/Chat/View/VoiceInputView.cs @@ -17,15 +17,7 @@ public class VoiceInputView : MonoBehaviour [Header("UI Components")] [SerializeField] private Button? _btnVoice; [SerializeField] private Button? _btnVoiceStop; - [SerializeField] private TextMeshProUGUI? _txtVoiceStatus; - [SerializeField] private Slider? _progressBar; // 녹음 진행률 표시 - [Header("Voice Settings")] - [SerializeField] private float _maxRecordingTime = 30f; - [SerializeField] private string _voiceStatusRecording = "Recording..."; // "녹음 중..."에서 변경 - [SerializeField] private string _voiceStatusProcessing = "Converting speech to text..."; // "음성을 텍스트로 변환 중..."에서 변경 - - private ChatManager? _chatManager; private AudioRecorder? _audioRecorder; @@ -64,7 +56,6 @@ private void OnDestroy() _audioRecorder.OnRecordingStarted -= OnRecordingStarted; _audioRecorder.OnRecordingStopped -= OnRecordingStopped; _audioRecorder.OnRecordingCompleted -= OnRecordingCompleted; - _audioRecorder.OnRecordingProgress -= OnRecordingProgress; _audioRecorder.OnError -= OnRecordingError; } } @@ -96,8 +87,6 @@ public async void SendVoiceMessage(byte[] audioData) try { - UpdateVoiceStatus(_voiceStatusProcessing); - string transcribedText = await ConvertSpeechToText(audioData); if (!string.IsNullOrWhiteSpace(transcribedText)) @@ -117,7 +106,6 @@ public async void SendVoiceMessage(byte[] audioData) } finally { - UpdateVoiceStatus(string.Empty); } } @@ -137,8 +125,6 @@ public void StartVoiceRecording() _isRecording = true; _recordingStartTime = Time.time; UpdateVoiceButtonState(true); - UpdateVoiceStatus(_voiceStatusRecording); - UpdateProgressBar(0f); bool success = _audioRecorder.StartRecording(); if (!success) @@ -169,8 +155,6 @@ public void StopVoiceRecording() { _isRecording = false; UpdateVoiceButtonState(false); - UpdateVoiceStatus(string.Empty); - UpdateProgressBar(0f); AudioClip? recordedClip = _audioRecorder.StopRecording(); if (recordedClip != null) @@ -213,23 +197,7 @@ private void SetupComponents() } } - if (_txtVoiceStatus == null) - { - _txtVoiceStatus = transform.Find("TxtVoiceStatus")?.GetComponent(); - if (_txtVoiceStatus == null) - { - Debug.LogWarning("[VoiceInputView] TxtVoiceStatus 텍스트를 찾을 수 없습니다."); - } - } - - if (_progressBar == null) - { - _progressBar = transform.Find("ProgressBar")?.GetComponent(); - if (_progressBar == null) - { - Debug.LogWarning("[VoiceInputView] ProgressBar 슬라이더를 찾을 수 없습니다."); - } - } + if (_audioRecorder == null) { @@ -263,7 +231,6 @@ private void SetupEventHandlers() _audioRecorder.OnRecordingStarted += OnRecordingStarted; _audioRecorder.OnRecordingStopped += OnRecordingStopped; _audioRecorder.OnRecordingCompleted += OnRecordingCompleted; - _audioRecorder.OnRecordingProgress += OnRecordingProgress; _audioRecorder.OnError += OnRecordingError; } } @@ -289,23 +256,7 @@ private void UpdateVoiceButtonState(bool isRecording) _btnVoiceStop.gameObject.SetActive(isRecording); } - private void UpdateVoiceStatus(string status) - { - if (_txtVoiceStatus != null) - { - _txtVoiceStatus.text = status; - _txtVoiceStatus.gameObject.SetActive(!string.IsNullOrEmpty(status)); - } - } - - private void UpdateProgressBar(float progress) - { - if (_progressBar != null) - { - _progressBar.value = progress; - _progressBar.gameObject.SetActive(progress > 0f); - } - } + private async System.Threading.Tasks.Task ConvertSpeechToText(byte[] audioData) { @@ -376,10 +327,7 @@ private void OnRecordingCompleted(AudioClip audioClip) // AudioRecorder에서 로그 출력 } - private void OnRecordingProgress(float progress) - { - UpdateProgressBar(progress); - } + private void OnRecordingError(string error) { diff --git a/Assets/Infrastructure/Config/AppEnvironmentConfig.cs b/Assets/Infrastructure/Config/AppEnvironmentConfig.cs index 8b2dca2..73ef763 100644 --- a/Assets/Infrastructure/Config/AppEnvironmentConfig.cs +++ b/Assets/Infrastructure/Config/AppEnvironmentConfig.cs @@ -8,6 +8,7 @@ namespace ProjectVG.Infrastructure.Config */ public class AppEnvironmentConfig : ScriptableObject { + #pragma warning disable CS0414 [Header("Override")] [SerializeField] private bool overrideEnabled = false; [SerializeField] private NetworkConfig.EnvironmentType overrideEnvironment = NetworkConfig.EnvironmentType.Development; @@ -15,13 +16,20 @@ public class AppEnvironmentConfig : ScriptableObject [Header("Per-Platform Default Environments")] [SerializeField] private NetworkConfig.EnvironmentType editorEnvironment = NetworkConfig.EnvironmentType.Development; [SerializeField] private NetworkConfig.EnvironmentType androidEnvironment = NetworkConfig.EnvironmentType.Production; + [SerializeField] private NetworkConfig.EnvironmentType iosEnvironment = NetworkConfig.EnvironmentType.Production; + [SerializeField] private NetworkConfig.EnvironmentType standaloneEnvironment = NetworkConfig.EnvironmentType.Development; [SerializeField] private NetworkConfig.EnvironmentType webglEnvironment = NetworkConfig.EnvironmentType.Development; [Header("Build Flags Mapping")] [SerializeField] private bool mapDevelopmentBuildToDevelopment = true; + #pragma warning restore CS0414 + + // 프로퍼티로 필드 사용을 명시적으로 표시 + public NetworkConfig.EnvironmentType IOSEnvironment => iosEnvironment; + private static AppEnvironmentConfig _instance; public static AppEnvironmentConfig Instance { @@ -91,3 +99,4 @@ private static AppEnvironmentConfig CreateDefaultAsset() } } } + diff --git a/Assets/Infrastructure/Network/Services/STTService.cs b/Assets/Infrastructure/Network/Services/STTService.cs index cca1769..3284d77 100644 --- a/Assets/Infrastructure/Network/Services/STTService.cs +++ b/Assets/Infrastructure/Network/Services/STTService.cs @@ -17,9 +17,9 @@ namespace ProjectVG.Infrastructure.Network.Services /// public class STTService { - private readonly HttpApiClient _httpClient; + private readonly HttpApiClient? _httpClient; - public bool IsConnected => true; // 항상 연결 가능하다고 가정 + public bool IsConnected => true; public bool IsAvailable => _httpClient != null; public STTService() @@ -67,18 +67,10 @@ public async UniTask ConvertSpeechToTextAsync(byte[] audioData, string a string forcedLanguage = "ko"; string endpoint = $"stt/transcribe?language={forcedLanguage}"; - Debug.Log($"[STTService] STT 변환 요청 시작 - 엔드포인트: {endpoint}, 파일 크기: {audioData.Length / 1024}KB, 강제 언어: {forcedLanguage}"); - Debug.Log($"[STTService] URL 확인: {endpoint}"); - var response = await _httpClient.PostFormDataAsync(endpoint, formData, fileNames, cancellationToken: cancellationToken); - Debug.Log($"[STTService] 응답 객체 - Text: '{response?.Text}', Language: '{response?.Language}'"); - Debug.Log($"[STTService] 응답 객체 - LanguageProbability: {response?.LanguageProbability}, SegmentsCount: {response?.SegmentsCount}"); - Debug.Log($"[STTService] 응답 객체 - ProcessingTime: {response?.ProcessingTime}"); - if (response != null && !string.IsNullOrEmpty(response.Text)) { - Debug.Log($"[STTService] STT 변환 성공 - 텍스트: '{response.Text}'"); return response.Text; } else diff --git a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs index 4b84e17..68bb660 100644 --- a/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs +++ b/Assets/Infrastructure/Network/WebSocket/WebSocketManager.cs @@ -75,7 +75,9 @@ public void Initialize() } _cancellationTokenSource = new CancellationTokenSource(); InitializeNativeWebSocket(); +#pragma warning disable CS4014 StartConnectionMonitoring(); +#pragma warning restore CS4014 } /// @@ -159,7 +161,7 @@ public async UniTask DisconnectAsync() /// /// 웹소켓 메시지 전송 /// - public async UniTask SendMessageAsync(string type, string data) + public UniTask SendMessageAsync(string type, string data) { throw new NotImplementedException(); } From 8195f5663a4f1c398af41685b2a546915b739ac3 Mon Sep 17 00:00:00 2001 From: WooSH Date: Thu, 14 Aug 2025 09:05:44 +0900 Subject: [PATCH 17/19] =?UTF-8?q?crone:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EA=B4=80=EB=A6=AC=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .coderabbit.yaml | 15 ------- .gitignore | 63 ++++++++++++---------------- .plasticignore | 44 ++++++++++++++++++++ ProjectVG_Live2D_Assignment.md | 75 ---------------------------------- ignore.conf | 67 +++++++++++++++++++++++++++++- .vsconfig => vsconfig | 0 6 files changed, 135 insertions(+), 129 deletions(-) delete mode 100644 .coderabbit.yaml delete mode 100644 ProjectVG_Live2D_Assignment.md rename .vsconfig => vsconfig (100%) diff --git a/.coderabbit.yaml b/.coderabbit.yaml deleted file mode 100644 index 065526b..0000000 --- a/.coderabbit.yaml +++ /dev/null @@ -1,15 +0,0 @@ -# .coderabbit.yaml -language: "ko-KR" -early_access: false -reviews: - profile: "chill" - request_changes_workflow: false - high_level_summary: true - poem: true - review_status: true - collapse_walkthrough: false - auto_review: - enabled: true - drafts: false -chat: - auto_reply: true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 540e128..485374c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -# This .gitignore file should be placed at the root of your Unity project directory -# -# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore -# +# Unity 프로젝트 .gitignore +# https://github.com/github/gitignore/blob/main/Unity.gitignore + +# Unity 기본 디렉토리 .utmp/ /[Ll]ibrary/ /[Tt]emp/ @@ -12,32 +12,29 @@ /[Uu]ser[Ss]ettings/ *.log -# By default unity supports Blender asset imports, *.blend1 blender files do not need to be commited to version control. +# Blender 파일 *.blend1 *.blend1.meta -# MemoryCaptures can get excessive in size. -# They also could contain extremely sensitive data +# 메모리 캡처 (크기가 크고 민감한 데이터 포함) /[Mm]emoryCaptures/ -# Recordings can get excessive in size +# 녹화 파일 (크기가 큼) /[Rr]ecordings/ -# Uncomment this line if you wish to ignore the asset store tools plugin +# Asset Store 도구 플러그인 # /[Aa]ssets/AssetStoreTools* -# Autogenerated Jetbrains Rider plugin +# Jetbrains Rider 플러그인 /[Aa]ssets/Plugins/Editor/JetBrains* -# Jetbrains Rider personal-layer settings *.DotSettings.user -# Visual Studio cache directory +# IDE 캐시 디렉토리 .vs/ - -# Gradle cache directory .gradle/ +.idea/ -# Autogenerated VS/MD/Consulo solution and project files +# IDE 생성 파일 ExportedObj/ .consulo/ *.csproj @@ -54,56 +51,50 @@ ExportedObj/ *.mdb *.opendb *.VC.db +*.vsconfig -# Unity3D generated meta files +# Unity 메타 파일 *.pidb.meta *.pdb.meta *.mdb.meta -# Unity3D generated file on crash reports +# 크래시 리포트 sysinfo.txt - -# Mono auto generated files mono_crash.* -# Builds +# 빌드 파일 *.apk *.aab *.unitypackage *.unitypackage.meta *.app -# Crashlytics generated file +# Crashlytics crashlytics-build.properties -# TestRunner generated files +# 테스트 파일 InitTestScene*.unity* -# Addressables default ignores, before user customizations +# Addressables /ServerData /[Aa]ssets/StreamingAssets/aa* /[Aa]ssets/AddressableAssetsData/link.xml* /[Aa]ssets/Addressables_Temp* -# By default, Addressables content builds will generate addressables_content_state.bin -# files in platform-specific subfolders, for example: -# /Assets/AddressableAssetsData/OSX/addressables_content_state.bin /[Aa]ssets/AddressableAssetsData/*/*.bin* -# Visual Scripting auto-generated files +# Visual Scripting 자동 생성 파일 /[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db /[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Flow/UnitOptions.db.meta /[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers /[Aa]ssets/Unity.VisualScripting.Generated/VisualScripting.Core/Property Providers.meta -# Auto-generated scenes by play mode tests +# 테스트 씬 /[Aa]ssets/[Ii]nit[Tt]est[Ss]cene*.unity* - -# Add +# 추가 항목 /.plastic/ *.stacktrace *.crash -.idea/ *.iml *.xcworkspace *.xcuserdata @@ -111,9 +102,7 @@ InitTestScene*.unity* *.mode1v3 *.mode2v3 -# Resources Large Files -*.wav -*.mp3 +# 대용량 미디어 파일 *.mp4 *.avi *.mov @@ -126,9 +115,7 @@ InitTestScene*.unity* *.m4b *.m4r *.m4p -*.m4v -*.m4b -*.m4r -*.m4p + +# 특정 플러그인 /Assets/Plugins/FiveMinuteChat /Assets/Plugins/WebGLTemplates diff --git a/.plasticignore b/.plasticignore index c0cb5ef..3481751 100644 --- a/.plasticignore +++ b/.plasticignore @@ -22,6 +22,7 @@ Desktop.ini *.tmp *.user *.userprefs +*.vsconfig # macOS 관련 *.DS_Store @@ -41,3 +42,46 @@ Desktop.ini *.log *.bak *.meta~ # 임시 메타파일 + +# 추가된 항목들 +*.blend1 +*.blend1.meta +/[Rr]ecordings/ +*.DotSettings.user +mono_crash.* +*.unitypackage.meta +InitTestScene*.unity* +/ServerData +/[Aa]ssets/AddressableAssetsData/link.xml* +/[Aa]ssets/Addressables_Temp* +*.stacktrace +*.crash +*.xcuserdata +*.pbxuser +*.mode1v3 +*.mode2v3 +*.private +*.private.meta +^*.private.[0-9]+$ +^*.private.[0-9]+.meta$ +~UnityDirMonSyncFile~* +**/Assets/StreamingAssets/aa.meta +**/assets/streamingassets/*/aa/* + +# 대용량 미디어 파일 +*.mp4 +*.avi +*.mov +*.wmv +*.flv +*.mkv +*.webm +*.m4a +*.m4v +*.m4b +*.m4r +*.m4p + +# 특정 플러그인 +/Assets/Plugins/FiveMinuteChat +/Assets/Plugins/WebGLTemplates diff --git a/ProjectVG_Live2D_Assignment.md b/ProjectVG_Live2D_Assignment.md deleted file mode 100644 index fb8f29e..0000000 --- a/ProjectVG_Live2D_Assignment.md +++ /dev/null @@ -1,75 +0,0 @@ -from pathlib import Path - -# ProjectVG 기술과제: Live2D 모바일 빌드 - - -## 🎯 목표 -Unity Live2D SDK를 사용하여 **Android 또는 iOS 애플리케이션**으로 빌드하여 실행하는 것이 최종 요구사항입니다. - ---- - -## 📌 제약 사항 - -1. 반드시 `feature/live2d-build-assignment` 브랜치에서 작업해야 합니다. -2. 기본 제공 캐릭터가 마음에 들지 않으면 **원하는 캐릭터로 교체 가능**합니다. - - 기본 제공해드리는 모델은 `/Assts/Domain/Chracter/Model`에 있습니다. - - 단, Live2D SDK를 지원하는 모델이어야 합니다. - - [Live2D 지원 모델 참고 사이트](https://booth.pm/ko/search/live2d?tags%5B%5D=Live2D) -3. **커밋 컨벤션 및 프로젝트 구조 가이드는 반드시 준수**해야 합니다. - - 프로젝트 시작 전 Readme을 필독해주세요 - - 변경 시 사전 공유 필수입니다. -4. **OOP 및 SOLID 원칙을 반드시 준수**해야 합니다. - - Input 이벤트는 InputManager 등 별도의 컴포넌트를 만들어 외부에서 처리해야합니다(캐릭터 오브젝트가 직접 Input을 처리하는 구조는 금지입니다) (필수사항) - - 이벤트 및 액션 전달은 인터페이스 기반(`IInputHandler`, `IMotionTrigger` 등)의 느슨한 결합 구조로 설계되어야 합니다. (선택사항) - ---- - -## ✅ 필수 요구사항 - -### 1. 캐릭터 모션 팔로우 -- 클릭한 위치를 기준으로 캐릭터의 시선이 자연스럽게 따라가야 합니다. -- SDK에서 제공하는 `LookAt` 또는 `Parameter` API를 사용하여 구현합니다. -- 단순한 위치 이동이 아닌 얼굴 방향 회전으로 표현되어야 하며, 최대 회전 각도 제한이 적용되어야 합니다. - -### 2. 이벤트 기반 캐릭터 반응 -- 특정 Input 이벤트 발생 시, Live2D 캐릭터의 **표정 또는 모션**을 변경해야 합니다. -- 예시: - - 특정 부위를 터치 → 표정 변화 - - 버튼 클릭 → 특정 행동 실행 -- 터치 이벤트는 모델의 `HitArea`를 통해 특정 부위를 식별 후 처리되어야 합니다. -- 표정은 Expression, 모션은 Motion으로 분리하여 구성해야 합니다. - - -### 🔊 오디오 액션 -- 특정 Input 이벤트 발생 시 **소리를 출력**해야 합니다. - - 음성, 효과음 모두 허용되며, **3MB 이하의 WAV 파일**만 사용 가능합니다. -- AudioManager 등을 통한 재생 시스템 설계 권장 (싱글톤 or 이벤트 기반) -- AudioClip은 Resources 또는 Addressable 방식으로 로드합니다. - - -## 📱 모바일 빌드 및 최적화 - -- 모든 요구사항을 만족하는 **APK 생성** 후 테스트 -- 모바일 환경에서 다음 조건을 만족해야 합니다: - - **프레임 드랍 없음 (60fps 이상 유지)** - - **성능 최적화 완료** -- 빌드 타겟 최소 요구: - - Android: API Level 29 (Android 10) 이상 - - iOS: iOS 14 이상 -- Unity Profiler, Frame Debugger 등을 활용하여 성능 측정 - ---- - -## ✨ 추가 요구사항 (선택사항) - -### 💫 상호작용 효과 추가 -- 캐릭터 액션에 **파티클, 빛 효과 등**의 시각 효과를 추가합니다. -- **Unity URP(Universal Render Pipeline)** 기반에서 구현합니다. -- **5MB 이하의 에셋**만 사용 가능합니다. - -### 🎨 그래픽 품질 개선 -- 모바일 및 PC 해상도 대응 필수 (최소 1080x1920 기준, 가능하다면 1440 x 3040까지) -- **안티앨리어싱(MSAA 4x 이상)** 적용을 통해 계단 현상을 줄입니다. - - ---- \ No newline at end of file diff --git a/ignore.conf b/ignore.conf index 301d24b..fcd5697 100644 --- a/ignore.conf +++ b/ignore.conf @@ -1,3 +1,4 @@ +# Unity 기본 디렉토리 Library library Temp @@ -14,18 +15,29 @@ MemoryCaptures memorycaptures Logs logs + +# Asset Store 도구 **/Assets/AssetStoreTools **/assets/assetstoretools + +# Plastic SCM /Assets/Plugins/PlasticSCM* /assets/plugins/PlasticSCM* + +# Unity 임시 파일 *.private *.private.meta ^*.private.[0-9]+$ ^*.private.[0-9]+.meta$ +~UnityDirMonSyncFile~* + +# IDE 디렉토리 .vs .vscode .idea .gradle + +# IDE 생성 파일 ExportedObj .consulo *.csproj @@ -42,27 +54,80 @@ ExportedObj *.mdb *.opendb *.VC.db +*.vsconfig *.pidb.meta *.pdb.meta *.mdb.meta + +# Unity 시스템 파일 sysinfo.txt crashlytics-build.properties + +# 빌드 파일 *.apk *.aab *.app *.unitypackage -~UnityDirMonSyncFile~* + +# Addressables **/Assets/AddressableAssetsData/*/*.bin* **/assets/addressableassetsdata/*/*.bin* **/Assets/StreamingAssets/aa.meta **/assets/streamingassets/*/aa/* + +# 시스템 파일 .DS_Store* Thumbs.db Desktop.ini +# Git 관련 .git/ .gitignore .gitattributes *.iml .idea/ .git + +# 추가된 항목들 +*.blend1 +*.blend1.meta +/[Rr]ecordings/ +*.DotSettings.user +mono_crash.* +*.unitypackage.meta +InitTestScene*.unity* +/ServerData +/[Aa]ssets/AddressableAssetsData/link.xml* +/[Aa]ssets/Addressables_Temp* +*.stacktrace +*.crash +*.xcworkspace +*.xcuserdata +*.pbxuser +*.mode1v3 +*.mode2v3 +*.ipa +*.xcodeproj +*.cmo3 +*.psd +*.log +*.bak +*.meta~ + +# 대용량 미디어 파일 +*.mp4 +*.avi +*.mov +*.wmv +*.flv +*.mkv +*.webm +*.m4a +*.m4v +*.m4b +*.m4r +*.m4p + +# 특정 플러그인 +/Assets/Plugins/FiveMinuteChat +/Assets/Plugins/WebGLTemplates diff --git a/.vsconfig b/vsconfig similarity index 100% rename from .vsconfig rename to vsconfig From e1bd36d7842786ee0f9692df9e8f2e58cd42d7f0 Mon Sep 17 00:00:00 2001 From: WooSH Date: Thu, 14 Aug 2025 09:25:51 +0900 Subject: [PATCH 18/19] =?UTF-8?q?feat:=20=EC=99=B8=EB=B6=80=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/PULL_REQUEST_TEMPLATE.md | 18 +++++++++++ Assets/Editor.meta | 8 +++++ Assets/Editor/OpenDocsMenu.cs | 14 +++++++++ Assets/Editor/OpenDocsMenu.cs.meta | 2 ++ Docs/Conventions/Branching.md | 9 ++++++ Docs/Conventions/CodeStyle_CSharp.md | 23 +++++++++++++++ Docs/Conventions/Commit_Message.md | 41 ++++++++++++++++++++++++++ Docs/Conventions/PR_Notes.md | 17 +++++++++++ Docs/Conventions/PR_Review.md | 30 +++++++++++++++++++ Docs/Conventions/README.md | 16 ++++++++++ Docs/Conventions/Unity_Asset_Naming.md | 28 ++++++++++++++++++ 11 files changed, 206 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 Assets/Editor.meta create mode 100644 Assets/Editor/OpenDocsMenu.cs create mode 100644 Assets/Editor/OpenDocsMenu.cs.meta create mode 100644 Docs/Conventions/Branching.md create mode 100644 Docs/Conventions/CodeStyle_CSharp.md create mode 100644 Docs/Conventions/Commit_Message.md create mode 100644 Docs/Conventions/PR_Notes.md create mode 100644 Docs/Conventions/PR_Review.md create mode 100644 Docs/Conventions/README.md create mode 100644 Docs/Conventions/Unity_Asset_Naming.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..c9b8903 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## 제목 +- `type(scope): subject` + +## 변경 의도 +- 왜 이 변경이 필요한가 + +## 변경 내용 요약 +- 핵심 변경 1~3줄 + +## 영향 범위 +- 시스템/모듈/성능/보안 영향 + +## 검증 방법 +- 재현/확인 절차 + +## 관련 링크 +- 이슈: # +- 문서: Docs/... diff --git a/Assets/Editor.meta b/Assets/Editor.meta new file mode 100644 index 0000000..0baaa73 --- /dev/null +++ b/Assets/Editor.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: c880b556fd897fc4b8ff8fec8f532d2f +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Editor/OpenDocsMenu.cs b/Assets/Editor/OpenDocsMenu.cs new file mode 100644 index 0000000..e067144 --- /dev/null +++ b/Assets/Editor/OpenDocsMenu.cs @@ -0,0 +1,14 @@ +using UnityEditor; +using UnityEngine; + +public static class OpenDocsMenu +{ + /** + * 프로젝트 문서(루트 Docs/)를 파일 탐색기로 여는 메뉴 항목을 추가합니다. + */ + [MenuItem("Help/Open Project Docs")] + public static void OpenProjectDocs() + { + + } +} diff --git a/Assets/Editor/OpenDocsMenu.cs.meta b/Assets/Editor/OpenDocsMenu.cs.meta new file mode 100644 index 0000000..30ad15e --- /dev/null +++ b/Assets/Editor/OpenDocsMenu.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 51cc0cffee369434a853777240686338 \ No newline at end of file diff --git a/Docs/Conventions/Branching.md b/Docs/Conventions/Branching.md new file mode 100644 index 0000000..c3df9c5 --- /dev/null +++ b/Docs/Conventions/Branching.md @@ -0,0 +1,9 @@ +# 브랜치 전략 + +## 기본 원칙 +- `develop` 브랜치를 기준으로 작업합니다. +- 기능 단위 브랜치를 생성합니다. 예: `feature/chat-ui`, `fix/network-error`. + +## 머지 정책 +- PR 머지 전 테스트 통과가 필수입니다. +- 문서 및 설명은 `Docs/` 폴더에 저장합니다. diff --git a/Docs/Conventions/CodeStyle_CSharp.md b/Docs/Conventions/CodeStyle_CSharp.md new file mode 100644 index 0000000..bc11cc4 --- /dev/null +++ b/Docs/Conventions/CodeStyle_CSharp.md @@ -0,0 +1,23 @@ +# C# 코드 스타일 + +## C# 식별자 +| 대상 | 규칙 | 예시 | +|---|---|---| +| 클래스/구조체/열거형/속성/메서드 | PascalCase | ChatManager, MessageType, LoadConfig | +| 인터페이스 | IPascalCase | IChatService | +| 상수(const) | UPPER_SNAKE_CASE | DEFAULT_TIMEOUT_MS | +| 비공개 필드 | _camelCase | _sessionId | +| 직렬화 비공개 필드 | _camelCase | _gain | +| 매개변수/지역변수 | camelCase | messageText | +| 제네릭 매개변수 | TName | TItem, TResponse | + +## C# 패턴 +| 항목 | 규칙 | 예시 | +|---|---|---| +| 불리언 | Is/Has/Can/Should 접두사 | IsConnected, HasData | +| 이벤트 이름 | OnXxx / XxxChanged | OnMessageReceived, VolumeChanged | +| 비동기 메서드 | Async 접미사, ct 매개변수 | LoadAsync(CancellationToken ct) | +| 네임스페이스 | 폴더 구조 반영, 루트 ProjectVG | ProjectVG.Domain.Chat | +| 파일명 | 공개 루트 타입명과 동일 | ChatManager.cs | + +공개 API는 명령형 동사를 사용합니다(Initialize/Apply/Load 등). diff --git a/Docs/Conventions/Commit_Message.md b/Docs/Conventions/Commit_Message.md new file mode 100644 index 0000000..c8b236e --- /dev/null +++ b/Docs/Conventions/Commit_Message.md @@ -0,0 +1,41 @@ +# 커밋 메시지 규칙 + +## 구성 요소 +| 항목 | 필수 | 설명 | 예시 | +|---|---|---|---| +| Type | 예 | Commit의 종류(소문자). 이모지 사용 금지 | `feat`, `fix`, `refactor` | +| Scope | 선택 | Commit의 범위(기능/함수/페이지/API 등) | `login`, `signup`, `network` | +| Subject | 예 | 제목은 간결하게, 명사형 어미로 종료 | `회원가입 기능 추가` | +| Body | 선택 | 왜/어떻게 변경했는지 요약 | 변경 배경, 접근, 영향 범위 | +| Footer | 선택 | 이슈 트래킹/참고 사항 | `Closes #123` | + +## 헤더 예시 +``` +(optional scope): +``` + +- 주의: 이모지 사용 금지, type은 전부 소문자 (예: `feat:`, `fix(login):`) + +## 예시 +| type | 예시 메시지 | +|---|---| +| feat | `feat(login/signup): 회원가입 기능 추가` | +| fix | `fix(login): 로그인 기능 수정` | +| style | `style: 코드 포맷 변경` | +| refactor | `refactor(signup): 회원 가입 로직 개선` | +| file | `file: 이미지 파일 추가` | +| test | `test: 테스트 코드 추가` | +| docs | `docs: README.md 업데이트` | +| remove | `remove: 사용하지 않는 파일 제거` | +| ci | `ci: 자동 배포 스크립트 변동` | +| release | `release: 릴리즈 버전 1.0.3` | +| chore | `chore: 설정파일 수정` | + +## 메세지 구조 +``` +(optional scope): + +[optional body] + +[optional footer(s)] +``` diff --git a/Docs/Conventions/PR_Notes.md b/Docs/Conventions/PR_Notes.md new file mode 100644 index 0000000..8dba869 --- /dev/null +++ b/Docs/Conventions/PR_Notes.md @@ -0,0 +1,17 @@ +# PR 유의 사항 + +## 작성 팁 +- 작은 단위로 제출: 한 PR = 한 목적 +- 제목은 `type(scope): subject` 규칙 사용 +- 설명은 왜/어떻게/영향/검증 순으로 간결히 +- 큰 리네이밍/포맷 변경은 선행 PR로 분리 + +## 피해야 할 것 +- 스타일/리팩터/기능을 한 PR에 혼합 +- 불필요한 파일 추가/삭제 +- 테스트/빌드 실패 상태로 제출 +- 모호한 설명("수정", "변경")만 있는 경우 + +## 참고 +- 브레이킹 체인지는 명시하고 마이그레이션 절차 포함 +- 관련 문서/이슈 링크를 본문 하단에 추가 diff --git a/Docs/Conventions/PR_Review.md b/Docs/Conventions/PR_Review.md new file mode 100644 index 0000000..23fcda2 --- /dev/null +++ b/Docs/Conventions/PR_Review.md @@ -0,0 +1,30 @@ +# PR 가이드 + +## 목적 +- 변경 의도와 영향 범위를 빠르게 이해하고 안전하게 병합하기 위함 + +## 제출 기준 +- 테스트가 존재하는 경우 모두 통과해야 합니다 +- 린트/빌드 오류가 없어야 합니다 +- 불필요한 파일/포맷 변경을 포함하지 않습니다(필요 시 선행 PR로 분리) + +## 제목 규칙 +- 형식(권장): `type(scope): subject` + - 예: `feat(chat): 메시지 전송 애니메이션 추가` + - 소문자 type 사용, 명사형 subject + +## 설명 구성(간결) +- 배경/문제: 왜 이 변경이 필요한가 +- 해결: 어떻게 해결했는가(핵심 1~3줄) +- 영향 범위: 시스템/모듈/성능/보안 +- 검증: 재현 및 확인 방법 +- 롤백: 실패 시 되돌리는 방법(선택) + +## 링크 +- 관련 이슈(선택): `Closes #123`, `Relates-to #456` +- 관련 문서: `Docs/` 내 가이드/설계 링크 + +## 머지 기준 +- CI 전 단계 통과 필수(빌드/테스트/린트) +- CodeRabbit 자동 리뷰 코멘트 해결(있을 경우) +- 브레이킹 체인지 포함 시 주의 문구와 마이그레이션 절차 명시 diff --git a/Docs/Conventions/README.md b/Docs/Conventions/README.md new file mode 100644 index 0000000..778d9be --- /dev/null +++ b/Docs/Conventions/README.md @@ -0,0 +1,16 @@ +# Conventions + +- 목적: 팀 공통 규칙의 단일 참조 지점 +- 적용 범위: C# 코드 스타일, Unity 에셋 네이밍, 브랜치/커밋/PR 규칙 + +## 문서 목록 +- [C# 코드 스타일](./CodeStyle_CSharp.md) +- [Unity 에셋 네이밍](./Unity_Asset_Naming.md) +- [브랜치 전략](./Branching.md) +- [커밋 메시지 규칙](./Commit_Message.md) +- [PR 리뷰 가이드](./PR_Review.md) + +## 관련 가이드 +- `Assets/Docs/Guides/Unity_Naming_Conventions.md` +- `Assets/Docs/Guides/ProjectVG_Structure_Guide.md` +- `Assets/Docs/Guides/Manager_System_Guide.md` diff --git a/Docs/Conventions/Unity_Asset_Naming.md b/Docs/Conventions/Unity_Asset_Naming.md new file mode 100644 index 0000000..a762fd3 --- /dev/null +++ b/Docs/Conventions/Unity_Asset_Naming.md @@ -0,0 +1,28 @@ +# Unity 에셋 네이밍 + +## Unity 자산 +| 타입 | 규칙 | 예시 | +|---|---|---| +| 씬 | PascalCase, 역할 접미 허용(Main/Boot/Loading/Sample) | MainScene, LoadingScene | +| 프리팹 | PascalCase, UI 루트 접두사 Panel/Dialog/HUD | PanelChat, DialogConfirm | +| SO 에셋 | 타입명 기반 + 키 | NetworkConfig_Prod | +| Addressables | Domain/Category/Name | UI/Panels/PanelChat | +| 폴더 | PascalCase 단수형 | Domain/Character/Model | + +## UI 위젯 +| 위치 | 규칙 | 예시 | +|---|---|---| +| Hierarchy 이름 | 접두사 사용 Panel/Btn/Img/Txt/Input/Scroll/Toggle/Slider/Dropdown | PanelChat, BtnSend | +| 코드 필드명 | 의미 중심 camelCase | sendButton, titleText | + +## 예시(요약) +- MonoBehaviour: ScreenTapManager, Live2DModelManagerFacade +- ScriptableObject: Live2DModelConfig, AppEnvironmentConfig +- Prefab: PanelChat, ChatBubbleUI, AudioInputView +- Scene: MainScene, Live2DScene + +## 금지/주의 +- 범용어 남용: Data/Util/Helper/Manager(무의미 사용) +- 약어 조합: Cfg/Ctrllr/Svc +- 파일명 ≠ 타입명 불일치 +- 부정형 이벤트/플래그: NotReady → 긍정형 ShouldWait/IsReady From 71db53ec703edcad2116bf0dd0eb865620e3c643 Mon Sep 17 00:00:00 2001 From: WooSH Date: Thu, 14 Aug 2025 13:17:23 +0900 Subject: [PATCH 19/19] =?UTF-8?q?feat:=20=EC=BD=94=EB=93=9C=20=EB=A0=88?= =?UTF-8?q?=EB=B9=97=20=EA=B6=8C=EC=9E=A5=20=EC=88=98=EC=A0=95=EC=82=AC?= =?UTF-8?q?=ED=95=AD=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Core/DebugConsole/GameDebugConsoleManager.cs | 6 +++--- Assets/Domain/Chat/Service/ChatManager.cs | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Assets/Core/DebugConsole/GameDebugConsoleManager.cs b/Assets/Core/DebugConsole/GameDebugConsoleManager.cs index 77aeb98..8078a99 100644 --- a/Assets/Core/DebugConsole/GameDebugConsoleManager.cs +++ b/Assets/Core/DebugConsole/GameDebugConsoleManager.cs @@ -191,7 +191,7 @@ private void CreateLogEntryObject(LogEntry entry) { if (_logEntryPrefab == null || _logContentParent == null) return; - GameObject logEntryObj; + GameObject? logEntryObj; if (_settings?.UseObjectPooling == true) { logEntryObj = GetPooledObject(); @@ -272,7 +272,7 @@ private void InitializeObjectPool() } } - private GameObject GetPooledObject() + private GameObject? GetPooledObject() { if (_objectPool.Count > 0) { @@ -286,7 +286,7 @@ private GameObject GetPooledObject() return Instantiate(_logEntryPrefab, _logContentParent); } - return null!; + return null; } private void ReturnToPool(GameObject obj) diff --git a/Assets/Domain/Chat/Service/ChatManager.cs b/Assets/Domain/Chat/Service/ChatManager.cs index c34deb3..a018824 100644 --- a/Assets/Domain/Chat/Service/ChatManager.cs +++ b/Assets/Domain/Chat/Service/ChatManager.cs @@ -79,13 +79,12 @@ private void Start() private IEnumerator InitializeWhenReady() { float timeout = 5f; - float elapsedTime = 0f; + float startTime = Time.realtimeSinceStartup; // ChatBubblePanel이 준비될 때까지 대기 - while (_chatBubblePanel == null && elapsedTime < timeout) + while (_chatBubblePanel == null && (Time.realtimeSinceStartup - startTime) < timeout) { - yield return new WaitForEndOfFrame(); - elapsedTime += Time.deltaTime; + yield return new WaitForSecondsRealtime(0.1f); } if (_chatBubblePanel == null)