Làm cách nào tôi có thể tránh khớp nối tập lệnh chặt chẽ trong Unity?


24

Cách đây một thời gian, tôi bắt đầu làm việc với Unity và tôi vẫn phải vật lộn với vấn đề kịch bản được kết hợp chặt chẽ. Làm thế nào tôi có thể cấu trúc mã của mình để tránh vấn đề này?

Ví dụ:

Tôi muốn có hệ thống sức khỏe và cái chết trong các kịch bản riêng biệt. Tôi cũng muốn có các kịch bản đi bộ có thể hoán đổi cho nhau để cho phép tôi thay đổi cách di chuyển nhân vật người chơi (dựa trên vật lý, điều khiển quán tính như trong Mario so với điều khiển chặt chẽ, co giật như trong Super Meat Boy). Tập lệnh Health cần giữ một tham chiếu đến tập lệnh Death, để nó có thể kích hoạt phương thức Die () khi sức khỏe của người chơi đạt đến 0. Tập lệnh Death nên giữ một số tham chiếu đến tập lệnh đi bộ được sử dụng, để vô hiệu hóa việc đi bộ trên cái chết (I Mệt mỏi vì thây ma).

Tôi thường sẽ tạo các giao diện, như IWalking, IHealthIDeath, để tôi có thể thay đổi các yếu tố này một cách nhanh chóng mà không phá vỡ phần còn lại của mã. Tôi sẽ có chúng được thiết lập bởi một tập lệnh riêng biệt trên đối tượng người chơi, nói PlayerScriptDependancyInjector. Có lẽ đó là kịch bản sẽ có công IWalking, IHealthIDeathcác thuộc tính, do đó phụ thuộc có thể được thiết lập bởi các nhà thiết kế trình độ từ thanh tra bằng cách kéo và thả các kịch bản thích hợp.

Điều đó sẽ cho phép tôi đơn giản thêm các hành vi vào các đối tượng trò chơi một cách dễ dàng và không lo lắng về các phụ thuộc được mã hóa cứng.

Vấn đề trong Unity

Các vấn đề là trong Unity Tôi không thể tiếp xúc với giao diện trong thanh tra, và nếu tôi viết Thanh tra của riêng tôi, các tài liệu tham khảo wont được đăng, và đó là rất nhiều công việc không cần thiết. Đó là lý do tại sao tôi còn lại với việc viết mã kết hợp chặt chẽ. DeathKịch bản của tôi hiển thị một tham chiếu đến một InertiveWalkingkịch bản. Nhưng nếu tôi quyết định tôi muốn nhân vật người chơi kiểm soát chặt chẽ, tôi không thể kéo và thả TightWalkingtập lệnh, tôi cần thay đổi Deathtập lệnh. Đó là hút. Tôi có thể đối phó với nó, nhưng tâm hồn tôi khóc mỗi khi tôi làm điều gì đó như thế này.

Cái gì thay thế ưa thích cho các giao diện trong Unity? Làm sao để giải quyết vấn đề này? Tôi đã tìm thấy cái này , nhưng nó cho tôi biết những gì tôi đã biết, và nó không cho tôi biết làm thế nào để làm điều đó trong Unity! Điều này cũng thảo luận về những gì nên được thực hiện, không phải như thế nào, nó không giải quyết vấn đề khớp nối chặt chẽ giữa các kịch bản.

Nói chung, tôi cảm thấy những thứ đó được viết cho những người đến với Unity với nền tảng thiết kế trò chơi, những người chỉ học cách viết mã và có rất ít tài nguyên về Unity cho các nhà phát triển thông thường. Có cách nào chuẩn để cấu trúc mã của bạn trong Unity không, hay tôi phải tìm ra phương pháp của riêng mình?


1
The problem is that in Unity I can't expose interfaces in the inspectorTôi không nghĩ tôi hiểu ý của bạn khi "phơi bày giao diện trong thanh tra" bởi vì suy nghĩ đầu tiên của tôi là "tại sao không?"
jhocking

