Cấu trúc dữ liệu cho nội suy và phân luồng?


20

Gần đây tôi đã xử lý một số vấn đề gây nhiễu tốc độ khung hình với trò chơi của mình và có vẻ như giải pháp tốt nhất sẽ là giải pháp được đề xuất bởi Glenn Fiedler (Gaffer on Games) trong Fix Your Timestep cổ điển ! bài báo.

Bây giờ - Tôi đã sử dụng một bước thời gian cố định cho bản cập nhật của mình. Vấn đề là tôi không thực hiện phép nội suy được đề xuất để kết xuất. Kết quả cuối cùng là tôi nhận được gấp đôi hoặc bỏ qua các khung nếu tốc độ kết xuất của tôi không khớp với tốc độ cập nhật của tôi. Đây có thể là đáng chú ý trực quan.

Vì vậy, tôi muốn thêm phép nội suy vào trò chơi của mình - và tôi muốn biết làm thế nào những người khác đã cấu trúc dữ liệu và mã của họ để hỗ trợ việc này.

Rõ ràng tôi sẽ cần lưu trữ (ở đâu? / Làm thế nào?) Hai bản sao thông tin trạng thái trò chơi có liên quan đến trình kết xuất của tôi, để nó có thể nội suy giữa chúng.

Ngoài ra - đây có vẻ là một nơi tốt để thêm luồng. Tôi tưởng tượng rằng một luồng cập nhật có thể hoạt động trên một bản sao thứ ba của trạng thái trò chơi, để lại hai bản sao khác là chỉ đọc cho luồng kết xuất. (Đây có phải là một ý tưởng tốt?)

Dường như có hai hoặc ba phiên bản của nhà nước của trò chơi có thể giới thiệu hiệu suất và - xa quan trọng hơn - vấn đề độ tin cậy và hiệu quả phát triển, so với việc chỉ là một phiên bản duy nhất. Vì vậy, tôi đặc biệt quan tâm đến các phương pháp giảm thiểu những vấn đề đó.

Đặc biệt lưu ý, tôi nghĩ, là vấn đề làm thế nào để xử lý việc thêm và xóa các đối tượng khỏi trạng thái trò chơi.

Cuối cùng, có vẻ như một số trạng thái không cần trực tiếp để kết xuất hoặc quá khó để theo dõi các phiên bản khác nhau của (ví dụ: công cụ vật lý của bên thứ ba lưu trữ một trạng thái) - vì vậy tôi rất muốn biết làm thế nào mọi người đã xử lý loại dữ liệu đó trong một hệ thống như vậy.

Câu trả lời:


4

Đừng cố gắng sao chép toàn bộ trạng thái trò chơi. Nội suy nó sẽ là một cơn ác mộng. Chỉ cần cô lập các phần có thể thay đổi và cần thiết bằng cách hiển thị (chúng ta hãy gọi đây là "Trạng thái thị giác").

Đối với mỗi lớp đối tượng, tạo một lớp đi kèm để có thể giữ trạng thái Visual Visual của đối tượng. Đối tượng này sẽ được tạo ra bởi mô phỏng và được sử dụng bởi kết xuất. Nội suy sẽ dễ dàng cắm vào giữa. Nếu trạng thái là bất biến và được truyền theo giá trị, bạn sẽ không có vấn đề luồng.

Kết xuất thường không cần biết gì về mối quan hệ logic giữa các đối tượng, do đó cấu trúc được sử dụng để kết xuất sẽ là một vectơ đơn giản hoặc nhiều nhất là một cây đơn giản.

Thí dụ

Thiết kế truyền thống

class Actor
{
  Matrix4x3 position;
  float fuel;
  float armor;
  float stamina;
  float age;

  void Simulate(float deltaT)
  {
    age += deltaT;
    armor -= HitByAWeapon();
  }
}

Sử dụng trạng thái trực quan

class IVisualState
{
  public:
  virtual void Interpolate(const IVisualState &newVS, float f) {}
};
class Actor
{
  struct VisualState: public IVisualState
  {
    Matrix4x3 position;
    float fuel;
    float armor;
    float stamina;
    float age;

    virtual auto_ptr<IVisualState> Interpolate(const IVisualState &newVS, float f)
    {
      const VisualState &newState = static_cast<const VisualState &>(newVS);
      IVisualState *ret = new VisualState;
      ret->age = lerp(this->age,newState.age);
      // ... interpolate other properties as well, using any suitable interpolation method
      // liner, spline, slerp, whatever works best for the given property
      return ret;
    };
  };

