24-06

Sandy·2024년 7월 21일

[Today I Learned]

목록 보기
18/18

240603_Singleton

Q Singleton으로 SoundManager 만들려고 하는데 Awake에서 DontDestoryOnLoad 못 쓴다

Instance 속성에서 _componentInstance가 생성될 때 Awake에서 DontDestroyOnLoad가 호출되는데 _componentInstance 변수가 null이다. \

  • DontDestroyOnLoad(_componentInstance);_componentInstance = _gameObject.GetOrAddComponent<T>(); 다음에 넣고 Awake 에서는 뺀다.
public class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    protected static T _componentInstance;

    public static T Instance
    {
        get
        {
            if (_componentInstance == null)
            {
                _componentInstance = (T)FindObjectOfType(typeof(T));

                if (_componentInstance == null)
                {
                    GameObject _gameObject = new GameObject();
                    _gameObject.name = typeof(T).ToString();
                    _componentInstance = _gameObject.GetOrAddComponent<T>();
                    DontDestroyOnLoad(_componentInstance);
                }
            }
            return _componentInstance;
        }
    }
}
  • 지난번 피드백 보기
  • utf-8 세팅 다시 해보기
  • 플레이어 애니메이션 적용하기

Q Bgm 있는데 null 이라고 뜬다. 왜 못 찾고 있을까?

SoundManager가 초기화되기 전에 Play 메서드를 호출했다.

  • Awake 에서 if (_componentInstance == null) 이면 _componentInstance = SoundManager.Instance; 부분을 추가했다.

Q utf-8 세팅 다시 해보기

.editorconfig 넣어놨는데 코드 utf-8로 안되어있다.

[*]
charset=utf-8
  • 파일에 이 부분만 넣고 나머지 다 뺐다

Q Tag string으로 찾는 부분 개선

  • 태그로 오브젝트 찾거나 할 때 CompareTag 쓴다. 문자열 Define에 저장해놓고 비교하면 유지보수에 좋다.
public class Define
{
    public enum Sound
    {
        Bgm = 0,
        Effect,
        Max,
    }

    public const string GroundTag = "Ground";
    public const string PlayerTag = "Player";

}

240605_PlayerState_Animation

Q 애니메이션 트랜지션 안 만들고 빠르게 작업하기

  • Player의 State에 따라 해당하는 애니메이션을 재생한다
public enum PlayerState
{
    Default = 0,
    Idle = 1 << 0,
    Walk = 1 << 1,
    Jump = 1 << 2,
    GetHit = 1 << 3,
    Die = 1 << 4,
    Interact = 1 << 5,
    Run = 1 << 6,
    // Fall = 1 << 7,
    // Climb = 1 << 8,

    Jumpable = Walk | Idle | Run,
}

public class PlayerAnimationController : MonoBehaviour
{
    Animator _animator;
    PlayerStateController _playerStateController;

    private void Awake()
    {
        _playerStateController = gameObject.GetOrAddComponent<PlayerStateController>();
        _playerStateController.OnStateChangeEvent += ChangeAnimation;

        _animator = GetComponent<Animator>();
    }

    private void ChangeAnimation(PlayerState playerStateEnum)
    {
        string State = playerStateEnum.ToString();
        _animator.CrossFade(State, 0.2f);
    }
}

240610_3D플랫포머

Unity 3D 프로젝트

1주일동안 4명이 3D 플랫포머 게임을 만들었다.

https://github.com/SandyLee-00/Unity_PuzzlePlatformer

느낀점

  • Player를 Rigidbody로 이동하다가 CharacterController를 이용해 이동시키려고 바꾸려고 했다. 유니티 엔진에서 제공하던 물리 기능 대신 움직임을 만들기가 어려웠다. Walk, Run, Jump 까지 만들었는데 카메라가 제대로 동작하지 않았다. 움직임을 멈추면 이상한 각도로 회전하는 현상을 고치지 못했다. 2일정도 작업한 분량을 날리고 기존 코드에서 다시 프로젝트를 진행했다.
  • Ridigbody를 이용해서 움직여서 플레이어가 미끄러졌다. 그래서 배경 컨셉을 아예 눈으로 바꿔버렸다. 실력이 좀 더 있었으면 빠르게 수정할 수 있었을텐데 아쉬웠다.

