C#程序员整理的Unity 3D笔记(十三):Unity 3D基于组件的思想

如果你接触过《设计模式》、软件架构的编程思想,就会知道优秀的设计“组合优于继承的”【这句话很简短,但开始学习OOP的不太好理解】。

      如果你接触过《设计模式》、软件架构的编程思想,就会知道优秀的设计准则:“组合优于继承的”。

      这句话很简短,但开始学习OOP的时候,真切的是—-不太好理解(以我个人当初学习为例)。

 

OOP的继承思想

image

在设计主角(Player)的时候,为了能够复用A、B、C的功能,我开始把A、B、C按照继承来写,多了一些Virutal\Override\Protected等修饰符,功能没有任何问题,就是有些别扭。如Start、Update方法,只能在A中采用模板方法处理,万一B、C、Player中直接用了Start、Update方法,会导致奇奇怪怪的问题;同时在继承的基类中,无形之间多了一些包袱,对于Player不得不使用A、B、C的函数、变量(非private的)。

整个关系变为了:

  • Player is a A
  • Player is a B
  • Player is a C

心理上疙疙瘩瘩的,总觉得有点别扭。

 

OOP的组合思想

image

以前使用组合思想较多的是构建树、树叶模型,例如电信中的网元模型。这种思想,属于Unity 3D的核心思想–组件。在Player、A、B、C中可自由使用Start、Update函数(请不考虑执行顺序,脚本组件的先后顺序外部可调整,但是意义不大),最重要的是,关系理顺了—主角变成更积极、主动。

  • Player have a A
  • Player have a B
  • Player have a C
    在Unity 3D中,可复用的几乎全部为封装为了组件,eg: transform、rigibody、render、camera、***.cs脚本;为了配合方便的使用非内置的组件,可使用gameObject.AddComponent<T>()、gameObject.GetComponent<T>()来添加、获得组件(一般是自定义的脚本)。

 

这里我举一个实际的例子,在《Unity 3D手机游戏开发》第二版的“太空射击游戏”中,有一个需求,需要给游戏中可复用的GameObject添加自动销毁的功能(通过时间计时器,或者触发器添加),代码很简单,不到100行,要添加的GameObject有5、6个,虽然工作量不大,但总不能每个都拷贝一遍代码吧。

开始我是按照OOP继承做的,

image

 

看了几天,很不爽,后来重构为如下图:

image

这样使得自动销毁组件的功能发挥的更加灵活、机动,即不必拘泥于静态的继承思想来实现。

从这个重构过程中,我学到Unity 3D组件思想的闪闪发光……

附录:完整的自动销毁组件代码:

public class AutoDestoryComponent : MonoBehaviour
{
    #region ICanCache
    public ParticleSystem[] m_pss = null;
    public int m_life = 1; //3条命
    public float m_AutoDeadTime = 3;//3s自动销毁

    private int m_life_Base = 3; //3条命【恢复用】
    private float m_AutoDeadTime_Base = 3;//3s自动销毁【恢复用】【-1:表示不自动销毁,如Enemy】

    void Update()
    {
        //需要自动销毁
        if (m_AutoDeadTime_Base >= 0)
        {
            m_AutoDeadTime -= Time.deltaTime;

            if (m_AutoDeadTime <= 0)
            {
                InnerDead();
                return;
            }
        }

        if (m_life <= 0)
        {
            InnerDead();
        }
    }

    /// <summary>
    /// 设置自动销毁数据
    /// </summary>
    /// <param name="life_base">默认生命值</param>
    /// <param name="autoDeadTime_base">-1不自动销毁;其他数据代表销毁时间(单位s)</param>
    public void SetBasePara(int life_base = 1, float autoDeadTime_base = -1)
    {
        m_AutoDeadTime = m_AutoDeadTime_Base = autoDeadTime_base;
        m_life = m_life_Base = life_base;
    }

