Bagaimana saya bisa menghindari pemasangan skrip ketat di Unity?

24

Beberapa waktu yang lalu saya mulai bekerja dengan Unity dan saya masih bergumul dengan masalah skrip yang sangat erat. Bagaimana saya bisa menyusun kode saya untuk menghindari masalah ini?

Sebagai contoh:

Saya ingin memiliki sistem kesehatan dan kematian dalam skrip terpisah. Saya juga ingin memiliki skrip berjalan dipertukarkan yang berbeda yang memungkinkan saya untuk mengubah cara karakter pemain dipindahkan (berbasis fisika, kontrol inersia seperti di Mario versus kontrol ketat, berkedut seperti di Super Meat Boy). Skrip Kesehatan perlu memiliki referensi ke skrip Kematian, sehingga dapat memicu metode Die () ketika kesehatan pemain mencapai 0. Skrip Kematian harus menyimpan beberapa referensi untuk skrip berjalan yang digunakan, untuk menonaktifkan walk on death (I lelah dengan zombie).

Saya biasanya akan membuat antarmuka, seperti IWalking, IHealthdan IDeath, sehingga saya dapat mengubah elemen-elemen ini dengan cepat tanpa merusak sisa kode saya. Saya ingin mereka mengaturnya dengan skrip terpisah pada objek pemain, katakanlah PlayerScriptDependancyInjector. Mungkin skrip itu akan memiliki publik IWalking, IHealthdan IDeathatribut, sehingga dependensi dapat ditetapkan oleh desainer level dari inspektur dengan menyeret dan menjatuhkan skrip yang sesuai.

Itu akan memungkinkan saya untuk menambahkan perilaku ke objek game dengan mudah dan tidak khawatir tentang dependensi hard-coded.

Masalah di Unity

The masalah adalah bahwa dalam Persatuan saya tidak dapat mengekspos antarmuka dalam inspektur, dan jika saya menulis inspektur saya sendiri, referensi wont mendapatkan serial, dan itu banyak pekerjaan yang tidak perlu. Itu sebabnya saya pergi dengan menulis kode yang sangat erat. DeathSkrip saya memaparkan referensi ke InertiveWalkingskrip. Tetapi jika saya memutuskan saya ingin karakter pemain untuk mengontrol dengan ketat, saya tidak bisa hanya drag and drop TightWalkingscript, saya perlu mengubah Deathskrip. Itu menyebalkan. Saya dapat menghadapinya, tetapi jiwa saya menangis setiap kali saya melakukan hal seperti ini.

Apa alternatif yang disukai untuk antarmuka di Unity? Bagaimana saya memperbaiki masalah ini? Saya menemukan ini , tetapi itu memberi tahu saya apa yang sudah saya ketahui, dan itu tidak memberi tahu saya bagaimana melakukannya di Unity! Ini juga membahas apa yang harus dilakukan, bukan bagaimana, itu tidak membahas masalah kopling ketat antar skrip.

Secara keseluruhan, saya merasa itu ditulis untuk orang-orang yang datang ke Unity dengan latar belakang desain game yang baru belajar cara membuat kode, dan ada sangat sedikit sumber daya di Unity untuk pengembang reguler. Apakah ada cara standar untuk menyusun kode Anda di Unity, atau apakah saya harus mencari tahu metode saya sendiri?

KL
sumber
1
The problem is that in Unity I can't expose interfaces in the inspectorSaya rasa saya tidak mengerti apa yang Anda maksud dengan "mengekspos antarmuka di inspektur" karena pikiran pertama saya adalah "mengapa tidak?"
jhocking
@jhocking bertanya pada devs persatuan. Anda hanya tidak melihatnya di inspektur. Itu tidak muncul di sana jika Anda menetapkannya sebagai atribut publik, dan semua atribut lainnya melakukannya.
KL
1
ohhh maksudmu menggunakan antarmuka sebagai jenis variabel, bukan hanya mereferensikan objek dengan antarmuka itu.
jhocking
1
Sudahkah Anda membaca artikel ECS asli? cowboyprogramming.com/2007/01/05/evolve-your-heirachy Bagaimana dengan desain SOLID? en.wikipedia.org/wiki/
KONSOLIDASI_%28object
1
@ jzx Saya tahu dan menggunakan desain SOLID dan saya penggemar berat buku Robert C. Martins, tapi pertanyaan ini adalah semua tentang bagaimana melakukannya di Unity. Dan tidak, saya tidak membaca artikel itu, membacanya sekarang, terima kasih :)
KL