  auto_ptr<VisualState> state_;

  void Simulate(float deltaT)
  {
    state_->age += deltaT;
    state_->armor -= HitByAWeapon();
  }
}

1
Ví dụ của bạn sẽ dễ đọc hơn nếu bạn không sử dụng "new" (một từ dành riêng trong C ++) làm tên tham số.
Steve S

3

Giải pháp của tôi ít thanh lịch / phức tạp hơn hầu hết. Tôi đang sử dụng Box2D làm công cụ vật lý của mình nên việc giữ nhiều hơn một bản sao trạng thái hệ thống là không thể quản lý được (sao chép hệ thống vật lý sau đó cố gắng giữ chúng đồng bộ, có thể có một cách tốt hơn nhưng tôi không thể nghĩ ra một).

Thay vào đó tôi giữ một bộ đếm của thế hệ vật lý . Mỗi bản cập nhật tăng thế hệ vật lý, khi hệ thống vật lý cập nhật gấp đôi, bộ đếm thế hệ cũng cập nhật gấp đôi.

Hệ thống kết xuất theo dõi thế hệ kết xuất cuối cùng và delta kể từ thế hệ đó. Khi kết xuất các đối tượng muốn nội suy vị trí của chúng có thể sử dụng các giá trị này cùng với vị trí và vận tốc của chúng để đoán vị trí của đối tượng sẽ được hiển thị.

Tôi đã không giải quyết phải làm gì nếu động cơ vật lý quá nhanh. Tôi gần như lập luận rằng bạn không nên nội suy để di chuyển nhanh. Nếu bạn đã làm cả hai, bạn cần phải cẩn thận để không làm cho các sprite nhảy xung quanh bằng cách đoán quá chậm sau đó đoán quá nhanh.

Khi tôi viết nội dung nội suy, tôi đang chạy đồ họa ở tần số 60Hz và vật lý ở 30Hz. Hóa ra Box2D ổn định hơn nhiều khi chạy ở tần số 120Hz. Do đó, mã nội suy của tôi được sử dụng rất ít. Bằng cách nhân đôi mục tiêu, tốc độ khung hình vật lý trung bình cập nhật hai lần trên mỗi khung. Với jitter có thể là 1 hoặc 3 lần, nhưng hầu như không bao giờ 0 hoặc 4+. Tỷ lệ vật lý cao hơn sẽ tự khắc phục vấn đề nội suy. Khi chạy cả vật lý và tốc độ khung hình ở 60hz, bạn có thể nhận được 0-2 cập nhật trên mỗi khung. Sự khác biệt trực quan giữa 0 và 2 là rất lớn so với 1 và 3.


3
Tôi cũng đã tìm thấy điều này. Một vòng lặp vật lý 120Hz với cập nhật khung hình gần 60Hz làm cho phép nội suy gần như không có giá trị. Thật không may, điều này chỉ hoạt động cho bộ trò chơi có thể đủ vòng lặp vật lý 120Hz.

Tôi vừa thử chuyển sang vòng cập nhật 120Hz. Điều này dường như có lợi ích kép khi làm cho vật lý của tôi ổn định hơn và làm cho trò chơi của tôi trông mượt mà ở tốc độ khung hình không quá 60Hz. Nhược điểm là nó phá vỡ tất cả các vật lý chơi trò chơi được điều chỉnh cẩn thận của tôi - vì vậy đây chắc chắn là một lựa chọn cần được lựa chọn sớm trong một dự án.
Andrew Russell

Ngoài ra: Tôi không thực sự hiểu lời giải thích của bạn về hệ thống nội suy của bạn. Nghe có vẻ giống như ngoại suy, thực sự?
Andrew Russell

Cuộc gọi tốt Tôi thực sự đã mô tả một hệ thống ngoại suy. Với vị trí, vận tốc và thời gian kể từ lần cập nhật vật lý cuối cùng, tôi ngoại suy vị trí của vật thể nếu động cơ vật lý không bị đình trệ.
deft_code

2

Tôi đã nghe cách tiếp cận dấu thời gian này được đề xuất khá thường xuyên, nhưng trong 10 năm trong các trò chơi, tôi chưa bao giờ làm việc trong một dự án trong thế giới thực dựa trên dấu thời gian và nội suy cố định.

Nhìn chung có vẻ nhiều nỗ lực hơn một hệ thống dấu thời gian thay đổi (giả sử một phạm vi khung hình hợp lý, trong phạm vi 25Hz-100Hz).

