Thiết kế một trò chơi theo lượt trong đó các hành động có tác dụng phụ


19

Tôi đang viết một phiên bản máy tính của trò chơi Dominion . Đây là một trò chơi bài theo lượt, trong đó thẻ hành động, thẻ kho báu và thẻ điểm chiến thắng được tích lũy vào bộ bài cá nhân của người chơi. Tôi có cấu trúc lớp phát triển khá tốt, và tôi đang bắt đầu thiết kế logic trò chơi. Tôi đang sử dụng python và tôi có thể thêm GUI đơn giản với pygame sau.

Trình tự lần lượt của người chơi được điều chỉnh bởi một bộ máy trạng thái rất đơn giản. Xoay qua chiều kim đồng hồ và người chơi không thể thoát khỏi trò chơi trước khi kết thúc. Chơi một lượt cũng là một máy trạng thái; nói chung, người chơi trải qua "giai đoạn hành động", "giai đoạn mua" và "giai đoạn làm sạch" (theo thứ tự đó). Dựa trên câu trả lời cho câu hỏi Làm thế nào để triển khai công cụ trò chơi theo lượt? , máy trạng thái là một kỹ thuật tiêu chuẩn cho tình huống này.

Vấn đề của tôi là trong giai đoạn hành động của người chơi, cô ấy có thể sử dụng thẻ hành động có tác dụng phụ, cho bản thân hoặc cho một hoặc nhiều người chơi khác. Ví dụ: một thẻ hành động cho phép người chơi thực hiện lượt thứ hai ngay sau khi kết thúc lượt chơi hiện tại. Một thẻ hành động khác khiến tất cả những người chơi khác loại bỏ hai thẻ khỏi tay họ. Tuy nhiên, một thẻ hành động khác không làm được gì cho lượt hiện tại, nhưng cho phép người chơi rút thêm thẻ vào lượt tiếp theo. Để làm cho mọi thứ thậm chí phức tạp hơn, thường có những bản mở rộng mới cho trò chơi có thêm thẻ mới. Dường như với tôi, việc mã hóa cứng kết quả của mọi thẻ hành động vào bộ máy trạng thái của trò chơi sẽ vừa xấu vừa không thể chấp nhận được. Câu trả lời cho Vòng lặp chiến lược theo lượt không đi vào một mức độ chi tiết giải quyết các thiết kế để giải quyết vấn đề này.

Tôi nên sử dụng loại mô hình lập trình nào để bao hàm thực tế rằng mô hình chung cho việc thay phiên có thể được sửa đổi bằng các hành động diễn ra trong lượt? Đối tượng trò chơi có nên theo dõi tác động của mọi thẻ hành động không? Hoặc, nếu các thẻ nên thực hiện các hiệu ứng của riêng chúng (ví dụ: bằng cách triển khai một giao diện), thiết lập nào là bắt buộc để cung cấp cho chúng đủ sức mạnh? Tôi đã nghĩ ra một vài giải pháp cho vấn đề này, nhưng tôi tự hỏi liệu có một cách tiêu chuẩn để giải quyết nó. Cụ thể, tôi muốn biết đối tượng / lớp / bất cứ điều gì chịu trách nhiệm theo dõi các hành động mà mọi người chơi phải thực hiện do hậu quả của thẻ hành động đang được chơi và cũng liên quan đến những thay đổi tạm thời trong chuỗi thông thường Máy trạng thái lần lượt.


2
Xin chào Apis Utilis, và chào mừng bạn đến với GDSE. Câu hỏi của bạn được viết tốt, và thật tuyệt khi bạn tham khảo các câu hỏi liên quan. Tuy nhiên, câu hỏi của bạn bao gồm rất nhiều vấn đề khác nhau, và để giải quyết đầy đủ, một câu hỏi có lẽ cần phải rất lớn. Bạn vẫn có thể nhận được một câu trả lời tốt, nhưng bản thân bạn và trang web sẽ có lợi nếu bạn phá vỡ vấn đề của mình thêm một số. Có thể bắt đầu với việc xây dựng một trò chơi đơn giản hơn và xây dựng lên Dominion?
michael.bartnett

