Làm thế nào tôi có thể duy trì khả năng tương thích ngược trò chơi đã lưu?


8

Tôi có một trò chơi sim phức tạp tôi muốn thêm chức năng lưu trò chơi vào. Tôi sẽ cập nhật nó với các tính năng mới liên tục sau khi phát hành.

Làm cách nào để đảm bảo các cập nhật của tôi không phá vỡ các trò chơi lưu hiện có? Tôi nên làm theo kiểu kiến ​​trúc nào để biến điều này thành có thể?


Tôi không biết về kiến ​​trúc chung cho mục tiêu này nhưng tôi sẽ thực hiện quy trình vá cũng cập nhật / chuyển đổi trò chơi lưu để đảm bảo khả năng tương thích với các tính năng mới.
loodakrawa

Câu trả lời:


9

Một cách tiếp cận dễ dàng là giữ các chức năng tải cũ xung quanh. Bạn chỉ cần một chức năng lưu duy nhất viết ra phiên bản mới nhất. Hàm tải phát hiện chức năng tải được phiên bản chính xác để gọi (thường bằng cách viết ra một số phiên bản ở đâu đó vào đầu định dạng tệp lưu của bạn). Cái gì đó như:

class GameState:
  loadV1(stream):
    // do stuff

  loadV2(stream):
    // do different stuff

  loadV3(stream):
    // yet other stuff

  save(stream):
    // note this is version 3
    stream.write(3)
    // write V3 data

  load(stream):
    version = stream.read()
    if version == 1: loadV1(stream)
    else if version == 2: loadV2(stream)
    else if version == 3: loadV3(stream)

Bạn có thể làm điều này cho toàn bộ tệp, cho các phần riêng lẻ của tệp, cho các đối tượng / thành phần trò chơi riêng lẻ, v.v ... Chính xác thì sự phân chia nào là tốt nhất sẽ phụ thuộc vào trò chơi của bạn và lượng trạng thái bạn đang tuần tự hóa.

Lưu ý rằng điều này chỉ đưa bạn đến nay. Tại một số điểm, bạn có thể thay đổi trò chơi của mình đủ để việc lưu dữ liệu từ các phiên bản trước chỉ đơn giản là vô nghĩa. Chẳng hạn, một game nhập vai có thể có các lớp nhân vật khác nhau mà người chơi có thể chọn. Nếu bạn loại bỏ một lớp nhân vật thì không có nhiều thứ bạn có thể làm với việc lưu các ký tự có lớp đó. Có lẽ bạn có thể chuyển đổi nó thành một lớp tương tự vẫn còn tồn tại ... có thể. Tương tự như vậy nếu bạn thay đổi các phần khác của trò chơi đủ để nó không giống với các phiên bản cũ.

Hãy lưu ý rằng một khi bạn gửi trò chơi của mình, nó đã "xong". Bạn có thể phát hành DLC hoặc các bản cập nhật khác theo thời gian nhưng chúng sẽ không phải là những thay đổi đặc biệt lớn đối với chính trò chơi. Lấy hầu hết các MMO chẳng hạn: WoW đã được duy trì trong nhiều năm với các bản cập nhật và thay đổi mới nhưng nó vẫn ít nhiều giống với trò chơi khi nó ra mắt.

Để phát triển sớm tôi chỉ đơn giản là không lo lắng về nó. Tiết kiệm là phù du trong thử nghiệm sớm. Tuy nhiên, đó là một câu chuyện khác khi bạn đến bản beta công khai.


1
Điều này. Thật không may, điều này hiếm khi hoạt động đẹp như quảng cáo. Thông thường các chức năng tải đó phụ thuộc vào các chức năng của trình trợ giúp ( ReadCharactercó thể gọi ReadStat, có thể thay đổi hoặc không thay đổi từ phiên bản này sang phiên bản tiếp theo), vì vậy bạn cần giữ các phiên bản cho từng phiên bản đó, khiến việc theo kịp khó khăn hơn và khó hơn. Như mọi khi, không có viên đạn bạc, và giữ các chức năng tải cũ là điểm khởi đầu tốt đẹp.
Panda Pajama

5

Một cách đơn giản để đạt được một ngữ nghĩa của phiên bản là hiểu ý nghĩa của các thành viên của các đối tượng bạn đang tuần tự hóa. Nếu mã của bạn có sự hiểu biết về các loại dữ liệu khác nhau sẽ được tuần tự hóa, bạn có thể có được sự mạnh mẽ mà không phải làm quá nhiều việc.

Giả sử chúng ta có một đối tượng nối tiếp trông như thế này:

ObjectType
{
  m_name = "a string"
  m_size = { 1.2, 2.1 }
  m_someStruct = {
    m_deeperInteger = 5
    m_radians = 3.14
  }
}

Nó sẽ được dễ dàng nhận thấy rằng các loại ObjectTypecó thành viên dữ liệu được gọi m_name, m_sizem_someStruct. Nếu bạn có thể lặp lại hoặc liệt kê các thành viên dữ liệu trong thời gian chạy (bằng cách nào đó) thì khi đọc tệp này, bạn có thể đọc tên thành viên và ghép nó với một thành viên thực tế trong thể hiện đối tượng của bạn.

Trong giai đoạn tra cứu này nếu bạn không tìm thấy thành viên dữ liệu phù hợp, bạn có thể bỏ qua phần lưu tệp này một cách an toàn. Ví dụ, phiên bản 1.0 của SomeStructđã có m_namethành viên dữ liệu. Sau đó, bạn vá và thành viên dữ liệu này đã được gỡ bỏ hoàn toàn. Khi tải tập tin lưu của bạn, bạn sẽ đi qua m_namevà tìm kiếm một thành viên phù hợp và không tìm thấy kết quả phù hợp. Mã của bạn có thể chỉ cần chuyển sang thành viên tiếp theo trong tệp mà không gặp sự cố. Điều này cho phép bạn xóa thành viên dữ liệu mà không phải lo lắng về việc phá vỡ các tệp lưu cũ.

Tương tự như vậy nếu bạn thêm một loại thành viên dữ liệu mới và thử tải từ tệp lưu cũ, mã của bạn có thể không khởi tạo thành viên mới. Điều này có thể được sử dụng cho một lợi thế: các thành viên dữ liệu mới có thể chèn vào các tệp lưu trong khi vá thủ công, có thể bằng cách đưa ra các giá trị mặc định (hoặc bằng các phương tiện thông minh hơn).

Định dạng này cũng cho phép các tệp lưu dễ dàng được thao tác hoặc sửa đổi bằng tay; thứ tự mà các thành viên dữ liệu không thực sự có liên quan đến tính hợp lệ của thói quen tuần tự hóa. Mỗi thành viên được tra cứu và khởi tạo độc lập. Đây có thể là một sự tốt đẹp thêm một chút mạnh mẽ.

Tất cả điều này có thể đạt được thông qua một số hình thức hướng nội. Bạn sẽ muốn có thể truy vấn một thành viên dữ liệu bằng cách tra cứu chuỗi và có thể cho biết loại dữ liệu thực sự của thành viên dữ liệu đó là gì. Điều này có thể đạt được trong C ++ bằng cách sử dụng một hình thức hướng nội tùy chỉnh và các ngôn ngữ khác có thể có các phương tiện hướng nội được tích hợp sẵn.


Điều này sẽ hữu ích để làm cho dữ liệu và các lớp mạnh mẽ hơn. (Trong .NET, tính năng này được gọi là "phản chiếu"). Tôi tự hỏi về các bộ sưu tập ... AI của tôi rất phức tạp và sử dụng nhiều bộ sưu tập tạm thời để xử lý dữ liệu. Tôi có nên cố gắng tránh cứu họ ...? Có lẽ hạn chế tiết kiệm đến "điểm an toàn" khi quá trình xử lý kết thúc.
Bánh mì

