0

这本身有一个游戏开发项目,但它实际上是关于编码和将数据映射到其他数据片段。这就是为什么我决定在这里发布它。

我用于外部库存项目数据存储的格式:

[ID:IT_FO_TROUT]
[Name:Trout]
[Description:A raw trout.]
[Value:10]

[3DModel:null]
[InventoryIcon:trout]

[Tag:Consumable]
[Tag:Food]
[Tag:Stackable]

[OnConsume:RestoreHealth(15)]
[OnConsume:RestoreFatigue(15)]

问题集中在最后 2 个OnConsume属性上。基本上,这两个属性意味着当物品被消耗时,消费者的健康会增加 15 点,他的疲劳也会增加。这在后台调用了 2 种不同的方法:

void RestoreHealth(Character Subject, int Amount);
void RestoreFatigue(Character Subject, int Amount);

您将如何将这些方法映射到它们的文件内字符串对应项?这是我想到的:

  1. 每次消费一个项目时,都会将一个字符串列表(事件)传递给一个项目事件管理器。管理器解析每个字符串并调用适当的方法。非常容易设置,并且由于这不是经常发生的操作,因此对性能的影响可能不会很大(字符串的大小也会很小(最多 10-15 个字符),并且在 O(n) 时间内解析)。

  2. 每个库存项目(类)在初始化时解析字符串事件一次且仅一次。每个字符串事件都通过字典。就性能而言,这是我能想到的最有效的方法,但它使做其他事情变得极其困难:字典中的所有值都必须是同一类型的委托。这意味着我无法保留

    a) 恢复健康(int)

    b) SummonMonster(位置,计数)

    在同一个字典中,并且必须为每种可调用方法设置一个新的数据结构。这是一项巨大的工作量。

想到了一些改进这两种方法的方法:

  1. 我可以在Item 事件管理器中使用某种临时缓存,这样一个项目的OnConsume事件就不会被解析两次?不过,我可能会遇到与我在 2) 中遇到的问题相同的问题,因为缓存必须是map<InventoryItem,List<delegate>>.

  2. .NET 库中的哈希表数据结构允许任何类型的对象在任何给定时间成为键和/或值(与字典不同)。我可以使用它并将字符串 A 映射到委托 X,同时也将字符串 B 映射到 同一结构中的 Y 委托。为什么我不应该这样做?你能预见这种方法会带来什么麻烦吗?

我也在思考反思的方式,但我并不完全有经验。而且我很确定每次解析字符串都会更快。

编辑

我的最终解决方案,考虑到 Alexey Raga 的回答。为每种事件使用接口。

public interface IConsumeEvent    
{    
    void ApplyConsumeEffects(BaseCharacter Consumer);   
}

示例实施者(特定事件):

public class RestoreHealthEvent : IConsumeEvent
{    
    private int Amount = Amount;

    public RestoreHealthEvent(int Amount)
    {
        this.Amount = Amount;
    }

    public void ApplyConsumeEffects(BaseCharacter Consumer)   
    {
        Consumer.Stats.AlterStat(CharacterStats.CharStat.Health, Amount);    
    }    
}

在解析器内部(我们关心事件特殊性的唯一地方 - 因为我们正在解析数据文件本身):

RestoreHealthEvent ResHealthEv = new RestoreHealthEvent (Value);
NewItem.ConsumeEvents.Add (ResHealthEv );

当角色消耗物品时:

foreach (IConsumeEvent ConsumeEvent in Item.ConsumeEvents)
{
    //We're inside a parent method that's inside a parent BaseCharacter class; we're consuming an item right now.
    ConsumeEvent.ApplyConsumeEffects(this);
}
4

2 回答 2

1

为什么不将它们“映射”到“命令”类一次且仅一次呢?

例如,

[OnConsume:RestoreHealth(15)]
[OnConsume:RestoreFatigue(15)]

可以映射到可以定义为的命令类RestoreHealthRestoreFatigue

public sealed class RestoreHealth : ICommand {
    public int Value { get; set; }
    //whatever else you need
}

public sealed class SummonMonster : ICommand {
    public int Count {get; set; }
    public Position Position { get; set; }
}

此时将命令视为参数的包装器;)因此,您总是包装它们并只传递一个,而不是传递多个参数。它也提供了一些语义。

现在,您可以将您的库存物品映射到在消耗每个物品时需要“发送”的命令。

您可以实现一个简单的“总线”接口,例如:

public interface IBus {
    void Send(ICommand command);
    void Subscribe(object subscriber);
}

现在您只需获得一个实例IBus并在适当的时候调用它的Send方法。

通过这样做,您将您的“定义”(需要做什么)和您的逻辑(如何执行操作)问题分开。

对于接收反应部分,您实现了Subscribe询问subscriber实例的方法(再次,并且只有一次),找出可以“处理”命令的所有方法。你可以IHandle<T> where T: ICommand在你的处理程序中提供一些接口,或者只是按照约定找到它们(任何Handle只接受一个参数ICommand并返回的方法void),或者任何适合你的方法。

