프로젝트 요약UNET을 활용한 2D 멀티플레이어 플랫포머 프로토타입이다. 직접 설계한 제네릭 기반 FSM(유한 상태 기계)을 네트워크 환경에 접목해, 플레이어의 이동·점프·2D 애니메이션 상태를 클라이언트 간에 동기화하는 로직을 구축했다.

무엇을 만들었나

UNET의 권한(Authority) 제어와 제네릭 FSM 패턴을 결합하여, 확장 가능하고 안정적인 2D 네트워크 캐릭터 컨트롤러를 설계하는 것을 목표로 했다.

항목내용
엔진Unity 2018.3.12f1
기여도100% (1인 개발)
네트워크UNET (isLocalPlayer 권한 분리 · [Command] · [ClientRpc])
상태 관리제네릭 StateMachine<T> 직접 구현 (Animator 미사용)
상태완성
Hierarchy에 PlayerObject(Clone)가 있고 Project 창에 BaseState 계열 스크립트가 보이는 유니티 에디터 화면
네트워크로 스폰된 `PlayerObject(Clone)` 와, Assets/Script 아래의 상태 머신 스크립트들. 플레이 화면은 원작 스프라이트를 참고용으로 쓴 것이라 싣지 않았다.

상태를 클래스로: PlayerObject 와 State

PlayerObject 는 내부에 FSM 인스턴스를 하나 들고 있다. IdleState, RunState, JumpState 각각이 자기 클래스이고, 입력 처리 결과에 따라 Invoke<STATE>() 로 다른 상태로 넘어간다.

private StateMachine<PlayerObject> _stateMachine = null;

void Start ()
{
    ShowSprite( eSprite.Idle );
    if( !isLocalPlayer ) return;

    _stateMachine = new StateMachine<PlayerObject>( this );
    StartCoroutine( _stateMachine.Coroutine<IdleState>() );
}

// RunState의 Update 예시
protected override void Update()
{
    if( Input.GetKey( KeyCode.LeftArrow ) )
    {
        Owner.CmdMove( true );
        Owner.CmdLookAt( true );
    }
    else if( Input.GetKey( KeyCode.RightArrow ) )
    {
        Owner.CmdMove( false );
        Owner.CmdLookAt( false );
    }
    else
    {
        Invoke<IdleState>();
    }

    if( Input.GetKey( KeyCode.Space ) ) Invoke<JumpState>();
}

Start() 에서 !isLocalPlayer 면 상태 머신을 아예 만들지 않는다. 남의 캐릭터는 입력을 읽을 필요가 없고, 화면에 보이는 스프라이트만 서버가 알려 주는 대로 따라가면 되기 때문이다. 대신 스프라이트 초기화(ShowSprite)는 그 검사 에 있어서 모든 클라이언트에서 실행된다.

RunStateUpdate 를 보면 상태 안에서 Owner.CmdMove() 를 바로 호출한다. 상태 클래스가 자기를 소유한 객체를 알고 있어서 가능한 형태다.

Animator 를 쓰지 않고 스프라이트를 바꾼다

[Command]
private void CmdShowSprite(eSprite type)
{
    RpcShowSprite( type );
}

[ClientRpc]
private void RpcShowSprite( eSprite type )
{
    if( !isClient ) return;
    ShowSprite( type );
}

private void ShowSprite( eSprite type )
{
    HideAllSprite();
    switch( type )
    {
        case eSprite.Idle: _spriteIdle.enabled = true; break;
        case eSprite.Run: _spriteRun.enabled = true; break;
        case eSprite.Jump: _spriteJump.enabled = true; break;
    }
}

상태가 바뀌면 클라이언트가 CmdShowSprite 로 서버에 알리고, 서버는 RpcShowSprite모든 클라이언트에게 그 유저의 스프라이트를 바꾸라고 명령한다. ShowSprite 는 전부 끈 뒤 해당 상태의 SpriteRenderer 하나만 켠다.

상태를 코드가 들고 있으면Animator 를 쓰면 상태 전이가 애니메이터 그래프와 코드 두 군데에 나뉜다. 상태를 코드에서 전부 들고 있으면 그 전이를 그대로 [Command] 로 보낼 수 있다. 동기화 대상이 '애니메이션'이 아니라 '상태'가 되는 셈이다.

제네릭 상태 머신의 뼈대

구조체나 enum 방식의 단순 조건문 분기를 탈피하려고 만든 제네릭 기반 상태 머신이다. Owner 접근성을 확보해서 각 상태 내부에서 대상 객체의 메서드를 직접 부를 수 있도록 설계했다.

// BaseStateMachine 클래스의 주요 상태 전환 메서드
virtual protected STATE ChangeState<STATE>() where STATE : BASE_STATE_TYPE, new()
{
    StopState( _currentStateType );
    SetCurrentState<STATE>();
    return (STATE)_currentState;
}

// BaseState 클래스의 코루틴 및 Update 루프
private IEnumerator Coroutine()
{
    Begin();
    yield return Enter();
    while( _bActive )
    {
        yield return null;
        Update();
    }
    End();
}

where STATE : BASE_STATE_TYPE, new() 제약 덕분에 잘못된 타입을 넘기면 컴파일 단계에서 걸린다. 상태 하나의 수명은 코루틴 하나가 통째로 들고 있다. BeginEnter → (Update 반복) → End 순서가 코드에 그대로 드러나서, 상태를 벗어날 때 정리해야 할 것을 End 한곳에 모을 수 있다.

정리