성과

  • 3D 프로젝트를 처음 해봤다. Player를 맡아서 이동, 점프 등 움직임을 구현하고 각 상태에서 다른 상태로 전환에 대해 고민해 본 것이 좋았다.

완성도

  • 7/10 플레이어 CharacterController로 바꾸지 못했다 -1점, 3인칭 시점을 제대로 구현하지 못했다 -1점, Climb 상태 못 넣었다 -1점

기술스택

  • FSM : 플레이어가 Idle, Walk, Run, Jump, GetHit, Die, Interact 상태에 맞는 애니메이션과 상황별 State 전환을 했다.
  • InputSystem : InputSystem을 이용해 WASD로 이동, Mouse로 회전, Space로 점프, Shift로 달리기, E키로 줍기, Tab으로 인벤토리, Esc로 UI로 전환 등을 하도록 매핑했다.
  • Singleton & Reflection & Generic : SoundManager를 Singleton으로 만들었다. Define에서 Enum으로 BGM과 Effect를 정의했다. 각 Enum 값에 해당하는 게임 오브젝트를 생성하고 AudioSource를 붙이는 방법으로 각 사운드를 볼륨 조절하고 재생 가능하게 했다.

240613_photonView.IsMine

Q photonView.IsMine?

핑퐁게임에서 플레이어가 움직이는 패들에 대한 내용에서 if (!photonView.IsMine) return; 이 무슨 의미인지 모르겠다.

Q MonoBehaviourPun? PhotonView?

public class Paddle : MonoBehaviourPun
{
    public float speed = 10.0f;

    private void Update()
    {
        if (!photonView.IsMine) return;
        float move = Input.GetAxis("Vertical") * speed * Time.deltaTime;
        transform.Translate(0, move, 0);
    }
}
/// <summary>
/// This class adds the property photonView, while logging a warning when your game still uses the networkView.
/// </summary>
public class MonoBehaviourPun : MonoBehaviour
{
    /// <summary>Cache field for the PhotonView on this GameObject.</summary>
    private PhotonView pvCache;

    /// <summary>A cached reference to a PhotonView on this GameObject.</summary>
    /// <remarks>
    /// If you intend to work with a PhotonView in a script, it's usually easier to write this.photonView.
    ///
    /// If you intend to remove the PhotonView component from the GameObject but keep this Photon.MonoBehaviour,
    /// avoid this reference or modify this code to use PhotonView.Get(obj) instead.
    /// </remarks>
    public PhotonView photonView
    {
        get
        {
            #if UNITY_EDITOR
            // In the editor we want to avoid caching this at design time, so changes in PV structure appear immediately.
            if (!Application.isPlaying || this.pvCache == null)
            {
                this.pvCache = PhotonView.Get(this);
            }
            #else
            if (this.pvCache == null)
            {
                this.pvCache = PhotonView.Get(this);
            }
            #endif
            return this.pvCache;
        }
    }

    //#if UNITY_EDITOR
    //protected virtual void Reset()
    //{
    //    this.pvCache = this.transform.GetParentComponent<PhotonView>();

    //    if (this.pvCache == null)
    //    {
    //        Debug.LogWarning(this.GetType().Name + " requires a PhotonView. No PhotonView was found, so one is being added to GameObject '" + this.transform.root.name + "'");
    //        this.pvCache = this.transform.root.gameObject.AddComponent<PhotonView>();
    //    }
    //}
    //#endif
}

240614_Resource.Load 캐싱