Jawaban:

14

Ada beberapa cara yang bisa Anda lakukan untuk menghindari pemasangan skrip yang ketat. Internal to Unity adalah fungsi SendMessage yang ketika ditargetkan pada GameObject Monobehaviour mengirimkan pesan itu ke semua yang ada di objek game. Jadi Anda mungkin memiliki sesuatu seperti ini di objek kesehatan Anda:

[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;
}

Ini sangat disederhanakan tetapi harus menunjukkan dengan tepat apa yang saya maksudkan. Properti yang melempar pesan seperti Anda akan menghadiri suatu acara. Skrip kematian Anda akan mencegat pesan ini (dan jika tidak ada yang mencegatnya, SendMessageOptions.DontRequireReceiver akan menjamin Anda tidak menerima pengecualian) dan melakukan tindakan berdasarkan nilai kesehatan baru setelah ditetapkan. Seperti itu:

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

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

Namun, tidak ada jaminan pemesanan. Dalam pengalaman saya, selalu ada skrip paling atas pada GameObject ke skrip terbawah, tapi saya tidak akan mengandalkan apa pun yang tidak bisa Anda amati sendiri.

Ada solusi lain yang dapat Anda selidiki yaitu membangun fungsi GameObject khusus yang mendapatkan Komponen dan hanya mengembalikan komponen yang memiliki antarmuka spesifik alih-alih Tipe, itu akan memakan waktu sedikit lebih lama tetapi dapat melayani tujuan Anda dengan baik.

Selain itu, Anda dapat menulis editor khusus yang memeriksa objek yang ditugaskan ke posisi di editor untuk Antarmuka itu sebelum benar-benar melakukan tugas (dalam hal ini Anda akan menetapkan skrip seperti biasa memungkinkan serialisasi, tetapi melemparkan kesalahan jika tidak menggunakan antarmuka yang benar dan menolak penugasan). Saya telah melakukan kedua hal ini sebelumnya dan dapat menghasilkan kode itu tetapi akan membutuhkan waktu untuk menemukannya terlebih dahulu.

Brett Satoru
sumber
5
SendMessage () memang menangani situasi ini dan saya menggunakan perintah itu dalam banyak situasi, tetapi sistem pesan yang tidak ditargetkan seperti ini kurang efisien daripada langsung memiliki referensi ke penerima. Anda harus berhati-hati mengandalkan perintah ini untuk semua komunikasi rutin antar objek.
jhocking
2
Saya penggemar pribadi penggunaan acara dan delegasi di mana objek Komponen tahu tentang sistem inti yang lebih internal tetapi sistem inti tidak tahu apa-apa tentang potongan-potongan komponen. Tetapi saya tidak seratus persen yakin itulah cara mereka menginginkan jawaban mereka dirancang. Jadi saya pergi dengan sesuatu yang menjawab bagaimana mendapatkan skrip yang tidak terpisahkan sepenuhnya.
Brett Satoru
1
Saya juga percaya bahwa ada beberapa plugin yang tersedia di unity asset store yang dapat mengekspos antarmuka dan konstruksi pemrograman lainnya yang tidak didukung oleh Unity Inspector, mungkin perlu dilakukan pencarian cepat.
Matius Pigram
7

Saya pribadi TIDAK PERNAH menggunakan SendMessage. Masih ada ketergantungan antara komponen Anda dengan SendMessage, hanya saja tampilannya sangat buruk dan mudah rusak. Menggunakan antarmuka dan / atau delegasi benar-benar menghilangkan kebutuhan untuk menggunakan SendMessage (yang lebih lambat, meskipun itu seharusnya tidak menjadi masalah sampai perlu).

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

