Bootstrap

unity制作FPS射击游戏


介绍

角色动作方面包括行走、奔跑、跳跃、武器切换、弹夹更换、武器展示、弹壳抛出效果、射击效果、全自动与半自动射击效果、瞄准效果、后坐力效果、弹痕效果等多种动作。

非玩家角色(NPC)具备多个动画状态,包括固定路径巡逻、行走、奔跑、寻路攻击等多种行为。

太空地图中拥有滑动门、激光枪、钥匙、传送门、电脑设备等多种功能性组件。

玩家可手持手枪或步枪,与NPC交火,取得钥匙,然后前往传送门以完成任务。在此过程中,游戏中丰富的特效将会呈现,包括玩家受伤特效、射击效果、钥匙获得特效、场景加载特效以及场景结束特效。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


鼠标移动控制视角

  1. 声明了鼠标灵敏度、玩家位置和摄像机旋转角度的变量。
  2. 在游戏开始时,锁定光标在窗口中心并隐藏。
  3. 在每帧更新中,根据鼠标移动量和灵敏度计算摄像机的旋转。
  4. 限制摄像机旋转在垂直方向上的角度,避免过度旋转。
  5. 将计算出的旋转应用于摄像机和玩家位置,实现视角控制。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 摄像机的旋转
/// 玩家左右旋转控制视线左右移动
/// 摄像机上下旋转控制视线上下移动
/// </summary>
public class MouseLook : MonoBehaviour
{
    public float mouseSensitivity=100f;//鼠标灵敏度

    public Transform playerBody;//玩家的位置
    private float xRotation=0f;

    // Start is called before the first frame update
    void Start()
    {
        //将光标锁定在该游戏窗口的中心,并且隐藏光标
        Cursor.lockState = CursorLockMode.Locked;
    }

    // Update is called once per frame
    void Update()
    {
        float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity * Time.deltaTime;
        float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity * Time.deltaTime;

        xRotation -= mouseY;//将上下旋转的轴值进行累计
        xRotation = Mathf.Clamp(xRotation,-90f,90f);//限制轴值的累计(这里就能发现上90度和下90度角正好相对于了90的轴值)
        transform.localRotation = Quaternion.Euler(xRotation, 0f,0f);
        playerBody.Rotate(Vector3.up * mouseX);
    }
}


行走、奔跑、跳跃、下蹲

在这里插入图片描述

这脚本控制玩家的基本动作和声音效果:

  1. 变量和属性:

    • 定义了变量和属性,如移动速度、跳跃力度、重力、地面检测等。
  2. 移动和跳跃:

    • 使用Moveing()方法实现玩家移动,包括获取移动方向、用CharacterController.Move()移动、模拟跳跃和在空中减小高度。
  3. 下蹲:

    • 通过Crouch()方法修改CharacterController的高度实现下蹲。
  4. 声音效果:

    • 使用PlayFootStepSound()方法播放移动音效,根据行走和奔跑状态播放不同音效。
  5. 其他功能:

    • 检测是否在斜坡上,施加额外力量;检测是否在地面上。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 控制玩家移动
/// </summary>
public class PlayerMovement : MonoBehaviour
{
    private CharacterController characterController;
    public float walkSpeed = 10f;// 移动速度
    public float runSpeed=15f;//奔跑速度
    public float jumpForce = 3f;//跳跃力度
    private Vector3 velocity;//设置玩家Y轴的一个冲量变化
    private Vector3 moveDirction; //设置移动方向
    public float gravity = -9f; //设置重力

    public Transform groundCheck;//地面检测物体
    private float groundDistance = 0.4f;//与地面的距离
    public LayerMask groundMask;
    private bool isJump;//判断是否在跳跃
    private bool isGround;//判断是否在地面上
    public bool isWalk;//判断是否在行走
    public bool isRun;//判断是否在奔跑
    private bool isCrouch;//判断是否蹲下

    [SerializeField] private float slopeForce=6.0f; //走斜坡施加的力(是一个乘量)
    [SerializeField] private float slopeForceRayLength=2.0f; //斜坡射线长度(自定义量)

    [Header("键位设置")]
    [SerializeField] [Tooltip("跳跃按键")] private string jumpInputName = "Jump";
    [SerializeField] [Tooltip("奔跑按键")] private KeyCode runInputName;
    [SerializeField] [Tooltip("下蹲按键")] private KeyCode crouchInputName;

    private AudioSource audioSource;
    public AudioClip walkingSound;
    public AudioClip runingSound;

    void Start()
    {
        characterController = GetComponent<CharacterController>();
        audioSource = GetComponent<AudioSource>();
        audioSource.clip = walkingSound;
        audioSource.loop = true;
    }

    // Update is called once per frame
    void Update()
    {

        CheckGround();
        Moveing();
        Crouch();
    }

    /// <summary>
    /// 判断是否在斜坡上
    /// </summary>
    /// <returns></returns>
    public bool OnSlope()
    {
        if (isJump)
            return false;

        RaycastHit hit;
        //向下打出射线(检查是否在斜坡上)
        if (Physics.Raycast(transform.position, Vector3.down, out hit, characterController.height / 2 * slopeForceRayLength))
        {
            //如果触碰到的点的法线,不是在(0,1,0)这个方向上的,那么就人物处在斜坡上
            if (hit.normal != Vector3.up)
            {
                return true;
            }
        }
        return false;
    }


    /// <summary>
    /// 地面检测
    /// </summary>
    public void CheckGround() {
        //在 groundCheck 位置上做一个球体检测判断是否处在地面上
        isGround = Physics.CheckSphere(groundCheck.position, groundDistance, groundMask);
        //如果处在地面上,重力设置成一个固定值
        if (isGround && velocity.y < 0)
        {
            velocity.y = -2f;
        }
    }

    /// <summary>
    /// 跳跃
    /// </summary>
    public void Jump() {
        isJump = Input.GetButtonDown(jumpInputName);
        //施加跳跃的力 
        if (isJump && isGround)
        {
            velocity.y = Mathf.Sqrt(jumpForce * -2f * gravity);
            //velocity.y = 20f;
        }
    }