    //是否启用
    public bool IsUse { get; set; }
    //死后位置
    public Vector3 DeathPosition
    {
        get
        {
            return new Vector3(2000, 2000, 2000);
        }
    }

    //复活
    public void Init(Vector3 position, Quaternion rotation)
    {
        transform.gameObject.SetActive(true);
        transform.position = position;
        transform.rotation = rotation;
        IsUse = true;
        foreach (ParticleSystem item in m_pss)
        {
            item.Play(true);
        }

        //有些绕
        m_life = m_life_Base;
        m_AutoDeadTime = m_AutoDeadTime_Base;
    }

    private void InnerDead()
    {
        IsUse = false;
        transform.position = DeathPosition;
        foreach (ParticleSystem item in m_pss)
        {
            item.Stop(true);
        }

        this.gameObject.SetActive(false);
    }
    #endregion
}

 

 

包括系统自带的Audio、Transform、Camera、Image、Button等等。GameObject是一个容器,没有Image的GameObject,只要新建一个空的GameObject,添加Image Component极为Image GameObject对象的。

也即是在Unity3D中,很少用GameObject.ID的概念,而是用GameObject.Tag、GameObject.name来区分不同的GameObject,且Tag、name不唯一。

 

结论:在Unity3D中,万事万物都是Component。

C#程序员整理的Unity 3D笔记(十二):Unity3D之单体模式实现GameManager

Unity3D中创建类,默认会继承自MonoBehaviour的类, 这样不需要自己创建它的实例、也不能自己创建(如 new 类名)–编译的时候可以编译过去,但是执行会报错错误在console窗口。

诚然,继承自MonoBehaviour的类,有一些好处,可用到Unity 事件驱动:如我们经常用到的Awake, Start, Update等。

而这里的GameManager是单例模式,我仅仅会暴露一些public接口、存储全局数据,生命周期不由Unity控制。(不用考虑拖一个Empty gameObject附加GameManager脚本, 既然是单体自然是唯我独尊。)

 

我个人更喜欢的单体是不含有这2个元素的单体模式

  • DontDestroyOnLoad: 不太好控制
  • MonoBehaviour:         需要考虑到各个Unity3D函数的调用顺序、生命周期。

 

image

 

形成基本的游戏架构后,构建游戏则较轻松了:数据交互、存储、游戏玩法、逻辑判断也好实现添加了。

 

 

本文参考:

C#程序员整理的Unity 3D笔记(十一):unity3d中脚本生命周期(MonoBehaviour lifecycle)

脚本自带函数执行顺序,看看下图即可明白。唯一需要注意的是,要注意函数字符的大小写—这个是C#程序员容易被坑的地方之一。

 

clip_image002

最先执行的方法是Awake,这是生命周期的开始,用于进行激活时的初始化代码,一般可以在这个地方将当前脚本禁用:this.enable=false,如果这样做了,则会直接跳转到OnDisable方法执行一次,然后其它的任何方法,都将不再被执行。

如果当前脚本处于可用状态,则正常的执行顺序是继续向下执行OnEnable,当然我们可以在另外一个脚本中实现这个脚本组件的启动:this.enable =true;

再向下执行,会进行一个判断,如果Start方法还没有被执行,则会被执行一次,如果已经被执行了,则不会再被执行。这是个什么意思呢?我们可以在某个脚本中将组件禁用this.enable=false,再启用时会转到OnEnable处执行,这时继续向下走,发现Start执行过了,将不再被执行。比如说:第一次启用时,将怪物的初始位置定在了(0,0,0)点,然后怪物可能会发生了位置的变换,后来被禁用了,再次启用时,不会让怪物又回到初始的(0,0,0)位置。

继续向后执行,就是FixedUpdate了,然后是Update,再然后是LateUpdate,如果后面写了Reset,则会又回到Update,在这4个事件间可以进行循环流动。

