그런데 노트가 Collider와 충돌하면 파괴되도록 했는데 왜 바와 충돌해도 파괴되지 않을까?
OnTriggerEnter 같은 이벤트 함수들은 자신->자식 순서로 이벤트가 일어나는지 확인 후, 상호작용하기 때문에 자식에 Collider가 붙어있어도 이미 자기 자신에 Collider가 붙어있기 때문에 파괴되지 않는 것이라고 예상하고 있다. 자세한건 테스트 해봐야겠다.
유니티에서 작업한 프로젝트를 Windows와 Android로 실행할 수 있는 파일로 변환하는 빌드 작업을 해볼 것이다.
빌드 과정(Windows편)
File - Build Settings
Platform - Windows 선택 후, Switch Platform 클릭
Scenes in Build에 원하는 씬 추가
Player Settings에서 게임 설정 변경(회사 이름, 게임 아이콘, 해상도 등)
Build 파일을 따로 만들어서 Build 하기
빌드 과정(Android편)
File - Build Settings
Platform - Android 선택 후, Switch Platform 클릭
Scenes in Build에 원하는 씬 추가
Player Settings에서 게임 설정 변경(회사 이름, 게임 아이콘 등)
Minimun API Level과 Target API Level 설정
Build을 따로 만들어서 Build 하기
윈도우와 안드로이드 등 빌드할 때 플러그인을 추가했다면 호완성 이슈가 발생할 수 있다. 안드로이드의 경우 JDK나 Android SDK가 기본적으로 설치 되어있는지 확인해야한다. 스마트폰은 해상도가 다양하기 때문에 UI를 구성할 때 Pivot이나 Anchor 등을 잘 설정해야한다.
Windows Build 화면 크기(해상도) 오류
윈도우를 빌드 할 때 창모드로 진행하는 경우가 많은데, 창모드로 진행하고 저장했을 때 해상도와 위치 또한 저장되기 때문에 다음에 빌드할 때 화상도를 저장해도 적용되지 않는 경우가 있다.
레지스트리편집기에서 HKCU\Software\[회사명]\[제품명] 검색 후 설정된 해상도 값을 수정하거나 삭제하면 된다.
public class EnemyIdleState : EnemyBaseState
{
public EnemyIdleState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
{
}
// 플레이어의 Idle 공유
public override void Enter()
{
stateMachine.MovementSpeedModifier = 0f;
base.Enter();
StartAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.IdleParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StopAnimation(stateMachine.Enemy.AnimationData.IdleParameterHash);
}
public override void Update()
{
if (IsInChaseRange())
{
stateMachine.ChangeState(stateMachine.ChasingState); // 적과 플레이어가 일정 거리가 되면 쫓아간다
return;
}
}
}
적이 플레이어를 쫓는 상태
public class EnemyChasingState : EnemyBaseState
{
public EnemyChasingState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = 1;
base.Enter();
StartAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.RunParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Enemy.AnimationData.GroundParameterHash);
StopAnimation(stateMachine.Enemy.AnimationData.RunParameterHash);
}
public override void Update()
{
base.Update();
if (!IsInChaseRange()) // 플레이어가 사정거리에 들어오지 않았다면
{
stateMachine.ChangeState(stateMachine.IdlingState);
return;
}
else if (IsInAttackRange())
{
stateMachine.ChangeState(stateMachine.AttackState);
return;
}
}
private bool IsInAttackRange()
{
// if (stateMachine.Target.IsDead) { return false; }
float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).sqrMagnitude;
return playerDistanceSqr <= stateMachine.Enemy.Data.AttackRange * stateMachine.Enemy.Data.AttackRange;
}
}
적이 플레이어를 공격하는 상태
public class EnemyAttackState : EnemyBaseState
{
private bool alreadyAppliedForce;
public EnemyAttackState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
{
}
public override void Enter()
{
stateMachine.MovementSpeedModifier = 0;
base.Enter();
StartAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
StopAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);
}
public override void Update()
{
base.Update();
ForceMove();
float normalizedTime = GetNormalizedTime(stateMachine.Enemy.Animator, "Attack");
if (normalizedTime < 1f)
{
if (normalizedTime >= stateMachine.Enemy.Data.ForceTransitionTime)
TryApplyForce();
}
else
{
if (IsInChaseRange()) // 플레이어가 쫓아가는 거리에 있다면
{
stateMachine.ChangeState(stateMachine.ChasingState);
return;
}
else
{
stateMachine.ChangeState(stateMachine.IdlingState);
return;
}
}
}
private void TryApplyForce()
{
if (alreadyAppliedForce) return;
alreadyAppliedForce = true;
stateMachine.Enemy.ForceReceiver.Reset();
stateMachine.Enemy.ForceReceiver.AddForce(stateMachine.Enemy.transform.forward * stateMachine.Enemy.Data.Force);
}
}
플레이어의 애니메이션 스크립트를 기반으로 동작하기 때문에 플레이어 애니메이션 스크립트에 적의 기본 공격을 추가한다.
[SerializeField] private string baseAttackParameterName = "BaseAttack";
public int BaseAttackParameterHash { get; private set; }
public void Initialize()
{
BaseAttackParameterHash = Animator.StringToHash(baseAttackParameterName);
}
유니티로 돌아와서 적의 Scriptable Object를 생성하고 적 스크립트에 연결한다.
기본 공격에 대한 애니메이션을 연결하면 적이 플레이어를 쫓아와서 때린다.
피격 및 처치
피격을 위해 HP와 무기 스크립트를 만든다.
public class Health : MonoBehaviour
{
[SerializeField] private int maxHealth = 100;
private int health;
public event Action OnDie;
public bool IsDead => health == 0;
private void Start()
{
health = maxHealth;
}
public void TakeDamage(int damage)
{
if (health == 0) return;
health = Mathf.Max(health - damage, 0);
if (health == 0)
OnDie?.Invoke();
Debug.Log(health);
}
}
public class Weapon : MonoBehaviour
{
[SerializeField] private Collider myCollider;
private int damage;
private float knockback;
private List<Collider> alreadyColliderWith = new List<Collider>();
private void OnEnable()
{
alreadyColliderWith.Clear();
}
private void OnTriggerEnter(Collider other)
{
if (other == myCollider) return;
if (alreadyColliderWith.Contains(other)) return;
alreadyColliderWith.Add(other);
if(other.TryGetComponent(out CharacterHealth health))
{
health.TakeDamage(damage);
}
if(other.TryGetComponent(out ForceReceiver forceReceiver))
{
Vector3 direction = (other.transform.position - myCollider.transform.position).normalized;
forceReceiver.AddForce(direction * knockback);
}
}
public void SetAttack(int damage, float knockback)
{
this.damage = damage;
this.knockback = knockback;
}
}
공격 애니메이션이 처리 될 때, 데미지를 줄 것인지 처리하기 위해 적 공격 상태 스크립트에 코드를 추가한다.
private bool alreadyAppliedDealing;
public override void Enter()
{
alreadyAppliedForce = false;
alreadyAppliedDealing = false;
stateMachine.MovementSpeedModifier = 0;
base.Enter();
StartAnimation(stateMachine.Enemy.AnimationData.AttackParameterHash);
StartAnimation(stateMachine.Enemy.AnimationData.BaseAttackParameterHash);
}
public override void Update()
{
base.Update();
ForceMove();
float normalizedTime = GetNormalizedTime(stateMachine.Enemy.Animator, "Attack");
if (normalizedTime < 1f)
{
if (normalizedTime >= stateMachine.Enemy.Data.ForceTransitionTime)
TryApplyForce();
if (!alreadyAppliedDealing && normalizedTime >= stateMachine.Enemy.Data.Dealing_Start_TransitionTime) // 딜 안했을 때
{
stateMachine.Enemy.Weapon.SetAttack(stateMachine.Enemy.Data.Damage, stateMachine.Enemy.Data.Force);
stateMachine.Enemy.Weapon.gameObject.SetActive(true);
alreadyAppliedDealing = true;
}
if (alreadyAppliedDealing && normalizedTime >= stateMachine.Enemy.Data.Dealing_End_TransitionTime) // 딜 했을 때
{
stateMachine.Enemy.Weapon.gameObject.SetActive(false);
}
}
else
{
if (IsInChaseRange()) // 플레이어가 쫓아가는 거리에 있다면
{
stateMachine.ChangeState(stateMachine.ChasingState);
return;
}
else
{
stateMachine.ChangeState(stateMachine.IdlingState);
return;
}
}
}
적 스크립트에 무기와 플레이어가 죽었는지에 대한 코드를 추가한다.
[field: SerializeField] public Weapon Weapon { get; private set; }
public Health Health { get; private set; }
void Awake()
{
AnimationData.Initialize();
Rigidbody = GetComponent<Rigidbody>();
Animator = GetComponentInChildren<Animator>();
Controller = GetComponent<CharacterController>();
ForceReceiver = GetComponent<ForceReceiver>();
Health = GetComponent<Health>();
stateMachine = new EnemyStateMachine(this);
}
private void Start()
{
stateMachine.ChangeState(stateMachine.IdlingState);
Health.OnDie += OnDie;
}
void OnDie()
{
Animator.SetTrigger("Die");
enabled = false;
}
유니티로 돌아와서 적 오른쪽 손에 무기 Collider와 Rigidbody를 걸어주고 무기 스크립트를 붙여준다.
적 오브젝트에 붙은 스크립트에 무기를 넣어준다.
플레이어에도 동일하게 스크립트와 무기에 Collider, Rigidbody를 추가한다.
주석 처리했던적 기본 상태 스크립트에서 주석 처리했던 죽음 처리를 풀고 적 상태 머신에서 임시로 처리한 Transform을 Health로 바꾼다.
public class EnemyStateMachine : StateMachine
{
public Enemy Enemy { get; }
// States
public Health Target { get; private set; }
public EnemyStateMachine(Enemy enemy)
{
Enemy = enemy;
Target = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
IdlingState = new EnemyIdleState(this);
ChasingState = new EnemyChasingState(this);
AttackState = new EnemyAttackState(this);
MovementSpeed = enemy.Data.GroundedData.BaseSpeed;
RotationDamping = enemy.Data.GroundedData.BaseRotationDamping;
}
}