    /// <summary>
    /// 下蹲
    /// </summary>
    public void Crouch() {
        isCrouch = Input.GetKey(crouchInputName);
        if (isCrouch)
        {
            characterController.height = 1f;
        }
        else
        {
            characterController.height =1.8f;
        }   
    }

    /// <summary>
    /// 移动
    /// </summary>
    public void Moveing()
    {
        float speed;
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
     
        isRun = Input.GetKey(runInputName);

        isWalk = (Mathf.Abs(h) > 0 || Mathf.Abs(v) > 0) ? true : false;
        speed = isRun ? runSpeed : walkSpeed; //设置行走或奔跑的速度

        moveDirction = (transform.right * h + transform.forward * v).normalized;//设置玩家移动方向(将移动速度进行规范化,防止斜向走速度变大)

        characterController.Move(moveDirction * speed * Time.deltaTime);//移动

        velocity.y += gravity * Time.deltaTime;//不在地面上(空中,累加重力值)
        characterController.Move(velocity * Time.deltaTime); //施加重力
        Jump();
        //如果处在斜面上移动
        if (OnSlope())
        {
            //向下增加力量
            characterController.Move(Vector3.down * characterController.height / 2 * slopeForce * Time.deltaTime);
        }
        PlayFootStepSound();



    }

    ///播放移动的音效
    public void PlayFootStepSound() {
        if (isGround && moveDirction.sqrMagnitude > 0.9f)
        {
            audioSource.clip = isRun ? runingSound : walkingSound;//设置行走或者奔跑的音效
            if (!audioSource.isPlaying)
            {
                audioSource.Play();
            }
        }
        else
        {
            if (audioSource.isPlaying)
            {
                audioSource.Pause();
            }
        }
    }

}


射击、后坐力、射速、瞄准、弹痕、枪火、抛壳

武器射击:控制射击位置、射程、子弹数量、射速、射击精度等参数,并播放相关音效、动画和特效。

武器装弹:控制子弹数量、弹匣容量、备弹数量、自动装弹、手动装弹等功能,并播放相关音效、动画和UI界面的更新。

武器切换:控制游戏中玩家切换主武器、副武器、全自动和半自动射击模式的功能,并更新UI界面的状态。

武器精度:根据射击模式(全自动或半自动)控制射击精度,实现游戏中的真实射击体验。

界面更新:控制UI界面的更新,包括弹匣数量、备弹数量、射击模式、准星等界面元素的实时更新。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// 武器射击
/// </summary>
public class WeaponController : MonoBehaviour
{
    public PlayerMovement PM;
    public Transform shooterPoint; // 射击的位置(枪口)
    public int range = 100; // 武器射程
    public int bulletsMag = 31; // 一个弹匣数量
    public int bulletLeft = 300; // 备弹
    public int currentBullets; // 当前子弹数

    public ParticleSystem muzzleFlash; // 枪口火焰特效
    public GameObject hitParticles; // 子弹击中粒子特效
    public GameObject bulletHole; // 弹孔
    public Light muzzleFlashLight; // 枪口火焰灯光

    public float fireRate = 0.1f; // 射速
    public float bulletdamage = 10.0f; // 单发子弹伤害
    private float fireTimer; // 计时器
    private float SpreadFactor; // 射击的一点偏移量

    [Header("键位设置")]
    [SerializeField] [Tooltip("填装子弹按键")] private KeyCode reloadInputName;
    [SerializeField] [Tooltip("查看武器按键")] private KeyCode inspectInputName;
    [SerializeField] [Tooltip("主武器按键")] private KeyCode AutoRifleKey;
    [SerializeField] [Tooltip("副武器按键")] private KeyCode HandGunKey;
    [SerializeField] [Tooltip("自动半自动切换按键")] private KeyCode GunShootModelInputName;

    private Animator anim;
    /*音效参数*/
    private AudioSource audioSource;
    public AudioClip AK47ShoundClip; /*枪声音效片段*/
    public AudioClip reloadAmmoLeftClip; // 换子弹1音效片段
    public AudioClip reloadOutOFAmmoClip; // 换子弹2音效片段(拉枪栓)

    private bool isReloading; // 判断是否在装弹
    private bool isAiming; // 判断是否在瞄准

    public Transform casingSpawnPoint; // 子弹壳抛出的位置
    public Transform casingPrefab; // 子弹壳预制体

    private Camera mainCamera;
    public ChooseGunController CGC; // 声明切换武器类的实例

    /*使用枚举区分全自动和半自动模式*/
    public enum ShootMode { AutoRifle, SemiGun };
    public ShootMode shootingMode;
    private bool GunShootInput; // 根据全自动和半自动 射击的键位输入发生改变
    private int modeNum = 1; // 模式切换的一个中间参数(1:全自动模式,2:半自动模式)
    private string shootModelName;

    /*UI的设置*/
    public Image crossHairUI;
    public Text ammoTextUI;
    public Text ShootModelTextUI;

    public Transform mainCameraTransform;
    public float recoilAmount = 2.0f; // 后坐力的强度
    public float recoilRecoverySpeed = 5.0f; // 后坐力恢复速度

    private Vector3 originalCameraPosition; // 原始相机位置,用于恢复后坐力
    private Quaternion originalCameraRotation;

    private void Start()
    {
        // 获取标签为"maincamera"的相机的Transform组件
        GameObject mainCameraObject = GameObject.FindGameObjectWithTag("MainCamera");
        if (mainCameraObject != null)
        {
            mainCameraTransform = mainCameraObject.transform;
            originalCameraPosition = mainCameraTransform.localPosition; // 记录原始相机位置
            originalCameraRotation = mainCameraTransform.localRotation;
        }

        audioSource = GetComponent<AudioSource>();
        anim = GetComponent<Animator>();
        currentBullets = bulletsMag;
        mainCamera = Camera.main;
        shootingMode = ShootMode.AutoRifle; // AK47步枪默认是全自动模式
        shootModelName = "AK47";
        UpdateAmmoUI();
    }

