FPS游戏制作笔记


本文是根据VipSkill的FPS训练营课程整理的笔记。

  1. 第一人称控制器使用的是Unity官方的,不用自己造轮子。
  2. 使用另外一个摄影机单独捕捉武器可以在第一人称视角防止武器穿模问题。
  3. 相机中使用Post-Process Layer组件,单独一个空物体挂上Post-Process Volume组件可以有效提高画面质量。
  4. 武器相对于玩家的位置为(0.386,-0.3,0.32),数值仅供参考。
  5. 本Demo使用有限状态机进行状态转换,因此PlayState属性外面还包一层,用来控制赋值时使用一次的事件。
  6. 在动画状态机中,死亡设成触发器类型,丧尸重新刷新时用再用一个触发器类型回到Idle。
  7. 怪物攻击时在攻击部位绑定一个碰撞体进行判定,碰撞体上由代码控制开关和碰撞事件。
  8. 素材图片往往是一个多张图合成的图片,需要用Sprite Editor进行分割。
  9. 玩家不需要碰撞体,一般用Character Controller来控制,通常第一人称控制器已自带。
  10. 一般来说,枪不需要真的射出子弹,只需要使用射线判断即可,否则会出现很多bug。
  11. 我们不需要在每个怪物死亡的时候都对它们Destroy删除,这会造成许多性能上的损耗,通常是将他们的状态直接设为false,到下次生成的时候设为true,然后移动到想要的位置即可,通常对于需要频繁生成删除的物体尤其需要如此。
  12. 怪物使用Unity自带的Nav Mesh Agent 组件完成寻路,再次之前要把环境设为static,并在Navigation中进行烘焙。
  13. 对于UI,玩家,怪物生成器这种只需要一个的东西,我们都可以使用单例模式,这样就能够很方便地相互调用函数。

玩家代码:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityStandardAssets.Characters.FirstPerson;

public enum PlayerState
{
    Idle,
    Shoot,
    Reload
}

public class PlayerController : MonoBehaviour
{
    public static PlayerController Instance;

    [SerializeField]
    private PlayerState playerState;

    private Animator animator;
    private AudioSource audioSource;
    public AudioClip[] audioClips;

    private FirstPersonController firstPersonController;
    public Transform weapon;
    public Camera[] cameras;

    [SerializeField]
    private int hp = 100;


    #region 射击
    private float shootInterval = 0.1f;
    [SerializeField]
    private bool canShoot = true;
    #endregion

    #region 弹匣相关
    [SerializeField]
    private int curr_BulletNum;
    private int curr_MaxBulletNum = 30;
    [SerializeField]
    private int standby_BulletNum;
    private int standby_MaxBulletNum = 300;
    #endregion

    #region 射击效果
    public Transform FirePoint;
    public Image CrossImage;
    public GameObject[] Prefab_bulletEF;
    #endregion

    public PlayerState PlayerState
    {
        get => playerState;
        set {
            playerState = value;
            switch (playerState)
            {
                case PlayerState.Idle:
                    animator.SetBool("Reload", false);
                    animator.SetBool("Shoot", false);
                    FirePoint.gameObject.SetActive(false);
                    break;
                case PlayerState.Shoot:
                    if(curr_BulletNum > 0)
                    {
                        Shoot();
                    }
                    else
                    {
                        if (standby_BulletNum > 0 && curr_BulletNum < curr_MaxBulletNum )
                        {
                            PlayerState = PlayerState.Reload;
                        }
                        else
                        {
                            PlayerState = PlayerState.Idle;
                        }
                    }
                    
                    break;
                case PlayerState.Reload:                                                
                    FirePoint.gameObject.SetActive(false);
                    Debug.Log(1);
                    PlayAudio(1, 0.3f);
                    animator.SetBool("Shoot", false);
                    animator.SetBool("Reload", true);
                    
                    
                    break;

            }
        }

    }

    private void Awake()
    {
        Instance = this;
    }

    public void Init()
    {
        hp = 100;
        curr_BulletNum = curr_MaxBulletNum;
        standby_BulletNum = standby_MaxBulletNum;
        PlayerState = PlayerState.Idle;
        UI_Panel.Instance.UpdateHP_Text(hp);
    }

    void Start()
    {
        Application.targetFrameRate = 60;
        animator = GetComponentInChildren<Animator>();
        audioSource = GetComponent<AudioSource>();
        firstPersonController = GetComponent<FirstPersonController>();

        // 初始化子弹
        curr_BulletNum = curr_MaxBulletNum;
        standby_BulletNum = standby_MaxBulletNum;

        UpdateBulletUI();
        weapon.transform.localPosition = new Vector3(0.386f, -0.3f, 0.32f);
        PlayerState = PlayerState.Idle;
    }

