Tổng Kết Thiết Kế Khung Gameplay - nói dối e blog

Tổng Kết Thiết Kế Khung Gameplay

Trải qua hơn 20 năm làm việc trong ngành game, tôi chủ yếu tập trung vào phát triển nền tảng. Dù có hỗ trợ các nhóm khác xây dựng logic gameplay, tôi cũng chỉ dừng lại ở việc triển khai các module chức năng cụ thể. Mãi đến gần đây khi tự mình phát triển game, tôi mới dành thời gian nghiêm túc suy nghĩ về cách xây dựng một hệ thống hiệu quả.

Từ những demo nguyên mẫu đầu tiên, tôi thường xuyên cảm thấy mất kiểm soát do thiếu kinh nghiệm. Tuy nhiên khi làm một mình, việc tái cấu trúc không gây áp lực tâm lý quá lớn. Khi muốn thay đổi, tôi sẵn sàng dành 2-3 ngày để viết lại toàn bộ. Gần đây mọi thứ đã trở nên mượt mà hơn, tôi thấy cần phải hệ thống hóa lại suy nghĩ và ghi lại thành bài viết này.

Mọi vấn đề phần mềm phức tạp đều có thể giải quyết bằng cách chia nhỏ thành các bài toán con.

Theo tôi, phần logic gameplay - tức lớp trên cùng của game, bao gồm ba thành phần chính: mô hình dữ liệu, biểu hiện bên ngoài và tương tác người-máy.

Mô hình dữ liệu là trái tim của gameplay, chính là phần còn lại khi tách bỏ các yếu tố hình ảnh, giao diện đồ họa và thiết bị điều khiển (bàn phím, chuột…). Cách đơn giản nhất để xác định một module có thuộc phần này hay không là kiểm tra xem nó có chứa mã gọi trực tiếp đến engine game hay không. Sau khi tách biệt hoàn toàn, phần này không nên chứa bất kỳ interface nào liên quan đến đồ họa, giao diện, đồng hồ hệ thống hay đầu vào điều khiển. Ngoại trừ một số interface IO cần thiết (đọc dữ liệu thiết kế gameplay, ghi log, lưu trữ dữ liệu…), cũng không nên sử dụng API hệ điều hành.

Điều này cho phép chúng ta dễ dàng kiểm thử cục bộ hoặc toàn bộ hệ thống. Trong trường hợp cần thay đổi engine hoặc chuyển từ game roguelike dạng văn bản sang thể hiện 3D, hệ thống này cũng không bị ảnh hưởng.

Biểu hiện bên ngoài hiển nhiên là các yếu tố hình ảnh, âm thanh, hiệu ứng… Thường phần này do engine đảm nhiệm, nhưng vẫn có lượng lớn mã code nằm trong gameplay. Khối code này quản lý nhiều trạng thái, nhưng không liên quan đến việc lưu trữ dữ liệu. Cách kiểm tra đơn giản: khi lưu trò chơi, mọi trạng thái ở đây đều có thể loại bỏ, và khi tải lại tiến độ, các trạng thái này sẽ tự động được xây dựng lại mà người chơi không nhận thấy mất mát dữ liệu nào.

Tương tác người-máy là thành phần thiết yếu của game. Nếu không có yếu tố “người”, trò chơi sẽ mất đi ý nghĩa. Phần này gồm hai khía cạnh: giao diện đồ họa (GUI) vừa hiển thị dữ liệu game, vừa tiếp nhận đầu vào gián tiếp từ người chơi; và các thiết bị điều khiển (tay cầm, chuột, bàn phím, cảm ứng…) cho phép điều khiển trực tiếp game.

Với game online, cần thêm thành phần thứ tư là đầu vào mạng. Bài viết này chỉ tập trung vào game offline.

Trong quá trình code, tôi đặt tên cho phần mô hình dữ liệu là gameplay. Có thể chia thành hai loại chính: Đối tượng thụ động (Object)Thực thể tự trị (Actor).

