Cách chúng tôi nâng cao cơ chế tính toán đạn đạo cho game bắn súng trên thiết bị di động bằng thuật toán bù độ trễ mạng

Cách chúng tôi nâng cao cơ chế tính toán đạn đạo cho game bắn súng trên thiết bị di động bằng thuật toán bù độ trễ mạng

Xin chào, tôi là Nikita Brizhak, nhà phát triển máy chủ từ Pixonic. Hôm nay tôi muốn nói về việc bù đắp độ trễ trong chế độ nhiều người chơi trên thiết bị di động.

Nhiều bài báo đã viết về việc bù đắp độ trễ của máy chủ, bao gồm cả bằng tiếng Nga. Điều này không có gì đáng ngạc nhiên, vì công nghệ này đã được sử dụng tích cực trong việc tạo ra FPS nhiều người chơi từ cuối những năm 90. Ví dụ: bạn có thể nhớ mod QuakeWorld, đây là một trong những mod đầu tiên sử dụng nó.

Chúng tôi cũng sử dụng nó trong game bắn súng nhiều người chơi di động Dino Squad.

Trong bài viết này, mục tiêu của tôi không phải là lặp lại những gì đã được viết hàng nghìn lần mà là để cho biết cách chúng tôi triển khai tính năng bù độ trễ trong trò chơi của mình, có tính đến nền tảng công nghệ và các tính năng trò chơi cốt lõi của chúng tôi.

Một vài lời về vỏ não và công nghệ của chúng tôi.

Dino Squad là một game bắn súng PvP mạng di động. Người chơi điều khiển những con khủng long được trang bị nhiều loại vũ khí và chiến đấu với nhau theo đội 6v6.

Cả máy khách và máy chủ đều dựa trên Unity. Kiến trúc khá cổ điển đối với các game bắn súng: máy chủ độc đoán và khả năng dự đoán của máy khách hoạt động trên máy khách. Mô phỏng trò chơi được viết bằng ECS ​​nội bộ và được sử dụng trên cả máy chủ và máy khách.

Nếu đây là lần đầu tiên bạn nghe nói về việc bù độ trễ thì đây là phần giới thiệu ngắn gọn về vấn đề này.

Trong các game FPS nhiều người chơi, trận đấu thường được mô phỏng trên máy chủ từ xa. Người chơi gửi thông tin đầu vào của họ (thông tin về các phím được nhấn) đến máy chủ và để phản hồi, máy chủ sẽ gửi cho họ trạng thái trò chơi cập nhật có tính đến dữ liệu nhận được. Với sơ đồ tương tác này, độ trễ giữa việc nhấn phím tiến và thời điểm nhân vật người chơi di chuyển trên màn hình sẽ luôn lớn hơn ping.

Mặc dù trên mạng cục bộ, độ trễ này (thường được gọi là độ trễ đầu vào) có thể không được chú ý, nhưng khi chơi qua Internet, nó tạo ra cảm giác “trượt trên băng” khi điều khiển nhân vật. Vấn đề này có liên quan gấp đôi đối với các mạng di động, trong đó trường hợp ping của người chơi là 200 ms vẫn được coi là một kết nối tuyệt vời. Thông thường ping có thể là 350, 500 hoặc 1000 ms. Sau đó, gần như không thể chơi một game bắn súng nhanh với độ trễ đầu vào.

Giải pháp cho vấn đề này là dự đoán mô phỏng phía máy khách. Ở đây, máy khách sẽ tự áp dụng thông tin đầu vào cho nhân vật người chơi mà không cần chờ phản hồi từ máy chủ. Và khi nhận được câu trả lời, nó chỉ cần so sánh kết quả và cập nhật vị trí của đối thủ. Độ trễ giữa việc nhấn phím và hiển thị kết quả trên màn hình trong trường hợp này là tối thiểu.

Điều quan trọng là phải hiểu sắc thái ở đây: máy khách luôn tự rút ra theo đầu vào cuối cùng của nó và kẻ thù - với độ trễ mạng, theo trạng thái trước đó từ dữ liệu từ máy chủ. Tức là khi bắn vào kẻ thù, người chơi sẽ nhìn thấy hắn trong quá khứ so với chính mình. Tìm hiểu thêm về dự đoán của khách hàng chúng tôi đã viết trước đó.

