【问题标题】:Can I create a pointer to poll a float from any source?我可以创建一个指针来从任何来源轮询浮点数吗?
【发布时间】:2018-05-28 13:19:42
【问题描述】:

有没有办法使用对象中的相同引用访问从同一类继承的所有不同类型的对象,而无需对其进行硬编码?

我正在统一开发,我想在我的游戏中添加一个模块,它可以观察 GameObject 中的任何特定浮点数,然后在另一个 GameObject 中的另一个浮点数达到某个值时更改它。

例如:一个“触发”对象/模块,当在胃对象中达到 Fullness

由于我将有大量可能的组合,我不想通过为每个组合创建触发器类的女儿来对其进行硬编码。

我最初的想法是使用指向好的浮点数的指针,以便在初始化时观察/更改。但显然,我们不能在迭代器(IEnumerator)中使用不安全代码,所以我不确定如何轮询 Fullness 的值。

举一个我想要的例子:

public Class Trigger{
    private float* ToPoll;
    private float* ToChange;

    public void SetTrigger(float* poll, float* change){
        ToPoll = poll;
        ToChange = change;

        // the loop would be a IEnumerator, not a litteral loop
        while(*ToPoll < 0.5f){
            sleep(0.1)
        }
        *ToChange = 1f
    }
}

void Main(){
    Trigger trigger1, trigger2;
    trigger1.SetTrigger(&Stomach.fullness, &Brain.hunger)
    trigger2.SetTrigger(&Sun.activityLevel, &Earth.radiationLevel)
    // ^ Literally any float from any object
}

您有什么想法或更好的方法来实现它吗?

【问题讨论】:

  • 您不能以这种方式“从任何对象轮询任何浮点数”。你可能已经能够在 C++ 中使用指针算法来破解类似的东西,但是 C# 中的那种东西让开发人员不寒而栗:它破坏了类型安全、垃圾收集以及你的 IDE 的辨别能力你做错了什么。
  • 有什么办法可以做类似的事情吗?不一定要用指针?也许参考或类似的东西?
  • 你想要一个对象的引用,你需要从已经拥有它的地方获取它。
  • 您将如何做到这一点以使其按我想要的方式工作?你能复制参考吗?
  • 您知道方法参数是如何工作的,对吧?将包含浮点数的对象传递给函数。

标签: c# pointers unity3d unsafe


【解决方案1】:

扩展@kara 的答案,以下代码实现了独立的StomachBrain 对象,并使用Being 将它们连接起来。

Being 知道什么Stomach

  • 它有一个NeedsFoodEvent

Being 知道什么Brain

  • 有一个OnRaiseIsHungryEvent(即“饥饿”信号——谁在乎它来自哪里)
  • 它有一个IsHungryEvent

请记住,在实际实现中,可能会有其他对象在监听这些事件。例如也许你有一个会切换到“饥饿”的情绪系统和一个基于目标的人工智能会切换到觅食模式。两个系统都不需要知道对方,但都可以响应来自Brain 的信号。在这个简单的实现中,Being 响应Stomach 信号,同时通知和响应Brain

重要的一点不是引发和响应事件的特定方法(在这种情况下是默认的 .Net 机制),而是两个对象都不知道另一个对象的内部结构(参见不同的实现) HumanStomachZombieStomach),而是以更合适的级别连接连接(在这种情况下为 Being)。还要注意对接口的依赖,它允许我们做一些事情,比如创建混合生物(即将ZombieBrainHumanStomach 配对)。

代码是使用 .Net Core CLI 作为控制台应用程序编写/测试的,但它应该与大多数 .Net > 3.5 版本兼容。

using System;
using System.Linq;
using System.Threading;

namespace so_example
{
    public class Program
    {
        static void Main(string[] args)
        {
            var person1 = new Being("Human 1", new HumanBrain(), new HumanStomach());
            var zombie1 = new Being("Zombie 1", new ZombieBrain(), new ZombieStomach());
            var hybrid1 = new Being("Hybrid 1", new ZombieBrain(), new HumanStomach());
            var hybrid2 = new Being("Hybrid 2", new HumanBrain(), new ZombieStomach());

            Console.WriteLine("Hit any key to exit");
            Console.ReadKey();
        }
    }