@jhocking hỏi các nhà phát triển đoàn kết. Bạn chỉ không nhìn thấy nó trong thanh tra. Nó chỉ không xuất hiện ở đó nếu bạn đặt nó làm thuộc tính công khai và tất cả các thuộc tính khác cũng vậy.
KL

1
ohhh bạn có nghĩa là sử dụng giao diện như là loại biến, không chỉ tham chiếu một đối tượng với giao diện đó.
jhocking

1
Bạn đã đọc bài viết ECS ban đầu? cowboyprogramming.com/2007/01/05/evolve-your-heirachy Điều gì về thiết kế RẮN? vi.wikipedia.org/wiki/SOLID_%28object-oriented_design%29
jzx

1
@jzx Tôi biết và sử dụng thiết kế RẮN và tôi là một fan hâm mộ lớn của các cuốn sách của Robert C. Martins, nhưng câu hỏi này là tất cả về cách thực hiện điều đó trong Unity. Và không, tôi đã không đọc bài báo đó, đọc nó ngay bây giờ, cảm ơn :)
KL

Câu trả lời:


14

Có một số cách bạn có thể làm việc để tránh khớp nối kịch bản chặt chẽ. Internal to Unity là một chức năng SendMessage mà khi được nhắm mục tiêu tại GameObject của Monobehaviour sẽ gửi thông điệp đó đến mọi thứ trên đối tượng trò chơi. Vì vậy, bạn có thể có một cái gì đó như thế này trong đối tượng sức khỏe của bạn:

[SerializeField]
private int _currentHealth;
public int currentHealth
{
    get { return _currentHealth;}
    protected set
    {
        _currentHealth = value;
        gameObject.SendMessage("HealthChanged", value, SendMessageOptions.DontRequireReceiver);
    }
}

[SerializeField]
private int _maxHealth;
public int maxHealth
{
    get { return _maxHealth;}
    protected set
    {
        _maxHealth = value;
        if (currentHealth > _maxHealth)
        {
            currentHealth = _maxHealth;
        }
    }
}

public void ChangeHealth(int deltaChange)
{
    currentHealth += deltaChange;
}

Điều này thực sự đơn giản nhưng sẽ hiển thị chính xác những gì tôi đang làm. Các tài sản ném tin nhắn như bạn sẽ một sự kiện. Kịch bản chết của bạn sau đó sẽ chặn tin nhắn này (và nếu không có gì để chặn nó, SendMessageOptions.DontRequireReceiver sẽ đảm bảo bạn không nhận được ngoại lệ) và thực hiện các hành động dựa trên giá trị sức khỏe mới sau khi được đặt. Thích như vậy:

void HealthChanged(int newHealth)
{
    if (newHealth <= 0)
    {
        Die();
    }
}

void Die ()
{
    Debug.Log ("I am undone!");
    DestroyObject (gameObject);
}

Không có sự đảm bảo về trật tự trong việc này, tuy nhiên. Theo kinh nghiệm của tôi, nó luôn đi từ hầu hết các tập lệnh trên GameObject đến hầu hết các tập lệnh, nhưng tôi sẽ không ngân hàng cho bất cứ điều gì bạn không thể tự quan sát.

Có một số giải pháp khác mà bạn có thể điều tra đó là xây dựng chức năng GameObject tùy chỉnh có các Thành phần và chỉ trả về các thành phần có giao diện cụ thể thay vì Loại, sẽ mất nhiều thời gian hơn nhưng có thể phục vụ tốt mục đích của bạn.

Ngoài ra, bạn có thể viết một trình soạn thảo tùy chỉnh để kiểm tra một đối tượng được gán cho một vị trí trong trình chỉnh sửa cho Giao diện đó trước khi thực sự gán (trong trường hợp này bạn sẽ gán tập lệnh như bình thường cho phép nó được tuần tự hóa, nhưng gây ra lỗi nếu nó không sử dụng đúng giao diện và từ chối chuyển nhượng). Tôi đã thực hiện cả hai điều này trước đây và có thể tạo mã đó nhưng sẽ cần một thời gian để tìm thấy nó trước.


