2 Các nghiên cứu liên quan
3.1 NavMesh và tìm đường đi ngắn nhất trong Unity
Hệ thống tìm đường trong Unity cần một cấu trúc dữ liệu riêng để biểu diễn các khu vực mà các nhân vật AI có thể đứng và di chuyển được. Trong Unity, các nhân vật AI được mô tả như một hình trụ. Khu vực có thể di chuyển được xây dựng tự động từ hình dạng của môi trường trong game bằng cách thử nghiệm các vị trí mà nhân vật AI có thể đứng. Sau đó, các vị trí này được kết nối với một bề mặt nằm trên bề mặt của môi trường. Bề mặt này được gọi là Navigation Mesh (viết tắt là NavMesh). NavMesh lưu trữ bề mặt này dưới dạng các đa giác lồi. Đa giác lồi là một biểu diễn hiệu quả, vì chúng ta biết rằng không có sự cản trở nào giữa hai điểm bất kỳ bên trong một đa giác lồi. Ngoài việc lưu trữ các biên của đa giác, Unity còn lưu trữ thông tin về đa giác nào là hàng xóm của nhau. Điều này cho phép biểu diễn toàn bộ khu vực có thể di chuyển được bởi các nhân vật AI.
Hình 7: NavMesh (lớp màu xanh nằm trên bề mặt của bản đồ) (Unity Technologies, 2018).
Để tìm đường đi giữa hai vị trí trong màn chơi, trước tiên Unity sẽ chuyển đổi vị trí xuất phát và vị trí đích thành đa giác gần nhất của chúng. Sau đó, Unity sẽ bắt đầu tìm kiếm từ vị trí bắt đầu, thăm tất cả các đa giác hàng
xóm cho đến khi đến được đa giác đích. Trong quá trình này Unity cũng lưu lại các đa giác đã thăm cho phép chúng ta tìm được chuỗi các đa giác từ điểm bắt đầu đến điểm đích. Thuật toán tìm chuỗi đa giác mà Unity sử dụng làA∗. VìA∗hoạt động trên một đồ thị gồm nhiều nốt, nhưng NavMesh trong Unity lại được biểu diễn bởi một lưới đa giác, cho nên đểA∗hoạt động được trên NavMesh thì việc Unity cần làm là với mỗi đa giác, Unity sẽ chọn một vị trí trong đa giác đó để làm vị trí của nốt trong đồ thị. Unity không đề cập về cách chọn vị trí này nhưng cách thông dụng nhất là chọn tâm của đa giác.
Giải thuật A*
A∗là một giải thuật tìm kiếm có thông tin (informed-search) và còn gọi là tìm kiếm ưu tiên tốt nhất (best-first search).A∗hoạt động trên đồ thị có trọng số: bắt đầu từ một nốt bắt đầu cụ thể trên đồ thị, nó sẽ tìm đường dẫn đến nút mục tiêu đã cho với chi phí nhỏ nhất (quãng đường di chuyển ít nhất, thời gian ngắn nhất, v.v.). Nó thực hiện điều này bằng cách xây một cây của các đường đi bắt đầu từ nút xuất phát và mở rộng các đường đi đó mỗi lần một cạnh cho đến khi điều kiện kết thúc của nó được thỏa mãn.
Tại mỗi vòng lặp chính,A∗cần xác định đường đi nào cần mở rộng. Việc làm này dựa trên chi phí của đường đi và ước tính chi phí cần thiết để mở rộng đường đi cho đến khi chạm mục tiêu. Cụ thể,A∗chọn đường đi tối thiểu phương trình:
f(n) =g(n) +h(n)
Trong đó, n là nốt tiếp theo trên đường đi, g(n) là chi phí của đường đi từ nốt bắt đầu đến nốt n và h(n) là một hàm heuristic ước tính chi phí của đường đi tốt nhất từ nốt n đến nút đích.A∗kết thúc khi đường đi mà nó chọn để mở rộng là đường đi từ nốt bắt đầu đến nốt mục tiêu hoặc nếu không có đường đi nào đủ điều kiện để được mở rộng. Việc lựa chọn hàm heuristic phụ thuộc vào vấn đề cần giải quyết. Nếu hàm heuristic là chấp nhận được (admissible), nghĩa là nó không bao giờ đánh giá quá cao chi phí thực tế để đến được nốt mục tiêu,A∗sẽ đảm bảo trả về một đường đi có ít chi phí nhất từ nốt đầu đến nốt mục tiêu.
Các hiện thực thường thấy củaA∗sử dụng hàng đợi ưu tiên (priority queue) để thực hiện việc lựa chọn các nốt có chi phí (ước tính) tối thiểu để mở rộng. Hàng đợi ưu tiên này được gọi là open set hoặc fringe. Tại mỗi bước của giải thuật, nốt có giá trị f(x) thấp nhất bị xóa khỏi hàng đợi, các giá trị f và g của các nốt lân cận của nó cũng được cập nhật tương ứng và các nốt lân cận này được thêm vào hàng đợi. Giải thuật tiếp tục cho đến khi một nốt bị loại bỏ (nốt có giá trị f thấp nhất trong số tất cả các nốt trong hàng đợi) là nốt mục tiêu. Giá trị f của nốt mục tiêu này cũng là chi phí của đường đi ngắn nhất, vì h tại nốt mục tiêu là 0 khi hàm heuristic là chấp nhận được (admissible). Giải thuật được mô tả đến đây chỉ cho chúng ta độ dài của đường đi ngắn nhất. Để tìm ra trình tự của các bước để đến được nốt mục tiêu, giải thuật có thể dễ dàng sửa đổi để mỗi nốt trên đường đi lưu thông tin về các nốt trước (predecessor) của nó. Sau khi giải thuật được chạy, nốt kết thúc sẽ trỏ đến nốt phía trước của nó và cứ như thế, cho đến khi nốt trước của một nốt là nốt bắt đầu.
Ví dụ, khi tìm kiếm tuyến đường ngắn nhất trên bản đồ, h(x) có thể đại diện cho khoảng cách đường thẳng (straight-line distance) đến mục tiêu, vì đó là khoảng cách nhỏ nhất có thể có giữa hai điểm bất kỳ. Đối với bản đồ dạng lưới (grid) việc sử dụng khoảng cách Manhattan hoặc khoảng cách ô vuông trở nên tốt hơn tùy thuộc vào các
bước di chuyển có thể có (4 hướng hoặc 8 hướng).
Nếu hàm heuristic h thỏa mãn thêm điều kiệnh(x)≤d(x,y) +h(y)với mọi cạnh (x, y) của đồ thị (trong đó d là độ dài của cạnh đó) thì h được gọi là nhất quán (consistent). Với một heuristic nhất quán,A∗sẽ đảm bảo tìm ra đường đi tối ưu mà không cần xử lý bất kỳ nốt nào nhiều hơn một lần.
Sau khi đã tìm được chuỗi các đa giác và các điểm đại diện cho chúng từ vị trí đầu đến vị trí mục tiêu, các nhân vật AI sẽ không đi theo các điểm này mà Unity sẽ "làm gọn" lại đường đi đó để đạt được sự di chuyển tự nhiên hơn, không giống như robot. Chuỗi các đa giác mô tả đường đi từ đa giác đầu đến đa giác đích được gọi là hành lang (corridor). Nhân vật AI sẽ đến đích bằng cách luôn hướng về phía góc có thể nhìn thấy tiếp theo của hành lang như được mô tả ở hình dưới.
Hình 8: Đường đi của nhân vật AI (Unity Technologies, 2018).
Sau khi tìm hiểu, em đã phát hiện ra một giải thuật làm gọn đường đi thông dụng khi làm việc với navigation mesh và cho ra kết quả tương tự như những gì mà Unity đã mô tả, giải thuật có tên là "String Pulling Algorithm". Giải thuật này giúp chúng ta tìm được đường đi ngắn nhất (danh sách các điểm cần phải đi qua) từ danh sách các tam giác đã tìm được khi áp dụng giải thuậtA∗(các đa giác lồi đều có thể được xây dựng từ các tam giác). Danh sách các tam giác này còn được gọi là "corridor".
Ở hình chữ thập trên, nếu chúng ta dùng một sợi dây bao xung quanh chữ thập này và kéo căng nó thì lúc này sợi dây sẽ tạo ra đường đi ngắn nhất và tự nhiên nhất khi đi xung quanh chữ thập này. Hình bên dưới giúp chúng ta hiểu rõ hơn về giải thuật này.
Hình 10: Các bước thực hiện của String Pulling Algorithm (gamedev, 2018).
Chúng ta có thể thấy ở bước A, giải thuật bắt đầu bằng việc tạo ra một "funnel" với 1 đỉnh là vị trí bắt đầu trên đường đi và 2 cạnh trái và phải nối từ đỉnh đến 2 đầu mút cạnh chung của tam giác bắt đầu và tam giác liền kề nó, cạnh chung này được gọi là "portal". Các bước từ B đến D là sự di chuyển của 2 cạnh funnel men theo rìa (border) của corridor đến các portal tiếp theo. Chú ý rằng sự di chuyển này luôn làm cho 2 cạnh của funnel gần nhau hơn.
Ở bước E chúng ta có một vấn đề là nếu cạnh màu cam (cạnh trái) di chuyển về portal tiếp theo thì nó sẽ làm cho 2 cạnh funnel cách xa nhau hơn so với bước trước đó. Trong trường hợp này chúng ta sẽ không di chuyển cạnh màu cam nữa mà thay vào đó di chuyển cạnh màu xanh ở bước F.
Thật không may, lúc này chúng ta gặp một vấn đề nữa đó là nếu di chuyển cạnh màu xanh đến portal tiếp theo thì sẽ làm cho 2 cạnh funnel chéo nhau và lúc này chúng ta phát hiện có một góc (corner) trong corridor. Lúc này chúng ta cần thêm một điểm vào đường đi cuối cùng của mình đó là đầu mút của cạnh màu cam. Ở bước G, chúng ta dời vị trí của funnel sao cho đỉnh của nó trùng với vị trí mà ta vừa thêm vào đường đi và khởi tạo lại 2 cạnh như
bước đầu. Giải thuật cứ tiếp tục cho đến khi 1 cạnh bất kỳ chạm đến vị trí đích. Để tóm tắt lại, giải thuật gồm có các bước sau:
• Di chuyển 2 cạnh funnel sao cho 2 cạnh càng gần nhau hơn.
• Nếu di chuyển 1 cạnh mà làm cho 2 cạnh xa nhau thì không di chuyển cạnh đó nữa.
• Nếu di chuyển 1 cạnh mà làm cho 2 cạnh chéo nhau thì cần thêm đầu mút của cạnh còn lại vào đường đi cần tìm và khởi tạo lại funnel ở vị trí vừa thêm.
Sau khi chạy giải thuật thì chúng ta đã thành công tìm được đường đi ngắn nhất từ danh sách các đa giác đã tìm được. Và bây giờ chỉ cần cho nhân vật AI của chúng ta di chuyển theo đường đi vừa mới tìm được thì việc di chuyển đã hoàn tất.