Unity 实战【Fps 枪击游戏-结合官方FPS案例 Microgame】

【简介】

  • 该游戏主要的功能是分为player玩家拿手枪 对野怪进行射击 player ,野怪也可以对玩家进行攻击,野怪可以对玩家进行攻击。野怪可以巡逻 在一定的范围内 可以自动监控到玩家进行攻击。攻击野怪一定的血量后 野怪会被杀死 这时候可以加血。。。
79D8F18D-34A6-40F3-A814-2D875F0150FA.gif

将项目可拆解成以下几个模块:

1.添加场景以及资源导入
2.脚本动态拼接地板
3.player制作
4.角色控制器CharacterController
5.枪后坐力
6.枪口与相机位置的调整
7.子弹连发以及开火音效
8.AI 导航网格和导航代理
9.AI 检测敌人自动开火
10.人物血条UI 显示
11.敌人随机生成

  • 添加场景以及资源导入

    image.png

    Assest 目录下为所有资源素材,包含了PlayerEnmey 以及天空盒子 地板的所有素材。
    image.png

  • 脚本动态拼接地板
    动态创建地板以及墙面,可以通过脚本去for 循环遍历依次添加每个地步的位置以及高度

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

public class Ryunm_FloorLayout : MonoBehaviour
{
    public GameObject basicFloor;
    public Vector3 cellSet = Vector3.one;
    public Vector3 floorSize = Vector3.zero;

    public Vector3 StartPos = Vector3.zero;

    public List floorList;

