7 Ứng dụng mô hình vào game cụ thể
7.2 Quá trình phát triển
Dựa vào Game design document ở phần trước, em đã bắt tay vào phát triển Dying Hope và trong quá trình phát triển thì không thể tránh khỏi những thay đổi so với thiết kế ban đầu và những thay đổi này đã được ghi chép lại trong suốt quá trình phát triển và Game design document cũng như thế mà cập nhật liên tục. Dying Hope được phát triển trên Unity phiên bản 2019.3.4f1 64-bit bằng ngôn ngữ C# và em đã sử dụng GitHub để làm source control cho project này. Trong Unity, mỗi một màn chơi là một Scene, trong một scene sẽ có nhiều game object đóng vai trò là các đối tượng trong game (nhân vật, kẻ địch, tòa nhà, cây cối,...) mà mỗi game object sẽ có các component của riêng mình. Các component này có thể là component có sẵn của Unity hoặc component mà người phát triển (developer) tạo ra dưới dạng C# script. Ví dụ của các component: Transform: là component của Unity giúp quản lý vị trí và góc xoay của game object trong không gian hoặc CharacterController do người phát triển tạo ra để điều khiển nhân vật trong màn chơi,... Trong phần này, em sẽ trình bày các game object chính trong màn chơi cùng các component quan trọng và vai trò, hành vi của các game object này. Mỗi component của bất kỳ game object sẽ có các hàm: Awake, Start và Update. Khi màn chơi được khởi động thì Unity sẽ gọi lần lượt hàm Awake và Start của các component và ở mỗi chu kỳ cập nhật (1 frame) thì sẽ gọi hàm Update.
7.2.1 Main Camera
Những gì camera này thấy được cũng là những gì mà người chơi thấy trên màn hình. Như trong thiết kế, camera sẽ có góc nhìn top-down với góc nghiêng 45 độ theo trục x của nó, với phép chiếu perspective. Camera này được
gắn một script CameraController do em tự viết để người chơi có thể điều khiển camera này (di chuyển, xoay, zoom)
Hình 42: Class diagram của CameraController.
• Instance: CameraController là một singleton vì trong mỗi màn chơi chỉ tồn tại duy nhất 1 instance của class này. Biến Instance này giúp các class khác có thể tham chiếu đến instance duy nhất của class này.
• panSpeed: tốc độ khi di chuyển camera (có giá trị là 30).
• panBorderThickness: độ dày các biên của màn hình mà khi người chơi di chuyển con trỏ chuột trong các biên này thì camera sẽ bắt đầu di chuyển (có giá trị là 10).
• scrollSpeed: tốc độ di chuyển của camera khi zoom in, zoom out (có giá trị là 20).
• rotationSpeed: tốc độ khi xoay camera (có giá trị là 30).
• yPanLimit: giới hạn zoom của camera, với x là cận dưới, y là cận trên. (có giá trị là x: 10, y: 30).
• lookAt: là vị trí tâm camera, khi xoay thì camera sẽ lấy vị trí này làm tâm xoay. Vị trí này sẽ thay đổi khi di chuyển camera.
Khi màn chơi bắt đầu CameraController sẽ raycast để lấy vị trí trung tâm của view trong màn chơi và gán vào biến lookAt. Ở mỗi frame, nó sẽ xét xem có nhận được input từ người chơi không. Nếu có và input này là để di chuyển camera thì camera sẽ di chuyển theo input của người chơi, khi di chuyển nó sẽ xét xem view của camera
có nằm trong diện tích màn chơi không, nếu không thì camera sẽ ngừng di chuyển. Tương tự, khi input là xoay thì camera sẽ xoay xung quanh điểm lookAt và khi input là zoom thì camera sẽ thay đổi khoảng cách của mình so với điểm lookAt và khi thay đổi sẽ xét điều kiện là tọa độ y của camera phải nằm trong khoảng của yPanLimit nếu nằm ngoài khoảng này camera sẽ không thay đổi.
Ngoài ra, Main camera cũng được gắn một script MouseControls để quản lý các input của nút chuột và các phím bấm khác. Cụ thể là khi người chơi trỏ chuột vào những đối tượng không tương tác được, icon của con trỏ sẽ thay đổi để người chơi biết đồng thời sẽ bỏ qua bất kỳ input của nút chuột. Song song đó, script này cũng kiểm tra và xử lí input về các tác vụ phụ trợ như đóng, mở Pause menu, đóng, mở Mission menu, highlight kẻ địch, quicksave, quickload, chọn nhân vật trong màn chơi.
7.2.2 Nhân vật
Nhân vật, như đã được mô tả, có thể được điều khiển bởi người chơi để thực hiện các hành động và kỹ năng: đi, chạy, ngồi,... Sự di chuyển của nhân vật trong màn chơi được thực hiện bằng NavMesh của Unity và các animation sẽ được quản lí bởi Animator để chạy các animation thích hợp cho từng hành động. Hành vi của nhân vật sẽ được quản lí bởi script CharacterControlling.
Hình 44: Class diagram của CharacterControlling.
• slotMapping: để map từng hành động và kỹ năng của nhân vật với các slot hành động thích hợp, để khi nhân vật thực hiện hành động hoặc kỹ năng thì các slot này sẽ thay đổi các chỉ số cũng như UI thích hợp, ví dụ: sau khi thực hiện kỹ năng bắn súng thì kỹ năng sẽ cooldown và slot của kỹ năng này sẽ thực hiện đếm ngược trên UI để người chơi biết là kỹ năng đang cooldown và chưa dùng được.
• characterStats: là các thuộc tính riêng biệt của từng nhân vật, ví dụ: tên, avatar, màu nền avatar, màu khi highlight,... Được dùng để thay đổi UI khi nhân vật được chọn.
• walkSpeed, crouchWalkSpeed, runSpeed: có giá trị lần lượt là 2, 1, 3.5 cho tất cả nhân vật.
• HealthPoints: là một property dùng để quản lí máu của nhân vật. Khi máu của nhân vật thay đổi thì nó cũng thực hiện việc gọi hàm callback để thay đổi UI của máu nhân vật và gọi hàm thực hiện hành động chết của nhân vật khi máu giảm về 0.
• currentAction: là hành động hoặc kỹ năng mà nhân vật đang thực hiện.
• CheckInput(): kiểm tra input từ các phím hành động và kỹ năng.
• ToggleSkill(string key): được gọi trong hàm CheckInput để thực hiện hành động thích hợp với phím đã bấm.
• MoveTo(Vector3 destination, MoveState state): ra lệnh cho NavMeshAgent di chuyển đến vị trí destination, với trạng thái state (có 3 giá trị: walk, run, crouchwalk), nhờ vào trạng thái này mà script sẽ thay đổi vận tốc di chuyển và animation cho phù hợp. Khi các animation của nhân vật đang chạy thì một số có gọi các hàm để phát ra âm thanh của hành động ứng với nó (hành động đi sẽ gọi hàm PlayFootstepSound để phát ra âm thanh của bước chân).
• EndCurrentAction(): dừng hành động đang thực hiện và trở về trạng thái trước đó (nếu trước khi hành động nhân vật đang đứng thì sau khi dừng hành động nhân vật sẽ đứng, điều tương tự cũng áp dụng cho ngồi). Khi màn chơi bắt đầu CharacterControlling sẽ gán máu của người chơi bằng với MAX_HEALTH_POINT và gán các giá trị thích hợp cho các field và property. Ở mỗi frame, nếu nhân vật đang rảnh rỗi (idle) nó sẽ xét xem có nhận được input nhấp phím trái chuột của người chơi không, nếu có thì nó sẽ thực hiện chuyển đổi vị trí mà người chơi mới nhấp chuột thành vị trí trên NavMesh mà NavMeshAgent của nhân vật đi được, sau đó cho nhân vật đi đến vị trí đó. Tương tự, khi người chơi bấm phím kỹ năng bất kỳ, thì nhân vật sẽ thực hiện kỹ năng đó cùng với thay đổi UI và animation cho người chơi biết được nhân vật đang thực hiện kỹ năng đó.
Như đã đề cập, mỗi nhân vật sẽ có 6 hành động có thể thực hiện ứng với 6 phím bấm (ngồi, nhặt đạn và 4 kỹ năng). Những hành động này sẽ có hành vi và các thuộc tính riêng biệt, nhưng để quản lý tốt hơn chúng đều sẽ được kế thừa từ một abstract class CharacterAction. Mỗi hành động sẽ được gắn vào một game object rỗng và gán làm con của game object của nhân vật để dễ dàng cho việc thêm hoặc bớt các hành động mà không làm ảnh hưởng đến các thành phần khác của nhân vật.
Hình 45: Cách tổ chức game object của nhân vật Doc McCoy trong Unity Editor.
• coolDownTime: thời gian chờ giữa 2 lần thực hiện hành động.
• description: mô tả hành động (để người chơi mới dễ dàng sử dụng).
• limitNumber: số lần tối đa để thực hiện hành động (nếu có giá trị bằng 0 thì hành động không có giới hạn số lần thực hiện).
• coolDownAmount: lượng đếm ngược hiện tại, có giá trị từ 0 đến 1. Khi hành động đang đếm ngược (cooldown) thì biến này sẽ có giá trị là 1 giảm dần về 0. Nếu bằng 0 xem như việc đếm ngược đã hoàn tất.
• currentNumber: số lần thực hiện còn lại của hành động.
• isDisable: hành động có đang bị vô hiệu hóa không (hành động bị vô hiệu hóa khi số lần thực hiện bằng với limitNumber hoặc khi hành động đang đếm ngược hay coolDownAmount > 0).
• StartAction(), ActionInProgress(), EndAction(): mỗi hành động sẽ được chia thành 3 giai đoạn (bắt đầu, đang thực hiện và kết thúc). Ở giai đoạn bắt đầu, script ứng với hành động sẽ thay đổi UI, animation cũng như bật các skill indicator (các hiệu ứng để người chơi dễ dàng chọn mục tiêu), sau khi chọn mục tiêu xong sẽ chuyển sang giai đoạn đang thực hiện (hành động ngồi không cần chọn mục tiêu). Trong giai đoạn đang thực hiện, nếu mục tiêu nằm ngoài tầm thực hiện hành động thì nhân vật sẽ di chuyển đến mục tiêu và thực hiện hành động lên mục tiêu, nếu mục tiêu nằm trong tầm thực hiện thì nhân vật sẽ thực hiện hành động lên mục tiêu ngay. Sau khi thực hiện hành động xong sẽ chuyển sang giai đoạn kết thúc. Ở giai đoạn kết thúc, script sẽ tiến hành đếm ngược (cooldown) hành động, kiểm tra xem đã hết số lần thực hiện chưa để vô hiệu hóa hành động và thay đổi UI đề người chơi biết được những điều đó và cuối cùng là thay đổi animation và trạng thái của nhân vật trước khi thực hiện hành động. Ví dụ: hành động Stealth Kill, khi bắt đầu thì con trỏ chuột sẽ thay đổi để người chơi biết là hành động đang chờ chọn mục tiêu, sau khi chọn mục tiêu phù hợp thì nhân vật sẽ đi đến sau lưng mục tiêu, khi đã đến sau lưng thì nhân vật thực hiện tiêu diệt mục tiêu và sau khi mục tiêu đã bị tiêu diệt thì hành động bắt đầu cooldown và nhân vật trở về trạng thái trước đó.
Từ abstract class ở trên, em đã tạo ra các class con tương ứng với các hành động như đã được mô tả trong thiết kế của game. Các class con này sẽ có những điểm tương đồng với nhau là các thuộc tính và hàm mà nó thừa kế từ class cha nhưng đồng thời chúng cũng sẽ có những điểm riêng biệt tùy theo hành động mà nó mô tả, bởi các hành động có bản chất khác nhau. Các class con này được mô tả như sau:
• CrouchAction: tương ứng với hành động ngồi chung của các nhân vật. Hành động ngồi không có thời gian chờ cũng không có giới hạn số lần sử dụng. Khi kích hoạt hành động ngồi, nhân vật sẽ dừng bất cứ hành động nào hiện tại bằng cách gọi hàm EndAction của hành động đó và biến currentAction của nhân vật sẽ được gán thành CrouchAction. Trong khi hành động ngồi đang được kích hoạt, nhân vật được tự do di chuyển như khi đang idle nhưng hình thức di chuyển là crouch walk. Ngoài ra, nhân vật cũng có thể chạy bằng cách nhấn đúp chuột vào vị trí thích hợp trong màn chơi nhưng khi này hành động ngồi sẽ bị hủy.
Hình 47: Các class CharacterAction con.
• InteractAction: là hành động nhặt vật phẩm chung của các nhân vật. Các vật phẩm có thể nhặt được là: đạn (từ các thùng đạn), túi bẫy (kỹ năng Bait Bag của nhân vật Doc McCoy). Riêng các con dao từ kỹ năng Throw Knife của nhân vật John Cooper thì chỉ cần di chuyển lại gần là nhân vật sẽ tự động nhặt, do con dao có kích thước quá bé gây khó khăn trong việc chọn mục tiêu để nhặt. Mỗi nhân vật chỉ nhặt được các vật phẩm phù hợp với mình, ví dụ: John Cooper chỉ nhặt được đạn pistol và dao mà không nhặt được đạn sniper và túi bẫy của Doc McCoy. Khi thực hiện hành động này, nhân vật cũng được tự do di chuyển như đang idle. Khi người chơi chọn vật phẩm để nhặt, nếu khi chọn nhấn 1 lần chuột thì người chơi sẽ đi (walk) về vị trí của vật phẩm và thực hiện nhặt còn khi nhấn đúp chuột thì nhân vật sẽ chạy lại và nhặt, còn khi nhân vật đang ngồi thì sẽ crouch walk mà nhặt. Trong lúc di chuyển lại vị trí vật phẩm, người chơi có thể hủy hành động bằng kích hoạt bất kỳ hành động nào khác kể cả hành động này. Sau khi nhặt xong bộ đếm số vật phẩm sẽ tăng lên và nhân vật sẽ trở về trạng thái lúc trước khi hành động.
• StealthKillAction: là hành động tiêu diệt địch từ sau lưng của các nhân vật. Hành vi của hành động này cũng tương tự như hành động nhặt đạn nhưng thay vì chọn vật phẩm thì ở đây người chơi sẽ chọn một kẻ thù bất kỳ miễn là kẻ thù đó đang không phát hiện nhân vật. Sau khi chọn mục tiêu xong, nhân vật sẽ di chuyển về vị trí sau lưng của kẻ thù và tiêu diệt hắn. Sau khi thực hiện xong nhân vật sẽ trở về trạng thái lúc trước khi hành động.
• ThrowAction: tương ứng với kỹ năng Throw Coin của John Cooper và kỹ năng Bait Bag của Doc McCoy. Sau khi kích hoạt kỹ năng, nhân vật không thể di chuyển, lúc này sẽ có hiệu ứng vẽ ra quỹ đạo parabol mà vật ném ra sẽ di chuyển, nếu trên quỹ đạo có vật cản thì người chơi cũng được thông báo về việc này. Từ đó giúp người chơi dễ dàng chọn được mục tiêu phù hợp. Mục tiêu ở đây là bất kỳ vị trí nào cho phép trong màn chơi. Sau khi chọn mục tiêu phù hợp, nhân vật sẽ tiến hành ném đối tượng cần ném ra, sau khi đối tượng đó chạm đất hoặc một vật bất kỳ nào khác, nó sẽ có những hành vi như đã định nghĩa, ví dụ: khi đồng tiền chạm đất sẽ phát ra một âm thanh lớn. Sau khi ném xong nhân vật sẽ trở về trạng thái trước đó, kỹ năng bắt đầu đếm ngược. Trong trường hợp kỹ năng Bait Bag thì kỹ năng sẽ bị vô hiệu hóa cho đến khi người chơi nhặt lại túi bẫy.
• ThrowProjectileAction: tương ứng với kỹ năng Throw Knife của John Cooper. Hành vi của hành động này cũng giống như ThrowAction nhưng ở đây người chơi có thể chọn bất kỳ kẻ địch nào làm mục tiêu và quỹ đạo của vật ném là đường thẳng. Sau khi thực hiện xong, kỹ năng sẽ đếm ngược và số lần thực hiện sẽ giảm xuống 1, nếu số lần thực hiện giảm về 0, thì kỹ năng sẽ bị vô hiệu hóa cho đến khi nhân vật nhặt lại những vật mình ném ra.
• DissolveBodyAction: tương ứng với kỹ năng Acid Bottle của Doc McCoy. Cách sử dụng hành động này cũng tương tự như hành động InteractAction nhưng ở đây mục tiêu là những xác chết mà sau khi tiêu diệt kẻ địch để lại nhằm tránh kẻ địch phát hiện xác và báo động.
• ShootAction: tương ứng với kỹ năng Shoot Pistol của John Cooper và Shoot Sniper Rifle của Doc McCoy. Cách sử dụng hành động này cũng tương tự như hành động ThrowProjectileAction nhưng ở đây nhân vật cần thực hiện animation lấy vũ khí ra khi thực hiện và cất vũ khí vào khi thực hiện xong. Sau khi thực hiện xong, kỹ năng sẽ đếm ngược và số lần thực hiện sẽ giảm xuống 1, nếu số lần thực hiện giảm về 0, thì kỹ năng sẽ bị vô hiệu hóa cho đến khi nhân vật nhặt loại thích hợp cho mình.
7.2.3 Mô hình nhận thức thị giác
Mô hình thị giác gồm hai thành phần đảm nhiệm hai chức năng khác nhau: DetectSystem cung cấp cho NPC khả năng phát hiện nhân vật và các đối tượng khác (xác chết, Bait Bag), FieldOfView vẽ ra các vùng tầm nhìn của NPC để người chơi dễ dàng quan sát.
DetectSystem
Hình 48: Hai vùng nhìn của NPC.
DetectSystem cung cấp các cơ chế cho việc nhìn thấy và phát hiện (detect) các đối tượng. Khi màn chơi khởi động DetectSystem sẽ lấy các thông số của các NPC (clearViewDistance, viewDistance, độ rộng Field of view, ...)
để xử lí và căn cứ vào những thông số này để điều chỉnh việc phát hiện phù hợp với từng loại NPC. DetectSystem sẽ hoạt động trên hai vùng nhìn như đã thiết kế: vùng nhìn gần sẽ phát hiện được các đối tượng với bất kỳ độ cao nào, vùng nhìn xa chỉ phát hiện được các đối tượng có chiều cao cao ngang tầm mắt của NPC (vùng nhìn phía sau