射击游戏模板研究笔记

怎么阅读模板源码

想直到效果对应哪一块代码,方法无非是加断点和加打印。运行到VS断点后Unity进程会完全卡住,无法查看很多数据,一般只有只想细抠代码运行时会用断点。

加打印,为了能和原本有的打印区分开,可以自己封装一个打印方法。后面也方便全部找到。

概述

这篇笔记记录研究其他射击游戏项目对一些问题的解决方案。主要有:

1.TPP如何改变人物仰角;

2.枪的绑定方式。枪绑定手还是手IK绑定枪;

3.相机和人物的旋转,谁带动谁;

4.如果兼容FPP和TPP,FPP和TPP的动作要求不同,是否用了两套动作;

5.使用的动画是否为humanoid;

6.上半身是否使用了AvatarMask覆盖,如何解决走路时上半身瞄准稳定的问题;

FPS Animation Framework

Welcome! | FPS Animation Framework Legacy (gitbook.io)

动作条件判断测试

先对它做射击游戏笔记里的那一套动作条件判断测试。

1.跑步时按换弹被忽略;

2.换弹时按跑步被忽略;

3.瞄准状态按换弹,并没有解除瞄准,而是相机在瞄准位置换弹,但很明显相机不是枪的子物体,而是一直看前方;瞄准状态按跑步,退出瞄准开始跑步;

13d29c7ce4bc494c841d94841e3b6aed.png27e70342a93547bf8b20137db1269601.png

4.换弹中按瞄准,和3一样会进入瞄准换弹状态;跑步中按瞄准,被忽略;

7.趴下中按换弹,双手播放了换弹动画,M4的换弹动画没问题,另外几把枪趴下的换弹动画枪会直接消失,换完弹再回来;趴下中按跑步,趴下后开始往前爬;趴下中按瞄准,瞄准好像可以和任何动作叠加,就是把相机往前一点;

f966cdba11bb4a32b24b1788aa0a1d92.png

8.爬行中按瞄准,相机往前移了,双手是介于爬行和瞄准之间的动作;我感觉这个系统应该是用了混合树之类的技术;

35f7151172b4482f830c225a3bcfa22f.png

另外发现

demo里一共5把枪,换到最后一把有可能会不指向中心,指向偏左的位置,换成下一把M4,也是偏左的,再换下一把AK就正常了。

09cabfc7a3474625932d9dbd04213de9.png282b32995ee14e7f9b43ace12fea39c0.png

趴下时不能换枪。

趴下时后面几把枪的换弹动画好像没做好。

这个框架自制的组件。看起来挺复杂。

cce01db5a91a4950ac64885a6b00ea8d.png

瞄准相机位

侧向移动的时候枪相对于瞄准相机有偏移,说明瞄准相机不是简单的作为枪的子物体。

e4abf112ba6a4132955fcdf9ec920bae.png

Third Person Shooter Bundle

1771dfb0ca4741059115e4b99ab80e3f.png

1.改变人物仰角

在ShootBehaviour脚本。使用的是animator.SetBoneLocalRotation()。第一个对Spine设置是改变水平旋转,第二个对Chest设置是改变仰角。根据相机forward得到仰角,加一个修正值armsRotation,绕transform.right旋转,就是绕人物x轴旋转,先不看对Long  weapons的特殊处理,后面的代码要怎么理解?

