3Dunity游戏项目实战——第一人称射击游戏

代码及资料

一、游戏策划

1.1 游戏介绍

在游戏场景中,会有若干个敌人出生点,定时生成一些敌人,敌人会主动寻找并攻击主角。游戏目的就是生存下去,消灭僵尸敌人,获得更高的积分。

1.2 UI界面

在游戏界面,包括主角的生命值,弹药数量,得分以及瞄准星。
游戏失败后,出现一个重新开始的按钮。

1.3 主角

第一人称视角无法看到主角自己,在屏幕上只能看到一致端起来的M16枪械,键盘W、S、A、D键控住主角上下左右移动,鼠标移动来旋转视角。

1.4 敌人

敌人是一个护士模样的僵尸,具有自动寻路的功能,躲避障碍物,并攻击主角。

二、游戏场景

  1. 打开工程,导入模型、动画、音效等资源。
  2. 打开场景Scene,该场景中使用LightmapLight Probe表现静态和动态模型的光影效果。
  3. 选择场景模型,为其添加Mesh Collider多边形碰撞组件。在开发中,模型通常复杂,会准备两组模型,一组质量较高用于显示,另一组用于物理碰撞。

三、主角

主角是看不见的,只能看到主角手里的枪械,尽管如此还是要为主角创建碰撞体,并控制其移动。

3.1 角色控制器

  1. 创建一个空游戏体,Tag设为player
  2. 为主角添加Character Controller角色控制器,这样主角行走时就不会穿模
  3. 为主角添加Rigidbody组件,取消重力模拟,选中Is Kinermaic使其不受物理推演影响。
  4. 创建Player脚本
//Player.cs  完整代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{

    //组件
    public Transform m_transform;
    CharacterController m_ch;

    //枪口transform
    Transform m_muzzlepoint;
    //射击时,射线能射到的碰撞层
    public LayerMask m_layer;
    //射中目标后的粒子效果
    public Transform m_fx;
    //射击音效
    public AudioSource m_audio;
    public AudioClip m_shootClip; //声音
    //射击间隔时间
    float m_shootTimer = 0;

    //摄像机Transform
    Transform m_camTransform;
    Vector3 m_camRot; //相机旋转角度
    float m_camHeight = 1.4f; //相机高度

    //角色移动速度
    float m_movSpeed = 3.0f;

    //重力
    float m_grivity = 2.0f;

    //生命值
    public int m_life = 5;

    // Start is called before the first frame update
    void Start()
    {
        //获取组件
        m_transform = this.transform;
        m_ch = this.GetComponent<CharacterController>();
        m_audio = this.GetComponent<AudioSource>();


        //获取摄像机
        m_camTransform = Camera.main.transform;

        //获取枪口
        m_muzzlepoint = m_camTransform.transform.Find("M16/weapon/muzzlepoint").transform;

        //设置相机初始位置
        Vector3 pos = m_transform.position;
        pos.y += m_camHeight;
        m_camTransform.position = pos;

        //设置摄像机的初始旋转角度
        /*
         * rotation是Quaternion类型,可以随时改变旋转角度
         * eulerAngles是Vector3类型,不能随时变化的,是一个定值
         */
        m_camTransform.rotation = m_transform.rotation; 
        m_camRot = m_camTransform.eulerAngles;

        //锁定鼠标
        Cursor.lockState = CursorLockMode.Locked;

    }


    // Update is called once per frame
    void Update()
    {
        //如果生命值为0,什么也不做
        if (m_life <= 0) return;
        

        //更新射击间隔时间
        m_shootTimer -= Time.deltaTime;
        //鼠标左键射击
        if(Input.GetMouseButton(0) && m_shootTimer <= 0)
        {
            m_shootTimer = 0.1f;
            //射击音效
            m_audio.PlayOneShot(m_shootClip);
            //减少弹药,更新UI
            GameManager.Instance.SetAmmo(1);

            //RaycastHit用来保存射线探测结果
            RaycastHit info;

            //从muzzlepoint的位置,向摄像机面向的正方向射出一根射线
            //射线只能与m_layer指定的层碰撞,Vector3.forward指(0,0,1)即z方向的单位向量
            bool hit = Physics.Raycast(m_muzzlepoint.position, 
                m_camTransform.TransformDirection(Vector3.forward), out info, 100, m_layer);

            //如果射中了Tag为enemy的物体
            if (info.transform.tag.CompareTo("enemy") == 0)
            {
                Enemy enemy = info.transform.GetComponent<Enemy>();
                //敌人减少生命
                enemy.OnDamage(1);
            }
            //在射中的地方释放一个粒子效果
            Instantiate(m_fx, info.point, info.transform.rotation);
        }
        Control();

    }


    
    //控制角色移动以及视角变化
    void Control()
    {
        float xm = 0, ym = 0, zm = 0;

        //获取鼠标移动距离
        float rh = Input.GetAxis("Mouse X"); //右为正
        float rv = Input.GetAxis("Mouse Y"); // 上为正


        //旋转摄像机,z控制着倾斜程度,使保持不变
        m_camRot.x -= rv;   //上下旋转镜头是x控制,上为减
        m_camRot.y += rh;   //左右旋转镜头是y控制,左为减
        m_camTransform.eulerAngles = m_camRot;

        //使主角面向方向与摄像机一致,即只有y轴跟着镜头转动,其他两个方向轴不动
        Vector3 camrot = m_camTransform.eulerAngles;
        camrot.x = 0;
        camrot.z = 0;
        m_transform.eulerAngles = camrot;

        //重力运动
        ym -= m_grivity * Time.deltaTime;

        //上下左右运动
        if (Input.GetKey(KeyCode.W))
        {
            zm += m_movSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.S))
        {
            zm -= m_movSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.A))
        {
            xm -= m_movSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.D))
        {
            xm += m_movSpeed * Time.deltaTime;
        }

        //移动,会根据角色的朝向,相对前后左右移动
        m_ch.Move(m_transform.TransformDirection(new Vector3(xm, ym, zm)));

        //使摄像机位置与主角一致
        Vector3 pos = m_transform.position;
        pos.y += m_camHeight;
        m_camTransform.position = pos;
    }

    public void OnDamage(int damage)
    {
        m_life -= damage;

        //更新UI
        GameManager.Instance.setLife(m_life);

        //如果生命值为0,取消鼠标锁定
        if(m_life <= 0)
        {
            Cursor.lockState = CursorLockMode.None;
        }
    }
    private void OnDrawGizmos()
    {
        Gizmos.DrawIcon(this.transform.position, "Spawn.tif");
    }
}

3.2 摄像机

Player.cs脚本中添加部分代码,使摄像机伴随主角移动
具体实现部分代码:

void Control()
    {
        float xm = 0, ym = 0, zm = 0;

        //获取鼠标移动距离
        float rh = Input.GetAxis("Mouse X"); //右为正
        float rv = Input.GetAxis("Mouse Y"); // 上为正


        //旋转摄像机,z控制着倾斜程度,使保持不变
        m_camRot.x -= rv;   //上下旋转镜头是x控制,上为减
        m_camRot.y += rh;   //左右旋转镜头是y控制,左为减
        m_camTransform.eulerAngles = m_camRot;

        //使主角面向方向与摄像机一致,即只有y轴跟着镜头转动,其他两个方向轴不动
        Vector3 camrot = m_camTransform.eulerAngles;
        camrot.x = 0;
        camrot.z = 0;
        m_transform.eulerAngles = camrot;

        //重力运动
        ym -= m_grivity * Time.deltaTime;

        //上下左右运动
        if (Input.GetKey(KeyCode.W))
        {
            zm += m_movSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.S))
        {
            zm -= m_movSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.A))
        {
            xm -= m_movSpeed * Time.deltaTime;
        }
        if (Input.GetKey(KeyCode.D))
        {
            xm += m_movSpeed * Time.deltaTime;
        }

        //移动,会根据角色的朝向,相对前后左右移动
        m_ch.Move(m_transform.TransformDirection(new Vector3(xm, ym, zm)));

        //使摄像机位置与主角一致
        Vector3 pos = m_transform.position;
        pos.y += m_camHeight;
        m_camTransform.position = pos;
    }

3.3 武器

把武器绑定到摄像机上,使其能够伴随主角移动

  1. 摄像机的位置和旋转角度设为0
  2. 摄像机的Clipping Plane/Near设为0.1,使其可以看到更近处的物体
  3. 将武器Prefab托入场景中
  4. 将武器Prefeb位置和旋转都设为0,置于摄像机的下层子物体,调整其在屏幕中的位置直到满意。

四、敌人

