
Last updated January 2020, 10 min. read
Ba cách để thiết kế kiến trúc trò chơi của bạn với ScriptableObjects
Trang web này đã được dịch bằng máy để thuận tiện cho bạn. Chúng tôi không thể đảm bảo tính chính xác hoặc độ tin cậy của nội dung được dịch. Nếu bạn có thắc mắc về tính chính xác của nội dung được dịch, vui lòng tham khảo phiên bản tiếng Anh chính thức của trang web.
Những gì bạn sẽ nhận được từ trang này : Mẹo để giữ cho mã trò chơi của bạn dễ thay đổi và gỡ lỗi bằng cách thiết kế kiến trúc với Scriptable Objects.
Những lời khuyên này đến từ Ryan Hipple , kỹ sư trưởng tại Schell Games , người có nhiều kinh nghiệm trong việc sử dụng Scriptable Objects để thiết kế kiến trúc trò chơi. Bạn có thể xem bài thuyết trình của Ryan Unite về Scriptable Objects tại đây ; chúng tôi cũng khuyên bạn nên xem buổi thuyết trình của kỹ sư Unity Richard Fine để có cái nhìn tổng quan tuyệt vời về Scriptable Objects .
ScriptableObjects là gì?
ScriptableObject là một lớp Unity có thể tuần tự hóa, cho phép bạn lưu trữ một lượng lớn dữ liệu dùng chung một cách độc lập với các thể hiện của script. Việc sử dụng ScriptableObjects giúp quản lý thay đổi và gỡ lỗi dễ dàng hơn. Bạn có thể xây dựng một hệ thống giao tiếp linh hoạt giữa các hệ thống khác nhau trong trò chơi của mình , giúp việc thay đổi và điều chỉnh chúng trong suốt quá trình sản xuất trở nên dễ dàng hơn, cũng như tái sử dụng các thành phần.
Ba trụ cột của kỹ thuật trò chơi
Sử dụng thiết kế dạng mô-đun:
- Tránh tạo ra các hệ thống phụ thuộc trực tiếp vào nhau. Ví dụ, một hệ thống kho đồ cần có khả năng giao tiếp với các hệ thống khác trong trò chơi của bạn, nhưng bạn không muốn tạo một liên kết cứng giữa chúng, vì điều đó sẽ gây khó khăn cho việc lắp ráp lại các hệ thống thành các cấu hình và mối quan hệ khác nhau.
- Hãy tạo các cảnh như những trang giấy trắng: tránh để dữ liệu tạm thời tồn tại giữa các cảnh. Mỗi khi bạn vào một cảnh, quá trình đó phải diễn ra suôn sẻ và tải lại ngay lập tức. Điều này cho phép bạn tạo ra những cảnh có hành vi độc đáo mà không có trong các cảnh khác, mà không cần phải chỉnh sửa mã nguồn.
- Thiết lập các Prefab sao cho chúng hoạt động độc lập. Tốt nhất là mọi prefab mà bạn kéo vào cảnh đều nên chứa đầy đủ chức năng của nó. Điều này giúp ích rất nhiều cho việc quản lý mã nguồn với các nhóm lớn, trong đó các cảnh là một danh sách các prefab và các prefab của bạn chứa các chức năng riêng lẻ. Bằng cách đó, hầu hết các thao tác kiểm tra (check-in) của bạn đều diễn ra ở cấp độ prefab, dẫn đến ít xung đột hơn trong cảnh.
- Mỗi thành phần cần tập trung giải quyết một vấn đề duy nhất. Điều này giúp việc ghép nhiều thành phần lại với nhau để tạo ra một thứ gì đó mới trở nên dễ dàng hơn.
Giúp việc thay đổi và chỉnh sửa các bộ phận trở nên dễ dàng hơn:
- Hãy tối ưu hóa trò chơi của bạn sao cho càng nhiều yếu tố dựa trên dữ liệu càng tốt. Khi bạn thiết kế hệ thống trò chơi của mình giống như những cỗ máy xử lý dữ liệu dưới dạng các lệnh, bạn có thể thực hiện các thay đổi đối với trò chơi một cách hiệu quả, ngay cả khi trò chơi đang chạy.
- Nếu hệ thống của bạn được thiết kế theo kiểu mô-đun và dựa trên các thành phần càng nhiều càng tốt, việc chỉnh sửa sẽ dễ dàng hơn, kể cả đối với các nghệ sĩ và nhà thiết kế của bạn. Ryan cho biết, nếu các nhà thiết kế có thể ghép các yếu tố lại với nhau trong trò chơi mà không cần yêu cầu một tính năng cụ thể nào – phần lớn nhờ vào việc triển khai các thành phần nhỏ, mỗi thành phần chỉ thực hiện một chức năng duy nhất – thì họ có thể kết hợp các thành phần đó theo nhiều cách khác nhau để tìm ra lối chơi/cơ chế mới. Anh gọi đó là “thiết kế phát sinh”.
- Điều tối quan trọng là nhóm của bạn có thể thực hiện các thay đổi đối với trò chơi trong quá trình chạy. Bạn càng có thể thay đổi trò chơi của mình trong quá trình chạy, bạn càng có thể tìm thấy sự cân bằng và giá trị, và nếu bạn có thể lưu lại trạng thái chạy của nó như Scriptable Objects, thì bạn đang ở một vị trí rất thuận lợi.
Giúp việc gỡ lỗi trở nên dễ dàng hơn:
Đây thực chất là một trụ cột phụ so với hai trụ cột đầu tiên. Trò chơi càng có tính mô-đun cao, việc kiểm tra từng phần riêng lẻ của nó càng dễ dàng hơn. Trò chơi của bạn càng dễ chỉnh sửa – càng có nhiều tính năng với cửa sổ Inspector riêng – thì việc gỡ lỗi càng dễ dàng hơn. Hãy đảm bảo bạn có thể xem trạng thái gỡ lỗi trong Trình kiểm tra và đừng bao giờ coi một tính năng là hoàn thiện cho đến khi bạn có kế hoạch cụ thể về cách thức gỡ lỗi nó.
Kiến trúc sư cho các biến
Một trong những điều đơn giản nhất bạn có thể xây dựng với ScriptableObjects là một biến độc lập, dựa trên tài sản. Dưới đây là một ví dụ cho kiểu dữ liệu FloatVariable, nhưng ví dụ này cũng có thể áp dụng cho bất kỳ kiểu dữ liệu nào khác có thể tuần tự hóa.
Mọi thành viên trong nhóm của bạn, bất kể trình độ kỹ thuật, đều có thể định nghĩa một biến trò chơi mới bằng cách tạo một tài nguyên FloatVariable mới. Bất kỳ MonoBehaviour hoặc ScriptableObject nào cũng có thể sử dụng một FloatVariable công khai thay vì một float công khai để tham chiếu đến giá trị được chia sẻ mới này.
Tuyệt vời hơn nữa, nếu một MonoBehaviour thay đổi Giá trị của một FloatVariable, các MonoBehaviour khác có thể thấy sự thay đổi đó. Điều này tạo ra một lớp truyền tin giữa các hệ thống không cần tham chiếu đến nhau.
[CreateAssetMenu]
public class FloatVariable : ScriptableObject
{
Giá trị float công khai;
}
Ví dụ: Điểm máu của người chơi
Một ví dụ về cách sử dụng điều này là điểm máu (HP) của người chơi. Trong trò chơi chỉ có một người chơi cục bộ, HP của người chơi có thể là một biến kiểu FloatVariable có tên là PlayerHP. Khi người chơi bị sát thương, lượng máu (PlayerHP) sẽ bị trừ đi, và khi người chơi hồi phục, lượng máu sẽ được cộng vào.
Giờ hãy tưởng tượng một Prefab thanh máu xuất hiện trong khung cảnh. Thanh hiển thị sức khỏe theo dõi biến PlayerHP để cập nhật trạng thái hiển thị. Nếu không thay đổi mã, nó hoàn toàn có thể trỏ đến một thứ khác, chẳng hạn như biến PlayerMP. Thanh máu không hề biết gì về người chơi trong cảnh, nó chỉ đọc dữ liệu từ cùng một biến mà người chơi ghi vào.
Khi đã thiết lập xong như vậy, việc thêm nhiều thứ khác để theo dõi PlayerHP sẽ rất dễ dàng. Hệ thống âm nhạc có thể thay đổi khi máu của người chơi giảm xuống, kẻ thù có thể thay đổi kiểu tấn công khi biết người chơi yếu, hiệu ứng trên màn hình có thể nhấn mạnh sự nguy hiểm của đòn tấn công tiếp theo, v.v. Điểm mấu chốt ở đây là kịch bản Người chơi không gửi thông báo đến các hệ thống này và các hệ thống này không cần biết về GameObject của người chơi. Bạn cũng có thể vào Trình kiểm tra khi trò chơi đang chạy và thay đổi giá trị của PlayerHP để kiểm tra mọi thứ.
Khi chỉnh sửa Giá trị của một FloatVariable, bạn nên sao chép dữ liệu của mình vào một giá trị thời gian chạy để không làm thay đổi giá trị được lưu trữ trên đĩa cho ScriptableObject. Nếu bạn làm vậy, MonoBehaviours sẽ truy cập RuntimeValue để ngăn việc chỉnh sửa InitialValue được lưu vào đĩa.
[CreateAssetMenu]
public class FloatVariable : ScriptableObject, ISerializationCallbackReceiver
{
Giá trị ban đầu (InitialValue) của một float công khai;
[Không được tuần tự hóa]
public float RuntimeValue;
public void OnAfterDeserialize()
{
Giá trị thời gian chạy = Giá trị ban đầu;
}
public void OnBeforeSerialize() { }
}
Kiến trúc sư sự kiện
Một trong những tính năng yêu thích của Ryan khi xây dựng dựa trên ScriptableObjects là hệ thống sự kiện. Kiến trúc sự kiện giúp phân chia mã nguồn thành các module bằng cách gửi thông điệp giữa các hệ thống không trực tiếp biết về nhau. Chúng cho phép các đối tượng phản hồi lại sự thay đổi trạng thái mà không cần liên tục theo dõi trạng thái đó trong một vòng lặp cập nhật.
Các ví dụ mã sau đây đến từ một hệ thống Sự kiện bao gồm hai phần: một GameEvent ScriptableObject và một GameEventListener MonoBehaviour. Các nhà thiết kế có thể tạo bất kỳ số lượng GameEvent nào trong dự án để thể hiện những thông điệp quan trọng cần gửi đi. Một GameEventListener chờ một GameEvent cụ thể được kích hoạt và phản hồi bằng cách gọi một UnityEvent (đây không phải là một sự kiện thực sự, mà giống như một lời gọi hàm được tuần tự hóa).
Ví dụ mã: GameEvent ScriptableObject
GameEvent ScriptableObject:
[CreateAssetMenu]
public class GameEvent : ScriptableObject
{
Danh sách riêng tư người nghe =
Danh sách mới ();
public void Raise()
{
for(int i = listeners.Count -1; i >= 0; i--)
listeners[i].OnEventRaised();
}
public void RegisterListener(GameEventListener listener)
{ listeners.Add(listener); }
public void UnregisterListener(GameEventListener listener)
{ listeners.Remove(listener); }
}
Ví dụ mã: GameEventListener
GameEventListener:
public class GameEventListener : MonoBehaviour
{
Sự kiện GameEvent công khai;
public UnityEvent Response;
private void OnEnable()
{ Event.RegisterListener(this); }
private void OnDisable()
{ Event.UnregisterListener(this); }
public void OnEventRaised()
{ Response.Invoke(); }
}

