Menjadikan keterampilan dan kemampuan karakter sebagai perintah, praktik yang baik?


11

Saya merancang untuk permainan yang terdiri dari karakter yang memiliki keterampilan ofensif yang unik dan kemampuan lain seperti membangun, memperbaiki, dll. Pemain dapat mengontrol banyak karakter tersebut.

Saya berpikir untuk menempatkan semua keterampilan dan kemampuan seperti itu ke dalam perintah individu. Kontroler statis akan mendaftarkan semua perintah ini ke daftar perintah statis. Daftar statis akan terdiri dari semua keterampilan dan kemampuan yang tersedia dari semua karakter dalam permainan. Jadi ketika seorang pemain memilih salah satu karakter dan mengklik tombol pada UI untuk membaca mantra atau melakukan kemampuan, tampilan akan memanggil pengontrol statis untuk mengambil perintah yang diinginkan dari daftar dan menjalankannya.

Namun saya tidak yakin apakah ini desain yang bagus mengingat saya sedang membangun game saya di Unity. Saya berpikir saya bisa membuat semua keterampilan dan kemampuan sebagai komponen individu, yang kemudian akan melekat pada GameObjects yang mewakili karakter dalam permainan. Maka UI perlu memegang GameObject dari karakter dan kemudian menjalankan perintah.

Apa yang akan menjadi desain dan praktik yang lebih baik untuk game yang saya rancang?


Sepertinya bagus! Hanya melempar fakta terkait ini di luar sana: Dalam beberapa bahasa, Anda bahkan dapat bertindak sejauh membuat setiap perintah berfungsi sendiri. Ini memiliki beberapa keuntungan luar biasa untuk pengujian, karena Anda dapat dengan mudah mengotomatiskan input. Juga, kontrol rebinding dapat dilakukan dengan mudah dengan menetapkan kembali variabel fungsi panggilan balik ke fungsi perintah yang berbeda.
Anko

@Anko, bagaimana dengan bagian di mana saya memiliki semua perintah dimasukkan ke dalam daftar statis? Saya khawatir bahwa daftar tersebut akan bertambah besar dan setiap kali ketika suatu perintah dibutuhkan, ia harus menanyakan daftar besar perintah tersebut.
xenon

1
@ xenon Anda sangat tidak mungkin melihat masalah kinerja di bagian kode ini. Sejauh sesuatu hanya dapat terjadi satu kali per interaksi pengguna, harus sangat intensif komputasi untuk membuat penyok yang nyata dalam kinerja.
aaaaaaaaaaaa

Jawaban:


17

TL; DR

Jawaban ini sedikit gila. Tapi itu karena saya melihat Anda sedang berbicara tentang menerapkan kemampuan Anda sebagai "Perintah," yang menyiratkan pola desain C ++ / Java / .NET, yang menyiratkan pendekatan kode-berat. Approah itu valid, tetapi ada cara yang lebih baik. Mungkin Anda sudah melakukan sebaliknya. Jika demikian, oh baiklah. Semoga orang lain merasakan manfaatnya jika itu masalahnya.

Lihatlah Pendekatan Data-Driven di bawah ini untuk memotong ke pengejaran. Dapatkan CustomAssetUility Jacob Pennock di sini dan baca postingnya tentang hal itu .

Bekerja Dengan Persatuan

Seperti yang telah disebutkan orang lain, melintasi daftar 100-300 item bukanlah masalah besar seperti yang Anda pikirkan. Jadi jika itu pendekatan intuitif untuk Anda, maka lakukan saja. Optimalkan untuk efisiensi otak. Tetapi Kamus, seperti yang diperlihatkan @Norguard dalam jawabannya , adalah cara mudah tanpa tenaga otak untuk menghilangkan masalah itu karena Anda mendapatkan penyisipan dan pengambilan waktu yang konstan. Anda mungkin harus menggunakannya.