Gunakan barang orang ini. Ini menyediakan banyak hal editor seperti antarmuka yang terbuka, DAN delegasi. Ada banyak barang di luar sana seperti ini, atau Anda bahkan dapat membuatnya sendiri. Apa pun yang Anda lakukan, JANGAN kompromi desain Anda karena kurangnya dukungan skrip Unity. Hal-hal ini harus dalam Unity secara default.

Jika ini bukan opsi, seret dan jatuhkan kelas monobehaviour sebagai gantinya dan secara dinamis melemparkan mereka ke antarmuka yang diperlukan. Ini lebih banyak pekerjaan, dan kurang elegan, tetapi itu menyelesaikan pekerjaan.

Ben
sumber
2
Secara pribadi saya curiga menjadi terlalu bergantung pada plugin dan perpustakaan untuk hal-hal tingkat rendah seperti ini. Saya mungkin menjadi sedikit paranoid, tetapi rasanya terlalu berisiko mengambil proyek saya karena kode mereka rusak setelah pembaruan Unity.
jhocking
Saya menggunakan kerangka itu dan akhirnya berhenti karena saya punya alasan yang sangat @jhocking menyebutkan menggerogoti pikiran saya (dan itu tidak membantu bahwa dari satu pembaruan ke yang lain, ia pergi dari editor pembantu ke sistem yang mencakup semuanya tapi dapur tenggelam sementara melanggar kompatibilitas ke belakang [dan menghapus satu atau dua fitur yang agak berguna])
Selali Adobor
6

Pendekatan Unity-spesifik yang terjadi pada saya akan awalnya GetComponents<MonoBehaviour>()untuk mendapatkan daftar skrip, dan kemudian melemparkan skrip-skrip tersebut ke dalam variabel pribadi untuk antarmuka spesifik. Anda ingin melakukan ini di Mulai () pada skrip injector ketergantungan. Sesuatu seperti:

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

Bahkan, dengan metode ini Anda bahkan bisa meminta masing-masing komponen memberi tahu skrip ketergantungan injeksi, dengan menggunakan perintah typeof () dan tipe 'Type' . Dengan kata lain, komponen memberikan injector dependensi daftar jenis, dan kemudian injector dependensi mengembalikan daftar objek jenis tersebut.


Atau, Anda bisa menggunakan kelas abstrak alih-alih antarmuka. Kelas abstrak dapat mencapai tujuan yang hampir sama dengan antarmuka, dan Anda dapat merujuknya pada Inspektur.

jhocking
sumber
Saya tidak bisa mewarisi dari beberapa kelas sementara saya bisa mengimplementasikan banyak antarmuka. Ini bisa menjadi masalah, tapi saya rasa ini jauh lebih baik daripada tidak sama sekali :) Terima kasih atas jawaban Anda!
KL
Adapun edit - setelah saya memiliki daftar skrip, bagaimana kode saya tahu skrip mana yang tidak bisa ke antarmuka yang mana?
KL
1
diedit untuk menjelaskan itu juga
jhocking
1
Di Unity 5, Anda dapat menggunakan GetComponent<IMyInterface>()dan GetComponents<IMyInterface>()secara langsung, yang mengembalikan komponen yang mengimplementasikannya.
S. Tarık Çetin
5

Dari pengalaman saya sendiri, game dev secara tradisional melibatkan pendekatan yang lebih pragmatis daripada pengembang industri (dengan lapisan abstraksi yang lebih sedikit). Sebagian karena Anda ingin mengunggulkan kinerja daripada pemeliharaan, dan juga karena Anda cenderung menggunakan kembali kode Anda dalam konteks lain (biasanya ada penggabungan intrinsik yang kuat antara grafik dan logika permainan)

