Unity之FPS

news2024/9/20 17:33:05

目录

🎮MouseLook摄像机旋转脚本

🎮PickUpItem武器拾取脚本

🎮PlayerController玩家控制器

🎮Inventory武器库

🎮Weapon武器抽象类

🎮Weapon_AutomaticGun武器脚本


其实这个教程很早就收藏了就是被20个小时时长给吓到了一直没开始。终于在七月中旬的时候开始动手跟着做了。


最近一直加班也很忙,参与的第一个项目计划在年底上线,也是有项目经验的人了哈哈。然后拖拖拉拉的一直跟着教程做,只是个半成品敌人部分还没做,以我目前的精力就先做到这里了。原教程链接:第一人称射击游戏教程2.0【已完结】_哔哩哔哩_bilibiliicon-default.png?t=O83Ahttps://www.bilibili.com/video/BV1J8411d75s/?spm_id_from=333.999.0.0&vd_source=6b664fb3236a2f45f99b9eb3868cdf31


 教程讲的很细,有C#和Unity基础的做到我现在这样没什么太大的问题,但教程讲的也有点慢,所以时长才会达到二十小时,我看的时候是1.5倍速看的。

主要功能:枪械有AK、消音m4、格洛克、霰弹枪、狙击枪;玩家行走、奔跑、跳跃、下蹲;

不同枪械不同的装弹方式、瞄准状态、准星动态变化、全自动半自动模式切换、武器库切换武器、随机弹道、武器的检视功能。

演示视频:

20240920

由于间隔时间太长本篇笔记就不从头开始讲了,只把核心脚本代码和Demo源码放出来,脚本代码也有对应的注释,感兴趣的话大家可以结合原教程博主和下面的核心代码一起看。


MouseLook摄像机旋转脚本

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

/// <summary>
/// 摄像机旋转
/// 玩家左右旋转控制实现左右移动
/// 摄像机上下旋转控制视线上下移动
/// </summary>
public class MouseLook : MonoBehaviour
{
    [Tooltip("视野灵敏度")]public float mouseSenstivity = 400f;

    public Transform playerBody;  //玩家位置

    private float yRotation = 0f; //摄像机上下旋转的数值

    private CharacterController characterController;
    [Tooltip("当前摄像机的初始高度")] public float height = 1.8f;
    private float interpolationSpeed = 12f;
    
    // Start is called before the first frame update
    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        //playerBody = transform.GetComponent<PlayerController>().transform;
        characterController = GetComponentInParent<CharacterController>();
    }

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

        yRotation -= mouseY;//上下旋转
        //限制旋转大小
        yRotation = Mathf.Clamp(yRotation, -60f, 60f);
        transform.localRotation = Quaternion.Euler(yRotation,0f,0f);//摄像机上下旋转
        playerBody.Rotate(Vector3.up * mouseX);//玩家左右移动
        
        //当人物下蹲或站立时,摄像机的高度也会发生变化
        float heightTarget = characterController.height * 0.9f;
        height = Mathf.Lerp(height, heightTarget, interpolationSpeed * Time.deltaTime);
        //设置下蹲站立时的摄像机高度
        transform.localPosition = Vector3.up * height; 
    }
}

PickUpItem武器拾取脚本

using UnityEngine;

/// <summary>
/// 武器拾取
/// </summary>
public class PickUpItem : MonoBehaviour
{
    [Tooltip("武器旋转的速度")] private float rotateSpeed;
    [Tooltip("武器编号")] public int itemID;
    private GameObject weaponModel;
    
    // Start is called before the first frame update
    void Start()
    {
        rotateSpeed = 100f;
    }

    // Update is called once per frame
    void Update()
    {
        transform.eulerAngles += new Vector3(0, rotateSpeed * Time.deltaTime, 0);
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.name == "Player")
        {
            PlayerController player = other.GetComponent<PlayerController>();
            
            //查找获取 Inventory 物体下的各个武器物体
            weaponModel = GameObject.Find("Player/Assult_Rife_Arm/Inventory/").gameObject.transform.GetChild(itemID).gameObject;
            //Debug.Log(weaponModel.name);
            player.PickUpWeapon(itemID,weaponModel);
            Destroy(gameObject);
        }
    }
}

PlayerController玩家控制器

using UnityEngine;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{

    private CharacterController characterController;

    public Vector3 moveDirection;   //人物控制方向
    private AudioSource audioSource;

    [Header("玩家数值")] 
    private float Speed;
    [Tooltip("行走速度")] public float walkSpeed;
    [Tooltip("奔跑速度")] public float runSpeed;
    [Tooltip("下蹲速度")] public float crouchSpeed;
    [Tooltip("玩家生命值")] public float playerHealth;
    [Tooltip("跳跃的力")] public float jumpForce;
    [Tooltip("下落的力")] public float fallForce;
    [Tooltip("下蹲时玩家的高度")] public float crouchHeight;
    [Tooltip("站立时玩家的高度")] public float standHeight;
    

    [Header("键位设置")] 
    [Tooltip("奔跑")] private KeyCode runInputName = KeyCode.LeftShift;
    [Tooltip("跳跃")] private KeyCode jumpInputName = KeyCode.Space;
    [Tooltip("下蹲")] private KeyCode crouchInputName = KeyCode.LeftControl;

    [Header("玩家属性判断")] 
    public MovementState state;
    private CollisionFlags collisionFlags;
    public bool isWalk;
    public bool isRun;
    public bool isJump;
    public bool isGround;//玩家是否在地面上
    public bool isCanCrouch; //判断玩家是否可以下蹲
    public bool isCrouching; //判断玩家是否处于下蹲状态
    private bool playerIsDead; //判断玩家是否死亡
    private bool isDamage; //判断玩家是否受到伤害

    public LayerMask crouchLayerMask;
    public Text playerHealthUI;

    [Header("音效")] 
    [Tooltip("行走音效")] public AudioClip walkSound;
    [Tooltip("奔跑音效")] public AudioClip runningSound;

    private Inventory inventory;
    
    // Start is called before the first frame update
    void Start()
    {
        characterController = GetComponent<CharacterController>();
        audioSource = GetComponent<AudioSource>();
        walkSpeed = 4f;
        runSpeed = 6f;
        crouchSpeed = 2f;
        jumpForce = 0f;
        fallForce = 10f;
        isGround = true;
        crouchHeight = 1f;
        standHeight = characterController.height;
        inventory = GetComponentInChildren<Inventory>();
        playerHealth = 100f;

        playerHealthUI.text = "生命值:" + playerHealth;
    }

    // Update is called once per frame
    void Update()
    {
        CanCrouch();
        if (Input.GetKey(crouchInputName))
        {
            Crouch(true);
        }
        else
        {
            Crouch(false);
        }
        PlayerFootSoundSet();
        Moving();
        Jump();
    }

    public void Moving()
    {
        //GetAxis 不急停  GetAxisRaw  急停
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");

        isRun = Input.GetKey(runInputName);
        isWalk = (Mathf.Abs(h) > 0 || Mathf.Abs(v) > 0) ? true : false;
        if (isRun && isGround && isCanCrouch && !isCrouching)
        {
            state = MovementState.running;
            Speed = runSpeed;
        }
        else if(isGround)
        {
            state = MovementState.walking;
            Speed = walkSpeed;
            if (isCrouching)
            {
                state = MovementState.crouching;
                Speed = crouchSpeed;
            }
        }
        
        //同时按下蹲和奔跑时,人物应该是下蹲速度
        if (isRun && isCrouching)
        {
            state = MovementState.crouching;
            Speed = crouchSpeed;
        }

        moveDirection = (transform.right * h + transform.forward * v).normalized;
        characterController.Move(moveDirection * Speed * Time.deltaTime); //人物移动
    }

    public void Jump()
    {
        if(!isCanCrouch) return; //不能进行站立时也不能进行跳跃操作
        isJump = Input.GetKeyDown(jumpInputName);
        //按下跳跃键并且在地面上才会进行跳跃
        if (isJump && isGround)
        {
            isGround = false;
            jumpForce = 5f;
        }
        
        //如果当前没有按下空格并且在检测地面上,那么isGround判断为false
        else if(!isJump && isGround)
        {
            isGround = false;
            jumpForce = -2f;
        }
        
        //此时玩家已经跳起来了
        if (!isGround)
        {
            jumpForce = jumpForce - fallForce * Time.deltaTime;
            Vector3 jump = new Vector3(0,jumpForce * Time.deltaTime,0); //将向上的力转换为V3坐标
            collisionFlags = characterController.Move(jump); //调用角色控制器移动方法,向上方模拟跳跃

            /*判断玩家是否在地面上  
             *CollisionFlags: characterController 内置的碰撞位置标识号
             * CollisionFlags.Below: 在地面上
             */
            if (collisionFlags == CollisionFlags.Below)
            {
                isGround = true;
                jumpForce = 0f;
            }
            //玩家什么都没有碰到说明不在地面上
            // if (isGround && collisionFlags == CollisionFlags.None)
            // {
            //     isGround = false;
            // }
        }
    }
    
    //判断人物是否可以下蹲
    //isCanCrouch: true说明头上没有物体可以下蹲
    public void CanCrouch()
    {
        //获取人物头顶的高度V3位置
        Vector3 sphereLocation = transform.position + Vector3.up * standHeight;
        //Debug.Log("sphereLocation:" + sphereLocation);
        //判断头顶和他自己碰撞的个数是否等于0
        isCanCrouch = (Physics.OverlapSphere(sphereLocation,characterController.radius,crouchLayerMask).Length) == 0;
        //
        // Collider[] colis = Physics.OverlapSphere(sphereLocation, characterController.radius, crouchLayerMask);
        // for (int i = 0; i < colis.Length; i++)
        // {
        //     Debug.Log("colis:" + colis[i].name);
        // }
        // Debug.Log("colis:" + colis.Length);
        //Debug.Log("isCanCrouch:" + isCanCrouch);
    }

    //下蹲
    public void Crouch(bool newCrouching)
    {
        if(!isCanCrouch) return; //不可下蹲时(在隧道),不能进行站立
        isCrouching = newCrouching;
        characterController.height = isCrouching ? crouchHeight : standHeight; //根据下蹲状态设置下蹲
        characterController.center = characterController.height / 2.0f * Vector3.up; //将角色控制器的中心位置Y,从头顶往下减少一半的高度
    }

    //人物移动音效
    public void PlayerFootSoundSet()
    {
        //sqrMagnitude 返回该向量的平方长度
        if (isGround && moveDirection.sqrMagnitude > 0)
        {
            audioSource.clip = isRun ? runningSound : walkSound;
            if (!audioSource.isPlaying)
            {
                //播放行走或者奔跑音效
                audioSource.Play();
            }
        }
        else
        {
            if (audioSource.isPlaying)
            {
                //暂停播放行走或奔跑音效
                audioSource.Pause();
            }
        }
        //下蹲时不播放新增音效
        if (isCrouching)
        {
            if (audioSource.isPlaying)
            {
                audioSource.Pause();
            }
        }
    }

    /// <summary>
    /// 拾取武器
    /// </summary>
    public void PickUpWeapon(int itemID,GameObject weapon)
    {
        //捡到武器后,在武器库里添加,否则补充弹药
        if (inventory.weapons.Contains(weapon))
        {
            weapon.GetComponent<Weapon_AutomaticGun>().bulletLeft =
                weapon.GetComponent<Weapon_AutomaticGun>().bulletMag * 5;
            
            weapon.GetComponent<Weapon_AutomaticGun>().UpdateAmmoUI();
            Debug.Log("集合里已存在此枪械,补充弹药");
            return;
        }
        else
        {
            inventory.AddWeapon(weapon);
        }
    }

    /// <summary>
    /// 玩家生命值
    /// </summary>
    /// <param name="damage">接收到的伤害值</param>
    public void PlayerHealth(float damage)
    {
        playerHealth -= damage;
        isDamage = true;
        playerHealthUI.text = "生命值:" + playerHealth;
        if (playerHealth <= 0)
        {
            playerIsDead = true;
            playerHealthUI.text = "玩家死亡";
            Time.timeScale = 0; //游戏暂停
        }
    }

    public enum MovementState
    {
        walking,
        running,
        crouching,
        idle
    }
}