public class ResourceManager : Singleton<ResourceManager>
{
    public Dictionary<string, Object> resourceCache = new Dictionary<string, Object>();

    public T LoadAsset<T>(string path) where T : Object
    {
        if (resourceCache.ContainsKey(path))
        {
            return resourceCache[path] as T;
        }

        T asset = Resources.Load<T>(path);
        if(asset == null)
        {
            Debug.LogError($"Failed to load asset at path : {path}");
            return null;
        }

        resourceCache.Add(path, asset);

        return asset;
    }
}

240617_StateMachine_Player

public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void FixedUpdate();
}

public class StateMachine
{
    protected IState currentState;

    public void ChangeState(IState state)
    {
        currentState?.Exit();
        currentState = state;
        currentState?.Enter();
    }

    public void HandleInput()
    {
        currentState?.HandleInput();
    }

    public void Update()
    {
        currentState?.Update();
    }

    public void FixedUpdate()
    {
        currentState?.FixedUpdate();
    }
}

public class PlayerStateMachine : StateMachine
{
    public FSM_Player Player { get; private set; }
    public GameObject Target { get; private set; }
    public Vector3 MovementDirection { get; set; }

    public PlayerIdleState IdleState { get; }
    public PlayerChasingState ChasingState { get; }
    public PlayerAttackingState AttackingState { get; }

    public PlayerStateMachine(FSM_Player fsmPlayer)
    {
        this.Player = fsmPlayer;

        IdleState = new PlayerIdleState(this);
        ChasingState = new PlayerChasingState(this);
        AttackingState = new PlayerAttackingState(this);

        SetTarget();
    }

    private void SetTarget()
    {
        Target = GameObject.FindGameObjectWithTag(Define.ENEMY_TAG);
        Debug.Log($"Target : {Target.name}");
    }
}
public class PlayerIdleState : PlayerBaseState
{
    public PlayerIdleState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }
    public override void Enter()
    {
        Debug.Log("PlayerIdleState::Enter()");

        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
        StartAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void Exit()
    {
        Debug.Log("PlayerIdleState::Exit()");

        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
        StopAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void Update()
    {
        base.Update();

        if (IsInChasingRange())
        {
            stateMachine.ChangeState(stateMachine.ChasingState);
        }
    }
}

public class PlayerBaseState : IState
{
    protected PlayerStateMachine stateMachine;

    public PlayerBaseState(PlayerStateMachine stateMachine)
    {
        this.stateMachine = stateMachine;
    }
    public virtual void Enter()
    {
    }

    public virtual void Exit()
    {
    }

    public virtual void FixedUpdate()
    {
    }

    public virtual void HandleInput()
    {
    }

    public virtual void Update()
    {
    }

    protected void StartAnimation(int animationHash)
    {
        stateMachine.Player.Animator.SetBool(animationHash, true);
    }

    protected void StopAnimation(int animationHash)
    {
        stateMachine.Player.Animator.SetBool(animationHash, false);
    }

    /// <summary>
    /// 애니메이터의 상태 정보를 가져와서, 주어진 태그에 해당하는 상태의 진행 시간을 반환합니다.
    /// </summary>
    /// <param name="animator"></param>
    /// <param name="tag"></param>
    /// <returns></returns>
    protected float GetNormalizedTime(Animator animator, string tag)
    {
        // 현재 애니메이터의 상태 정보를 가져옵니다.
        AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0);
        AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0);

        // 애니메이터가 전환 중인지 확인하고, 전환 중이라면 다음 상태의 정보를 확인합니다.
        if (animator.IsInTransition(0) && nextInfo.IsTag(tag))
        {
            return nextInfo.normalizedTime;
        }
        // 애니메이터가 전환 중이 아니고, 현재 상태가 주어진 태그와 일치하는지 확인합니다.
        else if (!animator.IsInTransition(0) && currentInfo.IsTag(tag))
        {
            return currentInfo.normalizedTime;
        }
        // 위의 조건에 모두 해당되지 않으면, 진행 시간을 0으로 반환합니다.
        else
        {
            return 0f;
        }
    }

    protected bool IsInChasingRange()
    {
        if(stateMachine.Target == null)
        {
            Debug.Log($"PlayerBaseState::IsInChasingRange() : Target is null");
            return false;
        }

        float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Player.transform.position).sqrMagnitude;

        return playerDistanceSqr <= stateMachine.Player.Data.PlayerChasingRange * stateMachine.Player.Data.PlayerChasingRange;
    }
}
public class PlayerChasingState : PlayerBaseState
{
    public PlayerChasingState(PlayerStateMachine stateMachine) : base(stateMachine)
    {
    }