4.1 寻路

Unity提供了非常实用的寻路功能,其寻路系统分为两个部分,一部分是对场景进行设置,使其满足寻路算法的需求,另一部分是设置寻路者。

  1. 在Inspector窗口小三角显示下拉菜单,确定Navigation Static被选中。
  2. 选择【Window】-【AI】-【Navigation】,这个窗口主要定义地形对寻路的影响。设置好选项后,选择Bake对地形进行计算。
  3. 对敌人的对象拖入场景,选择【Component】-【Nav Mesh Agent】将寻路组件指定给敌人。
  4. 创建敌人的脚本。
//Enemy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Enemy : MonoBehaviour
{

    Transform m_transform;
    Player m_player;
    UnityEngine.AI.NavMeshAgent m_agent;  //寻路组件
    float m_movSpeed = 0.5f; //敌人移动速度
    float m_rotSpeed = 120;  //角色旋转速度
    float m_timer = 2; //计时器
    int m_life = 5; //生命值

    //出生点
    protected EnemySpawn m_spawn;

    //初始化
    public void Init(EnemySpawn spawn)
    {
        m_spawn = spawn;
        m_spawn.m_enemyCount++;
    }

    Animator m_ani; //动画组件

    // Start is called before the first frame update
    void Start()
    {
        //获取组件
        m_transform = this.transform;
        m_ani = GetComponent<Animator>(); //获取动画组件

        //获取主角类实例
        m_player = GameObject.FindGameObjectWithTag("player").GetComponent<Player>();
        //获取寻路组件
        m_agent = GetComponent<UnityEngine.AI.NavMeshAgent>();
        //设置寻路目标
        m_agent.SetDestination(m_player.m_transform.position);
    }

    // Update is called once per frame
    void Update()
    {
        // 如果主角生命为0,什么也不做
        if (m_player.m_life <= 0)
            return;

        // 获取当前动画状态
        AnimatorStateInfo stateInfo = m_ani.GetCurrentAnimatorStateInfo(0);

        // 如果处于待机状态
        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.idle") && !m_ani.IsInTransition(0))
        {
            m_ani.SetBool("idle", false);

            // 待机一定时间
            m_timer -= Time.deltaTime;
            if (m_timer > 0)
                return;

            // 如果距离主角小于1.5米,进入攻击动画状态
            if (Vector3.Distance(m_transform.position, m_player.m_transform.position) < 1.5f)
            {
                m_ani.SetBool("attack", true);
            }
            else
            {
                // 重置定时器
                m_timer = 1;

                // 设置寻路目标点
                m_agent.SetDestination(m_player.m_transform.position);

                // 进入跑步动画状态
                m_ani.SetBool("run", true);
            }
        }

        // 如果处于跑步状态
        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.run") && !m_ani.IsInTransition(0))
        {
            m_ani.SetBool("run", false);


            // 每隔1秒重新定位主角的位置
            m_timer -= Time.deltaTime;
            if (m_timer < 0)
            {
                m_agent.SetDestination(m_player.m_transform.position);

                m_timer = 1;
            }

            // 追向主角
            MoveTo();

            // 如果距离主角小于1.5米,向主角攻击
            if (Vector3.Distance(m_transform.position, m_player.m_transform.position) <= 1.5f)
            {
                //停止寻路	
                m_agent.ResetPath();
                m_ani.SetBool("attack", true);
            }
        }

        // 如果处于攻击状态
        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.attack") && !m_ani.IsInTransition(0))
        {
            // 面向主角
            RotateTo();

            m_ani.SetBool("attack", false);

            // 如果攻击动画播完,重新进入待机状态
            if (stateInfo.normalizedTime >= 1.0f)
            {
                m_ani.SetBool("idle", true);

                // 重置计时器
                m_timer = 2;

                //更新主角生命
                m_player.OnDamage(1);

            }
        }

        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.death") && !m_ani.IsInTransition(0))
        {
            //当播放完死亡动画
            if (stateInfo.normalizedTime >= 1.0f)
            {
                OnDeath();
            }
        }       
    }

    //寻路移动
    void MoveTo()
    {
        float speed = m_movSpeed * Time.deltaTime;
        //TransformDirection从自身坐标到世界坐标变换方向
        m_agent.Move(m_transform.TransformDirection(new Vector3(0, 0, speed)));
    }

    //始终转向主角位置
    void RotateTo()
    {
        //当前角度
        Vector3 oldangle = m_transform.eulerAngles;

        //获得面向主角的角度
        m_transform.LookAt(m_player.m_transform);
        float target = m_transform.eulerAngles.y;

        //转向主角
        float speed = m_rotSpeed * Time.deltaTime;
        float angle = Mathf.MoveTowardsAngle(oldangle.y, target, speed); //计算出当前角度转向目标角度的旋转角度
        m_transform.eulerAngles = new Vector3(0, angle, 0);

    }

    public void OnDamage(int damage)
    {
        // 伤害
        m_life -= damage;
        if (m_life <= 0)
        {
            m_ani.SetBool("death", true);
        }
    }

    //当被销毁时
    public void OnDeath()
    {
        //更新敌人数量
        m_spawn.m_enemyCount--;
        //加分
        GameManager.Instance.SetScore(100);
        //销毁
        Destroy(this.gameObject);
    }
}

