이론/프로젝트 분석

[Server-Client] Monster FSM/Behavior Tree

킹숭이 2025. 10. 27. 15:20

개요

- 서버를 처음 접해보다 보니 서버의 로직을 짜는데 고민을 많이 했었다. 클라이언트만 구현하면 되었을 땐, 최대한 "클래스가 필요한 데이터를 가지고 '동작'만 할 수 있도록 짜는 것이 가장 좋다" 라고 생각하고 있었다.

- 온라인이 나타나기 시작하면서 다양한 사람들과 함께 플레이하는 게임이 만들어지기 시작했는데, 이때 플레이어들의 정보를 취합해서 전달하는 기능이 필요했다고 한다. 

 

  • 여러 사용자와 상호 작용
  • 클라이언트에서 해킹당하면 안 되는 처리
  • 플레이어의 상태 보관

이 세 가지가 서버가 하는 일이라고 " 게임 서버 프로그래밍 교과서 " 라는 책의 일부분에서 주워 들었다.

 

핵심 처리

- 그래서 나는 몬스터의 로직은 이렇게 나누기로 결정했다.

  • 1. 서버 : 몬스터의 정보, 판정, 변경되는 로직을 처리
  • 2. 클라이언트 : 서버가 전달하는 정보를 토대로 보여지는 렌더링 (ex. 애니메이션, 이펙트 등) 처리

- 그 이유는 이러한 구분 자체가 몬스터를 구현하는데 굉장히 깔끔하다는 느낌을 받았다. 또한, 정보 자체를 서버에서 관리하면 권위적으로 서버가 결정하여 전달하기 때문에 정보 꼬임으로부터 크게 신경 쓰지 않아도 되고, 보안적으로도 안전하기 때문이었다.

- 추가적으로 플레이어와의 상호작용이 많은 몬스터의 경우는 플레이어의 정보를 즉각 받아 처리하여 반응 속도에 대해서 클라이언트에서 관리하는 것보다 좋을 것이라고 생각했다.


 

 

서버 : FSM 패턴

- 서버는 핵심 정보를 변경하는 역할을 한다. 

- 때문에 몬스터의 상태 정보를 정확히 변경하고, 그 상태에 따른 행동을 명확히 해야 한다. 그 부분에 대해서는 FSM 패턴이 좋은 디자인 패턴이 되겠다고 생각했다.

 

- FSM 패턴 자체가 한 행동에 대한 부분을 클래스마다 분리하니까 문제가 되는 행동을 명확히 파악할 수 있다.

- 특히 유용한 점은 입장 함수에서 변경된 정보의 패킷을 한 번만 보냈기 때문에(Moving 시에는 제외) 패킷을 언제 보내는 지 정확히 파악하기가 쉬웠다.

 

로직

  // FSM Interface
  public interface ISkillBehavior
  {
      void OnStart(Monster caster, MonsterSkillData skillData);
      void OnUpdate(Monster caster);
      void OnHit(Monster caster, Creature target);
      void OnEnd(Monster caster);
  }
    
  // FSM 변경
  public override void Update()
  {
      _currentState?.Execute(this);
  }

  #region State
  public void ChangeState(IMonsterState newState)
  {
      _currentState?.Exit(this);

      State = DetermineMonsterState(newState);

      _currentState = newState;
      _currentState?.Enter(this);
  }

-

 

 

문제점 : AI의 문제인가, 로직의 문제인가

- 사실 충돌 시점에서 Monster의 Position에 대해서 문제가 발생했다. 

- 몬스터와 플레이어 사이의 장애물이 존재할 때 몬스터가 스킬을 사용하면 서버와 클라이언트 상의 위치가 현저히 꼬이는 것이다.

- 물론 서버의 정보가 정확했다. Astar와 Funnel 알고리즘을 구현할 때 데이터 쪼가리인 폴리곤을 보간하는 사이에 장애물에 대한 충돌 처리까지 포함시키지 않아 서버 상에서는 그대로 직선 거리를 장애물을 통과 시켜 만들어냈던 것이다..... (이걸 찾고자 작업 시간 내내 고민했다)

 

- 현업에 대한 문제이지만, 충돌 담당에게 벽 충돌 처리를 요청했더니 ~하면 될 것 같으니 네가 만들어보라는 대답을 들은 후, 

그렇게 되면 충돌 에디터까지 만들어 프로젝트 시간이 오래 걸릴 것이라 생각해 어차피 몬스터는 거의 장애물이 없는 공간에서만 움직이기에 타협하고 그 시간에 필요한 기능을 만들자는 생각을 했다.

(결국 장애물을 마주치면 장애물에 걸려 움직임이 틀어지면, 서버 위치로 몬스터가 순간이동 하여 재정비 시켰다.)

- 물론 현업 시에 불편한 부분, 충돌이라고 볼 수 있지만 팀원들이 수동적인 편이었다고 생각하기에 그날 팀 플레이를 이렇게 하는 것이 맞을까라는 생각을 했다. (협업 시 리더십 있는 사람이 되고 싶다고 생각했다 ㅠ.ㅠ)

 

결론

- 문제로 머리를 좀 싸매긴 했지만, 원인을 찾았고 정보 변경 상 FSM이 유용하다고 생각했다. DirectX로 프로젝트를 제작했을 땐 급하게 프로젝트를 마무리 해야 했기도 하고, 렌더링 변경 부분까지 상태 변경 코드에 집에 넣어 로직이 더럽다는 생각도 하고, 꼬일 때도 있었지만, 이번 프로젝트에서 동작 방시게 대해서 FSM을 사용했을 때는 이 패턴에 대한 장점을 직접 느낄 수 있었고, 조금이라는 꼬이는 부분이 있다면 금방 고칠 수 있었다.

- 살짝 아쉬웠던 부분은 사용되는 스킬에 따라서 처리 부분이 달랐기에 skill FSM을 추가로 만들었는데 이 부분에 대해서는

기존 스킬 데이터 json 파일에 호출할 스킬 클래스 이름을 데이터화 시켜 전략 패턴을 사용해도 좋았을 것이라고 생각한다.

 


 

 

클라이언트 : Behavior Tree

- 클라이언트는 Material을 변경하거나 Effect/Animation을 알맞게 호출을 해야 한다.

- Behavior Tree를 선택한 이유는 AND/OR 표현이 가능해서 이펙트 호출 조건의 다양성과 애니메이션 및 머티리얼 변경 같은 복합적인 상황을 순서 및 분기 흐름으로 효율적으로 조율할 수 있으며, 특히 요구 사항을 JSON 데이터로 관리하여 유연성을 높일 수 있다고 판단했기 때문에 선택했다.

- 또한, 한 번 만들어 놓은 후부터 데이터화만 시키면 쉽게 원하는 표현이 가능했고, 같은 부분에 대해서 여러 번 사용되기 때문에 재사용하는 데 있어서 좋을 것이라고 생각했다.

 

로직

 

 

 

 

 

Behavior Tree  참고 영상

https://www.youtube.com/watch?time_continue=1277&v=MT2C2Msr3xc&embeds_referring_euri=https%3A%2F%2Fchacha-nyang.tistory.com%2Fentry%2F%25EB%25B9%2584%25ED%2597%25A4%25EC%259D%25B4%25EB%25B9%2584%25EC%2596%25B4-%25ED%258A%25B8%25EB%25A6%25AC-%25ED%2596%2589%25EB%258F%2599-%25ED%258&source_ve_path=MjM4NTE