    private void Update()
    {
        if (health.isDead)
        {
            anim.SetBool("Run", false);
            anim.SetBool("Walk", false);
            anim.SetBool("Aim", false);
            return;
        }

        // 切换模式(全自动和半自动)
        if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 1)
        {
            modeNum = 1;
            shootModelName = "全自动";
            shootingMode = ShootMode.AutoRifle;
            ShootModelTextUI.text = shootModelName;
        }
        else if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 0)
        {
            modeNum = 0;
            shootModelName = "半自动";
            shootingMode = ShootMode.SemiGun;
            ShootModelTextUI.text = shootModelName;
        }

        /*控制射击模式的转换  后面就要用代码去动态控制了*/
        switch (shootingMode)
        {
            case ShootMode.AutoRifle:
                GunShootInput = Input.GetMouseButton(0);
                fireRate = 0.1f;
                break;
            case ShootMode.SemiGun:
                GunShootInput = Input.GetMouseButtonDown(0);
                fireRate = 0.2f;
                break;
        }

        if (GunShootInput && currentBullets > 0)
        {
            GunFire();
        }
        else
        {
            muzzleFlashLight.enabled = false;
        }

        // 计时器加时间
        if (fireTimer < fireRate)
        {
            fireTimer += Time.deltaTime;
        }

        anim.SetBool("Run", PM.isRun); // 播放跑步动画
        anim.SetBool("Walk", PM.isWalk);
        // 获取动画状态机第一层动画的状态
        AnimatorStateInfo info = anim.GetCurrentAnimatorStateInfo(0);
        // 两种换子弹的东湖
        if (info.IsName("reload_ammo_left") || info.IsName("reload_out_of_ammo"))
        {
            isReloading = true;
        }
        else
        {
            isReloading = false;
        }

        if (Input.GetKeyDown(reloadInputName) && currentBullets < bulletsMag && bulletLeft > 0)
        {
            Reload();
        }
        SpreadFactor = (isAiming) ? 0f : 0.01f;
        DoingAim();

        if (Input.GetKeyDown(inspectInputName))
        {
            anim.SetTrigger("Inspect");
        }
        // 切换主武器(自动步枪)
        if (Input.GetKeyDown(AutoRifleKey))
        {
            CGC.ChangeWeapon(0);
        }
        // 切换副武器(手枪)
        if (Input.GetKeyDown(HandGunKey))
        {
            CGC.ChangeWeapon(1);
        }

        if (mainCameraTransform != null)
        {
            mainCameraTransform.localPosition = Vector3.Lerp(mainCameraTransform.localPosition, originalCameraPosition, Time.deltaTime * recoilRecoverySpeed);
        }
    }

    // 更新UI
    public void UpdateAmmoUI()
    {
        ammoTextUI.text = currentBullets + " / " + bulletLeft;
        ShootModelTextUI.text = shootModelName;
    }

    /// <summary>
    /// 瞄准的逻辑
    /// </summary>
    public void DoingAim()
    {
        if (Input.GetMouseButton(1) && !isReloading && !PM.isRun)
        {
            isAiming = true;
            anim.SetBool("Aim", isAiming);
            crossHairUI.gameObject.SetActive(false);
            mainCamera.fieldOfView = 25; // 瞄准的时候摄像机视野变小
        }
        else
        {
            isAiming = false;
            anim.SetBool("Aim", isAiming);
            crossHairUI.gameObject.SetActive(true);
            mainCamera.fieldOfView = 60; // 瞄准的时候摄像机视野恢复
        }
    }

    /// <summary>
    /// 射击逻辑
    /// </summary>
    public void GunFire()
    {
        // 控制射速,当前弹夹打光了,正在装子弹,正在奔跑 就不可以发射了
        if (fireTimer < fireRate || currentBullets <= 0 || isReloading || PM.isRun) return;

        RaycastHit hit;
        Vector3 shootDirection = shooterPoint.forward;
        // 改成这个,shootDirection shooterPoint这个游戏物体进行小的偏移(TransformDirection 将local坐标转换为世界坐标)
        shootDirection = shootDirection + shooterPoint.TransformDirection(new Vector3(Random.Range(-SpreadFactor, SpreadFactor), Random.Range(-SpreadFactor, SpreadFactor)));

        if (Physics.Raycast(shooterPoint.position, shootDirection, out hit, range))
        {
            Debug.Log(hit.transform.name + "打到了");
            if (hit.transform.CompareTag("zombie"))
            {
                zombiehealth zombieHealth = hit.transform.GetComponent<zombiehealth>();
                if (zombieHealth != null)
                {
                    zombieHealth.TakeDamage(10f);
                }
                GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
                Destroy(hitParticleEffect, 1f);
            }
            else
            {
                GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
                Destroy(hitParticleEffect, 1f);
                GameObject bulletHoleEffect = Instantiate(bulletHole, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
                Destroy(bulletHoleEffect, 0.8f);
            }
            if (mainCameraTransform != null)
            {
                Vector3 recoilVector = Vector3.up * 0.3f;
                mainCameraTransform.localPosition += recoilVector;
            }
        }

        if (!isAiming)
        {
            anim.CrossFadeInFixedTime("fire", 0.1f); // 播放普通开火动画(使用动画的淡出淡入效果)
        }
        else
        {
            // 瞄准状态下,播放瞄准的开火动画
            anim.Play("aim_fire", 0, 0f);
        }
        muzzleFlash.Play(); // 播放火光特效
        muzzleFlashLight.enabled = true;
        PlayerShootSound(); // 播放射击音效
        // 实例抛弹壳
        Instantiate(casingPrefab, casingSpawnPoint.transform.position, casingSpawnPoint.transform.rotation);
        currentBullets--;
        UpdateAmmoUI();
        fireTimer = 0f; // 重置计时器
    }

    public void PlayerShootSound()
    {
        audioSource.clip = AK47ShoundClip;
        audioSource.Play();
    }

    /// <summary>
    /// 填装弹药逻辑
    /// </summary>
    public void Reload()
    {
        if (bulletLeft <= 0) return;
        DoReloadAnimation();

        // 计算需要填充的子弹
        int bulletToLoad = bulletsMag - currentBullets;
        // 计算备弹扣除的子弹
        int bulletToReduce = (bulletLeft >= bulletToLoad) ? bulletToLoad : bulletLeft;
        bulletLeft -= bulletToReduce; // 备弹减少
        currentBullets += bulletToReduce; // 当前子弹增加
        UpdateAmmoUI();
    }

    // 播放装弹动画
    public void DoReloadAnimation()
    {
        if (currentBullets > 0)
        {
            anim.Play("reload_ammo_left", 0, 0);
            audioSource.clip = reloadAmmoLeftClip;
            audioSource.Play();
        }

        if (currentBullets == 0)
        {
            anim.Play("reload_out_of_ammo", 0, 0);
            audioSource.clip = reloadOutOFAmmoClip;
            audioSource.Play();
        }
    }
}