public void OnAnimatorIK(int layerIndex)
	{
		if (isAiming && activeWeapon > 0)
		{
			if (CheckForBlockedAim())
				return;

			// Orientate upper body where camera  is targeting.
			Quaternion targetRot = Quaternion.Euler(0, transform.eulerAngles.y, 0);
			targetRot *= Quaternion.Euler(initialRootRotation);
			targetRot *= Quaternion.Euler(initialHipsRotation);
			targetRot *= Quaternion.Euler(initialSpineRotation);
			// Set upper body horizontal orientation.
			behaviourManager.GetAnim.SetBoneLocalRotation(HumanBodyBones.Spine, Quaternion.Inverse(hips.rotation) * targetRot);

			// Keep upper body orientation regardless strafe direction.
			float xCamRot = Quaternion.LookRotation(behaviourManager.playerCamera.forward).eulerAngles.x;
			targetRot = Quaternion.AngleAxis(xCamRot + armsRotation, this.transform.right);
			if (weapons[activeWeapon] && weapons[activeWeapon].type == InteractiveWeapon.WeaponType.LONG)
			{
				// Correction for long weapons.
				targetRot *= Quaternion.AngleAxis(9f, this.transform.right);
				targetRot *= Quaternion.AngleAxis(20f, this.transform.up);
			}
			targetRot *= spine.rotation;
			targetRot *= Quaternion.Euler(initialChestRotation);
			// Set upper body vertical orientation.
			behaviourManager.GetAnim.SetBoneLocalRotation(HumanBodyBones.Chest, Quaternion.Inverse(spine.rotation) * targetRot);
		}
	}

对四元数做实验可知:一个物体的世界旋转是从它的根父级到它的局部旋转相乘的的积:这里相乘顺序不能错,必须从根父级到它本身,因为四元数乘法不满足交换律!

[ContextMenu("看看四元数")]
    void PrintQuaternion(){
        Debug.Log("这个物体的世界旋转是"+transform.rotation);
        Debug.Log("这个物体和它父级的局部旋转的积是"+transform.parent.localRotation*transform.localRotation);
    }

73749fbfdda9441da2eab2808ab16de2.png

所以这个targetRot应该是chest在改变仰角之后的世界旋转

SetBoneLocalRotation(HumanBodyBones.Chest, Quaternion.Inverse(spine.rotation) * targetRot);

那么SetBoneLocalRotation里的Quaternion.Inverse()作用好像是和targetRot里的世界旋转成分抵消,使结果是chest需要设置的局部旋转。总之应该可以用一句spine.InverseTransformDirection(transform.right)代替。

SetBoneLocalRotation(HumanBodyBones.Chest, Quaternion.Inverse(spine.rotation) * targetRot);

又看了一下Quaternion.Inverse()的作用:

[ContextMenu("旋转和Inverse相乘")]
    void PrintInverse(){
        Debug.Log(Quaternion.Inverse(transform.rotation)*transform.rotation);
    }

果然Inverse和自己相乘是单位四元数:

7723d6967081489db738411d2ac7b4d4.png

学会了一种寻找人体部位的新方法animator.GetBoneTrasform(HumanBodyBones.XXX);比transform.Find("XXX/XXX/XXX")方便,不受人物骨架结构变化的影响。

432753c3ee7444039191bdcfcd701d3d.png

2.武器挂载

挂载在右手,枪的局部旋转是90度的整数倍,位置不是(0,0,0),枪的脚本里记录了枪的局部位置和旋转。

a06c2ab192ae480da7b7dba0f8ebf78b.png

3.人物和相机的旋转

研究1的时候已经知道,是把相机的仰角传给人物由于改变仰角,所以是玩家控制相机,相机带动人物。

跳跃实现

跳跃的向上移动是通过rigidbody.AddForce()实现的。我们已经知道这样贴着物体跳跃会受到摩擦跳不起来,为了避免这个物体在起跳瞬间竟然把碰撞体的物理材质的动摩擦和静摩擦设0,跳完再设回去。

Low Poly FPS Pack

2.武器绑定

这是一个人物只有手臂的纯FPS模板。从这个项目可以看出纯FPS游戏:

1.手臂和枪被打成一个fbx文件,属于一个模型(所以无需在游戏引擎里调整和记录枪的局部位置旋转),换枪的时候是手臂和枪的模型整体替换,扔枪的时候则是实例化了一把枪扔出去,把手臂模型替换成不拿枪的;

2.手臂和枪都由骨骼控制,且控制手臂、枪、弹匣的骨骼都是平级的,不存在靠父子关系带动;

3.由于手臂和枪是一个整体,手臂和枪互动的动画比如换弹、拉栓可以做得很细致,而如果是TPS,人物和枪是分离的物体,这就涉及到一个animator带动另一个animator转换状态的问题。

