2024년 7월 02일 화요일 개발일지 / 최종 프로젝트 개발 시작
2024년 7월 02일 화요일
What I did today : 유한 상태 머신(FSM) 시스템 구축하기
오늘 오전에는 기술 면접 대비 꾸준 실습이 끝나고, 10시부터 팀원 분들이랑 아침 회의를 진행했습니다. 일단 어제 깃허브 팀 등록까지만 하고, 유니티 프로젝트 세팅을 안 했기 때문에 팀장님이 대표로 유니티 프로젝트를 만들고, 저희 프로젝트에 필요한 Input System이나 AI Navigation을 다운로드한 다음, 필요한 폴더들을 미리 정리해서 초기 세팅을 했습니다.
점심을 먹고나서는 어제 정했던 각자의 역할을 토대로 개발 작업을 진행했습니다. 저는 유한 상태 머신을 맡았기 때문에 기존의 UML을 토대로 만들어 나갈 생각이었습니다.
그래서 일단은 강의에서 나왔던 대로 AnimationData.cs를 만들어 애니메이션에 필요한 Bool 값들을 미리 정의했습니다.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Character : MonoBehaviour
{
[field: Header("애니메이션")]
[field: SerializeField] public AnimationData AnimationData { get; private set; }
public Animator Animator { get; private set; }
private void Awake()
{
AnimationData.Initialize();
Animator = GetComponent<Animator>();
}
}
그다음에는 Character 클래스를 만들어서 만들었던 애니메이션 데이터와 애니메이터를 적용했고,
public interface IState
{
public void Enter();
public void Exit();
public void HandleInput();
public void Update();
public void PhysicsUpdate();
}
Enter, Exit, HandleInput, Update, PhysicsUpdate 함수가 있는 IState 인터페이스를 만들었습니다.
public abstract 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 PhysicsUpdate()
{
currentState?.PhysicsUpdate();
}
}
IState 인터페이스를 만들고 난 후에는 기본적인 StateMachine을 만들어서 인터페이스를 변수로서 현재 상태를 나타나게 끔 구성했고, 상태 전환을 위한 ChangeState 함수와 Input 관련된 내용을 처리할 HandleInput, 기존의 생명 주기 함수에서 업데이트를 처리하기 위한 Update 함수, 마찬가지로 FixedUpdate에서 처리할 PhysicsUpdate를 만들었습니다.
그다음에는 ScriptableObject로 임시 캐릭터 SO를 만들어서 캐릭터 스크립트에 SO 데이터를 적용 시켰고,
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CharacterStateMachine : StateMachine
{
public Character Character { get; }
public CharacterStateMachine(Character character)
{
this.Character = character;
}
}
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Character : MonoBehaviour
{
[field: Header("애니메이션")]
[field: SerializeField] public AnimationData AnimationData { get; private set; }
public Animator Animator { get; private set; }
private CharacterStateMachine stateMachine;
private void Awake()
{
AnimationData.Initialize();
Animator = GetComponent<Animator>();
stateMachine = new CharacterStateMachine(this);
}
private void Update()
{
stateMachine.HandleInput();
stateMachine.Update();
}
private void FixedUpdate()
{
stateMachine.PhysicsUpdate();
}
}
만들었던 StateMachine을 바탕으로 CharacterStateMachine 스크립트를 만들었습니다. 그리고 Character 스크립트를 수정하여 CharacterStateMachine을 객체로 생성하면서 자신의 데이터를 넘겨주는 식으로 만들었고, IState 인터페이스에 있던 인풋과 업데이트 관련은 업데이트에 주기적으로 호출하고, PhsicsUpdate는 FixedUpdate에 계속 호출되게 끔 만들었습니다.
public class CharacterBaseState : IState
{
protected CharacterStateMachine stateMachine;
public CharacterBaseState(CharacterStateMachine stateMachine)
{
this.stateMachine = stateMachine;
}
public virtual void Enter()
{
}
public virtual void Exit()
{
}
public virtual void HandleInput()
{
}
public virtual void PhysicsUpdate()
{
}
public virtual void Update()
{
}
protected void StartAnimation(int animatorHash)
{
stateMachine.Character.Animator.SetBool(animatorHash, true);
}
protected void StopAnimation(int animatorHash)
{
stateMachine.Character.Animator.SetBool(animatorHash, false);
}
}
그러고 나서 IState를 상속받는 CharacterBaseState를 만들고 스테이트 머신에 대한 생성자와 인터페이스로 인한 함수들, 그리고 애니메이션의 시작과 종료를 담당하는 함수를 만들어서 적용했습니다.
기본 베이스 상태가 만들어지고 나서는 베이스를 상속받는 GroundState도 만들고, 그다음에는 GroundState를 상속받는 IdleState 만들어서 초기 세팅을 했는데, 그 뒤의 초기 상태들은 비슷한 내용이기 때문에 위의 스크립트처럼 만들게 됐습니다. 뭔가 이렇게 겹치는 거였으면 새로운 도전을 통한 제네릭 상태 머신이라든가 아니면 그냥 통합하는 방식이 더 낫지 않았나 싶네요. 저녁에는 수정 작업을 좀 하다가 팀원분들이랑 작업한 거 공유한 다음에 TIL을 쓰고 오늘 하루를 마무리했습니다.