再向后执行,就进入了渲染模块(Rendering),非常重要的一个方法就是OnGUI,用于绘制图形界面。当然,如果你使用了NGUI,这个生命周期的事情你就不用考虑了。

再向后,就是卸载模块(TearDown),这里主要有两个方法OnDisableOnDestroy。当被禁用(enable=false)时,会执行OnDisable方法,但是这个时候,脚本并不会被销毁,在这个状态下,可以重新回到OnEnable状态(enable=true)。当手动销毁或附属的游戏对象被销毁时,OnDestroy才会被执行,当前脚本的生命周期结束。

脚本自带函数执行顺序如下:将下面脚本挂在任意物体运行即可得到

Awake ->OnEable-> Start -> -> FixedUpdate-> Update  -> LateUpdate ->OnGUI ->Reset -> OnDisable ->OnDestroy

  • 1.Awake:用于在游戏开始之前初始化变量或游戏状态。在脚本整个生命周期内它仅被调用一次.Awake在所有对象被初始化之后调用,所以你可以安全的与其他对象对话或用诸如GameObject.FindWithTag()这样的函数搜索它们。每个游戏物体上的Awake以随机的顺序被调用。因此,你应该用Awake来设置脚本间的引用,并用Start来传递信息Awake总是在Start之前被调用。它不能用来执行协同程序。
  • 2.Start:仅在Update函数第一次被调用前调用。Start在behaviour的生命周期中只被调用一次。它和Awake的不同是Start只在脚本实例被启用时调用。你可以按需调整延迟初始化代码。Awake总是在Start之前执行。这允许你协调初始化顺序。在所有脚本实例中,Start函数总是在Awake函数之后调用。
  • 3.FixedUpdate:固定帧更新,在Unity导航菜单栏中,点击“Edit”–>“Project Setting”–>“Time”菜单项后,右侧的Inspector视图将弹出时间管理器,其中“Fixed Timestep”选项用于设置FixedUpdate()的更新频率,更新频率默认为0.02s。
  • 4.Update:正常帧更新,用于更新逻辑。每一帧都执行,处理Rigidbody时,需要用FixedUpdate代替Update。例如:给刚体加一个作用力时,你必须应用作用力在FixedUpdate里的固定帧,而不是Update中的帧。(两者帧长不同)FixedUpdate,每固定帧绘制时执行一次,和update不同的是FixedUpdate是渲染帧执行,如果你的渲染效率低下的时候FixedUpdate调用次数就会跟着下降。FixedUpdate比较适用于物理引擎的计算,因为是跟每帧渲染有关。Update就比较适合做控制。
  • 5.LateUpdate:在所有Update函数调用后被调用,和fixedupdate一样都是每一帧都被调用执行,这可用于调整脚本执行顺序。例如:当物体在Update里移动时,跟随物体的相机可以在LateUpdate里实现。LateUpdate,在每帧Update执行完毕调用,他是在所有update结束后才调用,比较适合用于命令脚本的执行。官网上例子是摄像机的跟随,都是在所有update操作完才跟进摄像机,不然就有可能出现摄像机已经推进了,但是视角里还未有角色的空帧出现。
  • 6.OnGUI:在渲染和处理GUI事件时调用。比如:你画一个button或label时常常用到它。这意味着OnGUI也是每帧执行一次。
  • 7.Reset:在用户点击检视面板的Reset按钮或者首次添加该组件时被调用。此函数只在编辑模式下被调用。Reset最常用于在检视面板中给定一个默认值。
  • 8.OnDisable:当物体被销毁时 OnDisable将被调用,并且可用于任意清理代码。脚本被卸载时,OnDisable将被调用,OnEnable在脚本被载入后调用。注意: OnDisable不能用于协同程序。
  • 9.OnDestroy:当MonoBehaviour将被销毁时,这个函数被调用。OnDestroy只会在预先已经被激活的游戏物体上被调用。注意:OnDestroy也不能用于协同程序。