Dalam hal membuat ini bekerja dengan baik dalam Unity, usus saya mengatakan kepada saya bahwa satu MonoBehaviour per kemampuan adalah jalur berbahaya untuk turun. Jika salah satu dari kemampuan Anda mempertahankan status seiring berjalannya waktu, Anda harus mengelola bahwa menyediakan cara untuk mengatur ulang keadaan itu. Coroutines mengatasi masalah ini, tetapi Anda masih mengelola referensi IEnumerator pada setiap kerangka pembaruan skrip itu, dan harus benar-benar memastikan Anda memiliki cara yang pasti untuk mengatur ulang kemampuan agar jangan sampai tidak lengkap dan macet dalam keadaan tidak terkunci kemampuan diam-diam mulai mengacaukan stabilitas game Anda ketika mereka luput dari perhatian. "Tentu saja aku akan melakukannya!" Anda berkata, "Saya seorang 'Programmer Baik'!". Tapi sungguh, Anda tahu, kita semua adalah programmer yang secara objektif mengerikan dan bahkan para peneliti dan penulis kompiler AI terbesar selalu mengacaukan semuanya.

Dari semua cara Anda dapat mengimplementasikan instantiation dan pengambilan perintah di Unity, saya dapat memikirkan dua: satu baik-baik saja dan tidak akan memberi Anda aneurisma, dan yang lainnya memungkinkan untuk KREATIVITAS MAGIS YANG TIDAK DITINGGALKAN . Semacam.

Pendekatan Kode-Sentris

Pertama adalah pendekatan sebagian besar dalam kode. Apa yang saya sarankan adalah bahwa Anda membuat setiap perintah kelas sederhana yang mewarisi dari BaseCommand mengurangi kelas atau mengimplementasikan antarmuka ICommand (saya berasumsi untuk singkatnya bahwa Perintah ini hanya akan menjadi kemampuan karakter, tidak sulit untuk menggabungkan penggunaan lainnya). Sistem ini mengasumsikan bahwa setiap perintah adalah perintah IC, memiliki konstruktor publik yang tidak memiliki parameter, dan mengharuskan memperbarui setiap frame saat aktif.

Segalanya lebih sederhana jika Anda menggunakan kelas dasar abstrak, tetapi versi saya menggunakan antarmuka.

Penting bahwa MonoBehaviour Anda merangkum satu perilaku tertentu, atau sistem perilaku yang berkaitan erat. Tidak apa-apa untuk memiliki banyak MonoBehaviour yang secara efektif hanya proxy ke kelas C # biasa, tetapi jika Anda menemukan diri Anda melakukannya juga dapat memperbarui panggilan ke semua jenis objek yang berbeda ke titik di mana ia mulai terlihat seperti permainan XNA, maka Anda ' kembali dalam masalah serius dan perlu mengubah arsitektur Anda.

// ICommand.cs
public interface ICommand
{
    public void Execute(AbilityActivator originator, TargetingInfo targets);
    public void Update();
    public bool IsActive { get; }
}


// CommandList.cs
// Attach this to a game object in your loading screen
public static class CommandList
{
    public static ICommand GetInstance(string key)
    {
        return commandDict[key].GetRef();
    }


    static CommandListInitializerScript()
    {
        commandDict = new Dictionary<string, ICommand>() {

            { "SwordSpin", new CommandRef<SwordSpin>() },

            { "BellyRub", new CommandRef<BellyRub>() },

            { "StickyShield", new CommandRef<StickyShield>() },

            // Add more commands here
        };
    }


    private class CommandRef<T> where T : ICommand, new()
    {
        public ICommand GetNew()
        {
            return new T();
        }
    }

    private static Dictionary<string, ICommand> commandDict;
}


// AbilityActivator.cs
// Attach this to your character objects
public class AbilityActivator : MonoBehaviour
{
    List<ICommand> activeAbilities = new List<ICommand>();

    void Update()
    {
        string activatedAbility = GetActivatedAbilityThisFrame();
        if (!string.IsNullOrEmpty(acitvatedAbility))
            ICommand command = CommandList.Get(activatedAbility).GetRef();
            command.Execute(this, this.GetTargets());
            activeAbilities.Add(command);
        }

