我的场景中的一些GameObjects
实现了interace ISaveable
。在我的脚本中,我想找到这些接口并存储它们。稍后我可以遍历它们并调用它们实现的方法SaveData()
。
我目前找到这些接口的解决方法:
List<ISaveable> saveables = new List<ISaveable>();
MonoBehaviour[] sceneObjects = FindObjectsOfType<MonoBehaviour>();
for (int i = 0; i < sceneObjects.Length; i++)
{
MonoBehaviour currentObj = sceneObjects[i];
ISaveable currentComponent = currentObj.GetComponent<ISaveable>();
if (currentComponent != null)
{
saveables.Add(currentComponent);
}
}
代码工作正常但是有更好的方法吗?我不想在场景中搜索每个Monobehaviour,然后尝试获取其界面组件。
答案 0 :(得分:4)
您可以使用Linq OfType
ISaveable[] saveables = FindObjectsOfType<MonoBehaviour>().OfType<ISaveable>().ToArray();
当然还是需要先找到所有的对象。
请注意,它是 FindObjectsOfType
关于非活动游戏对象或禁用行为的一般限制的基础。
您可以扩展它以遍历场景的所有根级对象。这是有效的,因为 afaik GetComponentsInChildren
确实确实也适用于接口!
var saveables = new List<ISaveable>();
var rootObjs = SceneManager.GetActiveScene().GetRootGameObjects();
foreach(var root in rootObjs)
{
// Pass in "true" to include inactive and disabled children
saveables.AddRange(root.GetComponentsInChildren<ISaveable>(true));
}
如果它更有效 - 是的,不,也许,我不知道 - 但它也包括非活动和禁用的对象。
是的,可以使用
扩展它以遍历多个加载的场景var saveables = new List<ISaveable>();
for(var i = 0; i < SceneManager.sceneCount; i++)
{
var rootObjs = SceneManager.GetSceneAt(i).GetRootGameObjects();
foreach(var root in rootObjs)
{
saveables.AddRange(root.GetComponentsInChildren<ISaveable>(true));
}
}
这个替代方案有点类似于 this answer,但它有一个巨大的缺陷:你需要一个特定的接口实现才能使它工作,这使接口的整个概念无效。
所以评论中的一个大问题是:为什么要有一个界面?
如果它无论如何只用于 MonoBehaviour
,你应该有一个 abstract class
之类的
public abstract class SaveableBehaviour : MonoBehaviour
{
// Inheritors have to implement this (just like with an interface)
public abstract void SaveData();
}
无论如何,这已经解决了使用 FindObjectsOfType
的整个问题,因为现在您可以简单地使用
SaveableBehaviour[] saveables = FindObjectsOfType<SaveableBehaviour>();
但你仍然可以更进一步:为了更容易和更有效的访问,你可以让他们完全注册自己,而不需要管理器或单例模式!为什么类型不应该简单地自己处理它的实例?
public abstract class SaveableBehaviour : MonoBehaviour
{
// Inheritors have to implement this (just like with an interface)
public abstract void SaveData();
private static readonly HashSet<SaveableBehaviour> instances = new HashSet<SaveableBehaviour>();
// public read-only access to the instances by only providing a clone
// of the HashSet so nobody can remove items from the outside
public static HashSet<SaveableBehaviour> Instances => new HashSet<SaveableBehaviour>(instances);
protected virtual void Awake()
{
// simply register yourself to the existing instances
instances.Add(this);
}
protected virtual void OnDestroy()
{
// don't forget to also remove yourself at the end of your lifetime
instances.Remove(this);
}
}
这样你就可以简单地继承
public class Example : SaveableBehaviour
{
public override void SaveData()
{
// ...
}
protected override void Awake()
{
base.Awake(); // Make sure to always keep that line
// do additional stuff
}
}
并且您可以通过
访问该类型的所有实例HashSet<SaveableBehaviour> saveables = SaveableBehaviour.Instances;
foreach(var saveable in saveables)
{
saveable.SaveData();
}
答案 1 :(得分:3)
您可以拥有一个包含ISaveable
的集合/列表的经理类。
然后,您可以通过在其Awake方法中设置Singleton值使该管理器类成为单例。
class SaveManager(){
public static SaveManager Instance;
Awake(){
if (Instance == null)
{
Instance = this;
}
}
List<ISaveable> SaveAbles;
public void AddSaveAble(ISaveAble saveAble){
//add it to the list.
}
}
然后在实现ISaveable
接口的类的Start方法中,您可以使用Singleton将其添加到总列表中。
class Example : MonoBehaviour, ISaveable
{
void Start(){
SaveManager.Instance.AddSaveAble(this);
}
}
这样,每个SaveAble在创建时都会通过managers方法将自己添加到管理器中。
请注意,将Singleton设置为Awake非常重要,因此可以在Start中使用它,因为Awake在生命周期中排在第一位。
答案 2 :(得分:0)
如何拥有全局状态,可能是ISaveable
的列表,并让每个可保存的对象在start()
期间添加对它的引用?然后你可以简单地遍历数组,在每个SaveData()
引用上调用ISaveable
?
答案 3 :(得分:0)
请注意,此解决方案效率极低,但您可以使用LINQ过滤掉实现接口的组件。
var objects = GameObject.FindObjectsOfType<Transform>().
Where(o => o.GetComponent<YourInterface>().Select(o => o.GetComponent<YourInterface>());
答案 4 :(得分:0)
就我而言,我正在使用此实现:
MonoBehaviour[] saveableScripts = (MonoBehaviour[])FindObjectsOfType(typeof(MonoBehaviour));
foreach (var enemy in ISaveable)
{
if (enemy is ISaveable == false)
continue;
// you code ...
saveables.Add(currentComponent);
}
答案 5 :(得分:0)
这是您的方法的改进,用于在您的场景中实际获得所有 MonoBehavior 和您的界面。
void Start()
{
var myInterface = typeof(MyInterface);
var monoBehavior = typeof(MonoBehaviour);
var types = Assembly.GetExecutingAssembly()
.GetTypes()
.Where(p => myInterface.IsAssignableFrom(p) && monoBehavior.IsAssignableFrom(p));
var myInstance = new List<MyInterface>();
foreach (var type in types)
{
myInstance.AddRange(FindObjectsOfType(type).Select(x => x as MyInterface));
}
}
在这里,您没有遍历场景中的所有 GameObject,但是您执行了更多 FindObjectsOfType 调用,+ 您必须进行转换。但不再需要 GetComponent。
所以取决于你的情况,但如果你将反射部分放在缓存中(在加载时刻),它可以避免你实现管理器模式,你可以保持你的界面架构。
否则你可以看看观察者模式,它是一种管理不同类类型调用的不同方式。 https://sourcemaking.com/design_patterns/observer
答案 6 :(得分:0)
public interface Automateme
{
public void Automate();
}
然后……
void _do()
{
Automateme[] aa = GameObject
.FindObjectsOfType<MonoBehaviour>()
.OfType<Automateme>()
.Where(a => ((MonoBehaviour)a).enabled)
.ToArray<Automateme>();
if (aa.Length == 0)
{
Debug.Log($"Automator .. none");
return;
}
Debug.Log($"Automator .. " + aa.Length);
foreach (Automateme a in aa)
{
Debug.Log($"Automator {a.GetType()} {((MonoBehaviour)a).gameObject.name}");
}
// do something to one of them randomly, for example! ->
aa.AnyOne().Automate();
}
有一点,几乎不可避免的是,您还需要将对象作为 MonoBehavior,即您将其转换为 MonoBehavior(如获取游戏对象名称的简单示例),因此使用@derHugo 首次提到的 FindObjectsOfType<MonoBehaviour>()
想法是明智的(并且同样是低效的)
几点
当然,很明显,在某些情况下您根本不会执行此问题中所要求的操作,您只需简单地“列出”所考虑的项目。 (这会因情况而异,您是否只想要活动的,您是否想在他们的第一帧启动等中包含项目等)。问题的全部要点是针对(许多典型的)情况,您希望动态获取它们并且缓存不相关
Unity 开发,令人困惑的是,拥有庞大的、大得离谱的域范围。许多类型的项目在整个场景中可能有十几个游戏对象;其他类型的项目可以在场景中包含大量游戏对象。具有 2D 物理的游戏与具有 3D 物理的游戏截然不同,渲染方法与着色器方法也大不相同,等等。很自然地,对于像这样的任何“风格”问题,如果您来自脑海中的某个“领域”,答案可能会大不相同。
关于 FindObjectsOfType 和类似的调用,值得记住的是,Unity 的效率从早期就取得了令人难以置信的进步。过去,“你会告诉新 Unity 程序员的第一件事”是“不要在搜索场景的每一帧都做某事!!!我的天!你会减慢到 1fps!” 当然在某些领域,也许是许多领域,这仍然是正确的。但它(现在十年)通常只是完全错误。 Unity 对场景对象的缓存和处理现在非常快(你不能写得更快)而且 - 正如前一点提到的 - 你通常只是在谈论少数项目。 {整个 Unity 范式效率变化的一个很好的例子是“池化”。 (任何没有我年纪大的游戏程序员都可以查一下那是什么:))在 Unity 的早期,你会玩 Pool!一切都在恐慌中,我的上帝,我可能同时拥有三个坦克,获得一个共享图书馆!事实上,我太老了,我写了第一批流行的池化文章之一,每个人都使用了一段时间(早已删除)。现在 Unity 对实例化预制件或屏幕外模型的处理是如此快速和聪明,以至于您或多或少可以说它们内置了池化功能,而现在根本不需要池化诸如日常机枪子弹之类的东西。无论如何,重点这个例子的意义在于,现在必须更彻底地理解关于不搜索场景的(非常古老的)“标准建议”。}
回到最初的问题:
<块引用>我不想搜索场景中的每个 Monobehaviour 然后尝试获取其界面组件。
在游戏引擎中搜索场景中每个对象的唯一方法是搜索场景中的每个对象。所以没有办法解决。当然,人们可以“保留一份清单”,但它在某些/许多情况下忽略了两点 (A),这在游戏架构中是不可行/不合理的,并且 (B) Unity 已经 令人难以置信擅长保留游戏对象/组件列表,这是框架引擎的全部存在理由。 (请注意,如果它是一个物理游戏,那么为了上帝的缘故,整个 PhysX 系统正在做一项令人震惊的前沿工作,“每帧都找到东西”,使用数学上奇怪的空间散列等等,处理与场景中每个物理多边形的平方或更糟!相比之下,担心您从 50 个列表中提取 3 个项目是错误的(无论如何,框架引擎对它们进行了完美的散列和管理以提高效率)。>
答案 7 :(得分:-1)
我同意在此用例中使用 ISaveable
管理器的想法,正如 Doh09 和其他人所建议的那样。但是要回答您的问题,您可以遍历整个场景层次结构,或多个场景的层次结构,如下所示:
public class Foo : MonoBehaviour
{
List<ISaveable> saveables;
void RefreshSaveables()
{
saveables = new List<ISaveable>();
for (var i = 0; i < SceneManager.sceneCount; ++i)
{
var roots = SceneManager.GetSceneAt(i).GetRootGameObjects();
for (var j = 0; j < roots.Length; ++j)
{
saveables.AddRange(roots[j].GetComponentsInChildren<ISaveable>());
}
}
}
}
与原始代码相比,此代码的优势在于您不需要保存场景中的每个 MonoBehaviour 以便以后对其进行过滤 - 但是我仍然认为每个 ISaveable 最好在 {{1} 上注册自己} 并让经理调用他们的 Start
方法。
答案 8 :(得分:-2)
一种方法是使用统一容器(虽然稍微旧的方式)。通过配置将所有依赖注册到容器中。 ISaveable 的所有实例都可以注册到单元容器中,然后在代码中解析它们
示例:
web.config 中的统一容器:
<container name=“GameObjectContainer”>
<register type=“ISaveable” mapTo=“Example1” />
<register type=“ISaveable” mapTo=“Example2” />
</container>
ISavable 定义:
public interface ISavable
{
void SaveData();
}
ISavable 实现:
public class Example1 : ISavable
{
public void SaveData()
{
}
}
public class Example2 : ISavable
{
public void SaveData()
{
}
}