    public override void Enter()
    {
        Debug.Log("PlayerChasingState::Enter()");
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
        StartAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
    }

    public override void Exit()
    {
        Debug.Log("PlayerChasingState::Exit()");
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.GroundParameterHash);
        StopAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
    }

    public override void Update()
    {
        base.Update();

        if (!IsInChasingRange())
        {
            stateMachine.ChangeState(stateMachine.IdleState);
            return;
        }

        if (IsInAttackRange())
        {
            stateMachine.ChangeState(stateMachine.AttackingState);
            return;
        }

        UpdateMove();
        UpdateRotataion();
    }

    private void UpdateMove()
    {
        stateMachine.MovementDirection = (stateMachine.Target.transform.position - stateMachine.Player.transform.position).normalized;
        stateMachine.Player.transform.position += stateMachine.MovementDirection * stateMachine.Player.Data.BaseSpeed * Time.deltaTime;
    }

    private void UpdateRotataion()
    {
        stateMachine.Player.AimRotation.RotateSprite(stateMachine.MovementDirection);
    }

    protected bool IsInAttackRange()
    {
        float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Player.transform.position).sqrMagnitude;

        return playerDistanceSqr <= stateMachine.Player.Data.AttackRange * stateMachine.Player.Data.AttackRange;
    }
}
public class PlayerAttackingState : PlayerBaseState
{
    public PlayerAttackingState(PlayerStateMachine stateMachine) : base(stateMachine)
    {
    }

    public override void Enter()
    {
        Debug.Log("PlayerAttackingState::Enter()");
        base.Enter();

        StartAnimation(stateMachine.Player.AnimationData.AttackParameterHash);
        StartAnimation(stateMachine.Player.AnimationData.BaseAttackParameterHash);
    }

    public override void Exit()
    {
        Debug.Log("PlayerAttackingState::Exit()");
        base.Exit();

        StopAnimation(stateMachine.Player.AnimationData.AttackParameterHash);
        StopAnimation(stateMachine.Player.AnimationData.BaseAttackParameterHash);
    }

    public override void Update()
    {
        base.Update();

        float normalizedTime = GetNormalizedTime(stateMachine.Player.Animator, "Attack");
        Debug.Log($"normalizedTime : {normalizedTime}");

        // 공격 애니메이션 중
        if (normalizedTime < 1f)
        {

        }

        // 공격 애니메이션 끝
        else
        {
            // 추적 가능한 범위에 있으면 -> 추적 상태로 전환
            if (IsInChasingRange())
            {
                stateMachine.ChangeState(stateMachine.ChasingState);
                return;
            }
            // 추적 범위 밖에 있으면 -> 대기 상태로 전환
            else
            {
                stateMachine.ChangeState(stateMachine.IdleState);
                return;
            }
        }
    }
}

240619_StateMachine_Monster

public class Monster : MonoBehaviour
{
    [field: Header("Animations")]
    [field: SerializeField] public MonsterAnimationData AnimationData { get; private set; }
    [field: SerializeField] public MonsterData Data { get; private set; }

