场景的搭建

功能说明

把场景模型文件拖拽到层级视图中,创建游戏所需要的场景,同时把场景中所需要的元素补充完整。

实现步骤

1、把Assets/Models文件夹里面的env_stealth_static拖拽到层级视图中,transform组件属性设置如下:

images-p4_1.png

2、给env_stealth_static添加MeshCollider组件,组件属性设置如下:

images-p4_2.png

3、把Assets/Models文件夹里面的prop_battleBus拖拽到env_stealth_static下做为子物体,其transform组件属性设置如下:

images-p4_3.png

4、给prop_battleBus添加MeshCollider组件,组件属性设置如下:

images-p4_4.png

效果展示图

images-p4_5.png

场景中灯光的切换

功能说明和实现步骤

整个场景中的灯光颜色偏暗,当玩家触发到场景中的警报后,警报灯会亮起来,报警灯的颜色为红色,真实的模拟了现实生活中报警灯的闪烁效果,具体实现步骤如下

1、更改层级视图中DirectionalLight的Light组件属性如下:

images-p4_6.png

2、在层级视图中创建DirectionalLight类型的灯光,名字改为AlarmLight,其transform组件属性设置如下:

images-p4_7.png

3、AlarmLight的Light组件属性设置如下:

images-p4_8.png

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);
        }

    }
}

脚本组件属性设置截图

images-p4_10.png

效果展示图

images-p4_11.png

玩家移动

功能说明

通过键盘控制玩家的前后左右移动,玩家碰到场景中的墙壁时,就不能向前移动了,主要使用了动画器组件来切换不同的动画状态,同时使用物理引擎让玩家不会掉到场景以下。

实现步骤

1、把Assets/Models文件夹下的char_ethan的动画类型设置为人形动画,然后拖到层级视图中,名字改为Player,tag值设置为Player,具体设置如下:

images-p4_12.png

2、Player的transform组件属性设置如下:

images-p4_13png

3、给Player添加Rigidbody组件,其属性设置如下:

images-p4_14.png

4、给Player添加CapsuleCollider组件,其属性设置如下:

images-p4_15.png

5、给Player添加AudioSource组件,其属性设置如下:

images-p4_16.png

6、创建动画控制器PlayerAni,双击打开后添加动画参数如下:

images-p4_17.png

7、按以下顺序分别把动画片断Idle、Sneak、Dying拖到动画控制器里面,效果图如下:

images-p4_18.png

8、创建动画融合树,名字改为LocalMotion,然后设置Idle、Sneak、Dying和LocalMotion之间的过渡,过渡的逻辑如下:

images-p4_19.png

9、双击LocalMotion融合树,添加两个动画片断Walk和Run,相关参数设置如下:

images-p4_20.png

10、动画融合树LocalMotion的效果图如下:

images-p4_21.png

11、设置Idle到Sneak的过渡条件如下:

images-p4_22.png

12、设置Sneak到Idle的过渡条件如下:

images-p4_23.png

13、设置Idle到LocalMotion的过渡条件如下:

images-p4_24.png

14、设置LocalMotion到Idle的过渡条件如下:

images-p4_25.png

15、设置LocalMotion到Sneak的过渡条件如下:

images-p4_26.png

16、设置Sneak到LocalMotion的过渡条件如下:

images-p4_27.png

17、设置Any State到Dying的过渡条件如下:

images-p4_28.png

18、给Player添加动画器组件,其属性设置如下:

images-p4_29.png

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();
        }
    }
}

脚本组件属性设置截图

images-p4_31.png

效果展示图

images-p4_32.png

摄像机的跟随

功能说明

当玩家移动时,摄像机会保持一定的角度跟随玩家一起移动,在摄像机移动的过程中,摄像机的位置会有一个平滑的过渡,以增加视觉效果。

实现步骤

1、给层级视图中的MainCamera添加tag值为MainCamera,具体设置如下:

