최종프로젝트 제작 과정 (적 만들기)
2024. 2. 27. 10:40ㆍ카테고리 없음
플레이어를 따라와 공격을 하는 적의 경우 첫번째 플레이어를 만들었던 방식과 유사하게 만들어보았다. FSM 방식으로 만들어주었는데, 4개읭 State와 이 State를 관리하는 EnemyStateMachine을 만들었다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Enemy : MonoBehaviour
{
public float detectionRange = 2f; //감지 범위.
private Transform player;
private SoundManager soundManager;
private bool isPlayerInRange = false;
[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;
[field: SerializeField] public Weapon Weapon { get; private set; }
public CharacterHealth CharacterHealth { get; private set; }
void Awake()
{
AnimationData.Initialize();
CharacterHealth = GetComponent<CharacterHealth>();
Rigidbody = GetComponent<Rigidbody>();
Animator = GetComponentInChildren<Animator>();
Controller = GetComponent<CharacterController>();
ForceReceiver = GetComponent<ForceReceiver>();
stateMachine = new EnemyStateMachine(this);
}
private void Start()
{
stateMachine.ChangeState(stateMachine.IdlingState);
CharacterHealth.OnDie += OnDie;
player = GameObject.FindGameObjectWithTag("Player").transform;
soundManager = SoundManager.Instance;
}
private void Update()
{
stateMachine.HandleInput();
stateMachine.Update();
// 플레이어와 적의 거리 계산
float distanceToPlayer = Vector3.Distance(transform.position, player.position);
// 플레이어가 일정 범위 내에 있는지 확인
if (distanceToPlayer <= detectionRange)
{
if (!isPlayerInRange)
{
isPlayerInRange = true;
// 적의 범위 내로 플레이어가 들어왔을 때 배경 음악 변경
soundManager.UpdateBackgroundMusic(player.position, transform.position, detectionRange);
}
}
else
{
if (isPlayerInRange)
{
isPlayerInRange = false;
// 적의 범위에서 플레이어가 벗어났을 때 원래의 배경 음악으로 복구
soundManager.RestoreBackgroundMusic();
}
}
}
private void FixedUpdate()
{
stateMachine.PhysicsUpdate();
}
void OnDie()
{
soundManager.RestoreBackgroundMusic();
Animator.SetTrigger("Die");
enabled = false;
}
}
적 캐릭터가 적용되야 되는 모든 기능들은 Enemy 스크립트에 작성을 해주었고, 인터페이스 방식의 상속을 통해 StateMachine을 관리했다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Experimental.AI;
public abstract class StateMachine
{
protected IState currentState;
public void ChangeState(IState newState)
{
currentState?.Exit();
currentState = newState;
currentState?.Enter();
}
public void HandleInput()
{
currentState?.HandleInput();
}
public void Update()
{
currentState?.Update();
}
public void PhysicsUpdate()
{
currentState?.PhysicsUpdate();
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyStateMachine : StateMachine
{
public Enemy Enemy { get; }
public CharacterHealth 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").GetComponent<CharacterHealth>();
IdlingState = new EnemyIdleState(this);
ChasingState = new EnemyChasingState(this);
AttackState = new EnemyAttackState(this);
MovementSpeed = enemy.Data.GroundedData.BaseSpeed;
RotationDamping = enemy.Data.GroundedData.BaseRotationDamping;
}
}
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;
}
}
플레이어와 적 사이의 거리를 계산하여 특정 거리에 도달하게 되면 Chasing이 수행되도록 해주었다.
스크립터블오브젝트를 이용하여 적 데이터 값을 조정할 수 있다.
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; }
[field: SerializeField] public PlayerGroundData GroundedData { get; private set; }
}