5
SendMessage () giải quyết tình huống này và tôi sử dụng lệnh đó trong nhiều tình huống, nhưng một hệ thống tin nhắn không được nhắm mục tiêu như thế này sẽ kém hiệu quả hơn so với việc trực tiếp tham chiếu đến người nhận. Bạn nên cẩn thận dựa vào lệnh này cho tất cả các giao tiếp thông thường giữa các đối tượng.
jhocking

2
Tôi là một người hâm mộ cá nhân về việc sử dụng các sự kiện và đại biểu nơi các đối tượng Thành phần biết về các hệ thống lõi bên trong hơn nhưng các hệ thống cốt lõi không biết gì về các thành phần. Nhưng tôi không chắc chắn một trăm phần trăm đó là cách họ muốn câu trả lời của họ được thiết kế. Vì vậy, tôi đã đi với một cái gì đó trả lời làm thế nào để có được các kịch bản tách rời hoàn toàn.
Brett Satoru

1
Tôi cũng tin rằng có một số plugin có sẵn trong kho tài sản thống nhất có thể hiển thị giao diện và các cấu trúc lập trình khác không được Thanh tra Unity hỗ trợ, có thể đáng để tìm kiếm nhanh.
Matthew Pigram

7

Cá nhân tôi KHÔNG BAO GIỜ sử dụng SendMessage. Vẫn còn một sự phụ thuộc giữa các thành phần của bạn với SendMessage, nó chỉ được hiển thị rất kém và dễ bị hỏng. Sử dụng giao diện và / hoặc đại biểu thực sự loại bỏ nhu cầu sử dụng SendMessage bao giờ (chậm hơn, mặc dù đó không phải là vấn đề đáng lo ngại cho đến khi cần).

http://forum.unity3d.com/threads/free-vfw-full-set-of-drawers-savesystem-serialize-interfaces-generics-auto-props-delegates.266165/

Sử dụng công cụ của anh chàng này. Nó cung cấp một đống các công cụ biên tập như giao diện tiếp xúc và đại biểu AND. Có rất nhiều thứ như thế này, hoặc thậm chí bạn có thể làm cho riêng mình. Dù bạn làm gì, KHÔNG thỏa hiệp thiết kế của bạn vì thiếu hỗ trợ tập lệnh của Unity. Những thứ này nên ở trong Unity theo mặc định.

Nếu đây không phải là một tùy chọn, thay vào đó hãy kéo và thả các lớp monobehaviour và tự động chuyển chúng sang giao diện được yêu cầu. Đó là công việc nhiều hơn, và ít thanh lịch hơn, nhưng nó hoàn thành công việc.


2
Cá nhân tôi không muốn quá phụ thuộc vào plugin và thư viện cho những thứ cấp thấp như thế này. Có lẽ tôi hơi hoang tưởng, nhưng cảm thấy có quá nhiều rủi ro khi dự án của tôi bị phá vỡ vì mã của họ bị hỏng sau khi cập nhật Unity.
jhocking

Tôi đang sử dụng khung đó và cuối cùng đã dừng lại vì tôi có lý do rất chính đáng nhưng bồn rửa trong nhà bếp trong khi phá vỡ tính tương thích ngược [và loại bỏ một hoặc hai tính năng khá hữu ích])
Selali Adobor

6

Một cách tiếp cận dành riêng cho Unity xảy ra với tôi ban đầu GetComponents<MonoBehaviour>()là lấy danh sách các tập lệnh, sau đó chuyển các tập lệnh đó thành các biến riêng cho các giao diện cụ thể. Bạn muốn làm điều này trong Start () trên tập lệnh trình tiêm phụ thuộc. Cái gì đó như:

MonoBehaviour[] list = GetComponents<MonoBehaviour>();
for (int i = 0; i < list.Length; i++) {
    if (list[i] is IHealth) {
        Debug.Log("Found it!");
    }
}

Trong thực tế, với phương thức này, bạn thậm chí có thể có các thành phần riêng lẻ cho biết tập lệnh tiêm phụ thuộc là phụ thuộc của chúng là gì, bằng cách sử dụng lệnh typeof () và loại 'Type' . Nói cách khác, các thành phần cung cấp cho bộ tiêm phụ thuộc một danh sách các loại và sau đó bộ tiêm phụ thuộc trả về một danh sách các đối tượng của các loại đó.