Inventory武器库

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

/// <summary>
/// 武器库
/// 人物的武器切换,添加,去除功能
/// </summary>
public class Inventory : MonoBehaviour
{
    //武器库
    public List<GameObject> weapons = new List<GameObject>();
    
    //当前使用的武器编号
    public int currentWeaponID;
    
    // Start is called before the first frame update
    void Start()
    {
        currentWeaponID = -1;
    }

    // Update is called once per frame
    void Update()
    {
        ChargeCurrentWeaponID();
    }

    /// <summary>
    /// 更新武器编号
    /// </summary>
    public void ChargeCurrentWeaponID()
    {
        //鼠标滚轮向下 -0.1  向上 0.1 不动 0
        if (Input.GetAxis("Mouse ScrollWheel") < 0)
        {
            //下一把武器
            ChargeWeapon(currentWeaponID + 1);
        }
        else if(Input.GetAxis("Mouse ScrollWheel") > 0)
        {
            //上一把武器
            ChargeWeapon(currentWeaponID - 1);
        }

        //通过数字键盘来切换武器
        for (int i = 0; i < 10; i++)
        {
            if (Input.GetKeyDown(KeyCode.Alpha0 + i))
            {
                int num = 0;
                if (i == 10)
                {
                    num = 10;
                }
                else
                {
                    num = i - 1;
                }
                /*  只有数字小于武器列表个数的时候才能进一步处理
                    如果有3把武器,但按下4-9,那么是无意义的,不处理
                 */
                if (num<weapons.Count)
                {
                    ChargeWeapon(num);
                }
            }
        }
    }

    /// <summary>
    /// 武器切换
    /// </summary>
    /// <param name="weaponID">武器编号</param>
    public void ChargeWeapon(int weaponID)
    {
        if(weapons.Count == 0 )return;
        
        //如果切换到最大索引编号的枪的时候,就调出第一把枪
        //如果切换到最小索引编号的枪的时候,就调出最后一把枪
        
        //IndexOf: 获取列表中元素首次出现的索引
        //Max list里取最大元素
        if (weaponID > weapons.Max(weapons.IndexOf))
        {
            weaponID = weapons.Min(weapons.IndexOf);
        }
        else if (weaponID < weapons.Min(weapons.IndexOf))
        {
            weaponID = weapons.Max(weapons.IndexOf);
        }

        if (weaponID == currentWeaponID)
        {
            //只有一种武器不进行切换
            return;
        }

        //更新武器索引
        currentWeaponID = weaponID;

        //根据武器编号,显示出对应的武器
        for (int i = 0; i < weapons.Count; i++)
        {
            if (weaponID == i)
            {
                weapons[i].gameObject.SetActive(true);
            }
            else
            {
                weapons[i].gameObject.SetActive(false);
            }
        }
    }

    /// <summary>
    /// 拾取武器
    /// </summary>
    public void AddWeapon(GameObject weapon)
    {
        if (weapons.Contains(weapon))
        {
            print("集合已存在此枪械");
            return;
        }
        else
        {
            if (weapons.Count < 10) //最多携带3把枪
            {
                weapons.Add(weapon);
                ChargeWeapon(currentWeaponID+1);//显示武器
                weapon.gameObject.SetActive(true);
            }
        }
    }

    /// <summary>
    /// 丢弃武器
    /// </summary>
    public void ThrowWeapon(GameObject weapon)
    {
        if (!weapons.Contains(weapon) || weapons.Count == 0)
        {
            print("没有这个武器,无法抛弃");
            return;
        }
        else
        {
            weapons.Remove(weapon);//集合中删除对应的武器
            ChargeWeapon(currentWeaponID-1);
            weapon.gameObject.SetActive(false);
        }
    }
}

Weapon武器抽象类

using UnityEngine;

public abstract class Weapon : MonoBehaviour
{
    public abstract void GunFire();

    public abstract void Reload();