Do đó, dự đoán của khách hàng giải quyết được một vấn đề nhưng lại tạo ra một vấn đề khác: nếu người chơi bắn vào điểm mà kẻ thù đã ở trước đây, thì trên máy chủ khi bắn vào cùng một điểm, kẻ thù có thể không còn ở nơi đó nữa. Nỗ lực bù đắp độ trễ của máy chủ để giải quyết vấn đề này. Khi vũ khí được bắn, máy chủ sẽ khôi phục trạng thái trò chơi mà người chơi đã nhìn thấy cục bộ tại thời điểm bắn và kiểm tra xem liệu anh ta có thực sự bắn trúng kẻ thù hay không. Nếu câu trả lời là “có”, lượt đánh sẽ được tính, ngay cả khi kẻ địch không còn ở trên máy chủ vào thời điểm đó.

Được trang bị kiến ​​thức này, chúng tôi bắt đầu triển khai tính năng bù độ trễ của máy chủ trong Dino Squad. Trước hết, chúng ta phải hiểu cách khôi phục trên máy chủ những gì khách hàng đã thấy? Và chính xác những gì cần phải được khôi phục? Trong trò chơi của chúng tôi, các đòn đánh từ vũ khí và khả năng được tính toán thông qua các tia phát sóng và lớp phủ - nghĩa là thông qua các tương tác với máy va chạm vật lý của kẻ thù. Theo đó, chúng tôi cần tái tạo vị trí của các máy va chạm này mà người chơi “nhìn thấy” cục bộ trên máy chủ. Vào thời điểm đó chúng tôi đang sử dụng Unity phiên bản 2018.x. API vật lý ở đó là tĩnh, thế giới vật lý tồn tại trong một bản sao duy nhất. Không có cách nào để lưu trạng thái của nó và sau đó khôi phục nó từ hộp. Vậy lam gi?

Giải pháp nằm ở bề ngoài; tất cả các yếu tố của nó đã được chúng tôi sử dụng để giải quyết các vấn đề khác:

  1. Đối với mỗi khách hàng, chúng ta cần biết anh ta nhìn thấy đối thủ vào thời điểm nào khi nhấn phím. Chúng tôi đã ghi thông tin này vào gói đầu vào và sử dụng nó để điều chỉnh dự đoán của khách hàng.
  2. Chúng ta cần có khả năng lưu trữ lịch sử của các trạng thái trò chơi. Trong đó, chúng ta sẽ giữ vị trí của đối thủ (và do đó là người va chạm của họ). Chúng tôi đã có lịch sử trạng thái trên máy chủ, chúng tôi sử dụng nó để xây dựng đồng bằng châu thổ. Biết đúng thời điểm, chúng ta dễ dàng tìm được trạng thái phù hợp trong lịch sử.
  3. Bây giờ chúng ta đã có trạng thái trò chơi từ lịch sử trong tay, chúng ta cần có khả năng đồng bộ hóa dữ liệu người chơi với trạng thái của thế giới vật lý. Máy va chạm hiện có - di chuyển, những cái còn thiếu - tạo, những cái không cần thiết - phá hủy. Logic này cũng đã được viết sẵn và bao gồm một số hệ thống ECS. Chúng tôi đã sử dụng nó để tổ chức một số phòng trò chơi trong một quy trình Unity. Và vì thế giới vật chất là một thế giới cho mỗi quy trình nên nó phải được tái sử dụng giữa các phòng. Trước mỗi tích tắc mô phỏng, chúng tôi "đặt lại" trạng thái của thế giới vật lý và khởi tạo lại nó bằng dữ liệu cho phòng hiện tại, cố gắng sử dụng lại các đối tượng trong trò chơi Unity nhiều nhất có thể thông qua hệ thống tổng hợp thông minh. Tất cả những gì còn lại là sử dụng logic tương tự cho trạng thái trò chơi trong quá khứ.