    void Update()
    {
        StateForUpdate();
        if (Input.GetKeyDown(KeyCode.O))
        {
            hp -= 10;
            UI_Panel.Instance.UpdateHP_Text(hp);
        }

    }
    void StateForUpdate()
    {
        switch (playerState)
        {
            case PlayerState.Idle:
                if (canShoot && Input.GetMouseButton(0))
                {
                    PlayerState = PlayerState.Shoot;
                    return;
                }
                if (Input.GetKeyDown(KeyCode.R) && standby_BulletNum>0 && curr_BulletNum < curr_MaxBulletNum)
                {
                    PlayerState = PlayerState.Reload;
                    return;
                }
                if(Input.GetMouseButtonDown(1))
                {
                    StartAim();
                }
                if (Input.GetMouseButtonUp(1))
                {
                    StopAim();
                }
                break;
            case PlayerState.Shoot:
                if (Input.GetKeyDown(KeyCode.R) && standby_BulletNum > 0 && curr_BulletNum < curr_MaxBulletNum)
                { 
                    CancelInvoke("ReShootCD");
                    canShoot = true;
                    PlayerState = PlayerState.Reload;
                    return;
                }
                if (Input.GetMouseButtonDown(1))
                {
                    StartAim();
                }
                if (Input.GetMouseButtonUp(1))
                {
                    StopAim();
                }
                break;
            case PlayerState.Reload:
                if(animator.GetCurrentAnimatorClipInfo(0)[0].clip.name == "Replace"      //判断动画名称
                    && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1)     //1是指动画进度播放完一遍
                {
                    int want = curr_MaxBulletNum - curr_BulletNum;
                    if ((standby_BulletNum - want) < 0)
                    {
                        want = standby_BulletNum;
                    }
                    standby_BulletNum -= want;
                    curr_BulletNum += want;

                    UpdateBulletUI();
                    PlayerState = PlayerState.Idle;
                }
                break;
        
        }
    }

    public Camera ShootCamera;
    Vector2 ScreenCenterPoint = new Vector2(Screen.width / 2, Screen.height / 2);
    void Shoot()
    {
        curr_BulletNum -= 1;
        UpdateBulletUI();

        canShoot = false;
        // 射击表现
        PlayAudio(0);
        animator.SetBool("Shoot", true);
        FirePoint.gameObject.SetActive(true);
        StartShootRecoil();
        // 射线检测
        //Ray ray = ShootCamera.ScreenPointToRay(Input.mousePosition);
        Ray ray = ShootCamera.ScreenPointToRay(ScreenCenterPoint);
        if (Physics.Raycast(ray,out RaycastHit hitInfo, 1500f))
        {
            if(hitInfo.collider.gameObject.tag == "Zombie")
            {
                //攻击效果
                GameObject go = Instantiate(Prefab_bulletEF[1], hitInfo.point, Quaternion.identity);
                go.transform.LookAt(ShootCamera.transform);
                go.transform.SetParent(hitInfo.collider.gameObject.transform);
                //敌人逻辑
                ZombieController zombie = hitInfo.collider.gameObject.GetComponent<ZombieController>();
                if(zombie == null)
                    zombie = hitInfo.collider.gameObject.GetComponentInParent<ZombieController>();
                zombie.Hurt(10);

            }
            else if (hitInfo.collider.gameObject != this.gameObject)
            {
                //攻击效果
                GameObject go = Instantiate(Prefab_bulletEF[0], hitInfo.point, Quaternion.identity);
                go.transform.LookAt(ShootCamera.transform);
                go.transform.SetParent(hitInfo.collider.gameObject.transform);
            }
        }

        Invoke("ReShootCD", shootInterval);

    }

    private void UpdateBulletUI()
    {
        UI_Panel.Instance.UpdateCurrBullet_Text(curr_BulletNum, curr_MaxBulletNum);
        UI_Panel.Instance.UpdateStandByBullet_Text(standby_BulletNum);
    }

    private void ReShootCD()
    {
        canShoot = true;
        PlayerState = PlayerState.Idle;
    }

    private void PlayAudio(int index,float intensity = 1.0f)
    {
        audioSource.PlayOneShot(audioClips[index],intensity);    //playOnShot可以叠加播放
    }

    private void StartShootRecoil()
    {
        //瞄准器
        StartCoroutine(ShootRecoil_Cross());
        //视角
        StartCoroutine(ShootRecoil_Camera());

    }