@aman Nếu bạn lưu một bộ sưu tập thì bạn có thể viết dữ liệu thực tế trong các bộ sưu tập này như trong ví dụ ban đầu của tôi, ngoại trừ ở "định dạng mảng", như trong nhiều liên tiếp. Bạn vẫn có thể áp dụng cùng một ý tưởng cho từng thành phần riêng lẻ của một mảng hoặc bất kỳ vùng chứa nào khác. Bạn sẽ chỉ phải viết một số "serial serializer" chung, "list serializer", v.v ... Nếu bạn muốn một "serializer container" chung chung, bạn có thể sẽ cần một bản tóm tắt SerializingIteratorcủa một loại nào đó, và trình lặp này sẽ được triển khai cho từng loại container.
RandyGaul

1
Ồ và vâng, bạn nên cố gắng tránh lưu các bộ sưu tập phức tạp bằng con trỏ càng nhiều càng tốt. Thường thì điều này có thể tránh được với rất nhiều suy nghĩ và thiết kế thông minh. Tuần tự hóa là một cái gì đó có thể trở nên rất phức tạp, vì vậy nó sẽ được đền đáp để cố gắng đơn giản hóa nó càng nhiều càng tốt. @aman
RandyGaul

Ngoài ra còn có vấn đề khử lưu huỳnh một đối tượng khi lớp đã thay đổi ... Tôi nghĩ rằng trình giải nén .NET sẽ bị sập trong nhiều trường hợp.
Bánh mì

2

Đây là một vấn đề không chỉ tồn tại trên các trò chơi, mà còn trên bất kỳ ứng dụng trao đổi tập tin nào. Chắc chắn, không có giải pháp hoàn hảo nào và việc cố gắng tạo một định dạng tệp sẽ phù hợp với bất kỳ loại thay đổi nào là không thể, vì vậy có lẽ nên chuẩn bị cho loại thay đổi mà bạn có thể mong đợi.

Hầu hết thời gian, có lẽ bạn sẽ chỉ thêm / xóa các trường và giá trị, trong khi vẫn giữ nguyên cấu trúc chung của các tệp của bạn. Trong trường hợp đó, bạn có thể chỉ cần viết mã của mình để bỏ qua các trường không xác định và sử dụng các giá trị mặc định hợp lý khi không thể hiểu / phân tích giá trị. Thực hiện điều này khá đơn giản, và tôi làm nó rất nhiều.

Tuy nhiên, đôi khi bạn sẽ muốn thay đổi cấu trúc của tệp. Nói từ văn bản dựa trên nhị phân; hoặc từ các trường cố định đến kích thước-giá trị. Trong trường hợp như vậy, rất có thể bạn sẽ muốn đóng băng nguồn của trình đọc tệp cũ và tạo một nguồn mới cho loại tệp mới, như trong giải pháp của Sean. Hãy chắc chắn rằng bạn cách ly toàn bộ trình đọc cũ, hoặc cuối cùng bạn có thể sửa đổi một cái gì đó ảnh hưởng đến nó. Tôi khuyên bạn chỉ điều này cho những thay đổi cấu trúc tập tin lớn.

Hai phương pháp này sẽ hoạt động trong hầu hết các trường hợp, nhưng hãy nhớ rằng chúng không phải là những thay đổi khả dĩ duy nhất bạn có thể gặp phải. Tôi đã gặp trường hợp phải thay đổi toàn bộ mã tải cấp từ đọc toàn bộ sang phát trực tuyến (đối với phiên bản di động của trò chơi, hoạt động trên các thiết bị có băng thông và bộ nhớ giảm đáng kể). Một thay đổi như thế này sâu sắc hơn nhiều và rất có thể sẽ yêu cầu thay đổi ở nhiều phần khác của trò chơi, một số trong đó yêu cầu thay đổi cấu trúc của chính tệp.


0

Ở cấp độ cao hơn: nếu bạn thêm các tính năng mới vào trò chơi, hãy có chức năng "Đoán giá trị mới" có thể lấy các tính năng cũ và đoán xem giá trị của những tính năng mới sẽ là gì.

Một ví dụ có thể làm cho điều này rõ ràng hơn. Giả sử một thành phố mô hình hóa trò chơi và phiên bản 1.0 đó theo dõi mức độ phát triển chung của các thành phố, trong khi phiên bản 1.1 thêm các tòa nhà cụ thể giống như Civilization. (Cá nhân, tôi thích theo dõi sự phát triển tổng thể, vì ít thực tế hơn, nhưng tôi lạc đề.) GuessNewValues ​​() cho 1.1, đưa ra một tệp lưu trữ 1.0, sẽ bắt đầu bằng một con số phát triển cấp độ cũ, và đoán, dựa trên điều đó, cái gì các tòa nhà sẽ được xây dựng trong thành phố - có lẽ nhìn vào văn hóa của thành phố, vị trí địa lý của nó, trọng tâm của sự phát triển của nó, kiểu đó.

Tôi hy vọng rằng điều này có thể hiểu được nói chung - rằng nếu bạn đang thêm các tính năng mới vào trò chơi, tải một tệp lưu trữ chưa có các tính năng đó đòi hỏi phải dự đoán tốt nhất về dữ liệu mới sẽ là gì và kết hợp các tính năng đó với nhau với dữ liệu bạn đã tải.

Đối với khía cạnh cấp thấp, tôi tán thành câu trả lời của Sean Middleditch (mà tôi đã bình chọn): giữ logic tải hiện có, thậm chí có thể giữ các phiên bản cũ của các lớp có liên quan và gọi trước đó, sau đó gọi bộ chuyển đổi.


0

Tôi khuyên bạn nên sử dụng một cái gì đó như XML (nếu bạn lưu các tệp rất nhỏ) theo cách đó bạn chỉ cần 1 hàm để xử lý đánh dấu cho dù bạn có đặt gì vào đó. Nút gốc của tài liệu đó có thể khai báo phiên bản đã lưu trò chơi và cho phép bạn viết mã để cập nhật tệp lên phiên bản mới nhất nếu cần.

<save version="1">
  <player name="foo" score="10" />
  <data>![CDATA[lksdf9owelkjlkdfjdfgdfg]]</data>
</save>

Điều này cũng có nghĩa là bạn có thể áp dụng một biến đổi nếu bạn muốn chuyển đổi dữ liệu thành "định dạng phiên bản hiện tại" trước khi tải dữ liệu để thay vì có nhiều chức năng được phiên bản nằm xung quanh bạn chỉ cần có một tập tin xsl mà bạn chọn để thực hiện việc chuyển đổi. Điều này có thể tốn thời gian mặc dù nếu bạn không quen với xsl.

Nếu các tệp lưu của bạn có dung lượng lớn xml có thể là một vấn đề, thông thường tôi đã lưu các tệp hoạt động rất tốt khi bạn chỉ cần đổ các cặp giá trị khóa vào tệp như thế này ...

version=1
player=foo
data=lksdf9owelkjlkdfjdfgdfg
score=10

Sau đó, khi bạn đọc từ tệp này, bạn luôn viết và đọc một biến theo cùng một cách, nếu bạn cần một biến mới, bạn tạo một hàm mới để viết và đọc nó. bạn chỉ có thể viết một hàm cho các loại biến để bạn có "trình đọc chuỗi" và "trình đọc int", điều này sẽ chỉ sai nếu bạn thay đổi một loại biến giữa các phiên bản nhưng bạn không bao giờ nên làm điều đó bởi vì biến đó có nghĩa khác điểm này vì vậy bạn nên tạo một biến mới thay vì một tên khác.

Tất nhiên, cách khác là sử dụng định dạng loại cơ sở dữ liệu hoặc một cái gì đó giống như tệp csv, nhưng nó phụ thuộc vào dữ liệu bạn đang lưu.

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.