【问题标题】:Proper way to get the right implemented overload in a child class在子类中获得正确实现重载的正确方法
【发布时间】:2019-06-30 14:58:02
【问题描述】:

让我先介绍一下我在做什么。

在 Unity 中,我想让一些 GameObjects(比如玩家)能够拾取物品(其他 GameObjects)。

为了做到这一点,我设计了这个基本代码:

一个拉取物品的组件:

public class PickupMagnet : MonoBehaviour
{   
    // [...]
    private void Update()
    {
        Transform item = FindClosestItemInRange(); // Well, this line doesn't exist, it's just a simplification.
            if (ítem != null)
             Pickup(item);
    }

    private void Pickup(Transform item)
    {
        IPickup pickup = item.GetComponent<IPickup>();
        if (pickup != null)
        {
            pickup.Pickup();
            Destroy(item);
        }
    }   
}

那些(那个目前)项目的界面:

public interface IPickup
{
    void Pickup();
    // [...]
}

还有我当时做的单品:

public class Coin : MonoBehaviour, IPickup
{
    private int price;
    // [...]

    void IPickup.Pickup()
    {
        Global.money += price; // Increase player money
    }   
    // [...]
}

在我想添加一个新项目之前,一切都很好:一个健康包。该物品会增加拾取它的生物的生命值。但为了做到这一点,我需要生物脚本的实例:LivingObject

public class HealthPack: MonoBehaviour, IPickup
{
    private int healthRestored;
    // [...]

    void IPickup.Pickup(LivingObject livingObject)
    {
        livingObject.TakeHealing(healthRestored);
    }   
    // [...]
}

问题是IPickup.Pickup() 上面没有任何参数。显然,我可以将其更改为IPickup.Pickup(LivingObject livingObject) 并忽略Coin.Pickup 上的参数,但是如果将来我想添加更多种类的项目,需要不同的参数怎么办? 其他选择是向接口添加一个新方法,但这迫使我实现 Coin.Pickup(LivingObject livingObject) 并实现它。

经过考虑,我删除了 IPickup 并将其替换为:

public abstract class Pickupable : MonoBehaviour
{
    // [...]
    public abstract bool ShouldBeDestroyedOnPickup { get; }
    public virtual void Pickup() => throw new NotImplementedException();
    public virtual void Pickup(LivingObject livingObject) => throw new NotImplementedException();
}

然后覆盖CoinHealthPack中的必要方法。另外,我将PickupMagnet.Pickup(Transform item) 更改为:

public class PickupMagnet : MonoBehaviour
{
    // [...]
    private LivingObject livingObject;

    private void Start()
    {
        livingObject = gameObject.GetComponent<LivingObject>();
    }
    // [...]
    private void Pickup(Transform item)
    {
        Pickupable pickup = item.GetComponent<Pickupable>();
        if (pickup != null)
        {
            Action[] actions = new Action[] { pickup.Pickup, () => pickup.Pickup(livingObject) };
            bool hasFoundImplementedMethod = false;
            foreach (Action action in actions)
            {
                try
                {
                    action();
                    hasFoundImplementedMethod = true;
                    break;
                }
                catch (NotImplementedException) { }
            }

            if (!hasFoundImplementedMethod)
                throw new NotImplementedException($"The {item.gameObject}'s {nameof(Pickup)} class lack of any Pickup method implementation.");
            else if (pickup.ShouldBeDestroyedOnPickup)
                Destroy(item.gameObject);
        }
    }
}

基本上,这会遍历actions 中定义的所有方法并执行它们。如果他们提出NotImplementedException,它会继续尝试使用数组中的其他方法。

这段代码运行良好,但就个人而言,我不喜欢用Pickable.Pickup 的每个重载来定义该数组。

所以,我开始做一些研究,我发现了一种叫做“反思”的东西。我仍然不确定它是如何深入工作的,但我设法制作了这个工作代码。

