21:More Enemies 制作更多的敌人
修改EnemyController防止报错:
private void Start()
{
if(isGuard)//判断是否是站桩怪
{
enemyStates = EnemyStates.GUARD;
}
else//巡逻怪
{
enemyStates = EnemyStates.PATROL;
GetNewWayPoint();//得到初始移动的点
}
//FIXME:切换场景后修改掉
GameManager.Instance.AddObserver(this);//让观察者主动添加到列表
}
//切换场景时启用
//private void OnEnable()
//{
// GameManager.Instance.AddObserver(this);//让观察者主动添加到列表
//}
private void OnDisable()//销毁完成之后执行
{
if (!GameManager.IsInitialized) return;
GameManager.Instance.RemoveObserver(this);//让观察者移除列表
}
Player死亡的时候仍然可以通过点击鼠标来移动,在移动方法中加上死亡判断:
public void MoveToTarget(Vector3 target) //必须包含参数Vector3,保证函数命名方式定义方式是和onMouseClicked完全一致
{
if (isDead) return;
StopAllCoroutines();//打断攻击
agent.isStopped = false;//可以进行移动
agent.destination = target;
}
private void EventAttack(GameObject target)
{
if (isDead) return;
if (target != null)
{
attackTarget = target;
characterStats.isCritical = UnityEngine.Random.value < characterStats.attactData.criticalChance;
StartCoroutine(MoveToAttackTarget());//协程:攻击敌人
}
}
这样就解决了
如果复制出来一份敌人,将一个敌人杀死2个会一起死亡,这个问题如何解决:
ScriptableObject可以看作是一个模板,只要将这个模板在运行的时候复制出来一份给另一个敌人,它们就有了各自独立的那份Copy数据了,
public CharacterData_SO templateData;//模板数据
public CharacterData_SO characterData;
private void Awake()
{
if(templateData != null)
{
characterData = Instantiate(templateData);//生成一份模板数据
}
}
这样第一个敌人就做好了
现在尝试做其他的敌人:
?创建乌龟的Animator:
?选择覆盖Slime动画器进行覆盖:
?创建乌龟的Character Data和Attack Data并赋值
?这样就又制作一个敌人了
导入另外2个素材:
将材质升级到URP
?将2个敌人添加碰撞器添加到场景中:
?
22:Setup Grunt 设置兽人士兵
复制一份Enemy_SlimeController改名为Enemy_GruntController在里面更改条件,给到兽人士兵
所有的动画数据都需要进行修改:并且第二个攻击设置为技能Skill攻击?
?
?动画修改完成,接下来挂载代码Grunt,继承于EnemyController
Grunt:
public class Grunt : EnemyController
{
}
?设置属性值:
添加攻击事件
在EnemyController中将攻击目标设置为 protected GameObject attackTarget;可以让子类Grunt访问:
public class Grunt : EnemyController
{
[Header("Skill")]
public float kickForce = 10;//击飞的力
public void KickOff()//击飞方法
{
if (attackTarget != null)
{
transform.LookAt(attackTarget.transform);
//获得击飞的方向
}
}
}
?添加击飞事件:
现在就可以击飞了,
为Player添加眩晕动画:
?补充代码:
public class Grunt : EnemyController
{
[Header("Skill")]
public float kickForce = 10;//击飞的力
public void KickOff()//击飞方法
{
if (attackTarget != null)
{
transform.LookAt(attackTarget.transform);
//获得击飞的方向
Vector3 direction = attackTarget.transform.position - transform.position;
direction.Normalize();//单位化
attackTarget.GetComponent<NavMeshAgent>().isStopped = true;
attackTarget.GetComponent<NavMeshAgent>().velocity = direction * kickForce;
attackTarget.GetComponent<Animator>().SetTrigger("Dizzy");//播放眩晕动画
}
}
}
现在Player就可以被眩晕了
发现一个问题:当敌人暴击时,触发了受伤的动画,无论是出发了受伤的动画也好还是触发了眩晕的动画也好它仍然可以移动,我们希望的是当触发这些动画的时候是不能移动的,被击晕的效果,所以我们来学习一个新的方法:在Animator当中我们来添加代码来控制它切换的条件
?Add Behaviour:为它添加一个代码脚本来控制它进入动画和执行动画以及退出动画的时候要执行哪些命令
为GetHit创建一个StopAgent代码:?
?StopAgent:
// OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.GetComponent<NavMeshAgent>().isStopped = true;
}
// OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.GetComponent<NavMeshAgent>().isStopped = true;
}
// OnStateExit is called when a transition ends and the state machine finishes evaluating this state
override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
animator.GetComponent<NavMeshAgent>().isStopped = false;
}
现在人物受伤就没有办法移动了
当敌人攻击的时候Player移动的话,敌人的攻击也会有偏移,所以希望攻击的时候也能有一个停止的效果
找到Enemy的所有的Animator Controller ,为攻击添加StopAgent代码:
?有个问题:当敌人对Player产生攻击动画的时刻,StopAgent的Update一直在执行,这一时刻Player的攻击导致敌人死亡的时候,敌人身上的Animator就马上替换到了死亡的Animation,导致它丢失了NavMeshAgent,原因是敌人死亡的时候agent关闭了
?解决方法:将agent范围缩小,而不关闭。
case EnemyStates.DEAD:
coll.enabled = false;
//agent.enabled = false;
agent.radius = 0;
Destroy(gameObject, 2f);
break;
这样就解决了这个问题并且敌人死亡的时候不会成为障碍物了
23:Extension Method 扩展方法
当Player跑到敌人身后的时候敌人的攻击动画应该不会对Player造成伤害
创建一个代码Extension Method扩展方法在现有的函数和现有的类当中去延展一个我们想要的个性化的方法
Extension Method:将当前的方向和角度作为一个扩展
所有的扩展方法都不会继承其他的类,它必须是一个static
?Extension Method:this后面跟类名的扩展,逗号后面跟参数
public static class ExtensionMethod
{
private const float dotThreshold = 0.5f;
public static bool isFacingTarget(this Transform transform, Transform target)
{
var vectorToTarget = target.position - transform.position;
vectorToTarget.Normalize();
float dot = Vector3.Dot(transform.forward, vectorToTarget);
return dot >= dotThreshold;
}
}
修改EnemyController中的Hit方法:
void Hit()
{
if(attackTarget != null && transform.isFacingTarget(attackTarget.transform))
{
var targetStats = attackTarget.GetComponent<CharacterStats>();//临时变量
targetStats.TakeDamage(characterStats, targetStats);
}
}
现在玩家走到敌人后方就不会受到伤害了
24:Setup Golem 设置石头人Boss
添加一个Animator Ovrrider Controller:Enemy_Golem Controller
覆盖Grunt Controller并拖拽动画进来
?
?挂载代码Golem:同样继承于EnemyController:
? 将Enemy的characterStats修改为? protected CharacterStats characterStats;方便子类引用
Golem:
using UnityEngine.AI;
public class Golem : EnemyController
{
[Header("Skill")]
public float kickForce = 25;//击飞的力
public void KickOff()//击飞方法并造成伤害
{
if(attackTarget != null && transform.isFacingTarget(attackTarget.transform))
{
var targetStats = attackTarget.GetComponent<CharacterStats>();
//获得击飞的方向
Vector3 direction = (attackTarget.transform.position - transform.position).normalized;
//direction.Normalize();
attackTarget.GetComponent<NavMeshAgent>().isStopped = true;
attackTarget.GetComponent<NavMeshAgent>().velocity = direction * kickForce;
//产生伤害
targetStats.TakeDamage(characterStats,targetStats);
}
}
}
挂在代码, 添加事件
创建GolemData,填写属性
?这样就制作出石头人的正常攻击了
25:Throw Rocks 设置可以扔出的石头
我们发现Player走进石头人无法进行攻击,原因是无法到达石头人的中心区域
需要修改PlayerController:
用到stopDistance:? ? private float stopDistance;
在Awake初始化:? ?stopDistance = agent.stoppingDistance;
?攻击敌人移动到攻击范围:
IEnumerator MoveToAttackTarget()//协程:攻击敌人
{
agent.isStopped = false;
//攻击敌人移动到攻击范围
agent.stoppingDistance = characterStats.attactData.attackRange;
transform.LookAt(attackTarget.transform);//转向我的攻击目标
//修改攻击范围参数
while(Vector3.Distance(attackTarget.transform.position,transform.position) > characterStats.attactData.attackRange)
{
agent.destination = attackTarget.transform.position;
yield return null;
}
agent.isStopped = true;
//Attack
if(lastAttackTime < 0)
{
anim.SetTrigger("Attack");
anim.SetBool("Critical", characterStats.isCritical);
//重置冷却时间
lastAttackTime = characterStats.attactData.coolDown;
}
}
?正常移动移动到目标位置距离一个身位:
public void MoveToTarget(Vector3 target) //必须包含参数Vector3,保证函数命名方式定义方式是和onMouseClicked完全一致
{
if (isDead) return;
StopAllCoroutines();//打断攻击
agent.isStopped = false;//可以进行移动
agent.destination = target;
//正常移动移动到目标位置距离一个身位
agent.stoppingDistance = stopDistance;
}
现在就可以根据武器的长度来攻击敌人了?。
将Enemy的GetHit动画都添加Stop Agent代码:
接下来制作石头人扔出石头了:
扔出石头要做一些判断:首先找到Player,然后为它设置一个加速度飞向我们的Player,另外在碰撞到我们的Player的时候对Player产生伤害甚至将Player击开一段距离
为它创建一个代码Rock:添加Rigidbody组件模拟重力效果,添加Mesh Collider
?Rock:
public class Rock : MonoBehaviour
{
private Rigidbody rb;
[Header("Basic Settings")]
public float force;//向前冲击的力
[HideInInspector]
public GameObject target;//石头的目标
private Vector3 direction;//飞行的方向
private void Start()
{
rb = GetComponent<Rigidbody>();
FlyToTarget();//朝着目标飞
}
public void FlyToTarget()//朝着目标飞
{
direction = (target.transform.position - transform.position + Vector3.up).normalized;
rb.AddForce(direction * force, ForceMode.Impulse);//瞬间的冲击力
}
}
将石头制作为预制体,修改石头人代码:
Golem:?
public GameObject rockPrefab;//石头
public Transform handPos;//扔石头时手的坐标
拿到石头和扔石头的点的坐标:
//Animation Event
public void ThrowRock()//扔石头
{
if(attackTarget != null)
{
var rock = Instantiate(rockPrefab, handPos.position, Quaternion.identity);
rock.GetComponent<Rock>().target = attackTarget;
}
}
添加扔石头事件:
?输入扔石头的力:
?远程攻击设置为10:
?现在石头人就可以扔石头了,但扔石头的一瞬间Player拉托了范围石头就会消失,修改一下代码:
public void FlyToTarget()//朝着目标飞
{
if(target == null)
{
target = FindObjectOfType<PlayerController>().gameObject;
}
direction = (target.transform.position - transform.position + Vector3.up).normalized;
rb.AddForce(direction * force, ForceMode.Impulse);//瞬间的冲击力
}
|