这本身就有一个游戏开发项目,但它实际上是关于编码和将数据映射到其他数据。这就是我决定在这里发布的原因。
我用于外部库存商品数据存储的格式:
[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点,而他的疲劳也是如此。这在后台调用了两种不同的方法:
void RestoreHealth(Character Subject, int Amount);
void RestoreFatigue(Character Subject, int Amount);
您如何将方法映射到其文件字符串对应项?这就是我的想法:
每次消耗一个项目时,字符串列表(事件)都会传递给 Item事件管理器。管理器解析每个字符串并调用适当的方法。很容易设置,因为这不是经常发生的操作,对性能的影响可能不大(字符串也很小(最多10-15个字符)大小,并在O(n)时间解析)。
每个库存商品(类)在初始化时只解析一次字符串事件。每个字符串事件都通过 字典映射到其适当的方法。这是我能想到的最有效的性能方法,但它使得做其他事情变得非常困难: 字典中的所有值都必须是相同类型的委托。这意味着我无法保持
a)RestoreHealth(int)
b)SummonMonster(位置,计数)
在同一个字典中,并且必须为每种可调用方法设置新的数据结构。这是一项艰巨的工作。
想到改善这两种方法的一些方法:
我可以在 Item事件中使用某种临时缓存
管理员,以便不解析项目的 OnConsume 事件
两次?我可能会遇到与我在2)期间遇到的问题相同的问题
但是,因为缓存必须是map<InventoryItem,List<delegate>>
。
.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);
}
答案 0 :(得分:1)
为什么不将它们“映射”到“命令”类一次又一次呢?
例如,
[OnConsume:RestoreHealth(15)]
[OnConsume:RestoreFatigue(15)]
可以映射到RestoreHealth
和RestoreFatigue
命令类,可以定义为:
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之间保存一个映射,因此该类可以成为进程管理器:
ConsumeItem
命令(通过总线)Handle
方法中,它获取给定广告资源项的命令列表那么,现在我们已经明确区分了这三个问题:
IBus
并发送ConsumeItem
命令,我们不关心接下来会发生什么。 IBus', subscribes for
ConsumeItem`命令和“知道”当每个项目被消耗时需要做什么(命令列表) 。它只是发送这些命令而不关心谁以及如何处理它们。RestoreHealth
,Die
等)并且不关心它们来自何处(及其原因) 答案 1 :(得分: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);
}
}
如果满足以下条件,您可以这样做:
您完全确定可以进行调用,即存在指定名称的方法,参数的数量和类型是否正确
指定名称的方法未重载,否则您将获得System.Reflection.AmbiguousMatchException
存在一个超类,您希望在派生时使用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
,,因此您可以定义 {{1} } 通过指定返回类型并使用 Call
关键字以及适当的强制转换或其他将结果转换为所需类型的方法来返回某些值的方法,如果它是可能 - 记得检查一下。
如果不满足条件3.我会选择编写扩展方法。这是一个返回通用值的扩展方法的示例,在大多数情况下我认为应该足够了(再次,它不适用于 return
或 ref
)并且应该可以解决.NET Framework中几乎所有可能的对象(我要感谢指出一个反例):
out
然后,您应该可以使用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()
更具体的类型
使用比object
更复杂和唯一的名称 - 如果某个类的方法定义了相同的签名,它可能会变得模糊
查找协方差和逆转以获取更多有用的知识