    public abstract void ExpandingCrossUpdate(float expanDegree);

    public abstract void DoReloadAnimation();

    public abstract void AimIn();

    public abstract void AimOut();
}

Weapon_AutomaticGun武器脚本

using UnityEngine;
using Random = UnityEngine.Random;
using System.Collections;
using System.Collections.Generic;
using System.Numerics;
using UnityEngine.UI;
using Quaternion = UnityEngine.Quaternion;
using Vector3 = UnityEngine.Vector3;

//武器音效内部类
[System.Serializable]
public class SoundClips
{
    public AudioClip shootSound; //开火音效
    public AudioClip silencerShootSound; //开火音效带消音器
    public AudioClip reloadSoundAmmotLeft; //换子弹音效
    public AudioClip reloadSoundOutOfAmmo; //换子弹并拉枪栓(一个弹匣打完)
    public AudioClip aimSound; //瞄准音效
}

public class Weapon_AutomaticGun : Weapon
{
    public Animator animator;
    private PlayerController playerController;

    private Camera mainCamera;
    public Camera gunCamera;

    public bool IS_AUTORIFLE; //是否是自动武器
    public bool IS_SEMIGUN; //是否半自动武器
    
    [Header("武器部件位置")] 
    [Tooltip("射击的位置")] public Transform ShootPoint; //射线打出的位置 
    public Transform BulletShootPoint; //子弹特效打出的位置
    [Tooltip("子弹壳抛出的位置")] public Transform CasingBulletSpawnPoint;

    [Header("子弹预制体和特效")] 
    public Transform bulletPrefab; //子弹
    public Transform casingPrefab; //子弹抛壳

    [Header("枪械属性")] 
    [Tooltip("武器射程")] private float range;
    [Tooltip("武器射速")] public float fireRate;
    private float originRate; //原始射速
    private float SpreadFactor; //射击的一点偏移量
    private float fireTimer; //计时器  控制武器射速
    private float bulletForce; //子弹发射的力

    [Tooltip("当前武器的每个弹匣子弹数")] public int bulletMag;
    [Tooltip("当前子弹数")] public int currentBullets;
    [Tooltip("备弹")] public int bulletLeft;
    public bool isSilencer; //是否装备消音器
    private int shotgunFragment = 8;//1次打出的子弹数

    [Header("特效")] 
    public Light muzzleflashLight; //开火灯光
    private float lightDuration; //灯光持续时间
    public ParticleSystem muzzlePatic; //灯光火焰粒子特效1
    public ParticleSystem sparkPatic; //灯光火焰粒子特效2(火星子)v
    public int minSparkEmission = 1;
    public int maxSparkEmission = 7;

    [Header("音源")] 
    private AudioSource mainAudioSource;
    public SoundClips soundClips;
    //private AudioSource shootAudioSource;

    [Header("UI")] 
    public Image[] crossQuarterImgs; //准心
    public float currentExoanedDegree; //当前准心的开合度
    private float crossExpanedDegree; //每帧准心开合度
    private float maxCrossDegree;//最大开合度
    public Text ammoTextUI;
    public Text shootModeTextUI;

    public PlayerController.MovementState state;

    private bool isReloading; //判断是否在换弹
    [Header("键位设置")] 
    [SerializeField] [Tooltip("填装子弹按键")] private KeyCode reloadInputName = KeyCode.R;
    [SerializeField] [Tooltip("自动半自动切换按键")] private KeyCode gunShootModelInputName = KeyCode.V;
    [SerializeField] [Tooltip("检视武器按键")] private KeyCode inspectInputName = KeyCode.F;
    
    //切换全自动半自动
    public ShootMode shootingMode;
    private bool GunShootInput;
    private int modeNum; //模式切换的一个中间参数(1:全自动,2:半自动)
    private string shootModeName;
    
    //瞄准
    private bool isAiming;
    private Vector3 sniperingFiflePosition; //枪默认的初始位置
    public Vector3 sniperingFifleOnPosition; //开始瞄准的模型位置
    
    //------------------------------修改瞄准时的声音播放问题,只针对本项目,与B站教程无关
    private bool IsAimInPlay = true;

    [Header("狙击镜设置")] 
    [Tooltip("狙击镜材质")] public Material scopeRenderMaterial;
    [Tooltip("当没有进行瞄准时狙击镜的颜色")] public Color fadeColor;
    [Tooltip("当瞄准时狙击镜的颜色")] public Color defaultColor;
    
    

    private void Awake()
    {
        playerController = GetComponentInParent<PlayerController>();
        mainAudioSource = GetComponent<AudioSource>();
        animator = GetComponent<Animator>();
        mainCamera = Camera.main;
    }

    private void Start()
    {
        muzzleflashLight.enabled = false;
        crossExpanedDegree = 50f;
        maxCrossDegree = 300f;
        lightDuration = 0.02f;
        range = 300f;
        bulletLeft = bulletMag * 5;
        currentBullets = bulletMag;
        bulletForce = 100f;
        originRate = 0.1f;
        //随机弹道精准度
        SpreadFactor = 0.05f;
        UpdateAmmoUI();

        //根据不同枪械,游戏刚开始时进行不同射击模式设置
        if (IS_AUTORIFLE)
        {
            modeNum = 1;
            shootModeName = "全自动";
            shootingMode = ShootMode.AutoRifle;
            UpdateAmmoUI();
        }
        if (IS_SEMIGUN)
        {
            modeNum = 0;
            shootModeName = "半自动";
            shootingMode = ShootMode.SemiGun;
            UpdateAmmoUI();
        }
        
        //isAiming是否是瞄准状态
        isAiming = false;

        sniperingFiflePosition = transform.localPosition;
    }

