【问题标题】:Unity - Accessing other scripts' variables - put properlyUnity - 访问其他脚本的变量 - 正确放置
【发布时间】:2017-06-25 06:23:12
【问题描述】:

我知道如何访问另一个脚本的变量,但我只能通过function callsdelegates 访问。第一个很简单,但会使代码变得脆弱,因为一旦我编辑了原件,我必须再次编辑。 第二个更好,但由于我有很多函数,具有不同类型的返回值和参数,这会使事情复杂化很多。 假设我想在游戏开始时做一些事情。到目前为止,我在适当的脚本中创建了一个名为 OnGameStart() 的函数,并从那里调用了我需要的所有内容,并且将 OnGameStart() 设为 public 并从另一个脚本调用。

一开始我需要播放声音、检查保存数据、播放 UI 和其他动画等等,但我不想让我的代码成为灾难。 我在网上找了这个,但只找到了最简单的“如何在脚本之间通信”的东西,它与基本的函数调用,有时是事件一起使用。谁能指导我获取有关如何制作紧凑、隔离类以支持 Demeter's law 的资源?

【问题讨论】:

    标签: c# design-patterns unity3d organization code-organization


    【解决方案1】:

    解决此类问题肯定有很多可能性,例如,您可以从Hollywood principle 中获得一些灵感。

    不要让您的Player 搜索某些东西,而是在初始化时将其提供给他。

    这是一个非常简单的示例:

    游戏管理器界面的定义:

    using UnityEngine;
    
    namespace Assets.Scripts
    {
        public interface IGameManager
        {
            void PlayAudioClip(AudioClip audioClip);
        }
    }
    

    游戏管理员的定义:

    using UnityEngine;
    
    namespace Assets.Scripts
    {
        public class GameManager : MonoBehaviour, IGameManager
        {
            #region IGameManager Members
    
            public void PlayAudioClip(AudioClip audioClip)
            {
                // TODO
            }
    
            #endregion
        }
    }
    

    一个例子:

    using System;
    using UnityEngine;
    
    namespace Assets.Scripts
    {
        public class Player : MonoBehaviour
        {
            public GameManager GameManager; // TODO assign this in Inspector
    
            public void Start()
            {
                if (GameManager == null)
                    throw new InvalidOperationException("TODO");
            }
    
            public void Update()
            {
                // demo
                var wounded = true;
                var woundedAudioClip = new AudioClip();
                if (wounded)
                {
                    GameManager.PlayAudioClip(woundedAudioClip);
                }
            }
        }
    }
    

    您也可以在 Unity 中使用 Singleton (或任何合适的)。

    注意事项:

    上面的例子实际上只是给你一个提示如何思考的例子,你还没有提供所有的细节,即使你提供了,我们也很难为你提供任何进一步的帮助(只有你会随着时间的推移找到真正适合你当前问题的东西)。

    在您的游戏项目中推进时,您会发现没有要遵循的硬性规则,显然模式很重要,但您可能会发现自己以自己的方式结束(即多种模式的细粒度组合使用)。

    也许“Who wrote this programing saying? "Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live."”也会给你一些启发。

    结论是,发现/尝试模式,调整它们,随着时间的推移,您会发​​现最适合您的方法。

    【讨论】:

    • 这是“一切都必须是 MonoBehaviour” Unity 反模式的示例。它往往比使用静态实例更容易出错(无论如何你都将它视为静态实例,只是开销更大、检查员工作量更大和尴尬的情况,因为你必须尝试序列化 everything 这通常是完全没有必要的)。
    • 它绝对胜过static 的使用,继续阅读使用组合模式VS 使用static 的好处(如果两者之间可以进行任何比较的话)。在您的示例中,您输入了Player.Current,但如果您向后倾斜一点,您迟早会意识到这根本没有意义:)
    • 这不是一回事;同时使用两者 - 我参与了数百个 Unity 项目,所以我相当确定这就是大多数 Unity 开发人员的工作方式 :) 例如,我想你已经写了一个需要访问尚未实例化的玩家游戏对象的脚本。这将导致像GameObject.Find(...) 这样的行既慢又容易出错。要么存储对该游戏对象的静态引用,要么至少将其包装在某个静态属性中。
    • 谢谢大家的回答。事实上,目前我使用Singleton 作为MainGameManager 并处理来自那里的大部分呼叫。例如,对于非功能性逻辑,我有一个 UImanager,所以我有类似 2 个实际发生的脚本。我仔细阅读了答案和 cmets,肯定会有所收获。
    • @agiro FWIW,避免创建一个名为GameManager 的类——这样的名字实际上注定要成为神类:) 即它需要什么来管理一些更恰当命名的类不能做的事情(例如处理播放背景音乐的BackgroundMusic 类,而不是出现在这个“管理器”类中的 PlayMusic 方法)。
    【解决方案2】:

    使用静态类

    当您需要广泛访问程序的某些子系统时,静态类非常有用,因此它们在游戏中被广泛使用。

    /// <summary>Save/load is a good example because it 
    /// doesn't need any settings and it's 
    /// useful to call it from almost anywhere.</summary>
    public static class GameSaver {
    
        /// <summary>I save the game when I'm run.</summary>
        public static void Save() {
           // Save the game!
        }
    
    }
    

    要使用静态类,您只需直接使用成员 - 例如GameSaver.Save(); 将在“任何地方”工作。属性、字段、事件等都可以设为静态,但请先查看以下注释。

    这是避免某种"god class" 的最简单方法 - 这似乎是您所描述的(是的,它们通常是代码灾难!) - 这是一个类过于复杂,什么都做。将其分解为一系列小的、独立的模块。

    不过不要过度使用静态字段!

    为此使用单例。

    特别是在游戏中,只有一次实例化的事物(例如播放器或音频系统)非常普遍,它们还需要易于重置或具有大量属性。

    重要的是不要将它们全部作为静态字段 - 这将难以重置和调试。这就是您使用静态字段并实例化普通类的地方 - 这称为 singleton

    /// <summary>There's only ever one background music source!
    /// It has instance properties though (i.e. an AudioSource)
    /// so it works well as a singleton.</summary>
    public class BackgroundMusic {
    
        /// <summary>The static field - use the Play method from anywhere.</summary>
        private static BackgroundMusic Current;
    
        /// <summary>Plays the given clip.</summary>
        public static void Play(AudioClip clip) {
    
            if (Current == null) {
                // It's not been setup yet - create it now:
                Current = new BackgroundMusic();
            }
    
            // E.g. Current.Source.Play(clip);
    
        }
    
        public BackgroundMusic() {
            // Instance a source now. 
        }
    
    }
    

    这只是意味着BackgroundMusic.Play(..); 可以在任何地方使用。这种方法意味着您无需在检查器中设置任何内容 - 只需调用该方法即可按需设置。

    当 MonoBehaviour 很棒时

    通常认为所有代码都必须是 MonoBehaviour 并且必须附加到游戏对象。这不是 Unity 的实际工作方式。当一切都是 MonoBehaviour 并且必须全部手动实例化和连接时,这只会为使用编辑器的人带来更多的工作。

    要明确,我并不是说根本不要使用 MonoBehaviour。相反,您应该根据代码实际表示的内容来使用组件模型和静态的适当组合。

    一般:

    • 如果某事只有一个实例,请使用单例。
    • 但是如果只有一个并且它具有在检查器中编辑有用的属性,请使用 MonoBehaviour 并将对单个对象的引用也保留为静态字段。

    其中一个例子是玩家(在单人游戏中)具有一系列您希望更改的默认设置。您可以将播放器设置为预制件,并拥有某种引用当前实例的 PlayerSettings.Current 静态字段:

    /// <summary>Add this to a player prefab.</summary>
    public class PlayerSettings : MonoBehaviour{
    
        /// <summary>Still following the singleton pattern.</summary>
        public static PlayerSettings Current;
    
        /// <summary>Player speed. This can be edited in the inspector.</summary>
        public float Speed;
    
    
        public void Awake() {
            // Update the static field:
            Current = this;
        }
    
    }
    

    这种方法两全其美——您可以在任何地方使用PlayerSettings.Current(在播放器预制件被实例化之后),而不必放弃检查器。它也比GameObject.Find("Player/Body").GetComponent&lt;PlayerSettings&gt;(); 之类的东西更容易重构,使其更易于维护。

    多个实例

    如果有多个实例的东西,比如 NPC,那么通常你总是会使用带有 MonoBehaviour 的预制件。但是,使用静态方法对以下情况非常有用:

    public class NPC : MonoBehaviour{
    
        /// <summary>Gets an NPC by their name.</summary>
        public static NPC Locate(string name){
            // E.g. get all GameObject instances with an NPC component.
            // Return the first one which has a 'Name' value that matches.
        }
    
        /// <summary>The name of this NPC (editable in the inspector).
        public string Name;
    
    }
    

    NPC.Locate("Dave"); 对于它的实际预期作用变得不言自明。

    【讨论】:

    • 提防 Unity 中的 static,它是邪恶的 :)
    • @Aybe 不再是 - 许多年前,当编辑器没有正确卸载静态字段时,这是一场噩梦,但那些日子早已一去不复返了。
    • 据我所知,直到今天它仍然是邪恶的 :) (docs.unity3d.com/Manual/script-Serialization.html)
    • @Aybe 那是非常不同的东西——它指的是 MonoBehaviour 中的静态字段,无论如何都不会被序列化。 IE。与任何其他 C# 程序一样,它们的初始值(如果有)已正确存储在 DLL 中。
    • 太好了,我同意你的整体方法,只是缺乏解释令人困惑,并导致可能无休止的辩论/激烈的战争:)
    猜你喜欢
    • 1970-01-01
    • 2015-07-30
    • 2019-07-30
    • 1970-01-01
    • 1970-01-01
    • 2021-09-10
    • 1970-01-01
    • 1970-01-01
    • 2018-01-19
    相关资源
    最近更新 更多