Unity3D | FPS游戏_人物相关

这次报名参加了训练营,初次尝试Unity3D的游戏开发,很庆幸的是有老师很详细的指导,拖了一些时间,也总算完成了。依照惯例,继续来写总结,同时这次几乎上学到的都是新知识,在不熟悉的前提下,还比较复杂散碎,相互之间的关联还是比较密切的,之前实现都是比较简单,尽管有联系,每个知识单独拿出来记录也是可以的。所以说这次的知识有可能会来回跳转,最后呢,有什么不妥多多担待

###############

  • 思路
    • 有限状态机
    • 射线检测
    • 协程
  • 实现
    • 后坐力
    • 瞄准
    • 射击效果
  • 问题/解决方法/小技巧

简单的介绍一下游戏,和普遍的FPS游戏类似,玩家可以左键开火攻击敌人,每次射击造成的伤害固定,子弹小于当前弹匣子弹上限可以换弹,备用子弹数量有限,玩家生命值100。

在人物的模型以及控制上,直接使用老师的预制体,很感谢老师,着实省了很大是功夫。射击的方面,并不是采用的真实的子弹,而是利用射线检测,实例化弹坑,模拟出射击的效果;按住鼠标右键会有瞄准的功能;通过人物相机视角的变化,模拟出后坐力的效果;最主要的其实还是有限状态机的状态转换,这是这次最核心的技术。

思路

有限状态机

网上总结有限状态机的定义:

  • 有限个状态,且状态间存在转移关系
  • 某个时间点,有且仅有一个状态存在

实现有限状态机,需要清楚“状态-行为-转换”,根据此次训练实际内容,具体实现为一下三步

1.定义有限状态列表(状态)
首先枚举,列出所有的状态,之后定义状态函数,在这一步的状态中,可以改变控制动画播放的条件,这样之后状态转换,首先考虑的就是动画改变播放。

[SerializeField] private PlayerState playerState;
    //当某个状态切换时,进行该状态的初始化
    public PlayerState PlayerState
    {
        get => playerState;
        set
        {
            playerState = value;
            switch (playerState)
            {
                case PlayerState.Idle:
                    animator.SetBool("Shoot", false);
                    animator.SetBool("Reload", 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);
                    PlayAudio(1);
                    animator.SetBool("Shoot", false);
                    animator.SetBool("Reload", true);
                    break;
            }
        }
    }

2.定义Update中的状态函数(行为)
每次渲染新的一帧时,会执行一次Update(),这样可以在Update()中调用状态的刷新函数StateForUpdate(),这样会尽可能快的告诉机器状态转换的信号,从而执行状态的行为函数

void Update()
    {
        StateForUpdate();
    }
void StateForUpdate()
    {
        switch (playerState)
        {
            case PlayerState.Idle:
                if (Input.GetMouseButton(0) && canShoot)
                {
                    PlayerState = PlayerState.Shoot;
                    return;
                }
                if(Input.GetKeyDown(KeyCode.F)
                && curr_BulletNum==1
                && standby_BulletNum==300)
                {//彩蛋
                    HP=999;
                    curr_MaxBulletNum=300;
                    standby_MaxBulletNum=9999;
                    curr_BulletNum=curr_MaxBulletNum;
                    standby_BulletNum=standby_MaxBulletNum;
                    //更新HPUI
                    UI_MainPanel.instance.UpdateHp_Text(HP);
                    //更新子UI
                    UpdateBulletUI();
                }
                if (Input.GetKeyDown(KeyCode.R)
                && standby_BulletNum > 0
                && curr_BulletNum < curr_MaxBulletNum)
                {
                    PlayerState = PlayerState.Reload;
                }
                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;
                }
                break;
            case PlayerState.Reload:
                if (animator.GetCurrentAnimatorClipInfo(0)[0].clip.name == "Replace"
                && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 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;
        }
    }

3.状态转换(转换)
设定一些状态转换的条件,使多个状态之间可以形成循环,这个就需要根据实际情况考虑,可以在不同的地方实现状态的转换。根据此次训练项目,个人总结有以下两个需要注意的地方

  • 在状态机的定义函数中(第一步函数),不适合进行状态的转移,违背了状态机的意义
  • 特殊的终点状态,即Dead状态,进入到这个状态后,不会再发生任何的状态转换。主要体现在敌人方面,因为敌人采用的是对象池,这种终点状态会影响下次的使用。

状态机的状态,同样也控制当前动画的播放,有特殊的,不允许被打断的动画,在动画未完整播放完成之前,状态不可以转换,例如Reload状态。可以在当前状态下加入判断代码(因为在后续的通用性比较高,记录),以下为代码示例

case PlayerState.Reload:
                if (animator.GetCurrentAnimatorClipInfo(0)[0].clip.name == "Replace"
                && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1)
                {
                    //填充子弹
                }
                break;