    public Animator Animator { get; private set; }
    public BoxCollider2D BoxCollider2D { get; private set; }
    public SpriteRenderer SpriteRenderer { get; private set; }
    public Vector2 InitialPosition { get; private set; }

    private MonsterStateMachine stateMachine;

    private void Awake()
    {
        AnimationData = new MonsterAnimationData();
        AnimationData.Initialize();
        Data = new MonsterData();

        Animator = GetComponentInChildren<Animator>();
        BoxCollider2D = GetComponent<BoxCollider2D>();
        SpriteRenderer = GetComponentInChildren<SpriteRenderer>();
        InitialPosition = transform.position;

        stateMachine = new MonsterStateMachine(this);
    }

    private void Start()
    {
        stateMachine.ChangeState(stateMachine.IdleState);
    }

    private void Update()
    {
        stateMachine.HandleInput();
        stateMachine.Update();
    }

    private void FixedUpdate()
    {
        stateMachine.FixedUpdate();
    }
}
[SerializeField]
public class MonsterAnimationData
{
    [SerializeField] private string idleParameterName = "Idle";
    [SerializeField] private string walkParameterName = "Chasing";

    public int IdleParameterHash { get; private set; }
    public int ChasingParameterHash { get; private set; }

    public void Initialize()
    {
        IdleParameterHash = Animator.StringToHash(idleParameterName);
        ChasingParameterHash = Animator.StringToHash(walkParameterName);
    }
}
[SerializeField]
public class MonsterData
{
    [SerializeField] public float ChasingRange { get; private set; } = 5.0f;
    [SerializeField] public float BaseSpeed { get; private set; } = 5.0f;
    [SerializeField] public float IdleMovingRange { get; private set; } = 2.0f;
}
public class MonsterStateMachine : StateMachine
{
    public Monster Monster { get; private set; }
    public GameObject Target { get; private set; }
    public Vector3 MovementDirection { get; set; }

    public MonsterIdleState IdleState { get; }
    public MonsterChasingState ChasingState { get; }

    public MonsterStateMachine(Monster monster)
    {
        this.Monster = monster;

        IdleState = new MonsterIdleState(this);
        ChasingState = new MonsterChasingState(this);

        Target = FindTarget();
    }

    public GameObject FindTarget()
    {
        Target = GameObject.FindGameObjectWithTag(Define.PLAYER_TAG);
        if(Target == null)
        {
            Debug.Log($"MonsterStateMachine::FindTarget() Target is null");
            return null;
        }

        Debug.Log($"Target : {Target.name}");
        return Target;
    }
}
public class StateMachine
{
    protected IState currentState;

    public void ChangeState(IState state)
    {
        currentState?.Exit();
        currentState = state;
        currentState?.Enter();
    }

    public void HandleInput()
    {
        currentState?.HandleInput();
    }

    public void Update()
    {
        currentState?.Update();
    }

    public void FixedUpdate()
    {
        currentState?.FixedUpdate();
    }
}
public interface IState
{
    public void Enter();
    public void Exit();
    public void HandleInput();
    public void Update();
    public void FixedUpdate();
}
public class MonsterBaseState : IState
{
    protected MonsterStateMachine stateMachine;

    public MonsterBaseState(MonsterStateMachine stateMachine)
    {
        this.stateMachine = stateMachine;
    }
    public virtual void Enter()
    {
    }

    public virtual void Exit()
    {
    }

    public virtual void FixedUpdate()
    {
    }

    public virtual void HandleInput()
    {
    }

    public virtual void Update()
    {
    }

    protected void StartAnimation(int animationHash)
    {
        stateMachine.Monster.Animator.SetBool(animationHash, true);
    }

    protected void StopAnimation(int animationHash)
    {
        stateMachine.Monster.Animator.SetBool(animationHash, false);
    }

