将角色的技能和能力作为命令,很好的做法?


11

我正在设计一款包含具有独特进攻技能和其他能力(例如建造,修理等)的角色的游戏。玩家可以控制多个这样的角色。

我正在考虑将所有这些技能和能力放到单独的命令中。静态控制器会将所有这些命令注册到静态命令列表中。静态列表将包含游戏中所有角色的所有可用技能和能力。因此,当玩家选择一个角色并单击UI上的按钮以投射咒语或执行一项功能时,View会调用静态控制器从列表中获取所需命令并执行该命令。

但是,鉴于我要在Unity中构建游戏,因此我不确定这是否是一个好的设计。我想我可以将所有技能和能力作为独立的组件,然后将其附加到代表游戏角色的GameObjects上。然后,UI将需要保留角色的GameObject,然后执行命令。

对于我正在设计的游戏,什么是更好的设计和实践?


看起来不错!只需将相关事实抛诸脑后:在某些语言中,您甚至可以使每个命令本身成为一个函数。对于测试而言,这具有一些很棒的优点,因为您可以轻松地自动化输入。此外,通过将回调函数变量重新分配给其他命令函数,可以轻松完成控件重新绑定。
Anko 2013年

@Anko,将所有命令放入静态列表的那部分呢?我担心该列表可能会变得庞大,并且每当需要一个命令时,它都必须查询庞大的命令列表。
氙气

1
@xenon您不太可能在代码的这一部分看到性能问题。就每次用户交互而言,某件事只能发生一次,因此必须占用大量计算资源,才能显着降低性能。
aaaaaaaaaaaa 2013年

Answers:


17

TL; DR

这个答案有点疯狂。但这是因为我看到您正在谈论将您的功能实现为“命令”,这意味着C ++ / Java / .NET设计模式,这意味着代码繁重的方法。这种方法是有效的,但是有更好的方法。也许您已经在做其他事情了。如果是这样,那很好。希望其他情况下会有用。

请看下面的数据驱动方法。在此处获取Jacob Pennock的CustomAssetUility 并阅读其相关文章

与Unity合作

就像其他人提到的那样,遍历100-300个项目的列表并不像您想的那么重要。因此,如果这对您来说是一种直观的方法,那就去做。优化大脑效率。但是,正如@Norguard在他的回答中所展示的,Dictionary 是消除该问题的简便方法,因为您可以进行固定时间的插入和检索。您可能应该使用它。

关于如何在Unity中很好地完成这项工作,我的直觉告诉我,每项能力一个MonoBehaviour就是失败的危险之路。如果您的某项异能随着时间的流逝而保持其执行状态,则您需要管理该能力以提供一种重置该状态的方法。协程缓解了此问题,但是您仍在该脚本的每个更新帧上管理IEnumerator引用,并且必须绝对确保您有一种可靠的方式来重置功能,以免不完整和陷入状态循环技能在不被注意时会悄悄地开始破坏游戏的稳定性。“我当然会那样做!” 你说:“我是个'好程序员'!” 但实际上,您知道,我们都是客观上​​糟糕的程序员,甚至最伟大的AI研究人员和编译器编写人员始终都在费劲。

在Unity中可以实现命令实例化和检索的所有方式中,我可以想到两种:一种很好,不会给您动脉瘤,另一种可以实现无限制的神奇创造力。有点。

以代码为中心的方法

首先是一种大部分采用代码的方法。我的建议是让每个命令成为一个简单的类,该类可以继承自BaseCommand abtract类,也可以实现ICommand接口(为简洁起见,我假设这些命令仅是字符功能,因此合并起来并不难其他用途)。该系统假定每个命令都是ICommand,具有不带任何参数的公共构造函数,并且需要在每个框架处于活动状态时对其进行更新。

如果使用抽象基类,事情会更简单,但是我的版本使用接口。

重要的是,您的MonoBehaviours封装一个特定的行为或一系列密切相关的行为。可以有很多MonoBehaviours可以有效地代理普通的C#类,但是如果您发现自己也这样做,则可能会将对各种不同对象的调用更新到看起来像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很棒。它们的设计类似于MonoBehaviours,因为您可以在Unity的检查器中设置其公共字段,并且这些更改将序列化到磁盘上。但是,它们不附加到任何对象,也不必附加到场景中或实例化的游戏对象。它们是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(); 
}

您可以将“损害”部分进一步抽象为“可序列化”类,以便可以定义造成伤害或治愈的异能,并且在一个异能中具有多种伤害类型。唯一的规则是没有继承,除非您使用多个可编写脚本的对象并引用磁盘上不同的复杂损坏配置文件。

您仍然需要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等获得的设计师和美术师提供了所有数据输入功能。等

上个月,我们的艺术家采用了一种能增强动力的ScriptableObject类型,该类型可以定义不同种类的寻的导弹的行为,并将其与AnimationCurve和使导弹沿地面盘旋的行为结合在一起,并制作出这种疯狂的新型旋转曲棍球-冰球-死亡武器。

我仍然需要返回并为此行为添加特定的支持,以确保其有效运行。但是因为我们做了这个通用的数据描述界面,所以他能够空想而出,将这个想法付诸于游戏,而我们程序员甚至不知道他一直在努力直到他过来说,“嘿,看在这很酷的事情上!” 并且因为它确实很棒,所以我很高兴为它添加更多强大的支持。


3

TL:DR-如果您正在考虑将成百上千的能力填充到列表/数组中,然后反复进行遍历,那么每次调用一个动作时,都要查看该动作是否存在以及是否有一个角色可以执行它,然后阅读下面的内容。

如果没有,那就不用担心。
如果您要谈论的是6个字符/字符类型,也许是30种能力,那么您实际上做什么都没关系,因为管理复杂性的开销实际上可能需要更多的代码和更多的处理,而不仅仅是将所有内容都堆放并排序...

这就是为什么@eBusiness建议您不太可能在事件发送过程中看到性能问题,因为除非您非常努力地进行操作,否则与转换3-的位置相比,这里没有太多的繁琐工作屏幕上有1百万个顶点,等等。

而且,这不是解决方案,而是用于管理更多类似问题的解决方案 ...

但...

这全都取决于您制作游戏的规模,有多少个角色共享相同的技能,有多少个不同的角色/不同的技能,对吗?

技能是角色的组成部分,但是当角色加入或离开控件(或被敲除/等)时,让它们在命令界面中注册/注销仍然很有意义,这在星际争霸中非常有用,可以通过热键和命令卡。

我对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,那么只需将其更改为system.notify("register-ability", "dave:repair", this.Repair);

并通过使用 system.notify("use-action", "dave:repair");

我不确定您使用的列表是什么样的。(就UnityScript类型系统而言,以及就后期编译而言而言)。

我可能会说,如果您打算将数百种技能计划为仅填充到列表中(而不是根据当前可用的字符进行注册和注销),则遍历整个JS数组(同样,如果这就是他们正在执行的操作),则要检查与您要执行的操作的名称相匹配的类/对象的属性,其性能将不如此。

如果有更优化的结构,那么它们的性能将会更高。

但是无论哪种情况,现在都有角色来控制自己的动作(如果愿意,可以进一步采取行动,使其成为组件/实体),并且您的控制系统所需的迭代次数最少(因为您只是按名称进行表格查找)。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.