Bằng cách kết hợp tất cả những yếu tố này lại với nhau, chúng ta đã có được một “cỗ máy thời gian” có thể quay ngược trạng thái của thế giới vật chất về đúng thời điểm. Mã hóa ra rất đơn giản:

public class TimeMachine : ITimeMachine
{
     //История игровых состояний
     private readonly IGameStateHistory _history;

     //Текущее игровое состояние на сервере
     private readonly ExecutableSystem[] _systems;

     //Набор систем, расставляющих коллайдеры в физическом мире 
     //по данным из игрового состояния
     private readonly GameState _presentState;

     public TimeMachine(IGameStateHistory history, GameState presentState, ExecutableSystem[] timeInitSystems)
     {
         _history = history; 
         _presentState = presentState;
         _systems = timeInitSystems;  
     }

     public GameState TravelToTime(int tick)
     {
         var pastState = tick == _presentState.Time ? _presentState : _history.Get(tick);
         foreach (var system in _systems)
         {
             system.Execute(pastState);
         }
         return pastState;
     }
}

Tất cả những gì còn lại là tìm ra cách sử dụng chiếc máy này để dễ dàng bù đắp cho những cú đánh và khả năng.

Trong trường hợp đơn giản nhất, khi cơ chế dựa trên một lượt quét duy nhất, mọi thứ dường như đã rõ ràng: trước khi người chơi bắn, anh ta cần đưa thế giới vật chất trở lại trạng thái mong muốn, thực hiện raycast, đếm số lần bắn hoặc bắn trượt, và đưa thế giới trở lại trạng thái ban đầu.

Nhưng có rất ít cơ chế như vậy trong Dino Squad! Hầu hết các loại vũ khí trong trò chơi đều tạo ra đạn - những viên đạn có tuổi thọ cao bay trong vài tích tắc mô phỏng (trong một số trường hợp là hàng chục tích tắc). Phải làm gì với họ, họ nên bay vào lúc mấy giờ?

В đồ cổ về ngăn xếp mạng Half-Life, những người ở Valve đã hỏi câu hỏi tương tự và câu trả lời của họ là: việc bù độ trễ của đường đạn là có vấn đề và tốt hơn hết là nên tránh nó.

Chúng tôi không có tùy chọn này: vũ khí dựa trên đạn là tính năng chính của thiết kế trò chơi. Vì vậy, chúng tôi phải nghĩ ra một cái gì đó. Sau một hồi suy nghĩ, chúng tôi đã đưa ra hai lựa chọn có vẻ hiệu quả:

1. Chúng tôi buộc đường đạn vào thời điểm của người chơi đã tạo ra nó. Mỗi tích tắc của mô phỏng máy chủ, đối với mỗi viên đạn của mỗi người chơi, chúng tôi đưa thế giới vật lý về trạng thái máy khách và thực hiện các phép tính cần thiết. Cách tiếp cận này giúp có thể phân phối tải trên máy chủ và có thể dự đoán được thời gian bay của đạn. Khả năng dự đoán đặc biệt quan trọng đối với chúng tôi, vì chúng tôi có tất cả đường đạn, bao gồm cả đường đạn của kẻ thù, được dự đoán trên máy khách.

Cách chúng tôi nâng cao cơ chế tính toán đạn đạo cho game bắn súng trên thiết bị di động bằng thuật toán bù độ trễ mạng
Trong hình, người chơi ở mốc 30 bắn tên lửa với dự đoán: anh ta nhìn thấy hướng kẻ địch đang chạy và biết tốc độ gần đúng của tên lửa. Tại địa phương, anh ta thấy rằng mình đã bắn trúng mục tiêu ở tích tắc thứ 33. Nhờ bù lag nên nó cũng sẽ xuất hiện trên máy chủ

