- Bàn về cách tiếp cận thiết kế cơ chế cốt lõi của game bằng quan hệ logic và vị từ thay vì hàm, lớp hay biến trạng thái
- Lý do Prolog có cảm giác xa lạ hơn LISP không nằm ở cú pháp, mà ở việc nó xử lý cấu trúc dữ liệu và thuật toán xoay quanh quan hệ toán học
- Nếu xem các hàng trong cơ sở dữ liệu quan hệ, đầu vào/đầu ra của mạch số và đối tượng trong game đều là quan hệ n-ngôi, mô hình khai báo của Prolog sẽ kết nối với thiết kế hướng dữ liệu
- Dù khó triển khai mọi thứ bằng Prolog, từ đồ họa, âm thanh, vật lý đến I/O đơn giản, nhưng nó có tiềm năng áp dụng cho cơ chế gameplay
- Phần 1 biểu diễn quan hệ giữa tag và actor dưới dạng vị từ Prolog dựa trên world và actor; sự thay đổi theo thời gian và game loop được để lại cho phần sau
Vấn đề đặt ra khi áp dụng Prolog vào cơ chế game
- LISP và Prolog thuộc các mô hình lập trình khác với những ngôn ngữ hướng đối tượng phổ biến như C# hay Java
- LISP mạnh về lập trình hàm và các pattern mang tính siêu ngôn ngữ, còn Prolog được xem là ngôn ngữ tiêu biểu cho lập trình logic
- Nhờ các khái niệm như biểu thức lambda và hàm bậc cao trong C#, cùng nền tảng kỹ thuật điện và xử lý tín hiệu, LISP dễ tiếp cận hơn dự kiến
- Ngược lại, dù cú pháp Prolog trông đơn giản, cách nó xử lý cấu trúc dữ liệu và thuật toán dưới dạng quan hệ toán học lại là trở ngại lớn
- Cách tiếp cận của Prolog có nhiều điểm giao với các mô hình kỹ thuật khác nhau
- Mỗi hàng trong cơ sở dữ liệu quan hệ có thể được xem là vị từ n-ngôi trong cú pháp Prolog
- Hoạt động đầu vào/đầu ra của các thành phần mạch số có thể được triển khai như một quan hệ n-ngôi gộp các cổng vào và cổng ra
- Các đối tượng như một mảnh phần cứng hoặc dữ liệu thuần trong bộ nhớ cũng có thể được định nghĩa bằng quan hệ trong lập trình logic
- Cũng như trong lập trình hàm có thể nhìn đối tượng như hàm, trong lập trình logic có thể nhìn đối tượng như quan hệ
Cơ chế cốt lõi của game và thiết kế khai báo
- Lập trình logic có thể được dùng để thiết kế và triển khai các hệ thống phức tạp, chẳng hạn như cơ chế gameplay cốt lõi của video game
- Việc triển khai toàn bộ game bằng Prolog là một thách thức lớn với lập trình viên game thông thường, trừ khi nhằm mục đích thử nghiệm
- Không phải mọi thành phần của game đều phù hợp với Prolog
- Mô-đun I/O đơn giản
- Mô-đun đồ họa
- Mô-đun âm thanh
- Mô-đun vật lý
- Đối tượng áp dụng gần với cơ chế cốt lõi hơn là toàn bộ game
- Nếu cấu thành hệ thống gameplay bằng một tập các câu khai báo, hệ thống có thể vững chắc và mô-đun hóa hơn so với hệ thống mệnh lệnh, đồng thời ít bị rối bởi các edge case khó xử lý như race condition
- Theo phương pháp này, hệ thống phải được thiết kế chỉ bằng quan hệ logic và vị từ
- Không có hàm
- Không có struct
- Không có class
- Không có interface
- Không có biến trạng thái
Mô hình World và Actor
- Cốt lõi của lập trình game dựa trên Prolog là dùng quan hệ làm thành phần nguyên thủy nhất, giống như điện trở, transistor, tụ điện và cuộn cảm trong mạch điện
- Game được chia lớn thành world và actor
- world là bối cảnh nơi mọi việc diễn ra
- actor là đối tượng thuộc về world
- actor chỉ các thực thể riêng lẻ có tên và thuộc tính, như player, enemy, obstacle, item
- actor có thể tương tác với nhau hoặc với chính mình, và nhiều sự kiện khác nhau phát sinh trong quá trình này
- Gameplay có thể được xem là chuỗi các sự kiện như vậy
- Trong mô hình này, world chứa nhiều actor, còn mỗi actor có trạng thái và hành vi riêng
Biểu diễn tag bằng vị từ một ngôi
- Mỗi actor được định danh bằng một tên duy nhất
- Trong ví dụ,
actor1 và actor2 biểu thị actor thứ nhất và thứ hai
- Tag là từ khóa gắn vào actor, cho biết actor là gì
bread(actor1). và bread(actor2). có nghĩa là cả actor1 lẫn actor2 đều là bánh mì
bread(actor1).
bread(actor2).
- Trong Prolog,
bread là quan hệ một ngôi, còn bread(actor1) và bread(actor2) là các instance riêng của quan hệ đó
- actor có thể có nhiều tag
- Nếu mọi bánh mì đều phải là
flammable và decomposable, cách gắn tag thủ công cho từng actor sẽ rườm rà và dễ lỗi
- Dùng Horn clause, khi
bread(X) đúng thì flammable(X) và decomposable(X) sẽ tự động được suy ra là đúng
flammable(X) :- bread(X).
decomposable(X) :- bread(X).
- Các Horn clause này hoạt động như dữ liệu thiết lập của game
- Tương tự định nghĩa đặc tính của character type, skill type, mission type trong tài liệu thiết kế kỹ thuật hoặc spreadsheet
- Đây là cấu trúc trong đó type-specifier
bread bao gồm các thuộc tính flammable, decomposable
- Nếu ví với Unity, nó tương tự việc đưa các component
Flammable, Decomposable vào prefab Bread
- Trong môi trường hướng đối tượng, nó giống việc class
Bread triển khai các interface IFlammable, IDecomposable
- Horn clause của Prolog trong ngữ cảnh này đóng vai trò định nghĩa kiểu dữ liệu
Tag tùy chỉnh gắn cho từng actor
- Ngoài các tag được định nghĩa sẵn, có thể gắn tag tùy chỉnh chỉ cho một actor cụ thể
- Trong ví dụ, vì pháp sư đã yểm phép lên
actor2, chiếc bánh mì thứ hai, nên cần tag enchanted
enchanted(actor2).
flammable và decomposable là đặc tính thuộc về mọi bánh mì
enchanted là đặc tính chỉ gắn cho chiếc bánh mì đặc biệt đã bị yểm phép
Biểu diễn quan hệ giữa các Actor bằng vị từ hai ngôi
- Hệ thống gameplay không chỉ cần biểu diễn đặc tính của từng actor mà còn cả quan hệ giữa các actor
- Ví dụ về quan hệ gồm có:
- Trong hệ sinh thái, predator đuổi theo prey, còn prey chạy trốn predator
- Trong dating simulator, nam giới cố flirt với nữ giới, còn nữ giới từ chối
- Trong social simulator như The Sims, con người là bạn, kẻ thù hoặc ở mức nào đó giữa hai trạng thái này với nhau
- Trong cờ vua, bishop ăn rook theo đường chéo, còn rook ăn bishop theo hướng trực giao
- Trong Prolog, giống như vị từ một ngôi biểu thị đặc tính của một actor, vị từ hai ngôi có thể biểu thị quan hệ giữa hai actor
- Dùng Horn clause, có thể suy luận động quan hệ từ tập điều kiện cần thiết
Ví dụ canEat và canSpoil
- Nếu
actor3 là con người và con người có thể ăn bánh mì, ta có thể định nghĩa quan hệ “nếu X là human và Y là bread, thì X có thể ăn Y”
human(actor3).
canEat(X, Y) :- human(X), bread(Y).
- Ở đây,
canEat(X, Y) là quan hệ tồn tại giữa X và Y
- Nếu
actor4 là fungus và bánh mì là decomposable, thì fungus có thể spoil actor decomposable
fungus(actor4).
canSpoil(X, Y) :- fungus(X), decomposable(Y).
canSpoil(X, Y) là quan hệ “nếu X là fungus và Y là decomposable, thì X có thể spoil Y”
decomposable(Y) có thể được suy luận từ bread(Y) bằng Horn clause đã nói ở trên
Vấn đề còn lại: thay đổi theo thời gian và game loop
- Các ví dụ đến đây biểu diễn đặc tính và quan hệ của actor, nhưng tất cả đều tĩnh
- Game không phải một phong cảnh cố định; actor phải di chuyển và tương tác theo thời gian
- Chỉ với tính chất khai báo của Prolog, việc xử lý vấn đề đặc tính thay đổi theo thời gian có vẻ không dễ
- Cách khái niệm hóa game loop trong Prolog sẽ được tiếp tục ở Phần 2
1 bình luận
Ý kiến trên Hacker News
Thường thì tôi đánh giá thấp những bài chỉ có Phần 1 rồi dừng lại, nhưng bài này thực sự có các phần tiếp theo được đăng liên tục
https://thingspool.net/morsels/page-11.html (phần 2)
https://thingspool.net/morsels/page-12.html (phần 3)
https://thingspool.net/morsels/page-13.html (phần 4)
https://thingspool.net/morsels/page-14.html (phần 5)
Và vẫn tiếp tục
Có vẻ có thể mang các kỹ thuật này sang lập trình game bằng Rust
Trong thiết kế game của Breath of the Wild có khái niệm gọi là engine hóa học. Thông thường engine game có engine vật lý để tính toán các vật thể tương tác với nhau về mặt chuyển động như thế nào, còn engine hóa học xử lý cách các vật liệu tương tác với nhau theo nghĩa giả kim
Nó tính toán tương tác giữa các chất liệu khác nhau như một engine dựa trên luật, tạo ra những tương tác đáng ngạc nhiên kiểu như mũi tên có thể bắt lửa vì làm bằng gỗ
Liên kết YouTube: https://www.youtube.com/watch?v=QyMsF31NdNc&t=2354s
Các yếu tố dựa trên luật trong game thường tương đối đơn giản và không cần quá tổng quát, nên có vẻ được code thủ công. Khi hỏi tác giả Baba is You liệu có triển khai engine Datalog không thì câu trả lời cũng là không, và Breath of the Wild có lẽ cũng tương tự
Dù vậy, tôi vẫn thường nghĩ rằng một engine hóa học như thế tốt nhất nên được triển khai bằng ngôn ngữ logic như Prolog hay Datalog để thử nghiệm nhanh. SQL ban đầu cũng được dùng để giữ tính linh hoạt của truy vấn, rồi cuối cùng được triển khai nguyên như vậy; ngày xưa hẳn đã có nhiều phàn nàn rằng truy vấn SQL chậm, nhưng tính linh hoạt đó đủ hữu ích để người ta bỏ vô số thời gian làm cho nó nhanh hơn. Ngày nay gần như không ai nghĩ “mình có thể tự viết mã mệnh lệnh nhanh hơn truy vấn này”
Đây là một trong những game khá thú vị trong số những game tôi từng chơi
https://store.steampowered.com/app/881100/Noita/
https://www.youtube.com/watch?v=prXuyMCgbTc
MUD có lẽ cũng tương tự
Những điều mà các game đồ họa đang cố làm như một sự đổi mới thực ra đã được game chữ làm từ 40 năm trước, đặc biệt là interactive fiction. Điều này còn nổi bật hơn trong thời kỳ phục hưng của các game nghiệp dư xuất sắc thập niên 90 như Curses, Jigsaw, Anchorhead, Devours, Spider and Web, hơn cả các tác phẩm đầu của Infocom; rồi cả các roguelike như NetHack/Slash'EM, CDDA: Bright Nights, và các MUD trực tuyến xử lý những vật liệu kỳ lạ cùng tương tác của chúng trong bối cảnh không chỉ fantasy mà cả cyberpunk hay SF
Những nhà làm game hàng đầu như Warren Spector hẳn cũng đã chơi các game phần mềm tự do, game indie, game kỳ quặc và lấy từ đó các ý tưởng gameplay mới
Tôi cho rằng việc nhắc đến NetHack trong Deus Ex bản gốc không chỉ là một easter egg, mà có lẽ là lời tri ân tới gameplay phát sinh lấy cảm hứng từ NetHack
Điểm mạnh của Deus Ex không nằm ở đồ họa hay thế giới quan, mà ở việc bạn có thể làm gì trong game; nó phá vỡ cấu trúc tuyến tính của FPS nhưng vẫn dễ tiếp cận hơn System Shock 1 và 2. System Shock có những điểm thất bại ở cân bằng độ khó và mức độ hoàn thiện gameplay. Arx Fatalis (nay là Libertatis) và Ultima Underworld cũng tương tự
Từ các luật đơn giản nảy sinh rất nhiều độ phức tạp thật sự thú vị
Không phải chỉ có các thuật toán giải mà ngôn ngữ logic/quan hệ tổng quát triển khai mới khả thi; càng tổng quát thì chúng càng có thể kém hiệu quả với những bài toán mà engine game cần giải một cách hiệu quả. Ngược lại, nếu mở rộng ngôn ngữ logic để hỗ trợ các tính năng đó một cách native, ta có thể phải phá vỡ các giả định đơn giản hóa hoặc thay đổi thứ tự đánh giá, khiến ngôn ngữ khó được triển khai đơn giản và hiệu quả
Ví dụ, tôi muốn dùng cả quan hệ logic lẫn ràng buộc tuyến tính. Một biểu thức như
A <= (B-10.0) .OR. B <= (A-10.0)sẽ ràng buộc vị trí tâm của hai đối tượng game có bề rộng 10.0 để biên của chúng không chồng lên nhau, nhưng không quan tâm bên nào ở phía trước. Bằng các câu như vậy, ta có thể cấu thành cảnh hoặc các đối tượng game phức tạp theo cách định tính, rồi để bộ giải ràng buộc tuyến tính chọn tọa độ chính xácVấn đề là khi giải một ràng buộc tuyến tính như
f(A) <= g(B), nó có thể ảnh hưởng đến mọi biến liên quan. Vì A hoặc B cũng có thể có ràng buộc với C, D, E, F, nên nếu thực thi ngay khi chọn vế trái hay vế phải của công thức logicprop(X) .OR. prop(Y), các mệnh đề khác đã được xác định trước đó có thể trở nên vô hiệuVì vậy, thay vì diễn giải
<=của biến tuyến tính như một phép kiểm tra trả về kết quả boolean, tốt hơn là diễn giải nó như một lệnh thêmf(A) <= g(B)vào kho ràng buộc tuyến tính toàn cục, rồi sau đó chạy bộ giải trên toàn bộ ma trận biến tuyến tínhKhi đó nảy sinh các câu hỏi thiết kế như: làm sao ngôn ngữ biết khi nào việc thêm ràng buộc đã kết thúc và đã đến lúc giải; hay xử lý thế nào với các mệnh đề logic nơi ta thực sự cần kiểm tra giá trị chứ không được thêm ràng buộc. Nhưng điểm cốt lõi là trong bối cảnh engine game, ta cần kiểm soát nhiều hơn việc khi nào các phép tính dài trong cả giải ràng buộc tuyến tính lẫn lập trình logic được chạy, và chúng chạy trong bao lâu
Nói cách khác, ngay cả khi nhìn từ góc độ ngôn ngữ logic, ta thiết kế để tính năng mở rộng hoạt động tốt, thì từ góc độ hệ thống bên ngoài vẫn còn vấn đề là thời gian tìm kiếm để giải một phần chương trình logic có thể đột ngột bùng nổ, và lập trình viên không phải lúc nào cũng dễ dự đoán khi nào và ở đâu điều đó xảy ra. Theo nghĩa này, mã mệnh lệnh viết tay thậm chí không cần nhanh hơn; dù chậm hơn, chỉ cần có tính dự đoán được là đủ
Thực tế cũng không nhất thiết phải tự viết hết mọi thứ. Điểm mấu chốt là engine luật là một bộ giải mà mã game mệnh lệnh có thể điều khiển và ngắt từ bên ngoài, hay toàn bộ game chạy bên trong bộ giải, tức bên trong ngôn ngữ quan hệ. Tưởng tượng một game hoàn toàn quan hệ có thể làm được gì là điều rất hấp dẫn, nhưng để dùng kỹ thuật đó cho toàn bộ game thì cần một bộ giải quan hệ kỳ diệu vốn không thực sự tồn tại
Đây là phần nhập môn lập trình game bằng Prolog, nhưng điểm thú vị là nó đi thẳng vào game hành động. Nó đề cập đến các khía cạnh thời gian thực, timeline, 3D, ECS và sự kiện
Thông thường, sách nhập môn phát triển game bằng Prolog bắt đầu từ game phiêu lưu, đặc biệt là text adventure cổ điển. Lý do là mê cung và các câu đố inventory khớp trực tiếp với fact và rule của Prolog, cũng như DSL
Game bài hay board game cũng có thể biểu diễn luật bằng Prolog rất tiện, và gần như dễ dàng mở rộng thành đối thủ game tổng quát kiểu tổ hợp cơ bản, tương tự các planner Prolog dùng trong robot học, logistics, tài chính, công nghiệp, v.v.
Khi học môn AI, thứ đầu tiên tôi được học là Prolog, và bài tập là tất cả phải viết một game kiểu adventure/colossal cave. Prolog rất hợp với việc đó, và sự đa dạng của những game đơn giản được tạo ra trong lớp thật đáng ngạc nhiên
Tôi từng nghĩ giá mà mình giữ lại toàn bộ game do các sinh viên khác làm. Sau khi học vài tuần, lớp chuyển sang các chủ đề khác như CLIPS và Lisp
Trong bài tập của tôi, tôi làm Bureaucratic Maze [1], và việc này cũng khá trực quan trong Prolog
[1]. http://logicmazes.com/bureau/index.htm
Nhưng trong Inform6, nhờ hướng đối tượng được áp dụng vào thiết kế game và cách xử lý trực tiếp quan hệ giữa các đối tượng trong game như ZMachine, độ khó giảm hẳn xuống, gần như trở nên tầm thường
http://logicmazes.com/alice.html
alice.html:353 Uncaught TypeError: Cannot read properties of undefined (reading 'play')at playSound (alice.html:353:30)at finalize2 (alice.html:347:1)at :1:1Tôi đã lướt qua 12 bài viết; chúng có vẻ tốt để nhập môn sử dụng Prolog, nhưng vẫn có vẻ hơi gượng nếu nói rằng nó liên quan đến lập trình game
Có thể về sau sẽ phát triển thêm, nhưng đến bài 12 thì phần lớn nội dung là cố gắng mô hình hóa một vài khái niệm hướng đối tượng trong Prolog
Không biết tôi có bỏ sót gì không, nhưng phần tương tác người dùng, thứ có vẻ là tiền đề của game, vẫn chưa được đề cập. Có một đoạn ngắn nói về gửi thông điệp, nên có thể đó là điều họ định hướng tới
Trong bối cảnh tương tự, tôi đã đọc khá thích một cuốn sách về lập trình game theo kiểu lập trình logic dùng CLIPS thay vì Prolog
Adventures in Rule-Based Programming: A CLIPS Tutorial https://a.co/d/7wVOcZp
https://www.clipsrules.net/
Sách lập trình trên Kindle thường rất tệ, nhưng xem trên máy tính bảng Fire thì có thể ổn
Có một sự giao thoa thú vị giữa góc nhìn coi trạng thái game là tập hợp các fact và quan hệ (Prolog) với cốt lõi của ECS là “đây là một cơ sở dữ liệu”[1]
Cá nhân tôi đang thử nghiệm Datascript làm cơ sở dữ liệu trạng thái game, nhưng vẫn còn quá sớm để đánh giá thành công hay không. Thật hay khi thấy các ý tưởng trong tutorial này ánh xạ 1:1 với góc nhìn đó
Tôi vẫn chưa chắc liệu có thể định nghĩa rule ngắn gọn hơn những gì tôi vừa viết hay không. Trong bài báo của Stanford, có thể viết rule kiểu
(<= (column ?n ?x) ...), nhưng trong Datascript thì các công việc phụ để di chuyển dữ liệu nhiều hơn hẳnTôi không biết đây là giới hạn của Datascript/Datomic Datalog, hay do tôi chưa hiểu đủ
Tôi tò mò bạn đang tiếp cận thử nghiệm này như thế nào. Nếu có sản phẩm nào để chia sẻ hoặc lời khuyên về cách tôi đang làm, tôi sẽ rất quan tâm
[1]: https://www.cs.uic.edu/~hinrichs/papers/love2006general.pdf
Đây là một cách mới mẻ để xử lý bài toán state machine trong các game có nhiều logic, chẳng hạn như mô phỏng thành phố. Tôi chưa từng nghĩ đến việc dùng Prolog theo cách này
Có vẻ có thể làm được những điều rất hay trong các cuộc thảo luận về tính nhân quả và tính tương đối. Ví dụ, khi tạo một cuộc chạm trán ngẫu nhiên với NPC, có thể tạo lịch sử sự kiện của NPC đó bao gồm chuỗi nhân quả mà người chơi đã khởi đầu trong quá khứ
Vì mọi thứ đều là quan hệ, và có sự thật rằng NPC hiện đang tồn tại cùng với toàn bộ trạng thái thế giới đã quan sát được và lịch sử các hành động, nên có thể lần ngược lại để gán cho NPC một quá khứ hoàn toàn nhất quán mà không cần mô phỏng trước
Tôi chưa từng nghĩ rằng mô hình actor-world/entity-trait phổ biến lại khớp tốt với cách tiếp cận quan hệ của Prolog
Tuy nhiên, thời gian thực thi có thể dự đoán và hiệu quả cũng rất quan trọng. Prolog thường có xu hướng tìm kiếm vét cạn bằng cách khớp các hạng tử để thỏa mãn truy vấn. Tôi vẫn chưa đọc hết loạt bài, nên không biết liệu vấn đề này có được đề cập không
Phỏng đoán ban đầu của tôi là nó sẽ có dạng ghi các ràng buộc đã biết về lời giải như một kiểu “khai báo kiểu” nào đó