    /// <summary>
    /// 애니메이터의 상태 정보를 가져와서, 주어진 태그에 해당하는 상태의 진행 시간을 반환합니다.
    /// </summary>
    /// <param name="animator"></param>
    /// <param name="tag"></param>
    /// <returns></returns>
    protected float GetNormalizedTime(Animator animator, string tag)
    {
        // 현재 애니메이터의 상태 정보를 가져옵니다.
        AnimatorStateInfo currentInfo = animator.GetCurrentAnimatorStateInfo(0);
        AnimatorStateInfo nextInfo = animator.GetNextAnimatorStateInfo(0);

        // 애니메이터가 전환 중인지 확인하고, 전환 중이라면 다음 상태의 정보를 확인합니다.
        if (animator.IsInTransition(0) && nextInfo.IsTag(tag))
        {
            return nextInfo.normalizedTime;
        }
        // 애니메이터가 전환 중이 아니고, 현재 상태가 주어진 태그와 일치하는지 확인합니다.
        else if (!animator.IsInTransition(0) && currentInfo.IsTag(tag))
        {
            return currentInfo.normalizedTime;
        }
        // 위의 조건에 모두 해당되지 않으면, 진행 시간을 0으로 반환합니다.
        else
        {
            return 0f;
        }
    }

    protected bool IsInChasingRange()
    {
        if(stateMachine.Target == null)
        {
            Debug.Log($"MonsterBaseState::IsInChasingRange() : Target is null");

            return false;
        }

        float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Monster.transform.position).sqrMagnitude;

        return playerDistanceSqr <= stateMachine.Monster.Data.ChasingRange * stateMachine.Monster.Data.ChasingRange;
    }

    protected void RotateSprite(Vector2 direction)
    {
        float rotZ = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        stateMachine.Monster.SpriteRenderer.flipX = Mathf.Abs(rotZ) > 90f;
    }

    protected void UpdateDirection()
    {
        stateMachine.MovementDirection = (stateMachine.Target.transform.position - stateMachine.Monster.transform.position).normalized;
        RotateSprite(stateMachine.MovementDirection);
    }
}
ublic class MonsterIdleState : MonsterBaseState
{
    public MonsterIdleState(MonsterStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }
    public override void Enter()
    {
        Debug.Log("MonsterIdleState::Enter()");

        base.Enter();
        StartAnimation(stateMachine.Monster.AnimationData.IdleParameterHash);
    }

    public override void Exit()
    {
        Debug.Log("MonsterIdleState::Exit()");

        base.Exit();
        StopAnimation(stateMachine.Monster.AnimationData.IdleParameterHash);
    }

    public override void Update()
    {
        base.Update();

        if (IsInChasingRange())
        {
            stateMachine.ChangeState(stateMachine.ChasingState);
            return;
        }

        UpdateIdleMove();
    }

    private void UpdateIdleMove()
    {

    }
}
public class MonsterChasingState : MonsterBaseState
{
    public MonsterChasingState(MonsterStateMachine stateMachine) : base(stateMachine)
    {
    }

    public override void Enter()
    {
        Debug.Log("MonsterChasingState::Enter()");
        base.Enter();
        StartAnimation(stateMachine.Monster.AnimationData.ChasingParameterHash);
    }

    public override void Exit()
    {
        Debug.Log("MonsterChasingState::Exit()");
        base.Exit();
        StopAnimation(stateMachine.Monster.AnimationData.ChasingParameterHash);
    }

    public override void Update()
    {
        base.Update();

        if (!IsInChasingRange())
        {
            stateMachine.ChangeState(stateMachine.IdleState);
            return;
        }

        UpdateDirection();
        UpdateChasingMove();
    }
    private void UpdateChasingMove()
    {
        stateMachine.Monster.transform.position += stateMachine.MovementDirection * stateMachine.Monster.Data.BaseSpeed * Time.deltaTime;
    }
}

240620_코루틴

Q IdleState에 StartCoroutine, InvokeRepeating을 쓸 수 없다.