private void Pickup(Transform item)
{
    Pickupable pickup = item.GetComponent<Pickupable>();
    if (pickup != null)
    {
        bool hasFoundImplementedMethod = false;
        foreach (MethodInfo method in typeof(Pickupable).GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly))
        {
            if (method.Name == "Pickup")
            {
                ParameterInfo[] parametersGetted = method.GetParameters();
                int parametersAmount = parametersGetted.Length;                    
                object[] parametersObjects = new object[parametersAmount ];
                for (int i = 0; i < parametersAmount; i++)
                {
                    Type parameterType = parametersGetted[i].ParameterType;
                    if (parameters.TryGetValue(parameterType, out object parameterObject))
                        parametersObjects[i] = parameterObject;
                    else
                        throw new KeyNotFoundException($"The key Type {parameterType} was not found in the {nameof(parameters)} dictionary.");
                }
                bool succed = TryCatchInvoke(pickup, method, parametersObjects);
                if (succed) hasFoundImplementedMethod = true;                    
            }
        }

        if (!hasFoundImplementedMethod)
            throw new NotImplementedException($"The {item.gameObject}'s {nameof(Pickup)} class lack of any Pickup method implementation.");
        else if (pickup.ShouldBeDestroyedOnPickup)
            Destroy(item.gameObject);
    }
}

private bool TryCatchInvoke(Pickupable instance, MethodInfo method, object[] args)
{
    try
    {
        method.Invoke(instance, args);
        return true;
    }
    catch (Exception) // NotImplementedException doesn't work...
    {
        return false;
    }
}

并添加到MagnetPickup

private LivingObject livingObject;
private Dictionary<Type, object> parameters;

private void Start()
{
    livingObject = gameObject.GetComponent<LivingObject>();
    parameters = new Dictionary<Type, object> { { typeof(LivingObject), livingObject } };
}

...并且有效。

我对 Unity 分析器不是很熟悉,但我认为最后一个代码的运行速度要快一点(不到 1%)。

问题是我不确定该代码将来是否会给我带来问题,所以这是我的问题:反射是解决此问题的正确方法还是应该使用我的 try/catch 尝试或者其他代码?

仅 1% 我不确定是否应该冒险使用它。我不是在寻找最好的性能,只是在寻找解决这个问题的正确方法。

【问题讨论】:

  • 只有pickup.Pickup(LivingObject); 有什么问题?对于您不会使用参数的硬币,它们只会给玩家钱。或者就此而言,为什么健康包需要一个活物:只有玩家在捡硬币,所以除了玩家之外没有人是相关的活物。
  • @Draco18s 我想我试图概括太多,就像 Mockarutan 回答说的那样。

标签: c# oop unity3d reflection system.reflection


【解决方案1】:

我确实认为最好的方法是在 Pickup 中发送对 Player 对象的引用,然后根据 Pickup 对象的类型执行自定义逻辑。我可能会跳过界面,只使用一个名为“PickupObject”或其他东西的基础对象,然后让 FindClosestItemInRange 返回这些对象。

最后你应该销毁游戏对象,而不是你传递给 Pickup 函数的任何东西。您可能可以销毁 Transform 并获得相同的结果(我没有尝试过),但实际上销毁 GameObject 而不是 GameObject 的任何组件只是一个好习惯

public class PickupObject : MonoBehaviour
{
    virtual void Pickup(Player playerObject) { }
}

public class Coin : PickupObject 
{
    public int price;
    override void Pickup(Player playerObject)
    {
        playerObject.money += price; // Move money over to the player as it probably makes more sense
    }
}
public class HealthPack : PickupObject 
{
    public int healthRestored;
    override void Pickup(Player playerObject)
    {
        playerObject.health += healthRestored;
    }
}

public class PickupMagnet : MonoBehaviour
{   
    public Player PlayerObject;
    private void Update()
    {
        PickupObject item = FindClosestItemInRange();
        Pickup(item);
    }

    private void Pickup(PickupObject pickup)
    {
            pickup.Pickup(PlayerObject);
            Destroy(pickup.gameObject);
    }   
}

编辑,对你的代码的一些一般想法:

如果您有通用的“LivingObject”,如您的代码所建议的那样可以同时获取生命值和硬币,那么您可能过于概括了。听起来你只是有一个需要拾取东西的玩家。让“任何东西”能够拾取“任何东西”在我的经验中太过笼统了。不要试图在第一行代码上解决所有未来的问题。如果你不确定你的目标是什么,或者在这个早期阶段结构很棘手。编写代码来完成您需要做的事情,并在出现模式和重复时对其进行重构。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-12-03
    • 2010-09-21
    • 1970-01-01
    相关资源
    最近更新 更多