手臂摇摆

这个脚本通过监听鼠标输入,在武器手臂模型的位置上添加摇摆效果,增加了武器的真实感和动态性。实现了武器摇摆效果,具体包括:

  1. 摇摆参数:定义了摇摆的幅度(amout)、平滑值(smoothAmout)和最大幅度(maxAmout)。

  2. 初始位置:记录武器初始位置。

  3. Start 函数:在开始时获取武器的初始位置。

  4. Update 函数:每帧更新,根据鼠标输入控制武器的手臂模型位置。

  5. 摇摆计算:根据鼠标的移动输入,计算出摇摆的偏移量。

  6. 限制范围:限制摇摆的偏移量在一定范围内。

  7. 位置更新:根据摇摆的偏移量和初始位置,平滑地更新武器的位置,实现摇摆效果。

在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 武器摇摆
/// </summary>
public class WeaponSway : MonoBehaviour
{
    /*摇摆的参数*/
    public float amout; //摇摆幅度
    public float smoothAmout;//一个平滑值
    public float maxAmout;//最大摇摆幅度

    private Vector3 originPostion; //初始位置

    // Start is called before the first frame update
    void Start()
    {
        //自身位置(相对于父级物体变换得位置)
        originPostion = transform.localPosition;
    }

    // Update is called once per frame
    void Update()
    {
        //设置武器手臂模型位置得值,
        float movementX = -Input.GetAxis("Mouse X") * amout;
        float movementY = -Input.GetAxis("Mouse Y") * amout;
        //限制
        movementX = Mathf.Clamp(movementX, -maxAmout, maxAmout);
        movementY = Mathf.Clamp(movementY, -maxAmout, maxAmout);

        Vector3 finallyPostion = new Vector3(movementX, movementY, 0);
        //手柄位置变换
        transform.localPosition = Vector3.Lerp(transform.localPosition, finallyPostion + originPostion, Time.deltaTime * smoothAmout);
    }
}


手枪

这脚本实现了一个武器射击系统,包括:

  1. 武器属性:定义射程、弹匣容量等武器属性。
  2. 射击特效:创建火焰、粒子和弹孔特效。
  3. 射击模式:区分全自动和半自动射击,设置键位。
  4. 音效管理:播放射击和换弹音效。
  5. 瞄准与UI:按鼠标右键瞄准,显示准心和弹药数量。
  6. 射击逻辑:根据模式、键位和弹药数量触发射击。
  7. 装弹逻辑:播放动画、音效并填装弹药。
  8. 后坐力效果:模拟枪口抖动。
  9. 武器切换:按键切换主副武器。
  10. 更新UI:显示子弹数和射击模式。
  11. 瞄准逻辑:控制瞄准状态下视野和动画。

