为什么要继承组件?
比如游戏中,有很多种类型的小怪。每个小怪身上都有一个控制脚本。
当然,不可能所有小怪都用同样的控制脚本。
但是,在玩家攻击小怪的时候,不管是哪一种小怪,它都要减少血量。
当然,不可能枚举所有类型的控制脚本,然后调用它们各自的函数来减少血量。
这就到了面向对象思想发挥作用的时候了。
我们将所有小怪的共性,都抽象成一个小怪基类控制脚本,它包含所有小怪所拥有的基本逻辑。
比如都有血量,都有状态机,都有碰撞检测。
然后呢,每种不同的小怪,都有它不同的行为模式。
比如有的小怪,默认状态是在路边睡觉,有的小怪默认状态要巡逻。
这就需要各自派生的控制脚本中去实现了。
怎么对派生出来的各种小怪控制脚本执行统一的操作?
比如我现在有一个子弹的脚本,当它击打到小怪身上的时候,就要获取小怪身上的组件。
但是有很多种小怪,每种小怪身上挂的组件都是小怪基类组件的特定派生类,它们是不同的,当然不可能枚举地去获取派生类组件。
但它们又是相同的,它们都继承于同一基类。
获取基类的组件就好了。
需要注意的点
如果一个组件继承了另一个组件,并且派生类组件中有基类组件中使用过的 MonoBehaviour 生命周期函数,比如Update,那么在游戏的运行中,基类组件的同名的生命周期函数会被隐藏(不是重写)。如果需要它运行,则需要基类中,将该基类生命周期函数设置为public,然后在派生类中的相应生命周期函数中调用基类的该生命周期函数,比如 base.Update()。
示例
基类组件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class EmenyController : MonoBehaviour
{
public FiniteStateMachine finiteStateMachine;
public void SetTransition(TransitCondition t)
{
finiteStateMachine.Transit(t);
Debug.Log(t.ToString());
//转换到追逐状态时,显示UI
if (t == TransitCondition.SawPlayer || t == TransitCondition.BeAttacked)
floatingHealthBar.active = true;
else
floatingHealthBar.active = false;
}
public void Update() //会被隐藏而不再执行
{
isAlive();
UpdateUI();
}
public float health;
public GameObject floatingHealthBar;
public void OnHurt(float value)
{
health -= value;
//播放受击动画
GetComponent<Animator>().SetTrigger("Hurt");
}
void UpdateUI()
{
//血条
floatingHealthBar.transform.LookAt(Camera.main.transform);
floatingHealthBar.GetComponent<Slider>().value = health / 100;
}
void isAlive()
{
if (health <= 0)
{
GetComponent<Animator>().SetTrigger("Dead");
enabled = false;
}
}
private void OnCollisionEnter(Collision collision)
{
if (collision == null) return;
if (collision.gameObject.tag == "Attack")
SetTransition(TransitCondition.BeAttacked);
}
}
派生类组件
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class QQController : EmenyController
{
void Awake()
{
InstantiateFiniteStateMachine();
}
public Transform home;
void InstantiateFiniteStateMachine()
{
QQRestState restState = new QQRestState(gameObject);
restState.AddTransition(TransitCondition.SawPlayer, StateIndex.ChasingPlayer);
restState.AddTransition(TransitCondition.BeAttacked, StateIndex.ChasingPlayerAdvanced);
QQReturnHomeState qqReturnHomeState = new QQReturnHomeState(home);
qqReturnHomeState.AddTransition(TransitCondition.AtHome, StateIndex.Sleep);
qqReturnHomeState.AddTransition(TransitCondition.SawPlayer, StateIndex.ChasingPlayer);
qqReturnHomeState.AddTransition(TransitCondition.BeAttacked, StateIndex.ChasingPlayerAdvanced);
QQChaseState chaseState = new QQChaseState();
chaseState.AddTransition(TransitCondition.LostPlayer, StateIndex.ReturnHome);
QQChaseStateAdvanced chaseStateAdvanced = new QQChaseStateAdvanced();
chaseState.AddTransition(TransitCondition.LostPlayer, StateIndex.ReturnHome);
finiteStateMachine = new FiniteStateMachine();
finiteStateMachine.AddState(restState);
finiteStateMachine.AddState(chaseState);
finiteStateMachine.AddState(chaseStateAdvanced);
finiteStateMachine.AddState(qqReturnHomeState);
}
public GameObject player;
public float playerDistance;
public float homeDistance;
void Update()
{
base.Update();
if (player == null || player.active == false)
player = GameObject.FindGameObjectWithTag("Player");
playerDistance = Vector3.Distance(player.transform.position, transform.position);
homeDistance = Vector3.Distance(home.position, transform.position);
Debug.Log(finiteStateMachine.currentStateIndex);
}
void FixedUpdate()
{
finiteStateMachine.currentState.Work(player, gameObject);
finiteStateMachine.currentState.isTransitable(player, gameObject);
}
}
附加知识点:隐藏和覆盖的区别
隐藏
当子类继承父类,并且子类中有父类同名函数,那么子类将隐藏父类中所有同名函数,不可以对父类中同名函数直接进行访问。
如果子类需要调用父类中函数,需要用 base.函数名 去访问。
覆盖
当父类中同名函数被virtual修饰时,此时子类对父类函数进行重写并覆盖。
区别
当基类指针指向子类,并调用子类中的同名函数。如果该基类的函数是被隐藏的,则仍然可以通过该基类指针调用到。如果该函数被重写了,则调用的是子类重写的同名函数,父类的函数相当于被抹去了,不可被调用。
共同点
无论是重写,还是隐藏,通过子类对象进行函数调用,都只会调用子类的函数,而隐藏父类的所有同名函数。