1
Tôi sẽ bắt đầu từ việc đưa cho mỗi thẻ một kịch bản sửa đổi trạng thái của trò chơi và nếu không có gì lạ xảy ra, hãy quay lại quy tắc bật mặc định ...
Jari Komppa

Câu trả lời:


11

Tôi đồng ý với Jari Komppa rằng việc xác định hiệu ứng thẻ với ngôn ngữ kịch bản mạnh mẽ là cách tốt nhất. Nhưng tôi tin rằng chìa khóa để linh hoạt tối đa là xử lý sự kiện theo kịch bản.

Để cho phép thẻ tương tác với các sự kiện trò chơi sau này, bạn có thể thêm API tập lệnh để thêm "móc kịch bản" vào một số sự kiện nhất định, như bắt đầu và kết thúc các giai đoạn trò chơi hoặc một số hành động nhất định mà người chơi có thể thực hiện. Điều đó có nghĩa là tập lệnh được thực thi khi chơi bài có thể đăng ký một chức năng được gọi là lần tiếp theo đạt đến một giai đoạn cụ thể. Số lượng các chức năng có thể được đăng ký cho mỗi sự kiện nên không giới hạn. Khi có nhiều hơn một, sau đó họ được gọi theo thứ tự đăng ký (trừ khi tất nhiên có một quy tắc trò chơi cốt lõi nói lên điều gì đó khác biệt).

Có thể đăng ký các móc này cho tất cả người chơi hoặc chỉ cho một số người chơi nhất định. Tôi cũng sẽ đề nghị thêm khả năng cho các hook để tự quyết định xem chúng có nên tiếp tục được gọi hay không. Trong các ví dụ này, giá trị trả về của hàm hook (đúng hoặc sai) được sử dụng để thể hiện điều này.

Thẻ quay đôi của bạn sau đó sẽ làm một cái gì đó như thế này:

add_event_hook('cleanup_phase_end', current_player, function {
     setNextPlayer(current_player); // make the player take another turn
     return false; // unregister this hook afterwards
});

(Tôi không biết liệu Dominion thậm chí có thứ gì đó giống như "giai đoạn dọn dẹp" hay không - trong ví dụ này là giai đoạn cuối giả định của lượt người chơi)

Một thẻ cho phép mọi người chơi rút thêm một thẻ vào đầu giai đoạn rút thăm của họ sẽ như thế này:

add_event_hook('draw_phase_begin', NULL, function {
    drawCard(current_player); // draw a card
    return true; // keep doing this until the hook is removed explicitely
});

Một lá bài khiến người chơi mục tiêu bị mất điểm bất cứ khi nào họ chơi bài sẽ như thế này:

add_event_hook('play_card', target_player, function {
    changeHitPoints(target_player, -1); // remove a hit point
    return true; 
});

Bạn sẽ không phải loay hoay với việc mã hóa một số hành động trò chơi như rút thẻ hoặc mất điểm, bởi vì định nghĩa hoàn chỉnh của chúng - chính xác nghĩa là "rút thẻ" - là một phần của cơ chế trò chơi cốt lõi. Ví dụ, tôi biết một số TCG khi bạn phải rút thẻ vì bất kỳ lý do gì và bộ bài của bạn trống, bạn sẽ thua trò chơi. Quy tắc này không được in trên mỗi thẻ khiến bạn rút thẻ, vì nó nằm trong sách quy tắc. Vì vậy, bạn không cần phải kiểm tra tình trạng mất trong kịch bản của mỗi thẻ. Kiểm tra những thứ như thế nên là một phần của drawCard()chức năng được mã hóa cứng (nhân tiện, đây cũng sẽ là một ứng cử viên tốt cho một sự kiện có thể nối được).

Nhân tiện: Không chắc là bạn sẽ có thể lên kế hoạch trước cho mọi phiên bản tương lai cơ học khó hiểu có thể xuất hiện , vì vậy dù bạn có làm gì đi nữa, bạn vẫn sẽ phải thêm chức năng mới cho các phiên bản trong tương lai (trong trường hợp này trường hợp, một confetti ném minigame).


1
Ồ Đó là sự hỗn loạn điều confetti.
Jari Komppa

