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和使导弹沿地面盘旋的行为结合在一起,并制作出这种疯狂的新型旋转曲棍球-冰球-死亡武器。
我仍然需要返回并为此行为添加特定的支持,以确保其有效运行。但是因为我们做了这个通用的数据描述界面,所以他能够空想而出,将这个想法付诸于游戏,而我们程序员甚至不知道他一直在努力直到他过来说,“嘿,看在这很酷的事情上!” 并且因为它确实很棒,所以我很高兴为它添加更多强大的支持。