Ngoài ra, bạn chỉ có thể sử dụng các lớp trừu tượng thay vì giao diện. Một lớp trừu tượng có thể thực hiện khá nhiều mục đích giống như một giao diện và bạn có thể tham chiếu nó trong Thanh tra.


Tôi không thể kế thừa từ nhiều lớp trong khi tôi có thể thực hiện nhiều giao diện. Đây có thể là một vấn đề, nhưng tôi đoán nó tốt hơn nhiều so với không có gì :) Cảm ơn câu trả lời của bạn!
KL

đối với chỉnh sửa - một khi tôi có danh sách các tập lệnh, làm thế nào để mã của tôi biết tập lệnh nào không thể chuyển sang giao diện nào?
KL

1
chỉnh sửa để giải thích điều đó quá
jhocking

1
Trong Unity 5, bạn có thể sử dụng GetComponent<IMyInterface>()GetComponents<IMyInterface>()trực tiếp, whch trả về (các) thành phần thực hiện nó.
S. Tarık Çetin

5

Theo kinh nghiệm của riêng tôi, nhà phát triển trò chơi theo truyền thống bao gồm một cách tiếp cận thực tế hơn so với nhà phát triển công nghiệp (với các lớp trừu tượng ít hơn). Một phần vì bạn muốn ưu tiên hiệu năng hơn khả năng bảo trì và cũng vì bạn ít có khả năng sử dụng lại mã của mình trong các bối cảnh khác (thường có sự kết hợp nội tại mạnh mẽ giữa đồ họa và logic trò chơi)

Tôi hoàn toàn hiểu mối quan tâm của bạn, đặc biệt là vì mối quan tâm về hiệu suất ít quan trọng hơn với các thiết bị gần đây, nhưng cảm giác của tôi là bạn sẽ phải phát triển các giải pháp của riêng mình nếu bạn muốn áp dụng các thực tiễn tốt nhất OOP công nghiệp vào phát triển trò chơi Unity (chẳng hạn như tiêm phụ thuộc, v.v ...).


2

Hãy xem IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), cho phép bạn hiển thị các giao diện trong trình chỉnh sửa, giống như bạn đề cập. Có thêm một chút hệ thống dây điện để làm so với khai báo biến thông thường, đó là tất cả.

public interface IPinballValue{
   void Add(int value);
}

[System.Serializable]
public class IPinballValueContainer : IUnifiedContainer<IPinballValue> {}

public class MyClassThatExposesAnInterfaceProperty : MonoBehaviour {
   [SerializeField]
   IPinballValueContainer value;
}

Ngoài ra, như một câu trả lời khác đề cập, sử dụng SendMessage không loại bỏ khớp nối. Thay vào đó, nó làm cho nó không bị lỗi khi biên dịch. Rất tệ - khi bạn mở rộng quy mô dự án của mình, nó có thể trở thành một cơn ác mộng cần duy trì.

Nếu bạn đang tìm cách tách mã của mình mà không sử dụng giao diện trong trình chỉnh sửa, bạn có thể sử dụng một hệ thống dựa trên sự kiện được gõ mạnh (hoặc thậm chí là một hệ thống được gõ lỏng lẻo, nhưng một lần nữa, khó bảo trì hơn). Tôi đã dựa trên bài viết này , nó rất tiện lợi như một điểm khởi đầu. Hãy chú ý đến việc tạo ra một loạt các sự kiện vì nó có thể kích hoạt GC. Thay vào đó, tôi đã giải quyết vấn đề với nhóm đối tượng, nhưng chủ yếu là vì tôi làm việc với thiết bị di động.


1

Nguồn: http://www.udellgames.com/posts/ultra-usiously-unity-snippet-developers-use-interfaces/

[SerializeField]
private GameObject privateGameObjectName;