        foreach (var ability in activeAbilities) {
            ability.Update();
        }

        activeAbilities.RemoveAll(a => !a.IsActive);
    }
}

Ini berfungsi dengan sangat baik, tetapi Anda bisa melakukan yang lebih baik (juga, List<T>bukan struktur data yang optimal untuk menyimpan kemampuan waktunya, Anda mungkin ingin a LinkedList<T>atau a SortedDictionary<float, T>).

Pendekatan Berbasis Data

Mungkin saja Anda bisa mengurangi efek kemampuan Anda menjadi perilaku logis yang dapat dijadikan parameter. Inilah tujuan Unity dibangun. Anda, sebagai seorang programmer, merancang sebuah sistem yang kemudian Anda atau perancang dapat gunakan dan manipulasi dalam editor untuk menghasilkan berbagai efek. Ini akan sangat menyederhanakan "kecurangan" kode, dan fokus secara eksklusif pada pelaksanaan suatu kemampuan. Tidak perlu menyulap kelas dasar atau antarmuka dan generik di sini. Itu semua akan murni berbasis data (yang juga menyederhanakan inisialisasi instance perintah).

Hal pertama yang Anda butuhkan adalah ScriptableObject yang dapat menggambarkan kemampuan Anda. ScriptableObjects luar biasa. Mereka dirancang untuk bekerja seperti MonoBehaviour di mana Anda dapat mengatur bidang publik mereka di inspektur Unity, dan perubahan itu akan diserialisasi ke disk. Namun, mereka tidak melekat pada objek apa pun dan tidak harus melekat pada objek game dalam adegan atau instantiated. Mereka adalah semua-menangkap ember data Unity. Mereka dapat membuat serial jenis dasar, enum, dan kelas sederhana (tanpa warisan) yang ditandai [Serializable]. Structs tidak dapat diserialisasi dalam Unity, dan serialisasi adalah apa yang memungkinkan Anda untuk mengedit bidang objek di inspektur, jadi ingatlah itu.

Berikut adalah ScriptableObject yang mencoba melakukan banyak hal. Anda dapat memecah ini menjadi lebih banyak kelas serial dan ScriptableObjects, tetapi ini seharusnya hanya memberi Anda gambaran tentang bagaimana cara melakukannya. Biasanya ini terlihat jelek dalam bahasa berorientasi objek modern bagus seperti C #, karena itu benar-benar terasa seperti omong kosong C89 dengan semua enum itu, tetapi kekuatan sebenarnya di sini adalah bahwa sekarang Anda dapat membuat segala macam kemampuan yang berbeda tanpa pernah menulis kode baru untuk mendukung mereka. Dan jika format pertama Anda tidak melakukan apa yang perlu Anda lakukan, terus tambahkan sampai itu. Selama Anda tidak mengubah nama bidang, semua file aset serial lama Anda masih akan berfungsi.

// CommandAbilityDescription.cs
public class CommandAbilityDecription : ScriptableObject
{

    // Identification and information
    public string displayName; // Name used for display purposes for the GUI
    // We don't need an identifier field, because this will actually be stored
    // as a file on disk and thus implicitly have its own identifier string.

    // Description of damage to targets

    // I put this enum inside the class for answer readability, but it really belongs outside, inside a namespace rather than nested inside a class
    public enum DamageType
    {
        None,
        SingleTarget,
        SingleTargetOverTime,
        Area,
        AreaOverTime,
    }

    public DamageType damageType;
    public float damage; // Can represent either insta-hit damage, or damage rate over time (depend)
    public float duration; // Used for over-time type damages, or as a delay for insta-hit damage

    // Visual FX
    public enum EffectPlacement
    {
        CenteredOnTargets,
        CenteredOnFirstTarget,
        CenteredOnCharacter,
    }

    [Serializable]
    public class AbilityVisualEffect
    {
        public EffectPlacement placement;
        public VisualEffectBehavior visualEffect;
    }

