2019-5-27-CLX教程(一)面向对象封装的简单应用 封装HP

CLX course(1) OOT

Posted by BY LX on May 27, 2019

主要内容

前言

Show You The Code

结语

正文

1. 前言

对于刚入门的小萌新而言,面向程序中的封装可能会让大家感到十分困惑,甚至觉得这是十分没有必要的、累赘的。

我认为产生这样的感觉的原因究其根本是因为封装之概念不够直观,如果没有针对实际问题去应用的话当然是难以理解。在最近看了小萌新写的关于游戏的HP相关代码后,感觉确实不够理想,便写此博客浅谈我对封装相关的拙见。

希望这篇能够帮助到你,如果有问题请在留言区反馈,感谢!

2. Show You The Code

首先看一下萌新最开始的大致代码:

public class HP{
    public static float chp = 100;
    private float thp = 100;
    public Slider hpSlider;
    
    private void Update(){
        hpSlider.value = chp/thp;
    }
}

简单看一眼,Update函数每一帧都要调用,因此不管chp是否更新了,每一帧hpSlider的value属性都会进行更新,这时候发现了有很多很多赋值其实是白费的,因此我要求萌新接着去修改代码,这回代码如下:

public class HP{
    private void Update(){
        if(hp改变){
            hpSlider.value = chp/thp;
        }
    }
}

然而,即使是这样优化了一下,每一帧还是会进行一次判断,这样的代码依旧是产生了浪费。

那么,要怎么解决呢?

思考一下,到底是什么时候我们才需要执行那一段代码呢?

答案当然是:

  • HP被初始化赋值的时候
  • 受到伤害的时候
  • 恢复HP的时候

总而言之,其实就是: HP的值发生改变的时候

那么,我要怎样知道 HP发生了改变 呢?

如果针对于一个对象的public字段而言,其他的类的对象都可以任意访问到这个字段的话,这个字段在不知道何处被改动了的时候,按道理这时候我们其实并无法很方便的知道HP是否发生了改变,如下代码所示:

public class HP{
    public int HP;
}

public class OtherClass{
    private HP hp;
    public void ChangeHP(int val){
        hp.HP = 100;
        OnHpChange();
        /// 除非手动执行OnHpChange()
        /// 否则代码仍需使用Update一直去访问hp.HP看其是否发生改变
    }
    public void OnHpChange(){}
}

在此我们发现了,如果我们在要改变HP的其他的每个类里都写一个OnHpChange函数,然后再改变HP的时候去调用这个函数,也能解决这个问题。只是。。。如果有很多类的对象都要去改变这个HP呢?那么我们是不是可能写很多的重复代码呢?

那么,让我们把注意力集中于问题:如何想办法在 HP发生了改变的时候执行某些逻辑 这个问题上。

我们目前明确了一点:

  • 如果HP是一个 public 的字段的话,其他类可以任意改变它的话,那么这样会不好方便的知道HP是否改变了,或者会产生大量重复代码。

可是如果不能随便访问这个字段,我们给他们加上一层逻辑呢?

下面看看如下代码:

public class Health{
    public readonly MaxHP;
    private int nowHP;
    public int NowHP{
        get{
            return nowHP;
        }
        set{
            if(nowHP != value){
                OnHPChange();
            }
        }
    }
    public void OnHPChange(){}
}

这样的话,其他的类如果要对nowHP进行赋值的话,首先会执行一层判断是否nowHP发生改变的逻辑。

这样就很方便的解决了之前所提到的问题。

不过,不要高兴的太早,再来思考一个问题:在一个游戏中,会有哪些物件具有HP呢?

我思考的话大致有以下这些:

  • 玩家
  • 敌人
  • 障碍物

那么,这些物件当HP发生改变的时候,他们的OnHPChange方法里的逻辑明显是不同的,但是我们发现,其核心代码是很相似的,关键的不同之处就在于OnHPChange方法。

那么,这要怎么解决呢?

我思考的解决方案有以下两种:

  • 继承Health类
  • 委托

首先试一下继承这种方法:

public abstract class BaseHealth{
    public readonly MaxHP;
    private int nowHP;
    public int NowHP{
        get{
            return nowHP;
        }
        set{
            if(nowHP != value){
                OnHPChange();
            }
        }
    }
    public abstract void OnHPChange();
}

public class PlayerHealth:BaseHealth{
    public override void OnHPChange(){
        /// 玩家HP改变的事件
    }
}

public class EnemyHealth:BaseHealth{
    public override void OnHPChange(){
        /// 敌人HP改变的事件
    }
}

我们发现,这样我们就重复利用到了BaseHealth类里面的大部分代码,而只用去实现OnHPChange方法就可以解决之前的问题了。

不过,尽管使用继承已经足够方便了。不过,我认为仍存在以下问题:

  • 每需要改变一次逻辑,就要重新实现一个继承BaseHealth的类。
  • 继承会产生编译时依赖,不够灵活。
  • 多用组合,少用继承。
  • 当只用继承的时候,在Unity中去获取其他物体的话,比如PlayerHealth方法要去获得HPSlider上的Slider组件的时候,需要去使用GameObject.Find()方法或者一些其他的方法,会产生大量耦合。

接着,我们采用委托机制去简单实现我们的需求:

public class Health
{
    // private 字段防止不通过属性器去修改hp的值
    private int hp;
    public int HP
    {
        get { return hp; }
        set
        {
            if(value <= 0)
            {
                hp = 0;
                /// ?.表示如果onDead不为null的话,则调用该委托
                onDead?.Invoke();
                onHPChange?.Invoke(hp, MaxHP);
            }
            else if(value != hp)
            {
                hp = value;
                onHPChange?.Invoke(hp,MaxHP);
            }
        }
    }

    public readonly int MaxHP;

    private Action onDead;
    private Action<int, int> onHPChange;
    
    /// 构造函数
    public Health(int initHP)
    {
        hp = MaxHP = initHP;
    }

    /// 注册死亡事件
    public void RegisterOnDeadAction(Action action)
    {
        if (onDead != null)
        {
            onDead += action;
        }
        else
        {
            onDead = action;
        }
    }

    // 注册HP改变的事件
    public void RegisterOnHPChangeAction(Action<int,int> action)
    {
        if (onHPChange != null)
        {
            onHPChange += action;
        }
        else
        {
            onHPChange = action;
        }
        /// 每注册一个事件,都要执行一下
        if (action!=null)
        {
            action(HP, MaxHP);
        }
    }

    public void ChangeHP(int detalHP)
    {
        HP += detalHP;
    }

}

public class Enemy : MonoBehaviour
{
    private Health health;
    private Slider slider;

    /// <summary>
    /// 使用协程来进行测试
    /// </summary>
    private IEnumerator Start()
    {
        /// 初始化
        health = new Health(3);
        slider = GetComponent<Slider>();
        health.RegisterOnDeadAction(() => { Debug.Log("Ahhh~ I'm dead!"); });

        health.RegisterOnHPChangeAction((nowHP, maxHP) => {
            slider.value = (float)nowHP / maxHP;
        });

        health.RegisterOnHPChangeAction((nowHP, maxHP) => {
            Debug.Log("Now I have " + nowHP + " HP!");
        });

        ///开始每秒逐渐扣一滴血
        while(health.HP != 0)
        {
            yield return new WaitForSeconds(1f);
            health.ChangeHP(-1);
        };
    }
}

该示例代码我会放在Github上供大家 下载 ,大家导入Unity打开场景点击开始便可以看到效果。

稍微研究下代码应该就可以理解,不做详细讲解。

然后我们发现这样写会有以下优势:

  • 符合多用组合,少用继承的思想。
  • 使用事件注册机制,耦合性低,灵活性高。

目前来说就做这么多讲解,希望能对大家有所帮助。

3. 结语

首先必须要承认的是,笔者并非什么大佬,只能写这篇文章发表下自己的拙见,如果文章有不够深刻不够严谨之处,希望各位谅解或者指出,定会学习改正。

另外,当然一篇博客难以讲清楚全部的东西,今后有时间的话会继续进行分享,希望自己能坚持下去。