2. Chúng tôi làm mọi thứ giống như trong tùy chọn đầu tiên, nhưng, sau khi đếm được một tích tắc của mô phỏng viên đạn, chúng tôi không dừng lại mà tiếp tục mô phỏng chuyến bay của nó trong cùng một tích tắc máy chủ, mỗi lần đưa thời gian của nó đến gần máy chủ hơn đánh dấu từng cái một và cập nhật vị trí máy va chạm. Chúng tôi làm điều này cho đến khi một trong hai điều xảy ra:

  • Viên đạn đã hết hạn sử dụng. Điều này có nghĩa là việc tính toán đã kết thúc, chúng ta có thể đếm một cú đánh trượt hoặc một cú đánh. Và đây chính là thời điểm phát súng được bắn ra! Đối với chúng tôi đây vừa là điểm cộng vừa là điểm trừ. Một điểm cộng - bởi vì đối với người chơi bắn súng, điều này làm giảm đáng kể độ trễ giữa đòn đánh và mức giảm máu của kẻ thù. Nhược điểm là hiệu ứng tương tự cũng xảy ra khi đối thủ bắn vào người chơi: có vẻ như kẻ thù chỉ bắn một tên lửa chậm và sát thương đã được tính.
  • Viên đạn đã đến giờ máy chủ. Trong trường hợp này, mô phỏng của nó sẽ tiếp tục trong tích tắc máy chủ tiếp theo mà không có bất kỳ sự bù trừ độ trễ nào. Đối với các đường đạn bay chậm, về mặt lý thuyết, điều này có thể làm giảm số lần quay ngược vật lý so với tùy chọn đầu tiên. Đồng thời, tải không đồng đều trên mô phỏng tăng lên: máy chủ không hoạt động hoặc trong một tích tắc máy chủ, nó đang tính toán hàng tá tích tắc mô phỏng cho một số viên đạn.

Cách chúng tôi nâng cao cơ chế tính toán đạn đạo cho game bắn súng trên thiết bị di động bằng thuật toán bù độ trễ mạng
Kịch bản tương tự như trong hình trước, nhưng được tính theo sơ đồ thứ hai. Tên lửa “bắt kịp” thời gian của máy chủ vào đúng thời điểm phát bắn xảy ra và lần bắn trúng có thể được tính ngay từ tích tắc tiếp theo. Ở tích tắc thứ 31, trong trường hợp này, việc bù độ trễ không còn được áp dụng

Trong quá trình triển khai của chúng tôi, hai cách tiếp cận này chỉ khác nhau ở một vài dòng mã, vì vậy chúng tôi đã tạo cả hai và trong một thời gian dài chúng tồn tại song song. Tùy thuộc vào cơ chế của vũ khí và tốc độ của viên đạn, chúng tôi chọn phương án này hoặc phương án khác cho mỗi con khủng long. Bước ngoặt ở đây là sự xuất hiện trong trò chơi cơ học như “nếu bạn đánh kẻ thù nhiều lần trong thời gian như vậy, thì sẽ nhận được phần thưởng như vậy”. Bất kỳ thợ máy nào mà thời điểm người chơi đánh kẻ thù đóng vai trò quan trọng đều từ chối làm việc với cách tiếp cận thứ hai. Vì vậy, chúng tôi đã quyết định chọn tùy chọn đầu tiên và giờ đây nó áp dụng cho tất cả vũ khí và mọi khả năng hoạt động trong trò chơi.

Riêng biệt, cần nêu lên vấn đề hiệu suất. Nếu bạn nghĩ rằng tất cả những điều này sẽ làm mọi thứ chậm lại thì tôi trả lời: đúng vậy. Unity khá chậm trong việc di chuyển máy va chạm cũng như bật và tắt chúng. Trong Dino Squad, trong trường hợp "xấu nhất", có thể có hàng trăm quả đạn tồn tại đồng thời trong chiến đấu. Di chuyển máy va chạm để đếm từng viên đạn riêng lẻ là một điều xa xỉ không thể chấp nhận được. Vì vậy, chúng tôi thực sự cần thiết phải giảm thiểu số lần “rollback” vật lý. Để làm điều này, chúng tôi đã tạo một thành phần riêng trong ECS ​​để ghi lại thời gian của người chơi. Chúng tôi đã thêm nó vào tất cả các thực thể yêu cầu bù độ trễ (đạn, khả năng, v.v.). Trước khi bắt đầu xử lý các thực thể như vậy, chúng tôi phân cụm chúng vào thời điểm này và xử lý chúng cùng nhau, khôi phục thế giới vật chất một lần cho mỗi cụm.

Ở giai đoạn này, chúng tôi có một hệ thống làm việc chung. Mã của nó ở dạng hơi đơn giản hóa:

public sealed class LagCompensationSystemGroup : ExecutableSystem
{
     //Машина времени
     private readonly ITimeMachine _timeMachine;

     //Набор систем лагкомпенсации
     private readonly LagCompensationSystem[] _systems;
     
     //Наша реализация кластеризатора
     private readonly TimeTravelMap _travelMap = new TimeTravelMap();

    public LagCompensationSystemGroup(ITimeMachine timeMachine, 
        LagCompensationSystem[] lagCompensationSystems)
     {
         _timeMachine = timeMachine;
         _systems = lagCompensationSystems;
     }

     public override void Execute(GameState gs)
     {
         //На вход кластеризатор принимает текущее игровое состояние,
         //а на выход выдает набор «корзин». В каждой корзине лежат энтити,
         //которым для лагкомпенсации нужно одно и то же время из истории.
         var buckets = _travelMap.RefillBuckets(gs);

         for (int bucketIndex = 0; bucketIndex < buckets.Count; bucketIndex++)
         {
             ProcessBucket(gs, buckets[bucketIndex]);
         }

         //В конце лагкомпенсации мы восстанавливаем физический мир 
         //в исходное состояние
         _timeMachine.TravelToTime(gs.Time);
     }

     private void ProcessBucket(GameState presentState, TimeTravelMap.Bucket bucket)
     {
         //Откатываем время один раз для каждой корзины
         var pastState = _timeMachine.TravelToTime(bucket.Time);

         foreach (var system in _systems)
         {
               system.PastState = pastState;
               system.PresentState = presentState;

               foreach (var entity in bucket)
               {
                   system.Execute(entity);
               }
          }
     }
}

Tất cả những gì còn lại là cấu hình chi tiết:

1. Hiểu bao nhiêu để hạn chế khoảng cách di chuyển tối đa trong thời gian.

Điều quan trọng đối với chúng tôi là làm cho trò chơi trở nên dễ tiếp cận nhất có thể trong điều kiện mạng di động kém, vì vậy chúng tôi đã giới hạn câu chuyện ở biên độ 30 tích tắc (với tốc độ đánh dấu là 20 Hz). Điều này cho phép người chơi đánh được đối thủ ngay cả ở tốc độ ping rất cao.

2. Xác định đồ vật nào có thể chuyển động theo thời gian, đồ vật nào không thể chuyển động.

Tất nhiên, chúng tôi đang di chuyển đối thủ của mình. Nhưng các lá chắn năng lượng có thể lắp đặt được thì không. Chúng tôi quyết định rằng tốt hơn hết là nên ưu tiên khả năng phòng thủ, như thường làm trong các game bắn súng trực tuyến. Nếu người chơi đã đặt một tấm khiên ở hiện tại, những viên đạn bù độ trễ từ quá khứ sẽ không bay qua nó.

3. Quyết định xem có cần thiết phải bù đắp cho khả năng của khủng long hay không: cắn, tấn công đuôi, v.v. Chúng tôi đã quyết định những gì cần thiết và xử lý chúng theo các quy tắc tương tự như đạn.

4. Xác định phải làm gì với các máy va chạm của người chơi đang được thực hiện bù độ trễ. Theo một cách tốt, vị trí của họ không nên chuyển về quá khứ: người chơi sẽ nhìn thấy chính mình trong cùng thời điểm mà anh ta hiện đang ở trên máy chủ. Tuy nhiên, chúng tôi cũng khôi phục máy va chạm của người chơi bắn súng và có một số lý do cho việc này.

Đầu tiên, nó cải thiện khả năng phân cụm: chúng tôi có thể sử dụng cùng một trạng thái vật lý cho tất cả người chơi có ping gần.