这些功能共同构建了玩家可以与武器进行交互的复杂系统。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class HandGunController : MonoBehaviour
{
    // 引用其他脚本和游戏对象
    public PlayerMovement PM;
    public Transform shooterPoint; // 武器射击的位置(枪口)

    // 武器参数
    public int range = 100; // 武器射程
    public int bulletsMag = 31; // 一个弹匣数量
    public int currentBullets; // 当前子弹数
    public int bulletLeft = 300; // 备弹

    // 枪口特效和音效
    public ParticleSystem muzzleFlash; // 枪口火焰特效
    public GameObject hitParticles; // 子弹击中粒子特效
    public GameObject bulletHole; // 弹孔
    public Light muzzleFlashLight; // 枪口火焰灯光
    public AudioClip HandGunSound; // 枪声音效片段
    public AudioClip reloadAmmoLeftClip; // 换子弹1音效片段
    public AudioClip reloadOutOFAmmoClip; // 换子弹2音效片段(拉枪栓)

    // 武器射速和键位设置
    public float fireRate = 0.1f; // 射速
    private float fireTimer; // 计时器
    private float SpreadFactor; // 射击的一点偏移量

    [SerializeField] private KeyCode reloadInputName; // 填装子弹按键
    [SerializeField] private KeyCode inspectInputName; // 查看武器按键
    [SerializeField] private KeyCode AutoRifleKey; // 主武器按键
    [SerializeField] private KeyCode HandGunKey; // 副武器按键
    [SerializeField] private KeyCode GunShootModelInputName; // 自动半自动切换按键

    // 控制器和状态
    private Animator anim; // 动画控制器
    private AudioSource audioSource; // 音效控制器
    private bool isReloading; // 是否在装弹
    private bool isAiming; // 是否在瞄准

    // 子弹壳抛出
    public Transform casingSpawnPoint; // 子弹壳抛出的位置
    public Transform casingPrefab; // 子弹壳预制体

    private Camera mainCamera; // 主摄像机
    public ChooseGunController CGC; // 切换武器类的实例

    // 射击模式和UI
    public enum ShootMode { AutoRifle, SemiGun }; // 射击模式枚举
    public ShootMode shootingMode; // 当前射击模式
    private bool GunShootInput; // 根据射击模式改变的射击键位输入
    private int modeNum = 0; // 模式切换的参数(1:全自动模式,2:半自动模式)
    private string shootModelName; // 射击模式名称

    // UI元素
    public Image crossHairUI; // 十字准心
    public Text ammoTextUI; // 弹药文本
    public Text ShootModelTextUI; // 射击模式文本

    private void Start()
    {
        audioSource = GetComponent<AudioSource>();
        anim = GetComponent<Animator>();
        currentBullets = bulletsMag;
        mainCamera = Camera.main;
        shootingMode = ShootMode.SemiGun; // G18手枪默认是半自动模式
        shootModelName = "clock";
        UpdateAmmoUI();
    }

    private void Update()
    {
        // 切换射击模式(全自动和半自动)
        if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 1)
        {
            modeNum = 1;
            shootModelName = "全自动";
            shootingMode = ShootMode.AutoRifle;
            ShootModelTextUI.text = shootModelName;
        }
        else if (Input.GetKeyDown(GunShootModelInputName) && modeNum != 0)
        {
            modeNum = 0;
            shootModelName = "半自动";
            shootingMode = ShootMode.SemiGun;
            ShootModelTextUI.text = shootModelName;
        }

        // 根据射击模式确定射击输入和射速
        switch (shootingMode)
        {
            case ShootMode.AutoRifle:
                GunShootInput = Input.GetMouseButton(0);
                fireRate = 0.08f;
                break;
            case ShootMode.SemiGun:
                GunShootInput = Input.GetMouseButtonDown(0);
                fireRate = 0.2f;
                break;
        }

        // 射击逻辑
        if (GunShootInput && currentBullets > 0)
        {
            GunFire();
        }
        else
        {
            muzzleFlashLight.enabled = false;
        }

        // 计时器更新
        if (fireTimer < fireRate)
        {
            fireTimer += Time.deltaTime;
        }

        anim.SetBool("Run", PM.isRun);
        anim.SetBool("Walk", PM.isWalk);
        AnimatorStateInfo info = anim.GetCurrentAnimatorStateInfo(0);

        // 判断是否在换子弹动画中
        if (info.IsName("reload_ammo_left") || info.IsName("reload_out_of_ammo"))
        {
            isReloading = true;
        }
        else
        {
            isReloading = false;
        }

        // 换子弹逻辑
        if (Input.GetKeyDown(reloadInputName) && currentBullets < bulletsMag && bulletLeft > 0)
        {
            Reload();
        }
        SpreadFactor = (isAiming) ? 0f : 0.01f;
        DoingAim();

        // 查看武器动画
        if (Input.GetKeyDown(inspectInputName))
        {
            anim.SetTrigger("Inspect");
        }

        // 切换武器
        if (Input.GetKeyDown(AutoRifleKey))
        {
            CGC.ChangeWeapon(0); // 切换到主武器(自动步枪)
        }
        if (Input.GetKeyDown(HandGunKey))
        {
            CGC.ChangeWeapon(1); // 切换到副武器(手枪)
        }
    }

    // 更新UI
    public void UpdateAmmoUI()
    {
        ammoTextUI.text = currentBullets + " / " + bulletLeft;
        ShootModelTextUI.text = shootModelName;
    }

    // 瞄准逻辑
    public void DoingAim()
    {
        if (Input.GetMouseButton(1) && !isReloading && !PM.isRun)
        {
            isAiming = true;
            anim.SetBool("Aim", isAiming);
            crossHairUI.gameObject.SetActive(false);
            mainCamera.fieldOfView = 40;
        }
        else
        {
            isAiming = false;
            anim.SetBool("Aim", isAiming);
            crossHairUI.gameObject.SetActive(true);
            mainCamera.fieldOfView = 60;
        }
    }

    // 射击逻辑
    public void GunFire()
    {
        if (fireTimer < fireRate || currentBullets <= 0 || isReloading || PM.isRun) return;

        RaycastHit hit;
        Vector3 shootDirection = shooterPoint.forward;
        shootDirection = shootDirection + shooterPoint.TransformDirection(new Vector3(Random.Range(-SpreadFactor, SpreadFactor), Random.Range(-SpreadFactor, SpreadFactor)));

        if (Physics.Raycast(shooterPoint.position, shootDirection, out hit, range))
        {
            Debug.Log(hit.transform.name + "打到了");
            if (hit.transform.CompareTag("zombie"))
            {
                zombiehealth zombieHealth = hit.transform.GetComponent<zombiehealth>();
                if (zombieHealth != null)
                {
                    zombieHealth.TakeDamage(20f);
                }
                GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
                Destroy(hitParticleEffect, 1f);
            }
            else
            {
                GameObject hitParticleEffect = Instantiate(hitParticles, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
                Destroy(hitParticleEffect, 1f);
                GameObject bulletHoleEffect = Instantiate(bulletHole, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
                Destroy(bulletHoleEffect, 0.8f);
            }
        }

        if (!isAiming)
        {
            anim.CrossFadeInFixedTime("fire", 0.1f);
        }
        else
        {
            anim.Play("aim_fire", 0, 0f);
        }
        muzzleFlash.Play();
        muzzleFlashLight.enabled = true;
        PlayerShootSound();
        Instantiate(casingPrefab, casingSpawnPoint.transform.position, casingSpawnPoint.transform.rotation);
        currentBullets--;
        UpdateAmmoUI();
        fireTimer = 0f;
    }

    // 播放射击音效
    public void PlayerShootSound()
    {
        audioSource.clip = HandGunSound;
        audioSource.Play();
    }

    // 填装子弹逻辑
    public void Reload()
    {
        if (bulletLeft <= 0) return;
        DoReloadAnimation();

        int bulletToLoad = bulletsMag - currentBullets;
        int bulletToReduce = (bulletLeft >= bulletToLoad) ? bulletToLoad : bulletLeft;
        bulletLeft -= bulletToReduce;
        currentBullets += bulletToReduce;
        UpdateAmmoUI();
    }

    // 播放填装子弹动画
    public void DoReloadAnimation()
    {
        if (currentBullets > 0)
        {
            anim.Play("reload_ammo_left", 0, 0);
            audioSource.clip = reloadAmmoLeftClip;
            audioSource.Play();
        }

        if (currentBullets == 0)
        {
            anim.Play("reload_out_of_ammo", 0, 0);
            audioSource.clip = reloadOutOFAmmoClip;
            audioSource.Play();
        }
    }
}

在这里插入图片描述


切枪效果

这脚本实现了武器切换功能,主要包括:

  1. 武器集合:使用列表(List)存储多个武器游戏对象。

  2. 初始索引:在开始时初始化索引。

  3. 武器切换方法:根据传入的武器索引,控制显示对应的武器并隐藏其他武器。

  4. 循环遍历:遍历武器列表,根据索引设置武器的显示状态。

  5. 子武器处理:根据切换情况,更新对应子武器的弹药UI显示。

这个脚本通过列表管理多个武器,并根据切换情况动态显示和隐藏武器,实现了角色切换武器的功能。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 武器切换
/// </summary>
public class ChooseGunController : MonoBehaviour
{
    //武器设置为集合,存放多个武器
    public List<GameObject> Weapons=new List<GameObject>();
    private int index;

    // Start is called before the first frame update
    void Start()
    {
        index = 0;
    }

    //切换武器方法(全部武器挂载人物手上,默认隐藏,,谁需要就显示显示出来)
    public void ChangeWeapon(int WeaponIndex)
    {
        for (int i = 0; i < Weapons.Count; i++)
        {
            if (WeaponIndex == i)
            {
                Weapons[i].gameObject.SetActive(true);
                if (WeaponIndex == 0)
                {

                    Weapons[i].gameObject.GetComponentInChildren<WeaponController>().UpdateAmmoUI();      
                   // print(Weapons[i].gameObject.name);
                }
                else if (WeaponIndex == 1)
                {
                    Weapons[i].gameObject.GetComponentInChildren<HandGunController>().UpdateAmmoUI();
                    //print(Weapons[i].gameObject.name);
                }
            }
            else
            {
                Weapons[i].gameObject.SetActive(false);
            }
        }

    }

  
}


动画状态机

行走、奔跑、跳跃、开枪、换弹、切枪、瞄准

在这里插入图片描述


玩家血量

这脚本管理玩家的生命和死亡:

  • 有声音效果:受伤和死亡音效。
  • 跟踪生命值和死亡状态。
  • 控制死亡效果:播放音效、变暗场景、延迟后重载。
  • 当玩家受伤时,减少生命值和出血量。
  • 逐渐降低场景的颜色饱和度。
  • 如果玩家死亡,重新加载当前场景。
using UnityEngine;
using UnityStandardAssets.ImageEffects;
using UnityEngine.SceneManagement;

public class Health : MonoBehaviour
{
    public AudioClip hitSound;  // 受伤音效
    public AudioClip deathSound;  // 死亡音效
    public float maxHealth = 100f;  // 最大生命值
    public float currentHealth = 100f;  // 当前生命值
    private float timer = 0;  // 计时器

    public static bool isDead = false;  // 是否死亡

    private ColorCorrectionCurves colorCurves;  // 颜色校正曲线
    private MouseLook mouth;  // 鼠标观察

    private void Start()
    {
        BleedBehavior.BloodAmount = 0;  // 出血行为的血量
        colorCurves = Camera.main.GetComponent<ColorCorrectionCurves>();  // 获取颜色校正组件
        mouth = Camera.main.GetComponent<MouseLook>();  // 获取鼠标观察组件
    }

    void Update()
    {
        if (isDead)
        {
            LevelReset();  // 重置关卡
        }
    }

    // 受伤方法
    public void TakeDamage(float damage)
    {
        if (isDead)
        {
            return;  // 如果已经死亡,不处理伤害
        }
        
        currentHealth -= damage;  // 减少生命值

        if (currentHealth <= 0f)
        {
            Die();  // 死亡
        }
        else
        {
            currentHealth -= damage;
            AudioSource.PlayClipAtPoint(hitSound, transform.position);  // 播放受伤音效
        }
        
        BleedBehavior.BloodAmount += Mathf.Clamp01(damage / currentHealth);  // 增加出血量
        BleedBehavior.BloodAmount = 0.52f;  // 固定出血量(这一行可能需要调整或删除)
    }

    // 死亡方法
    private void Die()
    {
        isDead = true;  // 设置死亡状态
        AudioSource.PlayClipAtPoint(deathSound, transform.position);  // 播放死亡音效
        //this.GetComponent<PlayerMovement>().enabled = false;
        mouth.enabled = false;
        colorCurves.enabled = true;
        Invoke("ReloadCurrentScene", 6f);  // 延迟重载当前场景
    }

    // 重置关卡方法
    public void LevelReset()
    {
        //timer += Time.deltaTime;
        colorCurves.saturation -= (Time.deltaTime / 1.5f);  // 减少饱和度
        //colorCurves.saturation = Mathf.Max(0, colorCurves.saturation);
    }

    // 重新加载当前场景方法
    private void ReloadCurrentScene()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
        isDead = false;
    }
}