在这里主要解释两个条件:
①动画状态机当前动画片段信息的名称
②动画状态机当前动画状态信息的序列化时间,标准的数值[0,1],0表示刚开始播放或者未播放,1表示播放完毕,如果播放多遍,该值会持续上涨
两个条件同时存在,目的是精确的控制希望的动画进行完整的播放
如果只有后者条件,以射击为例子,射击的动画时间短,有可能会播放多次,但是从Shoot状态切换到Relaod状态,并不会立即播放换弹动画。这样一来,虽然满足了播放次数>=1的条件,但是,换弹动画并没有播放

射线检测

射线的有关应用,在射击状态的行为函数中,在这里解释一下射线的产生和应用

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        if (Physics.Raycast(ray, out RaycastHit hitInfo, 1500f))
        {
        //射击效果
        }

1.camera.ScreenPointToRay()
返回从摄像机通过屏幕点光线
2.Input.mousePosition
屏幕点:当前鼠标在像素坐标中的位置。屏幕空间以像素定义,屏幕左下角为(0,0),右上角是(pixelWidth-1,pixelHeight-1)
3.Physics.Raycast(ray, out RaycastHit hitInfo, 1500f)
投射光线,射出最大距离为1500,获取光线对抗碰撞体的信息hitinfo。返回值为bool类型
—————————————————————————————————
out RaycastHit hitinfo从光线投射中获取的结构信息,hitinfo,之后进入判断,其游戏项目的标签是否为敌人标签“Zombie”,如果为true,
①示例化弹坑预制体,
②设置弹坑朝向为摄像机,
③将预制体弹坑作为子项目添加到hitinfo项目下,
④添加僵尸逻辑(对僵尸造成伤害);
否则的话,再进行判断,(因为光线投射的原点,在人物的碰撞体内部),hitinfo的碰撞体所属游戏项目不是本身,这样就排除了上述特殊情况,再重复步骤,
①示例化弹坑预制体,
②设置弹坑朝向为摄像机,
③将预制体弹坑作为子项目添加到hitinfo项目下,实现对场景碰撞体造成弹坑的情况

协程

因为我也是在学习过程中第一次接触协程,查阅了很多文章之后,才略懂一二,这边建议直接去看原文作者,讲解的非常到位,其中的Unity脚本生命周期官方文档图片中,除了给出了协程在脚本中的执行顺序,也涵盖了全部的函数执行顺序,个人实际理解使用,非常适合找逻辑bug。

版权声明:本文为CSDN博主「做一只会飞的猪」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/beihuanlihe130/article/details/76098844.

版权声明:本文为CSDN博主「侯老夫子」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接: https://blog.csdn.net/weixin_43872954/article/details/86553991.

实现

后坐力

主要通过摄像机视角的随机移动,以及回归,来模拟后坐力的实现,细节优化的方面,可以调整准星的尺寸,并且使用Time.deltime添加一个过渡的过程,使其更加平滑自然。因为使用的是官方的第一人称模型素材,自带有FirstPersonCharacter脚本,在其中找到控制视角旋转的函数,使每次射击的同时,开启一个后坐力的协程,在视角旋转的x轴和y轴上,随机添加一个改变的值,在协程中的yield return可以有两中使用方法,①yield return new WaitForSeconds():通过一段指定的时间延迟之后继续执行,在所有Update函数完成调用的那一帧之后。这样的话,因为有帧率变化的情况存在,每一段指定时间内的帧率可能有所不同,摄像机的抖动也会更加变化,随机
②yield return 6;(任意数字)下六帧后在执行后续代码。这样每次射击的后坐力都很稳定。

下面是FirstPersonCharacter的挂载脚本中,有关视角旋转的函数

//FirstPersonController.cs
private MouseLook m_MouseLook;
//补充逻辑
        public float yRotOffset { get { return m_MouseLook.yRotOffset; }set { m_MouseLook.yRotOffset = value; } }
        public float xRotOffset { get { return m_MouseLook.xRotOffset; }set { m_MouseLook.xRotOffset = value; } }
//

m_MouseLook.LookRotation (transform, m_Camera.transform);


//MouseLook.cs
	float yRot = CrossPlatformInputManager.GetAxis("Mouse X") * XSensitivity;
	float xRot = CrossPlatformInputManager.GetAxis("Mouse Y") * YSensitivity;
//补充逻辑
	yRot += yRotOffset;
	xRot += xRotOffset;
//

瞄准

通过控制两个部分,实现瞄准的功能:

  • 移动武器模型的位置
  • 调整摄像机的Field of View

实现的过程为:在Idle状态,按下鼠标右键保持,开启打开瞄准协程,松开鼠标右键,开启关闭瞄准协程,在这边需要注意的一点:玩家可能多次连续按下右键,会导致瞄准协程重复多次开启,多个瞄准协程同时存在的情况。 (在敌人相关的脚本中也会遇到相同的问题)为此需要在开启协程之前,先执行关闭协程。细节优化方面,瞄准过程中添加平滑过渡,以模型的localPosition(武器模型是第一人称相机下的子项目,Position会改变其世界坐标)为例子,每次变化都以Time.delTime为单位,最后会产生微小的偏差,不会恰好到达预期位置,可以执行一次标准化代码(提前测量目标位置的localPositin,直接赋值)