Saya benar-benar memahami kekhawatiran Anda, terutama karena masalah kinerja kurang penting dengan perangkat terbaru, tetapi perasaan saya adalah bahwa Anda harus mengembangkan solusi sendiri jika Anda ingin menerapkan praktik terbaik OOP industri untuk pengembangan game Unity (seperti injeksi ketergantungan, dll ...).

sdabet
sumber
2

Lihatlah IUnified ( http://u3d.as/content/wounded-wolf/iunified/5H1 ), yang memungkinkan Anda mengekspos antarmuka di editor, seperti yang Anda sebutkan. Ada sedikit lebih banyak kabel yang harus dilakukan dibandingkan dengan deklarasi variabel normal, itu saja.

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

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

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

Selain itu, seperti jawaban lain menyebutkan, menggunakan SendMessage tidak menghapus kopling. Sebaliknya itu membuatnya tidak keluar pada saat kompilasi. Sangat buruk - saat Anda meningkatkan proyek Anda, itu bisa menjadi mimpi buruk untuk dipertahankan.

Jika Anda lebih lanjut mencari untuk memisahkan kode Anda tanpa menggunakan antarmuka di editor, Anda dapat menggunakan sistem berbasis peristiwa yang diketik dengan kuat (atau bahkan yang diketik dengan longgar sebenarnya, tetapi sekali lagi, lebih sulit untuk dipelihara). Saya mendasarkan tulisan saya pada posting ini , yang sangat nyaman sebagai titik awal. Berhati-hatilah dalam menciptakan banyak acara karena dapat memicu GC. Saya mengatasi masalah dengan kumpulan objek, tapi itu karena saya bekerja dengan ponsel.

ADB
sumber
1

Sumber: http://www.udellgames.com/posts/ultra-useful-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;
        }

    }
}
Louis Hong
sumber
0

Saya menggunakan beberapa strategi berbeda untuk mengatasi keterbatasan yang Anda sebutkan.

Salah satu yang saya tidak lihat disebutkan adalah menggunakan MonoBehavioryang memperlihatkan akses ke berbagai implementasi dari antarmuka yang diberikan. Sebagai contoh, saya memiliki antarmuka yang mendefinisikan bagaimana Raycast diimplementasikan bernamaIRaycastStrategy

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

Saya kemudian mengimplementasikannya di kelas yang berbeda dengan kelas suka LinecastRaycastStrategydan SphereRaycastStrategydan mengimplementasikan MonoBehavior RaycastStrategySelectoryang memperlihatkan contoh tunggal dari setiap jenis. Sesuatu seperti RaycastStrategySelectionBehavior, yang diimplementasikan sesuatu seperti:

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;
        }
    }
}

Jika implementasinya cenderung merujuk fungsi Unity dalam konstruksi mereka (atau hanya untuk berada di sisi yang aman, saya hanya lupa ini ketika mengetikkan contoh ini) digunakan Awakeuntuk menghindari masalah dengan memanggil konstruktor atau merujuk fungsi Unity di editor atau di luar main benang

Strategi ini memang memiliki beberapa kelemahan, terutama ketika Anda harus mengelola banyak implementasi berbeda yang berubah dengan cepat. Masalah dengan kurangnya waktu kompilasi dapat dibantu dengan menggunakan editor khusus, karena nilai enum dapat diserialisasi tanpa ada pekerjaan khusus (meskipun saya biasanya melupakan ini, karena implementasi saya cenderung tidak sebanyak itu).

Saya menggunakan strategi ini karena fleksibilitas yang diberikannya kepada Anda dengan didasarkan pada MonoBehaviors tanpa benar-benar memaksa Anda untuk mengimplementasikan antarmuka menggunakan MonoBehaviors. Ini memberi Anda "yang terbaik dari kedua dunia" karena Selector dapat diakses menggunakan GetComponent, serialisasi semua ditangani oleh Unity dan Selector dapat menerapkan logika pada saat run-time untuk secara otomatis beralih implementasi berdasarkan faktor-faktor tertentu.

Selali Adobor
sumber