    public AbilityVisualEffect[] visualEffects;
}

// VisualEffectBehavior.cs
public abtract class VisualEffectBehavior : MonoBehaviour
{
    // When an artist makes a visual effect, they generally make a GameObject Prefab.
    // You can extend this base class to support different kinds of visual effects
    // such as particle systems, post-processing screen effects, etc.
    public virtual void PlayEffect(); 
}

Anda bisa mengabstraksi bagian Damage lebih lanjut menjadi kelas Serializable sehingga Anda bisa mendefinisikan kemampuan yang menangani kerusakan, atau menyembuhkan, dan memiliki beberapa tipe kerusakan dalam satu kemampuan. Satu-satunya aturan adalah tidak ada warisan kecuali jika Anda menggunakan beberapa objek skrip dan referensi file konfigurasi kerusakan kompleks pada disk.

Anda masih memerlukan AbilityActivator MonoBehaviour, tetapi sekarang dia melakukan sedikit pekerjaan lagi.

// AbilityActivator.cs
public class AbilityActivator : MonoBehaviour
{
    public void ActivateAbility(string abilityName)
    {
        var command = (CommandAbilityDescription) Resources.Load(string.Format("Abilities/{0}", abilityName));
        ProcessCommand(command);
    }

    private void ProcessCommand(CommandAbilityDescription command)
    {

        foreach (var fx in command.visualEffects) {
            fx.PlayEffect();
        }

        switch(command.damageType) {
            // yatta yatta yatta
        }

        // and so forth, whatever your needs require

        // You could even make a copy of the CommandAbilityDescription
        var myCopy = Object.Instantiate(command);

        // So you can keep track of state changes (ie: damage duration)
    }
}

Bagian TERTINGGI

Jadi antarmuka dan tipuan generik pada pendekatan pertama akan berfungsi dengan baik. Tetapi untuk benar-benar mendapatkan yang terbaik dari Unity, ScriptableObjects akan membawa Anda ke tempat yang Anda inginkan. Unity sangat bagus karena menyediakan lingkungan yang sangat konsisten dan logis untuk programmer, tetapi juga memiliki semua masukan data yang bagus untuk desainer dan artis yang Anda dapatkan dari GameMaker, UDK, et. Al.

Bulan lalu, artis kami mengambil jenis ScriptableObject powerup yang seharusnya mendefinisikan perilaku untuk berbagai jenis rudal homing, dikombinasikan dengan AnimationCurve dan perilaku yang membuat rudal melayang-layang di tanah, dan membuat pemintalan-hoki-keping baru yang gila ini senjata kematian.

Saya masih perlu kembali dan menambahkan dukungan spesifik untuk perilaku ini untuk memastikan itu berjalan secara efisien. Tetapi karena kami membuat antarmuka deskripsi data umum ini, ia dapat mengeluarkan ide ini dari udara kosong dan memasukkannya ke dalam permainan tanpa kami pemrogram, bahkan mengetahui bahwa ia sedang mencoba melakukannya sampai ia datang dan berkata, "Hai teman-teman, lihat pada hal keren ini! " Dan karena itu jelas luar biasa, saya bersemangat untuk pergi menambahkan dukungan yang lebih kuat untuk itu.


3

TL: DR - jika Anda berpikir tentang memasukkan ratusan atau ribuan kemampuan ke dalam daftar / array yang akan Anda ulangi, setiap kali ada tindakan yang dipanggil, untuk melihat apakah tindakan itu ada dan jika ada karakter yang bisa lakukan itu, lalu baca di bawah ini.

Jika tidak, maka jangan khawatir.
Jika Anda berbicara tentang 6 karakter / tipe-karakter dan mungkin 30 kemampuan, maka itu benar-benar tidak akan menjadi masalah apa yang Anda lakukan, karena overhead pengelolaan kompleksitas mungkin sebenarnya membutuhkan lebih banyak kode dan lebih banyak pemrosesan daripada hanya membuang semuanya dalam tumpukan dan penyortiran...

