DEG-178-돌발이벤트패널-버그수정 #63
@ -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 int stageLevel = 1; // 스테이지 정보
|
||||
public int StageLevel => stageLevel;
|
||||
|
||||
// 내부에서만 사용하는 변수
|
||||
private PlayerHitEffectController hitEffectController;
|
||||
private CharacterController _characterController;
|
||||
private bool _isBattle;
|
||||
private GameObject weapon;
|
||||
private WeaponController _weaponController;
|
||||
public WeaponController WeaponController => _weaponController;
|
||||
private int tryStageCount = 0;
|
||||
public int TryStageCount => tryStageCount;
|
||||
|
||||
private IPlayerState _currentStateClass { get; set; }
|
||||
private IPlayerAction _currentAction;
|
||||
public IPlayerAction CurrentAction => _currentAction;
|
||||
// 날짜 변경 이벤트, 추후에 UI 상의 날짜를 변경할 때 사용
|
||||
public event Action<int> OnDayChanged;
|
||||
|
||||
// 강화 관련
|
||||
private float attackPowerLevel;
|
||||
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;
|
||||
//패널 관련
|
||||
private PanelManager panelManager;
|
||||
public PanelManager PanelManager => panelManager;
|
||||
|
||||
//대시 쿨타임 관련
|
||||
[SerializeField] private float dashCooldownDuration = 1.5f;
|
||||
private float dashCooldownTimer = 0f;
|
||||
public bool IsDashOnCooldown => dashCooldownTimer > 0f;
|
||||
public float DashCooldownRatio => dashCooldownTimer / dashCooldownDuration;
|
||||
private TutorialManager tutorialManager;
|
||||
|
||||
// 행동 관련
|
||||
private PlayerActionAttack _attackAction;
|
||||
private PlayerActionDash _actionDash;
|
||||
|
||||
// 외부에서도 사용하는 변수
|
||||
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();
|
||||
|
||||
//패널 매니저 생성
|
||||
panelManager = Instantiate(Resources.Load<GameObject>("Prefabs/PanelManager")).GetComponent<PanelManager>();
|
||||
}
|
||||
|
||||
// isBattle 초기화 (임시)
|
||||
bool isHousingScene = SceneManager.GetActiveScene().name.Contains("Housing");
|
||||
_isBattle = !isHousingScene;
|
||||
|
||||
AssignCharacterController();
|
||||
AssignAnimator();
|
||||
#region 대화 관련
|
||||
|
||||
public void StartNPCDialogue(GamePhase phase) // intro, gameplay, end 존재
|
||||
{
|
||||
StartCoroutine(StartNPCDialogueCoroutine(phase));
|
||||
}
|
||||
|
||||
protected override void Start()
|
||||
private IEnumerator StartNPCDialogueCoroutine(GamePhase phase)
|
||||
{
|
||||
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;
|
||||
if (chatWindowController == null)
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f); // 씬 전환 대기
|
||||
chatWindowController = FindObjectOfType<ChatWindowController>();
|
||||
}
|
||||
|
||||
chatWindowController.SetGamePhase(phase);
|
||||
}
|
||||
|
||||
attackPower *= attackPowerLevel;
|
||||
moveSpeed *= moveSpeedLevel;
|
||||
dashCooldownDuration -= dashCoolLevel;
|
||||
public void DirectStartDialogue()
|
||||
{
|
||||
if (chatWindowController == null) chatWindowController = FindObjectOfType<ChatWindowController>();
|
||||
chatWindowController.SetGamePhase(GamePhase.Gameplay);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
//일시 정지
|
||||
public void PauseGame()
|
||||
{
|
||||
Time.timeScale = 0;
|
||||
}
|
||||
|
||||
public void ResumeGame()
|
||||
{
|
||||
Time.timeScale = 1;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
// 이벤트 할당(PlayerStats Start에서 호출)
|
||||
public void SetEvents()
|
||||
{
|
||||
if (CurrentState != PlayerState.None)
|
||||
PlayerStats.Instance.OnDayEnded += AdvanceDay; // 날짜 변경
|
||||
PlayerStats.Instance.ZeroReputation += ZeroReputationEnd; // 평판 0 엔딩
|
||||
}
|
||||
|
||||
// 날짜 진행
|
||||
public void AdvanceDay()
|
||||
{
|
||||
currentDay++;
|
||||
OnDayChanged?.Invoke(currentDay);
|
||||
|
||||
// 최대 일수 도달 체크
|
||||
if (currentDay > maxDays) // 8일차에 검사
|
||||
{
|
||||
_playerStates[CurrentState].Update();
|
||||
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 (dashCooldownTimer > 0f)
|
||||
dashCooldownTimer -= Time.deltaTime;
|
||||
if(isNewStart) // 아예 메인에서 시작 시 튜토리얼 출력
|
||||
StartNPCDialogue(GamePhase.Intro); // StartCoroutine(StartTutorialCoroutine());
|
||||
|
||||
// Hit 상태거나 게임 끝났을 땐 땐 입력 무시
|
||||
if (CurrentState == PlayerState.Hit || CurrentState == PlayerState.Dead || CurrentState == PlayerState.Win)
|
||||
return;
|
||||
if (tryStageCount >= 3) FailEnd(); // 엔딩
|
||||
}
|
||||
|
||||
public IEnumerator StartTutorialCoroutine()
|
||||
{
|
||||
yield return new WaitForSeconds(0.5f);
|
||||
|
||||
// 대시 우선 입력 처리
|
||||
if (Input.GetKeyDown(KeyCode.Space))
|
||||
{
|
||||
dungeonPanelController.DashTouchMotion();
|
||||
StartDashAction();
|
||||
return;
|
||||
}
|
||||
if(tutorialManager == null)
|
||||
tutorialManager = FindObjectOfType<TutorialManager>();
|
||||
|
||||
// 공격 입력 처리
|
||||
if (Input.GetKeyDown(KeyCode.X) && (_currentAction == null || !_currentAction.IsActive)
|
||||
&& (CurrentState != PlayerState.Win && CurrentState != PlayerState.Dead))
|
||||
{
|
||||
dungeonPanelController.AttackTouchMotion();
|
||||
GameManager.Instance.PlayPlayerAttackSound();
|
||||
StartAttackAction();
|
||||
}
|
||||
PlayerStats.Instance.HideBubble();
|
||||
tutorialManager.StartTutorial(() => PlayerStats.Instance.ShowBubble());
|
||||
}
|
||||
|
||||
// TODO: Open Setting Panel 등 Panel 처리
|
||||
|
||||
protected override void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
{
|
||||
// TODO: 씬 로드 시 동작 구현. ex: BGM 변경
|
||||
|
||||
// 액션 업데이트
|
||||
if (_currentAction != null && _currentAction.IsActive)
|
||||
{
|
||||
_currentAction.UpdateAction();
|
||||
}
|
||||
// UI용 Canvas 찾기
|
||||
// _canvas = GameObject.FindObjectOfType<Canvas>();
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
OnGetHit -= HandlePlayerHit;
|
||||
if(PlayerStats.Instance != null)
|
||||
PlayerStats.Instance.OnDayEnded -= AdvanceDay; // 이벤트 구독 해제
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
// TODO: 게임 종료 시 로직 추가
|
||||
}
|
||||
|
||||
#region 초기화 관련
|
||||
|
||||
private void PlayerInit()
|
||||
public void ApplySaveData(Save save)
|
||||
{
|
||||
// 상태 초기화
|
||||
_playerStateIdle = new PlayerStateIdle();
|
||||
_playerStateMove = new PlayerStateMove();
|
||||
_playerStateHit = new PlayerStateHit();
|
||||
_playerStateWin = new PlayerStateWin();
|
||||
_playerStateDead = new PlayerStateDead();
|
||||
|
||||
_playerStates = new Dictionary<PlayerState, IPlayerState>
|
||||
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)
BIN
Assets/LIN/Prefabs/SuddenEventPanel.prefab
(Stored with Git LFS)
Binary file not shown.
BIN
Assets/LIN/Prefabs/TutorialExamplePanel.prefab
(Stored with Git LFS)
BIN
Assets/LIN/Prefabs/TutorialExamplePanel.prefab
(Stored with Git LFS)
Binary file not shown.
@ -21,6 +21,11 @@ public class InteractionController : MonoBehaviour
|
||||
PlayerStats.Instance.SetInteractionPanelController(interactionAnimationPanelController);
|
||||
PlayerStats.Instance.SetHousingCanvasController(housingCanvasController);
|
||||
}
|
||||
|
||||
public void ReSetAfterWorkEvent()
|
||||
{
|
||||
PlayerStats.Instance.OnWorked -= SuddenAfterWorkEventHappen;
|
||||
}
|
||||
|
||||
// 상호작용 가능한 사물 범위에 들어올 때
|
||||
private void OnTriggerEnter(Collider other)
|
||||
|
@ -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) 패널 애니메이션 시간
|
||||
|
@ -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);
|
||||
|
||||
@ -124,16 +136,19 @@ public class InteractionAnimationPanelController : MonoBehaviour
|
||||
StopCoroutine(_autoHideCoroutine);
|
||||
_autoHideCoroutine = null;
|
||||
}
|
||||
|
||||
|
||||
if (_isAbsenceToday) // 결근한 경우
|
||||
{
|
||||
PlayerStats.Instance.PerformAbsent();
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
159
Assets/LIN/Scripts/UI/InteractionAnimationPanelController.cs.bak
Normal file
159
Assets/LIN/Scripts/UI/InteractionAnimationPanelController.cs.bak
Normal 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
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
BIN
Assets/Prefabs/ReHousing/TutorialManager.prefab
(Stored with Git LFS)
Normal file
Binary file not shown.
7
Assets/Prefabs/ReHousing/TutorialManager.prefab.meta
Normal file
7
Assets/Prefabs/ReHousing/TutorialManager.prefab.meta
Normal 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
BIN
Assets/Prefabs/ReHousing/TutorialPanel.prefab
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -26,6 +26,8 @@ public partial class GameManager : Singleton<GameManager>,ISaveable
|
||||
private PanelManager panelManager;
|
||||
public PanelManager PanelManager => panelManager;
|
||||
|
||||
private TutorialManager tutorialManager;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
// 오디오 초기화
|
||||
@ -91,22 +93,45 @@ public partial class GameManager : Singleton<GameManager>,ISaveable
|
||||
TriggerTimeEnding();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
143
Assets/Scripts/Tutorial/TutorialManager.cs
Normal file
143
Assets/Scripts/Tutorial/TutorialManager.cs
Normal 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;
|
||||
}
|
||||
}
|
2
Assets/Scripts/Tutorial/TutorialManager.cs.bak.meta
Normal file
2
Assets/Scripts/Tutorial/TutorialManager.cs.bak.meta
Normal file
@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 23ceb52967652ad4c9179bc5b6cd1cbf
|
@ -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:
|
@ -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:
|
Loading…
x
Reference in New Issue
Block a user