Stealth秘密行动
在这个博客里,我将讲解一个潜入类型的游戏制作方法
1. 首先我们需要导入当前的场景,这里我们需要给场景添加碰撞器,将素材里的env_stealth拖入,加上meshCollieder,添加上env_stealth_collider就可以将预先设置好的碰撞器添加上,再将模型中的汽车拖入,对照碰撞器摆好好汽车即可,
2. 我们给场景添加光源
(1).改变天空盒子为黑色
(2)创建一个叫做Light的空物体,里面存放directLight和PointLight
(3),按照自己喜好放置光源的位置即可
3. 制作警报效果
我们首先给场景添加一个直射光,命名为alarmLight,然后在它下面添加这个脚本
逻辑如下:
1. light下有个intensity的组件,我们控制这个数值来决定亮度,因此我们定义个最大值和最小值
2. 我们通过Mathf.Lerp()方法来控制数值的变动
3. 为了实现灯光的循环闪动,我们加入判断当灯光趋近于临界值时,变为0,再反复循环
4. 为了方便以后调用警报,我们判断当AlanWake是false和true时是显示光还是不显示
5. 我们将这个方法封装成单例模式
using UnityEngine;
using System.Collections;
publicclassAlarmScripts : MonoBehaviour {
publicbool AlarmWake = false;
privatefloat lowIntensity = 0;//这里控制的是警报灯光的界限,即0到0.5
privatefloat highIntensity = 0.5f;
privatefloat targetIntensity;
publicfloat AlameSpeed = 3;
publicstaticAlarmScripts _instance;
void Awake()
{
targetIntensity = highIntensity;
AlarmWake = false;
_instance = this;
}
// Update is called once per frame
void Update () {
if (AlarmWake)
{
light.intensity = Mathf.Lerp(light.intensity,targetIntensity,Time.deltaTime);//这里通过Mathf.Lerp的方法,控制光照亮度的变化,后面的数值是控制0-1的一个界限,超过1即为1,小于0就是0
if (Mathf.Abs(light.intensity - targetIntensity) < 0.05f)//Math.lerp是抛物线,永远不会接近highIntensity,当两者差值到这个界限的时候,我们需要将光照从满变成暗
{
//这里实现的是来回循环播放光照的变化
if (targetIntensity == highIntensity)
{
targetIntensity =lowIntensity;
}
else
{
targetIntensity =highIntensity;
}
}
}
else
{
light.intensity = Mathf.Lerp(light.intensity,0, Time.deltaTime *AlameSpeed);
}
}
}
4. 创建游戏控制器来管理警报和灯的协同控制
我们希望当警报响起的时候灯也同时响起,这样,我们需要一个游戏物体来控制它们
1. 首先,我们创建一个空的游戏物体将其命名为gameController,然后在它下面添加这个脚本
逻辑如下
1我们创建一个bool值来判断此时警报和灯是否应该响起
2.我们上节将所有喇叭添加了一个tag,我们通过GameObject.findwithTag这个方法来找到所有警报装置
3.我们在unpdate里同步了灯光和之前的警报脚本保持一致
4.我们创建两个方法,通过audio下的isPlaying判断是否播放,通过foreach来遍历所有的路灯
5在update里判断灯光的变化来跟随声音的变化
6..这样就实现了灯光和警报的协同变化
2. using UnityEngine;
3. using System.Collections;
4.
5. publicclassGameController : MonoBehaviour {
6. //这是一个游戏控制器,在这里我们要控制游戏的各项属性
7. //首先我们进行声音的控制
8. publicbool alarmOn=false;
9. privateGameObject[] sirens;
10. // Use this for initialization
11. void Awake () {
12. alarmOn = false;
13. sirens = GameObject.FindGameObjectsWithTag("Siren");//通过tag找到所有的警报装置
14. }
15.
16. // Update is called once per frame
17. void Update () {
18. AlarmScripts._instance.AlarmWake = this.alarmOn;//保持两边警报一致
19. if (alarmOn)
20. {
21. OpenAlarm();
22. }
23. else
24. {
25. CloseAlarm();
26. }
27. }
28. privatevoid OpenAlarm()
29. {
30.
31. foreach(GameObject go in sirens)
32. {
33. if (!go.audio.isPlaying)
34. {
35.
36. go.audio.Play();
37.
38. }
39.
40.
41. }
42.
43. }
44. privatevoid CloseAlarm()
45. {
46. foreach (GameObject go in sirens)
47. {
48. if (go.audio.isPlaying)
49. {
50.
51. go.audio.Stop();
52.
53. }
54.
55.
56. }
57.
58. }
59. }
5. 控制摄像头的来回播放
这里运用unity自带的组件animation,控制的是摄像机y轴的变动,设置一个两秒钟内0到90,90到0的变动就行,要想添加到其他摄像头上只需要添加animatior,然后将controller添加上去就行
6. 通过tags来统一管理标签
因为本项目所用标签很多,如果每个都用字符串书写,容易导致错漏,因此,创建一个统一管理标签的tags脚本,这样再通过Tags进行访问,这样出错率降低,并且修改起来简单
using UnityEngine;
using System.Collections;
publicclassTags : MonoBehaviour {
//这里用来统一存放我们需要调用的各种tags,const存放的是静态不可修改的变量
publicconststring siren= "Siren";
publicconststring player = "Player";
publicconststring enemy = "Enemy";
}
7. 给场景添加激光通道
这个不牵扯代码知识,就是将model里的激光拖出来,用空物体存储,添加boxCollider和audioSource进行控制碰撞和音乐,再将其拖放到prefab里,移动到各个接口位置即可
8. 给激光添加闪烁
我们要控制激光闪烁,只要控制render是否渲染即可,因此我们设置开始和关闭时间,通过计时器判断是否到达相应时间,再通过render,enable就可以调整激光的是否亮起了
9. using UnityEngine;
10. using System.Collections;
11.
12. publicclassLaser : MonoBehaviour
13. {
14. publicbool isFlicker = false;//判断是否闪烁
15. //控制亮的时间,关闭的时间和计时器
16. publicfloat onTime = 3;
17. publicfloat offTime = 3;
18. publicfloat timer = 0;
19.
20.
21. void OnTriggerStay(Collider other)
22. {
23. GameController._instance.seePlayer(other.transform);
24.
25.
26. }
27. //这里我们要控制激光的闪烁
28. void Update()
29. {
30. if (isFlicker)
31. {
32. if (renderer.enabled == true)
33. {
34. timer += Time.deltaTime;
35. if (timer >= onTime)
36. {
37. renderer.enabled = false;
38. timer = 0;
39.
40. }
41. }
42. else
43. {
44. timer += Time.deltaTime;
45. if (timer >=offTime)
46. {
47. renderer.enabled = true;
48. timer = 0;
49. }
50. }
51. }
52.
53. }
54. }
8.设置人物动画的状态机
当我们想要控制一个人物行走的时候,首先,我们需要设置人物的一些动画属性
1. 我们将主角添加到场景,添加上刚体还有胶囊碰撞器,这里将胶囊碰撞器设置和主角身体差不多大即可
2. 在主角身上添加animator动画状态机,首先添加speed和sneaker两个属性,一个是控制移动的阈值,一个是控制按下shift是否移动,这里我们将主角的idle动画添加进行,创建一个blendtree叫做locomotion,其中添加两个域来控制行走和奔跑,将动画拖拽进去,设置关联,出来后,将idle和locomotion连接,设置speed大于0.1和小于0.1时候状态的切换,再拖拽进来sneaker动画,设置速度大于0.1和确认按下shift的bool值两个属性来进行切换,而sneaker和locomotion的切换则是需求速度大于0.1,判断shift是否按下
3. 然后我们在任意状态下都可能死亡,这里添加一个死亡的idel动画,通过bool值判断是否播放
9.控制主角的移动
这里先感慨一下我在这个控制移动上调了好久
1. 首先,我们的动画里是有一端人物行走的动画,因此,我们只需要控制主角的方向再播放动画,就可以成功控制主角的移动了,下面贴代码
这里讲一下逻辑,
1. 首先我们控制移动是在update方法里进行控制,我们先定义一个hv获得水平和垂直方向键盘的输入,然后通过Math.lerp方法控制动画里速度的变化决定人物由移动变换到奔跑
2. 当不输入时候,将速度设置为0
3. 我们要控制角色的旋转是通过Vector3.Angle计算你键盘朝向和当前朝向之间的角度,再通过transform.Rotate方法,控制你旋转的速度和角度还有转动的时间,这里注意自己调整的合适的手感
4. 我们要设置人物rigidbody的isKinematic属性勾选掉,这里是为了运用刚体的碰撞属性,如果不勾选就会穿墙
using UnityEngine;
using System.Collections;
publicclassPlayer : MonoBehaviour
{
//在这个脚本里,我们需要控制角色的移动
publicfloat moveSpeed = 1f;
publicfloat rotateSpeed = 6;
publicfloat angle1;
privateAnimator anim;
// Use this for initialization
void Awake()
{
anim = GetComponent<Animator>();
}
// Update is called once per frame
void Update()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
if (Mathf.Abs(h) > 0.1 || Mathf.Abs(v) > 0.1)
{
float newSpeed = Mathf.Lerp(anim.GetFloat("speed"), 5.6f, moveSpeed * Time.deltaTime);//这里是通过插值运算的方式,通过加速度的方式,控制状态机里速度变化以至于动画的改变
anim.SetFloat("speed", newSpeed);//将获得的速度设置给状态机
Vector3 targetDir = newVector3(h, 0, v);
Vector3 nowDir = transform.forward;
float angle = Vector3.Angle(targetDir, nowDir);
if (angle > 180)
{
angle= 360 - angle;
angle = -angle;
}
angle1 = angle;
transform.Rotate(Vector3.up * angle*Time.deltaTime *rotateSpeed);
}
else
{
anim.SetFloat("speed", 0);
}
}
1. }
10.优化主角的移动
嗯,上面那种转向方法不太适用于本案例,因此我们转向将用到Quaration自己包装的一个方法进行转向,它会自动判断最小的角度然后转向过去,用到了Quaration.lerp和Quaration.newRoation
float newSpeed = Mathf.Lerp(anim.GetFloat("speed"), 5.6f, moveSpeed * Time.deltaTime);//这里是通过插值运算的方式,通过加速度的方式,控制状态机里速度变化以至于动画的改变
anim.SetFloat("speed", newSpeed);
Vector3 targetDir = newVector3(h, 0, v);
Quaternion newRoation = Quaternion.LookRotation(targetDir, Vector3.up);
transform.rotation= Quaternion.Lerp(transform.rotation, newRoation, rotateSpeed*Time.deltaTime);
当我们想要控制sneaker也就是缓慢行走时,我们需要下面的代码
if (Input.GetKey(KeyCode.LeftShift))
{
anim.SetBool("sneaker", true);
}
else
{
anim.SetBool("sneaker", false);
}
11.控制主角行走伴随脚步声
1.我们给主角添加一个audioSource组件,然后将脚步声拖拽上去
2.在Player脚本上添加这样的代码,这样,当我们人物在行走时候,会判断状态机里此时是否处于locomotion状态,根据状态决定是否播放音乐
if (anim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion"))
{
PlayFootMusic();
}
else
{
StopFootMusic();
}
privatevoid PlayFootMusic()
{
if (!audio.isPlaying)
{
audio.Play();
}
}
privatevoid StopFootMusic()
{
if(audio.isPlaying)
{
audio.Stop();
}
}
12.控制电梯门和普通门的开关
1. 恩,这里的话我们先编写个脚本,叫做door,这个脚本是通过判断count决定是否开关,当我们触碰到门上设置的碰撞器时,就会使得count++,离开又会减少,这样来设置bool值实现开关,这里添加门的开关声音和动画就不再赘述了,比较简单,将相应动画拖过去设置bool值即可
using UnityEngine;
using System.Collections;
publicclassDoor : MonoBehaviour {
//这个脚本是控制门的开关
privateAnimator anim;
privateint count = 0;
// Use this for initialization
void Start () {
anim = this.GetComponent < Animator> ();
}
// Update is called once per frame
void Update () {
anim.SetBool("isopen", count>0);//通过count来判断是true还是false,当count大于0时,门打开
if (anim.IsInTransition(0))
{
if (!audio.isPlaying)
{
audio.Play();
}
}
}
void OnTriggerEnter(Collider other)
{
if (other.tag == Tags.player || other.tag == Tags.enemy){
count++;
}
}
void OnTriggerExit(Collider other)
{
if (other.tag == Tags.player || other.tag == Tags.enemy)
{
count--;
}
}
}
2. 电梯门相对特殊一点,分为内门和外门,外门的设置和普通门一样,door脚本可以复用,动画和声音自行添加即可,内门的代码如下
我们要做到当外门开启时,内门缓慢开启,这里我们可以通过Math,lerp,使门的x坐标按照时间自行变化,要想调试到合适的效果只需要改变innerSpeed即可,当然记得要将对应门的组件拖拽过去
3. using UnityEngine;
4. using System.Collections;
5.
6. publicclassLift : MonoBehaviour {
7. publicTransform inner_left;
8. publicTransform outter_left;
9. publicTransform inner_right;
10. publicTransform outter_right;
11. publicfloat innerSpeed = 3;
12. // Use this for initialization
13. void Start () {
14.
15. }
16.
17. // Update is called once per frame
18. void Update () {
19. float leftpositonx = Mathf.Lerp(inner_left.position.x, outter_left.position.x, Time.deltaTime*innerSpeed);
20. inner_left.position = newVector3(leftpositonx,inner_left.position.y, inner_left.position.z);
21. float rightpositonx = Mathf.Lerp(inner_right.position.x, outter_right.position.x, Time.deltaTime*innerSpeed);
22. inner_right.position = newVector3(rightpositonx,inner_right.position.y, inner_right.position.z);
23.
24.
25.
26.
27.
28. }
29. }
13.这里我们来做一个钥匙来使得主角捡到它才能打开电梯
这个算比较简单的,两部分
1. 控制主角碰撞到后销毁并且将主角hasKey设置为true
2. 在捡到后播放捡到的音乐
首先,我们将模型拖到地图上,添加上模型上自带的动画,给模型添加碰撞器
然后编写如下脚本,我们定义一个OnTriggerEnter的方法,碰撞到就设置hasKey为true,并且销毁该物体
其次,我们播放声音用到AudioSource下一个播放clip的方法,碰到就在该位置播放该音效,这样就解决了如果使用audioSource组件会因为销毁而没声音的问题
using UnityEngine;
using System.Collections;
publicclassKeyCard : MonoBehaviour {
publicAudioClip music_pickup;
void OnTriggerEnter(Collider other)
{
if (other.tag == Tags.player)
{
AudioSource.PlayClipAtPoint(music_pickup, transform.position);
Destroy(this.gameObject);
Player._instance.hasKey = true;
}
}
}
14.添加摄像机的跟随
这个我们只需要计算人物的偏移,然后在update里实时更新保持跟随即可
publicclassFollowPlayer : MonoBehaviour {
privateVector3 offset;
privateTransform player;
// Use this for initialization
void Awake () {
player = GameObject.FindGameObjectWithTag(Tags.player).transform;
offset = transform.position -player.position;
offset = newVector3(0, offset.y, offset.z);
}
// Update is called once per frame
void Update () {
this.transform.position = player.position + offset;
}
}
15.添加NaVigation来控制机器人行动的范围
这里不牵涉什么代码,具体就是在场景下添加一个1Navigation,然后调节它的一些参数使其覆盖地图,这个游戏因为没什么坡度,所以讲坡度设为了0,其他具体参数看实际情况来调节,这里就不多赘述了。
16.设置机器人的动画状态机
首先我们创建一个EnemyAnimator,然后添加一个叫做Locomotion的blendtree,然后我们将角色的各种动画拖过去,通过speed和angularSpeed来控制角色的状态切换
然后创建第二层是射击动画,这里通过bool值来判断是否射击,因为我们只需要控制头和脚步移动,因此添加个avatorMask来控制即可
,
17,添加机器人的模型并且控制机器人的视觉和听觉
这里的逻辑比较复杂了,首先,我们先添加一些组件,分别控制不要穿墙,移动,自动导航和监听范围的效果
3. 开始编写我们的脚本
这里逻辑比较复杂,我介绍一下,分为两部分,视觉和听觉
视觉:首先我们敌人有一个视线范围,我们假定为110度,当我们主角朝向敌人的方向与敌人的垂直视角夹角为55度以内,我们就认为主角在敌人视线内,这里用Vector3.angle来计算夹角,然后视线内的话就设置为true,离开视线设置成false,都是用OnTriggerstay和OnTriggerExit方法实现,然后当发现时候,我们设置alarmPosition为主角当前所在的位置i
听觉:我们这里听觉用到的是navMeshAgent下的一个方法navAgent.CalculatePath(other.transform.position,path,这个方法是个bool类型判断该物体到主角是否可以正常通行,并且返回一个个节点,因为navMeshAgent的寻路是多个直线的组合,因此path返回的就是各个节点的位置,然后我们通过计算多个节点的距离,就可以加起来得到总距离,这里我们定义了一个数组存储各个点,通过遍历的方式计算出长度,这里我们设置当距离小于半径的时候,就判断敌人听到了主角,在设置alarmPosition
这就是大体思路,如果不懂可以看看代码注释
4. using UnityEngine;
5. using System.Collections;
6.
7. publicclassEnemySight : MonoBehaviour {
8. publicfloat enmeySight=110;//这里设置的是怪物的视野度数
9. publicbool hasseePlayer = false;//通过bool值判断怪物是否应该向主角移动
10. publicVector3 alarmPosition = Vector3.zero;//设置触发警报的位置
11. privateAnimator playerAnim;//获得主角移动动画
12. privateNavMeshAgent navAgent;
13. privateCapsuleCollider collider1;
14. privatefloat pathLength = 0;
15. void Awake()
16. {
17.
18. playerAnim = this.GetComponent<Animator>();
19. navAgent = this.GetComponent<NavMeshAgent>();
20. collider1 = this.GetComponent<CapsuleCollider>();
21.
22. }
23. publicvoid OnTriggerStay(Collider other)
24. {
25. if (other.tag == Tags.player)
26. {
27. Vector3 enemyDir = transform.forward;//这里得到的是怪物的方向
28. Vector3 playerDir = other.transform.position -transform.position;//这里得到的是主角往向怪物的方向
29. float temp = Vector3.Angle(enemyDir, playerDir);//这里得到两者的夹角
30. if (temp <= 0.5f * enmeySight)
31. {
32. hasseePlayer = true;
33. alarmPosition =other.transform.position;
34.
35. }
36. else
37. {
38. hasseePlayer = false;
39. }
40. }
41.
42. //如果主角当前处于移动状态,我们就通过NaviGation来判断是否应该移动
43. if (playerAnim.GetCurrentAnimatorStateInfo(0).IsName("Locomotion"))
44. {
45. NavMeshPath path = newNavMeshPath();
46. //用来判断当前位置能否移动到目标位置,并且会返回一个路径
47. if (navAgent.CalculatePath(other.transform.position,path)){
48. //这里path是存放路径种所有的点,准确来说,navMeshPath种存储的是是一个个corners也就算拐角点,我们移动就是一条条直线的组合,通过累加这些点之间的距离实现移动,这里我们定义一个数组存放所有拐角点
49. Vector3[] waypoints = newVector3[path.corners.Length+2];//数组长度就是起始位置,主角位置和拐角点的数量
50. waypoints[0] =transform.position;
51. waypoints[waypoints.Length-1] =other.transform.position;
52. for(int i = 0; i < path.corners.Length; i++)
53. {
54. //这里用来给各个拐点赋值
55. waypoints[i + 1] =path.corners[i];
56.
57. }
58. for(int i = 0; i < waypoints.Length; i++)
59. {
60. pathLength += (waypoints[i]- waypoints[i-1]).magnitude;
61. }
62. if (pathLength <= collider1.radius)
63. {
64.
65. alarmPosition =other.transform.position;
66.
67. }
68. }
69.
70.
71. }
72. }
73. publicvoid OnTriggerExit(Collider other)
74. {
75. if (other.tag == Tags.player)
76. {
77.
78. hasseePlayer = false;
79.
80.
81. }
82.
83.
84.
85. }
86.
87.
88. }
18.控制机器人发出警报
首先我们在GameController里有一个发现主角触发警报的方法,所以我们可以将警报位置和这个enemySight中的prePosition进行同步,然后我们实时更新警报位置,这样机器人就知道了主角触发警报时候的位置并且可以控制seePlayer方法实现警报声音播放,当然这里只是看到会发出警告,听到并不会
GameController._instance.seePlayer(other.transform);
PrePosition= GameController._instance.lastPlayerPosition;
privateVector3 PrePosition;//这里更新的是警报触发时主角的位置
void Update()
{
if (PrePosition != GameController._instance.lastPlayerPosition)
{
alarmPosition = PrePosition;
PrePosition = GameController._instance.lastPlayerPosition;
}
}
19.用NavAgent控制敌人的自动巡逻
这里逻辑比较简单,定义一个数组存放坐标点,定义一个预估休息时间和计时器,然后定义一个巡逻方法,用到了navAgent里的remainDistance计算和目标之间的距离,用navAgent.destination来设置移动的坐标,就可以实现自动巡逻了,当然此时的移动是漂移,接下来还要完善
巡逻方法
using UnityEngine;
using System.Collections;
publicclassEnemyAI : MonoBehaviour {
publicGameObject[] wayPoints;//存放路标的数组
privateint index = 0;
publicfloat patrolTime = 3f;
publicfloat patrolTimmer = 0;//巡逻的休息时间计时器
privateNavMeshAgent navAgent;
void Awake()
{
navAgent = this.GetComponent<NavMeshAgent>();
}
// Update is called once per frame
void Update () {
Patrolling();
}
void Patrolling()
{
//这里是判断敌人到目标位置的剩余距离
if (navAgent.remainingDistance < 0.5f)
{
navAgent.Stop();//如果距离小到相当于到达目标位置,我们就停止自动导航
patrolTimmer += Time.deltaTime;
if (patrolTime < patrolTimmer)
{
index++;
index = index % 4;
navAgent.destination =wayPoints[index].transform.position;//navAgent设置的移动
patrolTimmer = 0;
}
}
}
}
20.将平移的自动巡逻改成用动画控制移动
这一块控制比较复杂,如果实现不了的可以多看看代码,这里概述一下大致的逻辑
1. 我们给Enemy下添加一个EnemyAnimation脚本来控制动画的播放
2. 引入naVAgentMesh组件和adminstor组件
3. 我们这里引入了一个navAgent.desiredVelocity,这个是期望运动的方向,我们通过计算期望运行方向和当前朝向方向的夹角来获得一个角度,再转变成弧度控制angular中的变化
4. 弧度有正负,这里我们用vector3.cross的方法来判断期望方向在当前方向的左边还是右边来设置弧度的正负以控制转向
5. 我们控制速度的变化是计算期望方向在当前方向上投影的长度来计算,当前方向越贴近期望方向,速度也就越大,这样保证了人物不会因为初始速度太大而偏离轨道
6. 为了保证动画切换不突兀,我们知道setFloat这个方法里可以控制切换的时间,然后我们通过,speedDampTime和angulSpeedDampTime来保证动画的流畅切换
using UnityEngine;
using System.Collections;
publicclassEnemyAnimation : MonoBehaviour {
privateNavMeshAgent navAgent;
privateAnimator anim;
publicfloat speedDampTime = 0.3f;//这两个是设置速度变化时间的
publicfloat angulSpeedDampTime=0.3f;
// Use this for initialization
void Start () {
navAgent = this.GetComponent<NavMeshAgent>();
anim = this.GetComponent<Animator>();
}
// Update is called once per frame
void Update () {
//navAgent.desiredVelocity表示期望趋近的速度
if (navAgent.desiredVelocity == Vector3.zero)
{
anim.SetFloat("speed", 0,speedDampTime,Time.deltaTime);
anim.SetFloat("angulSpeed",0,angulSpeedDampTime,Time.deltaTime);
}
//else
{
float angle = Vector3.Angle(transform.forward, navAgent.desiredVelocity);//计算期望到达的方向和robot当前方向的夹角
float angleRad = 0;
angleRad = angle * Mathf.Deg2Rad;//我们控制转向是用弧度控制,因此需要把角度转换成弧度
if (angle > 90)
{
anim.SetFloat("speed", 0,speedDampTime,Time.deltaTime);//如果夹角大于90,则只转向而不进行移动
}
else
{
// 这里我们获得是目标方向到我们当前朝向的一个垂直投影,我们不能直接设置速度为3.5的最大,这样容易偏离轨迹,因此我们当方向越来越趋近于目标方向时,才使得速度慢慢趋向于最大
Vector3 projection = Vector3.Project(navAgent.desiredVelocity,transform.forward);
anim.SetFloat("speed",projection.magnitude,speedDampTime,Time.deltaTime);//这个投影的长度就是我们希望设置的速度
}
//这里我们的弧度有正负,而我们怎么判断朝向是在我们机器人视线的左边还是右边呢,我们通过vector3.cross来计算,当目标方向在朝向的左右切换时,crossRes的y轴会由负变成正
Vector3 crossRes = Vector3.Cross(transform.forward, navAgent.desiredVelocity);
if (crossRes.y < 0)
{
angleRad = -angleRad;
}
anim.SetFloat("angulSpeed",angleRad,angulSpeedDampTime,Time.deltaTime);//将得到的弧度设置给状态机
}
}
}
21.制作人物的追捕功能
这一块比较简单,讲一下思路
1. 我们在EnemyMoveAi中控制机器人的各种行为,首先我们获取到Enemysight脚本,来获得到一个
警报位置和判断主角是否在敌人视线内
2,我们根据是否看到主角判断是否射击,警报位置当前不为0判断是否追击,不在视线内也没有触发警报实现巡逻这三种模式
3.书写追捕方法,定义追捕计时器和追捕时间上限,到达一定时间没发现主角则关闭警报,回归巡逻状态,不然的话会将自身位置实时和警报位置进行同步更新
privateEnemySight sight;
publicfloat chaseTime = 3f;
publicfloat chaseTimmer = 0;
void Update()
{
//这里有三种状态,巡逻,射击,追捕
if (sight.hasseePlayer)
{
//shoot
}
elseif(sight.alarmPosition!=Vector3.zero)
{
//chasing
Chasing();
}
else{
//巡逻
Patrolling();
}
}
privatevoid Chasing()
{
//追捕时速度加快,并且设置不用navAgent控制移动,设置目标位置为触发警报位置
navAgent.speed = 5;
navAgent.updatePosition = false;
navAgent.updateRotation = false;
navAgent.destination = sight.alarmPosition;
//当我们追捕到目标位置时候,需要计时器来控制移动时间,如果超过时间没有看到主角,就要回归原来的位置
//回到原来的位置需要解除警报,将警报位置归零,lastPlayerPosition设置为0
if (navAgent.remainingDistance < 2f)
{
chaseTimmer += Time.deltaTime;
if (chaseTimmer > chaseTime)
{
sight.alarmPosition = Vector3.zero;
GameController._instance.lastPlayerPosition = Vector3.zero;
GameController._instance.alarmOn = false;
}
}
}
22.修改一下门的bug
这里因为unity自身的问题,当一个物体上挂着两个碰撞器,很容易出现重叠的效果,因此我们将robot身上的capusal碰撞器放在body上,并且我们修改一下门的脚本和敌人身上的脚本,不要让视线穿墙检测到主角的位置
1. 在enmeySight上添加射线检测
定义一个Physics下的RayCast组件,射线检测是否碰撞到了物体并且在视线判断是进行修改
RaycastHit hitInfo;
bool res=Physics.Raycast(transform.position + Vector3.up, other.transform.position - transform.position,out hitInfo);
//这里我们不仅要目标在视野前方,还需要前方没有障碍物或者障碍物是主角,才能判断主角在视线内
1. if (temp <= 0.5f *enmeySight&&(res==false||hitInfo.collider.tag==Tags.player))
2. 修改门的脚本保证正常开闭
3. 不再是判断是否碰到敌人和主角,而是分开判断,敌人的判断要标签和碰撞器设置两重来限制,这样,就可以正确运行了
4. void OnTriggerEnter(Collider other)
5. {
6. if (requireKey)
7. {
8. if (other.tag == Tags.player)
9. {
10. Player player = other.GetComponent<Player>();
11. if (player.hasKey)
12. {
13. count++;
14.
15. }
16. else {
17. musicDennie.Play();
18. }
19. }
20.
21. }
22. elseif (other.tag == Tags.player)
23. {
24. count++;
25. }
26. elseif (other.tag == Tags.enemy && other.collider.isTrigger==false)
27. {
28.
29. count++;
30. }
31. }
32.
33.
34.
35.
36.
37.
38. void OnTriggerExit(Collider other)
39. {
40. if (requireKey)
41. {
42. if (other.tag == Tags.player)
43. {
44. Player player = other.GetComponent<Player>();
45. if (player.hasKey)
46. {
47. count--;
48.
49. }
50. }
51.
52. }
53. elseif (other.tag == Tags.player || other.tag == Tags.enemy)
54. {
55. count--;
56. }
57. elseif (other.tag == Tags.enemy && other.collider.isTrigger==false)
58. {
59. count--;
60. }
61. }
62. }
63.
23.控制射击动画的播放
这里的逻辑有两个,一个是敌人看到主角后停止移动并且进行射击,在EnemyAi里这么改
void Update()
{
//这里有三种状态,巡逻,射击,追捕
if (sight.hasseePlayer)
{
//shoot
Shooting();
}
privatevoid Shooting()
{
if(sight.hasseePlayer)
navAgent.Stop();
}
然后在EnemyAnimation脚本里控制动画的播放,这里我们在脚本里设置状态机,通过EnemySight里的HasseePlayer进行判断设置状态机里的playerInSight的bool值来控制动画的播放
anim.SetBool("playerInSight", sight.hasseePlayer);
22.添加敌人的伤害计算
我们开枪会导致主角的死亡,因此我们需要修改的有两个脚本,一个是新的,我管它叫playerHealth,还有一个是EnemyAi的射击脚本,health脚本比较简单,就是设计血量还有接受别人传过来的damage参数,并且执行dead动画
using UnityEngine;
using System.Collections;
publicclassPlayerHealth : MonoBehaviour {
publicfloat hp = 100;
privateAnimator anim;
void Awake()
{
anim = GetComponent<Animator>();
}
publicvoid TakeDamage(float damage)
{
hp -= damage;
if (hp < 0)
{
anim.SetBool("dead", true);
}
}
}
还要一个就是修改射击了,这里要加的就是巡逻里设置敌人重新开始巡逻
navAgent.destination= wayPoints[index].position;
当然这里还可以将子弹的射击进行实例化,我的方法比较差,这里就不介绍了,大家做子弹射击可以用LineRender,粒子系统等等,或者相关插件
24.创建多个敌人并且在死亡后重新加载关卡
多个敌人的创建比较简单,将机器人进行ctrl+d复制到相应位置,设置好路径点并赋值给机器人即可
重新加载关卡我们用到了携程控制,等待四秒便会重新加载场景,这里用到startCoroutine来调用这个方法
StartCoroutine(ReloadScene());
}
}
IEnumerator ReloadScene()
{
yieldreturnnewWaitForSeconds(4);
Application.LoadLevel(0);
}
25,游戏胜利坐电梯离开
1. 我们在lift下添加一个碰撞器,大小和电梯箱一样大,用来判断主角是否进入
2. 我们在电梯箱底部添加一个碰撞器,这样保证了主角和电梯箱同时移动,这里要取消主角在y轴上的锁定,保证正常移动
3.在lift下写以下脚本
我们用OntriggerEnter和OntriggerExit两个方法检测主角是否进入或者离开,然后在Update里检测,超过三秒,就使得电梯上升,待在电梯五秒,则重新加载游戏
3. using UnityEngine;
4. using System.Collections;
5.
6. publicclassLift : MonoBehaviour {
7. publicTransform inner_left;
8. publicTransform outter_left;
9. publicTransform inner_right;
10. publicTransform outter_right;
11. publicfloat innerSpeed = 3;
12. publicbool isIn = false;
13. publicfloat upTime = 3;
14. publicfloat upTimmer = 0;
15. privatefloat winTimmer = 0;
16. // Use this for initialization
17. void Start () {
18.
19. }
20.
21. // Update is called once per frame
22. void Update () {
23. float leftpositonx = Mathf.Lerp(inner_left.position.x, outter_left.position.x, Time.deltaTime*innerSpeed);
24. inner_left.position = newVector3(leftpositonx,inner_left.position.y, inner_left.position.z);
25. float rightpositonx = Mathf.Lerp(inner_right.position.x, outter_right.position.x, Time.deltaTime*innerSpeed);
26. inner_right.position = newVector3(rightpositonx,inner_right.position.y, inner_right.position.z);
27. if (isIn)
28. {
29. upTimmer += Time.deltaTime;
30. if (upTimmer > upTime)
31. {
32. transform.Translate(Vector3.up * Time.deltaTime);
33. winTimmer += Time.deltaTime;
34. if (winTimmer > 5f)
35. {
36. Application.LoadLevel(0);
37. }
38. }
39.
40. }
41. }
42. void OnTriggerEnter(Collider other)
43. {
44. if(other.tag==Tags.player)
45. isIn = true;
46.
47.
48. }
49. void OntTriggerExit(Collider other)
50. {
51.
52. isIn = false;
53. upTimmer = 0;
54.
55.
56. }
57. }
58.
26.给游戏添加UI提示
这里不牵扯代码,算是游戏的一个尾声了,这里添加上UI,游戏也就算做完了