4.2 设置动画

为敌人的不同动作设置不同的动画

  1. 在场景中选中敌人,默认它有一个Animator组件,在Controller中设置好一个Animator Controller,取消选择Apply Root Motion选项,强迫使其受脚本控制。
    在这里插入图片描述

  2. 【window】-【Animation】-【Animator】打开Animator窗口,添加动画过渡。点击状态方格,为其添加动画;点击箭头为其添加过渡条件。
    在这里插入图片描述

  3. 点击【parameter】,为几个状态添加参数。注意:不要把旁边的选项勾上
    在这里插入图片描述

  4. 设置动画过渡条件
    在这里插入图片描述

4.3 行为

修改敌人的脚本,在不同状态时使敌人的行为也发生改变。

  1. 添加动画组件等属性
  2. 添加RotateTo函数,使敌人始终转到面向主角的角度
//始终转向主角位置
    void RotateTo()
    {
        //当前角度
        Vector3 oldangle = m_transform.eulerAngles;

        //获得面向主角的角度
        m_transform.LookAt(m_player.m_transform);
        float target = m_transform.eulerAngles.y;

        //转向主角
        float speed = m_rotSpeed * Time.deltaTime;
        float angle = Mathf.MoveTowardsAngle(oldangle.y, target, speed); //计算出当前角度转向目标角度的旋转角度
        m_transform.eulerAngles = new Vector3(0, angle, 0);

    }

  1. 添加条件进行动画过渡
// 获取当前动画状态
        AnimatorStateInfo stateInfo = m_ani.GetCurrentAnimatorStateInfo(0);

        // 如果处于待机状态
        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.idle") && !m_ani.IsInTransition(0))
        {
            m_ani.SetBool("idle", false);

            // 待机一定时间
            m_timer -= Time.deltaTime;
            if (m_timer > 0)
                return;

            // 如果距离主角小于1.5米,进入攻击动画状态
            if (Vector3.Distance(m_transform.position, m_player.m_transform.position) < 1.5f)
            {
                m_ani.SetBool("attack", true);
            }
            else
            {
                // 重置定时器
                m_timer = 1;

                // 设置寻路目标点
                m_agent.SetDestination(m_player.m_transform.position);

                // 进入跑步动画状态
                m_ani.SetBool("run", true);
            }
        }

        // 如果处于跑步状态
        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.run") && !m_ani.IsInTransition(0))
        {
            m_ani.SetBool("run", false);


            // 每隔1秒重新定位主角的位置
            m_timer -= Time.deltaTime;
            if (m_timer < 0)
            {
                m_agent.SetDestination(m_player.m_transform.position);

                m_timer = 1;
            }

            // 追向主角
            MoveTo();

            // 如果距离主角小于1.5米,向主角攻击
            if (Vector3.Distance(m_transform.position, m_player.m_transform.position) <= 1.5f)
            {
                //停止寻路	
                m_agent.ResetPath();
                m_ani.SetBool("attack", true);
            }
        }

        // 如果处于攻击状态
        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.attack") && !m_ani.IsInTransition(0))
        {
            // 面向主角
            RotateTo();

            m_ani.SetBool("attack", false);

            // 如果攻击动画播完,重新进入待机状态
            if (stateInfo.normalizedTime >= 1.0f)
            {
                m_ani.SetBool("idle", true);

                // 重置计时器
                m_timer = 2;

                //更新主角生命
                m_player.OnDamage(1);

            }
        }

        if (stateInfo.fullPathHash == Animator.StringToHash("Base Layer.death") && !m_ani.IsInTransition(0))
        {
            //当播放完死亡动画
            if (stateInfo.normalizedTime >= 1.0f)
            {
                OnDeath();
            }
        }