    [ContextMenu("LayoutFloor")]
    public void LayoutFloors()
    {
        floorList = this.GetComponentsInChildren().ToList();//
        for (int i=1;i

然后动态去配置参数 控制地板的数量。


image.png
  • player制作
    Player 玩家主要包括 body身体 Eyes 视角 checkPoint 瞄准视角
    Eyes 主要是由枪的模型以及相机视角组成
    image.png
  • 角色控制器CharacterController
    player 玩家的移动以及WeaponCamera 的移动可以结合CharacterController的API去操作会更加的方便
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Ryunm_FPS_PlayerController : MonoBehaviour
{


    public float mouseOffset_x;
    public float mouseOffset_y;
    public float h_InputValue;
    public float v_InputValue;

    public float rotateSpeed = 180;
    public Transform playerTransform;
    public Transform eyesTransform;
    public float x_rotateLimit = 65;
    public float x_angleValue = 0;

    public CharacterController _player_CC;
    public float moveSpeed = 5;
    public Vector3 motionVlaue = Vector3.zero;
    public float GravityValue = -10;
    public float Volecity_y=0;
    public bool isFloor = false;
    public Transform checkPoint;
    public float checkRaius = 0.5f;
    public LayerMask floorLayer;
    public float jumpHight = 10;

    public Ryunm_AnimatorController playerAni;
    void Start()
    {
        Cursor.lockState = CursorLockMode.Locked;
        Cursor.visible = false;

        if (_player_CC == null)
        {
            _player_CC = this.GetComponent();
        }
        isFloor = false;
    }

    void Update()
    {
        mouseOffset_x = Input.GetAxis("Mouse X");
        mouseOffset_y = Input.GetAxis("Mouse Y");
        h_InputValue = Input.GetAxisRaw("Horizontal");
        v_InputValue = Input.GetAxisRaw("Vertical");

        PlayerViewChange();
        PlayerMove();

        if (h_InputValue != 0 || v_InputValue != 0)
        {
            playerAni.Alerted = true;
            playerAni.MoveSpeed = moveSpeed;
        }
        else
        {
            playerAni.Alerted = false;
            playerAni.MoveSpeed = 0;
        }
    }

    void PlayerViewChange()
    {
        playerTransform.Rotate(Vector3.up*rotateSpeed*Time.deltaTime* mouseOffset_x);

        x_angleValue -= rotateSpeed * Time.deltaTime * mouseOffset_y;
        x_angleValue = Mathf.Clamp(x_angleValue,-x_rotateLimit, x_rotateLimit);

        Quaternion qua = Quaternion.Euler(new Vector3(x_angleValue, eyesTransform.localEulerAngles.y, eyesTransform.localEulerAngles.z)) ;
        eyesTransform.localRotation = qua;
    }
    void PlayerMove()
    {
        if (_player_CC == null) return;
        motionVlaue = Vector3.zero;
        float forwardMoveVlaue = v_InputValue * moveSpeed * Time.deltaTime;
        motionVlaue += _player_CC.transform.forward * forwardMoveVlaue;
        motionVlaue += _player_CC.transform.right * (h_InputValue*moveSpeed*Time.deltaTime);

        Volecity_y += GravityValue * Time.deltaTime;
        motionVlaue += Vector3.up * Volecity_y * Time.deltaTime * 0.5f;
        isFloor = Physics.CheckSphere(checkPoint.position, checkRaius, floorLayer);
        if (isFloor)
        {
            if (Volecity_y < 0)
            {
                Volecity_y = 0;
            }
        }
        if (Input.GetButtonDown("Jump"))
        {
            float jumpTime = Mathf.Sqrt(Mathf.Abs(jumpHight * 2 / GravityValue));
            Volecity_y = -GravityValue * jumpTime;
            print(Volecity_y);
            isFloor = false;
        }
        
        _player_CC.Move(motionVlaue);
    }
}

上面是挂载在Player身上的脚本 通过获取鼠标的偏移值mouseOffset_x
mouseOffset_y h_InputValue v_InputValue _player_CC 去控制移动 跳。。。

  • 枪后坐力以及开火音效


    image.png

    枪在开火的时候会往后偏移 开完火会移到原位置。主要的思路是,定义好前后挪动的位置,在开火 结束开火偏移到对应位置。对应脚本的逻辑如下:

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

public class Ryunm_WeaponController : MonoBehaviour
{
 
    public Transform bulletStartPoint;
    public GameObject bulletPrefabs;
    public float bulletSpeed = 500;
    public AudioSource fireAudio;


    public float intervalTime = 0.2f;
    public float fireTimer = 0;

    public Transform weaponBody;
    public Transform startPoint;
    public Transform backPoint;
    public float lerpValue = 0.2f;

    public Camera mainCamera;
    public Camera weaponCamera;
    public float startFieldView = 60;
    public float endFieldView = 30;
    public Vector3 weaponCameraStartPoint;
    public Vector3 weaponCameraCenterPoint;
    public float smoothTime = 0.5f;

    public Ryunm_AnimatorController playerAni;

    public Slider bulletCountSlider;
    public float maxBulletCount = 50;
    public float currentBulletCount = 0;
    public float reloadSpeed = 5;
    public float reloadWaitTime = 2;


    void Start()
    {
        currentBulletCount = maxBulletCount;//Bullet step1
        bulletCountSlider = Ryunm_GameManager._instance.bulletCountSlider;
        bulletCountSlider.maxValue = maxBulletCount;
        bulletCountSlider.value = currentBulletCount;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            StopCoroutine("RealoadBullet");
            OpenFire();
        }
        if (Input.GetMouseButton(0))
        {
            fireTimer += Time.deltaTime;
            if (fireTimer>= intervalTime)
            {
                OpenFire();
            }
        }
        if (Input.GetMouseButtonUp(0))
        {
            //StartCoroutine(RealoadBullet());
            StartCoroutine("RealoadBullet");
        }
     
        if (Input.GetMouseButtonDown(1))
        {
            StopCoroutine("ToStart");
            StartCoroutine("ToCenter");
        }
        if (Input.GetMouseButtonUp(1))
        {
            StopCoroutine("ToCenter");
            StartCoroutine("ToStart");
        }


    }
    public void OpenFire()
    {
        if (currentBulletCount > 0)
        {
            playerAni.Attack();
            fireTimer = 0;
            if (fireAudio)
            {
                fireAudio.Play();
            }
          
            GameObject newBullet = Instantiate(bulletPrefabs, bulletStartPoint.position, bulletStartPoint.rotation);
            newBullet.GetComponent().bulletType = BulletType.Player_Bullet;
           
            newBullet.GetComponent().velocity = newBullet.transform.forward * bulletSpeed;

            currentBulletCount -= 1;
            bulletCountSlider.value = currentBulletCount;

            StopCoroutine("WeaponBack");
            StartCoroutine("WeaponBack");
        }
        else
        {
        }
    }

    IEnumerator WeaponBack()
    {
        while (weaponBody.localPosition.z>backPoint.localPosition.z+0.005f)
        {
            weaponBody.localPosition = Vector3.Lerp(weaponBody.localPosition, backPoint.localPosition, lerpValue);
            yield return null;
        }
        while (weaponBody.localPosition.z
  • 枪口与相机位置的调整
    player移动的过程中 相机是跟随的,MainCamera是在player的子节点下面这样会导致 相机的视角中也会看到 player ,而我们需要的是视野中只有抢的模型。所有我们需要把相机拆成两个部分 一个视角是正前方 一个视角只能看到WeaponCamera

    image.png

    image.png

    对应的camera 视角各司其职!

  • 子弹连发以及开火音效
    子弹的逻辑就是按下鼠标的时候 就执行openFire方法

  void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            StopCoroutine("RealoadBullet");
            OpenFire();
        }
        if (Input.GetMouseButton(0))
        {
            fireTimer += Time.deltaTime;
            if (fireTimer>= intervalTime)
            {
                OpenFire();
            }
        }
        if (Input.GetMouseButtonUp(0))
        {
            //StartCoroutine(RealoadBullet());
            StartCoroutine("RealoadBullet");
        }
        if (Input.GetMouseButtonDown(1))
        {
            StopCoroutine("ToStart");
            StartCoroutine("ToCenter");
        }
        if (Input.GetMouseButtonUp(1))
        {
            StopCoroutine("ToCenter");
            StartCoroutine("ToStart");
        }


        WeaponRay();
    }
    public void OpenFire()
    {
        if (currentBulletCount > 0)
        {
            playerAni.Attack();

            fireTimer = 0;
            if (fireAudio)
            {
                fireAudio.Play();
            }
            GameObject newBullet = Instantiate(bulletPrefabs, bulletStartPoint.position, bulletStartPoint.rotation);
            newBullet.GetComponent().bulletType = BulletType.Player_Bullet;
           
            newBullet.GetComponent().velocity = newBullet.transform.forward * bulletSpeed;

            currentBulletCount -= 1;
            bulletCountSlider.value = currentBulletCount;

            StopCoroutine("WeaponBack");
            StartCoroutine("WeaponBack");
        }
        else
        {
         
        }
    }

在openFire方法中 会创建bulletPrefabs 并且朝向坐标为bulletStartPoint的位置。并且开火时会执行 开火音效

      playerAni.Attack(); 
  • AI 导航网格和导航代理

可以借助Unity 提供的NavMeshAgent去执行导航的逻辑

image.png

如上面截图的 在inspect 中勾选 需要参与导航的路径 然后在Enmey中挂载NavMeshAgent 控件 ,在脚本添加如下方法 就可以控制其移动逻辑。
image.png

  • AI 检测敌人自动开火
    可以借助上面的逻辑基础上,规划好 Enmey需要巡逻的路径。
    image.png

    WayPoinList 为Enmey需要规划的位置。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.AI;

public class Ryunm_Enemy : MonoBehaviour
{
    public float PH = 100;
    public Slider ph_Slider;
    public GameObject enemyExplossion;

    public NavMeshAgent _EnmeyAgent;
    public Transform wayPointParent;
    public Transform[] points;
    private int destPoint = 0;

    public float minDistance = 10;
    public float minIntervalTime = 1;
    public float maxIntervalTime = 3;
    public float minDamge = 5;
    public float maxDamge = 15;

    public Transform bulletStartPoint;
    public GameObject bulletPrefab;
    public float bulletSpeed = 200;
    public bool isFireOpen = false;

    public Ryunm_AnimatorController ani;

    public GameObject healthPrefab;

    // Start is called before the first frame update
    void Start()
    {
        ani = this.GetComponent();

        ph_Slider.maxValue = PH;
        ph_Slider.value = PH;

        _EnmeyAgent = this.GetComponent();
        ani.MoveSpeed = _EnmeyAgent.speed;
        if (_EnmeyAgent)
        {
            _EnmeyAgent.autoBraking = false;//

            points = wayPointParent.GetComponentsInChildren();
            destPoint = Random.Range(0, points.Length);
        }
        isFireOpen = false;
    }

    void Update()
    {
        if (_EnmeyAgent)
        {
            //_EnmeyAgent.destination = GameObject.FindObjectOfType().transform.position;
            if (!_EnmeyAgent.pathPending && _EnmeyAgent.remainingDistance < 0.5f)
                GotoNextPoint();
        }

        var playerTransform = Ryunm_GameManager._instance._player.transform;
        float distance = Vector3.Distance(this.transform.position, playerTransform.position);
        if (distance < minDistance)
        {
            _EnmeyAgent.SetDestination(playerTransform.position);
            ani.Alerted = true;

            if(isFireOpen == false)
            {
                StartCoroutine("Fire");
                isFireOpen = true;
            }
        }
        else
        {
            StopCoroutine("Fire");
            isFireOpen = false;

            ani.Alerted = false;
        }
    }
    void GotoNextPoint()
    {
        if (points.Length == 0)
            return;

        _EnmeyAgent.destination = points[destPoint].position;

 
        int temp = Random.Range(0, points.Length);
        if (destPoint == temp)
        {
            destPoint = (destPoint + 1) % points.Length;
        }
        else
        {
            destPoint = temp;
        }
    }
  

}

上面SetDestination 加入了

              if (_EnmeyAgent)
        {
            //_EnmeyAgent.destination = GameObject.FindObjectOfType().transform.position;
            if (!_EnmeyAgent.pathPending && _EnmeyAgent.remainingDistance < 0.5f)
                GotoNextPoint();
        }

 void GotoNextPoint()
    {
        if (points.Length == 0)
            return;

        _EnmeyAgent.destination = points[destPoint].position;

     
        int temp = Random.Range(0, points.Length);
        if (destPoint == temp)
        {
            destPoint = (destPoint + 1) % points.Length;
        }
        else
        {
            destPoint = temp;
        }
    }

在阀值区间外 野怪都是会可以任意移动到points 的随机巡逻点坐标下面。在阀值 内 则会去执行开火的逻辑

      if (distance < minDistance)
        {
            _EnmeyAgent.SetDestination(playerTransform.position);
            ani.Alerted = true;

            if(isFireOpen == false)
            {
                StartCoroutine("Fire");
                isFireOpen = true;
            }
        }
  • 人物血条UI 显示
    2D界面会用Canvas 绘制。血条Slider子弹数量Slider 以及当前当前游戏关卡中最大游戏数量EnmeyCountText 以及剩余数量EnmeyCurrentCountText
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public enum BulletType
{
    Player_Bullet=0,
    Enemy_Bullet=1,
}
public class Ryunm_Bullet : MonoBehaviour
{
    public BulletType bulletType = BulletType.Player_Bullet;
    public float damageValue = 10;
    public GameObject bulletExplossion;
    // Start is called before the first frame update
    void Start()
    {
        
    }

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

    private void OnCollisionEnter(Collision collision)
    {
        if(bulletType == BulletType.Player_Bullet)
        {
            if (collision.gameObject.CompareTag("Enemy"))
            {
                collision.gameObject.GetComponent().OnDamage(damageValue);
            }

            if (!collision.gameObject.CompareTag("Player"))
            {
                GameObject newExplossion = Instantiate(bulletExplossion, this.transform.position, bulletExplossion.transform.rotation);

                Destroy(this.gameObject);
            }
        }
        if (bulletType == BulletType.Enemy_Bullet)
        {
            if (collision.gameObject.CompareTag("Player"))
            {
                collision.gameObject.GetComponent().OnDamage(damageValue);
            }

            if (!collision.gameObject.CompareTag("Enemy"))
            {
                GameObject newExplossion = Instantiate(bulletExplossion, this.transform.position, bulletExplossion.transform.rotation);

                Destroy(this.gameObject);
            }
        }

    }
    private void OnTriggerEnter(Collider other)
    {

    }
}

在子弹Bullet 上挂载的脚本 会监听发出的子弹和野怪发生碰撞的逻辑
,在碰撞事件OnCollisionEnter中 会执行Ryunm_EnemyRyunm_PlayerHealth 对应脚本的OnDamage方法
下面进入Player下面的Ryunm_PlayerHealth 脚本 可以看到关联了PhSlider 从而关联OnDamage方法

  • 敌人随机生成


    image.png

GameManager 为一个空物体单例,他关联了 Canvas 的野怪数量

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

public class Ryunm_GameManager : MonoBehaviour
{
    public static Ryunm_GameManager _instance;
    public Slider bulletCountSlider;
    public Ryunm_FPS_PlayerController _player;

    public int maxEnemyCount=50;
    public int remainingEnemyCount = 0;
    public int currentMaxEnemyCount = 10;
    public int currentEnemyCount = 0;
    public int currentDestoryEnmeyCount = 0;

    public Text enemyCountTxt;
    public Text currentCountTxt;

    public Transform wayPointList;
    public Transform[] wayPoints;

    public GameObject enmeyPrefab;

    private void Awake()
    {
        _instance = this;
    }
    // Start is called before the first frame update
    void Start()
    {
        remainingEnemyCount = maxEnemyCount;
        currentEnemyCount = 0;

        wayPoints = wayPointList.GetComponentsInChildren();

        EenmyUIReset();

        InvokeRepeating("BornEnmey",0.5f,2);
    }

    // Update is called once per frame
    void Update()
    {
        
    }
    public void EenmyUIReset()
    {
        enemyCountTxt.text = currentDestoryEnmeyCount.ToString() + "/" + maxEnemyCount.ToString();
        currentCountTxt.text = currentEnemyCount.ToString();
    }

    private void BornEnmey()
    {
        if (currentEnemyCount < currentMaxEnemyCount&& remainingEnemyCount>0)
        {
            int index = Random.Range(0, wayPoints.Length);
            GameObject newEnemy = Instantiate(enmeyPrefab, wayPoints[index].position, enmeyPrefab.transform.rotation);
            newEnemy.GetComponent().wayPointParent = wayPointList;

            remainingEnemyCount -= 1;
            currentEnemyCount += 1;

            currentCountTxt.text = currentEnemyCount.ToString();
        }
    }
    public void EnmeyBeDestory()
    {
        currentEnemyCount -= 1;
        currentCountTxt.text = currentEnemyCount.ToString();

        currentDestoryEnmeyCount += 1;
        enemyCountTxt.text = currentDestoryEnmeyCount.ToString() + "/" + maxEnemyCount.ToString();
    }


}

脚本定义了 最大的野怪生成数量变量 以及剩余变量 然后InvokeRepeating("BornEnmey",0.5f,2);
去不断生成野怪 ,在满足条件下 一直去生成野怪

if (currentEnemyCount < currentMaxEnemyCount&& remainingEnemyCount>0)

然后对应去现实当前currentCountTxtenemyCountTxt的数量

至此 ,所有逻辑就都通达了!

你可能感兴趣的:(Unity 实战【Fps 枪击游戏-结合官方FPS案例 Microgame】)