Có lẽ bạn đã quen với việc tạo cấp độ thủ tục; à, trong bài đăng này, tất cả đều nói về việc tạo nhiệm vụ theo thủ tục. Chúng ta sẽ xem xét bức tranh toàn cảnh về việc tạo ra các nhiệm vụ bằng cách sử dụng máy học cổ điển và mạng thần kinh tái diễn cho các trò chơi roguelike.
Chào mọi người! Tên tôi là Lev Kobelev và tôi là Nhà thiết kế trò chơi tại MY.GAMES. Trong bài viết này, tôi muốn chia sẻ kinh nghiệm sử dụng ML cổ điển và mạng thần kinh đơn giản khi tôi giải thích cách thức và lý do chúng tôi quyết định tạo nhiệm vụ theo thủ tục, đồng thời chúng tôi cũng sẽ đi sâu vào việc triển khai quy trình trong Zombie Tình trạng.
Tuyên bố miễn trừ trách nhiệm: Bài viết này chỉ nhằm mục đích thông tin/giải trí và khi sử dụng một giải pháp cụ thể, chúng tôi khuyên bạn nên kiểm tra cẩn thận các điều khoản sử dụng của một tài nguyên cụ thể và tham khảo ý kiến của nhân viên pháp lý!
☝🏻 Đầu tiên, một số thuật ngữ: “ đấu trường ”, “ cấp độ ” và “ địa điểm ” là đồng nghĩa trong bối cảnh này, cũng như “ khu vực ”, “ khu vực ” và “ khu vực sinh sản ”.
Bây giờ, hãy xác định “ sứ mệnh ”. Nhiệm vụ là một thứ tự được xác định trước trong đó kẻ thù xuất hiện tại một địa điểm theo những quy tắc nhất định . Như đã đề cập, ở Trạng thái Zombie, các địa điểm được tạo nên chúng tôi không tạo ra trải nghiệm “dàn dựng”. Tức là chúng ta không đặt kẻ thù vào những điểm định trước – thực tế là không có điểm nào như vậy. Trong trường hợp của chúng tôi, kẻ thù xuất hiện ở đâu đó gần người chơi hoặc một bức tường cụ thể. Hơn nữa, tất cả các đấu trường trong trò chơi đều có hình chữ nhật, vì vậy bất kỳ nhiệm vụ nào cũng có thể được thực hiện trên bất kỳ đấu trường nào.
Hãy giới thiệu thuật ngữ “ spawn ”. Sinh sản là sự xuất hiện của một số kẻ thù cùng loại theo các thông số định trước tại các điểm trong khu vực được chỉ định . Một điểm - một kẻ thù. Nếu không có đủ điểm trong một khu vực thì khu vực đó sẽ được mở rộng theo các quy tắc đặc biệt. Điều quan trọng là phải hiểu rằng khu vực này chỉ được xác định khi sinh sản được kích hoạt. Khu vực được xác định bởi các thông số sinh sản và chúng ta sẽ xem xét hai ví dụ bên dưới: một sinh sản gần người chơi và một sinh sản gần bức tường.
Loại sinh sản đầu tiên ở gần người chơi . Hình dáng gần trình phát được chỉ định thông qua một khu vực, được mô tả bằng hai bán kính: bên ngoài và bên trong (R và r), chiều rộng của khu vực (β), góc quay (α) so với trình phát và khả năng hiển thị (hoặc tàng hình) mong muốn về diện mạo của kẻ thù. Bên trong một khu vực là số điểm cần thiết cho kẻ thù - và đó là nơi chúng đến!
Loại sinh sản thứ hai là ở gần bức tường . Khi một cấp độ được tạo ra, mỗi bên sẽ được đánh dấu bằng một thẻ – hướng chính. Bức tường có lối ra luôn ở phía bắc. Sự xuất hiện của kẻ thù gần bức tường được chỉ định bởi thẻ, khoảng cách từ nó (o), chiều dài (a), chiều rộng của vùng (b) và khả năng hiển thị (hoặc tàng hình) mong muốn về diện mạo của kẻ thù. Tâm của một khu vực được xác định tương ứng với vị trí hiện tại của người chơi.
Đẻ trứng đến từng đợt . Một đợt là cách mà các đợt sinh sản xuất hiện, cụ thể là độ trễ giữa chúng – chúng tôi không muốn tấn công người chơi bằng tất cả kẻ thù cùng một lúc. Các đợt được kết hợp thành các nhiệm vụ và được tung ra nối tiếp nhau theo một logic nhất định. Ví dụ: đợt thứ hai có thể được phát động sau đợt đầu tiên 20 giây (hoặc nếu hơn 90% số zombie bên trong nó bị tiêu diệt). Vì vậy, toàn bộ nhiệm vụ có thể được coi là một chiếc hộp lớn, bên trong chiếc hộp đó có những chiếc hộp cỡ trung bình (sóng), và bên trong các làn sóng còn có những chiếc hộp nhỏ hơn (đợt sinh sản).
Vì vậy, trước khi thực hiện các nhiệm vụ phù hợp, chúng tôi đã xác định một số quy tắc:
Có thời điểm, chúng tôi đã có khoảng một trăm nhiệm vụ sẵn sàng, nhưng sau một thời gian, chúng tôi thậm chí còn cần nhiều nhiệm vụ hơn nữa. Tôi và các nhà thiết kế khác không muốn tốn nhiều thời gian và công sức để tạo ra hàng trăm nhiệm vụ khác, vì vậy chúng tôi bắt đầu tìm kiếm một phương pháp nhanh chóng và rẻ tiền để tạo nhiệm vụ.
Tất cả các máy phát điện đều hoạt động theo một số quy tắc nhất định và các nhiệm vụ được tạo thủ công của chúng tôi cũng được thực hiện theo một số khuyến nghị nhất định. Vì vậy, chúng tôi đã đưa ra một giả thuyết về các mẫu trong các nhiệm vụ và những mẫu đó sẽ đóng vai trò là quy tắc cho trình tạo.
✍🏻 Một số thuật ngữ bạn sẽ tìm thấy trong bài viết:
Phân cụm là nhiệm vụ chia một bộ sưu tập nhất định thành các tập hợp con (cụm) không chồng chéo, sao cho các đối tượng tương tự thuộc về cùng một cụm và các đối tượng từ các cụm khác nhau có sự khác biệt đáng kể.
Đặc điểm phân loại là dữ liệu lấy giá trị từ một tập hợp hữu hạn và không có biểu diễn bằng số. Ví dụ: thẻ tường sinh sản: Bắc, Nam, v.v.
Mã hóa các đặc điểm phân loại là một thủ tục chuyển đổi các đặc điểm phân loại thành biểu diễn số theo một số quy tắc đã được chỉ định trước đó. Ví dụ: Bắc → 0, Nam → 1, v.v.
Chuẩn hóa là một phương pháp xử lý trước các đặc tính số để đưa chúng về một thang đo chung nào đó mà không làm mất thông tin về sự khác biệt trong phạm vi. Ví dụ, chúng có thể được sử dụng để tính toán độ giống nhau của các đối tượng. Như đã đề cập trước đó, sự giống nhau của đối tượng đóng một vai trò quan trọng trong các vấn đề phân cụm.
Việc tìm kiếm tất cả các mẫu này theo cách thủ công sẽ cực kỳ tốn thời gian, vì vậy chúng tôi quyết định sử dụng phương pháp phân cụm. Đây là lúc máy học phát huy tác dụng vì nó xử lý tốt nhiệm vụ này.
Phân cụm hoạt động trong một số không gian N chiều và ML hoạt động cụ thể với các con số. Do đó tất cả các lần sinh sản sẽ trở thành vectơ:
Vì vậy, ví dụ, nơi sinh sản được mô tả là “sinh ra 10 game bắn súng zombie ở bức tường phía bắc trong một khu vực có vết lõm 2 mét, chiều rộng 10 và chiều dài 5” đã trở thành vectơ [0,5, 0,25, 0,2 , 0,8, …, 0,5] (←những con số này là trừu tượng).
Ngoài ra, sức mạnh của nhóm kẻ thù đã bị giảm đi bằng cách ánh xạ những kẻ thù cụ thể thành các loại trừu tượng. Đối với người mới bắt đầu, kiểu ánh xạ này giúp dễ dàng chỉ định kẻ thù mới vào một cụm nhất định. Điều này cũng giúp có thể giảm số lượng mẫu tối ưu và do đó, tăng độ chính xác của quá trình tạo – nhưng còn nhiều điều hơn thế sẽ được đề cập sau.
Có nhiều thuật toán phân cụm: K-Means, DBSCAN, quang phổ, phân cấp, v.v. Tất cả đều dựa trên những ý tưởng khác nhau nhưng có cùng một mục tiêu: tìm các cụm trong dữ liệu. Dưới đây, bạn sẽ thấy các cách khác nhau để tìm cụm cho cùng một dữ liệu, tùy thuộc vào thuật toán đã chọn.
Thuật toán K-Means hoạt động tốt nhất trong trường hợp sinh sản.
Bây giờ, có một chút lạc đề dành cho những người chưa biết gì về thuật toán này (sẽ không có lý luận toán học chặt chẽ nào vì bài viết này nói về phát triển trò chơi chứ không phải về những kiến thức cơ bản về ML). K-Means lặp đi lặp lại việc chia dữ liệu thành K cụm bằng cách giảm thiểu tổng khoảng cách bình phương từ mỗi đối tượng đến giá trị trung bình của cụm được chỉ định. Giá trị trung bình được biểu thị bằng tổng khoảng cách bình phương.
Điều quan trọng là phải hiểu những điều sau đây về phương pháp này:
Chúng ta hãy xem xét điểm thứ hai chi tiết hơn một chút.
Phương pháp khuỷu tay thường được sử dụng để chọn số cụm tối ưu. Ý tưởng rất đơn giản: chúng tôi chạy thuật toán và thử tất cả K từ 1 đến N, trong đó N là một số hợp lý. Trong trường hợp của chúng tôi, đó là 10 – không thể tìm thấy nhiều cụm hơn. Bây giờ, hãy tìm tổng bình phương khoảng cách trong mỗi cụm (điểm được gọi là WSS hoặc SS). Chúng tôi sẽ hiển thị tất cả những điều này trên biểu đồ và chọn một điểm mà sau đó giá trị trên trục y ngừng thay đổi đáng kể.
Để minh họa, chúng tôi sẽ sử dụng một tập dữ liệu nổi tiếng,
Nếu bạn không thể nhìn thấy khuỷu tay thì bạn có thể sử dụng phương pháp Silhouette, nhưng nó nằm ngoài phạm vi của bài viết.
Tất cả các phép tính ở trên và dưới đây đều được thực hiện bằng Python bằng cách sử dụng các thư viện tiêu chuẩn cho ML và phân tích dữ liệu: pandas, numpy, seaborn và sklearn. Tôi sẽ không chia sẻ mã vì mục đích chính của bài viết là minh họa các khả năng hơn là đi sâu vào chi tiết kỹ thuật.
Sau khi có được số cụm tối ưu, mỗi cụm cần được nghiên cứu chi tiết. Chúng ta cần xem những sinh vật nào được bao gồm trong đó và những giá trị mà chúng mang lại. Hãy tạo cài đặt riêng cho từng cụm để sử dụng cho thế hệ tiếp theo. Các thông số bao gồm:
Chúng ta hãy xem xét các cài đặt cụm, có thể được mô tả bằng lời nói là "sự xuất hiện của những kẻ thù đơn giản ở đâu đó gần người chơi ở một khoảng cách ngắn và rất có thể là ở những điểm có thể nhìn thấy được."
Bảng cụm 1
Kẻ thù | Kiểu | r | R-delta | Vòng xoay | chiều rộng | hiển thị |
---|---|---|---|---|---|---|
zombie_common_3_5=4, zombie_heavy=1 | Người chơi | 12-10 | 1-2 | 0-30 | 30-45 | Hiển thị=9, Vô hình=1 |
Dưới đây là hai thủ thuật hữu ích:
Việc này được thực hiện với mỗi cụm và có ít hơn 10 cụm nên không mất nhiều thời gian.
Chúng ta mới chỉ đề cập đến chủ đề này một chút nhưng vẫn còn rất nhiều điều thú vị để nghiên cứu. Dưới đây là một số bài viết để tham khảo; chúng cung cấp một mô tả hay về các quy trình làm việc với dữ liệu, phân cụm và phân tích kết quả.
Ngoài mô hình sinh sản, chúng tôi quyết định nghiên cứu sự phụ thuộc của tổng lượng máu của kẻ thù trong một nhiệm vụ vào thời gian dự kiến hoàn thành để sử dụng thông số này trong quá trình tạo.
Trong quá trình tạo các nhiệm vụ thủ công, nhiệm vụ là xây dựng nhịp độ phối hợp cho chương - một chuỗi các nhiệm vụ: ngắn, dài, ngắn, lại ngắn, v.v. Làm cách nào bạn có thể biết được tổng lượng máu của kẻ thù trong một nhiệm vụ nếu bạn biết DPS dự kiến của người chơi và thời gian của nó?
💡 Hồi quy tuyến tính là phương pháp xây dựng lại sự phụ thuộc của một biến vào một biến khác hoặc một số biến khác bằng hàm phụ thuộc tuyến tính. Các ví dụ dưới đây sẽ chỉ xem xét hồi quy tuyến tính từ một biến: f(x) = wx + b.
Hãy giới thiệu các thuật ngữ sau:
Vì vậy, HP = DPS * thời gian hành động + thời gian rảnh. Khi tạo một chương thủ công, chúng tôi đã ghi lại thời gian dự kiến của từng nhiệm vụ; Bây giờ, chúng ta cần tìm thời gian hành động.
Nếu bạn biết thời gian thực hiện nhiệm vụ dự kiến , bạn có thể tính thời gian hành động và trừ đi thời gian dự kiến để có thời gian rảnh : thời gian rảnh = thời gian nhiệm vụ - thời gian hành động = thời gian nhiệm vụ - HP * DPS. Con số này sau đó có thể được chia cho số lượng kẻ thù trung bình trong nhiệm vụ và bạn sẽ có thời gian rảnh cho mỗi kẻ thù. Do đó, tất cả những gì còn lại chỉ đơn giản là xây dựng một hồi quy tuyến tính từ thời gian thực hiện nhiệm vụ dự kiến đến thời gian rảnh rỗi cho mỗi kẻ thù.
Ngoài ra, chúng tôi sẽ xây dựng hồi quy tỷ lệ thời gian hành động so với thời gian thực hiện nhiệm vụ.
Chúng ta hãy xem một ví dụ về tính toán và xem tại sao những hồi quy này được sử dụng:
Đây là một câu hỏi: tại sao chúng ta cần biết thời gian rảnh rỗi cho kẻ thù? Như đã đề cập trước đó, lần sinh sản được sắp xếp theo thời gian. Do đó, thời gian của lần sinh sản thứ i có thể được tính bằng tổng thời gian hành động của lần sinh sản thứ i (i-1) và thời gian rảnh trong đó.
Và đây là một câu hỏi khác: tại sao phần thời gian hành động và thời gian rảnh không cố định?
Trong trò chơi của chúng tôi, độ khó của một nhiệm vụ liên quan đến thời lượng của nó. Nghĩa là, nhiệm vụ ngắn thì dễ hơn và nhiệm vụ dài thì khó hơn. Một trong những thông số khó khăn là thời gian rảnh cho mỗi kẻ thù. Có một số đường thẳng trong biểu đồ trên và chúng có cùng hệ số độ dốc (w), nhưng có độ lệch khác nhau (b). Vì vậy, để thay đổi độ khó, chỉ cần thay đổi độ lệch: tăng b làm cho trò chơi dễ hơn, giảm làm cho trò chơi khó hơn và cho phép các số âm. Các tùy chọn này giúp bạn thay đổi độ khó từ chương này sang chương khác.
Tôi tin rằng tất cả các nhà thiết kế nên đi sâu vào vấn đề hồi quy, vì nó thường giúp giải mã các dự án khác:
Vì vậy, chúng tôi đã tìm được các quy tắc cho trình tạo và bây giờ chúng tôi có thể chuyển sang quy trình tạo.
Nếu bạn nghĩ một cách trừu tượng, thì bất kỳ nhiệm vụ nào cũng có thể được biểu diễn dưới dạng một chuỗi các số, trong đó mỗi số phản ánh một cụm sinh sản cụ thể. Ví dụ: nhiệm vụ: 1, 2, 1, 1, 2, 3, 3, 2, 1, 3. Điều này có nghĩa là nhiệm vụ tạo ra các nhiệm vụ mới phụ thuộc vào việc tạo ra các chuỗi số mới. Sau khi tạo, bạn chỉ cần “mở rộng” từng số riêng lẻ theo cài đặt cụm.
Nếu chúng ta xem xét một phương pháp đơn giản để tạo ra một chuỗi, chúng ta có thể tính toán xác suất thống kê của một lần sinh sản cụ thể tiếp theo bất kỳ lần sinh sản nào khác. Ví dụ: chúng ta có sơ đồ sau:
Phần trên cùng của sơ đồ là cụm mà nó dẫn tới, một đỉnh và trọng số cạnh là xác suất cụm tiếp theo.
Đi qua biểu đồ như vậy, chúng ta có thể tạo ra một chuỗi. Tuy nhiên, cách tiếp cận này có một số nhược điểm. Ví dụ, chúng bao gồm thiếu bộ nhớ (nó chỉ biết trạng thái hiện tại) và khả năng “mắc kẹt” ở một trạng thái nếu nó có xác suất thống kê cao là biến thành chính nó.
✍🏻 Nếu coi đồ thị này là một quá trình thì chúng ta sẽ có một xích Markov đơn giản.
Hãy chuyển sang mạng nơ-ron, cụ thể là mạng tái phát vì chúng không có nhược điểm của phương pháp cơ bản. Các mạng này rất giỏi trong việc mô hình hóa các chuỗi như ký tự hoặc từ trong các tác vụ xử lý ngôn ngữ tự nhiên. Nói một cách rất đơn giản, mạng được đào tạo để dự đoán phần tử tiếp theo của chuỗi dựa trên các phần tử trước đó.
Mô tả về cách thức hoạt động của các mạng này nằm ngoài phạm vi của bài viết này vì đây là một chủ đề lớn. Thay vào đó, hãy xem những gì cần thiết cho việc đào tạo:
Một ví dụ đơn giản với N=2, L=3, C=5. Hãy lấy chuỗi 1, 2, 3, 4, 1 và tìm các chuỗi con có độ dài L+1 bên trong nó: [1, 2, 3, 4], [2, 3, 4, 1]. Hãy chia chuỗi thành đầu vào gồm L ký tự và câu trả lời (đích) - ký tự thứ (L+1)*.* Ví dụ: [1, 2, 3, 4] → [1, 2, 3] và [ 4]. Chúng tôi mã hóa các câu trả lời thành các vectơ nóng, [4] → [0, 0, 0, 0, 1].
Tiếp theo, bạn có thể phác thảo một mạng lưới thần kinh đơn giản bằng Python bằng cách sử dụng tensorflow hoặc pytorch. Bạn có thể xem cách thực hiện việc này bằng cách sử dụng các liên kết bên dưới. Tất cả những gì còn lại là bắt đầu quá trình đào tạo dựa trên dữ liệu được mô tả ở trên, đợi và... sau đó bạn có thể bắt đầu sản xuất!
Các mô hình học máy có các số liệu nhất định, chẳng hạn như độ chính xác. Độ chính xác thể hiện tỷ lệ câu trả lời được đưa ra đúng. Tuy nhiên, nó phải được xem xét một cách thận trọng vì có thể có sự mất cân bằng về lớp trong dữ liệu. Nếu không có (hoặc gần như không có), thì chúng ta có thể nói rằng mô hình hoạt động tốt nếu nó dự đoán câu trả lời tốt hơn ngẫu nhiên, nghĩa là độ chính xác > 1/C; nếu gần bằng 1 thì nó hoạt động rất tốt.
Trong trường hợp của chúng tôi, mô hình cho thấy độ chính xác tốt. Một trong những lý do dẫn đến những kết quả này là do số lượng cụm đạt được ít nhờ lập bản đồ kẻ thù theo chủng loại và số dư của chúng.
Dưới đây là các tài liệu khác về RNN cho những ai quan tâm:
Mô hình được huấn luyện có thể dễ dàng
Để tương tác với mô hình, một cửa sổ tùy chỉnh được tạo trong Unity nơi các nhà thiết kế trò chơi có thể đặt tất cả các tham số nhiệm vụ cần thiết:
Sau khi nhập cài đặt, tất cả những gì còn lại là nhấn nút và nhận tệp có thể chỉnh sửa nếu cần. Có, tôi muốn tạo trước các nhiệm vụ chứ không phải trong trò chơi để có thể điều chỉnh chúng.
Chúng ta hãy xem xét quá trình tạo:
Vì vậy, đây là một công cụ tốt giúp chúng tôi tăng tốc độ tạo nhiệm vụ lên gấp nhiều lần. Ngoài ra, nó còn giúp một số nhà thiết kế vượt qua nỗi sợ hãi về "sự cản trở của người viết", có thể nói, vì giờ đây bạn có thể nhận được giải pháp làm sẵn trong vài giây.
Trong bài viết, sử dụng ví dụ về việc tạo nhiệm vụ, tôi đã cố gắng chứng minh các phương pháp học máy cổ điển và mạng lưới thần kinh có thể giúp ích như thế nào trong việc phát triển trò chơi. Ngày nay có một xu hướng lớn về AI sáng tạo – nhưng đừng quên các nhánh khác của học máy, vì chúng cũng có rất nhiều khả năng.
Cảm ơn đã dành thời gian để đọc bài viết này! Tôi hy vọng bạn hiểu được cả cách tiếp cận nhiệm vụ tại các địa điểm được tạo và cách tạo nhiệm vụ. Đừng ngại học hỏi những điều mới, phát triển bản thân và tạo ra những trò chơi hay!
Minh họa bởi shabbyrist