Hệ thống sự kiện xử lý cái chết của người chơi
Một ví dụ về điều này là việc xử lý cái chết của người chơi trong trò chơi. Đây là điểm mà rất nhiều khía cạnh thực thi có thể thay đổi, nhưng việc xác định nơi để lập trình toàn bộ logic lại khá khó khăn. Liệu đoạn mã của người chơi có nên kích hoạt giao diện Game Over hay thay đổi nhạc nền không? Kẻ thù có nên kiểm tra người chơi còn sống ở mỗi khung hình không? Hệ thống sự kiện cho phép chúng ta tránh được những sự phụ thuộc rắc rối như thế này.
Khi người chơi chết, tập lệnh Player sẽ gọi hàm Raise trong sự kiện OnPlayerDied. Tập lệnh của người chơi không cần biết hệ thống nào quan tâm đến nó vì nó chỉ là một thông báo phát sóng. Giao diện Game Over lắng nghe sự kiện OnPlayerDied và bắt đầu hoạt ảnh, một kịch bản camera có thể lắng nghe sự kiện này và bắt đầu làm mờ dần sang màu đen, và hệ thống âm nhạc có thể phản hồi bằng cách thay đổi nhạc. Chúng ta cũng có thể cho mỗi kẻ thù lắng nghe sự kiện OnPlayerDied, kích hoạt hoạt ảnh khiêu khích hoặc thay đổi trạng thái để trở lại hành vi nhàn rỗi.
Mẫu này giúp việc thêm các phản hồi mới khi người chơi chết trở nên vô cùng dễ dàng. Ngoài ra, việc kiểm tra phản hồi khi người chơi chết cũng rất dễ dàng bằng cách gọi hàm Raise trên sự kiện đó từ một đoạn mã kiểm thử hoặc một nút trong Inspector.
Hệ thống sự kiện mà họ xây dựng tại Schell Games đã phát triển thành một hệ thống phức tạp hơn nhiều và có các tính năng cho phép truyền dữ liệu và tự động tạo kiểu dữ liệu. Ví dụ này về cơ bản là điểm khởi đầu cho những gì họ sử dụng ngày nay.
Kiến trúc sư cho các hệ thống khác
Các đối tượng có thể lập trình (Scriptable Objects) không nhất thiết chỉ là dữ liệu. Hãy thử lấy bất kỳ hệ thống nào bạn triển khai trong MonoBehaviour và xem liệu bạn có thể chuyển triển khai đó sang ScriptableObject hay không. Thay vì sử dụng InventoryManager trên MonoBehaviour DontDestroyOnLoad, hãy thử đặt nó trên ScriptableObject.
Vì nó không gắn liền với khung cảnh, nên nó không có Transform và không nhận được các chức năng Update, nhưng nó sẽ duy trì trạng thái giữa các lần tải khung cảnh mà không cần bất kỳ khởi tạo đặc biệt nào. Thay vì sử dụng singleton, hãy sử dụng tham chiếu công khai đến đối tượng hệ thống kho đồ của bạn khi cần một script truy cập vào kho đồ. Điều này giúp việc thay thế kho đồ thử nghiệm hoặc kho đồ hướng dẫn dễ dàng hơn so với khi sử dụng kiểu dữ liệu đơn lẻ.
Ở đây bạn có thể hình dung một đoạn mã của người chơi lấy tham chiếu đến hệ thống Kho đồ. Khi người chơi xuất hiện, họ có thể yêu cầu Kho đồ cung cấp tất cả các vật phẩm đang sở hữu và tạo ra bất kỳ trang bị nào. Giao diện trang bị cũng có thể tham chiếu đến Kho đồ và lặp qua các vật phẩm để xác định vật phẩm cần lấy.