DEG-178-돌발이벤트패널-버그수정 #63

Closed
heain0122 wants to merge 7 commits from DEG-178-돌발이벤트패널-버그수정 into main
37 changed files with 544 additions and 443 deletions

View File

@ -1,458 +1,186 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEditor.TextCore.Text;
using UnityEngine;
using UnityEngine.SceneManagement;
public enum PlayerState { None, Idle, Move, Win, Hit, Dead }
public class PlayerController : CharacterBase, IObserver<GameObject>
public partial class GameManager : Singleton<GameManager>,ISaveable
{
// 외부 접근 가능 변수
[Header("Attach Points")]
[SerializeField] private Transform rightHandTransform;
[SerializeField] private CameraShake cameraShake;
[SerializeField] private GameObject normalModel; // char_body : 일상복
[SerializeField] private GameObject battleModel; // warrior_1 : 전투복
[SerializeField] private Transform dashEffectAnchor; // 대시 이펙트 위치
// 게임 진행 상태
private int currentDay = 1; // 날짜
public int CurrentDay => currentDay;
private int maxDays = GameConstants.maxDays;
// 내부에서만 사용하는 변수
private PlayerHitEffectController hitEffectController;
private CharacterController _characterController;
private bool _isBattle;
private GameObject weapon;
private WeaponController _weaponController;
public WeaponController WeaponController => _weaponController;
private int stageLevel = 1; // 스테이지 정보
public int StageLevel => stageLevel;
private IPlayerState _currentStateClass { get; set; }
private IPlayerAction _currentAction;
public IPlayerAction CurrentAction => _currentAction;
private int tryStageCount = 0;
public int TryStageCount => tryStageCount;
// 강화 관련
private float attackPowerLevel;
private float moveSpeedLevel;
private float dashCoolLevel;
public float attackSpeedLevel;
// 날짜 변경 이벤트, 추후에 UI 상의 날짜를 변경할 때 사용
public event Action<int> OnDayChanged;
// 상태 관련
private PlayerStateIdle _playerStateIdle;
private PlayerStateMove _playerStateMove;
private PlayerStateWin _playerStateWin;
private PlayerStateHit _playerStateHit;
private PlayerStateDead _playerStateDead;
private ChatWindowController chatWindowController; // 대화창 컨트롤러
//대시 쿨타임 관련
[SerializeField] private float dashCooldownDuration = 1.5f;
private float dashCooldownTimer = 0f;
public bool IsDashOnCooldown => dashCooldownTimer > 0f;
public float DashCooldownRatio => dashCooldownTimer / dashCooldownDuration;
//패널 관련
private PanelManager panelManager;
public PanelManager PanelManager => panelManager;
// 행동 관련
private PlayerActionAttack _attackAction;
private PlayerActionDash _actionDash;
private TutorialManager tutorialManager;
// 외부에서도 사용하는 변수
public FixedJoystick Joystick { get; private set; }
public PlayerState CurrentState { get; private set; }
private Dictionary<PlayerState, IPlayerState> _playerStates;
public Animator PlayerAnimator { get; private set; }
public CharacterController CharacterController => _characterController;
public bool IsBattle => _isBattle;
public Transform DashEffectAnchor => dashEffectAnchor;
[Header("대시, 어택 터치 연출용")]
[SerializeField] private DungeonPanelController dungeonPanelController;
private void Awake()
private void Start()
{
if (Joystick == null)
{
Joystick = FindObjectOfType<FixedJoystick>();
}
// 오디오 초기화
InitializeAudio();
// isBattle 초기화 (임시)
bool isHousingScene = SceneManager.GetActiveScene().name.Contains("Housing");
_isBattle = !isHousingScene;
AssignCharacterController();
AssignAnimator();
//패널 매니저 생성
panelManager = Instantiate(Resources.Load<GameObject>("Prefabs/PanelManager")).GetComponent<PanelManager>();
}
protected override void Start()
#region
public void StartNPCDialogue(GamePhase phase) // intro, gameplay, end 존재
{
base.Start();
hitEffectController = GetComponentInChildren<PlayerHitEffectController>();
PlayerInit();
//강화 수치 적용
attackPowerLevel = 1 + (float)UpgradeManager.Instance.upgradeStat.CurrentUpgradeLevel(StatType.AttackPower) / 2;
moveSpeedLevel = 1 + (float)UpgradeManager.Instance.upgradeStat.CurrentUpgradeLevel(StatType.MoveSpeed) / 2;
dashCoolLevel = (float)UpgradeManager.Instance.upgradeStat.CurrentUpgradeLevel(StatType.DashCoolDown)/5;
attackSpeedLevel = (float)UpgradeManager.Instance.upgradeStat.CurrentUpgradeLevel(StatType.AttackSpeed)/10;
attackPower *= attackPowerLevel;
moveSpeed *= moveSpeedLevel;
dashCooldownDuration -= dashCoolLevel;
StartCoroutine(StartNPCDialogueCoroutine(phase));
}
private void Update()
private IEnumerator StartNPCDialogueCoroutine(GamePhase phase)
{
if (CurrentState != PlayerState.None)
if (chatWindowController == null)
{
_playerStates[CurrentState].Update();
yield return new WaitForSeconds(0.5f); // 씬 전환 대기
chatWindowController = FindObjectOfType<ChatWindowController>();
}
//대시 쿨타임 진행
if (dashCooldownTimer > 0f)
dashCooldownTimer -= Time.deltaTime;
chatWindowController.SetGamePhase(phase);
}
// Hit 상태거나 게임 끝났을 땐 땐 입력 무시
if (CurrentState == PlayerState.Hit || CurrentState == PlayerState.Dead || CurrentState == PlayerState.Win)
return;
public void DirectStartDialogue()
{
if (chatWindowController == null) chatWindowController = FindObjectOfType<ChatWindowController>();
chatWindowController.SetGamePhase(GamePhase.Gameplay);
}
// 대시 우선 입력 처리
if (Input.GetKeyDown(KeyCode.Space))
#endregion
//일시 정지
public void PauseGame()
{
Time.timeScale = 0;
}
public void ResumeGame()
{
Time.timeScale = 1;
}
// 이벤트 할당(PlayerStats Start에서 호출)
public void SetEvents()
{
PlayerStats.Instance.OnDayEnded += AdvanceDay; // 날짜 변경
PlayerStats.Instance.ZeroReputation += ZeroReputationEnd; // 평판 0 엔딩
}
// 날짜 진행
public void AdvanceDay()
{
currentDay++;
OnDayChanged?.Invoke(currentDay);
// 최대 일수 도달 체크
if (currentDay > maxDays) // 8일차에 검사
{
dungeonPanelController.DashTouchMotion();
StartDashAction();
return;
TriggerTimeEnding();
}
}
// 공격 입력 처리
if (Input.GetKeyDown(KeyCode.X) && (_currentAction == null || !_currentAction.IsActive)
&& (CurrentState != PlayerState.Win && CurrentState != PlayerState.Dead))
{
dungeonPanelController.AttackTouchMotion();
GameManager.Instance.PlayPlayerAttackSound();
StartAttackAction();
}
public void ChangeToMainScene()
{
SceneManager.LoadScene("Main");
}
// 액션 업데이트
if (_currentAction != null && _currentAction.IsActive)
{
_currentAction.UpdateAction();
}
public void ChangeToGameScene()
{
tryStageCount++; // 던전 시도 횟수 증가
var switchingPanel = PanelManager.GetPanel("SwitchingPanel").GetComponent<SwitchingPanelController>();
switchingPanel.FadeAndSceneLoad("ReDungeon"); // 던전 Scene
InteractionController interactionController = FindObjectOfType<InteractionController>();
interactionController.ReSetAfterWorkEvent();
HandleSceneAudio("Dungeon");
}
public void ChangeToHomeScene(bool isNewStart = false)
{
var switchingPanel = PanelManager.GetPanel("SwitchingPanel").GetComponent<SwitchingPanelController>();
switchingPanel.FadeAndSceneLoad("ReHousing"); // Home Scene
HandleSceneAudio("Housing");
if(isNewStart) // 아예 메인에서 시작 시 튜토리얼 출력
StartNPCDialogue(GamePhase.Intro); // StartCoroutine(StartTutorialCoroutine());
if (tryStageCount >= 3) FailEnd(); // 엔딩
}
public IEnumerator StartTutorialCoroutine()
{
yield return new WaitForSeconds(0.5f);
if(tutorialManager == null)
tutorialManager = FindObjectOfType<TutorialManager>();
PlayerStats.Instance.HideBubble();
tutorialManager.StartTutorial(() => PlayerStats.Instance.ShowBubble());
}
// TODO: Open Setting Panel 등 Panel 처리
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// TODO: 씬 로드 시 동작 구현. ex: BGM 변경
// UI용 Canvas 찾기
// _canvas = GameObject.FindObjectOfType<Canvas>();
}
private void OnDestroy()
{
OnGetHit -= HandlePlayerHit;
if(PlayerStats.Instance != null)
PlayerStats.Instance.OnDayEnded -= AdvanceDay; // 이벤트 구독 해제
}
#region
private void PlayerInit()
private void OnApplicationQuit()
{
// 상태 초기화
_playerStateIdle = new PlayerStateIdle();
_playerStateMove = new PlayerStateMove();
_playerStateHit = new PlayerStateHit();
_playerStateWin = new PlayerStateWin();
_playerStateDead = new PlayerStateDead();
// TODO: 게임 종료 시 로직 추가
}
_playerStates = new Dictionary<PlayerState, IPlayerState>
public void ApplySaveData(Save save)
{
if (save?.dungeonSave != null)
{
{ PlayerState.Idle, _playerStateIdle },
{ PlayerState.Move, _playerStateMove },
{ PlayerState.Hit, _playerStateHit },
{ PlayerState.Win, _playerStateWin },
{ PlayerState.Dead, _playerStateDead },
stageLevel = Mathf.Clamp(save.dungeonSave.stageLevel,1,2);
tryStageCount = Mathf.Clamp(save.dungeonSave.tryStageCount,0,3);
}
if (save?.homeSave != null)
{
currentDay = Mathf.Clamp(save.homeSave.currentDay,1,maxDays);
}
}
public Save ExtractSaveData()
{
return new Save
{
dungeonSave = new DungeonSave()
{
stageLevel = Mathf.Clamp(this.stageLevel,1,2),
tryStageCount = Mathf.Clamp(this.tryStageCount,0,3),
},
homeSave = new HomeSave
{
currentDay = Mathf.Clamp(this.currentDay,1,maxDays),
}
};
_attackAction = new PlayerActionAttack();
_actionDash = new PlayerActionDash();
OnGetHit -= HandlePlayerHit;
OnGetHit += HandlePlayerHit;
SetState(PlayerState.Idle);
InstantiateWeapon();
}
private void InstantiateWeapon()
{
if (weapon == null)
{
GameObject weaponObject = Resources.Load<GameObject>("Player/Weapon/Chopstick");
weapon = Instantiate(weaponObject, rightHandTransform);
_weaponController = weapon?.GetComponent<WeaponController>();
_weaponController?.Subscribe(this);
weapon?.SetActive(_isBattle);
}
}
/// <summary>
/// 애니메이션 초기화
/// </summary>
private void InitializeAnimatorParameters()
{
if (PlayerAnimator == null) return;
SafeSetBool("Walk", false);
SafeSetBool("Run", false);
// SafeSetBool(Dead, false);
SafeResetTrigger("Bore");
SafeResetTrigger("GetHit");
PlayerAnimator.Rebind(); // 레이어 초기화
// PlayerAnimator.Update(0f); // 즉시 반영
}
#endregion
#region
public void SafeSetBool(string paramName, bool value)
{
if (PlayerAnimator == null) return;
foreach (var param in PlayerAnimator.parameters)
{
if (param.name == paramName && param.type == AnimatorControllerParameterType.Bool)
{
PlayerAnimator.SetBool(paramName, value);
break;
}
}
}
private void SafeResetTrigger(string triggerName)
{
if (PlayerAnimator == null) return;
foreach (var param in PlayerAnimator.parameters)
{
if (param.name == triggerName && param.type == AnimatorControllerParameterType.Trigger)
{
PlayerAnimator.ResetTrigger(triggerName);
break;
}
}
}
#endregion
#region ,
public void SetState(PlayerState state)
{
if (CurrentState != PlayerState.None)
{
_playerStates[CurrentState].Exit();
}
CurrentState = state;
_currentStateClass = _playerStates[state];
_currentStateClass.Enter(this);
}
public void StartAttackAction()
{
if (!_isBattle) return;
_currentAction = _attackAction;
_currentAction.StartAction(this);
}
public void StartDashAction()
{
if (!_isBattle) return;
// 쿨타임 중이면 무시
if (IsDashOnCooldown)
{
Debug.Log("대시 쿨타임 중");
return;
}
// 만약 공격 중이면 강제로 공격 종료
if (_currentAction == _attackAction && _attackAction.IsActive)
{
_attackAction.EndAction(); // 애니메이션도 중단
_weaponController.AttackEnd();
}
// 기존 대시 중이면 중복 실행 안 함
if (_actionDash.IsActive)
return;
_currentAction = _actionDash;
_actionDash.StartAction(this);
// 쿨타임 시작
dashCooldownTimer = dashCooldownDuration;
}
public void OnActionEnded(IPlayerAction action)
{
if (_currentAction == action) _currentAction = null;
}
/// <summary>
/// 전투, 일상 모드 플레이어 프리팹에 따라 애니메이터 가져오기
/// </summary>
private void AssignAnimator()
{
PlayerAnimator = _isBattle
? battleModel.GetComponent<Animator>()
: normalModel.GetComponent<Animator>();
InitializeAnimatorParameters();
}
/// <summary>
/// 전투, 일상 모드 플레이어 프리팹에 따라 Character Controller 가져오기
/// </summary>
private void AssignCharacterController()
{
_characterController = _isBattle
? battleModel.GetComponent<CharacterController>()
: normalModel.GetComponent<CharacterController>();
}
#endregion
#region
public void SwitchBattleMode()
{
_isBattle = !_isBattle;
// 복장 전환
normalModel.SetActive(!_isBattle);
battleModel.SetActive(_isBattle);
// Animator, Character Controller 다시 참조 (복장에 붙은 걸로)
AssignAnimator();
AssignCharacterController();
// 무기도 전투모드에만
weapon.SetActive(_isBattle);
}
public void PlayAttackEffect()
{
if (_attackAction == null) return;
// 현재 콤보 단계 (1~4)
int comboStep = _attackAction.CurrentComboStep;
// 홀수면 기본 방향 (오→왼), 짝수면 반전 (왼→오)
bool isMirror = comboStep % 2 != 0;
Vector3 basePos = CharacterController.transform.position;
Vector3 forward = CharacterController.transform.forward;
float forwardPos = CurrentState == PlayerState.Move ? 1f : 0.2f;
// 이펙트 위치: 위로 0.5 + 앞으로 약간
Vector3 pos = basePos + Vector3.up * 0.5f + forward * forwardPos;
Quaternion rot = Quaternion.LookRotation(forward, Vector3.up) * Quaternion.Euler(0, 90, 0);
GameObject effect = EffectManager.Instance.PlayEffect(pos, rot, EffectManager.EffectType.Attack);
// 반전이 필요한 경우, X축 스케일 -1
if (isMirror && effect != null)
{
Vector3 scale = effect.transform.localScale;
scale.z *= -1;
effect.transform.localScale = scale;
}
}
public void OnAttackButtonPressed()
{
if ((_currentAction == null || !_currentAction.IsActive) &&
CurrentState != PlayerState.Win && CurrentState != PlayerState.Dead)
{
GameManager.Instance.PlayPlayerAttackSound();
StartAttackAction();
}
else if (_currentAction is PlayerActionAttack attackAction)
{
attackAction.OnComboInput();
}
}
#endregion
#region
public Vector3 GetMoveDirectionOrForward()
{
Vector3 dir = new Vector3(Joystick.Horizontal, 0, Joystick.Vertical);
return dir.sqrMagnitude > 0.01f ? dir.normalized : transform.forward;
}
public void OnDashButtonPressed()
{
if (!_actionDash.IsActive && CurrentState != PlayerState.Win && CurrentState != PlayerState.Dead)
{
StartDashAction();
}
}
#endregion
#region IObserver
public void OnNext(GameObject value)
{
float playerAttackPower = _weaponController.AttackPower * attackPower;
if (value.CompareTag("Enemy")) // 적이 Enemy일 때만 공격 처리
{
var enemyController = value.transform.GetComponent<EnemyController>();
if (enemyController != null)
{
enemyController.TakeDamage(playerAttackPower);
}
}
}
public void OnError(Exception error)
{
}
public void OnCompleted()
{
_weaponController.Unsubscribe(this);
}
#endregion
#region
// TODO: Editor에서 확인하기 위한 테스트용 메서드
public void HandlePlayerHit()
{
if (CurrentState == PlayerState.Dead) return;
SetState(PlayerState.Hit);
}
private void HandlePlayerHit(CharacterBase character)
{
if (character != this) return;
if (CurrentState == PlayerState.Dead) return;
GameManager.Instance.PlayPlayerHitSound();
SetState(PlayerState.Hit);
}
public void PlayHitEffect()
{
hitEffectController?.PlayHitEffect();
}
public void ShakeCamera()
{
cameraShake?.Shake();
}
#endregion
}