d3d12b3d561b448f89d23a5cc23dd48b.png3b2c8b1ea3eb40c7ab3e2b315507d322.png

3.相机和人物的旋转

这里是把相机、人物手臂、UI共同作为一个空节点的子节点,对这个空节点做旋转。

4974f6371cf942e6b0009e3ff881544d.png

瞄准镜实现

这个方案的瞄准镜是局部放大。是用双相机实现的。

f9b756532a1f4450b14cdc949e81a488.png

一个相机渲染手臂和枪。

3b7e4bc323044170b392287a67e2ac13.png

Clear Flags=Depth only:Clear Flags意思是画面里没有物体的部分画什么,Depth only我理解是画Depth小于这个相机的相机画的内容。但是我选Dont Clear也是一样的效果。

Culling Mask=Nothing:意思是什么都不渲染,但是为什么渲染了手臂和枪?我又选了一下Nothing,然后就都不渲染了。

ab88086015bd42a7b19a233c29dabcd2.png

手臂和枪的Layer是空,我只有选Everything或勾选所有层才能渲染手臂和枪。如果一个物体Layer是空,我在Culling Mask下拉框里就看不到它。

cae7abd13f71432db2cf2683c728cb60.png

给一个物体指定Layer时不允许指定空,但是初始却可以是空。

ab9b9636fa1b48119660e3ac80b8467a.png

写了个脚本打印这个物体的Layer:

[ContextMenu("看看Layer")]
    void PrintLayerThis(){
        Debug.Log(gameObject.layer);
    }

显示是8.

72d61e17e9354263a9192e346bc974cb.png

Layer列表里第8层没名字。

ea4d23b555e846e9bf6a5d0208a3f4b9.png

又写了个设置Layer的方法:

[ContextMenu("设置Layer")]
    void SetLayer(){
        gameObject.layer=10;
    }

可以设置成那个序号层,即使它是空的。

6c5f8252bac04bc0adbd537e4a90e045.png

综上,可以用脚本gameObject.Layer=设置成空Layer,但是编辑器界面不能设置成空Layer。脑残的设计。

所以上面的Culling Mask=Nothing也不是真Nothing,而是渲染了一些空Layer。我把第8层改名叫Player,Culling Mask就变Player了。

d8a1dcd306234e838203b5ca105c6f52.png

Depth=1:先渲染Depth=0,再渲染这个。

afcaea732fbd49dbaa76fc99b026ff02.png

前面设置玩家相机不渲染环境,但是瞄准镜里还是要看到远处的环境,这里用了第三个相机,输出到Texture,这个Texture放在瞄准镜后端,后面还有一个镜片物体,开镜的时候镜片使用透明贴图,没开镜时使用全黑贴图。

744ba4b9db5b41edac4c37586651e10e.png08c0e72380df4060a009a09eef59afa7.png

Unity - Manual: Camera component

44b4d0bb112c4fa8b7156f35bd83dc07.png

渲染世界的相机

b720b79d219441e1a3e5deef28d5bc2b.pngf8bb92eaadf44c8f9c1e4001b194a10e.png

动画状态机设计

给不同枪各自用了状态机,防止状态机巨大。

射击控制

FpsControllerLPFP这个脚本里有移动、旋转、跑步、跳的控制,没有开枪。这个脚本挂在人物根对象。

animator对象上挂了AutomaticGunScriptLPFP脚本是控制胳膊的。连发的射击控制用Update()计时器写,但是没有累加deltaTime,而是Time.time减上一次开枪时间,也就是用Time.time计时。

击中检测

用的是碰撞体,场景里子弹多的时候性能可能会差一点。

求生之路

调出了第三人称,发现这么几点:

1.第三人称的弹着点不在准星处,偏右下;

2.走路的时候枪并没有稳定指向前方,而发射子弹是朝前方,子弹应该是从相机中心发出,朝相机正前方;

3.改变瞄准仰角是通过旋转手臂,第三人称动作能达到的仰角范围小于实际的仰角范围,动画只是显示人物动作,并不负责发射子弹;

