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;
}
}
}
SoundManager가 초기화되기 전에 Play 메서드를 호출했다.
Awake 에서 if (_componentInstance == null) 이면 _componentInstance = SoundManager.Instance; 부분을 추가했다..editorconfig 넣어놨는데 코드 utf-8로 안되어있다.
[*]
charset=utf-8
CompareTag 쓴다. 문자열 Define에 저장해놓고 비교하면 유지보수에 좋다.public class Define
{
public enum Sound
{
Bgm = 0,
Effect,
Max,
}
public const string GroundTag = "Ground";
public const string PlayerTag = "Player";
}
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);
}
}
1주일동안 4명이 3D 플랫포머 게임을 만들었다.
https://github.com/SandyLee-00/Unity_PuzzlePlatformer
핑퐁게임에서 플레이어가 움직이는 패들에 대한 내용에서 if (!photonView.IsMine) return; 이 무슨 의미인지 모르겠다.
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
}
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;
}
}
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;
}
}
}
}
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;
}
}
몬스터가 가만히 있을 때 좌우로 움직인다. 1초마다 랜덤한 방향으로 방향을 반복해서 정하려고 했다. InvokeRepeating을 써서 반복해서 함수를 실행하려고 했다. 그런데 MonsterIdleState 는 MonsterBaseState 를 상속받고 MonsterBaseState 는 MonoBehaviour 를 상속받지 않는다. 그래서 Invoke, Coroutine 을 쓸 수 없었다.
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());
}
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);
}
}
}
string으로 변수 선언할 때 바로 넣어버렸더니 path가 null이 뜬다.
PersisitentDataPath 는 Awake 나 Start 에서 불러서 넣어야 한다.프리팹으로 만들어놨는데 실행하니까 null이 떴다.