2026-06-19 UI, UI로직

This commit is contained in:
skrwns304@gmail.com
2026-06-19 14:27:40 +09:00
parent b751a9ed66
commit b1e85a5b89
549 changed files with 18058 additions and 20 deletions

View File

@@ -0,0 +1,514 @@
# 진실의 샘 UI 스크립트 사용 설명서
이 폴더는 `진실의 샘 UI`를 만들기 위한 Unity C# 스크립트 세트입니다.
진실의 샘은 질문에 답하면 선택지의 `Lie Amount`에 따라 코 게이지가 올라가고, 최종적으로 성공/실패를 판정하는 구조입니다.
---
## 1. 포함된 스크립트
| 파일 | 역할 | 오브젝트에 붙이는가 |
|---|---|---|
| `TruthChoiceData.cs` | 선택지 하나의 데이터 | 붙이지 않음 |
| `TruthQuestionData.cs` | 질문 하나의 데이터 | 붙이지 않음 |
| `TruthFountainData.cs` | 전체 질문 묶음 ScriptableObject | 붙이지 않음, asset 생성용 |
| `TruthChoiceButtonUI.cs` | 선택지 버튼 하나 표시/클릭 처리 | 선택지 버튼 3개 각각 |
| `TruthFountainUI.cs` | 질문, 선택지, 코 게이지, 결과창 표시 | `TruthFountainUI` 오브젝트 |
| `TruthFountainGameManager.cs` | 게임 진행, 점수 계산, 성공/실패 판정 | `TruthFountainSystem` |
| `TruthFountainReward.cs` | 성공 보상 지급 | `TruthFountainSystem` |
| `TruthFountainOpenClose.cs` | UI 열기/닫기, VR 카메라 앞 배치 | `TruthFountainUI` 또는 별도 시스템 오브젝트 |
---
## 2. Unity 폴더 위치
추천 폴더 구조입니다.
```text
Assets
└── My project
└── TruthFountain
├── Data
├── Prefabs
├── Scripts
└── Sprites
```
스크립트는 여기에 넣으세요.
```text
Assets/My project/TruthFountain/Scripts
```
데이터 asset은 여기에 만드는 것을 추천합니다.
```text
Assets/My project/TruthFountain/Data
```
---
## 3. 권장 Hierarchy
```text
TruthFountainCanvas
└── TruthFountainUI
├── TruthFountainUI.cs
├── TruthFountainOpenClose.cs
├── Background
├── TitleText
├── QuestionPanel
│ ├── QuestionBG
│ ├── QuestionText
│ └── QuestionHintText
├── ChoiceRoot
│ ├── ChoiceButton_01
│ │ └── TruthChoiceButtonUI.cs
│ ├── ChoiceButton_02
│ │ └── TruthChoiceButtonUI.cs
│ └── ChoiceButton_03
│ └── TruthChoiceButtonUI.cs
├── NoseGaugePanel
│ ├── NoseGaugeBG
│ ├── NoseGaugeFill
│ ├── NoseIcon
│ └── NoseValueText
├── ProgressText
├── ResultText
├── FinalResultPanel
│ ├── FinalResultBG
│ ├── FinalTitleText
│ ├── FinalDescriptionText
│ └── ConfirmButton
│ └── ConfirmButtonText
└── CloseButton
└── CloseText
TruthFountainSystem
├── TruthFountainGameManager.cs
└── TruthFountainReward.cs
```
---
## 4. TruthFountainData asset 만들기
Project 창에서 우클릭합니다.
```text
Create
→ Adventure
→ Truth Fountain
→ Truth Fountain Data
```
파일 이름 예시:
```text
TruthFountainData_Default
```
설정 예시:
```text
Title: 진실의 샘
Max Lie Score: 3
Fail Immediately At Max Lie Score: 체크
Result Delay: 1
Success Title: 진실의 샘이 빛납니다
Success Description: 당신의 진심이 샘에 닿았습니다. 기억의 조각을 획득했습니다.
Fail Title: 코가 너무 길어졌습니다
Fail Description: 샘이 당신의 대답을 믿지 못했습니다. 다시 도전해보세요.
```
질문은 `Questions` 배열에 추가합니다.
---
## 5. 질문 데이터 입력 예시
### 질문 1
```text
Question Text:
친구를 돕기 위해 거짓말을 해도 될까?
Hint Text:
당신의 선택에 따라 코가 반응합니다.
Choice 1:
Choice Text: 솔직하게 말한다
Lie Amount: 0
Result Message: 샘의 물결이 맑게 빛납니다.
Choice 2:
Choice Text: 좋은 의도라면 거짓말한다
Lie Amount: 1
Result Message: 피노키오의 코가 조금 길어졌습니다.
```
### 질문 2
```text
Question Text:
위험을 피하려고 약속을 어겨도 될까?
Choice 1:
Choice Text: 약속을 지킨다
Lie Amount: 0
Result Message: 샘이 조용히 빛납니다.
Choice 2:
Choice Text: 아무 말 없이 도망친다
Lie Amount: 1
Result Message: 코끝이 불편하게 간질거립니다.
```
### 질문 3
```text
Question Text:
보상을 얻기 위해 모르는 척해도 될까?
Choice 1:
Choice Text: 사실을 말한다
Lie Amount: 0
Result Message: 맑은 물방울이 떠오릅니다.
Choice 2:
Choice Text: 모르는 척한다
Lie Amount: 1
Result Message: 샘이 잠시 어두워집니다.
```
### 질문 4
```text
Question Text:
실수를 숨기면 모두가 편해질까?
Choice 1:
Choice Text: 실수를 인정한다
Lie Amount: 0
Result Message: 물결이 부드럽게 퍼집니다.
Choice 2:
Choice Text: 아무도 모르게 숨긴다
Lie Amount: 1
Result Message: 코가 길어지는 느낌이 듭니다.
```
### 질문 5
```text
Question Text:
진실이 아파도 말해야 할까?
Choice 1:
Choice Text: 조심스럽게 진실을 말한다
Lie Amount: 0
Result Message: 샘이 밝게 빛납니다.
Choice 2:
Choice Text: 상처 주지 않기 위해 숨긴다
Lie Amount: 1
Result Message: 샘의 빛이 흔들립니다.
```
---
## 6. `TruthChoiceButtonUI.cs` 붙이기
선택지 버튼 3개에 각각 붙입니다.
```text
ChoiceButton_01 → TruthChoiceButtonUI
ChoiceButton_02 → TruthChoiceButtonUI
ChoiceButton_03 → TruthChoiceButtonUI
```
각 버튼에서 연결할 것:
```text
Button → 자기 자신의 Button 컴포넌트
Background Image → 자기 자신의 Image 컴포넌트
Choice Text → ChoiceText
```
Sprite가 아직 없다면 아래는 비워도 됩니다.
```text
Normal Sprite
Selected Sprite
Truth Sprite
Lie Sprite
```
Sprite가 없으면 색상으로 버튼 상태가 표시됩니다.
---
## 7. `TruthFountainUI.cs` 붙이기
붙일 위치:
```text
TruthFountainCanvas
└── TruthFountainUI
└── TruthFountainUI.cs
```
Inspector 연결:
```text
UI Root → TruthFountainUI
Title Text → TitleText
Question Text → QuestionPanel / QuestionText
Question Hint Text → QuestionPanel / QuestionHintText
Progress Text → ProgressText
Result Text → ResultText
Choice Buttons Size: 3
Element 0 → ChoiceButton_01의 TruthChoiceButtonUI
Element 1 → ChoiceButton_02의 TruthChoiceButtonUI
Element 2 → ChoiceButton_03의 TruthChoiceButtonUI
Nose Gauge Fill → NoseGaugePanel / NoseGaugeFill
Nose Value Text → NoseGaugePanel / NoseValueText
Final Result Panel → FinalResultPanel
Final Title Text → FinalResultPanel / FinalTitleText
Final Description Text → FinalResultPanel / FinalDescriptionText
Confirm Button → FinalResultPanel / ConfirmButton
```
`NoseGaugeFill`의 Image 설정:
```text
Image Type: Filled
Fill Method: Vertical
Fill Origin: Bottom
Fill Amount: 0
```
---
## 8. `TruthFountainGameManager.cs` 붙이기
빈 오브젝트를 만듭니다.
```text
TruthFountainSystem
```
여기에 붙입니다.
```text
TruthFountainSystem
└── TruthFountainGameManager.cs
```
Inspector 연결:
```text
Fountain Data → TruthFountainData_Default
UI → TruthFountainUI 오브젝트의 TruthFountainUI 컴포넌트
Reward → TruthFountainSystem의 TruthFountainReward 컴포넌트
Start On Awake → 테스트할 때만 체크
Show Debug Log → 체크
```
테스트할 때는 `Start On Awake`를 켜면 Play 직후 바로 질문이 표시됩니다.
실제 게임에서는 `TruthFountainOpenClose.Open()`에서 시작하게 하는 것을 추천합니다.
---
## 9. `TruthFountainReward.cs` 붙이기
붙일 위치:
```text
TruthFountainSystem
└── TruthFountainReward.cs
```
기본 설정:
```text
Give Only Once → 체크
Show Debug Log → 체크
```
### 기억의 조각 보상과 연결하는 방법 A
이미 만든 `MemoryPieceReward.cs``TruthFountainSystem`에 같이 붙입니다.
```text
TruthFountainSystem
├── TruthFountainGameManager
├── TruthFountainReward
└── MemoryPieceReward
```
`TruthFountainReward`에서:
```text
Reward Receiver → MemoryPieceReward 컴포넌트
Reward Method Name → GiveReward
```
이렇게 하면 진실의 샘 성공 시 `MemoryPieceReward.GiveReward()`가 자동 호출됩니다.
### 기억의 조각 보상과 연결하는 방법 B
`TruthFountainReward``On Reward Given` 이벤트에 직접 연결합니다.
```text
On Reward Given
→ MemoryPieceReward
→ GiveReward()
```
방 클리어 상태도 같이 바꾸고 싶으면 `On Reward Given` 이벤트에 `RoomProgressManager.MarkRoomCleared(RoomData)` 또는 `RoomProgressManager.MarkMemoryPieceCollected(RoomData)`를 추가로 연결할 수 있습니다.
---
## 10. `TruthFountainOpenClose.cs` 붙이기
붙일 위치 추천:
```text
TruthFountainCanvas
└── TruthFountainUI
└── TruthFountainOpenClose.cs
```
Inspector 연결:
```text
Truth Fountain Root → TruthFountainUI
Game Manager → TruthFountainSystem의 TruthFountainGameManager
Target Camera → Main Camera 또는 XR Camera
Open On Start → 테스트할 때만 체크
Start Game On Open → 체크
Place In Front Of Camera On Open → 체크
Face Camera On Open → 체크
Distance From Camera → 2.2
Vertical Offset → -0.1
```
닫기 버튼 연결:
```text
CloseButton OnClick
→ TruthFountainUI
→ TruthFountainOpenClose.Close()
```
방 선택 UI에서 진실의 샘 방 입장 시 연결:
```text
RoomEnterHandler 또는 진실의 샘 입장 이벤트
→ TruthFountainOpenClose.Open()
```
---
## 11. 최종 작동 흐름
```text
TruthFountainOpenClose.Open()
→ TruthFountainUI 켜짐
→ TruthFountainGameManager.StartGame()
→ 첫 질문 표시
→ 플레이어 선택지 클릭
→ lieScore 증가 또는 유지
→ NoseGaugeFill 갱신
→ 다음 질문 표시
→ 모든 질문 종료
→ 성공/실패 판정
→ 성공 시 TruthFountainReward.GiveReward()
→ 최종 결과 패널 표시
```
---
## 12. 테스트 순서
```text
1. TruthFountainData_Default asset 만들기
2. 질문 5개 입력하기
3. ChoiceButton 3개에 TruthChoiceButtonUI 붙이기
4. TruthFountainUI 오브젝트에 TruthFountainUI.cs 붙이기
5. TruthFountainSystem 만들기
6. TruthFountainGameManager 붙이기
7. TruthFountainReward 붙이기
8. GameManager에 Data, UI, Reward 연결하기
9. Start On Awake 체크
10. Play 누르기
11. 질문이 표시되는지 확인
12. 선택지를 누르면 ResultText와 NoseGaugeFill이 바뀌는지 확인
13. 최종 성공/실패 패널이 뜨는지 확인
14. 성공 시 보상 이벤트가 호출되는지 확인
```
---
## 13. 자주 나는 오류
### 질문이 안 뜸
확인할 것:
```text
TruthFountainGameManager의 Fountain Data가 비어 있지 않은가?
TruthFountainData의 Questions 배열에 질문이 들어 있는가?
TruthFountainGameManager의 UI가 연결되어 있는가?
```
### 선택지 버튼이 안 보임
확인할 것:
```text
TruthQuestionData의 Choices 배열에 선택지가 들어 있는가?
TruthFountainUI의 Choice Buttons 배열이 연결되어 있는가?
각 ChoiceButton에 TruthChoiceButtonUI가 붙어 있는가?
```
### 코 게이지가 안 움직임
확인할 것:
```text
NoseGaugeFill이 TruthFountainUI에 연결되어 있는가?
NoseGaugeFill의 Image Type이 Filled인가?
Fill Method가 Vertical인가?
선택지의 Lie Amount가 1 이상인가?
```
### 버튼 클릭이 안 됨
확인할 것:
```text
Canvas에 Graphic Raycaster가 있는가?
씬에 EventSystem이 있는가?
Button Image의 Raycast Target이 켜져 있는가?
VR이라면 XR UI Input Module과 XR Ray Interactor UI Interaction이 켜져 있는가?
```
---
## 14. 현재 단계에서 가장 먼저 할 것
처음에는 보상 연결보다 아래 3가지만 먼저 확인하세요.
```text
1. 질문 표시
2. 선택지 클릭
3. 코 게이지 증가
```
이 3개가 정상 작동하면 진실의 샘 UI의 핵심은 완성입니다.

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: cac5f4ffa0618d242b30be932e29daf6
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,133 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
[RequireComponent(typeof(Button))]
public class TruthChoiceButtonUI : MonoBehaviour
{
[Header("UI References")]
[SerializeField] private Button button;
[SerializeField] private Image backgroundImage;
[SerializeField] private TMP_Text choiceText;
[Header("Optional Sprites")]
[SerializeField] private Sprite normalSprite;
[SerializeField] private Sprite selectedSprite;
[SerializeField] private Sprite truthSprite;
[SerializeField] private Sprite lieSprite;
[Header("Fallback Colors")]
[SerializeField] private Color normalColor = new Color(0.08f, 0.35f, 0.42f, 0.9f);
[SerializeField] private Color selectedColor = new Color(0.2f, 0.75f, 0.85f, 1f);
[SerializeField] private Color truthColor = new Color(0.25f, 0.85f, 0.65f, 1f);
[SerializeField] private Color lieColor = new Color(0.85f, 0.35f, 0.35f, 1f);
private TruthChoiceData currentChoice;
private TruthFountainGameManager gameManager;
private void Reset()
{
button = GetComponent<Button>();
backgroundImage = GetComponent<Image>();
choiceText = GetComponentInChildren<TMP_Text>(true);
}
private void Awake()
{
if (button == null)
{
button = GetComponent<Button>();
}
if (backgroundImage == null)
{
backgroundImage = GetComponent<Image>();
}
if (choiceText == null)
{
choiceText = GetComponentInChildren<TMP_Text>(true);
}
button.onClick.RemoveListener(HandleClick);
button.onClick.AddListener(HandleClick);
}
public void Setup(TruthChoiceData choiceData, TruthFountainGameManager manager)
{
currentChoice = choiceData;
gameManager = manager;
bool hasChoice = currentChoice != null && currentChoice.IsValid();
gameObject.SetActive(hasChoice);
if (!hasChoice)
{
return;
}
if (choiceText != null)
{
choiceText.text = currentChoice.ChoiceText;
}
SetInteractable(true);
SetVisual(normalSprite, normalColor);
}
public void SetInteractable(bool value)
{
if (button != null)
{
button.interactable = value;
}
}
public void ShowSelectedFeedback()
{
SetVisual(selectedSprite, selectedColor);
}
public void ShowTruthOrLieFeedback()
{
if (currentChoice != null && currentChoice.MarkAsTruthChoice)
{
SetVisual(truthSprite, truthColor);
}
else
{
SetVisual(lieSprite, lieColor);
}
}
public void ResetVisual()
{
SetVisual(normalSprite, normalColor);
}
private void HandleClick()
{
if (currentChoice == null || gameManager == null)
{
return;
}
ShowSelectedFeedback();
gameManager.SelectChoice(currentChoice, this);
}
private void SetVisual(Sprite sprite, Color color)
{
if (backgroundImage == null)
{
return;
}
if (sprite != null)
{
backgroundImage.sprite = sprite;
}
backgroundImage.color = color;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ff4308a02dc0dbd4d90489f2f89dc837

View File

@@ -0,0 +1,30 @@
using System;
using UnityEngine;
[Serializable]
public class TruthChoiceData
{
[Header("Choice")]
[TextArea(1, 3)]
[SerializeField] private string choiceText;
[Header("Result")]
[Min(0)]
[SerializeField] private int lieAmount = 0;
[TextArea(1, 4)]
[SerializeField] private string resultMessage;
[Tooltip("선택 후 버튼을 정답/진실처럼 강조할지 여부입니다. lieAmount가 0이면 자동으로 진실 선택처럼 볼 수 있습니다.")]
[SerializeField] private bool markAsTruthChoice;
public string ChoiceText => choiceText;
public int LieAmount => lieAmount;
public string ResultMessage => resultMessage;
public bool MarkAsTruthChoice => markAsTruthChoice || lieAmount <= 0;
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(choiceText);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 02d98b539fd31b347be44b5595f703b6

View File

@@ -0,0 +1,55 @@
using UnityEngine;
[CreateAssetMenu(fileName = "TruthFountainData_Default", menuName = "Adventure/Truth Fountain/Truth Fountain Data")]
public class TruthFountainData : ScriptableObject
{
[Header("Basic")]
[SerializeField] private string title = "진실의 샘";
[Header("Questions")]
[SerializeField] private TruthQuestionData[] questions;
[Header("Rules")]
[Min(1)]
[SerializeField] private int maxLieScore = 3;
[Tooltip("켜두면 거짓 점수가 최대치에 도달하는 순간 바로 실패합니다. 끄면 모든 질문이 끝난 뒤 판정합니다.")]
[SerializeField] private bool failImmediatelyAtMaxLieScore = true;
[Min(0f)]
[SerializeField] private float resultDelay = 1.0f;
[Header("Success Result")]
[SerializeField] private string successTitle = "진실의 샘이 빛납니다";
[TextArea(2, 5)]
[SerializeField] private string successDescription = "당신의 진심이 샘에 닿았습니다.\n기억의 조각을 획득했습니다.";
[Header("Fail Result")]
[SerializeField] private string failTitle = "코가 너무 길어졌습니다";
[TextArea(2, 5)]
[SerializeField] private string failDescription = "샘이 당신의 대답을 믿지 못했습니다.\n다시 도전해보세요.";
public string Title => title;
public TruthQuestionData[] Questions => questions;
public int MaxLieScore => Mathf.Max(1, maxLieScore);
public bool FailImmediatelyAtMaxLieScore => failImmediatelyAtMaxLieScore;
public float ResultDelay => Mathf.Max(0f, resultDelay);
public string SuccessTitle => successTitle;
public string SuccessDescription => successDescription;
public string FailTitle => failTitle;
public string FailDescription => failDescription;
public int QuestionCount => questions == null ? 0 : questions.Length;
public TruthQuestionData GetQuestion(int index)
{
if (questions == null || index < 0 || index >= questions.Length)
{
return null;
}
return questions[index];
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0f6934cb9bb93ca44815ef21af5126f8

View File

@@ -0,0 +1,230 @@
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
public class TruthFountainGameManager : MonoBehaviour
{
[Header("References")]
[SerializeField] private TruthFountainData fountainData;
[SerializeField] private TruthFountainUI ui;
[SerializeField] private TruthFountainReward reward;
[Header("Start Option")]
[SerializeField] private bool startOnAwake = false;
[Header("Events")]
public UnityEvent onGameStarted;
public UnityEvent onQuestionAnswered;
public UnityEvent onGameSuccess;
public UnityEvent onGameFailed;
public UnityEvent onFinalConfirmed;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private int currentQuestionIndex;
private int lieScore;
private bool isRunning;
private bool isWaitingForNextQuestion;
private bool finalSuccess;
private Coroutine choiceRoutine;
public int CurrentQuestionIndex => currentQuestionIndex;
public int LieScore => lieScore;
public bool IsRunning => isRunning;
private void Awake()
{
if (ui == null)
{
ui = FindFirstObjectByType<TruthFountainUI>();
}
if (reward == null)
{
reward = GetComponent<TruthFountainReward>();
}
if (ui != null)
{
ui.Bind(this);
}
}
private void Start()
{
if (startOnAwake)
{
StartGame();
}
}
public void StartGame()
{
if (fountainData == null)
{
Debug.LogWarning($"[{nameof(TruthFountainGameManager)}] TruthFountainData가 연결되지 않았습니다.");
return;
}
if (fountainData.QuestionCount <= 0)
{
Debug.LogWarning($"[{nameof(TruthFountainGameManager)}] 질문 데이터가 비어 있습니다.");
return;
}
if (choiceRoutine != null)
{
StopCoroutine(choiceRoutine);
choiceRoutine = null;
}
currentQuestionIndex = 0;
lieScore = 0;
isRunning = true;
isWaitingForNextQuestion = false;
finalSuccess = false;
if (ui != null)
{
ui.Initialize(fountainData);
ui.ShowQuestion(fountainData.GetQuestion(currentQuestionIndex), currentQuestionIndex, fountainData.QuestionCount, this);
ui.UpdateNoseGauge(lieScore, fountainData.MaxLieScore);
}
onGameStarted?.Invoke();
if (showDebugLog)
{
Debug.Log($"[{nameof(TruthFountainGameManager)}] 진실의 샘 시작");
}
}
public void SelectChoice(TruthChoiceData choice, TruthChoiceButtonUI selectedButton = null)
{
if (!isRunning || isWaitingForNextQuestion || choice == null)
{
return;
}
if (choiceRoutine != null)
{
StopCoroutine(choiceRoutine);
}
choiceRoutine = StartCoroutine(HandleChoiceRoutine(choice, selectedButton));
}
private IEnumerator HandleChoiceRoutine(TruthChoiceData choice, TruthChoiceButtonUI selectedButton)
{
isWaitingForNextQuestion = true;
lieScore = Mathf.Max(0, lieScore + choice.LieAmount);
if (ui != null)
{
ui.SetChoiceButtonsInteractable(false);
ui.ShowChoiceFeedback(selectedButton);
ui.SetResultMessage(choice.ResultMessage, true);
ui.UpdateNoseGauge(lieScore, fountainData.MaxLieScore);
}
onQuestionAnswered?.Invoke();
if (showDebugLog)
{
Debug.Log($"[{nameof(TruthFountainGameManager)}] 선택: {choice.ChoiceText}, lie +{choice.LieAmount}, 현재 lieScore={lieScore}");
}
yield return new WaitForSeconds(fountainData.ResultDelay);
bool reachedMaxLieScore = lieScore >= fountainData.MaxLieScore;
if (reachedMaxLieScore && fountainData.FailImmediatelyAtMaxLieScore)
{
FinishGame(false);
yield break;
}
currentQuestionIndex++;
if (currentQuestionIndex >= fountainData.QuestionCount)
{
bool success = lieScore < fountainData.MaxLieScore;
FinishGame(success);
yield break;
}
if (ui != null)
{
ui.ShowQuestion(fountainData.GetQuestion(currentQuestionIndex), currentQuestionIndex, fountainData.QuestionCount, this);
}
isWaitingForNextQuestion = false;
choiceRoutine = null;
}
private void FinishGame(bool success)
{
isRunning = false;
isWaitingForNextQuestion = false;
finalSuccess = success;
choiceRoutine = null;
if (ui != null)
{
string title = success ? fountainData.SuccessTitle : fountainData.FailTitle;
string description = success ? fountainData.SuccessDescription : fountainData.FailDescription;
ui.SetResultMessage(string.Empty, false);
ui.ShowFinalResult(true, title, description);
}
if (success)
{
reward?.GiveReward();
onGameSuccess?.Invoke();
}
else
{
onGameFailed?.Invoke();
}
if (showDebugLog)
{
Debug.Log($"[{nameof(TruthFountainGameManager)}] 게임 종료: {(success ? "" : "")}, lieScore={lieScore}");
}
}
public void ConfirmFinalResult()
{
if (ui != null)
{
ui.ShowFinalResult(false, string.Empty, string.Empty);
}
onFinalConfirmed?.Invoke();
if (showDebugLog)
{
Debug.Log($"[{nameof(TruthFountainGameManager)}] 최종 결과 확인: {(finalSuccess ? "" : "")}");
}
}
public void ResetGameOnly()
{
if (choiceRoutine != null)
{
StopCoroutine(choiceRoutine);
choiceRoutine = null;
}
currentQuestionIndex = 0;
lieScore = 0;
isRunning = false;
isWaitingForNextQuestion = false;
if (ui != null)
{
ui.Initialize(fountainData);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 70c77a1d5b979604cbafc1108512420b

View File

@@ -0,0 +1,140 @@
using UnityEngine;
public class TruthFountainOpenClose : MonoBehaviour
{
[Header("References")]
[SerializeField] private GameObject truthFountainRoot;
[SerializeField] private TruthFountainGameManager gameManager;
[SerializeField] private Transform targetCamera;
[Header("Open Option")]
[SerializeField] private bool openOnStart = false;
[SerializeField] private bool startGameOnOpen = true;
[Header("VR Placement")]
[SerializeField] private bool placeInFrontOfCameraOnOpen = true;
[SerializeField] private bool faceCameraOnOpen = true;
[Min(0.1f)]
[SerializeField] private float distanceFromCamera = 2.2f;
[SerializeField] private float verticalOffset = -0.1f;
private void Reset()
{
truthFountainRoot = gameObject;
gameManager = GetComponentInParent<TruthFountainGameManager>();
}
private void Awake()
{
if (truthFountainRoot == null)
{
truthFountainRoot = gameObject;
}
if (gameManager == null)
{
gameManager = FindFirstObjectByType<TruthFountainGameManager>();
}
if (targetCamera == null && Camera.main != null)
{
targetCamera = Camera.main.transform;
}
}
private void Start()
{
if (openOnStart)
{
Open();
}
else
{
Close();
}
}
public void Open()
{
if (placeInFrontOfCameraOnOpen)
{
PlaceInFrontOfCamera();
}
if (truthFountainRoot != null)
{
truthFountainRoot.SetActive(true);
}
if (startGameOnOpen && gameManager != null)
{
gameManager.StartGame();
}
}
public void Close()
{
if (truthFountainRoot != null)
{
truthFountainRoot.SetActive(false);
}
}
public void Toggle()
{
if (truthFountainRoot == null)
{
return;
}
if (truthFountainRoot.activeSelf)
{
Close();
}
else
{
Open();
}
}
public void Restart()
{
if (truthFountainRoot != null && !truthFountainRoot.activeSelf)
{
truthFountainRoot.SetActive(true);
}
gameManager?.StartGame();
}
private void PlaceInFrontOfCamera()
{
if (targetCamera == null)
{
return;
}
Transform rootTransform = truthFountainRoot != null ? truthFountainRoot.transform : transform;
Vector3 forward = targetCamera.forward;
forward.y = 0f;
if (forward.sqrMagnitude < 0.001f)
{
forward = targetCamera.forward;
}
forward.Normalize();
rootTransform.position = targetCamera.position + forward * distanceFromCamera + Vector3.up * verticalOffset;
if (faceCameraOnOpen)
{
Vector3 directionToCamera = rootTransform.position - targetCamera.position;
directionToCamera.y = 0f;
if (directionToCamera.sqrMagnitude > 0.001f)
{
rootTransform.rotation = Quaternion.LookRotation(directionToCamera.normalized, Vector3.up);
}
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: b02efc909d86c8a469c91b2981052e17

View File

@@ -0,0 +1,80 @@
using System;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;
public class TruthFountainReward : MonoBehaviour
{
[Header("Reward Control")]
[SerializeField] private bool giveOnlyOnce = true;
[Header("Optional Auto Call")]
[Tooltip("MemoryPieceReward 같은 컴포넌트를 넣고 methodName을 GiveReward로 두면 성공 시 자동 호출됩니다. 비워두고 Events에 직접 연결해도 됩니다.")]
[SerializeField] private MonoBehaviour rewardReceiver;
[SerializeField] private string rewardMethodName = "GiveReward";
[Header("Events")]
public UnityEvent onRewardGiven;
public UnityEvent onRewardAlreadyGiven;
[Header("Debug")]
[SerializeField] private bool showDebugLog = true;
private bool alreadyGiven;
public bool AlreadyGiven => alreadyGiven;
public void GiveReward()
{
if (giveOnlyOnce && alreadyGiven)
{
onRewardAlreadyGiven?.Invoke();
if (showDebugLog)
{
Debug.Log($"[{nameof(TruthFountainReward)}] 이미 지급된 진실의 샘 보상입니다.");
}
return;
}
TryCallRewardReceiver();
alreadyGiven = true;
onRewardGiven?.Invoke();
if (showDebugLog)
{
Debug.Log($"[{nameof(TruthFountainReward)}] 진실의 샘 보상을 지급했습니다.");
}
}
public void ResetReward()
{
alreadyGiven = false;
}
private void TryCallRewardReceiver()
{
if (rewardReceiver == null || string.IsNullOrWhiteSpace(rewardMethodName))
{
return;
}
MethodInfo method = rewardReceiver.GetType().GetMethod(
rewardMethodName,
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic,
null,
Type.EmptyTypes,
null
);
if (method == null)
{
Debug.LogWarning($"[{nameof(TruthFountainReward)}] {rewardReceiver.name}에서 {rewardMethodName}() 메서드를 찾지 못했습니다.");
return;
}
method.Invoke(rewardReceiver, null);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f4e260c3dc9c058478480d3d19f8eb96

View File

@@ -0,0 +1,191 @@
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class TruthFountainUI : MonoBehaviour
{
[Header("Root")]
[SerializeField] private GameObject uiRoot;
[Header("Texts")]
[SerializeField] private TMP_Text titleText;
[SerializeField] private TMP_Text questionText;
[SerializeField] private TMP_Text questionHintText;
[SerializeField] private TMP_Text progressText;
[SerializeField] private TMP_Text resultText;
[Header("Choices")]
[SerializeField] private TruthChoiceButtonUI[] choiceButtons;
[Header("Nose Gauge")]
[SerializeField] private Image noseGaugeFill;
[SerializeField] private TMP_Text noseValueText;
[Header("Final Result")]
[SerializeField] private GameObject finalResultPanel;
[SerializeField] private TMP_Text finalTitleText;
[SerializeField] private TMP_Text finalDescriptionText;
[SerializeField] private Button confirmButton;
private TruthFountainGameManager gameManager;
private void Reset()
{
uiRoot = gameObject;
}
public void Bind(TruthFountainGameManager manager)
{
gameManager = manager;
if (confirmButton != null)
{
confirmButton.onClick.RemoveAllListeners();
confirmButton.onClick.AddListener(HandleConfirmClicked);
}
}
public void SetVisible(bool visible)
{
GameObject targetRoot = uiRoot != null ? uiRoot : gameObject;
targetRoot.SetActive(visible);
}
public void Initialize(TruthFountainData data)
{
if (titleText != null)
{
titleText.text = data != null ? data.Title : "진실의 샘";
}
SetResultMessage(string.Empty, false);
ShowFinalResult(false, string.Empty, string.Empty);
UpdateNoseGauge(0, data != null ? data.MaxLieScore : 1);
}
public void ShowQuestion(TruthQuestionData question, int questionIndex, int totalQuestions, TruthFountainGameManager manager)
{
if (questionText != null)
{
questionText.text = question != null ? question.QuestionText : "질문 데이터가 없습니다.";
}
if (questionHintText != null)
{
questionHintText.text = question != null ? question.HintText : string.Empty;
}
if (progressText != null)
{
progressText.text = $"질문 {questionIndex + 1} / {Mathf.Max(1, totalQuestions)}";
}
SetResultMessage(string.Empty, false);
SetupChoiceButtons(question, manager);
}
public void SetupChoiceButtons(TruthQuestionData question, TruthFountainGameManager manager)
{
TruthChoiceData[] choices = question != null ? question.Choices : null;
for (int i = 0; i < choiceButtons.Length; i++)
{
TruthChoiceData choice = choices != null && i < choices.Length ? choices[i] : null;
if (choiceButtons[i] != null)
{
choiceButtons[i].Setup(choice, manager);
}
}
}
public void SetChoiceButtonsInteractable(bool interactable)
{
foreach (TruthChoiceButtonUI choiceButton in choiceButtons)
{
if (choiceButton != null && choiceButton.gameObject.activeSelf)
{
choiceButton.SetInteractable(interactable);
}
}
}
public void ShowChoiceFeedback(TruthChoiceButtonUI selectedButton)
{
foreach (TruthChoiceButtonUI choiceButton in choiceButtons)
{
if (choiceButton == null || !choiceButton.gameObject.activeSelf)
{
continue;
}
if (choiceButton == selectedButton)
{
choiceButton.ShowTruthOrLieFeedback();
}
else
{
choiceButton.ResetVisual();
}
}
}
public void SetResultMessage(string message, bool visible = true)
{
if (resultText == null)
{
return;
}
resultText.text = message;
resultText.gameObject.SetActive(visible && !string.IsNullOrWhiteSpace(message));
}
public void UpdateNoseGauge(int lieScore, int maxLieScore)
{
int safeMax = Mathf.Max(1, maxLieScore);
float fillAmount = Mathf.Clamp01((float)lieScore / safeMax);
if (noseGaugeFill != null)
{
noseGaugeFill.fillAmount = fillAmount;
}
if (noseValueText != null)
{
noseValueText.text = $"코 길이 {Mathf.RoundToInt(fillAmount * 100f)}%";
}
}
public void ShowFinalResult(bool visible, string title, string description)
{
if (finalResultPanel != null)
{
finalResultPanel.SetActive(visible);
}
if (finalTitleText != null)
{
finalTitleText.text = title;
}
if (finalDescriptionText != null)
{
finalDescriptionText.text = description;
}
SetChoiceButtonsInteractable(!visible);
}
private void HandleConfirmClicked()
{
if (gameManager != null)
{
gameManager.ConfirmFinalResult();
}
else
{
ShowFinalResult(false, string.Empty, string.Empty);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f7f6a091efa74d648b3100bbbc3836ed

View File

@@ -0,0 +1,25 @@
using System;
using UnityEngine;
[Serializable]
public class TruthQuestionData
{
[Header("Question")]
[TextArea(2, 5)]
[SerializeField] private string questionText;
[TextArea(1, 3)]
[SerializeField] private string hintText = "당신의 선택에 따라 코가 반응합니다.";
[Header("Choices")]
[SerializeField] private TruthChoiceData[] choices;
public string QuestionText => questionText;
public string HintText => hintText;
public TruthChoiceData[] Choices => choices;
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(questionText) && choices != null && choices.Length > 0;
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 0e7d0f9c777e02c43ae6656a1c680fc7