4.步枪的换弹动作是同一个(曾经第三人称射击游戏都无法给每种枪定制换弹动画,直到绝地求生);

3b1277ce644140fa9997b8531b8dd940.jpegf780e77e5f854f6896f81ee8dc3b32db.jpeg8fe0ddcc53374aff82a3ab123dd2aed3.jpeg

PUBG人物动作素材

在tb上买了一套吃鸡的动作素材。

把一个人物模型拖进来,可以看到:

身体和头分成两个网格,为了在第一人称隐藏头,防止挡第一人称相机;

有很多ik节点,用来动态调整人物的动作。

ik_aim和下面的左右子对象在人物脚下。尚不清除这一套ik的作用。

ik_hand_root在人物脚下。

ik_hand_gun在枪的位置

把骨架进一步展开:能看到近战武器、投掷物、背包、第一人称相机、主武器、副武器挂点;

ik_hand_gun应该是放枪的。

TPS Shooter(Military Style)

1.是旋转Spine实现的。具体方法是在LateUpdate()里通过LookAt()让Spine看向正前方的目标物,目标物绑定在Pivot上,会改变仰角,再让Spine旋转一个角度,也就是右偏。这个偏转角度写在脚本的检查器面板上。

2.枪绑定在右手上,左手通过IK绑定在枪上。所有枪绑定在右手上,使用哪把枪就激活哪把。

5.是。看fbx里的骨架名字,用了一些Mixamo的动画。步枪走路的动画不是Mixamo的,且只有6个方向,右前和左后走路是二维混合树自己混合的。

6.用了AvatarMask。人物用了Hips角度稳定的一套走路动画。

瞄准

机瞄的时候使用了另一套胳膊持枪,人物为了不挡相机身体后仰。

胳膊是绑定在枪上的,每把枪都有一套胳膊,开瞄准时激活。

移动

Animator不在人物根对象,所以没有用Root motion。

使用characterController.Move()移动

_characterController.Move(movement * Time.deltaTime);

跳跃的实现

人物用了CharacterController,重力是代码写的,通过给characterController.Move()一个向下的矢量实现。跳跃时暂时把重力矢量改成向上的,再通过协程,一段时间后把跳跃bool置false,重力矢量又变成向下的。

交互检测

对开车选项的检测使用了射线检测。

射击连发控制

找到输入脚本检测左键,里面是事件调用,看起来用了事件中心。是在Update调用的,按下左键时每帧会调用一次。

去找给这个事件添加的订阅:

订阅的函数。里面在反复停止、开启一个协程。

里面的协程函数。打开IsFire,2帧之后关闭,但是每帧停止再开启协程就是一直打开,直到松开2帧后关闭。

看一下IsFire是干什么的。它表示人物正在射击,用来1.在人物跑步中开始射击时打断跑步;2.被GetNoise使用,Noise其实是玩家会被敌人发现的距离。距离小于Noise时玩家被发现。那么那个停止、开启协程的作用就是让玩家开火中、开火结束2帧后有一个相应的被发现距离。

Fire()函数,里面执行了武器Fire(),调用了PlayerFire事件,这个事件的订阅函数有相机抖动和更新子弹数UI。

武器的Fire()函数。通过Invoke()延时执行,把_canShoot开关打开。else里面IdleSound是没子弹的声音。但是这里如果冷却没结束,枪里有子弹,不是也会进else播放没子弹声音吗?在else里打印了一下,没有打印,发现在上面OnFireRequested()里如果当前武器不能射击,直接就return了。

总结起来就是通过Invoke()计算射击冷却,比手写计时器简单。

动画状态机设计

分为这几层:

Base层是运动相关动画。分为空手和装备状态,里面又分为走路、跑步、蹲、跳跃。

移动到跳跃有两种转换,分别对应跳跃和坠落。

第二层瞄准层使用了覆盖上半身的AvatarMask。跑步的时候进入Empty状态,里面无动画。也就是跑步是放在Base层,上层置空,没有用上下半身叠加。跳的时候会回到Aiming。