Itulah sebabnya @eBusiness menyarankan Anda untuk tidak melihat masalah kinerja selama pengiriman acara, karena kecuali Anda berusaha sangat keras untuk melakukannya, tidak ada banyak pekerjaan yang melelahkan di sini, dibandingkan dengan mengubah posisi 3- juta simpul di layar, dll ...

Selain itu, ini bukan solusinya , melainkan solusi untuk mengelola set masalah yang lebih besar ...

Tapi...

Semuanya tergantung pada seberapa besar Anda membuat game, berapa banyak karakter yang berbagi keterampilan yang sama, berapa banyak karakter yang berbeda / keterampilan yang berbeda, kan?

Memiliki keterampilan menjadi komponen karakter, tetapi meminta mereka mendaftar / tidak mendaftar dari antarmuka perintah ketika karakter bergabung atau meninggalkan kendali Anda (atau tersingkir / dll) masih masuk akal, dalam semacam StarCraft, dengan hotkey dan kartu perintah.

Saya memiliki pengalaman yang sangat, sangat sedikit dengan scripting Unity, tetapi saya sangat nyaman dengan JavaScript sebagai bahasa.
Jika mereka mengizinkannya, mengapa daftar itu tidak menjadi objek sederhana:

// Command interface wraps this
var registered_abilities = {},

    register = function (name, callback) {
        registered_abilities[name] = callback;
    },
    unregister = function (name) {
        registered_abilities[name] = null;
    },

    call = function (name,/*arr/undef*/params) {
        var callback = registered_abilities[name];
        if (callback) { callback(params); }
    },

    public_interface = {
        register : register,
        unregister : unregister,
        call : call
    };

return public_interface;

Dan itu bisa digunakan seperti:

var command_card = new CommandInterface();

// one-time setup
system.listen("register-ability",   command_card.register  );
system.listen("unregister-ability", command_card.unregister);
system.listen("use-action",         command_card.call      );

// init characters
var dave = new PlayerCharacter("Dave"); // Character Factory pulls out Dave + dependencies
dave.init();

Di mana fungsi Dave (). Init mungkin terlihat seperti:

// Inside of Dave class
init = function () {
    // other instance-level stuff ...

    system.notify("register-ability", "repair",  this.Repair );
    system.notify("register-ability", "science", this.Science);
},

die = function () {
    // other clean-up stuff ...

    system.notify("unregister-ability", "repair" );
    system.notify("unregister-ability", "science");
},

resurrect = function () { /* same idea as init */ };

Jika lebih banyak orang daripada yang dimiliki Dave .Repair(), tetapi Anda dapat menjamin bahwa hanya akan ada satu Dave, maka ubah saja menjadisystem.notify("register-ability", "dave:repair", this.Repair);

Dan panggil keterampilan dengan menggunakan system.notify("use-action", "dave:repair");

Saya tidak yakin seperti apa daftar yang Anda gunakan. (Dalam hal sistem tipe UnityScript, DAN dalam hal apa yang terjadi setelah kompilasi).

Saya mungkin dapat mengatakan bahwa jika Anda memiliki ratusan keterampilan yang Anda rencanakan hanya memasukkan ke dalam daftar (daripada mendaftar dan membatalkan pendaftaran, berdasarkan karakter yang saat ini Anda miliki), yang berulang melalui seluruh array JS (sekali lagi, jika itu yang mereka lakukan) untuk memeriksa properti kelas / objek, yang cocok dengan nama tindakan yang ingin Anda lakukan, akan menjadi lebih sedikit performan daripada ini.

Jika ada struktur yang lebih dioptimalkan, maka mereka akan lebih berkinerja daripada ini.

Tetapi dalam kedua kasus tersebut, sekarang Anda memiliki Karakter yang mengontrol tindakan mereka sendiri (selangkah lebih maju dan jadikan mereka komponen / entitas, jika Anda mau), DAN Anda memiliki sistem kontrol yang membutuhkan iterasi minimum (karena Anda baru saja melakukan pencarian tabel berdasarkan nama).

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.