把场景模型文件拖拽到层级视图中,创建游戏所需要的场景,同时把场景中所需要的元素补充完整。
1、把Assets/Models文件夹里面的env_stealth_static拖拽到层级视图中,transform组件属性设置如下:
2、给env_stealth_static添加MeshCollider组件,组件属性设置如下:
3、把Assets/Models文件夹里面的prop_battleBus拖拽到env_stealth_static下做为子物体,其transform组件属性设置如下:
4、给prop_battleBus添加MeshCollider组件,组件属性设置如下:
整个场景中的灯光颜色偏暗,当玩家触发到场景中的警报后,警报灯会亮起来,报警灯的颜色为红色,真实的模拟了现实生活中报警灯的闪烁效果,具体实现步骤如下
1、更改层级视图中DirectionalLight的Light组件属性如下:
2、在层级视图中创建DirectionalLight类型的灯光,名字改为AlarmLight,其transform组件属性设置如下:
3、AlarmLight的Light组件属性设置如下:
4、给AlarmLight添加脚本组件AlarmLight,实现报警灯的切换。
public class AlarmLight : MonoBehaviour {
public bool alarmOn = false;
public float turnSpeed = 3;
Light light;
float target = 2;
void Start () {
light = GetComponent<Light>();
}
void Update () {
if (alarmOn)
{
light.intensity = Mathf.Lerp(light.intensity, target, Time.deltaTime * turnSpeed);
if (light.intensity>=1.98f)
{
target = 0;
}
else if (light.intensity<=0.05f)
{
target = 2;
}
}
else
{
light.intensity = Mathf.Lerp(light.intensity, 0, Time.deltaTime * turnSpeed);
}
}
}
通过键盘控制玩家的前后左右移动,玩家碰到场景中的墙壁时,就不能向前移动了,主要使用了动画器组件来切换不同的动画状态,同时使用物理引擎让玩家不会掉到场景以下。
1、把Assets/Models文件夹下的char_ethan的动画类型设置为人形动画,然后拖到层级视图中,名字改为Player,tag值设置为Player,具体设置如下:
2、Player的transform组件属性设置如下:
3、给Player添加Rigidbody组件,其属性设置如下:
4、给Player添加CapsuleCollider组件,其属性设置如下:
5、给Player添加AudioSource组件,其属性设置如下:
6、创建动画控制器PlayerAni,双击打开后添加动画参数如下:
7、按以下顺序分别把动画片断Idle、Sneak、Dying拖到动画控制器里面,效果图如下:
8、创建动画融合树,名字改为LocalMotion,然后设置Idle、Sneak、Dying和LocalMotion之间的过渡,过渡的逻辑如下:
9、双击LocalMotion融合树,添加两个动画片断Walk和Run,相关参数设置如下:
10、动画融合树LocalMotion的效果图如下:
11、设置Idle到Sneak的过渡条件如下:
12、设置Sneak到Idle的过渡条件如下:
13、设置Idle到LocalMotion的过渡条件如下:
14、设置LocalMotion到Idle的过渡条件如下:
15、设置LocalMotion到Sneak的过渡条件如下:
16、设置Sneak到LocalMotion的过渡条件如下:
17、设置Any State到Dying的过渡条件如下:
18、给Player添加动画器组件,其属性设置如下:
19、给Player添加脚本组件PlayerHealth和PlayerMove,实现键盘控制Player的移动。
public class PlayerHealth : MonoBehaviour {
public float health = 100;
Animator ani;
bool isDead = false;
bool gameIsEnd = false;
public AudioClip endGameClip;
private float timer = 0;
void Start () {
ani = GetComponent<Animator>();
}
void Update () {
if (health<=0&&isDead==false)
{
ani.SetTrigger(Parameters.Dead);
isDead = true;
}
else if (isDead&&gameIsEnd==false)
{
gameIsEnd = true;
AudioSource.PlayClipAtPoint(endGameClip, transform.position);
Invoke("ChangeScene", 4.5f);
}
}
void ChangeScene()
{
SceneManager.LoadScene(0);
}
}
public class PlayerMove : MonoBehaviour {
public bool hasKey = false;
private float hor, ver;
public float turnSpeed = 20;
private bool isSneak = false;
private Animator ani;
private AudioSource aud;
private PlayerHealth playerHealth;
void Start () {
ani = GetComponent<Animator>();
aud = GetComponent<AudioSource>();
playerHealth = GetComponent<PlayerHealth>();
}
void Update () {
if (playerHealth.health>0)
{
hor = Input.GetAxis("Horizontal");
ver = Input.GetAxis("Vertical");
}
else
{
hor = 0;
ver = 0;
}
if (hor != 0 || ver != 0)
{
ani.SetFloat(Parameters.Speed, 1, 0.1f, Time.deltaTime);
Turn(hor, ver);
}
else {
ani.SetFloat(Parameters.Speed, 0);
}
if (Input.GetKey(KeyCode.LeftShift)&&ani.GetFloat(Parameters.Speed)>0.1f)
{
isSneak = true;
}
else
{
isSneak = false;
}
ani.SetBool(Parameters.Sneak,isSneak);
FootStep();
}
void Turn(float hor,float ver)
{
Vector3 dir = new Vector3(hor, 0, ver);
Quaternion qua = Quaternion.LookRotation(dir);
transform.rotation = Quaternion.Lerp(transform.rotation, qua, Time.deltaTime * turnSpeed);
}
void FootStep()
{
if (ani.GetCurrentAnimatorStateInfo(0).IsName(Parameters.LocalMotion))
{
if (aud.isPlaying==false)
{
aud.Play();
}
}
else
{
aud.Stop();
}
}
}
当玩家移动时,摄像机会保持一定的角度跟随玩家一起移动,在摄像机移动的过程中,摄像机的位置会有一个平滑的过渡,以增加视觉效果。
1、给层级视图中的MainCamera添加tag值为MainCamera,具体设置如下:
2、MainCamera的transform组件属性设置如下:
3、给MainCamrea添加CameraMove脚本,实现摄像机的跟随效果。
public class CameraMove : MonoBehaviour {
public float moveSpeed = 3;
private Transform player;
private Vector3 direction;
RaycastHit hit;
float distance;
private Vector3[] viewPoints;
private float turnSpeed = 10;
void Start () {
player = GameObject.FindWithTag(Tags.Player).transform;
viewPoints = new Vector3[5];
distance = Vector3.Distance(player.position, transform.position);
direction = player.position - transform.position;
}
void LateUpdate () {
Vector3 startPos = player.position - direction;
viewPoints[0] = startPos;
Vector3 endPos = player.position + Vector3.up * distance;
viewPoints[4] = endPos;
viewPoints[1] = Vector3.Lerp(startPos, endPos, 0.25f);
viewPoints[2] = Vector3.Lerp(startPos, endPos, 0.5f);
viewPoints[3] = Vector3.Lerp(startPos, endPos, 0.75f);
for (int i = 0;i < viewPoints.Length;i++)
{
Debug.Log(viewPoints[i]);
}
Vector3 targetPos = viewPoints[0];
for (int i = 0;i < viewPoints.Length;i++)
{
if (CanLook(viewPoints[i]))
{
targetPos = viewPoints[i];
break;
}
}
transform.position = Vector3.Lerp(transform.position, targetPos, Time.deltaTime * moveSpeed);
SmoothRotate();
}
bool CanLook(Vector3 pos)
{
Vector3 dir = player.position - pos;
if (Physics.Raycast(pos,dir,out hit))
{
if (hit.collider.tag==Tags.Player)
{
return true;
}
}
return false;
}
void SmoothRotate()
{
Vector3 dir = player.position - transform.position;
Quaternion qua = Quaternion.LookRotation(dir);
transform.rotation = Quaternion.Lerp(transform.rotation, qua, Time.deltaTime * turnSpeed);
transform.eulerAngles = new Vector3(transform.eulerAngles.x, 0, 0);
}
}
游戏运行后,会播放背景音乐,当玩家触发警报后,红色的警报灯会亮起,同时会伴有警报声,另外背景音乐也会进行更换。
1、在层级视图中创建空物体,名字为GameController,设置其tag值为GameController,具体设置如下:
2、给GameController添加AudioSource组件,其属性设置如下:
3、给GameController创建空子物体,名字为SecondSound,给SecondSound添加AudioSource组件,其属性设置如下:
4、给GameController添加脚本组件LastPlayerSighting,实现背景音乐的切换。
public class LastPlayerSighting : MonoBehaviour {
public Vector3 normalPosition = Vector3.zero;
public Vector3 alarmPosition = Vector3.zero;
private float turnSpeed = 3;
private AudioSource mainAudio;
private AudioSource panicAudio;
private AudioSource[] alarmAudios;
AlarmLight alarmLight;
void Start () {
mainAudio = GetComponent<AudioSource>();
panicAudio = transform.GetChild(0).GetComponent<AudioSource>();
GameObject[] gos = GameObject.FindGameObjectsWithTag(Tags.Siren);
alarmAudios = new AudioSource[gos.Length];
for (int i = 0;i < gos.Length;i++)
{
alarmAudios[i] = gos[i].GetComponent<AudioSource>();
}
alarmLight = GameObject.FindWithTag(Tags.AlarmLight).GetComponent<AlarmLight>();
}
void Update () {
if (alarmPosition == normalPosition)
{
alarmLight.alarmOn = false;
mainAudio.volume = Mathf.Lerp(mainAudio.volume, 0.5f, Time.deltaTime * turnSpeed);
if (mainAudio.volume>=0.45f)
{
mainAudio.volume = 0.5f;
}
panicAudio.volume = Mathf.Lerp(panicAudio.volume, 0, Time.deltaTime * turnSpeed);
if (panicAudio.volume<0.05f)
{
panicAudio.volume = 0;
}
for (int i = 0;i < alarmAudios.Length;i++)
{
alarmAudios[i].volume = Mathf.Lerp(alarmAudios[i].volume, 0, Time.deltaTime * turnSpeed);
if (alarmAudios[i].volume<0.05f)
{
alarmAudios[i].volume = 0;
}
}
}
else {
alarmLight.alarmOn = true;
mainAudio.volume = Mathf.Lerp(mainAudio.volume, 0, Time.deltaTime * turnSpeed);
if (mainAudio.volume<0.05f)
{
mainAudio.volume = 0;
}
panicAudio.volume = Mathf.Lerp(panicAudio.volume, 0.5f, Time.deltaTime * turnSpeed);
if (panicAudio.volume>0.45f)
{
panicAudio.volume = 0.5f;
}
for (int i = 0;i < alarmAudios.Length;i++)
{
alarmAudios[i].volume = Mathf.Lerp(alarmAudios[i].volume, 0.5f, Time.deltaTime * turnSpeed);
if (alarmAudios[i].volume>0.45f)
{
alarmAudios[i].volume = 0.5f;
}
}
}
}
}
当玩家移动时,碰撞到激光门会触发警报,此时警报灯会亮起,警报声音和背景音乐会切换,故需要实现激光门和玩家之间的触发检测。
1、在层级视图中创建空物体,名字改为Lasers,用来管理场景中所有的激光门,然后把Assets/Models文件夹里面的fx_laserFence_lasers拖到Lasers下面做为子物体,名字改为laser1,其transform组件属性设置如下:
2、给laser1添加MeshCollider组件,其属性设置如下:
3、给laser1添加AudioSource组件,其属性设置如下:
4、给laser1添加Light组件,其属性设置如下:
5、给laser1添加给Laser脚本组件,实现激光门的触发检测功能。
6、把laser1克隆出第一个副本,名字改为laser2,其transform组件的属性设置如下:
7、把laser1克隆出第二个副本,名字改为laser3,其transform组件的属性设置如下:
8、把laser1克隆出第三个副本,名字改为laser4,其transform组件的属性设置如下:
9、把laser1克隆出第四个副本,名字改为laser5,其transform组件的属性设置如下:
10、把laser1克隆出第五个副本,名字改为laser6,其transform组件的属性设置如下:
public class Laser : MonoBehaviour {
public bool isBlinking = false;
public float timeInterval = 3;
float timer = 0;
LastPlayerSighting lastPlayerSighting;
void Start () {
lastPlayerSighting = GameObject.FindWithTag(Tags.GameController).GetComponent<LastPlayerSighting>();
}
void Update () {
timer += Time.deltaTime;
if (timer>timeInterval&&isBlinking)
{
timer = 0;
//gameObject.SetActive(!gameObject.activeSelf);
GetComponent<MeshRenderer>().enabled = !GetComponent<MeshRenderer>().enabled;
GetComponent<MeshCollider>().enabled = !GetComponent<MeshCollider>().enabled;
GetComponent<AudioSource>().enabled = !GetComponent<AudioSource>().enabled;
GetComponent<Light>().enabled = !GetComponent<Light>().enabled;
}
}
private void OnTriggerEnter(Collider other)
{
if (other.tag==Tags.Player)
{
lastPlayerSighting.alarmPosition = other.transform.position;
}
}
}
自动门默认状态是关闭,当玩家或者小机器人进入自动门的触发范围以内,自动门会打开,离开触发范围自动门会关闭,自动门开关的功能使用了动画器组件,触发检测使用了物理引擎。
1、在层级视图中创建空物体,名字改为Doors,用来管理场景中的自动门,然后点击Assets/Models下的door_generic_slide,设置右侧检视面板中Rig的选项如下:
2、把door_generic_slide拖到Doors下做为其子物体,名字改为door1,door1的transform组件属性设置如下:
3、给door1添加SphereCollider组件,其属性设置如下:
4、给door1添加AudioSource组件,其属性设置如下:
5、给door1的子物体添加MeshCollider组件,其属性设置如下:
6、创建动画控制器DoorAni,并把Assets/Models下的door_generic_slide的两个动画片断拖到状态机里面,并设置好动画片断之间的过渡,具体效果图如下:
7、在DoorAni动画状态机中添加bool类型的动画参数DoorOpen。
8、设置Closed到Open的过渡条件如下:
9、设置Open到Closed的过渡条件如下:
10、给door1添加Animator组件,并把DoorAni拖到动画器组件上,具体设置如下:
11、给door1添加脚本组件Door,实现自动门的开关。
public class Door : MonoBehaviour {
public bool needKey = false;
public AudioClip refuseClip;
private Animator ani;
private bool playerIn = false;
private int count = 0;
private PlayerMove playerMove;
private AudioSource aud;
void Start () {
ani = GetComponent<Animator>();
playerMove = GameObject.FindWithTag(Tags.Player).GetComponent<PlayerMove>();
aud = GetComponent<AudioSource>();
}
void Update () {
if (needKey==false)
{
if (count > 0)
{
ani.SetBool(Parameters.DoorOpen, true);
}
else {
ani.SetBool(Parameters.DoorOpen, false);
}
}
else
{
if (playerIn && playerMove.hasKey)
{
ani.SetBool(Parameters.DoorOpen, true);
}
else
{
ani.SetBool(Parameters.DoorOpen, false);
}
}
}
private void OnTriggerEnter(Collider other)
{
if (other.tag==Tags.Player||(other.tag==Tags.Enemy&&other.GetType()==typeof(CapsuleCollider)))
{
count++;
if (other.tag==Tags.Player)
{
playerIn = true;
if (needKey&&playerMove.hasKey==false)
{
AudioSource.PlayClipAtPoint(refuseClip, transform.position);
}
}
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag==Tags.Player||(other.tag == Tags.Enemy && other.GetType() == typeof(CapsuleCollider)))
{
count--;
if (other.tag==Tags.Player)
{
playerIn = false;
}
}
}
public void PlayVoice()
{
if (Time.time>0.1f)
{
aud.Play();
}
}
}
分别在墙角和卡车上安装监控探头,主要的功能是当玩家触发到监控探头后,警报声会响起来,同时警报灯也会亮起来,使用引擎自带的动画编辑器给监控探头创建左右摇摆的动画,使用物理引擎进行触发检测。
1、在层级视图中创建空物体,名字改为CCTVS,用来管理监控探头,把Assets/Models下的prop_cctvCam拖拽到CCTVS下做为子物体,名字改为cctv1,其transform组件属性设置如下:
2、选中cctv1的子物体prop_cctvCam_joint,然后再按下快捷键Ctrl+6调出动画编辑器窗口,创建动画名字为CCTV,给prop_cctvCam_joint添加Rotation属性如下图:
3、第0帧的Rotation值设置如下:
4、第60帧的Rotation值设置如下:
5、第120帧的Rotation值设置如下:
6、第180帧的Rotation值设置如下:
7、第240帧的Rotation值设置如下:
8、给cctv1的子物体prop_cctvCam_body添加空子物体体,名字为Trigger,其transform组件属性设置如下:
9、给Trigger添加MeshCollider组件,其属性设置如下:
10、给Trigger添加Light组件,其属性设置如下:
11、给Trigger添加脚本组件CCTV,实现监控探头的触发检测功能。
public class CCTV : MonoBehaviour {
private LastPlayerSighting lastPlayerSighting;
void Start () {
lastPlayerSighting = GameObject.FindWithTag(Tags.GameController).GetComponent<LastPlayerSighting>();
}
private void OnTriggerEnter(Collider other)
{
if (other.tag==Tags.Player)
{
lastPlayerSighting.alarmPosition = other.transform.position;
}
}
}
钥匙在场景中旋转,当玩家触发到钥匙后,钥匙消失,玩家得到钥匙,主要应用了动画器组件和物理引擎。
1、把Assets/Models下的prop_keycard拖到层级视图中,名字改为key,其transform组件属性设置如下:
2、给key添加SphereCollider组件,属性设置如下:
3、给key添加AudioSource组件,属性设置如下:
4、创建动画控制器KeyCardAni,把prop_keycard下的动画片断拖到动画状态机中,效果如下:
5、给key添加Animator组件,属性设置如下:
6、给key添加脚本组件KeyCard,实现钥匙的触发检测功能。
public class keyCard : MonoBehaviour {
AudioSource aud;
PlayerMove playerMove;
void Start () {
aud = GetComponent<AudioSource>();
playerMove = GameObject.FindWithTag(Tags.Player).GetComponent<PlayerMove>();
}
private void OnTriggerEnter(Collider other)
{
if (other.tag==Tags.Player)
{
playerMove.hasKey = true;
aud.Play();
Destroy(gameObject,0.2f);
}
}
}
在房间中放置控制激光门隐藏的电门,当玩家站在电门触发范围内,同时按下Z键后,电门上方锁的图标会由红色改成绿色,电门所控制的激光门将会隐藏,这样玩家就不会触发警报了。
1、在层级视图中创建空物体,名字改为LaserSwitchs,用来管理所有的电门,把Assets/Models下的prop_switchUnit拖到LaserSwitchs下做为其子物体,名字改为switchUnit,其transform组件属性设置如下:
2、给switchUnit添加MeshCollider组件,属性设置如下:
3、给switchUnit添加BoxCollider组件,属性设置如下:
4、给switchUnit添加AudioSource组件,属性设置如下:
5、给switchUnit添加LaserSwitch组件,实现控制激光门的隐藏。
public class LaserSwitch : MonoBehaviour {
private AudioSource aud;
public Material greenMaterial;
public GameObject laser;
void Start () {
aud = GetComponent<AudioSource>();
}
private void OnTriggerStay(Collider other)
{
if (other.tag==Tags.Player)
{
if (Input.GetKeyDown(KeyCode.Z))
{
aud.Play();
laser.SetActive(false);
transform.GetChild(0).GetComponent<MeshRenderer>().material =greenMaterial;
}
}
}
}
当玩家拿到钥匙进入电梯等5秒钟以后,电梯开始上升,玩家不能够随意移动。电梯在上升的过程中,会播放声音,同时也会响起游戏结束的声音,当电梯上升到一定高度后,游戏转场景并重新开始。
1、把Assets/Models下的prop_lift_exit拖到层级视图中,名字改为lift,其transform属性设置如下:
2、给lift添加AudioSource组件,其属性设置如下:
3、给lift的子物体prop_lift_exit_carriage添加BoxCollider组件,用来检测玩家是否在电梯里面,属性设置如下:
4、给prop_lift_exit_carriage添加AudioSource组件,属性设置如下:
5、给prop_lift_exit_carriage添加LiftDoor脚本组件,实现电梯上升的功能。
6、给lift的子物体door_exit_inner添加SyncDoor脚本组件,实现电梯门随红色门一起开关。
public class LiftDoor : MonoBehaviour {
public float waitTime = 2;
private float timer = 0;
public float moveTime = 3;
public float moveSpeed = 2;
private Transform player;
private AudioSource liftAud;
public AudioSource endGameAud;
void Start () {
player = GameObject.FindWithTag(Tags.Player).transform;
liftAud = transform.root.GetComponent<AudioSource>();
endGameAud = GetComponent<AudioSource>();
}
private void OnTriggerStay(Collider other)
{
if (other.tag==Tags.Player)
{
timer += Time.deltaTime;
if (timer>waitTime)
{
transform.root.position += Vector3.up * Time.deltaTime * moveSpeed;
player.position += Vector3.up * Time.deltaTime * moveSpeed;
player.GetComponent<PlayerMove>().enabled = false;
if (!liftAud.isPlaying)
{
liftAud.Play();
}
if (!endGameAud.isPlaying)
{
endGameAud.Play();
}
if (timer>waitTime+moveTime)
{
SceneManager.LoadScene(0);
}
}
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag==Tags.Player)
{
timer = 0;
}
}
}
public class SyncDoor : MonoBehaviour {
public Transform outerLeft;
public Transform outerRight;
private Transform innerLeft;
private Transform innerRight;
void Start () {
innerLeft = transform.GetChild(0);
innerRight = transform.GetChild(1);
}
void Update () {
innerLeft.localPosition = new Vector3(innerLeft.localPosition.x, innerLeft.localPosition.y, outerLeft.localPosition.z);
innerRight.localPosition = new Vector3(innerRight.localPosition.x, innerRight.localPosition.y, outerRight.localPosition.z);
}
}
游戏开始后,小机器人会在自己的巡逻位置进行巡逻,当玩家触发警报后,小机器人会跑到玩家触发警报的位置,如果在移动的过程中看到玩家,则小机器人会追踪玩家,如果玩家停下来,小机器人会对玩家开枪,玩家血量会减少,如果血量减少到0,玩家执行死亡的动画,游戏结束。如果小机器人跑到警报位置没有看到玩家,等待几秒钟后,小机器人会自动回到巡逻点进行巡逻。当玩家在没有死亡的情况下,拿到钥匙并进入电梯,等电梯上升后表示玩家胜利,游戏结束并重新开始。小机器人的功能主要使用了Unity引擎动画系统和导航系统,主要的技术包括:动画融合,动画层,动画遮罩,动画事件,IK动画,动画曲线以及导航组件的使用。
1、把Assets/Models下的char_robotGuard拖到层级视图中,名字改为enemy,设置其tag值为Enemy如下:
2、enemy的transform组件属性设置如下:
3、创建动画控制器EnemyAni,创建动画融合树,名字改为LocoMotion,动画状态机如下:
4、在EnemyAni中添加不同类型的动画参数,如下:
5、双击LocoMotion打开融合树,设置动画融合类型以及添加要融合的动画片断,如下:
6、创建动画遮罩EnemyShotMask,具体设置如下:
7、添加新的动画层ShootLayer,设置如下:
8、在ShootLayer里面添加动画片断并设置过渡逻辑,如下:
9、Empty->WeaponRaise的过渡条件如下:
10、WeaponRaise->WeaponLower的过渡条件如下:
11、WeaponLower->Empty的过渡条件如下:
12、WeaponLower->WeaponRaise的过渡条件如下:
13、WeaponRaise->WeaponShoot的过渡条件如下:
14、WeaponShoot->WeaponLower的过渡条件如下:
15、在层级视图中创建空物体point1,其transform组件属性设置如下:
16、在层级视图中创建空物体point2,其transform组件属性设置如下:
17、在层级视图中创建空物体point3,其transform组件属性设置如下:
18、在层级视图中创建空物体point4,其transform组件属性设置如下:
19、给enemy添加Animator组件,属性设置如下:
20、给enemy添加Rigidbody组件,属性设置如下:
21、给enemy添加SphereCollider组件,属性设置如下:
22、给enemy添加CapsuleCollider组件,属性设置如下:
23、给enemy添加NavMeshAgent组件,属性设置如下:
24、给enemy添加EnemyAI脚本组件,实现小机器人的自动寻路。
25、给enemy添加EnemySight脚本组件,实现小机器人的听觉和视觉功能。
26、给enemy添加EnemyAnimation脚本组件,实现小机器人的动画。
27、给enemy添加EnemyShooting脚本组件,实现小机器人的射击。
public class EnemyAI : MonoBehaviour {
public Transform[] wayPoints;
public float patrallingSpeed = 2.5f;
public float chasingSpeed = 6;
public float waitTime = 3;
private float chasingTimer = 0;
private float patrallingTimer = 0;
private NavMeshAgent nav;
private int index = 0;
private EnemySight enemySight;
private PlayerHealth playerHealth;
private LastPlayerSighting lastPlayerSighting;
void Start () {
nav = GetComponent<NavMeshAgent>();
enemySight = GetComponent<EnemySight>();
playerHealth = GameObject.FindWithTag(Tags.Player).GetComponent<PlayerHealth>();
lastPlayerSighting = GameObject.FindWithTag(Tags.GameController).GetComponent<LastPlayerSighting>();
}
void Update () {
if (enemySight.playerInSight&&playerHealth.health>0)
{
StopNav();
}
else if (enemySight.personalAlarmPosition!=lastPlayerSighting.normalPosition&&playerHealth.health>0)
{
Chasing();
}
else
{
Patralling();
}
}
void StopNav()
{
nav.isStopped = true;
}
void Chasing()
{
nav.isStopped = false;
nav.speed = chasingSpeed;
nav.SetDestination(enemySight.personalAlarmPosition);
if (nav.remainingDistance-nav.stoppingDistance<0.5f)
{
chasingTimer += Time.deltaTime;
if (chasingTimer>waitTime)
{
chasingTimer = 0;
lastPlayerSighting.alarmPosition = lastPlayerSighting.normalPosition;
}
}
else
{
chasingTimer = 0;
}
}
void Patralling()
{
nav.speed = patrallingSpeed;
nav.SetDestination(wayPoints[index].position);
if (nav.remainingDistance-nav.stoppingDistance<0.5f)
{
patrallingTimer += Time.deltaTime;
if (patrallingTimer>waitTime)
{
patrallingTimer = 0;
index++;
index %= wayPoints.Length;
}
}
else
{
patrallingTimer = 0;
}
}
}
public class EnemyAnimation : MonoBehaviour {
public float deadZone = 4;
private NavMeshAgent nav;
private EnemySight enemySight;
private Transform player;
private Animator ani;
void Start () {
nav = GetComponent<NavMeshAgent>();
ani = GetComponent<Animator>();
deadZone *= Mathf.Deg2Rad;
player = GameObject.FindWithTag(Tags.Player).transform;
enemySight = GetComponent<EnemySight>();
}
private void OnAnimatorMove()
{
nav.velocity = ani.deltaPosition / Time.deltaTime;
transform.rotation = ani.rootRotation;
}
void Update () {
float speed = 0;
float angularSpeed = 0;
if (enemySight.playerInSight)
{
speed = 0;
angularSpeed = FindAngle();
if (Mathf.Abs(angularSpeed)<deadZone)
{
angularSpeed = 0;
transform.LookAt(player);
}
}
else
{
speed = Vector3.Project(nav.desiredVelocity, transform.forward).magnitude;
angularSpeed = FindAngle();
}
ani.SetFloat(Parameters.Speed, speed,0.1f,Time.deltaTime);
ani.SetFloat(Parameters.AngularSpeed, angularSpeed, 0.1f, Time.deltaTime);
}
float FindAngle()
{
Vector3 dir = nav.desiredVelocity;
float angle = Vector3.Angle(dir, transform.forward);
Vector3 normal = Vector3.Cross(transform.forward, dir);
if (normal.y<0)
{
angle = -angle;
}
angle *= Mathf.Deg2Rad;
if (dir==Vector3.zero)
{
angle = 0;
}
return angle;
}
}
public class EnemySight : MonoBehaviour {
public bool playerInSight = false;
public Vector3 personalAlarmPosition;
public Vector3 previousAlarmPosition;
private LastPlayerSighting lastPlayerSighting;
public float fieldOfView = 110;
public float distanceOfView;
private SphereCollider sph;
RaycastHit hit;
private NavMeshAgent nav;
private PlayerHealth playerHealth;
private Animator ani;
private void Awake()
{
sph = GetComponent<SphereCollider>();
nav = GetComponent<NavMeshAgent>();
ani = GetComponent<Animator>();
distanceOfView = sph.radius;
}
void Start () {
lastPlayerSighting = GameObject.FindWithTag(Tags.GameController).GetComponent<LastPlayerSighting>();
personalAlarmPosition = lastPlayerSighting.normalPosition;
previousAlarmPosition = lastPlayerSighting.normalPosition;
playerHealth = GameObject.FindWithTag(Tags.Player).GetComponent<PlayerHealth>();
}
void Update () {
if (previousAlarmPosition!=lastPlayerSighting.alarmPosition)
{
personalAlarmPosition = lastPlayerSighting.alarmPosition;
}
previousAlarmPosition = lastPlayerSighting.alarmPosition;
if (playerHealth.health>0)
{
ani.SetBool(Parameters.PlayerInSight, playerInSight);
}
else
{
ani.SetBool(Parameters.PlayerInSight, false);
}
}
private void OnTriggerStay(Collider other)
{
if (other.tag==Tags.Player)
{
playerInSight = false;
float distance = Vector3.Distance(other.transform.position, transform.position);
Vector3 dir = other.transform.position - transform.position;
float angular = Vector3.Angle(transform.forward, dir);
if (angular<=fieldOfView/2&&distance<distanceOfView)
{
if (Physics.Raycast(transform.position+Vector3.up*1.7f,dir,out hit))
{
if (hit.collider.tag==Tags.Player)
{
playerInSight = true;
personalAlarmPosition = other.transform.position;
}
}
}
if (EnemyCanListen(other.transform.position))
{
if (other.GetComponent<AudioSource>().isPlaying)
{
personalAlarmPosition = other.transform.position;
}
}
}
}
private void OnTriggerExit(Collider other)
{
if (other.tag==Tags.Player)
{
playerInSight = false;
}
}
bool EnemyCanListen(Vector3 playerPos)
{
NavMeshPath path = new NavMeshPath();
if (nav.CalculatePath(playerPos, path))
{
Vector3[] points = new Vector3[path.corners.Length + 2];
points[0] = transform.position;
points[points.Length - 1] = playerPos;
for (int i = 1;i < points.Length - 1;i++)
{
points[i] = path.corners[i - 1];
}
float navDistance = 0;
for (int i = 0;i < points.Length - 1;i++)
{
navDistance += Vector3.Distance(points[i], points[i + 1]);
}
if (navDistance < distanceOfView)
{
return true;
}
}
return false;
}
}
public class EnemyShooting : MonoBehaviour {
public float damage = 10;
private Animator ani;
private Light shootLight;
private LineRenderer line;
bool isShooting = false;
private Transform player;
private PlayerHealth playerHealth;
void Start () {
ani = GetComponent<Animator>();
shootLight = GetComponentInChildren<Light>();
line = GetComponentInChildren<LineRenderer>();
player = GameObject.FindWithTag(Tags.Player).transform;
playerHealth = player.GetComponent<PlayerHealth>();
}
void Update () {
if (ani.GetFloat(Parameters.Shot)>0.5f&&isShooting==false)
{
Shoot();
}
else if (ani.GetFloat(Parameters.Shot)<0.5f)
{
isShooting = false;
shootLight.enabled = false;
line.enabled = false;
}
}
void Shoot()
{
shootLight.enabled = true;
isShooting = true;
line.enabled = true;
line.positionCount = 2;
line.SetPosition(0, line.transform.position);
line.SetPosition(1, player.position + Vector3.up * 1.5f);
playerHealth.health -= damage;
}
private void OnAnimatorIK(int layerIndex)
{
float weight = ani.GetFloat(Parameters.AimWeight);
ani.SetIKPositionWeight(AvatarIKGoal.RightHand, weight);
ani.SetIKPosition(AvatarIKGoal.RightHand, player.position + Vector3.up * 1.5f);
ani.SetLookAtWeight(weight);
ani.SetLookAtPosition(player.position + Vector3.up * 1.7f);
}
}
1、EnemyAI脚本组件设置如下:
2、EnemySight脚本组件设置如下:
3、EnemyAnimation脚本组件设置如下:
4、EnemyShooting脚本组件设置如下: