ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스파르타) The Last Rollback (D-10, Node.js 게임 서버 최종 프로젝트) - DotRecast (4)
    TIL-sparta 2024. 8. 18. 01:41

     

    학습 키워드: C#, .NET, Recast & Detour, DotRecast

     

    1. Progress

    1) Agent 추가 및 이동 테스트:

     

    GitHub - donkim1212/PathfindingDediServer: (WIP) 내배캠 스파르타 Node.js 5기 Team 12 최종 프로젝트 The Last Rollba

    (WIP) 내배캠 스파르타 Node.js 5기 Team 12 최종 프로젝트 The Last Rollback에서 사용될 길찾기 전용 서버 입니다. - donkim1212/PathfindingDediServer

    github.com

     > 24-08-17 일자 commit history 참고

     

    주요 작업 내용

    - AddMonster() 구현하여 DtCrowd에 DtCrowdAgent 추가

    - Start()로 GameLoop() 비동기 실행, 매 루프마다 DtCrowd 업데이트

    - GameLoop의 Update 주기를 _tickRate를 통해 조절

    Figure 1. agent 추가 및 이동 테스트

     

    // Program.cs
    public static void Main()
    {
      Init();
    
      CrowdManager cm = new (1);
      cm.Start();
      cm.AddMonster(1);
      Console.WriteLine("pos: " + cm.GetMonsterPos(1));
    
      SchedulerUtils.SetIntervalAction(1000, () =>
      {
        Console.WriteLine("new pos: " + cm.GetMonsterPos(1));
      });
    
      // Start the TCP server
      StartTcpServer();
    }
    ...
    // CrowdManager.cs
    ...
    private async Task GameLoop()
    {
      _startTime = DateTime.Now;
      DateTime prevTime = _startTime;
      int count = 0;
      while (_state == CrowdManagerState.RUNNING)
      {
        DateTime curTime = DateTime.Now;
        
        float deltaTime = (float) (curTime - prevTime).TotalSeconds;
        Update(deltaTime);
        Console.WriteLine($"[{count++}], deltaTime: {deltaTime}s");
    
        double elapsed = (DateTime.Now - curTime).TotalMilliseconds;
        if (elapsed < _tickRate)
        {
          await Task.Delay(_tickRate - (int) elapsed);
        }
        prevTime = curTime;
      }
    
      Console.WriteLine("Game loop ended.");
    }
    ...

     

     

     

    2) AddMonster 구현하며 학습한 DtCrowd 관련 내용:

    // CrowdManager.cs (작성한 코드)
    ...
    /// <summary>
    /// Add a new monster to the given position, with the given option.
    /// </summary>
    /// <param name="monsterIdx">monster's index to be added to _monsters list</param>
    /// <param name="pos">vector position on navMesh the mosnter is to be spwawned at</param>
    /// <param name="option">crowd agent's option parameters</param>
    public void AddMonster(int monsterIdx, RcVec3f pos, DtCrowdAgentParams option)
    {
      // Add new agent
      DtCrowdAgent agent = _crowd.AddAgent(pos, option);
      _monsters.Add(monsterIdx, agent.idx);
      Console.WriteLine($"monster[{monsterIdx}] spawned at Vector3({pos.X},{pos.Y},{pos.Z})");
    
      // Query NavMesh
      DtNavMeshQuery navQuery = _crowd.GetNavMeshQuery();
      RcVec3f center = new(-5.04f, 0.55f, 135.68f);
      RcVec3f halfExtents = new(3f, 1.5f, 3f);
      
      navQuery.FindNearestPoly(
        center,
        halfExtents,
        new DtQueryDefaultFilter(),
        out long nearestRef,
        out RcVec3f nearestPt,
        out bool isOverPoly
      );
    
      // Set the agent's initial target
      _crowd.RequestMoveTarget(agent, nearestRef, nearestPt);
      Console.WriteLine($"monsterIdx[{monsterIdx}] state: " + agent.state);
    }
    
    public void AddMonster(int monsterIdx, DtCrowdAgentParams option)
    {
      // TODO: create a external function to get a random spawn position on a map
      RcVec3f spawnPos = SpawnerManager.GetRandomSpawnerPosition(_dungeonCode, 1234);
      AddMonster(monsterIdx, spawnPos, option);
    }
    
    public void AddMonster(int monsterIdx)
    {
      // TODO: fetch monster's agent param stats from JSON
    
      DtCrowdAgentParams option = new() // temp stats
      {
        radius = 0.6f,
        height = 1.5f,
        maxAcceleration = 5f,
        maxSpeed = 3.5f,
        collisionQueryRange = 0.6f,
        pathOptimizationRange = 10f, // temp
        separationWeight = 0,
        updateFlags = DtCrowdAgentUpdateFlags.DT_CROWD_ANTICIPATE_TURNS,
        obstacleAvoidanceType = 2,
        queryFilterType = 0,
        userData = new(),
      };
      AddMonster(monsterIdx, option);
    }
    ...
    // DtCrowdAgentParams.cs  (DotRecast)
    ...
    public class DtCrowdAgentParams
    {
        public float radius; // < Agent radius. [Limit: >= 0]
        public float height; // < Agent height. [Limit: > 0]
        public float maxAcceleration; // < Maximum allowed acceleration. [Limit: >= 0]
        public float maxSpeed; // < Maximum allowed speed. [Limit: >= 0]
    
        /// Defines how close a collision element must be before it is considered for steering behaviors. [Limits: > 0]
        public float collisionQueryRange;
    
        public float pathOptimizationRange; // < The path visibility optimization range. [Limit: > 0]
    
        /// How aggresive the agent manager should be at avoiding collisions with this agent. [Limit: >= 0]
        public float separationWeight;
    
        /// the agent path.
        /// Flags that impact steering behavior. (See: #UpdateFlags)
        public int updateFlags;
    
        /// The index of the avoidance configuration to use for the agent.
        /// [Limits: 0 <= value < #DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS]
        public int obstacleAvoidanceType;
    
        /// The index of the query filter used by this agent.
        public int queryFilterType;
    
        /// User defined data attached to the agent.
        public object userData;
    }

     

     AddMonster를 여러 형태의 overload로 구현했다. 기본적으로 사용할 overload는 인자가 하나인 AddMonster(int monsterIdx) 인데, 나중에 몬스터 별 agent 설정을 바꾸게 되면 그 외 두 개의 overload를 사용하게 될 것이다. DtCrowdAgentParams는 agent가 NavMesh 위를 활보할 때 적용되는 각종 설정 값이다. 각각의 값이 무엇을 의미하는지는 라이브러리 코드에 대략적으로 적혀있다.

     

     유의할 부분은 가장 아래 4개 항목이다. 먼저 updateFlags는 DtCrowdAgentUpdateFlags에 상수 값으로 존재하며, 어떤 값을 주느냐에 따라 agent의 steering behavior가 바뀐다. 각각이 정확히 어떻게 동작하는지는 Docs에 따로 언급되어있지 않아서 추후에 따로 알아보기로 했다.

     

    // DtCrowd.cs (DotRecast)
    ...
    public DtCrowd(DtCrowdConfig config, DtNavMesh nav, Func<int, IDtQueryFilter> queryFilterFactory)
    {
        _config = config;
        _agentPlacementHalfExtents = new RcVec3f(config.maxAgentRadius * 2.0f, config.maxAgentRadius * 1.5f, config.maxAgentRadius * 2.0f);
    
        _obstacleQuery = new DtObstacleAvoidanceQuery(config.maxObstacleAvoidanceCircles, config.maxObstacleAvoidanceSegments);
    
        _filters = new IDtQueryFilter[DtCrowdConst.DT_CROWD_MAX_QUERY_FILTER_TYPE];
        for (int i = 0; i < DtCrowdConst.DT_CROWD_MAX_QUERY_FILTER_TYPE; i++)
        {
            _filters[i] = queryFilterFactory.Invoke(i);
        }
    
        // Init obstacle query option.
        _obstacleQueryParams = new DtObstacleAvoidanceParams[DtCrowdConst.DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS];
        for (int i = 0; i < DtCrowdConst.DT_CROWD_MAX_OBSTAVOIDANCE_PARAMS; ++i)
        {
            _obstacleQueryParams[i] = new DtObstacleAvoidanceParams();
        }
    
        // Allocate temp buffer for merging paths.
        _maxPathResult = DtCrowdConst.MAX_PATH_RESULT;
        _pathQ = new DtPathQueue(config);
        _agentIdx = new RcAtomicInteger(0);
        _agents = new Dictionary<int, DtCrowdAgent>();
        _activeAgents = new List<DtCrowdAgent>();
    
        // The navQuery is mostly used for local searches, no need for large node pool.
        SetNavMesh(nav);
    }
    ...
    public void SetObstacleAvoidanceParams(int idx, DtObstacleAvoidanceParams option)
    ...
    public class DtObstacleAvoidanceParams
    {
        public float velBias;
        public float weightDesVel;
        public float weightCurVel;
        public float weightSide;
        public float weightToi;
        public float horizTime;
        public int gridSize; // < grid
        public int adaptiveDivs; // < adaptive
        public int adaptiveRings; // < adaptive
        public int adaptiveDepth; // < adaptive
        ...

     

     다음은 obstacleAvoidanceType인데, 이 값은 0 부터 MAX_OBSTAVOIDANCE_PARAMS 값 사이를 지정해줘야 하고, MAX 값은 DtCrowdConst에 상수로 지정되어있다. DtCrowd를 생성하면 MAX 값 만큼의 길이를 가진 DtObstacleAvoidanceParams 배열이 생기는데, DtCrowd의 SetObstacleAvoidanceParams method를 사용하여 이 배열의 각 index마다 다른 option을 지정할 수 있다. 따라서 agent 생성시에 주는 obstacleAvoidanceType 값은  DtCrowd에서 어느 index에 저장된 avoidance option을 사용할 지를 정하는 것이다.

     

     queryFilterType 또한 DtCrowdConst에 MAX 값이 저장되어있는데, obstacleAvoidanceType과 마찬가지로 DtCrowd가 생성될 때 이 MAX 값 만큼의 배열이 생성된다. IDtQueryFilter를 implement한 custom queryFilter를 여러 개 만들어서 적용할 수 있다. 따로 수정할 계획이 없다면 기본 제공되는 DtQueryDefaultFilter를 사용하면 된다.

     

     마지막 userData는 Object로 선언된, 유저가 정의한 커스텀 데이터를 저장하는 공간이다. 당장에 추가할 정보가 없어서 그냥 new()로 초기화만 해주고 넘어갔다.

     

    (계속)

     

     

    --


    REFERENCES:

     

    https://github.com/eliotjang/the-last-rollback-server

     > The Last Rollback 서버 repo

    https://github.com/donkim1212/PathfindingDediServer/tree/feat/crowd-manager

     > Pathfinding Dedi 서버 repo

    https://rwindegger.github.io/recastnavigation/index.html

     > Recast & Detour Docs

     

    728x90
Designed by Tistory.