Câu trả lời tuyệt vời, @Philipp, và điều này quan tâm đến rất nhiều thứ được thực hiện ở Dominion. Tuy nhiên, có những hành động phải xảy ra ngay lập tức khi chơi bài, tức là chơi bài buộc người chơi khác phải lật lá bài trên cùng của thư viện của mình và cho phép người chơi hiện tại nói "Giữ lại" hoặc "Hủy bỏ nó". Bạn sẽ viết các móc sự kiện để xử lý các hành động ngay lập tức như vậy, hoặc bạn sẽ cần đưa ra các phương pháp bổ sung cho kịch bản thẻ?
fnord

2
Khi một cái gì đó cần phải xảy ra ngay lập tức, kịch bản nên gọi trực tiếp các chức năng thích hợp và không đăng ký một chức năng hook.
Phi

@JariKomppa: Bộ Unglued cố tình vô nghĩa và đầy những lá bài điên rồ vô nghĩa. Yêu thích của tôi là một thẻ làm cho tất cả mọi người có một điểm thiệt hại khi họ nói một từ cụ thể. Tôi đã chọn 'cái'.
Jack Aidley

9

Tôi đã đưa ra vấn đề này - công cụ trò chơi thẻ máy tính linh hoạt - một số suy nghĩ một thời gian trước đây.

Trước hết, một trò chơi bài phức tạp như Chez Geek hoặc Fluxx (và, tôi tin rằng, Dominion) sẽ yêu cầu các thẻ phải có kịch bản. Về cơ bản, mỗi thẻ sẽ đi kèm với một loạt các kịch bản riêng có thể thay đổi trạng thái của trò chơi theo nhiều cách khác nhau. Điều này sẽ cho phép bạn cung cấp cho hệ thống một số bằng chứng trong tương lai, vì các tập lệnh có thể có thể thực hiện những điều bạn không thể nghĩ ra ngay bây giờ, nhưng có thể đến trong bản mở rộng trong tương lai.

Thứ hai, "biến" cứng nhắc có thể gây ra vấn đề.

Bạn cần một số loại "lượt xếp" có chứa "lượt đặc biệt", chẳng hạn như "loại bỏ 2 thẻ". Khi ngăn xếp trống, lượt bình thường mặc định tiếp tục.

Trong Fluxx, hoàn toàn có thể một lượt đi giống như:

  • Chọn thẻ N (theo quy định hiện hành, có thể thay đổi qua thẻ)
  • Chơi thẻ N (theo quy định hiện hành, có thể thay đổi qua thẻ)
    • Một trong những thẻ có thể là "lấy 3, chơi 2 trong số chúng"
      • Một trong những thẻ đó có thể là "rẽ khác"
    • Một trong những thẻ có thể là "loại bỏ và rút ra"
  • Nếu bạn thay đổi quy tắc để chọn nhiều thẻ hơn so với khi bạn bắt đầu, hãy chọn nhiều thẻ hơn
  • Nếu bạn thay đổi quy tắc cho ít thẻ hơn trong tay, mọi người khác phải loại bỏ thẻ ngay lập tức
  • Khi lượt của bạn kết thúc, hãy loại bỏ thẻ cho đến khi bạn có thẻ N (có thể thay đổi qua thẻ, một lần nữa), sau đó thực hiện một lượt khác (nếu bạn đã chơi thẻ "rẽ khác" vào lúc lộn xộn ở trên).

..Vân vân và vân vân. Vì vậy, thiết kế một cấu trúc rẽ có thể xử lý lạm dụng ở trên có thể khá khó khăn. Thêm vào đó là vô số trò chơi có thẻ "bất cứ khi nào" (như trong "chez geek") trong đó thẻ "bất cứ khi nào" có thể phá vỡ dòng chảy thông thường bằng cách, ví dụ, hủy bỏ bất kỳ thẻ nào được chơi lần cuối ..

