적 상태 머신 준비
스크립트 작성
Enemy
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
[field: Header("References")]
[field: SerializeField] public EnemySO Data { get; private set; }
[field: Header("Animations")]
[field: SerializeField] public PlayerAnimationData AnimationData { get; private set; }
public Rigidbody Rigidbody { get; private set; }
public Animator Animator { get; private set; }
public ForceReceiver ForceReceiver { get; private set; }
public CharacterController Controller { get; private set; }
private EnemyStateMachine stateMachine;
void Awake()
{
AnimationData.Initialize();
Rigidbody = GetComponent<Rigidbody>();
Animator = GetComponentInChildren<Animator>();
Controller = GetComponent<CharacterController>();
ForceReceiver = GetComponent<ForceReceiver>();
stateMachine = new EnemyStateMachine(this);
}
private void Start()
{
stateMachine.ChangeState(stateMachine.IdlingState);
}
private void Update()
{
stateMachine.HandleInput();
stateMachine.Update();
}
private void FixedUpdate()
{
stateMachine.PhysicsUpdate();
}
}
EnemySO
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "EnemySO", menuName = "Characters/Enemy")]
public class EnemySO : ScriptableObject
{
[field: SerializeField] public float PlayerChasingRange { get; private set; } = 10f;
[field: SerializeField] public float AttackRange { get; private set; } = 1.5f;
[field: SerializeField][field: Range(0f, 3f)] public float ForceTransitionTime { get; private set; }
[field: SerializeField][field: Range(-10f, 10f)] public float Force { get; private set; }
[field: SerializeField] public int Damage { get; private set; }
[field: SerializeField][field: Range(0f, 1f)] public float Dealing_Start_TransitionTime { get; private set; }
[field: SerializeField][field: Range(0f, 1f)] public float Dealing_End_TransitionTime { get; private set; }
// PlayerGroundData 공유해서 사용
[field: SerializeField] public PlayerGroundData GroundedData { get; private set; }
}
EnemyStateMachine
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyStateMachine : StateMachine
{
public Enemy Enemy { get; }
public Transform Target { get; private set; }
public EnemyIdleState IdlingState { get; }
public EnemyChasingState ChasingState { get; }
public EnemyAttackState AttackState { get; }
public Vector2 MovementInput { get; set; }
public float MovementSpeed { get; private set; }
public float RotationDamping { get; private set; }
public float MovementSpeedModifier { get; set; } = 1f;
public EnemyStateMachine(Enemy enemy)
{
Enemy = enemy;
Target = GameObject.FindGameObjectWithTag("Player").transform;
IdlingState = new EnemyIdleState(this);
ChasingState = new EnemyChasingState(this);
AttackState = new EnemyAttackState(this);
MovementSpeed = enemy.Data.GroundedData.BaseSpeed;
RotationDamping = enemy.Data.GroundedData.BaseRotationDamping;
}
}
적 상태 적용하기
1. 스크립트 작성
EnemyBaseState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyBaseState : IState
{
protected EnemyStateMachine stateMachine;
protected readonly PlayerGroundData groundData;
public EnemyBaseState(EnemyStateMachine ememyStateMachine)
{
stateMachine = ememyStateMachine;
groundData = stateMachine.Enemy.Data.GroundedData;
}
public virtual void Enter()
{
}
public virtual void Exit()
{
}
public virtual void HandleInput()
{
}
public virtual void Update()
{
Move();
}
public virtual void PhysicsUpdate()
{
}
protected void StartAnimation(int animationHash)
{
stateMachine.Enemy.Animator.SetBool(animationHash, true);
}
protected void StopAnimation(int animationHash)
{
stateMachine.Enemy.Animator.SetBool(animationHash, false);
}
private void Move()
{
Vector3 movementDirection = GetMovementDirection();
Rotate(movementDirection);
Move(movementDirection);
}
protected void ForceMove()
{
stateMachine.Enemy.Controller.Move(stateMachine.Enemy.ForceReceiver.Movement * Time.deltaTime);
}
//
private Vector3 GetMovementDirection()
{
return (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).normalized;
}
private void Move(Vector3 direction)
{
float movementSpeed = GetMovementSpeed();
stateMachine.Enemy.Controller.Move(((direction * movementSpeed) + stateMachine.Enemy.ForceReceiver.Movement) * Time.deltaTime);
}
private void Rotate(Vector3 direction)
{
if (direction != Vector3.zero)
{
direction.y = 0;
Quaternion targetRotation = Quaternion.LookRotation(direction);
stateMachine.Enemy.transform.rotation = Quaternion.Slerp(stateMachine.Enemy.transform.rotation, targetRotation, stateMachine.RotationDamping * Time.deltaTime);
}
}
protected float GetMovementSpeed()
{
float movementSpeed = stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
return movementSpeed;
}
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;
}
else
{
return 0f;
}
}
//
protected bool IsInChaseRange()
{
// if (stateMachine.Target.IsDead) { return false; }
float playerDistanceSqr = (stateMachine.Target.transform.position - stateMachine.Enemy.transform.position).sqrMagnitude;
return playerDistanceSqr <= stateMachine.Enemy.Data.PlayerChasingRange * stateMachine.Enemy.Data.PlayerChasingRange;
}
}
EnemyIdleState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyIdleState : EnemyBaseState
{
public EnemyIdleState(EnemyStateMachine ememyStateMachine) : base(ememyStateMachine)
{
}
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;
}
}
}
EnemyChasingState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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;
}
}
EnemyAttackState
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
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);
}
}
PlayerAnimationData 수정
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
[Serializable]
public class PlayerAnimationData
{
[SerializeField] private string groundParameterName = "@Ground";
[SerializeField] private string idleParameterName = "Idle";
[SerializeField] private string walkParameterName = "Walk";
[SerializeField] private string runParameterName = "Run";
[SerializeField] private string airParameterName = "@Air";
[SerializeField] private string jumpParameterName = "Jump";
[SerializeField] private string fallParameterName = "Fall";
[SerializeField] private string attackParameterName = "@Attack";
[SerializeField] private string baseAttackParameterName = "BaseAttack"; // 추가
[SerializeField] private string comboAttackParameterName = "ComboAttack";
public int GroundParameterHash { get; private set; }
public int IdleParameterHash { get; private set; }
public int WalkParameterHash { get; private set; }
public int RunParameterHash { get; private set; }
public int AirParameterHash { get; private set; }
public int JumpParameterHash { get; private set; }
public int fallParameterHash { get; private set; }
public int AttackParameterHash { get; private set; }
public int ComboAttackParameterHash { get; private set; }
public int BaseAttackParameterHash { get; private set; } // 추가
public void Initialize()
{
GroundParameterHash = Animator.StringToHash(groundParameterName);
IdleParameterHash = Animator.StringToHash(idleParameterName);
WalkParameterHash = Animator.StringToHash(walkParameterName);
RunParameterHash = Animator.StringToHash(runParameterName);
AirParameterHash = Animator.StringToHash(airParameterName);
JumpParameterHash = Animator.StringToHash(jumpParameterName);
fallParameterHash = Animator.StringToHash(fallParameterName);
AttackParameterHash = Animator.StringToHash(attackParameterName);
ComboAttackParameterHash = Animator.StringToHash(comboAttackParameterName);
BaseAttackParameterHash = Animator.StringToHash(baseAttackParameterName); // 추가
}
}
2. Enemy 만들기
Player 복사해서 Enemy 추가
3. EnemySO 생성
4. Enemy 수정
Player, Player Input → Remove Component
Enemy 추가, Enemy SO 연결
5. Animator 수정
BaseAttack 추가
BaseAttack 애니메이션 추가
Default State 설정
6. Player 태그 설정
'유니티' 카테고리의 다른 글
게임 빌드 프로세스 (1) | 2023.12.26 |
---|---|
3D FSM 피격 구현 (3) (1) | 2023.12.26 |
시네머신 카메라 적용 (1) | 2023.12.26 |
3D FSM Player 이동, 점프, 공격 구현 (1) (2) | 2023.12.22 |
3D 오브젝트 이동 정리 (0) | 2023.12.22 |