EnemyMotor.cs
注意:
1.transform.LookAt(targetPos) 表示当该物体设置了Lookat并指定了目标物体时,该物体的z轴将始终指向目标物体,所以MovementForward() 函数只用改变Z坐标即可,而且此处使用Time.deltaTime,是由于MovementForward()会在Pathfinding()中调用,而Pathfinding()将会在EnemyAI中的Update中调用,这使得敌人运动可以保证是恒速。
2.由于前期没有写WayLIne类,所以需要测试代码,测试子功能是否正确,注释掉部分是测试该功能的代码。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 敌人马达 提供移动、旋转、寻路的功能
///
public class EnemyMotor : MonoBehaviour
{
//临时创建路点 用于测试
//public Transform[] points;
//创建线路
public WayLine line;
//速度
public float Speed = 5;
//移动
public void MovementForward()
{
transform.Translate(0, 0, Speed * Time.deltaTime);
}
//朝着路点旋转
public void LookRotation(Vector3 targetPos)
{
transform.LookAt(targetPos);
}
private int currentIndex;
public bool Pathfinding()
{
//寻路结束
if (currentIndex >= line.Points.Length || line.Points == null)
return false;
LookRotation(line.Points[currentIndex]);
MovementForward();
//到达路点后,索引值++
if (Vector3.Distance(this.transform.position, line.Points[currentIndex]) <= 0.1f)
currentIndex++;
//临时创建路点 用于测试
//if (currentIndex >= points.Length || points == null)
// return false;
//LookRotation(points[currentIndex].position);
//MovementForward();
//if(Vector3.Distance(this.transform.position, points[currentIndex].position) <= 0.1f)
//currentIndex++;
return true;
}
}
EnemyStatusInfo
注意:
public EnemySpawn spawn; 是为了EnemySpwn类中的CreatEnemy()函数中产生一个敌人后,将生成该敌人的生成器赋给该spawn变量,便于在EnemyStatusInfo类中Death()函数中完成一个敌人死后,可以用该生成器生成下一个敌人,避免使用到其他生成器,造成同一路线生成多个敌人。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
// 敌人的状态信息
///
public class EnemyStatusInfo : MonoBehaviour
{
public EnemySpawn spawn;
//当前血量
public float currentHP = 80;
private void Demage(float amount)
{
currentHP -= amount;
if (currentHP <= 0)
//死亡
Death();
}
private EnemyAnimation anim;
//死亡延迟时间
public float deathDelay = 5;
private void Death()
{
//销毁当前游戏对象
Destroy(gameObject, deathDelay);
//播放死亡动画
anim = GetComponent<EnemyAnimation>();
anim.Play(anim.deatName);
//生成下一个敌人
spawn.GenerateEnemy();
}
}
EnemyAnimation
注意:
1.定义各种动画名称的变量的原因:
EnemyAnmation脚本挂在Solier物体上,而其子物体Model上有Animation脚本,由于不同Model的Animation中动作的名称不同,所以我们要定义各种动画名称的变量,且是Public的,这样就可以对照着不同Model的动画名称在界面中填入相对应的名称,这样的话就算Soldier的子物体Model修改了,我们也只用在EnemyAnmation脚本的界面中做出相应的修改即可,不用改变EnemyAnmation代码内容。
2. anim = GetComponentInChildren< Animation>(); 因为EnemyAnmation脚本挂在父物体Solier物体上,而我们需要寻找子物体Model上的Animation组件,所以此处使用GetComponentInChildren。
3. Play和CrossFade的区别:
Play:直接切换动画,如果人物之前处于倾斜跑步状态,则会立即变成站立状态,表现上比较不真实,特别是当两个动画姿势差别较大时。
CrossFade:通过动画融合来切换动画,第二个参数可以指定融合的时间,如果人物之前处于倾斜跑步状态,则会在指定的融合时间内逐渐变成站立状态,表现上接近真实的人物动作切换效果。
EnemyAnimation
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 敌人动画
///
public class EnemyAnimation : MonoBehaviour
{
public string runName = "run";
//攻击动画
public string atkName = "shooting";
//死亡动画
public string deatName = "death";
//闲置动画
public string idleName = "idleWgun";
private Animation anim;
private void Awake()
{
anim = GetComponentInChildren<Animation>();
}
//播放动画
public void Play(string name)
{
anim.CrossFade(name);
}
//判断动画是否播放
public bool IsPlaying(string name)
{
return anim.IsPlaying(name);
}
}
EnemyAI
1.快捷键:选中内容,按control+R+M键,创建新方法。
2.Update中执行游戏逻辑,根据敌人状态,执行寻路或攻击。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 敌人 人工智能
///
public class EnemyAI : MonoBehaviour
{
//获取[敌人马达、敌人动画]脚本对象引用
private EnemyMotor motor;
private EnemyAnimation anim;
//枚举敌人状态
public enum State
{
//攻击
Attack,
//寻路
Pathfinding
}
private void Start()
{
motor = GetComponent<EnemyMotor>();
anim = GetComponent<EnemyAnimation>();
}
private State state = State.Pathfinding;
private float atkTimer;
//攻击间隔
public float atkInterval = 0.5f;
//Update中执行的是游戏逻辑
private void Update()
{
switch (state)
{
case State.Attack:
Attack();
break;
case State.Pathfinding:
Pathfinding();
break;
default:
break;
}
}
private void Attack()
{
//当攻击动画未播放时,播放闲置动画
if (!anim.IsPlaying(anim.atkName))
{
anim.Play(anim.idleName);
}
if (atkTimer <= Time.time)
{
anim.Play(anim.atkName);
atkTimer = Time.time + atkInterval;
}
}
private void Pathfinding()
{
//执行寻路
//如果寻路结束,修改状态为攻击
anim.Play(anim.runName);
if (!motor.Pathfinding())
state = State.Attack;
}
}
创建根路线:
1.调试阶段让路点坐标的标签显示出来,方便观察。
2.敌人生成器EnemySpawn脚本应该挂在WayLineRoot这个父物体身上,这样就可以选择下面不同的子路线来创建不同的敌人。
WayLine
1.Points这个数组存放的是三维向量的引用,所以默认是null,该Point变量的属性是可读可写。
2.当创建一条线路时,需要初始化线路,所以创建了构造函数。
3.当创建的数组内部存放的是引用类型的时候,我们往往需要New 引用类型来初始化。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 路线类
///
public class WayLine
{
//坐标数组 装的是三维向量的引用 默认是null
public Vector3[] Points { get; set; }
//线路是否可用 默认是 false
public bool IsUseAble { get; set; }
//构造函数,初始化属性
public WayLine(int countPoints)
{
this.Points = new Vector3[countPoints];
this.IsUseAble = true;
}
}
EnemySpawn
重点:1.lines这数组用于储存所有的路线,也是引用类型,所以需要New 引用类型来初始化,New完以后lines成为一个有数组大小的数组,且lines,IsUseable=true,但是每个数组内位置仍然为null,接下来就是遍历子物体,即界面中WayLIneRoot下面的子路线Wayline01、Wayline02、Wayline03,由于每条子路线为null,所以将每条子路线都New 引用类型,这样就初始化了每条子路线,但现在每条子路线中的Points是(0,0,0),所以遍历每条子路线的路点,将每个路点都赋值。
2.由于有的线路如果当前产生了敌人,有敌人占据,则该线路不可用 IsUseable = false,所以需要SelectWayLines()方法将所有的线路进行筛选,选出所有的有用路线,且将该路线放在List<>集合里面,由于该方法的返回值是WayLine[]数组,所以需要ToArray操作,将集合转为数组。
3.在所选择路线的起始点创建一个敌人以后,需要注意需要配置敌人的信息,这个敌人创建好以后,上面应该自动挂着EnemyAI、EnemyAnimation、EnemyStatusInfo、EnemyMotor脚本(由于创建预制体时候已经设置好了哦),由于寻路需要知道线路,所以找到所创建的敌人的EnemyMotorjiao组件的引用,将路线line传给它,此外该线路被这个敌人占据,所以当前不可用。
4.由于后面敌人可能会死亡,所以每次创建敌人时,将产生该敌人的生成器保留,为了下次可能会使用该生成器来创建这条线路的敌人,避免产生一条线路产生多个敌人。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
///
/// 敌人生成器
///
public class EnemySpawn : MonoBehaviour
{
//存储所有路线
private WayLine[] lines;
//记录敌人预制体
public GameObject[] enemyTypes;
//开始需要创建的敌人数目
public int startCount = 2;
//记录已经产生的敌人数量
private int spawnedCount;
//记录产生敌人数量的上限
public int maxCount;
//最大延迟时间
public float maxDelay = 10;
//计算所有路线以及坐标
private void CalculateWayLines()
{
lines = new WayLine[transform.childCount];
for (int i = 0; i < transform.childCount; i++)
{
Transform WaylinesTF = transform.GetChild(i);
lines[i] = new WayLine(WaylinesTF.childCount);
for (int pointsIndex = 0; pointsIndex < WaylinesTF.childCount; pointsIndex++)
{
lines[i].Points[pointsIndex] = WaylinesTF.GetChild(pointsIndex).position;
}
}
}
//产生敌人
private void CreatEnemy()
{
//获取所有有用路线
WayLine[] useableLines = SelectWayLines();
//随机选择其中一条路线
WayLine wayLine = useableLines[Random.Range(0, useableLines.Length)];
//创建一个敌人
int enemyTypeIndex = Random.Range(0, enemyTypes.Length);
GameObject enemyGO = Instantiate(enemyTypes[enemyTypeIndex], wayLine.Points[0], Quaternion.identity) as GameObject;
//给敌人配置信息
EnemyMotor motor = enemyGO.GetComponent<EnemyMotor>();
motor.line = wayLine;
//wayLine.IsUseAble = false;//产生了敌人,该路线已经被占用
motor.line.IsUseAble = false;
enemyGO.GetComponent<EnemyStatusInfo>().spawn = this;
}
//计算出所有的路线,并且产生初敌人
private void Start()
{
CalculateWayLines();
for (int i = 0; i < startCount; i++)
{
GenerateEnemy();
}
}
public void GenerateEnemy()
{
spawnedCount++;
if (spawnedCount >= maxCount) return;
float delay = Random.Range(0, maxDelay);
//延迟产生敌人
Invoke("CreatEnemy", delay);
}
//获取所有有用路线
private WayLine[] SelectWayLines()
{
List<WayLine> WayLines = new List<WayLine>(lines.Length);
foreach (var item in lines)
{
if (item.IsUseAble) WayLines.Add(item);
}
return WayLines.ToArray();
}
}
敌人模块代码的逻辑顺序:
EnemySpawn(挂在WayLineRoot产生敌人) --> WayLine --> EnemyAI --> EnemyMotor --> EnemyAnimation --> EnemStausInfo(如果敌人受伤或死亡)
注意代码的执行顺序:由于Sodiler物体上挂有四个脚本,需要注意在同一物体上脚本的执行,关注下Awake、Start、Update的执行顺序。
注意:
1.Gun应该放在FistPersonCharacter下面,而不是FPSController下面,因为Camera组件在FistPersonCharacter里面,这样Gun才会随着视角移动旋转,
2.因为可能有多把枪,所以创建空物体Gun,再做一个空物体HandGun作为手枪分类,该处手枪预制体放在HangGun下面。