Tôi đã thử cách tiếp cận nội suy cố định + dấu thời gian cố định một lần cho một nguyên mẫu rất nhỏ - không có luồng, nhưng cập nhật logic dấu thời gian cố định và hiển thị nhanh nhất có thể khi không cập nhật. Cách tiếp cận của tôi là có một vài lớp như CInterpiatedVector và CInterpiatedMatrix - lưu trữ các giá trị trước đó / hiện tại và có một trình truy cập được sử dụng từ mã kết xuất, để lấy giá trị cho thời gian kết xuất hiện tại (luôn luôn ở giữa trước đó và thời điểm hiện tại)

Mỗi đối tượng trò chơi, vào cuối bản cập nhật của nó, sẽ đặt trạng thái hiện tại của nó thành một tập hợp các vectơ / ma trận nội suy này. Loại điều này có thể được mở rộng để hỗ trợ luồng, bạn cần ít nhất 3 bộ giá trị - một bộ đang được cập nhật và ít nhất 2 giá trị trước đó để nội suy giữa ...

Lưu ý rằng một số giá trị không thể được nội suy một cách tầm thường (ví dụ: 'khung hoạt hình sprite', 'hoạt động hiệu ứng đặc biệt'). Bạn có thể bỏ qua nội suy hoàn toàn hoặc nó có thể gây ra sự cố, tùy thuộc vào nhu cầu trò chơi của bạn.

IMHO, tốt nhất là chỉ nên đi dấu thời gian thay đổi - trừ khi bạn tạo RTS hoặc trò chơi khác nơi bạn có số lượng đối tượng lớn và phải đồng bộ 2 mô phỏng độc lập cho các trò chơi mạng (chỉ gửi đơn đặt hàng / lệnh qua mạng, thay vì vị trí đối tượng). Trong tình huống đó, dấu thời gian cố định là lựa chọn duy nhất.


1
Có vẻ như ít nhất Quake 3 đã sử dụng phương pháp này, với "tick" mặc định là 20 khung hình / giây (50 ms).
Suma

Hấp dẫn. Tôi cho rằng nó có lợi thế cho các trò chơi PC nhiều người chơi có tính cạnh tranh cao, để đảm bảo rằng PC nhanh hơn / tốc độ khung hình cao hơn không có quá nhiều lợi thế (điều khiển nhạy hơn hoặc khác biệt nhỏ nhưng có thể khai thác trong hành vi va chạm / vật lý) ?
bluescrn

1
Bạn có trong 10 năm không chạy vào bất kỳ trò chơi nào chạy vật lý không theo bước với trình mô phỏng và kết xuất không? Bởi vì thời điểm bạn làm điều đó, bạn sẽ phải nội suy hoặc chấp nhận sự giật mình trong hoạt hình của mình.
Kaj

2

Rõ ràng tôi sẽ cần lưu trữ (ở đâu? / Làm thế nào?) Hai bản sao thông tin trạng thái trò chơi có liên quan đến trình kết xuất của tôi, để nó có thể nội suy giữa chúng.

Vâng, rất may là chìa khóa ở đây là "có liên quan đến trình kết xuất của tôi". Điều này có thể không hơn gì việc thêm một vị trí cũ và dấu thời gian cho vị trí đó vào hỗn hợp. Đưa ra 2 vị trí, bạn có thể nội suy đến một vị trí giữa chúng và nếu bạn có hệ thống hoạt hình 3D, bạn thường chỉ có thể yêu cầu tư thế tại thời điểm chính xác đó.

Thực sự khá đơn giản - hãy tưởng tượng trình kết xuất của bạn phải có khả năng kết xuất đối tượng trò chơi của bạn. Nó được sử dụng để hỏi đối tượng trông như thế nào, nhưng bây giờ nó phải hỏi nó trông như thế nào tại một thời điểm nhất định. Bạn chỉ cần lưu trữ bất cứ thông tin nào là cần thiết để trả lời câu hỏi đó.

Ngoài ra - đây có vẻ là một nơi tốt để thêm luồng. Tôi tưởng tượng rằng một luồng cập nhật có thể hoạt động trên một bản sao thứ ba của trạng thái trò chơi, để lại hai bản sao khác là chỉ đọc cho luồng kết xuất. (Đây có phải là một ý tưởng tốt?)