新地图

创建太空站地图。

在这里插入图片描述
在这里插入图片描述


场景颜色渐变

在这里插入图片描述

using UnityEngine;
using UnityEngine.UI;

public class FadeOut : MonoBehaviour
{
	public float speed = 1.0f;

	private Image image;
	private float alpha = 1.0f;
	
	public AudioClip enter;


	private void Start()
	{
		image = GetComponent<Image>();
		GetComponent<AudioSource>().PlayOneShot(enter);

	}

	private void Update()
	{
		alpha = Mathf.Lerp(alpha, 0.0f, speed * Time.deltaTime);
		image.color = new Color(image.color.r, image.color.g, image.color.b, alpha);
	}
}

激光墙

玩家通过激光墙会受到伤害。
给激光墙添加光源、空间音效。

在这里插入图片描述

在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Laser : MonoBehaviour
{
    public int damage = 30;  // 伤害值
    public float damageDelay = 1f;  // 伤害延迟
    private float lastDamageTime = 0f;  // 上次伤害时间
    private GameObject player;  // 玩家对象

    void Start()
    {
        player = GameObject.FindGameObjectWithTag("Player");  // 获取玩家对象
    }

    void OnTriggerEnter(Collider other)
    {
        if (other.gameObject == player && Time.time > lastDamageTime + damageDelay)
        {
            player.GetComponent<Health>().TakeDamage(damage);  // 对玩家造成伤害
            lastDamageTime = Time.time;  // 更新上次伤害时间
        }
    }
}


获取钥匙

玩家获取钥匙才能打开后续的门。

在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class card : MonoBehaviour
{
	public AudioClip keyGrab;
	private GameObject player;

    void Start()
    {
	    player = GameObject.FindGameObjectWithTag("Player");
    }
 
	void OnTriggerEnter(Collider other)
	{
		if (other.gameObject == player)
		{
			AudioSource.PlayClipAtPoint(keyGrab, transform.position);
			Destroy(this.gameObject);
		}
	}
}


滑动门

玩家触碰到门,门会打开。

在这里插入图片描述

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class door : MonoBehaviour
{
	private GameObject player;
	public AudioClip enter;
	public AudioClip exit;
	public float speed = 1f;
	private bool isOpen = false;
	private bool isMoving = false;
	private Vector3 targetPosition;

	private void Start()
	{
		player = GameObject.FindGameObjectWithTag("Player");
		//targetPosition = transform.position;
	}

	private void Update()
	{
		if (isMoving)
		{
			// 修复:更新门的位置而不是碰撞体的位置
			this.gameObject.transform.position = Vector3.Lerp(transform.position, targetPosition, speed * Time.deltaTime);

			if (Vector3.Distance(transform.position, targetPosition) < 0.01f)
			{
				isMoving = false;
			}
		}
	}

	private void OnTriggerEnter(Collider other)
	{
		if (other.gameObject == player && !isOpen)
		{
			isOpen = true;
			targetPosition = new Vector3(transform.position.x, 5.3f, transform.position.z);
			AudioSource.PlayClipAtPoint(enter, transform.position);
			isMoving = true;
		}
	}

	private void OnTriggerExit(Collider other)
	{
		if (other.gameObject == player && isOpen)
		{
			isOpen = false;
			targetPosition = new Vector3(transform.position.x, 1f, transform.position.z);
			AudioSource.PlayClipAtPoint(exit, transform.position);
			isMoving = true;
		}
	}
}


NPC属性

在这里插入图片描述

  1. Start(): 在脚本启动时进行初始化,获取组件引用、设置初始参数、获取玩家对象,禁用导航代理,获取动画控制器。

  2. Update(): 在每帧更新中处理椭圆运动、朝向、接近原始位置的计算,以及判断玩家是否在视野范围内,根据情况设置移动方式和动画。

  3. OnTriggerEnter(Collider other): 当碰撞体进入触发器范围时,检查碰撞对象标签是否为玩家,如果是,设置攻击动画、播放音效,并对玩家造成伤害。

  4. OnTriggerExit(Collider other): 当碰撞体离开触发器范围时,检查碰撞对象标签是否为玩家,如果是,恢复奔跑动画。

脚本实现了僵尸的椭圆运动、追踪玩家、攻击玩家等基本逻辑。

using UnityEngine;
using UnityEngine.AI;

public class zombie : MonoBehaviour
{
	public float distance = 5f;  // 移动距离
	public float speed = 1f;  // 移动速度
	public float eccentricity = 0.8f;  // 离心率
	public float rotationSpeed = 5f;  // 旋转速度
	public float scale = 1f;  // 移动倍率
	public float maxDistance = 5f; // 最大距离,当玩家距离小于该值时,设置isInSight为true
	public int damage = 30;

	private Vector3 startPosition;
	private float elapsedTime = 0f;

	private GameObject player;
	private bool isInSight ;
	public AudioClip Sound;
	public AudioClip bit;
	private Animator zombieanimator;
	private NavMeshAgent agent;
	

	public 	bool isEllipseMovementEnabled ; // 是否允许椭圆运动
	public  bool canmove ; // 是否允许椭圆运动
	public zombiehealth zomhealth;

	private void Start()
	{
		zomhealth=this.GetComponent<zombiehealth>();
		isInSight = false;
		isEllipseMovementEnabled = true;
		canmove = false;
		startPosition = transform.position;
		player = GameObject.FindGameObjectWithTag("Player");
		agent = GetComponent<NavMeshAgent>();
		agent.enabled = false;
		zombieanimator = GetComponent<Animator>();
	}