Vì vậy, về cơ bản, tôi sẽ bắt đầu từ việc thiết kế một cấu trúc rẽ rất linh hoạt, thiết kế nó sao cho nó có thể được mô tả như một kịch bản (vì mỗi trò chơi sẽ cần "kịch bản chính" xử lý cấu trúc trò chơi cơ bản). Sau đó, bất kỳ thẻ nên có kịch bản; hầu hết các thẻ có thể không làm bất cứ điều gì lạ, nhưng những người khác làm. Thẻ cũng có thể có nhiều thuộc tính khác nhau - cho dù chúng có thể được giữ trong tay, chơi "bất cứ khi nào", cho dù chúng có thể được lưu trữ dưới dạng tài sản (như fluxx 'người giữ', hoặc nhiều thứ khác trong 'chez geek' như thực phẩm) ...

Tôi chưa bao giờ thực sự bắt đầu thực hiện bất kỳ điều này, vì vậy trong thực tế, bạn có thể tìm thấy rất nhiều thách thức khác. Cách dễ nhất để bắt đầu là bắt đầu với bất cứ điều gì bạn biết về hệ thống bạn muốn triển khai và triển khai chúng theo cách có thể viết được theo kịch bản, đặt càng ít càng tốt, vì vậy khi mở rộng, bạn sẽ không cần phải sửa đổi hệ thống cơ sở - nhiều. =)


Đây là một câu trả lời tuyệt vời, và tôi đã chấp nhận cả hai nếu tôi có thể có. Tôi đã phá vỡ sự ràng buộc bằng cách chấp nhận câu trả lời của người có uy tín thấp hơn :)
Apis Utilis

Không có thăm dò, bây giờ tôi đã quen với nó .. =)
Jari Komppa

0

Hearthstone dường như làm mọi thứ có thể tin được và thành thật mà nói tôi nghĩ cách tốt nhất để đạt được sự linh hoạt là thông qua một công cụ ECS với thiết kế hướng dữ liệu. Đã cố gắng tạo ra một bản sao lò sưởi và điều đó đã được chứng minh là không thể. Tất cả các trường hợp cạnh. Nếu bạn phải đối mặt với nhiều trường hợp cạnh kỳ lạ này thì đó có lẽ là cách tốt nhất để giải quyết nó. Tôi khá thiên vị mặc dù từ kinh nghiệm gần đây đã thử kỹ thuật này.

Chỉnh sửa: ECS thậm chí có thể không cần thiết tùy thuộc vào loại linh hoạt và tối ưu hóa bạn muốn. Đó chỉ là một cách để thực hiện điều này. DOD tôi đã lầm tưởng là lập trình thủ tục mặc dù chúng liên quan rất nhiều. Ý của tôi là. Rằng bạn nên cân nhắc loại bỏ hoàn toàn với OOP hoặc ít nhất là thay vào đó tập trung sự chú ý của bạn vào dữ liệu và cách thức tổ chức. Tránh kế thừa và phương pháp. Thay vào đó tập trung vào các chức năng công cộng (hệ thống) để thao tác dữ liệu thẻ của bạn. Mỗi hành động không phải là một thứ hoặc logic của bất kỳ loại nào mà thay vào đó là dữ liệu thô. Trường hợp hệ thống của bạn sau đó sử dụng nó để thực hiện logic. Trường hợp chuyển đổi số nguyên hoặc sử dụng một số nguyên để truy cập vào một mảng các con trỏ hàm giúp tìm ra logic mong muốn từ dữ liệu đầu vào một cách hiệu quả.

Các quy tắc cơ bản cần tuân thủ là bạn nên tránh buộc logic trực tiếp cùng với dữ liệu, bạn nên tránh làm cho dữ liệu phụ thuộc vào nhau càng nhiều càng tốt (ngoại lệ có thể áp dụng) và khi bạn muốn logic linh hoạt ngoài tầm với ... Xem xét chuyển đổi nó thành dữ liệu.