它基本上与您所说的“委托/操作”列表的部分相同,只是现在它是每个命令

map<CommandType, List<action>>

因为现在所有操作都只接受一个参数(即ICommand),所以您可以轻松地将它们全部保存在同一个列表中。

当接收到某个命令时,您的IBus实现只获取给定命令类型的操作列表,并简单地调用这些操作,将给定命令作为参数传递。

希望能帮助到你。

高级:你可以更进一步:有一个ConsumeItem命令:

public sealed void ConsumeItem: ICommand {
    public InventoryItem Item { get; set; }
}

您已经有一个类负责保存 InventoryItem 和 Commands 之间的映射,因此该类可以成为流程管理器

  1. 它订阅ConsumeItem命令(通过总线)
  2. 在其Handle方法中,它获取给定库存项目的命令列表
  3. 它将这些命令发送到总线。

好了,现在我们已经清楚地分离了这三个关注点:

  1. 在消耗库存物品时,我们只是“知道”IBus并发送ConsumeItem命令,我们并不关心接下来会发生什么。
  2. “ConsumeInventoryManager”(不管你怎么称呼它)也知道IBus', subscribes forConsumeItem` 命令,并且“知道”在每个项目被消耗时需要做什么(命令列表)。它只是发送这些命令,并不关心谁以及如何处理它们。
  3. 业务逻辑(角色、怪物等)只处理对它们有意义的命令(RestoreHealthDie等),而不关心它们来自哪里(以及为什么)。

祝你好运 :)

于 2013-01-27T03:42:45.040 回答
0

我的建议是使用反射,即定义一个基于指定名称调用所需方法的方法。这是一个工作示例:

class Program
{
    static void Main(string[] args)
    {
        SomeClass someInstance = new SomeClass();
        string name = Console.ReadLine();
        someInstance.Call("SayHello", name);
    }
}

class SomeClass
{
    public void SayHello(string name)
    {
        Console.WriteLine(String.Format("Hello, {0}!", name));
    }

    public void Call(string methodName, params object[] args)
    {
        this.GetType().GetMethod(methodName).Invoke(this, args);
    }
}

只要满足以下条件,您就可以这样做:

  1. 您绝对确定调用是可能的,即存在指定名称的方法并且参数的数量和类型正确

  2. 指定名称的方法没有被重载,否则你会得到一个System.Reflection.AmbiguousMatchException

  3. 存在一个超类,您希望Call从中派生该方法的所有类;您应该在该类中定义此方法

确保*满足条件 1. 和 2. 您可以使用更具体的版本,Type.GetMethod不仅要考虑方法的名称,还要考虑参数的数量和类型,并检查是否存在这样的方法在调用它之前;那么该Call方法将如下所示(*它不适用于参数标记为 out ref的方法):

public void Call(string methodName, params object[] args)
{
    //get the method with the specified name and parameter list
    Type[] argTypes = args.Select(arg => arg.GetType()).ToArray();
    MethodInfo method = this.GetType().GetMethod(methodName, argTypes);

    //check if the method exists and invoke it
    if (method != null)
        method.Invoke(this, args);
}

备注MethodInfo.Invoke 方法实际上返回一个 object因此您可以通过指定返回类型并使用 关键字以及适当的强制转换或将结果转换为所需类型的其他方法(如果可能)来定义该Call 方法以返回一些值- 请记住 return 检查是否是。

如果条件 3. 不满足,我会写一个扩展方法。这是一个返回泛型值的扩展方法的示例,我认为它在大多数情况下应该足够了(同样,它不适用于 ref or out)并且应该适用于 .NET Framework 中几乎所有可能的对象(我会感谢您指出一个反例):

public static class Extensions
{
    //invoke a method with the specified name and parameter list
    // and return a result of type T
    public static T Call<T>(this object subject, string methodName, params object[] args)
    {
        //get the method with the specified name and parameter list
        Type[] argTypes = args.Select(arg => arg.GetType()).ToArray();
        MethodInfo method = subject.GetType().GetMethod(methodName, argTypes);

        //check if the method exists
        if (method == null)
            return default(T); //or throw an exception

        //invoke the method and get the result
        object result = method.Invoke(subject, args);

        //check if something was returned
        if (result == null)
            return default(T); //or throw an exception
        //check if the result is of the expected type (or derives from it)
        if (result.GetType().Equals(typeof(T)) || result.GetType().IsSubclassOf(typeof(T)))
            return (T)result;
        else
            return default(T); //or throw an exception
    }

    //invoke a void method more conveniently
    public static void Call(this object subject, string methodName, params object[] args)
    {
        //invoke Call<object> method and ignore the result
        subject.Call<object>(methodName, args);
    }
}

然后,您应该能够使用,例如,someObject.Call<string>("ToString")而不是someObject.ToString(). 最后,在这一点上,我强烈建议:

  1. object尽可能使用更具体的类型

  2. 使用一些更复杂和唯一的名称Call- 如果某些类具有定义相同签名的方法,它可能会变得模糊

  3. 查找协方差逆变以获取更多有用的知识

于 2013-01-30T15:07:46.293 回答