Nó chỉ giống như một công thức để thêm đau vào thời điểm này. Tôi đã không nghĩ qua toàn bộ hàm ý nhưng tôi đoán bạn có thể đạt được một chút thông lượng bổ sung với chi phí có độ trễ cao hơn. Ồ, và bạn có thể nhận được một số lợi ích từ việc có thể sử dụng lõi khác, nhưng tôi không biết.


1

Lưu ý Tôi không thực sự xem xét nội suy nên câu trả lời này không đề cập đến nó; Tôi chỉ quan tâm đến việc có một bản sao trạng thái trò chơi cho luồng kết xuất và một bản khác cho luồng cập nhật. Vì vậy, tôi không thể nhận xét về vấn đề nội suy, mặc dù bạn có thể sửa đổi giải pháp sau đây để nội suy.

Tôi đã tự hỏi về điều này khi tôi đang thiết kế và suy nghĩ về một công cụ đa luồng. Vì vậy, tôi đã hỏi một câu hỏi về Stack Overflow, về cách triển khai một số kiểu thiết kế "ghi nhật ký" hoặc "giao dịch" . Tôi nhận được một số phản hồi tốt, và câu trả lời được chấp nhận thực sự khiến tôi suy nghĩ.

Thật khó để tạo ra một vật thể bất biến, vì tất cả những đứa trẻ của nó cũng phải bất biến, và bạn cần phải thực sự cẩn thận rằng mọi thứ thực sự là bất biến. Nhưng nếu bạn thực sự cẩn thận, bạn có thể tạo một siêu lớp GameStatechứa tất cả dữ liệu (và subata, v.v.) trong trò chơi của bạn; phần "Model" của kiểu tổ chức Model-View-Controller.

Sau đó, như Jeffrey nói , các phiên bản của đối tượng GameState của bạn rất nhanh, hiệu quả về bộ nhớ và luồng an toàn. Nhược điểm lớn là để thay đổi bất cứ điều gì về mô hình, bạn cần phải tạo lại mô hình, vì vậy bạn cần thực sự cẩn thận rằng mã của bạn không biến thành một mớ hỗn độn lớn. Việc đặt một biến trong đối tượng GameState thành một giá trị mới có liên quan nhiều hơn chỉ là var = val;về mặt dòng mã.

Tôi cực kỳ bị cuốn hút bởi nó. Bạn không cần phải sao chép toàn bộ cấu trúc dữ liệu của mình vào mỗi khung; bạn chỉ cần sao chép một con trỏ vào cấu trúc bất biến. Điều đó tự nó rất ấn tượng, bạn có đồng ý không?


Đó là một cấu trúc thú vị thực sự. Tuy nhiên tôi không chắc nó sẽ hoạt động tốt cho một trò chơi - vì trường hợp chung là một cây đối tượng khá phẳng mà mỗi đối tượng thay đổi chính xác một lần trên mỗi khung. Cũng bởi vì cấp phát bộ nhớ động là một không lớn.
Andrew Russell

Phân bổ động trong trường hợp như thế này rất dễ thực hiện một cách hiệu quả. Bạn có thể sử dụng bộ đệm tròn, phát triển từ một phía, di chuyển từ bên thứ hai.
Suma

... đó sẽ không phải là phân bổ động, chỉ là sử dụng động bộ nhớ được sắp xếp trước;)
Kaj

1

Tôi bắt đầu bằng cách có ba bản sao trạng thái trò chơi của mỗi nút trong biểu đồ cảnh của tôi. Một đang được ghi bởi luồng của biểu đồ cảnh, một đang được trình kết xuất đọc và một phần ba có sẵn để đọc / ghi ngay khi một trong những nhu cầu đó được trao đổi. Điều này làm việc tốt, nhưng đã quá phức tạp.

Sau đó tôi nhận ra rằng tôi chỉ cần giữ ba trạng thái của những gì sẽ được hiển thị. Chủ đề cập nhật của tôi hiện lấp đầy một trong ba bộ đệm "RenderCommands" nhỏ hơn nhiều và Trình kết xuất đọc từ bộ đệm mới nhất hiện không được ghi vào, điều này ngăn các luồng không bao giờ chờ đợi nhau.

Trong thiết lập của tôi, mỗi RenderCommand có hình học / vật liệu 3d, ma trận biến đổi và danh sách các đèn ảnh hưởng đến nó (vẫn thực hiện kết xuất chuyển tiếp).

Chủ đề kết xuất của tôi không còn phải thực hiện bất kỳ tính toán loại bỏ hoặc khoảng cách ánh sáng nào nữa, và điều này đã tăng tốc mọi thứ lên đáng kể trên các cảnh lớn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.