public GameObject PublicGameObjectName
{
    get { return privateGameObjectName; }
    set
    {
        //Make sure changes to privateGameObjectName ripple to privateInterfaceName too
        if (privateGameObjectName != value)
        {
            privateInterfaceName = null;
            privateGameObjectName = value;
        }
    }
}

private InterfaceType privateInterfaceName;
public InterfaceType PublicInterfaceName
{
    get
    {
        if (privateInterfaceName == null)
        {
            privateInterfaceName = PublicGameObjectName.GetComponent(typeof(InterfaceType)) as InterfaceType;
        }
        return privateInterfaceName;
    }
    set
    {
        //Make sure changes to PublicInterfaceName ripple to PublicGameObjectName too
        if (privateInterfaceName != value)
        {
            privateGameObjectName = (privateInterfaceName as Component).gameObject;
            privateInterfaceName = value;
        }

    }
}

0

Tôi sử dụng một vài chiến lược khác nhau để khắc phục những hạn chế mà bạn đề cập.

Một trong những điều mà tôi không thấy được đề cập là sử dụng một cách MonoBehaviorhiển thị quyền truy cập vào các triển khai khác nhau của một giao diện nhất định. Ví dụ: tôi có một giao diện xác định cách thực hiện RaycastsIRaycastStrategy

public interface IRaycastStrategy 
{
    bool Cast(Ray ray, out RaycastHit raycastHit, float distance, LayerMask layerMask);
}

Sau đó, tôi triển khai nó trong các lớp khác nhau với các lớp như LinecastRaycastStrategySphereRaycastStrategyvà triển khai MonoBehavior RaycastStrategySelectorhiển thị một thể hiện duy nhất của mỗi loại. Một cái gì đó như RaycastStrategySelectionBehavior, được thực hiện một cái gì đó như:

public class RaycastStrategySelectionBehavior : MonoBehavior
{

    private readonly Dictionary<RaycastStrategyType, IRaycastStrategy> _raycastStrategies
        = new Dictionary<RaycastStrategyType, IRaycastStrategy>
        {
            { RaycastStrategyType.Line, new LineRaycastStrategy() },
            { RaycastStrategyType.Sphere, new SphereRaycastStrategy() }
        };

    public RaycastStrategyType StrategyType;


    public IRaycastStrategy RaycastStrategy
    {
        get
        {
            IRaycastStrategy raycastStrategy;
            if (!_raycastStrategies.TryGetValue(StrategyType, out raycastStrategy))
            {
                throw new InvalidOperationException("There is no registered implementation for the selected strategy");
            }
            return raycastStrategy;
        }
    }
}

Nếu việc triển khai có khả năng tham chiếu các hàm Unity trong các cấu trúc của chúng (hoặc chỉ để ở bên an toàn, tôi chỉ quên điều này khi nhập ví dụ này) Awakeđể tránh các vấn đề với việc gọi hàm tạo hoặc tham chiếu các hàm Unity trong trình soạn thảo hoặc bên ngoài chính chủ đề

Chiến lược này có một số nhược điểm, đặc biệt là khi bạn phải quản lý nhiều triển khai khác nhau đang thay đổi nhanh chóng. Các vấn đề với việc thiếu kiểm tra thời gian biên dịch có thể được trợ giúp bằng cách sử dụng trình chỉnh sửa tùy chỉnh, vì giá trị enum có thể được tuần tự hóa mà không cần bất kỳ công việc đặc biệt nào (mặc dù tôi thường từ bỏ điều này, vì việc triển khai của tôi không có xu hướng nhiều như vậy).

Tôi sử dụng chiến lược này vì tính linh hoạt mà nó mang lại cho bạn bằng cách dựa trên MonoBehaviors mà không thực sự buộc bạn phải thực hiện các giao diện bằng MonoBehaviors. Điều này mang lại cho bạn "tốt nhất của cả hai thế giới" vì Selector có thể được truy cập bằng cách sử dụng GetComponent, việc xê-ri hóa được Unity xử lý và Selector có thể áp dụng logic vào thời gian chạy để tự động chuyển đổi triển khai dựa trên các yếu tố nhất định.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.