Theo tôi, các Object nên được phân loại theo nhóm, mỗi nhóm quản lý tập trung. Chúng có thể là các vật thể trong cảnh game, nhân vật game (có tương ứng ở lớp biểu hiện), hoặc các yếu tố như danh sách nhiệm vụ (không hiển thị trực tiếp nhưng có thể xuất hiện trên giao diện). Khi thiết kế Object, cần làm rõ dữ liệu và các phương thức thao tác dữ liệu đó.

Phần dữ liệu cần được cân nhắc kỹ về khả năng lưu trữ ngay từ đầu - tức cách thực hiện chức năng lưu/chơi lại (Load/Save). Thông thường là thực hiện lưu trữ riêng lẻ từng Object. Để hỗ trợ việc này, mỗi Object cần có ID quản lý, với ID và typename là thuộc tính chung. Các dữ liệu khác nên độc lập, tránh tham chiếu lẫn nhau. Nếu không thực sự cần thiết, không nên cung cấp các phương thức điều khiển dữ liệu đặc biệt. Vì khi cần phương thức cụ thể, thường là do nhiều dữ liệu liên quan chặt chẽ, phải dùng phương thức duy nhất để đảm bảo tính nhất quán. Dựa trên nguyên tắc này, Object không nên có phương thức update. Nói cách khác, Object là tập hợp dữ liệu tĩnh, thụ động.

Vậy làm thế nào để game vận hành? Chúng ta cần xây dựng một loạt Actor tự trị. Mỗi Actor tương ứng với một thực thể trong thế giới game, có thể liên kết với một hoặc nhiều Object để đọc/ghi dữ liệu và điều khiển chúng. Hầu hết các game không gặp vấn đề hiệu năng nghiêm trọng ở tầng gameplay, nên không cần xử lý song song. Dù về mặt logic các Actor độc lập, xử lý tuần tự sẽ tránh được xung đột đọc/ghi dữ liệu Object.

Actor hoạt động theo cơ chế tin nhắn. Các Actor khác nhau có hàm update xử lý hành vi theo từng khung hình (tick), đồng thời xử lý tin nhắn. Thế giới game chỉ nhận đầu vào từ bên ngoài thông qua việc gửi tin nhắn đến Actor. Actor thường được thiết kế dưới dạng máy trạng thái (state machine), cho phép các nhân vật ảo hành xử khác nhau tùy theo trạng thái. Actor cần quản lý nhiều trạng thái trung gian, đồng thời giải quyết vấn đề lưu trữ, nhưng phần lớn trạng thái nội bộ không cần lưu trữ. Trong đa số trường hợp, chỉ cần lưu tên trạng thái hiện tại của máy trạng thái là đủ. Các trạng thái chạy thời gian thực có thể được xây dựng lại từ trạng thái này.

Ví dụ: Trong game có một công nhân nhận nhiệm vụ lấy hàng từ điểm A đến điểm B.

  • Đơn hàng là một Object, chứa thông tin vị trí điểm A/B, loại và số lượng hàng hóa.
  • Công nhân là một Object, lưu trữ vị trí hiện tại, vật phẩm mang theo, mã đơn hàng…
  • Một Actor đóng vai trò điều khiển công nhân, quản lý các hành vi như nhận đơn, thực hiện đơn…

Quy trình thực hiện đơn hàng có thể chia thành các bước:

  1. Xác định lộ trình đến điểm A
  2. Di chuyển đến điểm A
  3. Lấy hàng
  4. Xác định lộ trình đến điểm B
  5. Di chuyển đến điểm B
  6. Giao hàng

Một số bước thực hiện ngay lập tức, một số cần nhiều khung hình. Các quy trình kéo dài sẽ tạo ra trạng thái trung gian không cần lưu trữ. Các trạng thái tạm thời này phải có thể xây dựng lại. Ví dụ, trong bước “di chuyển”, nếu môi trường thay đổi (đường đi bị chặn), Actor nhận tin nhắn và chuyển trạng thái sang “tìm đường”, lúc này trạng thái trung gian của bước “di chuyển” trước đó không còn cần thiết.

Thiết kế hệ thống lưu trữ cần được ưu tiên cao. Việc này buộc chúng ta phải thiết kế mô hình dữ liệu kỹ l

0%