images-p4_33.png

2、MainCamera的transform组件属性设置如下:

images-p4_34.png

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);
    }
}

脚本组件属性设置截图

images-p4_36.png

效果展示图

images-p4_37.png

背景音乐的切换

功能说明

游戏运行后,会播放背景音乐,当玩家触发警报后,红色的警报灯会亮起,同时会伴有警报声,另外背景音乐也会进行更换。

实现步骤

1、在层级视图中创建空物体,名字为GameController,设置其tag值为GameController,具体设置如下:

images-p4_38.png

2、给GameController添加AudioSource组件,其属性设置如下:

images-p4_39.png

3、给GameController创建空子物体,名字为SecondSound,给SecondSound添加AudioSource组件,其属性设置如下:

images-p4_40.png

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;
                }
            }
        }
	}
}

脚本组件属性设置截图

images-p4_42.png

激光门的触发检测

功能说明

当玩家移动时,碰撞到激光门会触发警报,此时警报灯会亮起,警报声音和背景音乐会切换,故需要实现激光门和玩家之间的触发检测。

实现步骤

1、在层级视图中创建空物体,名字改为Lasers,用来管理场景中所有的激光门,然后把Assets/Models文件夹里面的fx_laserFence_lasers拖到Lasers下面做为子物体,名字改为laser1,其transform组件属性设置如下:

images-p4_43.png

2、给laser1添加MeshCollider组件,其属性设置如下:

images-p4_44.png

3、给laser1添加AudioSource组件,其属性设置如下:

images-p4_45.png

4、给laser1添加Light组件,其属性设置如下:

images-p4_46.png

5、给laser1添加给Laser脚本组件,实现激光门的触发检测功能。

6、把laser1克隆出第一个副本,名字改为laser2,其transform组件的属性设置如下:

images-p4_47.png

7、把laser1克隆出第二个副本,名字改为laser3,其transform组件的属性设置如下:

images-p4_48.png

8、把laser1克隆出第三个副本,名字改为laser4,其transform组件的属性设置如下:

images-p4_49.png

9、把laser1克隆出第四个副本,名字改为laser5,其transform组件的属性设置如下:

images-p4_50.png

10、把laser1克隆出第五个副本,名字改为laser6,其transform组件的属性设置如下:

images-p4_51.png

重点代码

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;
        }
    }
}

脚本组件属性设置截图

images-p4_53.png

效果展示图

images-p4_54.png

自动门的触发检测

功能说明

自动门默认状态是关闭,当玩家或者小机器人进入自动门的触发范围以内,自动门会打开,离开触发范围自动门会关闭,自动门开关的功能使用了动画器组件,触发检测使用了物理引擎。

实现步骤

1、在层级视图中创建空物体,名字改为Doors,用来管理场景中的自动门,然后点击Assets/Models下的door_generic_slide,设置右侧检视面板中Rig的选项如下:

images-p4_55.png

2、把door_generic_slide拖到Doors下做为其子物体,名字改为door1,door1的transform组件属性设置如下:

images-p4_56.png

3、给door1添加SphereCollider组件,其属性设置如下:

images-p4_57.png

4、给door1添加AudioSource组件,其属性设置如下:

images-p4_58.png

5、给door1的子物体添加MeshCollider组件,其属性设置如下:

images-p4_59.png

6、创建动画控制器DoorAni,并把Assets/Models下的door_generic_slide的两个动画片断拖到状态机里面,并设置好动画片断之间的过渡,具体效果图如下:

images-p4_60.png

7、在DoorAni动画状态机中添加bool类型的动画参数DoorOpen。

images-p4_61.png

8、设置Closed到Open的过渡条件如下:

images-p4_62.png

9、设置Open到Closed的过渡条件如下:

images-p4_63.png

10、给door1添加Animator组件,并把DoorAni拖到动画器组件上,具体设置如下:

images-p4_64.png

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();
        }
    }
}

脚本组件属性设置截图

images-p4_66.png

效果展示图

images-p4_67.png

监控探头的触发检测

功能说明

分别在墙角和卡车上安装监控探头,主要的功能是当玩家触发到监控探头后,警报声会响起来,同时警报灯也会亮起来,使用引擎自带的动画编辑器给监控探头创建左右摇摆的动画,使用物理引擎进行触发检测。

实现步骤

1、在层级视图中创建空物体,名字改为CCTVS,用来管理监控探头,把Assets/Models下的prop_cctvCam拖拽到CCTVS下做为子物体,名字改为cctv1,其transform组件属性设置如下:

images-p4_68.png

2、选中cctv1的子物体prop_cctvCam_joint,然后再按下快捷键Ctrl+6调出动画编辑器窗口,创建动画名字为CCTV,给prop_cctvCam_joint添加Rotation属性如下图:

images-p4_69.png

3、第0帧的Rotation值设置如下:

images-p4_70.png

4、第60帧的Rotation值设置如下:

images-p4_71.png

5、第120帧的Rotation值设置如下:

images-p4_72.png

6、第180帧的Rotation值设置如下:

images-p4_73.png

7、第240帧的Rotation值设置如下:

images-p4_74.png

8、给cctv1的子物体prop_cctvCam_body添加空子物体体,名字为Trigger,其transform组件属性设置如下:

images-p4_75.png

9、给Trigger添加MeshCollider组件,其属性设置如下:

images-p4_76.png

10、给Trigger添加Light组件,其属性设置如下:

images-p4_77.png

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;
        }
    }
}

脚本组件属性设置截图

images-p4_79.png

效果展示图

images-p4_80.png

钥匙的触发检测

功能说明

钥匙在场景中旋转,当玩家触发到钥匙后,钥匙消失,玩家得到钥匙,主要应用了动画器组件和物理引擎。

实现步骤

1、把Assets/Models下的prop_keycard拖到层级视图中,名字改为key,其transform组件属性设置如下:

images-p4_81.png

2、给key添加SphereCollider组件,属性设置如下:

images-p4_82.png

3、给key添加AudioSource组件,属性设置如下:

images-p4_83.png

4、创建动画控制器KeyCardAni,把prop_keycard下的动画片断拖到动画状态机中,效果如下:

images-p4_84.png

5、给key添加Animator组件,属性设置如下:

images-p4_85.png

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);
        }
    }
}

脚本组件属性设置截图

images-p4_87.png

效果展示图

images-p4_88.png

激光门的控制

功能说明

在房间中放置控制激光门隐藏的电门,当玩家站在电门触发范围内,同时按下Z键后,电门上方锁的图标会由红色改成绿色,电门所控制的激光门将会隐藏,这样玩家就不会触发警报了。

实现步骤

1、在层级视图中创建空物体,名字改为LaserSwitchs,用来管理所有的电门,把Assets/Models下的prop_switchUnit拖到LaserSwitchs下做为其子物体,名字改为switchUnit,其transform组件属性设置如下:

images-p4_89.png

2、给switchUnit添加MeshCollider组件,属性设置如下:

images-p4_90.png

3、给switchUnit添加BoxCollider组件,属性设置如下:

images-p4_91.png

4、给switchUnit添加AudioSource组件,属性设置如下:

images-p4_92.png

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;
            }
        }
    }
}

脚本组件属性设置截图

images-p4_94.png

效果展示图

images-p4_95.png

电梯的上升

功能说明和实现步骤

当玩家拿到钥匙进入电梯等5秒钟以后,电梯开始上升,玩家不能够随意移动。电梯在上升的过程中,会播放声音,同时也会响起游戏结束的声音,当电梯上升到一定高度后,游戏转场景并重新开始。

实现步骤

1、把Assets/Models下的prop_lift_exit拖到层级视图中,名字改为lift,其transform属性设置如下:

images-p4_96.png

2、给lift添加AudioSource组件,其属性设置如下:

images-p4_97.png

3、给lift的子物体prop_lift_exit_carriage添加BoxCollider组件,用来检测玩家是否在电梯里面,属性设置如下:

images-p4_98.png

4、给prop_lift_exit_carriage添加AudioSource组件,属性设置如下:

images-p4_99.png

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);
	}
}

脚本组件属性设置截图

images-p4_101.png

效果展示图

images-p4_102.png

小机器人功能的实现

功能说明

游戏开始后,小机器人会在自己的巡逻位置进行巡逻,当玩家触发警报后,小机器人会跑到玩家触发警报的位置,如果在移动的过程中看到玩家,则小机器人会追踪玩家,如果玩家停下来,小机器人会对玩家开枪,玩家血量会减少,如果血量减少到0,玩家执行死亡的动画,游戏结束。如果小机器人跑到警报位置没有看到玩家,等待几秒钟后,小机器人会自动回到巡逻点进行巡逻。当玩家在没有死亡的情况下,拿到钥匙并进入电梯,等电梯上升后表示玩家胜利,游戏结束并重新开始。小机器人的功能主要使用了Unity引擎动画系统和导航系统,主要的技术包括:动画融合,动画层,动画遮罩,动画事件,IK动画,动画曲线以及导航组件的使用。

实现步骤

1、把Assets/Models下的char_robotGuard拖到层级视图中,名字改为enemy,设置其tag值为Enemy如下:

images-p4_103.png

2、enemy的transform组件属性设置如下:

images-p4_104.png

3、创建动画控制器EnemyAni,创建动画融合树,名字改为LocoMotion,动画状态机如下:

images-p4_105.png

4、在EnemyAni中添加不同类型的动画参数,如下:

images-p4_106.png

5、双击LocoMotion打开融合树,设置动画融合类型以及添加要融合的动画片断,如下:

images-p4_107.png

6、创建动画遮罩EnemyShotMask,具体设置如下:

images-p4_108.png

7、添加新的动画层ShootLayer,设置如下:

images-p4_109.png

8、在ShootLayer里面添加动画片断并设置过渡逻辑,如下:

images-p4_110.png

9、Empty->WeaponRaise的过渡条件如下:

images-p4_111.png

10、WeaponRaise->WeaponLower的过渡条件如下:

images-p4_112.png

11、WeaponLower->Empty的过渡条件如下:

images-p4_113.png

12、WeaponLower->WeaponRaise的过渡条件如下:

images-p4_114.png

13、WeaponRaise->WeaponShoot的过渡条件如下:

images-p4_115.png

14、WeaponShoot->WeaponLower的过渡条件如下:

images-p4_116.png

15、在层级视图中创建空物体point1,其transform组件属性设置如下:

images-p4_117.png

16、在层级视图中创建空物体point2,其transform组件属性设置如下:

images-p4_118.png

17、在层级视图中创建空物体point3,其transform组件属性设置如下:

images-p4_119.png

18、在层级视图中创建空物体point4,其transform组件属性设置如下:

images-p4_120.png

19、给enemy添加Animator组件,属性设置如下:

images-p4_121.png

20、给enemy添加Rigidbody组件,属性设置如下:

images-p4_122.png

21、给enemy添加SphereCollider组件,属性设置如下:

images-p4_123.png

22、给enemy添加CapsuleCollider组件,属性设置如下:

images-p4_124.png

23、给enemy添加NavMeshAgent组件,属性设置如下:

images-p4_125png

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脚本组件设置如下:

images-p4_127.png

2、EnemySight脚本组件设置如下:

images-p4_128.png

3、EnemyAnimation脚本组件设置如下:

images-p4_129.png

4、EnemyShooting脚本组件设置如下:

images-p4_130.png

效果展示图

images-p4_131.png