    private void Update()
    {
        // 自动枪械鼠标输入方式 可以在 GetMouseButton 和 GetMouseButtonDown 里切换
        if (IS_AUTORIFLE)
        {
            //切换射击模式(全自动和半自动)
            if (Input.GetKeyDown(gunShootModelInputName) && modeNum != 1)
            {
                modeNum = 1;
                shootModeName = "全自动";
                shootingMode = ShootMode.AutoRifle;
                UpdateAmmoUI();
            }
            else if(Input.GetKeyDown(gunShootModelInputName) && modeNum != 0)
            {
                modeNum = 0;
                shootModeName = "半自动";
                shootingMode = ShootMode.SemiGun;
                UpdateAmmoUI();
            }
            //控制射击模式的转换  后面就要用代码去动态控制了
            switch (shootingMode)
            {
                case ShootMode.AutoRifle:
                    GunShootInput = Input.GetMouseButton(0);
                    fireRate = originRate;
                    break;
                case ShootMode.SemiGun:
                    GunShootInput = Input.GetMouseButtonDown(0);
                    fireRate = 0.2f;
                    break;
            }
        }
        else
        {
            //半自动枪械鼠标输入方式改为GetMouseButtonDown
            GunShootInput = Input.GetMouseButtonDown(0);
        }

        state = playerController.state;//这里实时获取任务的移动状态(行走,奔跑,下蹲)
        if (state == PlayerController.MovementState.walking && Vector3.SqrMagnitude(playerController.moveDirection)>0 && 
            state != PlayerController.MovementState.running && state != PlayerController.MovementState.crouching)
        {
            //移动时的准心开合度
            ExpandingCrossUpdate(crossExpanedDegree);
        }
        else if (state != PlayerController.MovementState.walking && state == PlayerController.MovementState.running &&
                 state != PlayerController.MovementState.crouching)
        {
            //奔跑时的准心开合度(2倍)
            ExpandingCrossUpdate(crossExpanedDegree*2);
        }
        else
        {
            //站立或者下蹲时,不调整准心开合度
            ExpandingCrossUpdate(0);
        }
        
        if (GunShootInput && currentBullets > 0)
        {
            //霰弹枪射击1次同时打出8次射线,其余枪械正常1次射线
            if (IS_SEMIGUN && gameObject.name == "4")
            {
                shotgunFragment = 8;
            }
            else
            {
                shotgunFragment = 1;
            }

            //开枪射击
            GunFire();   
        }
        
        //腰射和瞄准射击精准不同
        SpreadFactor = (isAiming) ? 0.01f : 0.1f;

        if (Input.GetKeyDown(inspectInputName))
        {
            animator.SetTrigger("inspect");
        }

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

        //playerController.isWalk;
        animator.SetBool("Run",playerController.isRun);
        animator.SetBool("Walk",playerController.isWalk);
        
        //两种换子弹的动画(包括霰弹枪换弹动画逻辑)
        AnimatorStateInfo info = animator.GetCurrentAnimatorStateInfo(0);
        if (
              info.IsName("reload01") || 
              info.IsName("reload") || 
              info.IsName("reload_open") ||
              info.IsName("reload_insert") ||
              info.IsName("reload_insert 1") ||
              info.IsName("reload_insert 2") ||
              info.IsName("reload_insert 3") ||
              info.IsName("reload_insert 4") ||
              info.IsName("reload_insert 5") ||
              info.IsName("reload_close")
            )
        {
            isReloading = true;
        }
        else
        {
            isReloading = false;
        }
        
        //根据当前霰弹枪子弹数填装的数量判断结束insert动画
        //解决了本来放2个子弹,但实际上添加子弹的动画播放了3次的问题
        if (
             (
                 info.IsName("reload_insert") ||
                 info.IsName("reload_insert 1") ||
                 info.IsName("reload_insert 2") ||
                 info.IsName("reload_insert 3") ||
                 info.IsName("reload_insert 4") ||
                 info.IsName("reload_insert 5")) && currentBullets == bulletMag)
        {
            //当前霰弹枪子弹填装完毕,随时结束换弹判断
            animator.Play("reload_close");
            isReloading = false;
        }

        //按R换弹
        if (Input.GetKeyDown(reloadInputName) && currentBullets < bulletMag && bulletLeft > 0 && !isReloading && !isAiming)
        {
            DoReloadAnimation();
        }
        
        //鼠标右键进入瞄准
        if (Input.GetMouseButton(1) && !isReloading && !playerController.isRun)
        {
            isAiming = true;
            animator.SetBool("Aim",isAiming);
            //瞄准时需要微调一下枪的模型位置
            transform.localPosition = sniperingFifleOnPosition;
            AimIn();
        }
        else if(Input.GetMouseButtonUp(1))
        {
            isAiming = false;
            animator.SetBool("Aim",isAiming);
            transform.localPosition = sniperingFiflePosition;
            AimOut();
        }
    }

