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 { // 외부 접근 가능 변수 [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 PlayerHitEffectController hitEffectController; private CharacterController _characterController; private bool _isBattle; private GameObject weapon; private WeaponController _weaponController; public WeaponController WeaponController => _weaponController; private IPlayerState _currentStateClass { get; set; } private IPlayerAction _currentAction; public IPlayerAction CurrentAction => _currentAction; // 강화 관련 private float attackPowerLevel; private float moveSpeedLevel; private float dashCoolLevel; public float attackSpeedLevel; // 상태 관련 private PlayerStateIdle _playerStateIdle; private PlayerStateMove _playerStateMove; private PlayerStateWin _playerStateWin; private PlayerStateHit _playerStateHit; private PlayerStateDead _playerStateDead; //대시 쿨타임 관련 [SerializeField] private float dashCooldownDuration = 1.5f; private float dashCooldownTimer = 0f; public bool IsDashOnCooldown => dashCooldownTimer > 0f; public float DashCooldownRatio => dashCooldownTimer / dashCooldownDuration; // 행동 관련 private PlayerActionAttack _attackAction; private PlayerActionDash _actionDash; // 외부에서도 사용하는 변수 public FixedJoystick Joystick { get; private set; } public PlayerState CurrentState { get; private set; } private Dictionary _playerStates; public Animator PlayerAnimator { get; private set; } public CharacterController CharacterController => _characterController; public bool IsBattle => _isBattle; public Transform DashEffectAnchor => dashEffectAnchor; private void Awake() { if (Joystick == null) { Joystick = FindObjectOfType(); } // isBattle 초기화 (임시) bool isHousingScene = SceneManager.GetActiveScene().name.Contains("Housing"); _isBattle = !isHousingScene; AssignCharacterController(); AssignAnimator(); } protected override void Start() { base.Start(); hitEffectController = GetComponentInChildren(); 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() { if (CurrentState != PlayerState.None) { _playerStates[CurrentState].Update(); } //대시 쿨타임 진행 if (dashCooldownTimer > 0f) dashCooldownTimer -= Time.deltaTime; // Hit 상태거나 게임 끝났을 땐 땐 입력 무시 if (CurrentState == PlayerState.Hit || CurrentState == PlayerState.Dead || CurrentState == PlayerState.Win) return; // 대시 우선 입력 처리 if (Input.GetKeyDown(KeyCode.Space)) { StartDashAction(); return; } // 공격 입력 처리 if (Input.GetKeyDown(KeyCode.X) && (_currentAction == null || !_currentAction.IsActive) && (CurrentState != PlayerState.Win && CurrentState != PlayerState.Dead)) { GameManager.Instance.PlayPlayerAttackSound(); StartAttackAction(); } // 액션 업데이트 if (_currentAction != null && _currentAction.IsActive) { _currentAction.UpdateAction(); } } private void OnDestroy() { OnGetHit -= HandlePlayerHit; } #region 초기화 관련 private void PlayerInit() { // 상태 초기화 _playerStateIdle = new PlayerStateIdle(); _playerStateMove = new PlayerStateMove(); _playerStateHit = new PlayerStateHit(); _playerStateWin = new PlayerStateWin(); _playerStateDead = new PlayerStateDead(); _playerStates = new Dictionary { { PlayerState.Idle, _playerStateIdle }, { PlayerState.Move, _playerStateMove }, { PlayerState.Hit, _playerStateHit }, { PlayerState.Win, _playerStateWin }, { PlayerState.Dead, _playerStateDead }, }; _attackAction = new PlayerActionAttack(); _actionDash = new PlayerActionDash(); OnGetHit -= HandlePlayerHit; OnGetHit += HandlePlayerHit; SetState(PlayerState.Idle); InstantiateWeapon(); } private void InstantiateWeapon() { if (weapon == null) { GameObject weaponObject = Resources.Load("Player/Weapon/Chopstick"); weapon = Instantiate(weaponObject, rightHandTransform); _weaponController = weapon?.GetComponent(); _weaponController?.Subscribe(this); weapon?.SetActive(_isBattle); } } /// /// 애니메이션 초기화 /// 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; } /// /// 전투, 일상 모드 플레이어 프리팹에 따라 애니메이터 가져오기 /// private void AssignAnimator() { PlayerAnimator = _isBattle ? battleModel.GetComponent() : normalModel.GetComponent(); InitializeAnimatorParameters(); } /// /// 전투, 일상 모드 플레이어 프리팹에 따라 Character Controller 가져오기 /// private void AssignCharacterController() { _characterController = _isBattle ? battleModel.GetComponent() : normalModel.GetComponent(); } #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(); 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 }