目录
声明
26:Kick it Back 反击石头人
27:Health Bar 设置血条显示
28:Player LevelUp 玩家升级系统
29:Player UI 添加玩家信息显示
30:Create Portal 创建传送门
本教程学习均来自U3D中文课堂麦扣老师
现在实现石头会对Player产生伤害并且将Player击出一段距离,也要实现Player来反击石头人
添加枚举变量表示石头的状态,先对Player造成击退和伤害的代码:
using UnityEngine.AI;
public enum RockStates { HitPlayer,HitEnemy,HitNothing }
public class Rock : MonoBehaviour
{
private RockStates rockStates;//枚举变量,石头的状态
public int damage;//石头的伤害
private void Start()
{
rb = GetComponent();
rockStates = RockStates.HitPlayer;
FlyToTarget();//朝着目标飞
}
private void OnCollisionEnter(Collision collision)
{
switch(rockStates)//根据石头不同的状态
{
case RockStates.HitPlayer://击退Player并造成伤害
if (collision.gameObject.CompareTag("Player"))
{
collision.gameObject.GetComponent().isStopped = true;//停止移动
collision.gameObject.GetComponent().velocity = direction * force;//击退
collision.gameObject.GetComponent().SetTrigger("Dizzy");//眩晕
//造成伤害
}
}
}
因为石头没有characterStats,无法调用CharacterStats里的TakeDamage()方法,因此在CharacterStats中进行函数重载:
public void TakeDamage(int damage,CharacterStats defener)
{
}
在Rock中即可调用函数重载:
//造成伤害
var otherStats = collision.gameObject.GetComponent();
otherStats.TakeDamage(damage, otherStats);
rockStates = RockStates.HitNothing;
}
break;
characterStats中实现函数重载方法:
public void TakeDamage(int damage,CharacterStats defener)
{
int currentDamage = Mathf.Max(damage - defener.CurrentDefence, 0);//保证伤害不会是负值
CurrentHealth = Mathf.Max(CurrentHealth - currentDamage, 0);//保证血量不会是负值
}
接下来解决Player 会回到之前的位置的问题,打开Animator:
发现Player受伤和眩晕的时候都调用了Stop Agent的方法,在动画结束之后代码将Agent的停止的状态给改回去了,这就导致了它在眩晕停止移动之后又返回去移动,那么当stop结束的时候又回到之前的坐标的位置了,
将这一方法注释掉,能保证敌人攻击我们不是一边攻击一边移动了
这样就完成了石头的第一个状态攻击Player,接下来完成另外两种状态:
首先确认是否是石头人,然后给石头人产生反击的伤害
case RockStates.HitEnemy://反击敌人状态
if(collision.gameObject.GetComponent())//确认是否是石头人
{
//给石头人产生反击的伤害
var otherStats = collision.gameObject.GetComponent();
otherStats.TakeDamage(damage,otherStats);
Destroy(gameObject);
}
break;
我们需要让Player可以攻击这个石头,并且在攻击石头的时候判断石头的状态如果是HitNothing的话,我就在攻击的那一瞬间给石头添加一个力给它反推回去,同时让它的状态切换到攻击敌人,
来到PlayerController当中,在攻击的时候要添加有些内容在Hit()方法内,让Player既可以攻击敌人也可以攻击石头,用一个方法区别开攻击的是敌人还是石头,一个简单的区别方法就是为它们添加不同的Tag,为石头添加Attackable标签:
在Hit方法中添加对标签的判断:
void Hit()
{
if(attackTarget.CompareTag("Attackable"))//石头
{
}
else//敌人
{
var targetStats = attackTarget.GetComponent();//临时变量
targetStats.TakeDamage(characterStats, targetStats);
}
}
在MouseManager中也添加对标签的判断:
void MouseControl()//返回鼠标左键点击返回值
{
if(Input.GetMouseButtonDown(0)&&hitInfo.collider != null)
{
if(hitInfo.collider.gameObject.CompareTag("Ground"))
{
OnMouseClicked?.Invoke(hitInfo.point); //当前OnMouseClicked事件如果不为空,将点击到地面上的坐标传回给这个事件(执行所有加入到onMouseClicked的函数方法)
}
if (hitInfo.collider.gameObject.CompareTag("Enemy"))
{
OnEnemyClicked?.Invoke(hitInfo.collider.gameObject); //当前OnEnemyClicked事件如果不为空,将点击到敌人的gameObject传回给这个事件(执行所有加入到OnEnemyClicked的函数方法)
}
if (hitInfo.collider.gameObject.CompareTag("Attackable"))
{
OnEnemyClicked?.Invoke(hitInfo.collider.gameObject); //当前OnEnemyClicked事件如果不为空,将点击到敌人的gameObject传回给这个事件(执行所有加入到OnEnemyClicked的函数方法)
}
}
}
PlayerController:写攻击石头的方法:
将Rock代码中枚举变量修改为Public方便PlayerController访问:
public RockStates rockStates;//枚举变量,石头的状态
//Animation Event
void Hit()//造成伤害
{
if(attackTarget.CompareTag("Attackable"))//可攻击的物体
{
if(attackTarget.GetComponent() && attackTarget.GetComponent().rockStates == RockStates.HitNothing)//判断是否是石头且状态为空
{
attackTarget.GetComponent().rockStates = RockStates.HitEnemy;//将石头状态改为攻击敌人状态
attackTarget.GetComponent().AddForce(transform.forward*20,ForceMode.Impulse);//为石头添加一个Player前方方向的冲击力
}
}
else//敌人
{
var targetStats = attackTarget.GetComponent();//临时变量
targetStats.TakeDamage(characterStats, targetStats);//造成伤害
}
}
这样石头砸到Player变为Nothing状态,Player就能通过攻击石头将石头变为攻击敌人状态,但如果石头没有砸到Player,就要判断石头静止移动就把它改为Nothing状态,用石头刚体的velocity检测石头的速度,因为是物理判断,所以在FixUpdate中判断:
用Vector3的一个方法sqrMagnitude来获得向量的长度
Rock:
private void FixedUpdate()
{
if(rb.velocity.sqrMagnitude < 1f)//石头速度小于1则变为HitNothing状态
{
rockStates = RockStates.HitNothing;
}
}
为了防止石头刚出来速度为0而变为HitNothing状态注意在Start中加入:
rb.velocity = Vector3.one;
在PlayerController当中也要加入:
attackTarget.GetComponent
//Animation Event
void Hit()//造成伤害
{
if(attackTarget.CompareTag("Attackable"))//可攻击的物体
{
if(attackTarget.GetComponent() && attackTarget.GetComponent().rockStates == RockStates.HitNothing)//判断是否是石头且状态为空
{
attackTarget.GetComponent().rockStates = RockStates.HitEnemy;//将石头状态改为攻击敌人状态
attackTarget.GetComponent().velocity = Vector3.one;//防止石头刚出来速度为0而变为HitNothing状态
attackTarget.GetComponent().AddForce(transform.forward*20,ForceMode.Impulse);//为石头添加一个Player前方方向的冲击力
}
}
else//敌人
{
var targetStats = attackTarget.GetComponent();//临时变量
targetStats.TakeDamage(characterStats, targetStats);//造成伤害
}
}
为了避免Player穿过石头,为player添加刚体组件:勾选isKinematic防止在斜坡的时候Gravity将人物往下掉而NavmeshAgen往上走
最后实现石头在击中石头人的时候产生一些碎片,这样看起来更真实一些
调整Particle System:
先获得石头碎片Particle System特效:
public GameObject breakEffect;
case RockStates.HitEnemy://反击敌人状态
if(collision.gameObject.GetComponent())//确认是否是石头人
{
//给石头人产生反击的伤害
var otherStats = collision.gameObject.GetComponent();
otherStats.TakeDamage(damage,otherStats);
Instantiate(breakEffect, transform.position, Quaternion.identity);//产生特效
Destroy(gameObject);
}
break;
}
人物和人物的攻击所有的内容基本就完成了
创建HealthBar Canvas:
创建一个Image,调整宽高:
因为Bar Holder没有Sprite,手动添加一个:
创建一个Square,
将Sprite复制一份到UI Assets,删除创建的Square
将Sprite拖入Bar Holder,更改颜色为灰色
在Bar Holder基础上再创建一个image:CurrentHealth,高度相同,Type为Filled,Horizontal,Right。
保存为Prefab
现在写一个代码HealthBarUI挂载到每一个敌人的身上,让它能够在受到攻击或者保持长久状态来显示HealthBar
HealthBarUI :
public class HealthBarUI : MonoBehaviour
{
public GameObject healthBarUIPrefab;//敌人血条
}
在代码上直接挂载血量预制体
为每一个敌人的Prefab都创建HealthBar Point
HealthBarUI:
using UnityEngine.UI;
public class HealthBarUI : MonoBehaviour
{
public GameObject healthBarUIPrefab;//敌人血条
public Transform barPoint;//血条创建的目标位置,血条需要实时等于这个坐标
Image healthSlider;//滑动条
Transform UIbar;//血条的位置
Transform cam;//相机位置
}
要拿到CharacterStats里的血量,并且完成CharacterStats里TakeDamage()方法更新UI,我们用Action的方法:创建了Action之后,每一个Action事件触发的时候我们能够激活所有订阅了它的函数的方法来调用它们。
public event Action
更新参数:
public void TakeDamage(CharacterStats attacker, CharacterStats defener)
{
int damage = Mathf.Max(attacker.CurrentDamage() - defener.CurrentDefence,0);//保证伤害不会是负值
CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);//保证血量不会是负值
if(attacker.isCritical)
{
defener.GetComponent().SetTrigger("Hit");
}
//TODO:Update UI
UpdateHealthBarOnAttack?.Invoke(CurrentHealth, MaxHealth);
//TODO:经验Update
}
public void TakeDamage(int damage,CharacterStats defener)
{
int currentDamage = Mathf.Max(damage - defener.CurrentDefence, 0);//保证伤害不会是负值
CurrentHealth = Mathf.Max(CurrentHealth - currentDamage, 0);//保证血量不会是负值
//Update UI
UpdateHealthBarOnAttack?.Invoke(CurrentHealth, MaxHealth);
}
HealthBarUI:为事件UpdateHealthBarOnAttack添加更新血条方法
CharacterStats currentSatas;//当前的CharacterStats拿到它的血量
private void Awake()
{
currentSatas = GetComponent();
currentSatas.UpdateHealthBarOnAttack += UpdateHealthBar;
}
private void UpdateHealthBar(int currentHealth, int maxHealth)//更新血条
{
if (currentHealth <= 0)
Destroy(UIbar.gameObject);
}
生成血条到世界坐标系:
public bool alwaysVisible;//是否是长久可见
public float visibleTime;//可视化时间
private void OnEnable()
{
cam = Camera.main.transform;
foreach (Canvas canvas in FindObjectsOfType
补充更新血条的方法:让滑动条滑动
private void UpdateHealthBar(int currentHealth, int maxHealth)//更新血条
{
if (currentHealth <= 0)
Destroy(UIbar.gameObject);
UIbar.gameObject.SetActive(true);//受到攻击的时候将血条强行设为可见
float sliderPercent = (float)currentHealth / maxHealth;//滑动条fillAmount参数值
healthSlider.fillAmount = sliderPercent;
}
将每个敌人的预制体添加HealthBarUI代码并拿到HealthBar Point位置。
为了让血条能够跟随敌人的移动,调用LateUpdate()方法:在上一帧渲染之后才执行这里的命令,保证人物先移动,然后血条跟上,不会造成血条有闪烁的效果,其实在做摄像机跟随的时候也应该在LateUpdate当中去做跟随
private void LateUpdate()
{
if(UIbar != null)//防止血条消失了而报错
{
UIbar.position = barPoint.position;//让血条坐标实时等于barPoint目标坐标
UIbar.forward = -cam.forward;//保证血条实时对准摄像机
}
我们希望血条不是一直显示,而是攻击的这一刻显示血条,过一段固定的时间之后血条就不显示出来了,我们用计时器的方法来做血条显示的启动
创建一个变量来记录剩余可显示的时间
private float timeLeft; //血条剩余可显示的时间
血量显示出来的时候重新计时:
private void UpdateHealthBar(int currentHealth, int maxHealth)//更新血条
{
if (currentHealth <= 0)
Destroy(UIbar.gameObject);
UIbar.gameObject.SetActive(true);//受到攻击的时候将血条强行设为可见
timeLeft = visibleTime;//血量显示出来的时候重新计时
float sliderPercent = (float)currentHealth / maxHealth;//滑动条fillAmount参数值
healthSlider.fillAmount = sliderPercent;
}
过一段固定的时间之后血条就不显示出来:
private void LateUpdate()//血条跟随敌人移动
{
if(UIbar != null)//防止血条消失了而报错
{
UIbar.position = barPoint.position;//让血条坐标实时等于barPoint目标坐标
UIbar.forward = -cam.forward;//保证血条实时对准摄像机
if(timeLeft <= 0 && !alwaysVisible)
{
UIbar.gameObject.SetActive(false);
}
else
{
timeLeft -= Time.deltaTime;
}
}
}
设置显示时间为3秒,这样就能让血条正常显示了
在CharacterData_SO中做一些经验值的判断:
添加一系列我们需要的参数的变量:
[Header("Kill")]
public int killPoint;//死亡经验点
[Header("Level")]
public int currentLevel;//当前等级
public int maxLevel;//最高等级
public int baseExp;//升级需要的总经验值
public int currentExp;//当前经验值
public float levelBuff;//等级属性加成
public float LevelMultiplier//等级提升百分比加成
{
get { return 1 + (currentLevel - 1) * levelBuff; }
}
public void UpdateExp(int point)//提升经验值
{
currentExp += point;//获取敌人的经验值
if(currentExp >= baseExp)
{
LeveUp();//升级
}
}
private void LeveUp()//升级
{
//所有你想提升的数据方法
currentLevel = Mathf.Clamp(currentLevel + 1, 0, maxLevel);//升级,不会超过最高等级
baseExp += (int)(baseExp * LevelMultiplier);//下一级所需的经验提升
maxHealth += (int)(maxHealth * LevelMultiplier);//最大血量提升
currentHealth = maxHealth;//回复满血
Debug.Log("LEVEL UP!" + currentLevel + "Max Health:" + maxHealth);
}
填写属性值:
在计算伤害值的那一刻进行引用:
在CharacterStats代码中造成伤害的方法中进行经验的更新:
public void TakeDamage(CharacterStats attacker, CharacterStats defener)//被攻击者调用这个方法
{
int damage = Mathf.Max(attacker.CurrentDamage() - defener.CurrentDefence,0);//保证伤害不会是负值
CurrentHealth = Mathf.Max(CurrentHealth - damage, 0);//保证血量不会是负值
if(attacker.isCritical)
{
defener.GetComponent().SetTrigger("Hit");
}
//TODO:Update UI
UpdateHealthBarOnAttack?.Invoke(CurrentHealth, MaxHealth); //更新参数
//TODO:经验Update
if(CurrentHealth <= 0)//被攻击者死亡
{
attacker.characterData.UpdateExp(characterData.killPoint);//攻击者提升经验值
}
}
现在升级后经验和等级就提升了。
现在来制作Player UI的显示,我们要显示的就是Player当前的血量还有经验值,也可以显示它的等级
新建一个Canvas:PlayerHealth Canvas保留本来的Render Mode是Screen Space_Overlay,让它可以覆盖整个屏幕
基本屏幕分辨率设置为:1920×1080,Match0.5兼顾长和宽
创建Player血量Image:HealthBar Holder和滑动条Health Silider
复制一份改为经验值Exp Holder 和经验滑动条Exp Silider
创建Text文本Level:
选择字体大小
下面创建一个脚本让人物数据进行匹配:
创建脚本PlayerHealthUI,挂载到PlayerHealth Canvas上以便获取到子物体:
PlayerHealthUI:
我们希望以后PlayerHealth Canvas无论切换到任何场景都能够保持显示,会将它设置成Prefab然后在不同的场景当中去添加,我们不希望用拖拽的方式去将PlayerHealth Canvas里面的每一个子物体的组件去添加,接下来用代码的方式去获得子物体和子物体的子物体身上的组件:
先拿到子物体和子物体的子物体身上的组件,然后用GameManager拿到属性进行Update更新:
using UnityEngine.UI;
public class PlayerHealthUI : MonoBehaviour
{
Text levelText;//等级文本
Image healthSlider;//血量滑动条
Image expSlider;//经验滑动条
private void Awake()//拿到组件
{
levelText = transform.GetChild(2).GetComponent();
healthSlider = transform.GetChild(0).GetChild(0).GetComponent();
expSlider = transform.GetChild(1).GetChild(0).GetComponent();
}
private void Update()
{
levelText.text = "Level " + GameManager.Instance.playerStats.characterData.currentLevel.ToString("00");
UpdateHealth();//更新血条
UpdateExp();//更新经验值
}
void UpdateHealth()//更新血条
{
float sliderPercent = (float)GameManager.Instance.playerStats.CurrentHealth / GameManager.Instance.playerStats.MaxHealth;
healthSlider.fillAmount = sliderPercent;
}
void UpdateExp()//更新经验值
{
float sliderPercent = (float)GameManager.Instance.playerStats.characterData.currentExp / GameManager.Instance.playerStats.characterData.baseExp;
expSlider.fillAmount = sliderPercent;
}
}
同样用GameManager在石头打死Boss进行Player经验的获取:
public void TakeDamage(int damage,CharacterStats defener)//被攻击者调用这个方法
{
int currentDamage = Mathf.Max(damage - defener.CurrentDefence, 0);//保证伤害不会是负值
CurrentHealth = Mathf.Max(CurrentHealth - currentDamage, 0);//保证血量不会是负值
//Update UI
UpdateHealthBarOnAttack?.Invoke(CurrentHealth, MaxHealth);//更新参数
//经验Update
if(CurrentHealth <= 0)
{
GameManager.Instance.playerStats.characterData.UpdateExp(characterData.killPoint);
}
}
我们用Shader Graph来制作一个旋转的传送门:Portal Shader Graph
打开:设置Main Preview显示的样子为Quda平面,更方便看出效果
创建Twirl旋转效果OutPut输出跟Voronoi节点做一个连接,创建一个Time和float类型的变量相乘输出到Twirl的Offset,创建float类型的变量Strength控制强度
如果现在用这个节点去创建一个Shader的画它将是一个方块的形状,我希望它将是出现一个圆形周围有渐变,这就需要有一个Texture,并创建一个Texture变量选择默认的样式
然后现在将它的输出和Voronoi相乘连接到一起
接下来需要设置一个旋转门的颜色 ,创建一个Color型的变量Color,添加到Multiply
将Out输出到Emission,为了让Main Preview背景颜色不是深色我们将Surface设置为Transparent,Out输出也连接到Alpha,为了2面都可以看到传送门我们将Two Slider选上
保存好返回Unity,在Portal Shader Graph基础上新建一个Material:Green Portal
创建一个Quad,添加 Green Portal材质球
将Cast Shadows设置为off防止出现阴影
传送门亮度不明显,将Color的Mode设置为HDR
增强强度添加Power和Power强度变量PowerScale
现在让传送门可以传送Player:
先让Player进入左边的传送门按住E键可以进入右边的传送门:
给传送门创建一个空物体DesinationPoint,添加Icon ,给不同的传送门添加不同的标签或者序号,设定传送门可以传送到指定的序号点,就可以把Player传送到那个位置了
写2个代码:
TransitionPoint挂载在传送门身上,需要另外一个代码:传送门的终点TransitionDestination挂载在蓝色的球上(DesinationPoint),挂载好代码后将传送门保存为预制体
TransitionPoint:枚举同场景传送和异场景传送
public class TransitionPoint : MonoBehaviour
{
public enum TransitionType//枚举变量
{
SameScene,DifferentScene
}
[Header("Transition Info")]//传送信息
public string sceneName;//传送的场景名字
public TransitionType transitionType;//传送的类型:下拉菜单进行选择
}
TransitionDestination:枚举传送点的标签
public class TransitionDestination : MonoBehaviour
{
public enum DestinationTag//枚举变量:传送的DesinationPoint标签
{
ENTER,A,B,C
}
public DestinationTag destinationTag;
}
TransitionPoint:生成传送点的标签
public TransitionDestination.DestinationTag destinationTag;//传送的目标位置标签
将传送门的预制体添加Box collider,设置为Trigger,通过触发检测判断Player是否进入传送门:
TransitionPoint:
private bool canTrans;//判断能否传送
private void OnTriggerStay(Collider other)
{
if(other.CompareTag("Player"))
{
canTrans = true;
}
}
private void OnTriggerExit(Collider other)
{
if (other.CompareTag("Player"))
{
canTrans = false;
}
}