人物人物的代码总数在几百行到上千行可以分为几个模块适合用partial class分散到几个脚本基础行为移动、旋转、俯仰、跑步、蹲下、站起物理相关重力、地面检测、跳跃、枪被遮挡检测、游泳检测装备交互拿出枪、收起枪、切换枪、放下枪、捡起枪、手榴弹也有这么一套、换弹、射击、切换自动模式输入模块和玩家人物的耦合方式有输入调用人物和人物监听输入两种架构。输入调用人物的方式到后期如果做载具功能调用之前还要先判断玩家有没有开载具根据徒步还是开载具调用相应类的移动方法。而监听架构人物上车时解除监听玩家的车注册监听输入模块只管调用委托代码看起来更舒服。不过即使游戏有步行、开汽车、开坦克、开飞机还是可以输入模块调用玩家。只需要让人物、汽车、坦克、飞机都继承接口IControllable输入模块持有一个IControllable controllable上车时把汽车赋值到controllable。不过这样又有问题是人物接收的输入比汽车要多了蹲、瞄准、换弹等动作。接口要定义一系列所有子类都要实现的方法那么是否IControllable定义蹲、换弹定义了汽车就用不上不定义人物又要用。综上监听架构允许人物、汽车、坦克、飞机绑定不同数量种类的监听更合适。一个人物监听输入架构的典型样子输入模块public event UnityActionVector2 onMove; public event UnityActionVector2 onRotate; public event UnityActionbool onRun; public event UnityActionbool onFire; public event UnityAction onStartPullTrigger; public event UnityAction onStopPullTrigger; public event UnityAction onUseMainGun; public event UnityAction onUseHandgun;玩家人物模块private void OnEnable() { MyInput.Single.onMove Move; MyInput.Single.onRotate Rotate; MyInput.Single.onRun Run; MyInput.Single.onFire SetPullTrigger; MyInput.Single.onStartPullTrigger StartPullTrigger; MyInput.Single.onStopPullTrigger StopPullTrigger; MyInput.Single.onUseMainGun CheckUseRifle; MyInput.Single.onUseHandgun ChechUseHandgun; } private void OnDisable() { if (MyInput.Single) { MyInput.Single.onMove - Move; MyInput.Single.onRotate - Rotate; MyInput.Single.onRun - Run; MyInput.Single.onFire - SetPullTrigger; MyInput.Single.onStartPullTrigger - StartPullTrigger; MyInput.Single.onStopPullTrigger - StopPullTrigger; MyInput.Single.onUseMainGun - CheckUseRifle; MyInput.Single.onUseHandgun - ChechUseHandgun; } }趴下功能需要实现碰撞体倒下由于CharacterController的碰撞体只能立着需要加一个倒下的胶囊碰撞体。趴下前在人物低处从前到后做一个连线检测没有检测到障碍物则执行趴下人物在斜面上趴着的时候身体和斜面平行通过向下发射射线打到地面得到碰撞点法线再把人物y轴对齐法线。人物站起后还要把y轴调回竖直方向站起的上方有没有空间的范围检测所有上半身动画的趴下版本换弹不同枪型换弹动画还不一样、打药、扔手雷。人物在斜面上趴着的时候身体和斜面平行向下发射线击中地面获得击中点法向使用Quaternion.FromToRotation()把人物y轴转向法向。RaycastHit raycastHit; void Start(){ } void Update(){ if(Physics.Raycast(transform.position,Vector3.down, out raycastHit,1,MyGameManager.Instance.layersGround)){ transform.rotation*Quaternion.FromToRotation(transform.up,raycastHit.normal); } }射击逻辑射击输入检测需要在准备好射击手里有枪没在跑步没在换弹枪有子弹玩家按下鼠标时射击。人物可能有多把枪需要先判断哪把是手里的。人在射击中还按着鼠标突然死亡、开始跑步、换弹等各种动作停止射击。准备好射击和有输入分别用bool readyToFire和bool pullTrigger它们与的结果每帧检测不能有“保持原状”的情况否则容易出现射击不能开始或停止的情况。射速控制动画和代码的比较游戏里大部分参数都既可以用代码控制也可以用动画控制。二者的特点不同。动画时间控制精确逻辑死板代码逻辑灵活时间控制困难只有第一帧和每一帧执行的生命周期函数其他时机都要写计时器代码控制。比如写一个人物定时眨眼的效果二者都能控制模型skinned mesh renderer的blendShapes动画需要打4个关键帧极简单代码需要一个计时器变量在Update()判断到没到该眨眼的帧用Mathf.Lerp()写过渡效果……代码方案我一开始的射击功能就是靠代码、计时器变量写的。而枪的射速是固定的用动画实现应该会更简单。动画方案枪上挂Animator。枪的动画状态机枪的动画状态机的设计面临几个选择1.进入射击状态的条件用bool还是trigger2.射击状态的动画是否选择loop time是选择loop time在firing为false时退出还是不选择loop time在Exit time退出或者说连发射击时枪在射击状态反复播放还是在射击和Idle间反复跳转我这里的设计firing为true时进入RecoilRecoil通过hasExitTime退出另外通过hasAmmo判断是跳转到Idle还是HoldOpen空仓挂机换弹后hasAmmotrue进入Idle。枪的射击动画Fire()在第一帧执行一共有几帧取决于枪的射速枪一秒射击n发动画一秒60帧动画长度就是60/n。动画有回膛就做回膛无事可做就加个无关紧要的属性比如Scale保持不变。如果想加一个小的后座加给MeshRenderer所在的物体不要加给根节点否则开枪的时候枪会跑到世界的那个位置。换句话说枪的实体别做根节点。进入和退出射击状态的动画过渡设置为0。射击控制代码人物脚本声明一个bool pullTrigger玩家按下左键判断人物准备好射击后没在跑步、换弹把pullTriggertrue把枪动画状态机的参数bool firingtruepublic void FireControlWithAnim(bool triggerPulled){ switch(autoMode){ case AutoMode.Full: animator.SetBool(firing,triggerPulled); break; case AutoMode.Semi: case AutoMode.Burst: animator.SetBool(firing,triggerPulled!firingReg); firingRegtriggerPulled; break; } }半自动射击的实现严格的半自动射击除了在鼠标按下时射击还需要玩家高频按下鼠标时也不超过枪的全自动射速。使用InputManager使用GetMouseButtonDown()时才把firing设置true。这不能防止玩家高频按下鼠标时半自动射速超过全自动。使用InputSystem可以定义一个bool fireReg用来记录上一帧有没有按下左键在执行射击后把fireRegtriggerPulled射击判断写成triggerPulled!fireReg。PlayerInput的回调函数只在输入值变化时执行可以在回调函数执行且input.ReadValuefloat()1时执行StartPullTrigger()input.ReadValuefloat()0时执行StopPullTrigger()。之所以要得到松开鼠标的时机是因为扔手雷需要这个时机。StartPullTrigger()放在输入类还是人物类姑且放在人物类。现在的问题是人物在Update()里检测全自动射击枪是半自动模式时要把这个检测关掉或者退一步检测射击的代码放在人物类还是枪类应该是人物类先判断有没有使用枪有则执行这把枪的射击检测就是说检测代码在枪类由人物类执行。从逻辑上半自动全自动不应该影响人物执行射击检测人物只负责收到输入后扣下扳机半自动是枪的属性半自动枪射击后停止射击就是说方案1更符合实际。但是这里是输入类得到按下鼠标的时机要把信号经过“人物可以射击”的判断后传给枪人物不再在Update()里检测射击而是由输入类调用StartPullTrigger()里面是一次射击。这和“人物只管根据输入扣扳机”的逻辑相违背好像半自动模式人物不再监听输入而是由鼠标按下射击直接让枪射击。三连发实现先总结三连点射需要处理的所有情况1.按一下鼠标发射三发2.一直按着鼠标发射三发3.剩余子弹不到三发按一下鼠标发射完如果不允许切换为半自动不考虑3就可以做一个触发三次Fire()动画事件的AnimationClip设计会大大简单参考求生之路的scar。要考虑3则可以第一次发射后根据是不是Burst选择到Idle还是下一个射击状态每次发射根据还有没有子弹判断是到下一个发射状态还是Hold Open最后一次发射到Idle或Hold Open。总之充分利用动画状态机的逻辑功能可以使代码简化。击中不同部位造成不同伤害要实现击中不同部位造成不同伤害需要由中枪触发器实现。或者直接用Unity内置的Ragdoll生成器。这样写射击射线检测的时候需要把人物的碰撞体排除把身体触发器包括人物根节点和骨骼节点需要在不同层。如果测试时明明击中人却显示没有击中可以排查是否人物碰撞体和中枪触发器在同一层。public class BodyTriggerTool : MonoBehaviour { Animator animator; public Transform head,hips; [ContextMenu(创建头和躯干中枪触发器)] void CreateTrunkTrigger(){ if(!animator){ animatorGetComponentAnimator(); } //躯干触发器 hipsanimator.GetBoneTransform(HumanBodyBones.Hips); BodyTrigger bodyParthips.gameObject.AddComponentBodyTrigger(); bodyPart.bodyPartOptionBodyPartOptions.trunk; CapsuleCollider trunkTrigger; if(!hips.TryGetComponent(out trunkTrigger)){ trunkTriggerhips.gameObject.AddComponentCapsuleCollider(); trunkTrigger.isTriggertrue; trunkTrigger.radius.1f; trunkTrigger.heightanimator.GetBoneTransform(HumanBodyBones.Neck).position.y-hips.position.y; trunkTrigger.centernew Vector3(0,trunkTrigger.height/2,0); } //头触发器 SphereCollider headTrigger; headanimator.GetBoneTransform(HumanBodyBones.Head); BodyTrigger headBodyParthead.gameObject.AddComponentBodyTrigger(); headBodyPart.bodyPartOptionBodyPartOptions.head; if(!head.TryGetComponent(out headTrigger)){ headTriggerhead.gameObject.AddComponentSphereCollider(); headTrigger.isTriggertrue; headTrigger.centernew Vector3(0,.12f,0); headTrigger.radius.15f; } } public Transform[]bodyParts; public Transform[] bodyPartChildren; void GetBones(){ //加触发器的骨骼 bodyPartsnew Transform[]{ animator.GetBoneTransform(HumanBodyBones.LeftUpperArm), animator.GetBoneTransform(HumanBodyBones.RightUpperArm), animator.GetBoneTransform(HumanBodyBones.LeftUpperLeg), animator.GetBoneTransform(HumanBodyBones.RightUpperLeg), animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg), animator.GetBoneTransform(HumanBodyBones.RightLowerLeg), animator.GetBoneTransform(HumanBodyBones.LeftLowerArm), animator.GetBoneTransform(HumanBodyBones.RightLowerArm) }; //上面骨骼的子骨骼,用于确定触发器长度 bodyPartChildrennew Transform[]{ animator.GetBoneTransform(HumanBodyBones.LeftLowerArm), animator.GetBoneTransform(HumanBodyBones.RightLowerArm), animator.GetBoneTransform(HumanBodyBones.LeftLowerLeg), animator.GetBoneTransform(HumanBodyBones.RightLowerLeg), animator.GetBoneTransform(HumanBodyBones.LeftFoot), animator.GetBoneTransform(HumanBodyBones.RightFoot), animator.GetBoneTransform(HumanBodyBones.LeftHand), animator.GetBoneTransform(HumanBodyBones.RightHand) }; } [ContextMenu(创建四肢中枪触发器)] public void CreatLimbTriggers(){ if(!animator){ animatorGetComponentAnimator(); } GetBones(); CapsuleCollider shotTrigger; BodyTrigger bodyPart; for(int i0;ibodyParts.Length;i){ if(bodyParts[i]!bodyParts[i].TryGetComponent(out shotTrigger)){ shotTriggerbodyParts[i].gameObject.AddComponentCapsuleCollider(); shotTrigger.isTriggertrue; } shotTriggerbodyParts[i].GetComponentCapsuleCollider(); shotTrigger.heightbodyPartChildren[i].localPosition.y; shotTrigger.centernew Vector3(0,shotTrigger.height/2,0); shotTrigger.radius.05f; if(!bodyParts[i].TryGetComponent(out bodyPart)){ bodyPartbodyParts[i].gameObject.AddComponentBodyTrigger(); } } } [ContextMenu(清除中枪触发器)] void ClearBodyTriggers(){ Collider collider; BodyTrigger[] bodyTriggersGetComponentsInChildrenBodyTrigger(); for(int i0;ibodyTriggers.Length;i){ if(bodyTriggers[i].TryGetComponent(out collider)){ DestroyImmediate(collider); } DestroyImmediate(bodyTriggers[i]); } } }瞄准瞄准相机配置在枪上挂一个记录瞄准相机位置的节点。步枪瞄准和手枪瞄准的方案不太一样。手枪瞄准手枪的瞄准相机不能做手枪的子物体因为手枪射击要上跳瞄准相机做手枪的子物体就和手枪一起上跳。人物的眼睛是不会随手枪上跳的。手枪的瞄准相机需要跟随枪的位置不跟随枪的旋转。实现这个功能有几种方案父物体为空使用Cinemachine瞄准相机Follow枪的瞄准节点LookAt设置目标物也就是瞄准相机的旋转由目标物决定。还可以加一点damping看起来更真实但是Composer不要加Vertical Damping否则上抬的时候相机可能穿到枪身里。父物体为头使用Cinemachine瞄准相机Follow枪的瞄准节点LookAt不设置也就是瞄准相机的旋转由头决定。步枪瞄准步枪射击时因为托腮人物眼睛随枪上跳瞄准相机就可以直接挂在瞄准位置。其他方案总之位置可以用Follow、PositionConstraint、ParentConstraint或父子级约束旋转可以用LookAt、RotationConstraint、ParentConstraint或父子级约束。瞄准镜我直接参考了这两篇文章【unity小技巧】使用三种方式实现瞄准瞄具放大变焦效果_unity放大镜效果-CSDN博客【unity小技巧】实现FPS武器的瞄准放大效果UGUI实现反向遮罩全屏遮挡局部镂空效果_unity 开镜-CSDN博客第二篇文章的效果挺不错能满足要求。第一篇文章方案三相机输出到贴图贴图再应用到瞄准镜后端实现局部放大的效果材质配置Base Map颜色选黑贴图给Emission Map颜色选白色。效果可惜我的人物头发和瞄准镜后端穿模了没法用。弹道子弹起点和方向众所周知第三人称相机和枪离得稍远如果子弹从枪口发出沿枪z轴飞行如果相机和枪z轴平行弹着点只能在无穷远处和屏幕中心一致弹着点近时都不能命中屏幕中心。虽然这是最真实的设计用户体验却不好。总结起来弹道和第三人称相机的关系有这么3种设计子弹从枪口发出方向和相机z轴平行子弹从枪口发出朝相机中心落点飞行子弹从相机发出沿相机z轴飞行如果硬要用1这种真实设计应该提供机瞄视角并提醒用户使用机瞄。如果没有机瞄使用越肩视角瞄准就必须使用2或3.然后我去看了一下和平精英的设计发现如下几点第三人称没有障碍物时弹着点在屏幕中心说明弹道和相机z轴不平行有障碍物时可能挡住子弹无法击中屏幕中心说明子弹不是从相机发出而是从枪口发出瞄准近处时第三人称和机瞄屏幕中心的落点不一致但子弹总能击中屏幕中心可以得出结论和平精英的弹道是方案2从枪口发出去找屏幕中心落点。在跑步中开枪、坐在车里开枪都能看到人物立即摆出据枪姿势但是子弹击中准星落点说明弹道完全和枪管没关系。不同情况下得到弹道方向大多数时间子弹都不沿枪z轴发射对于玩家子弹朝准星落点发射第三人称和机瞄状态相机位置不同如果用Cinemachine两个状态使用两个虚拟相机还要先得到激活中的相机。对于敌人需要弹道指向玩家一些随机偏差。发射子弹应该是枪的方法但是玩家瞄准、玩家未瞄准、敌人得到弹道的逻辑不一样难道枪发射子弹时弹道还要由外部某类算好射击命中检测有射线检测方案和发射弹头实体的方案。射线检测方案比较简单弹道只能是直线没有弹头火光飞过效果没有击中延迟。void FireRayCast(){ RaycastHit _hit; if(Physics.Raycast(bulletOrigin,bulletVector,out _hit,fireRange,fireLayerMask)){//击中点效果 if(debugger){ debugger.position_hit.point; } // Debug.Log($打中了{_hit.transform});Debug.Log($碰撞体{_hit.collider}); if(_hit.collider.TryGetComponent(out bodyPart)){//打到人 bodyPart.GetHurt(damageData); } else{//打到东西播放粒子效果 ImpactEffectRecorder myImpactEffect; if(_hit.transform.TryGetComponent(out myImpactEffect)){ GameObject effectInstance; effectInstanceDepool(myImpactEffect.impactEffectPrefab.gameObject,_hit.point);//缓冲池出池 effectInstance.transform.LookAt(_hit.point _hit.normal); // Destroy(shotObj,5f); // Invoke(Enpool,5f); } else{ Debug.Log(_hit.transform.name没有击中效果); } } } }发射弹头实体方案能模拟抛物线弹道有子弹火光。但是击中检测不能用碰撞检测因为子弹每帧飞过几米到几十米大概率穿过物体。只能用一个Vector3记录上一帧的位置使用Physics.Linecast()做连线检测。这样击中检测的轨迹其实是拟合抛物线的折线。但是勉强能模拟重力。发射代码[Header(发射弹头实体需要的信息)] public MyBullet bulletPrefab; public int bulletSpeed; void FireBulletGameObject(){ if(!bulletPrefab){ Debug.Log(没有弹头预制体); return; } GameObject bulletInstantiate(bulletPrefab.gameObject); if(muzzleEffects.Length0){ bullet.transform.positionmuzzleEffects[0].transform.position; } else{ bullet.transform.positiontransform.position; } bullet.transform.rotationtransform.rotation; if(bulletSpeed0){ bulletSpeed700; } bullet.GetComponentRigidbody().velocitytransform.forward*bulletSpeed; bullet.GetComponentMyBullet().damageDatadamageData; Destroy(bullet,1); }弹头脚本public class MyBullet : MonoBehaviour { public LayerMask bulletLayerMask; int groundLayer8; public Weapon.DamageData damageData; Vector3 lastFramePosition; void Start(){ lastFramePosition transform.position; } RaycastHit raycastHit; BodyTrigger bodyTrigger; ImpactEffectRecorder myImpactEffect; void Update(){ if(Physics.Linecast(lastFramePosition,transform.position,out raycastHit,bulletLayerMask,QueryTriggerInteraction.UseGlobal)){ if(raycastHit.collider.gameObject.layergroundLayer){ if(raycastHit.transform.TryGetComponent(out myImpactEffect)){ GameObject effectInstance; effectInstanceMyGameManager.Instance.Depool(myImpactEffect.impactEffectPrefab.gameObject,raycastHit.point);//缓冲池出池 effectInstance.transform.LookAt(raycastHit.pointraycastHit.normal); } else{ Debug.Log(raycastHit.transform.name没有击中效果); } } else{ if(raycastHit.collider.TryGetComponent(out bodyTrigger)){ bodyTrigger.GetHurt(damageData); } } Destroy(gameObject); } } }击中反馈击中敌人时准星周围显示一小段时间的X提示击中。用协程延迟隐藏X。击中时先把之前等待隐藏X的协程停止再开新协程。或者就用计时器写击中时隐藏X倒计时刷新。防止人物攻击打到自己有层过滤物理检测拿到碰撞体判断是自己可以把玩家和敌人放不同层但是敌人也不能打到自己敌人的数量不确定不可能一个敌人一个层只能拿到碰撞体判断。但是发现Physics.Linecast只能获得第一个碰撞体没有Physics.LinecastAll。动画系统射击游戏动作系统的特点很难不用分层和AvatarMask有些动作需要生动性跑步、跳有些动作需要精确性主要是瞄准人和随身物品的交互关系较复杂武器在不同的动作跟随不同节点有些动作受多个节点影响移动时保持上半身稳定的问题我想让人物移动的同时人物端枪瞄准前方。所以我加了一个Arm层移动放在Base层希望走路时上半身稳定。AvatarMask覆盖上半身问题如果走路动画Hips的旋转是摇晃的那么AvatarMask覆盖的上半身也会跟着摇晃。AvatarMask覆盖双臂问题走路时Chest朝向和静止时不一样导致走路时双臂的方向歪。静止时Chest的朝向前进时Chest的朝向效果AvatarMask覆盖双臂和根节点又会导致腿走路的方向歪。解决方法给Spine加Rotation Constraint由一个指向瞄准方向的物体约束它。先预览人物静止端枪的动画再点Rotation Constraint的Is activated组件会计算出当前Spine相对约束物体的旋转偏移再Lock。但是想在开始跑步时腰从约束状态平滑过渡到动画状态这个约束平滑把权重降到0腰也没有播放动画而是局部旋转不变。AnimationRigging的Multi Rotation Constraint可以通过权重平滑变化到0平滑过渡到动画状态。这是AnimationRigging约束很重要的一个优势射击上跳动画本来想在Arm层加一个从持枪空闲Pistol Stand到射击的状态。但是遇到了问题Pistol Stand状态脚本的OnStateExit()我写了解除瞄准因为离开Pistol Stand进入的所有状态都不能瞄准。然后射击时就会解除瞄准。只能另开一层Hand放射击上跳动画。另外这里站和趴都有上跳动画如果Hand层是Override那么站的上跳动画就不能用于趴但是改成Addictive这个上跳动画可以同时用于站和趴。物品拾取和扔掉功能扔掉、捡起物品需要做的可以粗分为两部分1.对物体的操作改变父级、设置位置旋转2.播放人物动作二者的调用关系可以是1.在一个方法里执行物体操作2.方法里设置状态机参数在动画事件里执行物体操作动画状态机的问题动画状态机里我只想用一个整数gunStatus表示手的状态0空手、1拿步枪、2拿手枪但是这样出现了一些含糊不清的情况gunStatus从1到0可能是扔掉步枪和收起步枪都需要从1到0为了播放正确的动画必须1.加另外一个参数区分两种转换2.扔枪也要把gunStatus设0但要在进入扔枪状态后防止进入背枪状态给扔枪加了Trigger PutDown捡枪加了Trigger PickUp捡枪时先设置PickUp捡起的动画调用动画事件在里面把手的状态设置为1或2。捡枪后手应该进入1还是2状态也由动画事件的方法根据枪的类型判断。一个功能要播放动画且执行一些代码这些代码可放在3个地方1.和播放动画的代码写在一起在动画开始时执行2.放在状态脚本的OnStateEnter()或OnStateExit()3.使用动画事件。动画事件可以精确控制方法执行的时机但是不能传参数需要在脚本里加字段。人物装备系统人物拾取枪时根据拾取的是主枪还是手枪绑定在相应的挂点并建立引用人物装备有同类枪时需要放下装备的同类枪人物获取枪的情景有从地上拾取从箱子拾取从地上拾取需要销毁地上的可拾取枪预制体从箱子拾取需要从箱子包含的物品的数据结构移除这个枪的记录还需要程序能给某人物敌人装备枪以上情况如何减少代码重复并正确处理各情景各自的特殊需求持枪跑步使用AvatarMask把上半身和下半身合成的动画刚结束跑步后立即再进入跑步上半身会剧烈晃动鉴定为上下半身的状态转换时间不同如果有一层还没有回到静止而另一层已经回到静止那么再开始跑步两层开始播放跑步动画的时机不同晃动的节奏不一致。所以跑步动画不要用AvatarMask。AvatarMask只用于需要上下半身同时做不同的动作如换弹和移动。斜身功能本来想做左右斜身的动画发现用Avatar做这两个动画极难。使用了animator.SetBoneLocalRotation()实现和俯仰写在一个方法里因为一帧里好像只有最后一次animator.SetBoneLocalRotation()是最终效果所以把改变Spine的仰角和左右倾斜写到一个Quaternion加给Spine的旋转。使用Mathf.Lerp()加了倾斜角度渐变。在持枪的状态机行为脚本里调用。换弹读条功能几个要点UGUI的Image设置Filled使用了animatorStateInfo.normalizedTime写入fillAmount。在状态机行为脚本里执行的因为它的生命周期函数的输入参数直接就有animatorStateInfo。麻烦的是要判断一下这个实例是不是玩家。public class ReloadState : SMBBase{ bool isPlayer; override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { base.OnStateEnter(animator, stateInfo, layerIndex); myCharacter.reloading true; isPlayer myCharacter MyInput.Instance.player; if (isPlayer) { PanelGame.Instance.actionPregress.enabled true; } } override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { if (isPlayer) { PanelGame.Instance.actionPregress.fillAmount stateInfo.normalizedTime; } } // OnStateExit is called when a transition ends and the state machine finishes evaluating this state override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex) { myCharacter.reloading false; if (isPlayer) { PanelGame.Instance.actionPregress.enabled false; } }不同枪的换弹动画长枪的换弹动画可以分为几类右边拉栓AK系、Mini14、SKS等、左边拉栓MP5、G3、FAL等、按按钮AR系、SCAR等、旋转后拉栓动步枪、左手撸滑套霰弹枪。武器信息界面的维护武器信息栏包含武器名、自动模式、子弹数、弹匣数。哪些情况需要更新武器信息栏1.拿出枪2.收起枪隐藏3.交换枪4.放下枪5.射击6.换弹7.拿起手里枪的弹匣8.放下手里枪的弹匣9.改变自动模式除了把这一坨信息可能改变的情况都找出来加代码有没有可能把这些信息做成属性在Set方法里顺便改ui首先这些信息都是谁的字段使用的武器是人物的自动方式和枪内子弹数是枪的弹匣数又来自背包……然后人物并没有一个字段记录当前用的枪是xxx而是记录当前携带的主枪是xxx手枪是yyy当前使用的枪通过动画参数可以获得。再加一个“正在使用的枪”字段就要保持它和人物动画正在使用的枪一致。枪的自动方式和子弹数是字段但是枪不知道自己是不是玩家手里的还要访问主人判断是不是玩家。背包里当前枪的弹匣数没有字段。武器信息界面的维护还是没有一个简单的方法。使用的武器的变化发生在人物对象枪的子弹数和自动方式变化发生在枪对象弹匣数变化发生在背包对象。这些来源分散的事件都会导致界面变化。而且变化时还要判断是不是玩家的而不是npc的变化。人物对象可以写一个玩家子类重写方法但是枪是不可能给玩家的枪写一个特殊子类的。人物死亡人物在动画状态机的任何状态都可能死亡不可能给每个状态加一个到死亡状态的转换。有几个办法解决。Any State注意Any State到Dead的转换不要勾选转换到自己。animator.Play()播放一个状态的名字不管有没有转移直接跳到那个状态。AnimatorController加一层Dead覆盖在所有层上只有两个状态Alive状态播放no MotionDead状态播放死亡。玩家死的时候要关闭输入如果玩家此时开着背包要关闭背包面板。但是关闭背包面板本来会激活输入。控制器需要保证先关闭背包面板等关闭背包的效果执行完再关闭输入。NPC检测其他人通过检测目标人物在不在自己前面的扇区判断有没有看到其他人。问题是怎么知道要检测的目标人物有哪些FindObjects找到所有人物脚本实例很明显开销太大。如果有扇区触发器碰撞体是最合适的但是没有。只能退而求其次先用球形触发器碰撞体把附近的人物加到一个列表再做扇区检测。随机放置敌人位置及遇到的困难测试中每次敌人都在相同的位置次数多了及其无聊。就想让敌人的位置在一个区域内随机又不能穿模在建筑里。使用do while循环随机得到一个位置使用Physics.CheckSphere()和Physics.OverlapSphere()判断此处有没有建筑结果完全在建筑内部也得到没有碰撞体。然后又想到先得到随机位置NavMesh.SamplePosition把人物放到就近导航网格上。还要先给建筑物挂载NavMesh Obstacle勾选carve防止在内部生成导航网格。这个是可行的。然后为了防止敌人在玩家面前生成需要给生成器弄一个较大的触发半径五十米以上。然后我们希望在玩家看不见的转角在随机时间、随机位置生成敌人可以先判断位置在不在玩家面前的扇形区域。但是这样可能直接生成在玩家身后玩家体验很差。第一人称首先在建模软件里把头部分离。注意头部网格不要和头部骨骼或任何骨骼同名。否则动画系统出问题前面全白干。第一人称把头部renderer设置shadows only。相机的旋转要跟随瞄准轴位置跟随头部骨骼。需要加个约束。或者代码控制。模块职责划分人物捡枪 放下枪包括地上和箱子里一定会涉及到两个模块的操作。每个模块应该负责到哪里人物负责在自己的武器挂点生成或销毁枪模型以及建立、消除引用。地上的枪模销毁自己管理器在地上生成枪模箱子数量变化那么玩家的操作应该调用管理器管理器完成对两个模块的调用。人物掏出手榴弹时这份数据是否要移除背包还是扔出时移除我觉得是不应该移除的假如掏出手榴弹移除背包此时捡东西把背包容量撑满手榴弹就塞不回去了。然后我看了一下荒野行动它把手榴弹塞回背包就是增加了占用把背包塞满手榴弹槽的手榴弹是塞不回背包的但是它手榴弹槽有东西时也可以空手等于它的手榴弹槽也有容量不一定要占用手。而且它的背包可以轻微超过容量那么手里的手榴弹也算背包占用就必须在手榴弹投出时把背包数据-1。动画调用逻辑 和 动画逻辑分离有很多需要动画执行到某帧执行的逻辑换弹生效、治疗生效、换弹治疗进度条。动作进度条可以直接使用状态机行为脚本读取动画进度。用动画关键帧、事件调用逻辑能直接看见那一帧动画的状态复用动画状态机的逻辑。但是专业项目好像大多是动画、逻辑分离的。动画某一帧执行的逻辑通过程序延迟调用延迟时间和动画的时间手动调成一致。