キャラクターのスキルと能力をコマンドとして作成することは良い習慣ですか?


11

私は、ユニークな攻撃スキルと、構築、修復などの他の能力を持つキャラクターで構成されるゲームを設計しています。プレイヤーは、そのようなキャラクターの複数を制御できます。

そのようなスキルや能力をすべて個別のコマンドに組み込むことを考えています。静的コントローラーは、これらすべてのコマンドを静的コマンドリストに登録します。静的リストは、ゲーム内のすべてのキャラクターの利用可能なすべてのスキルと能力で構成されます。そのため、プレイヤーがいずれかのキャラクターを選択し、UIのボタンをクリックして呪文を唱えたり能力を実行したりすると、ビューは静的コントローラーを呼び出して、リストから目的のコマンドをフェッチして実行します。

Unityでゲームを構築しているので、これが良いデザインであるかどうかはわかりません。すべてのスキルと能力を個別のコンポーネントとして作成し、それをゲーム内のキャラクターを表すGameObjectsに関連付けることができると思います。次に、UIはキャラクターのGameObject を保持してからコマンドを実行する必要があります。

私がデザインしているゲームのデザインと練習は何がいいですか?


良いようです!この関連する事実をそのまま捨てる:一部の言語では、各コマンドをそれ自体に対する関数にすることさえできます。入力を簡単に自動化できるため、これにはテストにとって素晴らしい利点があります。また、コールバック関数変数を別のコマンド関数に再割り当てすることで、コントロールの再バインドを簡単に行うことができます。
Anko

@Anko、すべてのコマンドを静的リストに入れる部分についてはどうですか?リストが膨大になる可能性があり、コマンドが必要になるたびに、コマンドの膨大なリストを照会する必要があるのではないかと心配しています。
キセノン

1
@xenonコードのこの部分でパフォーマンスの問題が発生することはほとんどありません。ユーザーインタラクションごとに1回だけ発生する可能性がある限り、パフォーマンスを著しく低下させるために非常に多くの計算を行う必要があります。
aaaaaaaaaaaa 2013年

回答:


17

TL; DR

この答えは少しおかしいです。しかし、それは、あなたがあなたの能力を「コマンド」として実装することについて話しているのを見ているからです。そのアプローチは有効ですが、もっと良い方法があります。多分あなたはすでに他の方法をやっています。もしそうなら、まあ。うまくいけば、他の人がそれが役に立つと思うでしょう。

以下のデータドリブンアプローチを参照して、追跡に進んでください。Jacob PennockのCustomAssetUilityをここで取得し、彼に関する投稿を読んでください。

Unityでの作業

他の人が述べたように、100-300のアイテムのリストをトラバースすることは、あなたが考えるほど大したことではありません。したがって、それが直感的なアプローチである場合は、それを実行してください。脳の効率を最適化します。しかし、@ Norguardが彼の回答で示したように、ディクショナリは、一定の時間の挿入と取得ができるため、その問題を簡単に解決するための簡単な方法です。あなたはおそらくそれを使うべきです。

Unity内でこれをうまく機能させるという点で、私の腸は、能力ごとに1つのMonoBehaviourは危険な道を進むことを教えてくれます。いずれかの能力が実行中に状態を維持する場合、その状態をリセットする方法を提供するために、その能力を管理する必要があります。コルーチンはこの問題を軽減しますが、そのスクリプトのすべての更新フレームでIEnumerator参照を管理しているため、不完全でスタックした状態ループを防ぐために、確実にアビリティをリセットする確実な方法があることを確認する必要があります。アビリティは、気づかれないうちに静かにゲームの安定性を台無しにし始めます。「もちろん私がやります!」あなたは「私は「良いプログラマ」です!」と言います。しかし、実際には、私たちはみな客観的にひどいプログラマーであり、最高のAI研究者やコンパイラーライターでさえも、常に物事を台無しにしています。

Unityでコマンドのインスタンス化と取得を実装するすべての方法のうち、2つを考えることができます。1つは問題なく、動脈瘤をもたらさず、もう1つはUNBOUNDED MAGICAL CREATIVITYを可能にします。ちょっと。

コード中心のアプローチ

1つは、ほぼコード内のアプローチです。私がお勧めするのは、各コマンドを、BaseCommand抽象クラスから継承するか、ICommandインターフェースを実装する単純なクラスにすることです(簡潔にするために、これらのコマンドは文字の能力のみであり、組み込むのは難しくありません)その他の用途)。このシステムは、各コマンドがICommandであり、パラメーターを取らないパブリックコンストラクターがあり、アクティブなときに各フレームを更新する必要があると想定しています。

抽象基本クラスを使用すればより簡単ですが、私のバージョンではインターフェースを使用しています。

MonoBehavioursが1つの特定の動作、または密接に関連する動作のシステムをカプセル化することが重要です。単純なC#クラスに効果的にプロキシする多くのMonoBehaviourがあっても問題ありませんが、自分自身もさまざまな種類のオブジェクトへの呼び出しを更新して、XNAゲームのように見え始めるところまで更新する場合があります。深刻な問題を抱えており、アーキテクチャを変更する必要があります。

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

これは完全に問題なく機能しますが、あなたはより良いことができます(また、a List<T>は時限能力を格納するための最適なデータ構造ではないため、a LinkedList<T>またはa が必要になる場合がありますSortedDictionary<float, T>)。

データ駆動型アプローチ

能力の効果を、パラメーター化できる論理的な動作にまで減らすことができる可能性があります。これはUnityが本当に構築された目的です。プログラマーとして、システムを設計します。このシステムを使用すると、ユーザーまたはデザイナーは、エディターで移動して操作し、さまざまな効果を生み出すことができます。これにより、コードの「リギング」が大幅に簡略化され、アビリティの実行のみに集中できます。ここでは、基本クラスまたはインターフェイスとジェネリックスを調整する必要はありません。すべて純粋にデータ駆動型になります(コマンドインスタンスの初期化も簡単になります)。