	private void Update()
	{
		if (isEllipseMovementEnabled&&!zomhealth.isDead)
		{
			elapsedTime += Time.deltaTime * speed;

			// 根据参数计算椭圆轨迹上的位置
			float x = distance * Mathf.Cos(elapsedTime * scale);
			float z = distance * eccentricity * Mathf.Sin(elapsedTime * scale);

			Vector3 targetPosition = startPosition + new Vector3(x, 0f, z);

			// 平滑移动
			transform.position = Vector3.Lerp(transform.position, targetPosition, Time.deltaTime * speed);

			// 计算椭圆曲线上的切线向量
			float dx = -distance * scale * Mathf.Sin(elapsedTime * scale);
			float dz = distance * eccentricity * scale * Mathf.Cos(elapsedTime * scale);
			Vector3 tangent = new Vector3(dx, 0f, dz).normalized;

			// 计算平滑的旋转
			Quaternion targetRotation = Quaternion.LookRotation(tangent, Vector3.up);
			transform.rotation = Quaternion.Lerp(transform.rotation, targetRotation, Time.deltaTime * rotationSpeed);

			// 检查是否接近原始位置
			float distanceToStart = Vector3.Distance(transform.position, startPosition);

			if (distanceToStart < 0.1f)
			{
				// 返回原始位置,重置时间
				elapsedTime = 0f;
			}
		}

		if (!isInSight)
		{
			float playerDistance = Vector3.Distance(transform.position, player.transform.position);

			if (playerDistance < maxDistance&&!zomhealth.isDead)
			{
				isEllipseMovementEnabled = false; // 停止椭圆运动
				agent.enabled = true;
				canmove=true;
				isInSight = true;
				AudioSource.PlayClipAtPoint(Sound, transform.position);
				zombieanimator.SetBool("run",true);
			}
		}
		
		if(canmove){
			agent.SetDestination(player.gameObject.transform.position);}
	}
	
	void OnTriggerEnter(Collider other)
	{
		if (other.gameObject.tag == "Player")
		{
			//	zombieanimator.SetBool("run",false);

			zombieanimator.SetBool("attack",true);
			AudioSource.PlayClipAtPoint(bit, transform.position);
			player.GetComponent<health>().TakeDamage(damage);

		}
	}
	void OnTriggerExit(Collider other)
	{
		if (other.gameObject.tag == "Player")
		{
			zombieanimator.SetBool("run",true);
			zombieanimator.SetBool("attack",false);
			//zombieanimator.SetBool("attack",true);

		}
	}
	
}


攻击逻辑

玩家开枪能对怪兽造成伤害,怪兽接触到玩家也能造成伤害,这脚本管理了僵尸的生命和死亡。

在这里插入图片描述

  1. 生命管理:

    • 跟踪最大生命值和当前生命值。
    • 接收伤害并减少生命值,如果已死亡则不再处理。
  2. 死亡效果:

    • 播放死亡声音,禁用移动和导航代理。
    • 设置死亡动画,1.5秒后销毁僵尸对象。
using UnityEngine;
using UnityEngine.AI;

public class zombiehealth : MonoBehaviour
{
    public AudioClip deathSound;        // 死亡时播放的声音
    public float maxHealth = 100f;       // 僵尸的最大生命值
    public float currentHealth;          // 当前生命值
    public bool isDead;                  // 僵尸是否已死亡
    private NavMeshAgent agent;          // 导航网格代理组件的引用
    private Animator zombieanimator;     // 动画控制器组件的引用
    public zombie zom;                   // 僵尸脚本的引用

    private void Start()
    {
        zom = this.GetComponent<zombie>();                  // 获取僵尸脚本组件
        isDead = false;                                     // 初始化死亡状态为否
        currentHealth = maxHealth;                          // 设置初始生命值
        agent = GetComponent<NavMeshAgent>();               // 获取导航网格代理组件
        zombieanimator = GetComponent<Animator>();          // 获取动画控制器组件
    }

    public void TakeDamage(float damage)
    {
        if (isDead)
        {
            return;                                         // 如果已死亡,退出函数
        }

        currentHealth -= damage;                            // 减少生命值

        if (currentHealth <= 0f)
        {
            Die();                                          // 如果生命值小于等于0,触发死亡
        }
        else
        {
            currentHealth -= damage;                        // 多余的减少生命值操作
        }
    }

    private void Die()
    {
        GetComponent<AudioSource>().PlayOneShot(deathSound); // 播放死亡声音
        isDead = true;                                      // 设置死亡状态为真
        zom.canmove = false;                                // 禁用僵尸移动
        agent.enabled = false;                              // 禁用导航网格代理
        zombieanimator.SetBool("dead", true);               // 在动画控制器中设置"dead"参数为真
        Destroy(this.gameObject, 1.5f);                     // 1.5秒后销毁僵尸对象
    }
}


终点传送门

在这里插入图片描述
这脚本实现了玩家离开游戏场景的效果:

  1. 声音与特效:

    • 定义退出声音(ex)和退出音乐(exitmusic)。
    • 获取玩家对象和主摄像机的色彩校正曲线特效(ColorCorrectionCurves)组件。
  2. 重置场景:

    • 在离开触发区域后,增加色彩饱和度,逐渐变暗场景。
    • 播放退出声音和音乐,延迟后重新加载当前场景。
  3. 场景重载:

    • 重新加载当前游戏场景。

这个脚本通过渐变色彩和播放音效,为玩家离开游戏场景创造了一个独特的效果。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityStandardAssets.ImageEffects;

public class exit : MonoBehaviour
{
	public AudioClip ex;
	
	private GameObject player;
	
	public float delayTime=5f;
	private ColorCorrectionCurves colorCurves;
	
	private bool isend=false;
	public AudioClip exitmusic;

	
	
	// Start is called before the first frame update
	void Start()
	{
		player = GameObject.FindGameObjectWithTag("Player");
		colorCurves = Camera.main.GetComponent<ColorCorrectionCurves>();
	}


	void Update()
	{
		if (isend)
		{
			LevelReset();
		}
	}
   
	public void LevelReset()
	{
		//timer += Time.deltaTime;
		colorCurves.saturation += (Time.deltaTime);
		colorCurves.saturation = Mathf.Max(0, colorCurves.saturation);
	}
   
	void OnTriggerEnter(Collider other)
	{
		if (other.gameObject == player)
		{
			isend=true;
			colorCurves.enabled=true;
			AudioSource.PlayClipAtPoint(ex, transform.position);

			Invoke("playexitmusic", 1.5f);
			Invoke("ReloadCurrentScene", delayTime);
		}
	}
	
	private void ReloadCurrentScene()
	{
		SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex);
	}
	
	private void playexitmusic()
	{
		AudioSource.PlayClipAtPoint(exitmusic, transform.position);
	}
}



;