    //射击
    public override void GunFire()
    {
        //控制射速或当前没有子弹的时候就不可以发射了
        //animator.GetCurrentAnimatorStateInfo(0).IsName("take_out")  获取第0层动画中名为take_out动画的状态
        if (fireTimer < fireRate || playerController.isRun || currentBullets <= 0 || animator.GetCurrentAnimatorStateInfo(0).IsName("take_out") || isReloading) return;

        //调用协程控制开火灯光
        StartCoroutine(MuzzleFlashLightCon());
        
        muzzlePatic.Emit(1);//每次发射一个枪口火焰
        sparkPatic.Emit(Random.Range(minSparkEmission,maxSparkEmission));//发射枪口火星粒子
        StartCoroutine(Shoot_Crss()); //增大准心大小
        
        if (!isAiming)
        {
            //播放普通开火动画(使用动画的淡入淡出效果)
            animator.CrossFadeInFixedTime("fire",0.1f);
        }
        else
        {
            //瞄准状态下,播放瞄准开火动画
            animator.Play("aim_fire",0,0);
        }

        for (int i = 0; i < shotgunFragment; i++)
        {
            RaycastHit hit;
            Vector3 shootDirection = ShootPoint.forward;//射击方向
            shootDirection = shootDirection + ShootPoint.TransformDirection(new Vector3(Random.Range(-SpreadFactor,SpreadFactor),Random.Range(-SpreadFactor,SpreadFactor)));
            //射线参数: 发射点,方向,碰撞信息,最大距离(射线检测的方式从屏幕正中心射出)
            if (Physics.Raycast(ShootPoint.position, shootDirection, out hit, range))
            {
                Transform bullet;
                if (IS_AUTORIFLE || (IS_SEMIGUN && gameObject.name=="2"))
                {
                    //实例化出子弹拖尾特效(拖尾特效里包含击中和弹孔特效)
                    bullet = (Transform)Instantiate(bulletPrefab, BulletShootPoint.transform.position,
                        BulletShootPoint.transform.rotation);
                }
                else
                {
                    //霰弹枪特殊处理下,将子弹限制位置设定到 hit.point
                    bullet = Instantiate(bulletPrefab, hit.point, Quaternion.FromToRotation(Vector3.up, hit.normal));
                }

                
                //给子弹拖尾向前的力(加上射线打出去的偏移值)
                bullet.GetComponent<Rigidbody>().velocity = (bullet.transform.forward + shootDirection)*bulletForce;
                //Debug.Log("打到了" + hit.transform.gameObject.name);
            }   
        }
        //实例抛弹壳
        Instantiate(casingPrefab, CasingBulletSpawnPoint.transform.position, CasingBulletSpawnPoint.transform.rotation);
        //根据是否装备消音器,切换不同的射击音效
        mainAudioSource.clip = isSilencer ? soundClips.silencerShootSound : soundClips.shootSound;
        //mainAudioSource.clip = soundClips.shootSound;
        mainAudioSource.Play(); //播放射击音效
        fireTimer = 0f; //重置计时器
        currentBullets--; //子弹数量减少
        UpdateAmmoUI();
        //ExpendCross(30);
    }

    //设置开火的灯光
    IEnumerator MuzzleFlashLightCon()
    {
        muzzleflashLight.enabled = true;
        yield return new WaitForSeconds(lightDuration);
        muzzleflashLight.enabled = false;
    }

    //换弹逻辑
    public override void Reload()
    {
        if(bulletLeft <= 0) return;
        //计算需要填充的子弹
        int bulletToload = bulletMag - currentBullets;
        //计算备弹扣除的子弹数
        int bulletToReduce = bulletLeft >= bulletToload ? bulletToload : bulletLeft;
        bulletLeft -= bulletToReduce;//备弹减少
        currentBullets += bulletToReduce;//当前子弹增加
        UpdateAmmoUI();
    }

    /// <summary>
    /// 霰弹枪换弹逻辑
    /// </summary>
    public void ShotGunReload()
    {
        if (currentBullets < bulletMag)
        {
            currentBullets++;
            bulletLeft--;
            UpdateAmmoUI();
        }
        else
        {
            animator.Play("reload_close");
            return;
        }
        if(bulletLeft <= 0) return;
    }

    //根据指定大小,来增加或减小准心的开合度
    public override void ExpandingCrossUpdate(float expanDegree)
    {
        if (currentExoanedDegree < expanDegree-5)
        {
            ExpendCross(150 * Time.deltaTime);
        }
        else if(currentExoanedDegree > expanDegree + 5)
        {
            ExpendCross(-300 * Time.deltaTime);
        }
    }

    //换弹(播放动画)
    public override void DoReloadAnimation()
    {
        if (!(IS_SEMIGUN && (gameObject.name == "4" || gameObject.name == "6")))
        {
            if (currentBullets > 0 && bulletLeft > 0)
            {
                animator.Play("reload01",0,0);
                Reload();
                mainAudioSource.clip = soundClips.reloadSoundAmmotLeft;
                mainAudioSource.Play();
            }
            if (currentBullets == 0 && bulletLeft > 0)
            {
                animator.Play("reload",0,0);
                Reload();
                mainAudioSource.clip = soundClips.reloadSoundOutOfAmmo;
                mainAudioSource.Play();
            }   
        }
        else
        {
            if(currentBullets == bulletMag) return;
            //霰弹枪换子弹动画触发
            animator.SetTrigger("shotgun_reload");
        }
    }

    //进入瞄准,隐藏准心,摄像机视野变近
    public override void AimIn()
    {
        float currentVelocity = 0f;
        for (int i = 0; i < crossQuarterImgs.Length; i++)
        {
            crossQuarterImgs[i].gameObject.SetActive(false);
        }
        
        //狙击枪瞄准的时候,改变gunCamera的视野和瞄准镜的颜色
        if (IS_SEMIGUN && (gameObject.name == "6"))
        {
            scopeRenderMaterial.color = defaultColor;
            gunCamera.fieldOfView = 10;
        }

        //瞄准的时候摄像机视野变近
        mainCamera.fieldOfView = Mathf.SmoothDamp(30,60,ref currentVelocity,0.1f);
        if (IsAimInPlay == true)
        {
            IsAimInPlay = false;
            mainAudioSource.clip = soundClips.aimSound;
            mainAudioSource.Play();   
        }
    }

    //退出瞄准,显示准心,摄像机视野恢复
    public override void AimOut()
    {
        float currentVelocity = 0f;
        for (int i = 0; i < crossQuarterImgs.Length; i++)
        {
            crossQuarterImgs[i].gameObject.SetActive(true);
        }
        
        //狙击枪瞄准的时候,改变gunCamera的视野和瞄准镜的颜色
        if (IS_SEMIGUN && (gameObject.name == "6"))
        {
            scopeRenderMaterial.color = fadeColor;
            gunCamera.fieldOfView = 35;
        }
        
        mainCamera.fieldOfView = Mathf.SmoothDamp(60,30,ref currentVelocity,0.1f);
        if (IsAimInPlay == false)
        {
            IsAimInPlay = true;
            mainAudioSource.clip = soundClips.aimSound;
            mainAudioSource.Play();   
        }
    }

    //改变准心的开合度,并且记录当前准心开合度
    public void ExpendCross(float add)
    {
        crossQuarterImgs[0].transform.localPosition += new Vector3(-add,0,0); //左准心
        crossQuarterImgs[1].transform.localPosition += new Vector3(add,0,0); //右准心
        crossQuarterImgs[2].transform.localPosition += new Vector3(0,add,0); //上准心
        crossQuarterImgs[3].transform.localPosition += new Vector3(0,-add,0); //下准心

        currentExoanedDegree += add;//保持当前准心开合度
        currentExoanedDegree = Mathf.Clamp(currentExoanedDegree, 0, maxCrossDegree);//限制准心开合度大小
    }

    //协程,调用准心开合度,1帧执行了7次
    //只负责射击时瞬间增大准心
    public IEnumerator Shoot_Crss()
    {
        yield return null;
        for (int i = 0; i < 7; i++)
        {
            ExpendCross(Time.deltaTime*500);
        }
    }
    
    //更新子弹的UI
    public void UpdateAmmoUI()
    {
        ammoTextUI.text = currentBullets + "/" + bulletLeft;
        shootModeTextUI.text = shootModeName;
    }
    
    public enum ShootMode
    {
        AutoRifle,
        SemiGun
    }
}

游戏素材资源包下载地址:【免费】FPS游戏资源包FPS游戏资源包资源-CSDN文库

Demo源码资源下载地址:五种武器FPS游戏Demo资源-CSDN文库

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2149525.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL之表内容的增删改查(含oracel 9i经典测试雇佣表下载)

目录 一:Create 二:Retrieve 1.select列 2.where条件 3.结果排序 4. 筛选分页结果 三:Update 四:Delete 1.删除数据 2. 截断表 五&#xff1a;插入查询结果 六&#xff1a;聚合函数 七:group by子句的使用 表内容的CRUD操作 : Create(创建), Retrieve(读取)…

助力企业降低成本,ByteHouse打造新一代“弹性”云数仓

更多技术交流、求职机会&#xff0c;欢迎关注字节跳动数据平台微信公众号&#xff0c;回复【1】进入官方交流群 在当今的互联网应用中&#xff0c;业务流量往往具有很大的不确定性。 例如&#xff0c;电商平台在 “618”“双 11” 等促销活动期间&#xff0c;访问量会呈爆发式增…

计算机毕业论文题目之基于Web技术B/S结构的新生管理系统包含报道,寝室宿舍,缴费学费,数据统计分析汇总等功能的源代码下载

为了满足功能需求&#xff0c;我们将设计并实现一个基于Web技术的B/S架构下的新生管理系统。本系统旨在通过前端与后端分离的设计模式&#xff0c;为用户提供简洁、高效的交互体验&#xff0c;并确保数据的安全性和系统的可扩展性。下面将从系统架构、功能模块以及技术选型三个…

LeetCode[中等] 142. 环形链表 II

给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整…

SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决

0. 问题现象 我们 watchOS 中的 App 和 Widgets 共享同一个 SwiftData 底层数据库&#xff0c;但是在 App 中对数据库所做的更改并不能被 Widgets 所感知。换句话说&#xff0c;App 更新了数据但在 Widgets 中却看不到。 如上图所示&#xff1a;我们的 App 在切换至后台之前会…

你是不是分不清哪些字体是商用,哪些非商用?快来看,免得莫名其妙负债。

前言 最近发现有好多小伙伴在做PPT的时候&#xff0c;都有一个很不好的习惯&#xff1a;没有调整好字体。 这里说的没有调整好字体的意思是&#xff1a;在一些公开发布的内容上使用一些可能造成侵权的字体。 字体侵权‌的后果相当严重。轻者可能面临法律纠纷&#xff0c;重者…

软件开发团队时间管理的5大技巧