    // 后坐力,瞄准器
    IEnumerator ShootRecoil_Cross()
    {
        Vector2 scale = CrossImage.transform.localScale;
        //放大
        while (scale.x < 1.3)
        {
            yield return null;
            scale.x += Time.deltaTime * 3;
            scale.y += Time.deltaTime * 3;
            CrossImage.transform.localScale = scale;
        }
        //缩小
        while (scale.x > 1)
        {
            scale.x -= Time.deltaTime * 3;
            scale.y -= Time.deltaTime * 3;
            CrossImage.transform.localScale = scale;
        }
        scale = Vector2.one;
        CrossImage.transform.localScale = scale;

    }
    //后坐力
    IEnumerator ShootRecoil_Camera()
    {
        float xOffset = Random.Range(0.5f, 0.7f);
        float yOffset = Random.Range(-0.2f, 0.2f);
        firstPersonController.xRotOffset = xOffset;
        firstPersonController.yRotOffset = yOffset;
        //yield return new WaitForSeconds(0.1f);
        yield return 6;
        firstPersonController.xRotOffset = -xOffset;
        firstPersonController.yRotOffset = -yOffset;
        // yield return new WaitForSeconds(0.1f);
        yield return 6;
        firstPersonController.xRotOffset = 0;
        firstPersonController.yRotOffset = 0;
    }

    void StartAim()
    {
        StopCoroutine("DoStartAim");
        StartCoroutine("DoStartAim");
    }
    void StopAim()
    {
        StopCoroutine("DoStopAim");
        StartCoroutine("DoStopAim");
    }
    IEnumerator DoStartAim()
    {
        Vector3 pos = weapon.transform.localPosition;
        foreach (Camera camera in cameras)
        {
            camera.fieldOfView = 60;
        }

        while (pos.x > 0)
        {
            pos.x -= Time.deltaTime * 2;
            weapon.transform.localPosition = pos;
            yield return null;
        }
        pos.x = 0;
        weapon.transform.localPosition = pos;
        for (int i = 0; i < 10; i++)
        {
            yield return null;
            foreach (Camera camera in cameras)
            {
                camera.fieldOfView -= 2;
            }
        }
        
        
    }
    IEnumerator DoStopAim()
    {
        Vector3 pos = weapon.transform.localPosition;
        foreach (Camera camera in cameras)
        {
            camera.fieldOfView = 42;
        }

        while (pos.x < 0.386)
        {
            pos.x += Time.deltaTime * 2;
            weapon.transform.localPosition = pos;
            yield return null;
        }
        pos.x = 0.386f;
        weapon.transform.localPosition = pos;
        for (int i = 0; i < 10; i++)
        {
            yield return null;
            foreach (Camera camera in cameras)
            {
                camera.fieldOfView += 2;
            }
        }

    }

    public void Hurt(int damge)
    {
        hp -= damge;
        if(hp <= 0)
        {
            hp = 0;
            //TODO:死亡逻辑
            Dead();
        }
        UI_Panel.Instance.UpdateHP_Text(hp);
    }

    void Dead()
    {
        ZombieManager.Instance.StopAllZombie();
        UI_Panel.Instance.PlayerDead();
        firstPersonController.enabled = false;
        this.enabled = false;
        Cursor.lockState = CursorLockMode.Confined;
        Cursor.visible = true;
    }

    public void Revive()
    {
        Init();
        ZombieManager.Instance.StartAllZombie();
        UI_Panel.Instance.PlayerRevive();
        firstPersonController.enabled = true;
        this.enabled = true;
        Cursor.lockState = CursorLockMode.Locked;
    }

    public void Win()
    {
        Init();
        UI_Panel.Instance.YouWin();
        firstPersonController.enabled = false;
        this.enabled = false;
        Cursor.lockState = CursorLockMode.Confined;
        Cursor.visible = true;
    }

    public void ExitGame()
    {
        Application.Quit();
    }
}

丧尸代码:

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

public enum ZombieState
{
    Idle,
    Walk,
    Run,
    Attack,
    Hurt,
    Dead
}

public class ZombieController : MonoBehaviour
{
    [SerializeField]
    private ZombieState zombieState;
    private NavMeshAgent navMeshagent;
    private AudioSource audioSource;
    private Animator animator;
    private CapsuleCollider capsuleCollider;
    public ZombieWeapon weapon;

    [SerializeField]
    public int hp = 50;
    public AudioClip[] FootstepAudioClips;
    public AudioClip[] IdleAudioClips;
    public AudioClip[] HurtAudioClips;
    public AudioClip[] AttackAudioClips;

    private Vector3 target;