换枪、换弹、扔手榴弹各加了一层,AvatarMask都是上半身。虽然我觉得这些动作是互斥的,可以和Aiming放在一层。

一些细节

动画参数IsRun在跳跃中会变成false,因为每帧执行UpdateRun()时IsJumping会把IsRun设false。

IsJumping又是怎么维护的?是用起跳和落地动画的动画事件写入的。

IsAiming的维护:在LateUpdate()执行的UpdateSpineIK()里把NeedToUseSpineIK()的结果写入NeedToUseSpineIK()里面写在人物活着&&没有跑步&&没有扔手雷&&没有空手&&没有开载具时为true。也就是在跳跃中、空中是可以瞄准的。

看了几套框架,都发现除了动画参数,脚本里还有一些bool字段控制这些参数。有时候让人感觉多余,有些却是必不可少的。

脚步声

定义了FootsetpData : ScriptableObject,里面有一个TextureType数组,里面有一个Texture数组、一个AudioClip数组。人物向下发射射线,得到脚下地面的贴图,通过和人物脚下的地的材质的漫反射贴图比较,确定要播放的AudioClip数组,随机播放里面的一个AudioClip。还分为了脚下是MeshRenderer和是Terrain两种情况。播放脚步声通过动画事件触发,加了巨量动画事件。

Starter Assets Third Person

想起来这个模板的跳跃效果不错,特来研究。

跳跃

这个实例用了CharacterController,但没有用SimpleMove()提供的重力,而是用Move()手写重力。注意到:

1.跳的动画参数使用了bool而非trigger;

2.在地面上时每一帧都在设置动画参数jump和freefall为false;

3.令人匪夷所思的有一个bool hasAnimator,好像在应对animator不一定存在的情况,更匪夷所思的是每一帧都在判断hasAnimator,好像animator随时会被删除;

4.也没有用CharacterController自带的isGrounded,而是声明一个bool grounded,用Physics.CheckSphere()每一帧判断(触发器检测方法只用OnTriggerEnter和OnTriggerExit在从一块地面进入另一块时会判定离地,还是要OnTriggerStay每帧判定,比这个方法没有优势。);

这个官方模板也并不完美:

1.跳跃中可以移动,不符合真实世界;

2.从楼梯上跑下来时不能跳跃,因为被判定为离地;

DecayedState

1.

使用了混合树。

2.

武器绑定在手上。

6.

动画状态机有4层:移动、手和头(瞄准层)、骑马、交互。瞄准层覆盖了双臂和头。

跑步用了AvatarMask,使用持枪放松的动作覆盖。

跳跃

模板使用了CharacterController,自写重力。地面检测使用了Physics.CheckSphere()。

这个模板不支持随意跳跃,但是可以翻越。翻越时为了防止持枪动作影响翻越,把瞄准层权重关闭了。

这是通过动画事件实现的。

其他亮点

这个模板有一些效果不错的细节。

碎玻璃效果

在玻璃上挂了脚本,记录了破碎后的预制体和破碎声音,破碎时:

  1. 关闭渲染器和碰撞体;
  2. 实例化碎玻璃预制体,作为玻璃的子对象;
  3. 一段时间后销毁玻璃;

碎玻璃预制体的每个碎玻璃上有刚体和碰撞体。

环境交互动作

实现了丰富的环境交互动作如翻越、踹门、上下车。环境交互检测都是人物向前发射射线检测的,通过目标Tag检测交互类型。

Vector3 ahead = transform.forward;
			Vector3 rayStart = new Vector3(this.transform.position.x, this.transform.position.y+1f, this.transform.position.z);
			Ray	ray = new Ray(rayStart, ahead);
			RaycastHit hit;
			if(Physics.Raycast(ray, out hit, 10f)){

总结

  1. 在增加动作时,大多数模板倾向于增加层,而非在已有层增加状态。
  2. 很多模板会给每个动画参数在脚本定义一个对应的字段,每帧设置参数;
  3. 人物动作系统很难通过简洁的状态机和代码达到较好的效果,免不了修修补补。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值