BIN
Assets/LIN/Prefabs/SuddenEventPanel.prefab (Stored with Git LFS)

Binary file not shown.

Binary file not shown.

View File

@ -22,6 +22,11 @@ public class InteractionController : MonoBehaviour
PlayerStats.Instance.SetHousingCanvasController(housingCanvasController);
}
public void ReSetAfterWorkEvent()
{
PlayerStats.Instance.OnWorked -= SuddenAfterWorkEventHappen;
}
// 상호작용 가능한 사물 범위에 들어올 때
private void OnTriggerEnter(Collider other)
{

View File

@ -13,7 +13,7 @@ public enum AfterWorkEventType
public static class HousingConstants
{
//돌발 이벤트 확률 계산
public static int AFTER_WORK_DENOMINATOR = 2;
public static int AFTER_WORK_DENOMINATOR = 3;
//돌발 이벤트 보여줄 시간
public static float SUDDENEVENT_IAMGE_SHOW_TIME = 4.0f;
//전환효과(Switching) 패널 애니메이션 시간

View File

@ -5,6 +5,7 @@ using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
using Random = UnityEngine.Random;
public class InteractionAnimationPanelController : MonoBehaviour
{
@ -33,8 +34,9 @@ public class InteractionAnimationPanelController : MonoBehaviour
public void ShowAnimationPanel(ActionType actionType, string animationText)
{
PlayerStats.Instance.HideBubble();
if (actionType == ActionType.Sleep && !PlayerStats.Instance.HasWorkedToday) // 결근
if (actionType == ActionType.Sleep && !PlayerStats.Instance.HasWorkedToday
&& PlayerStats.Instance.LastAction != ActionType.TeamDinner
&& PlayerStats.Instance.LastAction != ActionType.OvertimeWork) // 결근
{
_isAbsenceToday = true;
}
@ -89,7 +91,7 @@ public class InteractionAnimationPanelController : MonoBehaviour
/// 패널이 2초후 자동으로 닫히거나 터치시 닫히도록 합니다.
/// </summary>
/// <returns></returns>
private IEnumerator AutoHidePanel(ActionType actionType)
private IEnumerator AutoHidePanel(ActionType actionType, bool isTutorial = false)
{
float startTime = Time.time;
while (Time.time - startTime < animationDuration)
@ -105,11 +107,21 @@ public class InteractionAnimationPanelController : MonoBehaviour
GameManager.Instance.StopInteractionSound(actionType);
//패널 닫고 애니메이션 null처리
HidePanel();
HidePanel(isTutorial);
_autoHideCoroutine = null;
if (actionType == ActionType.Housework)
{
var chance = 0.7f;
if (Random.value < chance)
{
UpgradeManager.Instance.StartUpgradeInHome();
}
}
}
private void HidePanel()
private void HidePanel(bool isTutorial = false)
{
panel.SetActive(false);
@ -131,9 +143,12 @@ public class InteractionAnimationPanelController : MonoBehaviour
return;
}
// 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함
PlayerStats.Instance.CheckAbsent();
PlayerStats.Instance.ShowBubble();
if (!isTutorial) // 튜토리얼 시 결근과 말풍선 오류로 인해 조건문 추가
{
// 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함
PlayerStats.Instance.CheckAbsent();
PlayerStats.Instance.ShowBubble();
}
}
public void TutorialSleepAnimation()
@ -141,7 +156,19 @@ public class InteractionAnimationPanelController : MonoBehaviour
_parentCanvas = FindObjectOfType(typeof(Canvas)) as Canvas;
HousingConstants.interactions.TryGetValue(ActionType.Sleep, out var interactionTexts);
ShowAnimationPanel(ActionType.Sleep, interactionTexts.AnimationText);
// 1) 패널 활성화
panel.SetActive(true);
// 2) 기존 코루틴 정리
if (_textAnimCoroutine != null) StopCoroutine(_textAnimCoroutine);
if (_autoHideCoroutine != null) StopCoroutine(_autoHideCoroutine);
// 3) 텍스트 및 애니메이션 세팅
doingText.text = interactionTexts.AnimationText;
animator.Play("Sleep");
_textAnimCoroutine = StartCoroutine(TextDotsAnimation());
_autoHideCoroutine = StartCoroutine(AutoHidePanel(ActionType.Sleep, true));
}
}

View File

@ -0,0 +1,159 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEngine;
using UnityEngine.UI;
public class InteractionAnimationPanelController : MonoBehaviour
{
[SerializeField] private GameObject panel;
[SerializeField] private Image doingImage;
[SerializeField] private TMP_Text doingText;
[SerializeField] private Animator animator;
[SerializeField] private float animationDuration = 2.0f;
private Coroutine _textAnimCoroutine;
private Coroutine _autoHideCoroutine;
private Canvas _parentCanvas;
private bool _isAbsenceToday = false;
public void SetDoingText(string text)
{
doingText.text = text;
}
public bool IsPanelActive()
{
return panel.activeSelf;
}
public void ShowAnimationPanel(ActionType actionType, string animationText)
{
PlayerStats.Instance.HideBubble();
if (actionType == ActionType.Sleep && !PlayerStats.Instance.HasWorkedToday) // 결근
{
_isAbsenceToday = true;
}
// 1) 패널 활성화
panel.SetActive(true);
// 2) 기존 코루틴 정리
if (_textAnimCoroutine != null) StopCoroutine(_textAnimCoroutine);
if (_autoHideCoroutine != null) StopCoroutine(_autoHideCoroutine);
// 3) 텍스트 및 애니메이션 세팅
doingText.text = animationText;
switch (actionType)
{
case ActionType.Sleep:
animator.Play("Sleep");
break;
case ActionType.Work:
animator.Play("Go2Work");
break;
case ActionType.Eat:
animator.Play("Meal");
break;
case ActionType.Dungeon:
animator.Play("Dungeon");
break;
case ActionType.Housework:
animator.Play("Laundry");
break;
}
_textAnimCoroutine = StartCoroutine(TextDotsAnimation());
_autoHideCoroutine = StartCoroutine(AutoHidePanel(actionType));
}
private IEnumerator TextDotsAnimation()
{
var tempText = doingText.text;
float startTime = Time.time;
while (Time.time - startTime < 3)
{
for (int i = 0; i < 3; i++)
{
yield return new WaitForSeconds(0.3f);
doingText.text = tempText + new string('.', i + 1);
}
yield return new WaitForSeconds(0.3f);
}
}
/// <summary>
/// 패널이 2초후 자동으로 닫히거나 터치시 닫히도록 합니다.
/// </summary>
/// <returns></returns>
private IEnumerator AutoHidePanel(ActionType actionType)
{
float startTime = Time.time;
while (Time.time - startTime < animationDuration)
{
if (Input.touchCount > 0 || Input.GetMouseButtonDown(0))
{
break;
}
yield return null;
}
//로테이션 초기화
doingImage.rectTransform.localRotation = Quaternion.identity;
GameManager.Instance.StopInteractionSound(actionType);
//패널 닫고 애니메이션 null처리
HidePanel();
_autoHideCoroutine = null;
}
private void HidePanel()
{
panel.SetActive(false);
if (_textAnimCoroutine != null)
{
StopCoroutine(_textAnimCoroutine);
_textAnimCoroutine = null;
}
if (_autoHideCoroutine != null)
{
StopCoroutine(_autoHideCoroutine);
_autoHideCoroutine = null;
}
if (_isAbsenceToday) // 결근한 경우
{
PlayerStats.Instance.PerformAbsent();
return;
}
// 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함
PlayerStats.Instance.CheckAbsent();
PlayerStats.Instance.ShowBubble();
}
public void TutorialSleepAnimation()
{
_parentCanvas = FindObjectOfType(typeof(Canvas)) as Canvas;
HousingConstants.interactions.TryGetValue(ActionType.Sleep, out var interactionTexts);
// 1) 패널 활성화
panel.SetActive(true);
// 2) 기존 코루틴 정리
if (_textAnimCoroutine != null) StopCoroutine(_textAnimCoroutine);
if (_autoHideCoroutine != null) StopCoroutine(_autoHideCoroutine);
// 3) 텍스트 및 애니메이션 세팅
doingText.text = interactionTexts.AnimationText;
animator.Play("Sleep");
_textAnimCoroutine = StartCoroutine(TextDotsAnimation());
_autoHideCoroutine = StartCoroutine(AutoHidePanel(ActionType.Sleep));
}
}

BIN
Assets/Prefabs/ReHousing/SuddenEventPanel.prefab (Stored with Git LFS) Normal file

Binary file not shown.

BIN
Assets/Prefabs/ReHousing/TutorialManager.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

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

BIN
Assets/Prefabs/ReHousing/TutorialPanel.prefab (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -26,6 +26,8 @@ public partial class GameManager : Singleton<GameManager>,ISaveable
private PanelManager panelManager;
public PanelManager PanelManager => panelManager;
private TutorialManager tutorialManager;
private void Start()
{
// 오디오 초기화
@ -92,21 +94,44 @@ public partial class GameManager : Singleton<GameManager>,ISaveable
}
}
public void ChangeToMainScene()
{
SceneManager.LoadScene("Main");
}
public void ChangeToGameScene()
{
tryStageCount++; // 던전 시도 횟수 증가
SceneManager.LoadScene("ReDungeon"); // 던전 Scene
InteractionController interactionController = FindObjectOfType<InteractionController>();
interactionController.ReSetAfterWorkEvent();
var switchingPanel = PanelManager.GetPanel("SwitchingPanel").GetComponent<SwitchingPanelController>();
switchingPanel.FadeAndSceneLoad("ReDungeon"); // 던전 Scene
HandleSceneAudio("Dungeon");
}
public void ChangeToHomeScene()
public void ChangeToHomeScene(bool isNewStart = false)
{
SceneManager.LoadScene("ReHousing"); // Home Scene
var switchingPanel = PanelManager.GetPanel("SwitchingPanel").GetComponent<SwitchingPanelController>();
switchingPanel.FadeAndSceneLoad("ReHousing"); // Home Scene
HandleSceneAudio("Housing");
if(isNewStart) // 아예 메인에서 시작 시 튜토리얼 출력
StartNPCDialogue(GamePhase.Intro); // StartCoroutine(StartTutorialCoroutine());
if (tryStageCount >= 3) FailEnd(); // 엔딩
}
public IEnumerator StartTutorialCoroutine()
{
yield return new WaitForSeconds(0.5f);
if(tutorialManager == null)
tutorialManager = FindObjectOfType<TutorialManager>();
PlayerStats.Instance.HideBubble();
tutorialManager.StartTutorial(() => PlayerStats.Instance.ShowBubble());
}
// TODO: Open Setting Panel 등 Panel 처리
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
@ -133,6 +158,7 @@ public partial class GameManager : Singleton<GameManager>,ISaveable
if (save?.dungeonSave != null)
{
stageLevel = Mathf.Clamp(save.dungeonSave.stageLevel,1,2);
tryStageCount = Mathf.Clamp(save.dungeonSave.tryStageCount,0,3);
}
if (save?.homeSave != null)
@ -148,6 +174,7 @@ public partial class GameManager : Singleton<GameManager>,ISaveable
dungeonSave = new DungeonSave()
{
stageLevel = Mathf.Clamp(this.stageLevel,1,2),
tryStageCount = Mathf.Clamp(this.tryStageCount,0,3),
},
homeSave = new HomeSave

View File

@ -0,0 +1,143 @@
using System;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using System.Collections;
using Unity.VisualScripting;
using UnityEngine.Serialization;
using UnityEngine.Events;
public class TutorialManager : MonoBehaviour
{
[SerializeField] private TutorialStep firstStep; // 인스펙터에서 첫 단계 드래그
[Header("튜토리얼 패널 생성")]
[SerializeField] private GameObject[] tutorialPanelPrefabs;
[FormerlySerializedAs("parentObject")] public Canvas parentCanvas;
private GameObject _tutorialPanelObject;
private TutorialPanelController _tutorialPanelController;
private Coroutine _runningCoroutine;
private CanvasGroup overlay; // 화면 암전 및 입력 차단
private RectTransform targetRt;
private Canvas overlayCanvas; // RectTransformUtility를 위한 Canvas
private Action onTutorialComplete;
public void StartTutorial(Action onTutorialEnd, int panelIndex = 0)
{
if(parentCanvas == null)
parentCanvas = FindObjectOfType(typeof(Canvas)) as Canvas;
if (parentCanvas != null)
{
overlayCanvas = parentCanvas as Canvas;
_tutorialPanelObject = Instantiate(tutorialPanelPrefabs[panelIndex], parentCanvas.GameObject().transform);
overlay = _tutorialPanelObject.GetComponent<CanvasGroup>();
_tutorialPanelController = _tutorialPanelObject.GetComponent<TutorialPanelController>();
}
if (_tutorialPanelController != null)
{
onTutorialComplete = onTutorialEnd;
overlay.alpha = 1f;
overlay.blocksRaycasts = true;
RunStep(firstStep);
}
else Debug.Log("패널 생성 실패, 튜토리얼 진행이 불가능합니다.");
}
private void RunStep(TutorialStep step)
{
if (_runningCoroutine != null) StopCoroutine(_runningCoroutine);
_runningCoroutine = StartCoroutine(RunStepCoroutine(step));
}
private IEnumerator RunStepCoroutine(TutorialStep step)
{
// 단계 시작 이벤트
step.onStepBegin?.Invoke();
// 메시지 갱신
_tutorialPanelController.setTutorialText(step.message);
float elapsed = 0f;
bool done = false;
//터치해야 할 위치가 있는지 체크
if (step.touchTargetIndex >= 0)
{
_tutorialPanelController.ShowTouchTarget(step.touchTargetIndex);
targetRt = _tutorialPanelController.touchTargets[step.touchTargetIndex].GetComponent<RectTransform>();
}
if (step.imageIndex >= 0)
{
_tutorialPanelController.ShowImage(step.imageIndex);
}
while (!done)
{
// 1) 영역 터치 체크
if (targetRt != null)
{
// 클릭 또는 터치 이벤트
bool pressed = Input.GetMouseButtonDown(0) || Input.touchCount > 0;
if (pressed)
{
Vector2 screenPos = Input.touchCount > 0
? Input.GetTouch(0).position
: (Vector2)Input.mousePosition;
// 터치 위치가 지정 RectTransform 안에 있는지 검사
if (RectTransformUtility.RectangleContainsScreenPoint(
targetRt, screenPos, overlayCanvas.worldCamera))
{
targetRt = null;
_tutorialPanelController.HideTouchTarget(step.touchTargetIndex);
done = true;
}
}
}
// 타임아웃 체크
if (step.timeout > 0f && elapsed >= step.timeout)
done = true;
elapsed += Time.deltaTime;
yield return null;
}
// 단계 완료 이벤트
step.onStepComplete?.Invoke();
if (step.imageIndex >= 0)
{
_tutorialPanelController.HideImage(step.imageIndex);
}
// 다음 단계로
if (step.nextStep != null)
RunStep(step.nextStep);
else
EndTutorial();
}
private void EndTutorial()
{
_tutorialPanelController.setTutorialText("");
overlay.alpha = 0f;
overlay.blocksRaycasts = false;
if(onTutorialComplete!=null)
{
onTutorialComplete?.Invoke();
onTutorialComplete = null;
}
if (_tutorialPanelObject == null)
return;
Destroy(_tutorialPanelObject);
_tutorialPanelController = null;
}
}

View File

@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 23ceb52967652ad4c9179bc5b6cd1cbf

View File

@ -12,8 +12,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: b22d834cf5e26e647be215074940d40e, type: 3}
m_Name: TutorialStep1
m_EditorClassIdentifier:
message: "\uC9D1\uC5D0\uC11C\uB3C4 \uC2AC\uB77C\uC774\uB354\uB97C \uC870\uC791\uD574
\uCE90\uB9AD\uD130\uB97C \uC6C0\uC9C1\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4."
message: "\uC2AC\uB77C\uC774\uB354\uB97C \uC870\uC791\uD574 \uCE90\uB9AD\uD130\uB97C
\uC6C0\uC9C1\uC77C \uC218 \uC788\uC2B5\uB2C8\uB2E4."
timeout: 0
onStepBegin:
m_PersistentCalls:

View File

@ -13,8 +13,8 @@ MonoBehaviour:
m_Name: TutorialStep2
m_EditorClassIdentifier:
message: "\uCE68\uB300, \uB0C9\uC7A5\uACE0, \uD604\uAD00 \uADF8\uB9AC\uACE0 \uC8FC\uBC29\uC5D0
\uAC00\uAE4C\uC774 \uAC00\uBA74 \uADF8\uC5D0 \uC5B4\uC6B8\uB9AC\uB294 \uC0C1\uD638\uC791\uC6A9\uC744
\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4."
\uAC00\uAE4C\uC774 \uAC00\uBA74\n\uC0C1\uD638\uC791\uC6A9\uC744 \uD560 \uC218
\uC788\uC2B5\uB2C8\uB2E4."
timeout: 0
onStepBegin:
m_PersistentCalls: