【发布时间】:2021-05-17 08:13:48
【问题描述】:
我在项目中使用 pub/sub 模式。如果一个类正在跟踪像浮点数这样的原始类型,并且其他子系统和类需要能够读取,是否可以使用消息来传递浮点数的句柄?不是价值,而是可以让他们在必要时阅读它的东西。
【问题讨论】:
我在项目中使用 pub/sub 模式。如果一个类正在跟踪像浮点数这样的原始类型,并且其他子系统和类需要能够读取,是否可以使用消息来传递浮点数的句柄?不是价值,而是可以让他们在必要时阅读它的东西。
【问题讨论】:
如果一个类正在跟踪像浮点数这样的原始类型,并且其他子系统和类需要能够读取,是否可以使用消息来传递浮点数的句柄?
绝对!事实上,Unity 提供了一个令人惊讶的健壮(ish)message system,实现这一点并不难。尽管在我个人看来,他们的系统不如 MSDN 使用的传统 Producer/Subscriber event based pattern 直观。
为了将“句柄”(MSDN 将其称为 alias)传递给一个值,比如您的示例中的 float,在 C# 中我们可以使用 ref 关键字。
C# 中的 ref 关键字,当你把它归结为它的基础时,你是定义还是请求一个对象的引用,而不是它本身的对象。(这个为简洁起见,被过度概括了)。
当您请求对该对象的引用时,您不会获得该对象的副本,而是获得一个别名,该别名允许您使用该变量获取、设置、读取或任何您想要的东西,就像您控制它一样(就像这个例子)
要使用 ref 关键字,这里有一个简短的示例来帮助您入门:
class Alice{
private int RandomInt = 4; // guaranteed to be random
public ref int GetReferenceToPrivateInt()
{
return ref RandomInt;
}
public void LogPrivateInt()
{
Console.WriteLine(RandomInt);
}
}
Alice alice = new Alice();
ref int retrieveReference = alice.GetReferenceToPrivateInt();
retrieveReference = 16;
alice.LogPrivateInt();
// outputs:
16
我们可以在这个例子中看到,即使我们从不可以直接访问 Alice 类中的私有变量,但当我们收到 by-reference 值时来自GetReferenceToPrivateInt() 方法。我们能够修改该值,并且这些更改反映在私有 Alice 类变量中。无需手动设置变量!太棒了!
如果我们将 ref 关键字与 Unity 消息传递系统一起使用,我们就能够“传递”引用变量,这样我们就可以变异它们而无需直接访问它们。
让我们从创建一个定义订阅者的IEventSystemHandler开始。
public delegate ref T ReferenceAction<T>();
public interface IReferenceMessage<T> : IEventSystemHandler
{
void MutateValue(ref T MessageValue);
void StoreGetter(ReferenceAction<T> GetterForReferencePrimitive);
}
我们在这里所做的是创建interface,它定义了订阅者应该有哪些方法来正确“使用”我们发送给他们的内容。
我们创建了两个方法,MutateValue(ref T) 和 StoreGetter(ReferenceAction<T>)。 MutateValue() 接受 by reference 泛型参数,StoreGetter() 接受名为 ReferenceAction<T> 的东西。
ReferenceAction<T> 这里不是 Unity 或 MSDN 创建的方法、类型或对象,我们是在上面的代码中创建的:
public delegate ref T ReferenceAction<T>();
它的作用是创建一个delegate,在验证简化意义上,我们告诉编译器,“我想提供一个方法作为这个函数StoreGetter的参数,但我希望该方法具有我需要的非常具体的signature。”
现在我们已经定义了订阅者的外观,让我们创建一个。
public class Subcriber: MonoBehaviour, IReferenceMessage<float>
{
// we store the method that we can call to get a reference value here
private ReferenceAction<float> FloatGetter;
// Whenever the producer sends out a message called MutateValue, we're gonna run this automatically because we're subscribed to the producer
public void MutateValue(ref float MessageValue)
{
// change the value of the reference float to prove that value changes on the producer
MessageValue *= 2;
}
// whenever the producer starts, since we're a subscriber, we will have this method called, which will allow us to store the method we can call to get a reference to the hidden float on the producer object
public void StoreGetter(ReferenceAction<float> GetterForReferencePrimitive)
{
// save the method for later so we can get a reference value any time we need it
this.FloatGetter = GetterForReferencePrimitive;
}
}
让我们也创建一个 Producer 类并将所有内容整合在一起。
public class Producer : MonoBehaviour
{
// for example purposes we should keep this private, so we know we are changing its value outside this class using it's reference, and not the actual object
private float SecretFloat = 1.0f;
// allow the user to assign a subscriber in the inspector
public GameObject Subcriber;
// create a property that is public, that hides the secret float.
public float Primitive
{
get => SecretFloat;
set
{
SecretFloat = value;
// send message to subscriber that the value of the secret float has changed
ExecuteEvents.Execute<IReferenceMessage<float>>(
Subcriber,
null,
/* pass the secret float as a reference and not a copy */
(x, y) => x.MutateValue(ref SecretFloat)
);
}
}
private void Start()
{
// send message to subscriber so they can get reference float any time using the method we send them
ExecuteEvents.Execute<IReferenceMessage<float>>(Subcriber, null, (x, y) => x.StoreGetter(FloatGetter));
}
// create a method that we send as a message at Start() so other objects can retrieve a reference at any time, not just when the value of secret float has changed
private ref float FloatGetter()
{
// return a reference to secret float, not a copy.
return ref SecretFloat;
}
}
上面非常简单(如未完全实现/功能)的脚本允许您做一些非常酷的事情,我认为这就是您想要的。
当Start() 被调用时,订阅者会获得一个保存以供以后使用的方法,它获得的方法是FloatGetter(),当它被调用时 - 返回一个引用引用float(SecretFloat)。
当订阅者收到该消息时(在StoreGetter() 中),它将存储该方法,以便以后可以随时使用它来获取引用。
除此之外,我们还做到了,每当SecretFloat 的值发生更改时,都会向订阅者发送第二条消息,这次我们直接发送引用而不是返回引用的方法。这样我们就不需要经常检查我们的引用是否已经更新,生产者会告诉我们何时更新,如果我们愿意,我们可以在获得引用时改变它。超级酷!
您现在可能想知道为什么我们需要存储一个方法来获取引用,我们不能将引用变量存储在字段或属性中并使用它代替?
不幸的是ref 值和变量不能存储在属性或字段中,以便以后保留。 一般由于 C# 的工作方式,ref 变量和引用可以never leave the stack and have limitations for safety reasons.
如果您有兴趣在纯 C# 中实现相同的效果,可以使用以下方法开始,您会发现使用传统的 EventHandler 方法更加精简。
public class Subscriber
{
public void MutateFloat(object caller, ref float primitive)
{
primitive *= 2;
}
}
public class Producer<T>
{
private T SecretPrimitive = default;
public delegate void RefPrimitiveHandler(object caller, ref T primitive);
public event RefPrimitiveHandler OnPrimitiveChange;
public T Primitive
{
get => SecretPrimitive;
set
{
SecretPrimitive = value;
OnPrimitiveChange?.Invoke(this, ref SecretPrimitive);
}
}
}
【讨论】:
如果我理解正确,您可能会使用Func<float>。并且只需存储一个返回浮点值的方法。
例如
public class System : MonoBehaviour
{
public SubSystem SomeSubSystem;
private float currentFloat;
private float GetFloat()
{
return currentFloat;
}
private void Awake ()
{
SomeSubSystem.Initialize(GetFloat);
}
private void Update()
{
currentFloat += Time.deltaTime;
}
}
然后像这样使用它
public class SubSystem : MonoBehaviour
{
private Func<float> _getFloat;
public void Initialize (Func<float> getFloat)
{
_getFloat = getFloat;
}
private IEnumerator Start ()
{
while(true)
{
yield return new WaitForSeconds (1);
Debug.Log(_getFloat());
}
}
}
当然也可以在类似的活动中提供同样的东西
public event Action<Func<float>> SomeEvent;
因此,该事件的侦听器不会直接接收浮点值,而是如何获取它并存储它以供以后使用的方法。
【讨论】: