[Fix] 메인 충돌 추가 해결

This commit is contained in:
HaeinLEE 2025-05-15 09:45:55 +09:00
parent d941326d61
commit bb48246a96
6 changed files with 488 additions and 441 deletions

View File

@ -1,458 +1,186 @@
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEditor.TextCore.Text;
using UnityEngine; using UnityEngine;
using UnityEngine.SceneManagement; using UnityEngine.SceneManagement;
public enum PlayerState { None, Idle, Move, Win, Hit, Dead } public partial class GameManager : Singleton<GameManager>,ISaveable
public class PlayerController : CharacterBase, IObserver<GameObject>
{ {
// 외부 접근 가능 변수 // 게임 진행 상태
[Header("Attach Points")] private int currentDay = 1; // 날짜
[SerializeField] private Transform rightHandTransform; public int CurrentDay => currentDay;
[SerializeField] private CameraShake cameraShake; private int maxDays = GameConstants.maxDays;
[SerializeField] private GameObject normalModel; // char_body : 일상복
[SerializeField] private GameObject battleModel; // warrior_1 : 전투복
[SerializeField] private Transform dashEffectAnchor; // 대시 이펙트 위치
// 내부에서만 사용하는 변수 private int stageLevel = 1; // 스테이지 정보
private PlayerHitEffectController hitEffectController; public int StageLevel => stageLevel;
private CharacterController _characterController;
private bool _isBattle;
private GameObject weapon;
private WeaponController _weaponController;
public WeaponController WeaponController => _weaponController;
private IPlayerState _currentStateClass { get; set; } private int tryStageCount = 0;
private IPlayerAction _currentAction; public int TryStageCount => tryStageCount;
public IPlayerAction CurrentAction => _currentAction;
// 강화 관련 // 날짜 변경 이벤트, 추후에 UI 상의 날짜를 변경할 때 사용
private float attackPowerLevel; public event Action<int> OnDayChanged;
private float moveSpeedLevel;
private float dashCoolLevel;
public float attackSpeedLevel;
// 상태 관련 private ChatWindowController chatWindowController; // 대화창 컨트롤러
private PlayerStateIdle _playerStateIdle;
private PlayerStateMove _playerStateMove;
private PlayerStateWin _playerStateWin;
private PlayerStateHit _playerStateHit;
private PlayerStateDead _playerStateDead;
//대시 쿨타임 관련 //패널 관련
[SerializeField] private float dashCooldownDuration = 1.5f; private PanelManager panelManager;
private float dashCooldownTimer = 0f; public PanelManager PanelManager => panelManager;
public bool IsDashOnCooldown => dashCooldownTimer > 0f;
public float DashCooldownRatio => dashCooldownTimer / dashCooldownDuration;
// 행동 관련 private TutorialManager tutorialManager;
private PlayerActionAttack _attackAction;
private PlayerActionDash _actionDash;
// 외부에서도 사용하는 변수 private void Start()
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()
{ {
if (Joystick == null) // 오디오 초기화
{ InitializeAudio();
Joystick = FindObjectOfType<FixedJoystick>();
//패널 매니저 생성
panelManager = Instantiate(Resources.Load<GameObject>("Prefabs/PanelManager")).GetComponent<PanelManager>();
} }
// isBattle 초기화 (임시) #region
bool isHousingScene = SceneManager.GetActiveScene().name.Contains("Housing");
_isBattle = !isHousingScene;
AssignCharacterController(); public void StartNPCDialogue(GamePhase phase) // intro, gameplay, end 존재
AssignAnimator(); {
StartCoroutine(StartNPCDialogueCoroutine(phase));
} }
protected override void Start() private IEnumerator StartNPCDialogueCoroutine(GamePhase phase)
{ {
base.Start(); if (chatWindowController == null)
{
hitEffectController = GetComponentInChildren<PlayerHitEffectController>(); yield return new WaitForSeconds(0.5f); // 씬 전환 대기
chatWindowController = FindObjectOfType<ChatWindowController>();
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;
} }
private void Update() chatWindowController.SetGamePhase(phase);
{
if (CurrentState != PlayerState.None)
{
_playerStates[CurrentState].Update();
} }
//대시 쿨타임 진행 public void DirectStartDialogue()
if (dashCooldownTimer > 0f)
dashCooldownTimer -= Time.deltaTime;
// Hit 상태거나 게임 끝났을 땐 땐 입력 무시
if (CurrentState == PlayerState.Hit || CurrentState == PlayerState.Dead || CurrentState == PlayerState.Win)
return;
// 대시 우선 입력 처리
if (Input.GetKeyDown(KeyCode.Space))
{ {
dungeonPanelController.DashTouchMotion(); if (chatWindowController == null) chatWindowController = FindObjectOfType<ChatWindowController>();
StartDashAction(); chatWindowController.SetGamePhase(GamePhase.Gameplay);
return;
} }
// 공격 입력 처리 #endregion
if (Input.GetKeyDown(KeyCode.X) && (_currentAction == null || !_currentAction.IsActive)
&& (CurrentState != PlayerState.Win && CurrentState != PlayerState.Dead)) //일시 정지
public void PauseGame()
{ {
dungeonPanelController.AttackTouchMotion(); Time.timeScale = 0;
GameManager.Instance.PlayPlayerAttackSound();
StartAttackAction();
} }
// 액션 업데이트 public void ResumeGame()
if (_currentAction != null && _currentAction.IsActive)
{ {
_currentAction.UpdateAction(); 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일차에 검사
{
TriggerTimeEnding();
}
}
public void ChangeToMainScene()
{
SceneManager.LoadScene("Main");
}
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() private void OnDestroy()
{ {
OnGetHit -= HandlePlayerHit; if(PlayerStats.Instance != null)
PlayerStats.Instance.OnDayEnded -= AdvanceDay; // 이벤트 구독 해제
} }
#region private void OnApplicationQuit()
private void PlayerInit()
{ {
// 상태 초기화 // TODO: 게임 종료 시 로직 추가
_playerStateIdle = new PlayerStateIdle(); }
_playerStateMove = new PlayerStateMove();
_playerStateHit = new PlayerStateHit();
_playerStateWin = new PlayerStateWin();
_playerStateDead = new PlayerStateDead();
_playerStates = new Dictionary<PlayerState, IPlayerState> public void ApplySaveData(Save save)
{ {
{ PlayerState.Idle, _playerStateIdle }, if (save?.dungeonSave != null)
{ PlayerState.Move, _playerStateMove }, {
{ PlayerState.Hit, _playerStateHit }, stageLevel = Mathf.Clamp(save.dungeonSave.stageLevel,1,2);
{ PlayerState.Win, _playerStateWin }, tryStageCount = Mathf.Clamp(save.dungeonSave.tryStageCount,0,3);
{ PlayerState.Dead, _playerStateDead }, }
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
}

View File

@ -5,6 +5,7 @@ using System.Text;
using TMPro; using TMPro;
using UnityEngine; using UnityEngine;
using UnityEngine.UI; using UnityEngine.UI;
using Random = UnityEngine.Random;
public class InteractionAnimationPanelController : MonoBehaviour public class InteractionAnimationPanelController : MonoBehaviour
{ {
@ -33,8 +34,9 @@ public class InteractionAnimationPanelController : MonoBehaviour
public void ShowAnimationPanel(ActionType actionType, string animationText) public void ShowAnimationPanel(ActionType actionType, string animationText)
{ {
PlayerStats.Instance.HideBubble(); 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; _isAbsenceToday = true;
} }
@ -89,7 +91,7 @@ public class InteractionAnimationPanelController : MonoBehaviour
/// 패널이 2초후 자동으로 닫히거나 터치시 닫히도록 합니다. /// 패널이 2초후 자동으로 닫히거나 터치시 닫히도록 합니다.
/// </summary> /// </summary>
/// <returns></returns> /// <returns></returns>
private IEnumerator AutoHidePanel(ActionType actionType) private IEnumerator AutoHidePanel(ActionType actionType, bool isTutorial = false)
{ {
float startTime = Time.time; float startTime = Time.time;
while (Time.time - startTime < animationDuration) while (Time.time - startTime < animationDuration)
@ -105,11 +107,21 @@ public class InteractionAnimationPanelController : MonoBehaviour
GameManager.Instance.StopInteractionSound(actionType); GameManager.Instance.StopInteractionSound(actionType);
//패널 닫고 애니메이션 null처리 //패널 닫고 애니메이션 null처리
HidePanel(); HidePanel(isTutorial);
_autoHideCoroutine = null; _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); panel.SetActive(false);
@ -131,10 +143,13 @@ public class InteractionAnimationPanelController : MonoBehaviour
return; return;
} }
if (!isTutorial) // 튜토리얼 시 결근과 말풍선 오류로 인해 조건문 추가
{
// 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함 // 패널 닫히고 결근 체크, 상호작용 패널과 결근 엔딩 채팅창이 겹치지 않기 위함
PlayerStats.Instance.CheckAbsent(); PlayerStats.Instance.CheckAbsent();
PlayerStats.Instance.ShowBubble(); PlayerStats.Instance.ShowBubble();
} }
}
public void TutorialSleepAnimation() public void TutorialSleepAnimation()
{ {
@ -153,7 +168,7 @@ public class InteractionAnimationPanelController : MonoBehaviour
animator.Play("Sleep"); animator.Play("Sleep");
_textAnimCoroutine = StartCoroutine(TextDotsAnimation()); _textAnimCoroutine = StartCoroutine(TextDotsAnimation());
_autoHideCoroutine = StartCoroutine(AutoHidePanel(ActionType.Sleep)); _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));
}
}

View File

@ -24,10 +24,6 @@ public class TutorialManager : MonoBehaviour
private Canvas overlayCanvas; // RectTransformUtility를 위한 Canvas private Canvas overlayCanvas; // RectTransformUtility를 위한 Canvas
private Action onTutorialComplete; private Action onTutorialComplete;
public void Start( )
{
StartTutorial(null);
}
public void StartTutorial(Action onTutorialEnd, int panelIndex = 0) public void StartTutorial(Action onTutorialEnd, int panelIndex = 0)
{ {
if(parentCanvas == null) if(parentCanvas == null)
@ -97,15 +93,10 @@ public class TutorialManager : MonoBehaviour
if (RectTransformUtility.RectangleContainsScreenPoint( if (RectTransformUtility.RectangleContainsScreenPoint(
targetRt, screenPos, overlayCanvas.worldCamera)) targetRt, screenPos, overlayCanvas.worldCamera))
{ {
Debug.Log("타겟 터치");
targetRt = null; targetRt = null;
_tutorialPanelController.HideTouchTarget(step.touchTargetIndex); _tutorialPanelController.HideTouchTarget(step.touchTargetIndex);
done = true; done = true;
} }
else
{
Debug.Log("타겟이 아닌 곳 터치");
}
} }
} }

View File

@ -0,0 +1,152 @@
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 Start( )
{
StartTutorial(null);
}
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))
{
Debug.Log("타겟 터치");
targetRt = null;
_tutorialPanelController.HideTouchTarget(step.touchTargetIndex);
done = true;
}
else
{
Debug.Log("타겟이 아닌 곳 터치");
}
}
}
// 타임아웃 체크
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