    public class HungryEventArgs : EventArgs
    {
        public string Message { get; set; }
    }

    public interface IStomach
    {
        event EventHandler<HungryEventArgs> NeedsFoodEvent;
    }

    public class Stomach : IStomach
    {
        public event EventHandler<HungryEventArgs> NeedsFoodEvent;

        protected virtual void OnRaiseNeedsFoodEvent(HungryEventArgs e)
        {
            EventHandler<HungryEventArgs> handler = NeedsFoodEvent;

            if (handler != null)
            {
                handler(this, e);
            }
        }
    }

    public class HumanStomach : Stomach
    {
        private Timer _hungerTimer;

        public HumanStomach()
        {
            _hungerTimer = new Timer(o =>
            {
                // only trigger if breakfast, lunch or dinner (24h notation)
                if (new [] { 8, 13, 19 }.Any(t => t == DateTime.Now.Hour))
                {
                    OnRaiseNeedsFoodEvent(new HungryEventArgs { Message = "I'm empty!" });
                }
                else
                {
                    Console.WriteLine("It's not mealtime");
                }
            }, null, 1000, 1000);
        }
    }

    public class ZombieStomach : Stomach
    {
        private Timer _hungerTimer;

        public ZombieStomach()
        {
            _hungerTimer = new Timer(o =>
            {
                OnRaiseNeedsFoodEvent(new HungryEventArgs { Message = "Need brains in stomach!" });
            }, null, 1000, 1000);
        }
    }

    public interface IBrain
    {
        event EventHandler<HungryEventArgs> IsHungryEvent;
        void OnRaiseIsHungryEvent();
    }

    public class Brain : IBrain
    {
        public event EventHandler<HungryEventArgs> IsHungryEvent;
        protected string _hungryMessage;

        public void OnRaiseIsHungryEvent()
        {
            EventHandler<HungryEventArgs> handler = IsHungryEvent;

            if (handler != null)
            {
                var e = new HungryEventArgs
                {
                Message = _hungryMessage
                };

                handler(this, e);
            }
        }
    }

    public class HumanBrain : Brain
    {
        public HumanBrain()
        {
            _hungryMessage = "Need food!";
        }
    }

    public class ZombieBrain : Brain
    {
        public ZombieBrain()
        {
            _hungryMessage = "Braaaaaains!";
        }
    }

    public class Being
    {
        protected readonly IBrain _brain;
        protected readonly IStomach _stomach;
        private readonly string _name;

        public Being(string name, IBrain brain, IStomach stomach)
        {
            _stomach = stomach;
            _brain = brain;
            _name = name;

            _stomach.NeedsFoodEvent += (s, e) =>
            {
                Console.WriteLine($"{_name}: {e.Message}");
                _brain.OnRaiseIsHungryEvent();
            };

            _brain.IsHungryEvent += (s, e) =>
            {
                Console.WriteLine($"{_name}: {e.Message}");
            };
        }
    }
}

一些注意事项

为了提供一些输出,我在 2 个IStomach 实现中伪造了一些东西。 HumanStomach 在构造函数中创建一个计时器回调,它每 1 秒触发一次,并检查当前时间是否是用餐时间。如果是,则引发NeedsFoodEventZombieStomach 也每 1 秒使用一次回调,但它每次都会触发 NeedsFoodEvent。在真正的 Unity 实现中,您可能会根据来自 Unity 的某些事件触发偶数 - 玩家在预设时间后采取的动作等。

【讨论】:

  • 太棒了!虽然我将不得不更多地研究您的代码才能完全理解它,但我认为这是我最好的选择。非常感谢。您是否知道一种无需为每个变量编写 Eventthanlder 就可以使其具有通用性的方法,因此 Even 可以附加到任何变量?您还可以指导我在您的代码中的哪个位置检查比方说肚子饱的值,以便我可以开始通过您的代码进行锻炼?或者它没有指定,我应该了解更多关于事件的信息来理解这一点?再次感谢您
  • 在计时器回调处查看HumanStomach 的构造函数(另请参阅已编辑答案中的注释),了解我是如何生成事件的。您可能希望在您的Stomach 实现上提供一个公共方法,以允许外部代码轻松触发它。您将检查该方法中的任何变量。您真的不想直接考虑监控内部变量,而是考虑广告意图。
