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笔记(七):接口、虚函数使用之个人见解

接口、虚函数使用之个人见解

最近在封装重构太空大战的代码,结合原来C#的知识点发现很多有意思的地方,此文主要介绍下接口和虚函数的使用,但对于接口和虚函数的区别这里不在描述(自行百度、谷歌)。

虚函数的使用:

定义一个Enemy的基类,它具有所有的敌人的共有属性及行为。而它的派生类会有较多的属性及特有/变种行。例如以下代码,Enemy具有生命、速度属性,有死亡、复活的行为,所有派生类也都具有这些属性及行为,把该类中的初始化方法,死亡、复活方法定义为虚方法,方便在派生类中有特殊逻辑可以在相应的重载函数中实现。

代码如下:

public class Enemy : MonoBehaviour {

public Transform m_transform;

///生命属性

public float m_life

{

get;

set;

}

///速度

public float m_speed

{

get;

set;

}

// Use this for initialization

void Start () {

Init();

m_transform = this.transform;

}

//初始化

protected virtual void Init()

{

m_speed = 1;

m_life = 5;

}

//移动轨迹

protected virtual void EnemyMove()

{

Move.Instance.MoveToDir(m_transform, CustomerMoveDirection.line,-m_speed);

}

//死亡

public virtual void Dead()

{

m_transform.position = Move.Instance.HidePosition;

m_transform.gameObject.SetActive(false);

}

//复活

public virtual void Revive(Vector3 position, Quaternion rotation)

{

m_transform.gameObject.SetActive(true);

m_transform.position = position;

m_transform.rotation = rotation;

}

}

派生类:

public class EnemyLevel1 : Enemy {

private float m_timer = 5f;

//生命及速度重写

protected override void Init()

{

m_life = 10;

m_speed = 0.5f;

}

//移动轨迹重写

protected override void EnemyMove()

{

#region 水平方向 z轴 1.5左右来回移动

if (m_timer > 0)

{

m_timer -= Time.deltaTime;

Move.Instance.MoveToDir(m_transform, CustomerMoveDirection.sign, m_speed);

}

else

{

Move.Instance.MoveToDir(m_transform, CustomerMoveDirection.sign,-m_speed);

if (m_transform.position.z > 4)

m_timer = 5f;

}

#endregion

}

public override void Dead()

{

//如果有特殊逻辑,可以在此处完成

base.Dead();

}

public override void Revive(Vector3 position, Quaternion rotation)

{

//如果 有特殊逻辑,可以再此处完成

base.Revive(position, rotation);

}

}

接口的使用:

定义IEnemy接口

public interface IEnemy

{

float m_life { get; set; }

float m_speed{get;set;}

void Dead();

void Revive(Vector3 position, Quaternion rotation);

}

类Enemy和EnemyLevel1 分别继承该接口(别忘了继承MonoBehaviour这个类),并在各自类中按各自的业务逻辑实现(当然公共部分可以抽出封装起来),此部分不贴代码(可以百度、谷歌 “C#接口实现”).

对比以上两种实现方式,个人感觉单就这个游戏的例子而言虚函数的使用要比接口方便,派生类中只需要在有特殊处理的方法中完善代码即可,但接口就需要在每个继承接口的类中去写实现。但如果要实现多继承的话,接口反而是最好的选择。

以上是个人在使用过程中的理解,有不对的地方欢迎大家指正、拍砖。 :)