Thứ hai, trong tất cả các lần phát sóng và chồng chéo, chúng tôi luôn loại trừ các máy va chạm của người chơi sở hữu khả năng hoặc đường đạn. Trong Dino Squad, người chơi điều khiển những con khủng long có hình dạng khá khác thường so với tiêu chuẩn của game bắn súng. Ngay cả khi người chơi bắn ở một góc bất thường và quỹ đạo của viên đạn đi qua máy va chạm khủng long của người chơi, viên đạn sẽ bỏ qua nó.

Thứ ba, chúng tôi tính toán vị trí của vũ khí của khủng long hoặc điểm áp dụng khả năng bằng cách sử dụng dữ liệu từ ECS ngay cả trước khi bắt đầu bù độ trễ.

Do đó, vị trí thực sự của máy va chạm của người chơi được bù độ trễ là không quan trọng đối với chúng tôi, vì vậy chúng tôi đã chọn một con đường hiệu quả hơn và đồng thời đơn giản hơn.

Độ trễ mạng không thể được loại bỏ một cách đơn giản mà nó chỉ có thể bị che đi. Giống như bất kỳ phương pháp ngụy trang nào khác, việc bù đắp độ trễ của máy chủ cũng có những điểm cân bằng. Nó cải thiện trải nghiệm chơi trò chơi của người chơi đang bắn với cái giá phải trả là người chơi bị bắn. Tuy nhiên, đối với Dino Squad, sự lựa chọn ở đây là hiển nhiên.

Tất nhiên, tất cả những điều này cũng phải trả giá bằng sự phức tạp ngày càng tăng của toàn bộ mã máy chủ - đối với cả lập trình viên và nhà thiết kế trò chơi. Nếu trước đó mô phỏng là một cuộc gọi tuần tự đơn giản của các hệ thống, thì với tính năng bù độ trễ, các vòng lặp và nhánh lồng nhau sẽ xuất hiện trong đó. Chúng tôi cũng đã dành rất nhiều nỗ lực để làm cho nó thuận tiện khi làm việc.

Trong phiên bản 2019 (và có thể sớm hơn một chút), Unity đã bổ sung hỗ trợ đầy đủ cho các cảnh vật lý độc lập. Chúng tôi đã triển khai chúng trên máy chủ gần như ngay lập tức sau khi cập nhật, vì chúng tôi muốn nhanh chóng loại bỏ thế giới vật lý chung cho tất cả các phòng.

Chúng tôi cung cấp cho mỗi phòng trò chơi khung cảnh vật lý riêng và do đó loại bỏ nhu cầu “xóa” cảnh đó khỏi dữ liệu của phòng lân cận trước khi tính toán mô phỏng. Thứ nhất, nó mang lại sự gia tăng đáng kể về năng suất. Thứ hai, nó có thể loại bỏ toàn bộ loại lỗi phát sinh nếu lập trình viên mắc lỗi trong mã dọn dẹp cảnh khi thêm các phần tử trò chơi mới. Những lỗi như vậy rất khó sửa và chúng thường dẫn đến trạng thái của các vật thể trong cảnh của phòng này "chảy" sang phòng khác.

Ngoài ra, chúng tôi đã thực hiện một số nghiên cứu về việc liệu các khung cảnh vật chất có thể được sử dụng để lưu trữ lịch sử của thế giới vật chất hay không. Nghĩa là, có điều kiện, phân bổ không phải một cảnh cho mỗi phòng mà là 30 cảnh và tạo một vùng đệm tuần hoàn từ chúng để lưu trữ câu chuyện. Nhìn chung, giải pháp này hóa ra đang hoạt động, nhưng chúng tôi đã không triển khai nó: nó không cho thấy bất kỳ sự gia tăng năng suất điên cuồng nào mà đòi hỏi những thay đổi khá rủi ro. Thật khó để dự đoán máy chủ sẽ hoạt động như thế nào khi làm việc trong thời gian dài với nhiều cảnh như vậy. Vì vậy, chúng tôi đã tuân theo quy tắc: “Nếu nó không bị hỏng, đừng sửa nó'.

Nguồn: www.habr.com

Thêm một lời nhận xét