//开始瞄准
    void StartAim()
    {//玩家有可能连续按下右键
        StopCoroutine("DoStartAim");
        StartCoroutine("DoStartAim");
    }
    void StopAim()
    {
        StopCoroutine("DoStopAim");
        StartCoroutine("DoStopAim");
    }
    //瞄准
    IEnumerator DoStartAim()
    {
        Vector3 pos = weapon.transform.localPosition;
        while (pos.x > 0)
        {
            pos.x -= Time.deltaTime * 3;
            weapon.transform.localPosition = pos;
            yield return null;
        }//循环结束不会刚好归零,会存在很小的偏差
        //坐标过度完成立马执行归零
        pos.x = 0;
        weapon.transform.localPosition = pos;
        for (int i = 0; i < 5; i++)
        {
            yield return null;
            foreach (Camera camera in cameras)
            {
                camera.fieldOfView -= 5; ;
            }
        }
    }
//停止瞄准
    IEnumerator DoStopAim()
    {
        Vector3 pos = weapon.transform.localPosition;
        while (pos.x < 0.386f)
        {
            pos.x += Time.deltaTime * 3;
            weapon.transform.localPosition = pos;
            yield return null;
        }//循环结束不会刚好归零,会存在很小的偏差
        //坐标过度完成立马执行归零
        pos.x = 0.386f;
        weapon.transform.localPosition = pos;
        for (int i = 0; i < 5; i++)
        {
            yield return null;
            foreach (Camera camera in cameras)
            {
                camera.fieldOfView += 5;
            }
        }
    }

射击效果

1.射击火花效果
将预制体的枪口火花粒子效果,添加到枪口模型下为子项目,并且在射击表现的代码中,SetActive(true);在每次射击之后,会切换到Idle状态,这样再SetActive(false);通过状态转换条件判断,从而形成一个状态的循环
**粒子效果中的Stop Action选择Destory

2.屏幕准星
创建Canvas,创建Image作为子项目,Anchor presets按住Alt将其设置为center-middle,拖入准星的图片。在每次开枪,启动后坐力的协程中,可以通过图片尺寸逐渐加减(例如:scale.x+=Time.deltaTime;),来模拟准星缩放
**在Canvas中,Canvas->Reference Resolution,设定1920X1080可以锁定尺寸

3.玩家换弹以及弹匣
在上面Reload的状态中的判断条件内,实现填充子弹的过程。在各处进入Reload状态之前也要进行判断:①当前的后备子弹数大于0②当前弹匣子弹数小于弹匣子弹最大值(满弹时不进行换弹)

if (animator.GetCurrentAnimatorClipInfo(0)[0].clip.name == "Replace"
                && animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 1)
                {
                    //填充子弹
                    int want = curr_MaxBulletNum - curr_BulletNum;
                    if ((standby_BulletNum - want) < 0)
                    {
                        want = standby_BulletNum;
                    }
                    standby_BulletNum -= want;
                    curr_BulletNum += want;
                    //更新子弹UI
                    UpdateBulletUI();
                    PlayerState = PlayerState.Idle;
                }

问题/解决方法/小技巧

1.地形材质丢失
不明白什么原因,在导入素材资源包的之后,拖入环境预制体,地形的一大片地方是白色的,在Scenes文件下也没有NewLayer。之后的解决方法是,在环境预制体下,找到Terrain,并且选中Terrain组件下的笔刷按钮(Paint Terrain),下拉选择Paint Texture,
这个时候在Terrain Layers下有两种情况:
①有许多的空白层;将所有都删除之后,点击Edit Terrain Layer ->Create Layer,选择想要的材质
②没有Layer;直接点击Edit Terrain Layer,同上步骤

2.武器穿模问题
这里用到了一个取巧的方法,就是修改图层。首先说明,在FirstPersonCharacter下已经添加相机,同时新建子项目Camera作为专门渲染武器的相机。
Unity3D | FPS游戏_人物相关_第1张图片

  • 首先为武器新建添加图层为Weapon,在Camera中选择Clear Flags -> Depth only(默认是Sky Box,表示屏幕的未绘制部分是空的,则会显示为天空盒,修改之后为只显示深度)
  • 在Culling Mask(剔除遮罩)中选择Weapon层,同时,为了使其显示在其他相机视角的顶部,在下面的Depth设置为1
  • 回到FirstPersonCharacter的Camera组件下,在Culling Mask 中取消勾选Weapon(默认是Everything),这样就不会出现两把武器的问题,同时将其Depth改为0
    这样的话,即使在游戏世界中,武器已经发生了穿模的情况,但在玩家视角中,由于武器处在最顶层的缘故,武器模型仍然会完整的显示出来。

3.帧数波动严重

Application.targetFrameRate = 60;//Start()

在测试过程中,使帧率尽可能贴近在60帧左右
4.射线可视化

Debug.DrawRay(GameObject.Find("FirstPersonCharacter").transform.position,-Input.mousePosition,Color.red);

Debug.DrawRay(Vector3 射线起点,Vector3 射线方向,Color color)
根据两点一线的原理,将射线可视化为设置颜色

你可能感兴趣的:(Unity3D,unity,3d,游戏引擎,c#)