软件开发团队运用时间管理技巧&#xff0c;有助于提升项目效率&#xff0c;确保任务按时完成&#xff0c;减少资源浪费&#xff0c;节约开发时间&#xff0c;增强团队协作&#xff0c;最终有利于项目成功交付。如果开发团队不采取时间管理技巧&#xff0c;可能导致项目延期、资…

如何搭建客户服务知识库?五项基本方法让你业务增长100%

在竞争激烈的市场环境中&#xff0c;优质的客户服务已成为企业脱颖而出的关键。而一个高效、全面的客户服务知识库&#xff0c;不仅能够提升客户满意度&#xff0c;还能显著降低客服团队的工作负担&#xff0c;促进业务的稳健增长。本文将介绍五项基本方法&#xff0c;帮助你搭…

SpringBoot Admin调整类的日志级别

进入 SpringBoot Admin &#xff0c;通过服务名称&#xff0c; 找到服务后。 点击 “日志” – “日志配置” &#xff0c;输入类名&#xff0c;即可调整 这个类的日志级别。

Python模块和包:标准库模块(os, sys, datetime, math等)②

文章目录 一、os 模块1.1 获取当前工作目录1.2 列出目录内容1.3 创建和删除目录1.4 文件和目录操作 二、sys 模块2.1 获取命令行参数2.2 退出程序2.3 获取 Python 版本信息 三、datetime 模块3.1 获取当前日期和时间3.2 日期和时间的格式化3.3 日期和时间的运算 四、math 模块4…

.NET常见的几种项目架构模式,你知道几种?

前言 项目架构模式在软件开发中扮演着至关重要的角色&#xff0c;它们为开发者提供了一套组织和管理代码的指导原则&#xff0c;以提高软件的可维护性、可扩展性、可重用性和可测试性。 假如你有其他的项目架构模式推荐&#xff0c;欢迎在文末留言&#x1f91e;&#xff01;&am…

flutter遇到问题及解决方案

目录 1、easy_refresh相关问题 2、 父子作用域关联问题 3. 刘海屏底部安全距离 4. 了解保证金弹窗 iOS端闪退 &#xff08;待优化&#xff09; 5. loading无法消失 6. dialog蒙版问题 7. 倒计时优化 8. scrollController.offset报错 9. 断点不走 10.我的出价报红 11…

4、(PCT)Point Cloud Transformer

4、&#xff08;PCT&#xff09;Point Cloud Transformer 论文链接&#xff1a;PCT论文链接 本篇论文介绍Transformer在3D点云领域的应用&#xff0c;Transformer在NLP领域和图像处理领域都得到了广泛的应用&#xff0c;特别是近年来在图像领域的应用&#xff0c;本篇论文主要…

希亦超声波清洗机值得购买吗?百元清洁技术之王,大揭秘!

现代社会的高速发展&#xff0c;很多人由于工作繁忙的原因&#xff0c;根本没有时间去清洗自己的日常物品&#xff0c;要知道这些日常物品堆积灰尘之后是很容易就滋生细菌的&#xff0c;并且还会对人体的健康造成一定的危害&#xff01;这个时候很多人就会选择购买一台超声波清…

耐高温滑环的应用场景及市场前景分析

耐高温滑环是一种重要的电气连接装置&#xff0c;广泛应用于需要传递电力和信号的旋转设备中。随着工业技术的发展&#xff0c;对耐高温滑环的需求不断增加&#xff0c;尤其是在极端温度环境下的应用场合&#xff0c;耐高温滑环展现出其独特的优势。 耐高温滑环在工业自动化领…

全国网安众测招募计划启动啦,欢迎加入~

在数字化时代&#xff0c;网络安全已成为维护社会稳定、促进经济发展的基石。为了积极响应国家关于加强网络安全工作的号召&#xff0c;确保某区域关键信息系统的稳固运行&#xff0c;我们特此启动一项网络安全众测活动。该活动旨在通过汇聚业界有经验的网络安全攻防人才&#…

【小程序 - 大智慧】深入微信小程序的渲染周期

目录 前言应用生命周期页面的生命周期组件的生命周期渲染顺序页面路由运行机制更新机制同步更新异步更新 前言 跟 Vue、React 框架一样&#xff0c;微信小程序框架也存在生命周期&#xff0c;实质也是一堆会在特定时期执行的函数。 小程序中&#xff0c;生命周期主要分成了三…

使用 VSCode 在 Python 中创建项目环境

了解如何管理 Python 项目的不同环境&#xff0c;欢迎来到雲闪世界。 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; 介绍 创建数据科学项目非常简单。如今&#xff0c;有了众多资源&#xff0c;您只需选择开发工具并启动项目即可。 除了多个人工智能机…

24.9.16数据结构|平衡二叉树

一、理解逻辑 平衡二叉是有限制的二叉搜索树&#xff0c;满足平衡因子绝对值小于1的二叉搜索树是平衡二叉树。 平衡指的是树的左右两边的节点左右高度平衡&#xff0c;要求平衡因子处于规定范围 平衡因子&#xff1a;该节点的左高度-右高度&#xff0c;绝对值小于1 如何平衡化&…

2024年9月20日历史上的今天大事件早读

公元前480年9月20日 希腊人在爱琴海萨拉米海战中击败了波斯人 383年9月20日 发生“淝水之战” 1013年9月20日 《君臣事迹》书成&#xff0c;赐名《册府元龟》 1519年9月20日 葡萄牙航海家麦哲伦环球航行 1644年9月20日 清顺治帝驾车由盛京出发&#xff0c;迁都北平&#xf…