몬스터가 가만히 있을 때 좌우로 움직인다. 1초마다 랜덤한 방향으로 방향을 반복해서 정하려고 했다. InvokeRepeating을 써서 반복해서 함수를 실행하려고 했다. 그런데 MonsterIdleStateMonsterBaseState 를 상속받고 MonsterBaseStateMonoBehaviour 를 상속받지 않는다. 그래서 Invoke, Coroutine 을 쓸 수 없었다.

  • StateMachine을 갖고있는 Monster는 MonoBehaviour 를 상속 받았다. MonsterIdleState에서 코루틴을 시작하고 종료할 때 stateMachine.Monster를 통해 실행한다.
public class MonsterIdleState : MonsterBaseState
{
    private Vector3 idleMoveDirection;
    private Coroutine directionCoroutine;

    public MonsterIdleState(MonsterStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }
    public override void Enter()
    {
        Debug.Log("MonsterIdleState::Enter()");

        base.Enter();
        StartAnimation(stateMachine.Monster.AnimationData.IdleParameterHash);
        directionCoroutine = stateMachine.Monster.StartCoroutine(SetDirectionCoroutine());
    }

Q 코루틴이 빠르게 실행된다

  • Exit()에서 StopAnimation(stateMachine.Monster.AnimationData.IdleParameterHash); 부분이 없었다.

Monster 가 Idle 상태에서 Chasing 상태로 변한 뒤에 다시 Idle로 들어오면 코루틴이 또 시작되었다. 그래서 1초였던 코루틴 실행간격이 짧아졌었다. 코루틴이 추가로 실행된 것이 문제였다. Idle에서 나갈 때 기존에 실행되던 코루틴을 멈춰줘야 했다.

public class MonsterIdleState : MonsterBaseState
{
    private Vector3 idleMoveDirection;
    private Coroutine directionCoroutine;

    public MonsterIdleState(MonsterStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }
    public override void Enter()
    {
        Debug.Log("MonsterIdleState::Enter()");

        base.Enter();
        StartAnimation(stateMachine.Monster.AnimationData.IdleParameterHash);
        directionCoroutine = stateMachine.Monster.StartCoroutine(SetDirectionCoroutine());
    }

    public override void Exit()
    {
        Debug.Log("MonsterIdleState::Exit()");

        base.Exit();
        StopAnimation(stateMachine.Monster.AnimationData.IdleParameterHash);
        stateMachine.Monster.StopCoroutine(directionCoroutine); 
    }

    /// <summary>
    /// 1초마다 방향을 바꾸는 코루틴
    /// </summary>
    /// <returns></returns>
    private IEnumerator SetDirectionCoroutine()
    {
        while (true)
        {
            int randomValue = Random.Range(-1, 2);

            if (stateMachine.Monster.Data.MonsterType == MonsterType.Horizontal)
            {
                idleMoveDirection = new Vector3(randomValue, 0, 0);
                Debug.Log($"MonsterIdleState::SetDirectionCoroutine() : {idleMoveDirection}");
                RotateSprite(idleMoveDirection);
            }
            else
            {
                idleMoveDirection = new Vector3(0, randomValue, 0);
            }

            yield return new WaitForSeconds(stateMachine.Monster.Data.IdleChangeDirectionSecond);
        }
    }
}

Q PersisitentDataPath 초기화

string으로 변수 선언할 때 바로 넣어버렸더니 path가 null이 뜬다.

  • PersisitentDataPath 는 Awake 나 Start 에서 불러서 넣어야 한다.

Q 프리팹으로 만들어놨는데 인스펙터에서 넣어놓은 내용 빠졌다.

프리팹으로 만들어놨는데 실행하니까 null이 떴다.

  • 네이밍 컨벤션 맞추려고 이름 바꿨다. 그랬더니 인스팩터에서 none으로 떠서 다시 넣었다. 이름 바꾸고 올리기 전에 꼭 테스트 하고 올리자.

0개의 댓글