这次报名参加了训练营,初次尝试Unity3D的游戏开发,很庆幸的是有老师很详细的指导,拖了一些时间,也总算完成了。依照惯例,继续来写总结,同时这次几乎上学到的都是新知识,在不熟悉的前提下,还比较复杂散碎,相互之间的关联还是比较密切的,之前实现都是比较简单,尽管有联系,每个知识单独拿出来记录也是可以的。所以说这次的知识有可能会来回跳转,最后呢,有什么不妥多多担待
在人物的模型以及控制上,直接使用老师的预制体,很感谢老师,着实省了很大是功夫。射击的方面,并不是采用的真实的子弹,而是利用射线检测,实例化弹坑,模拟出射击的效果;按住鼠标右键会有瞄准的功能;通过人物相机视角的变化,模拟出后坐力的效果;最主要的其实还是有限状态机的状态转换,这是这次最核心的技术。
网上总结有限状态机的定义:
实现有限状态机,需要清楚“状态-行为-转换”,根据此次训练实际内容,具体实现为一下三步
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.状态转换(转换)
设定一些状态转换的条件,使多个状态之间可以形成循环,这个就需要根据实际情况考虑,可以在不同的地方实现状态的转换。根据此次训练项目,个人总结有以下两个需要注意的地方
状态机的状态,同样也控制当前动画的播放,有特殊的,不允许被打断的动画,在动画未完整播放完成之前,状态不可以转换,例如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;
//
通过控制两个部分,实现瞄准的功能:
实现的过程为:在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作为专门渲染武器的相机。
3.帧数波动严重
Application.targetFrameRate = 60;//Start()
在测试过程中,使帧率尽可能贴近在60帧左右
4.射线可视化
Debug.DrawRay(GameObject.Find("FirstPersonCharacter").transform.position,-Input.mousePosition,Color.red);
Debug.DrawRay(Vector3 射线起点,Vector3 射线方向,Color color)
根据两点一线的原理,将射线可视化为设置颜色