五、UI界面

使用2D贴图的方式展示UI效果,比使用OnGUI更节省资源。

  1. 创建一个GameManager空物体,为其添加【Component】-【UI】-【Image】组件,在组件中插入图片,也可以使用【Component】-【UI】-【Text】添加文字组件
  2. 调整UI图片和文字的位置。小技巧:点击位置方块,可以使用【shft】+【Alt】确定其相对位置
    在这里插入图片描述
  3. 创建GameManager的脚本文件
//GameManager.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GameManager : MonoBehaviour
{

    public static GameManager Instance = null;

    //游戏得分
    int m_score = 0;

    //游戏最高分
    static int m_hiscore = 0;

    //弹药数量
    int m_ammo = 100;

    //游戏主角
    Player m_player;

    //UI文字
    UnityEngine.UI.Text txt_ammo;
    UnityEngine.UI.Text txt_hiscore;
    UnityEngine.UI.Text txt_life;
    UnityEngine.UI.Text txt_score;

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

        //获取主角
        m_player = GameObject.FindGameObjectWithTag("player").GetComponent<Player>();

        //获得设置的UI文字
        txt_ammo = this.transform.Find("Canvas/txt_ammo").GetComponent<UnityEngine.UI.Text>();
        txt_hiscore = this.transform.Find("Canvas/txt_hiscore").GetComponent<UnityEngine.UI.Text>();
        txt_life = this.transform.Find("Canvas/txt_life").GetComponent<UnityEngine.UI.Text>();
        txt_score = this.transform.Find("Canvas/txt_score").GetComponent<UnityEngine.UI.Text>();
    }

    //更新分数
    public void SetScore(int score)
    {
        m_score += score;

        if(m_score > m_hiscore)
        {
            m_hiscore = m_score;
        }

        txt_score.text = "Score " + m_score;
        txt_hiscore.text = "High Score " + m_hiscore;
    }

    //更新弹药
    public void SetAmmo(int ammo)
    {
        m_ammo -= ammo;

        //如果弹药为负数,重新填弹
        if(m_ammo <= 0)
        {
            m_ammo = 100 - m_ammo;
        }

        txt_ammo.text = m_ammo.ToString() + "/100";
    }

    //更新生命
    public void setLife(int life)
    {
        txt_life.text = life.ToString();
    }

    private void OnGUI()
    {
        if(m_player.m_life <= 0)
        {
            //显示游戏结束
            GUI.skin.label.alignment = TextAnchor.MiddleCenter;
            GUI.skin.label.fontSize = 40;
            GUI.Label(new Rect(0, 0, Screen.width, Screen.height), "Game Over");

            //显示游戏重新开始
            GUI.skin.label.fontSize = 30;
            if (GUI.Button(new Rect(Screen.width * 0.5f - 150, Screen.height * 0.75f, 300, 40), "Try Again"))
            {
                //Application.LoadLevel(Application.loadedLevelName);
                UnityEngine.SceneManagement.SceneManager.LoadScene("Demo");

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

六、交互

6.1 主角的射击

  1. 打开Player.cs,添加OnDamage函数,用于减少主角生命,并更新UI
  2. 武器模型的枪口射出一根射线,实现射击功能,射线只能与m_layer指定的层碰撞,具体看前文Player.cs完整代码
  3. 创建两个碰撞层,enemy和level,分别指定给敌人和场景模型,再创建一个enemy的Tag指定给敌人,为脚本的m_lay指定碰撞层
    在这里插入图片描述
  4. 在脚本中指定音乐源以及子弹射击特效
  5. 创建一个AutoDestroy.cs将其指定给射击特效,让其一秒后销毁
//AutoDestroy.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AutoDestroy : MonoBehaviour
{

    public float m_timer = 1.0f;
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        m_timer -= Time.deltaTime;
        if (m_timer <= 0)
            Destroy(this.gameObject);
    }
}

6.2 敌人的进攻和死亡

  1. 选择敌人,添加 Capsule Collider 组件,添加刚体组件并设置好
    在这里插入图片描述
  2. 在enemy.cs脚本中添加OnDamage函数更新敌人伤害,生命为0时,进入死亡状态,播放相应动画,更新UI分数,并销毁

七、出生地

为了能控制敌人数量,每个出生点都需要清楚生成了多少敌人,达到最大值时停止产生敌人,敌人被消灭时提示产生新的敌人。

  1. 创建脚本EnemySpawn.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class EnemySpawn : MonoBehaviour
{

    public Transform m_enemy; //放入敌人预设
    public int m_enemyCount = 0; //敌人的数量
    public int m_maxEnemy = 3;//敌人的最大生成数量
    public float m_timer = 0; //敌人生成间隔时间
    protected Transform m_transform;
    // Start is called before the first frame update
    void Start()
    {
        m_transform = this.transform;
    }

    // Update is called once per frame
    void Update()
    {
        //如果生成敌人数量达到最大值,停止生成敌人
        if (m_enemyCount >= m_maxEnemy)
            return;

        //每隔一段时间,生成敌人
        m_timer -= Time.deltaTime;
        if(m_timer <= 0)
        {
            m_timer = 5+Random.value * 10.0f;
            Transform obj = (Transform)Instantiate(m_enemy, m_transform.position, Quaternion.identity);
            //获得敌人脚本
            Enemy enemy = obj.GetComponent<Enemy>();
            enemy.Init(this);
        }            
    }

    private void OnDrawGizmos()
    {
        Gizmos.DrawIcon(this.transform.position, "item.png",true);
    }
}

  1. 创建一个空游戏体,指定脚本EnemySpawn.cs,并关联敌人的Prefab

八、小地图

  1. 创建一个新的【Camera】,放在地图的正上方,对其进行设置
    在这里插入图片描述

  2. 在敌人和主角的子项中分别创建不同颜色的球体对象,命名为dummy,作为主角和敌人的“替代品”,注意要把球体的【Sphere Colider】去掉

  3. 创建一个新的Layer,命名为dummy,并设置球体对象的Layer为dummy

  4. 选择主摄像机,使其不显示dummy层

  5. 选择新的摄像机,使其只显示dummy层和场景level层

  6. 显示小地图:
    6.1首先创建一个新的【Render Texture】,命名为camera;
    6.2在GameManager中添加一个【Image】,并用camera为其赋值,调整大小;
    在这里插入图片描述
    6.3为新的相机的【Target Texture】赋值
    在这里插入图片描述
    代码及资料

2022.12.5 更新

有网友反映尝试将代码跑起来时,会出现以下问题,问题截图来源于网友:
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
错误提示:nullreference exception。object reference not set to an instance of an object

鉴于有多个网友私信我这个问题,我将之前回复的内容统一贴一下(由于项目有点久,很多细节我自己也记不清懒得回头一点点分析,只把之前帮网友解决问题的回复贴一下):

大致说一下各个script.cs的执行流程,首先是Enemy spawn空对象(不可见)一开始就存在场景中,它执行update函数,通过Instantiate函数产生enemy对象(可见),然后执行enemy对象的init函数初始化,并将当前enemySpawn对象当做参数传入,这样enemy对象中的protect属性m_spawn就有值所以不会有空指针异常。所以反过来推测,产生空指针异常是这个m_spawn为空导致的,所以是不是你自己手动生成了一个Enemy对象,然后开枪打死他所以才会报错。正确的流程应该是敌人是通过spawn产生,这样打死他才能正确的把对应的spawn对象的enemyCount值-1

简单总结以下:教程中是一点点由浅入深,开始教的大家如何在场景中手动创建一个prefab敌人,后来深入后是自动创建prefab敌人,并且有了后续消灭敌人的一系列流程。而一开始手动创建的prefab敌人是没有后续的,所以教程讲解到自动生成敌人时,要把之前教程中场景中手动创建的敌人prefab删除,就解决了这个问题。之前写教程的时候是完成之后回过头来写的,所以这一步貌似忽略了。

另外,本人也是看《unity 3D手机游戏开发 金玺曾》学习一边做的,书中很多细节和新版的unity有些不一样,大家可以对照着看。书和书附赠的代码链接我也一并放出来:

链接:https://pan.baidu.com/s/1c6FviWaYhnWdjjSWQOYtUg?pwd=1234
提取码:1234

  • 20
    点赞
  • 212
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值