Có những lợi ích để có được điều này. Mỗi thẻ có thể có một giá trị enum (s) hoặc chuỗi (s) để thể hiện (các) hành động của chúng. Thực tập sinh này cho phép bạn thiết kế thẻ thông qua các tệp văn bản hoặc json và cho phép chương trình tự động nhập chúng. Nếu bạn tạo cho người chơi hành động một danh sách dữ liệu, điều này thậm chí còn linh hoạt hơn, đặc biệt là nếu thẻ phụ thuộc vào logic quá khứ như Hearthstone, hoặc nếu bạn muốn lưu trò chơi hoặc phát lại trò chơi bất cứ lúc nào. Có tiềm năng tạo ra AI dễ dàng hơn. Đặc biệt là khi sử dụng "hệ thống tiện ích" thay vì "cây hành vi". Việc kết nối mạng cũng trở nên dễ dàng hơn vì thay vì cần phải tìm ra cách để có được toàn bộ các đối tượng đa hình có thể truyền qua dây và cách thiết lập tuần tự hóa sau khi thực tế, bạn đã có các đối tượng trò chơi của mình không gì khác hơn là dữ liệu đơn giản mà cuối cùng thực sự dễ dàng di chuyển. Và cuối cùng nhưng không kém phần quan trọng, điều này cho phép bạn tối ưu hóa dễ dàng hơn vì thay vì lãng phí thời gian lo lắng về mã, bạn có thể tổ chức dữ liệu tốt hơn để bộ xử lý có thời gian xử lý dễ dàng hơn. Python có thể có vấn đề ở đây nhưng tìm kiếm "dòng cache" và cách nó liên quan đến nhà phát triển trò chơi. Không quan trọng đối với công cụ tạo mẫu có lẽ nhưng trên đường đi, nó sẽ đến trong thời gian lớn tiện dụng.

Một số liên kết hữu ích.

Lưu ý: ECS cho phép một người tự động thêm / xóa các biến (được gọi là các thành phần) khi chạy. Một ví dụ về chương trình c về cách ECS "có thể" trông như thế nào (có rất nhiều cách để thực hiện).

unsigned int textureID = ECSRegisterComponent("texture", sizeof(struct Texture));
unsigned int positionID = ECSRegisterComponent("position", sizeof(struct Point2DI));
for (unsigned int i = 0; i < 10; i++) {
    void *newEnt = ECSGetNewEntity();
    struct Point2DI pos = { 0 + i * 64, 0 };
    struct Texture tex;
    getTexture("test.png", &tex);
    ECSAddComponentToEntity(newEnt, &pos, positionID);
    ECSAddComponentToEntity(newEnt, &tex, textureID);
}
void *ent = ECSGetParentEntity(textureID, 3);
ECSDestroyEntity(ent);

Tạo ra một loạt các thực thể với dữ liệu kết cấu và vị trí và cuối cùng sẽ phá hủy một thực thể có thành phần kết cấu xảy ra ở chỉ số thứ ba của mảng thành phần kết cấu. Có vẻ kỳ quặc nhưng là một cách để làm việc. Đây là một ví dụ về cách bạn kết xuất mọi thứ có thành phần kết cấu.

unsigned int textureCount;
unsigned int positionID = ECSGetComponentTypeFromName("position");
unsigned int textureID = ECSGetComponentTypeFromName("texture");
struct Texture *textures = ECSGetAllComponentsOfType(textureID, &textureCount);
for (unsigned int i = 0; i < textureCount; i++) {
    void *parentEntity = ECSGetParentEntity(textureID, i);
    struct Point2DI *drawPos = ECSGetComponentFromEntity(positionID, parentEntity);
    if (drawPos) {
        struct Texture *t = &textures[i];
        drawTexture(t, drawPos->x, drawPos->y);
    }
}

1
Câu trả lời này sẽ tốt hơn nếu nó đi sâu vào chi tiết hơn về cách bạn khuyên bạn nên thiết lập ECS hướng dữ liệu của mình và áp dụng nó để giải quyết vấn đề cụ thể này.
DMGregory

Cập nhật cảm ơn bạn đã chỉ ra rằng.
Blue_Pyro

Nói chung, tôi nghĩ thật tệ khi nói với ai đó "cách" thiết lập cách tiếp cận này mà thay vào đó hãy để họ thiết kế giải pháp của riêng họ. Nó chứng tỏ là một cách tốt để thực hành và cho phép một giải pháp tiềm năng tốt hơn cho vấn đề. Khi nghĩ về dữ liệu nhiều hơn logic theo cách này, cuối cùng, có rất nhiều cách để hoàn thành cùng một thứ và tất cả phụ thuộc vào nhu cầu của ứng dụng. Cũng như thời gian / kiến ​​thức lập trình viên.
Blue_Pyro
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.