    public ZombieState ZombieState
    {
        get => zombieState;
        set
        {
            if(zombieState == ZombieState.Dead && value != ZombieState.Idle)
            {
                return;
            }
            zombieState = value;
            //Debug.Log(zombieState);
            switch (zombieState)
            {
                case ZombieState.Idle:
                    animator.SetBool("Walk", false);
                    animator.SetBool("Run", false);
                    navMeshagent.isStopped = true;
                    Invoke("GoWalk", Random.Range(1, 3));
                    break;
                case ZombieState.Walk:
                    animator.SetBool("Walk", true);
                    animator.SetBool("Run", false);
                    navMeshagent.isStopped = false;
                    navMeshagent.speed = 0.5f;
                    //去一个目标点
                    target = GameManager.Instance.GetPoints();
                    navMeshagent.SetDestination(target);

                    break;
                case ZombieState.Run:
                    navMeshagent.speed = 3f;
                    animator.SetBool("Walk", false);
                    animator.SetBool("Run", true);
                    navMeshagent.isStopped = false;
                    break;
                case ZombieState.Attack:
                    navMeshagent.isStopped = true;
                    animator.SetBool("Walk", false);
                    animator.SetBool("Run", false);
                    animator.SetTrigger("Attack");
                    break;
                case ZombieState.Hurt:
                    navMeshagent.isStopped = true;
                    animator.SetBool("Walk", false);
                    animator.SetBool("Run", false);
                    animator.SetTrigger("Hurt");

                    break;
                case ZombieState.Dead:
                    navMeshagent.isStopped = true;
                    animator.SetTrigger("Dead");
                    animator.SetBool("Walk", false);
                    animator.SetBool("Run", false);
                    capsuleCollider.enabled = false;
                    Invoke("Destroy", 5);
                    break;

            }
        }
    }

    void Start()
    {
        navMeshagent = GetComponent<NavMeshAgent>();
        audioSource = GetComponent<AudioSource>();
        animator = GetComponent<Animator>();
        capsuleCollider = GetComponent<CapsuleCollider>();
        ZombieState = ZombieState.Idle;
        weapon.Init(this);
    }

    //处理脏数据
    public void Init()
    {
        animator.SetTrigger("Init");
        capsuleCollider.enabled = true;
        hp = 100;
        ZombieState = ZombieState.Idle;
    }

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

    void StateForUpdate()
    {
        float dis = PlayerController.Instance.PlayerState == PlayerState.Shoot ? 30f : 10f;
        switch (zombieState)
        {
            case ZombieState.Idle:
                break;
            case ZombieState.Walk:
                if (Vector3.Distance(transform.position, PlayerController.Instance.transform.position) < dis)
                {
                    //去追玩家
                    ZombieState = ZombieState.Run;
                    return;
                }
                if (Vector3.Distance(target,transform.position) < 1)
                {
                    ZombieState = ZombieState.Idle;
                }
                break;
            case ZombieState.Run:
                navMeshagent.SetDestination(PlayerController.Instance.transform.position);
                
                if (Vector3.Distance(transform.position, PlayerController.Instance.transform.position) < 2f)
                {
                    ZombieState = ZombieState.Attack;
                }

                break;
            case ZombieState.Attack:
                if (animator.GetCurrentAnimatorClipInfo(0)[0].clip.name == "Attack"      
                    && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1)
                {
                    ZombieState = ZombieState.Run;
                }

                    break;
            case ZombieState.Hurt:
                break;
            case ZombieState.Dead:
                break;

        }
    }

    void GoWalk()
    {
        ZombieState = ZombieState.Walk;
    }

    public void Hurt(int value)
    {
        hp -= value;
        if (hp <= 0)
        {
            hp = 0;
            ZombieState = ZombieState.Dead;
        }
        else
        {
            //击退
            StartCoroutine(MovePuase());
        }
    }

    void Destroy()
    {
        ZombieManager.Instance.ZombieDead(this);
    }

    IEnumerator MovePuase()
    {
        ZombieState = ZombieState.Hurt;
        yield return new WaitForSeconds(0.5f);
        if(ZombieState != ZombieState.Run)
        {
            ZombieState = ZombieState.Run;
        }
    }

    #region 动画事件
    void IdelAudio()
    {
        if (Random.Range(0, 4) == 1)
        {
            audioSource.PlayOneShot(IdleAudioClips[Random.Range(0, IdleAudioClips.Length)]);
        }
    }

    void FootStep()
    {
        audioSource.PlayOneShot(FootstepAudioClips[Random.Range(0, FootstepAudioClips.Length)]);
    }

    private void HurtAudio()
    {
        audioSource.PlayOneShot(HurtAudioClips[Random.Range(0, HurtAudioClips.Length)]);
    }

    private void AttackAudio()
    {
        audioSource.PlayOneShot(AttackAudioClips[Random.Range(0, AttackAudioClips.Length)]);
    }

    private void StartAttack()
    {
        weapon.StartAttack();
    }
   
    private void EndAttack()
    {
        weapon.EndAttack();
    }
    #endregion
}

文章作者: 微笑紫瞳星
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 微笑紫瞳星 !
  目录