最初に必要なのは、自分の能力を説明できるScriptableObjectです。ScriptableObjectsは素晴らしいです。Unityのインスペクターでパブリックフィールドを設定できるという点でMonoBehavioursのように機能するように設計されており、それらの変更はディスクにシリアル化されます。ただし、これらはオブジェクトにアタッチされておらず、シーン内のゲームオブジェクトにアタッチしたり、インスタンス化したりする必要はありません。Unityの包括的なデータバケットです。マークされた基本タイプ、列挙、および単純なクラス(継承なし)をシリアル化でき[Serializable]ます。Unityで構造体をシリアル化することはできません。シリアル化により、インスペクターでオブジェクトフィールドを編集できます。覚えておいてください。

以下は、多くのことを実行しようとするScriptableObjectです。これをさらにシリアライズされたクラスとScriptableObjectsに分割することができますが、これはそれを実行する方法のアイデアを提供するだけのものです。通常、これはC#のような素敵なモダンなオブジェクト指向言語では醜く見えます。これは、これらの列挙型をすべて備えたC89のたわごとのように感じるからです。それら。そして、最初のフォーマットが必要なことを実行しない場合は、実行するまで追加を続けてください。フィールド名を変更しない限り、古いシリアル化されたアセットファイルはすべて機能します。

// 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(); 
}

ダメージセクションをSerializableクラスにさらに抽象化して、ダメージを与える、または回復する能力を定義し、1つの能力に複数のダメージタイプを含めることができます。複数のスクリプト可能なオブジェクトを使用し、ディスク上のさまざまな複雑な損傷構成ファイルを参照しない限り、唯一のルールは継承ではありません。

あなたはまだAbilityActivator MonoBehaviourを必要としていますが、彼はもう少し仕事をしています。

// 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)
    }
}

クールな部分

したがって、最初のアプローチのインターフェースと一般的なトリックはうまく機能します。しかし、Unityを最大限に活用するために、ScriptableObjectsはあなたが望む場所を手に入れます。Unityは、プログラマーに非常に一貫した論理的な環境を提供するという点で優れていますが、GameMaker、UDKなどから取得したデザイナーやアーティストのためのすべてのデータ入力機能も備えています。al。

先月、私たちのアーティストは、さまざまな種類のホーミングミサイルの動作を定義するはずのPowerable ScriptableObjectタイプを採用し、AnimationCurveと組み合わせて、ミサイルを地面にホバリングさせる動作と、このクレイジーな新しい回転ホッケーパックを作成しました死の武器。

まだ戻ってこの動作の特定のサポートを追加して、効率的に実行されるようにする必要があります。しかし、この一般的なデータ記述インターフェイスを作成したので、プログラマーがやって来て、「こんにちは、見てください。このクールなことで!」そして、それは明らかに素晴らしいものだったので、より強力なサポートを追加できることに興奮しています。


3

TL:DR-数百または数千の能力をリスト/配列に詰め込むことを考えている場合は、アクションが呼び出されるたびに、アクションが存在するかどうか、およびできるキャラクターがあるかどうかを確認します実行してから、以下をお読みください。

そうでない場合は、心配する必要はありません。
6文字/文字タイプと30の能力について話している場合、複雑さを管理するオーバーヘッドは、実際にすべてを山積みにダンプするだけではなく、より多くのコードとより多くの処理を必要とする可能性があるため、実際には何をしてもかまいません。並べ替え...

@eBusinessが、イベントディスパッチ中にパフォーマンスの問題が発生する可能性は低いと示唆しているのはまさにこれが理由です。画面上の100万頂点など

また、これは解決策ではなく同様の問題のより大きなセットを管理するための解決策です...

だが...

それは、ゲームをどれだけ大きくするか、同じスキルを共有するキャラクターの数、異なるキャラクター/異なるスキルの数に関係します。

スキルをキャラクターのコンポーネントにすることはできますが、キャラクターがあなたのコントロールに加わったり去ったりするときに(またはノックアウトされるなどして)、コマンドインターフェースからスキルを登録/登録解除することは、非常にStarCraftのような方法で、ホットキーとコマンドカード。

Unityのスクリプトを使用した経験はほとんどありませんが、言語としてのJavaScriptには非常に慣れています。
許可されている場合は、そのリストを単純なオブジェクトにしないでください。

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

そしてそれは次のように使われるかもしれません:

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();

Dave()。init関数は次のようになります。

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

Daveだけではない多くの人.Repair()がを持っているが、Daveが1つだけになることを保証できる場合は、それを次のように変更します。system.notify("register-ability", "dave:repair", this.Repair);

を使用してスキルを呼び出します system.notify("use-action", "dave:repair");

あなたが使っているリストがどんなものかはわかりません。(UnityScriptタイプシステムの観点から、およびコンパイル後の状況に関して)。

リストに詰め込むだけで計画している数百のスキルがある場合(現在使用できる文字に基づいて登録および登録解除するのではなく)、JS配列全体を繰り返し処理する(もう一度、それが彼らがやっていることである場合)クラス/オブジェクトのプロパティをチェックすることは、実行したいアクションの名前と一致しますが、これよりもパフォーマンスが低下します。

より最適化された構造がある場合は、これよりもパフォーマンスが高くなります。

しかし、どちらの場合でも、独自のアクションを制御するキャラクター(さらに一歩踏み出して、必要に応じてコンポーネント/エンティティにする)があり、さらに、最小限の反復を必要とする制御システムがあります(名前によるテーブルルックアップの実行)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.