【解决方案2】:

我不太确定你想做什么,但听起来你想为你的对象添加触发器。据我了解,在这种情况下,触发器应该是委托。

这里是一个如何定义委托类型并将触发器列表添加到您的 Brain 类的示例。

现在每个大脑都可以有不同的触发器。我设置了两个派生大脑来向您展示如何使用它:

public class TestBrain
{
    private static int NextId = 1;
    public TestBrain(List<MyTrigger> triggers)
    {
        this.Triggers = triggers;
        this.Id = NextId++;
    }

    public int Id { get; private set; }
    public int Hunger { get; set; }
    public int StomachFullness { get; set; }
    public List<MyTrigger> Triggers { get; private set; }

    public void FireTriggers()
    {
        foreach (MyTrigger t in this.Triggers)
        {
            t.Invoke(this);
            this.StomachFullness = 100;
        }
    }

    public delegate void MyTrigger(TestBrain b);
}

public class HumanBrain : TestBrain
{
    static readonly List<MyTrigger> defaultHumanTriggers = new List<MyTrigger>()
    {
        b => { if (b.StomachFullness < 50) { b.Hunger = 1; Console.WriteLine("{0} is hungry..", b.Id); } }
    };

    public HumanBrain() : base(defaultHumanTriggers)
    {

    }
}

public class RobotBrain : TestBrain
{
    static readonly List<MyTrigger> defaultRobotTriggers = new List<MyTrigger>()
    {
        b => { if (b.StomachFullness < 50) { Console.WriteLine("{0} ignores hunger only want's some oil..", b.Id); } }
    };

    public RobotBrain() : base(defaultRobotTriggers)
    {

    }
}

static void Main()
{
    // Create some test-data
    List<TestBrain> brains = new List<TestBrain>()
    {
        new HumanBrain(),
        new HumanBrain(),
        new RobotBrain(),
        new HumanBrain(),
    };

    Console.WriteLine(" - - - Output our Testdata - - -");
    foreach (TestBrain b in brains)
    {
        Console.WriteLine("Status Brain {0} - Stomachfulness: {1} Hunger: {2}", b.Id, b.StomachFullness, b.Hunger);
    }

    Console.WriteLine(" - - - Empty stomachs - - -");
    foreach (TestBrain b in brains)
    {
        b.StomachFullness = 0;
    }

    Console.WriteLine(" - - - Fire triggers - - -");
    foreach (TestBrain b in brains)
    {
        b.FireTriggers();
    }

    Console.WriteLine(" - - - Output our Testdata - - -");
    foreach (TestBrain b in brains)
    {
        Console.WriteLine("Status Brain {0} - Stomachfulness: {1} Hunger: {2}", b.Id, b.StomachFullness, b.Hunger);
    }

}

【讨论】:

  • 哇,令人印象深刻。我什至不确定我是否有能力理解你的答案啊哈。但也许要澄清一下自己,这样你就可以告诉我你的解决方案是否仍然有效:我只想要可以轮询任何对象的任何浮点数的触发器,然后以任意方式修改任何对象的任何浮点数(这意味着可能会有 1 或2 个触发对象,每个对象都有一个浮动轮询和一个修改)。所以它可以很容易地使用胃饱来触发大脑中的饥饿感,反之亦然,或者轮询足部的浮动来改变膝盖的浮动......告诉我这是否有意义?
  • Unity 为这种跨对象通信提供了一个事件系统,基本上就是这样做的。使用消息传递系统的优点是,类可以宣传其意图(例如“我是空的!”),而不是进入一个班级,然后可以触发其他意图(例如“我饿了!”)而不需要了解具体细节。如果您将“对象”的公共接口(此处松散地使用该术语)视为意图,并让您的实现保持私有,您会发现您的代码将更易于维护。
  • @ColinYoung:不知道这个——但我不是真正的统一专家:)。你能举个例子吗?我会感兴趣的。
  • @kara 我也不是 Unity 专家。 This example 是一个很好的起点,Will R. Miller 的 this example 也是如此。我会看看我能对一个具体的例子做些什么
猜你喜欢
  • 2021-10-17
  • 2019-07-10
  • 2012-03-10
  • 1970-01-01
  • 2010-10-20
  • 1970-01-01
  • 2012-08-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多