[Unity Demo]从零开始制作空洞骑士Hollow Knight第十集:制作后坐力系统Recoil和小骑士的生命系统和受伤系统

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、制作后坐力系统Recoil
    • 1.使用代码实现扩展新的后坐力行为
    • 2.为敌人脚本添加后坐力行为
    • 3.为小骑士添加后坐力行为
  • 二、制作小骑士的生命系统和受伤系统
    • 1.制作动画以及使用UNITY编辑器编辑
    • 2.使用代码制作生命系统和受伤系统
  • 总结


前言

        警告:此篇文章难度比上一篇还高,非常不适合刚刚入门的或者没看过我前几期的读者,我特么又做了一整天才把明显的bug给解决了,因此请读者如果在阅读后感到身体不适请立刻退出这篇文章。

        本期主要涉及的内容是:制作后坐力系统Recoil和小骑士的生命系统和受伤系统,每一个大纲我都已经设置好分类,OK话不多说直接开Code!


一、制作后坐力系统Recoil

1.使用代码实现扩展新的后坐力行为

为每一个敌人添加一个新的脚本叫Recoil.cs:

在打代码之前我们先想想后坐力行为有几种状态:Ready准备进入状态,Frozen冻结状态,Recoiling正在后坐力状态

需要的变量:private float recoilDuration; //后坐力持续时间

private float recoilSpeedBase = 15f; //基本后坐力速度

 public bool freezeInPlace; //是否会不动

以及取消后坐力后的事件,冻结时的事件:

    public delegate void CancelRecoilEvent();
    public event CancelRecoilEvent OnCancelRecoil;

    public delegate void FreezeEvent();
    public event FreezeEvent OnHandleFreeze;

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Recoil : MonoBehaviour
{private Rigidbody2D rb2d;private Collider2D bodyCollider;[SerializeField] private bool recoilUp; //是否有向上的后坐力[SerializeField] private float recoilSpeedBase = 15f; //基本后坐力速度[SerializeField] private float recoilDuration; //后坐力持续时间[SerializeField] public bool freezeInPlace; //是否会不动[SerializeField] private bool stopVelocityXWhenRecoilingUp; //当正在受上后坐力的时候停止X轴方向的速度[SerializeField] private bool preventRecoilUp;private bool skipFreezingByController;[SerializeField]private States state;private float recoilTimeRemaining; //后坐力持续时间private float recoilSpeed;//最终后坐力的速度private Sweep recoilSweep; //检测地形private bool isRecoilSweeping; //是否private const int SweepLayerMask = 256; //也就是Layer "Terrain"public delegate void CancelRecoilEvent();public event CancelRecoilEvent OnCancelRecoil;public delegate void FreezeEvent();public event FreezeEvent OnHandleFreeze;public bool SkipFreezingByController{get{return skipFreezingByController;}set{skipFreezingByController = value;}}public bool IsRecoiling{get{return state == States.Recoiling || state == States.Frozen;}}protected void Reset(){freezeInPlace = false;stopVelocityXWhenRecoilingUp = true;recoilDuration = 0.5f;recoilSpeedBase = 15f;preventRecoilUp = false;}protected void Awake(){rb2d = GetComponent<Rigidbody2D>();bodyCollider = GetComponent<BoxCollider2D>();}private void OnEnable(){CancelRecoil();}protected void FixedUpdate(){UpdatePhysics(Time.fixedDeltaTime);}/// <summary>/// 更新游戏对象的物理行为/// </summary>/// <param name="deltaTime"></param>private void UpdatePhysics(float deltaTime){if(state == States.Frozen){if(rb2d != null){rb2d.velocity = Vector2.zero;}recoilTimeRemaining -= deltaTime;if(recoilTimeRemaining <= 0f){CancelRecoil();return;}}else if(state == States.Recoiling){if (isRecoilSweeping){float num;if(recoilSweep.Check(transform.position,recoilSpeed * deltaTime, SweepLayerMask,out num)){isRecoilSweeping = false;}if(num > Mathf.Epsilon){transform.Translate(recoilSweep.Direction * num, Space.World);}}recoilTimeRemaining -= deltaTime;if (recoilTimeRemaining <= 0f){CancelRecoil();}}}/// <summary>/// 在某个方向上受后坐力的行为/// </summary>/// <param name="attackDirection"></param>/// <param name="attackMagnitude"></param>public void RecoilByDirection(int attackDirection,float attackMagnitude){if(state != States.Ready){return;}if (freezeInPlace){Freeze();return;}if(attackDirection == 1&& preventRecoilUp){return;}if (bodyCollider == null){bodyCollider = GetComponent<Collider2D>();}state = States.Recoiling;recoilSpeed = recoilSpeedBase * attackMagnitude;recoilSweep = new Sweep(bodyCollider, attackDirection, 3, 0.1f);isRecoilSweeping = true;recoilTimeRemaining = recoilDuration;switch (attackDirection){case 0:FSMUtility.SendEventToGameObject(gameObject, "RECOIL HORIZONTAL", false);FSMUtility.SendEventToGameObject(gameObject, "HIT RIGHT", false);break;case 1:FSMUtility.SendEventToGameObject(gameObject, "HIT UP", false);break;case 2:FSMUtility.SendEventToGameObject(gameObject, "RECOIL HORIZONTAL", false);FSMUtility.SendEventToGameObject(gameObject, "HIT LEFT", false);break;case 3:FSMUtility.SendEventToGameObject(gameObject, "HIT DOWN", false);break;}UpdatePhysics(0f);}/// <summary>/// 冻结状态发生的行为/// </summary>private void Freeze(){if (skipFreezingByController){if (OnHandleFreeze != null){OnHandleFreeze();}state = States.Ready;return;}state = States.Frozen;if(rb2d != null){rb2d.velocity = Vector2.zero;}PlayMakerFSM playMakerFSM = PlayMakerFSM.FindFsmOnGameObject(gameObject, "Climber Control");if(playMakerFSM != null){playMakerFSM.SendEvent("FREEZE IN PLACE");}recoilTimeRemaining = recoilDuration;UpdatePhysics(0f);}public void CancelRecoil(){if(state != States.Ready){state = States.Ready;if (OnCancelRecoil != null){OnCancelRecoil();}}}public void SetRecoilSpeed(float newSpeed){recoilSpeedBase = newSpeed;}private enum States{Ready,Frozen,Recoiling}
}

还记得我们制作敌人行为(我记得是Walker.cs)的时候用到的Sweep结构体,当时被用于与地面的碰撞检测,这一次我们还是要用到它,这次我再把代码贴出来吧:

using System;
using UnityEngine;public struct Sweep
{public int CardinalDirection;//基数(1-9的数字)的方向public Vector2 Direction;public Vector2 ColliderOffset;public Vector2 ColliderExtents;public float SkinThickness;public int RayCount;public const float DefaultSkinThickness = 0.1f;public const int DefaultRayCount = 3;public Sweep(Collider2D collider, int cardinalDirection, int rayCount, float skinThickness = DefaultSkinThickness){CardinalDirection = cardinalDirection;Direction = new Vector2(DirectionUtils.GetX(cardinalDirection), DirectionUtils.GetY(cardinalDirection));ColliderOffset = collider.offset.MultiplyElements(collider.transform.localScale);ColliderExtents = collider.bounds.extents;RayCount = rayCount;SkinThickness = skinThickness;}public bool Check(Vector2 offset, float distance, int layerMask){float num;return Check(offset, distance, layerMask, out num);}public bool Check(Vector2 offset, float distance, int layerMask, out float clippedDistance){if (distance <= 0f){clippedDistance = 0f;return false;}Vector2 a = ColliderOffset + Vector2.Scale(ColliderExtents, Direction);Vector2 a2 = Vector2.Scale(ColliderExtents, new Vector2(Mathf.Abs(Direction.y), Mathf.Abs(Direction.x)));float num = distance;for (int i = 0; i < RayCount; i++){float d = 2f * ((float)i / (float)(RayCount - 1)) - 1f;Vector2 b = a + a2 * d + Direction * -SkinThickness;Vector2 vector = offset + b;RaycastHit2D hit = Physics2D.Raycast(vector, Direction, num + SkinThickness, layerMask);float num2 = hit.distance - SkinThickness;if (hit && num2 < num){num = num2;Debug.DrawLine(vector, vector + Direction * hit.distance, Color.red);}else{Debug.DrawLine(vector, vector + Direction * (distance + SkinThickness), Color.green);}}clippedDistance = num;return distance - num > Mathf.Epsilon;}
}

2.为敌人脚本添加后坐力行为

然后回到我们之间创作的代码,为敌人脚本添加上后坐力的系统,这里先从Climber.cs开始,在Start()函数中:我们订阅这个事件并实现这个事件Stun:速度设置为0,播放Stun动画后回到Walk()状态,

Recoil component = GetComponent<Recoil>();
    if (component)
    {
        component.SkipFreezingByController = true;
        component.OnHandleFreeze += Stun;
    }

 public void Stun()
    {
    if(turnRoutine == null)
    {
        StopAllCoroutines();
        StartCoroutine(DoStun());
    }
    }

    private IEnumerator DoStun()
    {
    body.velocity = Vector2.zero;
    yield return StartCoroutine(anim.PlayAnimWait("Stun"));
    StartCoroutine(Walk());
    }

完整的代码如下:

using System;
using System.Collections;
using UnityEngine;public class Climber : MonoBehaviour
{private tk2dSpriteAnimator anim;private Rigidbody2D body;private BoxCollider2D col;public bool startRight; //开始的方向是右边private bool clockwise; //是否顺时针旋转public float speed;//移动速度public float spinTime; //旋转时间[Space]public float wallRayPadding; //墙壁射线检测距离[Space]public Vector2 constrain; //束缚public float minTurnDistance; //最小转向距离private Vector2 previousPos;private Vector2 previousTurnPos;[SerializeField]private Direction currentDirection; //Debug用,发现没问题可以删了[SerializeField]private Coroutine turnRoutine; //给转向设置为协程,循序渐进的实现转身的效果public Climber(){startRight = true;clockwise = true;speed = 2f;spinTime = 0.25f;wallRayPadding = 0.1f;constrain = new Vector2(0.1f, 0.1f);minTurnDistance = 0.25f;}private void Awake(){//公式化三件套anim = GetComponent<tk2dSpriteAnimator>();body = GetComponent<Rigidbody2D>();col = GetComponent<BoxCollider2D>();}private void Start(){StickToGround();float num = Mathf.Sign(transform.localScale.x);if (!startRight){num *= -1f;}clockwise = num > 0f; //判断是顺时针还是逆时针float num2 = transform.eulerAngles.z % 360f;//获取开始游戏时climber当前方向if(num2 > 45f && num2 <= 135f){currentDirection = clockwise ? Direction.Up : Direction.Down;}else if(num2 > 135f && num2 <= 225f){currentDirection = clockwise ? Direction.Left : Direction.Right;}else if (num2 > 225f && num2 <= 315f){currentDirection = clockwise ? Direction.Down : Direction.Up;}else{currentDirection = clockwise ? Direction.Right : Direction.Left;}Recoil component = GetComponent<Recoil>();if (component){component.SkipFreezingByController = true;component.OnHandleFreeze += Stun;}previousPos = transform.position;StartCoroutine(Walk());}private IEnumerator Walk(){anim.Play("Walk");body.velocity = GetVelocity(currentDirection);for(; ; ){Vector2 vector = transform.position;bool flag = false;if(Mathf.Abs(vector.x - previousPos.x) > constrain.x){vector.x = previousPos.x;flag = true;}if (Mathf.Abs(vector.y - previousPos.y) > constrain.y){vector.y = previousPos.y;flag = true;}if(flag){transform.position = vector;}else{previousPos = transform.position;}if (Vector3.Distance(previousTurnPos, transform.position) >= minTurnDistance){if (!CheckGround()){turnRoutine = StartCoroutine(Turn(clockwise, false));yield return turnRoutine;}else if (CheckWall()) //当不在地面上以及碰到墙壁后挂机并执行Turn协程{turnRoutine = StartCoroutine(Turn(!clockwise, true));yield return turnRoutine;}}yield return null;}}private IEnumerator Turn(bool turnClockwise, bool tweenPos = false){body.velocity = Vector2.zero;float currentRotation = transform.eulerAngles.z;float targetRotation = currentRotation + (turnClockwise ? -90 : 90);Vector3 currentPosition = transform.position;Vector3 targetPosition = currentPosition + GetTweenPos(currentDirection);for (float elapsed = 0f; elapsed < spinTime; elapsed += Time.deltaTime){float t = elapsed / spinTime;transform.SetRotation2D(Mathf.Lerp(currentRotation, targetRotation, t)); //更改rotation和positionif (tweenPos){transform.position = Vector3.Lerp(currentPosition, targetPosition, t);}yield return null;}transform.SetRotation2D(targetRotation);int num = (int)currentDirection;num += (turnClockwise ? 1 : -1);int num2 = Enum.GetNames(typeof(Direction)).Length; //4//防止数字超出枚举长度或者小于0if(num < 0){num = num2 - 1;}else if(num >= num2){num = 0;}currentDirection = (Direction)num;body.velocity = GetVelocity(currentDirection);previousPos = transform.position;previousTurnPos = previousPos;turnRoutine = null;}/// <summary>/// 不同方向上赋值的速度不同/// </summary>/// <param name="direction"></param>/// <returns></returns>private Vector2 GetVelocity(Direction direction){Vector2 zero = Vector2.zero;switch (direction){case Direction.Right:zero = new Vector2(speed, 0f);break;case Direction.Down:zero = new Vector2(0f, -speed);break;case Direction.Left:zero = new Vector2(-speed, 0f);break;case Direction.Up:zero = new Vector2(0f, speed);break;}return zero;}private bool CheckGround(){return FireRayLocal(Vector2.down, 1f).collider != null;}private bool CheckWall(){return FireRayLocal(clockwise ? Vector2.right : Vector2.left, col.size.x / 2f + wallRayPadding).collider != null;}/// <summary>/// 以后做到人物攻击时才要用到/// </summary>public void Stun(){if(turnRoutine == null){StopAllCoroutines();StartCoroutine(DoStun());}}private IEnumerator DoStun(){body.velocity = Vector2.zero;yield return StartCoroutine(anim.PlayAnimWait("Stun"));StartCoroutine(Walk());}private RaycastHit2D FireRayLocal(Vector2 direction, float length){Vector2 vector = transform.TransformPoint(col.offset);Vector2 vector2 = transform.TransformDirection(direction);RaycastHit2D result = Physics2D.Raycast(vector, vector2, length, LayerMask.GetMask("Terrain"));Debug.DrawRay(vector, vector2);return result;}private Vector3 GetTweenPos(Direction direction){Vector2 result = Vector2.zero;switch (direction){case Direction.Right:result = (clockwise ? new Vector2(col.size.x / 2f, col.size.y / 2f) : new Vector2(col.size.x / 2f, -(col.size.y / 2f)));result.x += wallRayPadding;break;case Direction.Down:result = (clockwise ? new Vector2(col.size.x / 2f, -(col.size.y / 2f)) : new Vector2(-(col.size.x / 2f), -(col.size.y / 2f)));result.y -= wallRayPadding;break;case Direction.Left:result = (clockwise ? new Vector2(-(col.size.x / 2f), -(col.size.y / 2f)) : new Vector2(-(col.size.x / 2f), col.size.y / 2f));result.x -= wallRayPadding;break;case Direction.Up:result = (clockwise ? new Vector2(-(col.size.x / 2f), col.size.y / 2f) : new Vector2(col.size.x / 2f, col.size.y / 2f));result.y += wallRayPadding;break;}return result;}/// <summary>/// 在开始游戏时让它粘在离它向下射线2f最近的地面。/// </summary>private void StickToGround(){RaycastHit2D raycastHit2D = FireRayLocal(Vector2.down, 2f);if(raycastHit2D.collider != null){transform.position = raycastHit2D.point;}}private enum Direction{Right,Down,Left,Up}
}

然后回到Crawler.cs代码中:

  private Recoil recoil;

recoil = GetComponent<Recoil>();

在Start()函数中订阅OnCancelRecoil事件,当取消后坐力后让速度回到原始的速度,通过类型判断是否应该开启freezeInPlace:

recoil.SetRecoilSpeed(0f);
    recoil.OnCancelRecoil += delegate()
    {
        body.velocity = velocity;
    };
    CrawlerType crawlerType = type;
    if(crawlerType != CrawlerType.Floor)
    {
        if(crawlerType - CrawlerType.Roof <= 1)
        {
        body.gravityScale = 0;//如果在墙面面上rb2d的重力就设置为1
        recoil.freezeInPlace = true;         
        }
    }
    else
    {
        body.gravityScale = 1; //如果在地面上rb2d的重力就设置为1
        recoil.freezeInPlace = false;
    }

完整的代码如下:

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Crawler : MonoBehaviour
{public float speed;[Space]public Transform wallCheck; //墙面检测的位置public Transform groundCheck; //地面检测的位置private Vector2 velocity; //记录速度private CrawlerType type;private Rigidbody2D body;private Recoil recoil;private tk2dSpriteAnimator anim;private void Awake(){body = GetComponent<Rigidbody2D>();recoil = GetComponent<Recoil>();anim = GetComponent<tk2dSpriteAnimator>();}private void Start(){float z = transform.eulerAngles.z;//通过transform.eulerAngles.z来判断哪种类型的Crawlerif (z >= 45f && z <= 135f){type = CrawlerType.Wall;velocity = new Vector2(0f, Mathf.Sign(-transform.localScale.x) * speed);}else if (z >= 135f && z <= 225f){type = ((transform.localScale.y > 0f) ? CrawlerType.Roof : CrawlerType.Floor);velocity = new Vector2(Mathf.Sign(transform.localScale.x) * speed, 0f);}else if (z >= 225f && z <= 315f){type = CrawlerType.Wall;velocity = new Vector2(0f, Mathf.Sign(transform.localScale.x) * speed);}else{type = ((transform.localScale.y > 0f) ? CrawlerType.Floor : CrawlerType.Roof);velocity = new Vector2(Mathf.Sign(-transform.localScale.x) * speed, 0f);}recoil.SetRecoilSpeed(0f);recoil.OnCancelRecoil += delegate(){body.velocity = velocity;};CrawlerType crawlerType = type;if(crawlerType != CrawlerType.Floor){if(crawlerType - CrawlerType.Roof <= 1){body.gravityScale = 0;//如果在墙面面上rb2d的重力就设置为1recoil.freezeInPlace = true;	     }}else{body.gravityScale = 1; //如果在地面上rb2d的重力就设置为1recoil.freezeInPlace = false;}StartCoroutine(nameof(Walk));}/// <summary>/// 使用协程实现Walk函数,循环直至hit=true后挂起然后启用协程Turn()/// </summary>/// <returns></returns>private IEnumerator Walk(){for(; ; ){anim.Play("Walk");body.velocity = velocity;bool hit = false;while (!hit){if(CheckRayLocal(wallCheck.localPosition,(transform.localScale.x > 0f )? Vector2.left : Vector2.right, 1f)){hit = true;break;}if (CheckRayLocal(groundCheck.localPosition, (transform.localScale.y > 0f) ? Vector2.down : Vector2.up, 1f)){hit = true;break;}yield return null;}yield return StartCoroutine(Turn());yield return null;}}/// <summary>/// 使用协程实现转向函数/// </summary>/// <returns></returns>private IEnumerator Turn(){body.velocity = Vector2.zero;yield return StartCoroutine(anim.PlayAnimWait("Turn"));transform.SetScaleX(transform.localScale.x * -1f);velocity.x = velocity.x * -1f;velocity.y = velocity.y * -1f;}/// <summary>/// 发射射线,检测是否有LayerMask.GetMask("Terrain").collider/// </summary>/// <param name="originLocal"></param>/// <param name="directionLocal"></param>/// <param name="length"></param>/// <returns></returns>public bool CheckRayLocal(Vector3 originLocal, Vector2 directionLocal, float length){Vector2 vector = transform.TransformPoint(originLocal);Vector2 vector2 = transform.TransformDirection(directionLocal);RaycastHit2D raycastHit2D = Physics2D.Raycast(vector, vector2, length, LayerMask.GetMask("Terrain"));Debug.DrawLine(vector, vector + vector2 * length);return raycastHit2D.collider != null;}private enum CrawlerType{Floor,Roof,Wall}
}

3.为小骑士添加后坐力行为  

    private int recoilSteps; 
    private float recoilTimer; //后坐力计时器
    private bool recoilLarge; //是否是更大的后坐力
    private Vector2 recoilVector; //后坐力二维上的速度

    public float RECOIL_HOR_VELOCITY; //后坐力X轴上的速度
    public float RECOIL_HOR_VELOCITY_LONG; //后坐力X轴上更大的速度
    public float RECOIL_DOWN_VELOCITY; //后坐力Y轴上的速度
    public float RECOIL_HOR_STEPS; //后坐力X轴的步
    public float RECOIL_DURATION; //后坐力持续时间
    public float RECOIL_VELOCITY; //后坐力时的速度(是两个轴上都适用的)

在HeroControllerState.cs中创建新的状态:

    public bool recoilFrozen;
    public bool recoiling;
    public bool recoilingLeft;
    public bool recoilingRight;

[Serializable]
public class HeroControllerStates
{public bool facingRight;public bool onGround;public bool wasOnGround;public bool attacking;public bool altAttack;public bool upAttacking;public bool downAttacking;public bool inWalkZone;public bool jumping;public bool falling;public bool dashing;public bool backDashing;public bool touchingWall;public bool wallSliding;public bool willHardLand;public bool recoilFrozen;public bool recoiling;public bool recoilingLeft;public bool recoilingRight;public bool dead;public bool hazardDeath;public bool invulnerable;public bool preventDash;public bool preventBackDash;public bool dashCooldown;public bool backDashCooldown;public bool isPaused;public HeroControllerStates(){facingRight = false;onGround = false;wasOnGround = false;attacking = false;altAttack = false;upAttacking = false;downAttacking = false;inWalkZone = false;jumping = false;falling = false;dashing = false;backDashing = false;touchingWall = false;wallSliding = false;willHardLand = false;recoilFrozen = false;recoiling = false;recoilingLeft = false;recoilingRight = false;dead = false;hazardDeath = false;invulnerable = false;preventDash = false;preventBackDash = false;dashCooldown = false;backDashCooldown = false;isPaused = false;}
}

在Update()函数中,如果hero_state == ActorStates.no_input,通过后坐力状态来切换是否要关闭后坐力行为,如果有输入了就取消进入recoiling状态

if (hero_state == ActorStates.no_input){if (cState.recoiling){if (recoilTimer < RECOIL_DURATION){recoilTimer += Time.deltaTime;}else{CancelDamageRecoil();if ((prev_hero_state == ActorStates.idle || prev_hero_state == ActorStates.running) && !CheckTouchingGround()){cState.onGround = false;SetState(ActorStates.airborne);}else{SetState(ActorStates.previous);}}}}
else if (hero_state != ActorStates.no_input){LookForInput();if (cState.recoiling){cState.recoiling = false;AffectedByGravity(true);}if(cState.attacking && !cState.dashing){attack_time += Time.deltaTime;if(attack_time >= attackDuration){ResetAttacks();animCtrl.StopAttack();}}}

 在FixedUpdate()中如果recoil步骤到达后就取消X轴上的后坐力CancelRecoilHorizonal:

    if(cState.recoilingLeft || cState.recoilingRight)
    {
            if(recoilSteps <= RECOIL_HOR_STEPS)
        {
                recoilSteps++;
        }
        else
        {
                CancelRecoilHorizonal();
        }
    }

还有判断后坐力时左边还是右边,以及是否开启recoilLarge来赋予 rb2d.velocity不同的速度:

 if(cState.recoilingLeft)
        {
                    float num;
                    if (recoilLarge)
                    {
                        num = RECOIL_HOR_VELOCITY_LONG;
                    }
                    else
                    {
                        num = RECOIL_HOR_VELOCITY;
                    }
                    if(rb2d.velocity.x > -num)
            {
                        rb2d.velocity = new Vector2(-num, rb2d.velocity.y);
            }
            else
            {
                        rb2d.velocity = new Vector2(rb2d.velocity.x - num, rb2d.velocity.y);
            }
        }
         if (cState.recoilingRight)
                {
                    float num2;
                    if(recoilLarge)
            {
                        num2 = RECOIL_HOR_VELOCITY_LONG;
            }
                    else
            {
                        num2 = RECOIL_HOR_VELOCITY;
                    }
                    if (rb2d.velocity.x < num2)
                    {
                        rb2d.velocity = new Vector2(num2, rb2d.velocity.y);
                    }
                    else
                    {
                        rb2d.velocity = new Vector2(rb2d.velocity.x + num2, rb2d.velocity.y);
                    }
                }

还需要添加几个新的后坐力函数:

public void RecoilLeft(){if(!cState.recoilingLeft && !cState.recoilingRight){CancelDash();recoilSteps = 0;cState.recoilingLeft = true;cState.recoilingRight = false;recoilLarge = false;rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY, rb2d.velocity.y);}}public void RecoilRight(){if (!cState.recoilingLeft && !cState.recoilingRight){CancelDash();recoilSteps = 0;cState.recoilingLeft = false;cState.recoilingRight = true;recoilLarge = false;rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY, rb2d.velocity.y);}}public void RecoilLeftLong(){if (!cState.recoilingLeft && !cState.recoilingRight){CancelDash();ResetAttacks();recoilSteps = 0;cState.recoilingLeft = true;cState.recoilingRight = false;recoilLarge = true;rb2d.velocity = new Vector2(-RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);}}public void RecoilRightLong(){if (!cState.recoilingLeft && !cState.recoilingRight){CancelDash();ResetAttacks();recoilSteps = 0;cState.recoilingLeft = false;cState.recoilingRight = true;recoilLarge = true;rb2d.velocity = new Vector2(RECOIL_HOR_VELOCITY_LONG, rb2d.velocity.y);}}public void RecoilDown(){CancelJump();if(rb2d.velocity.y > RECOIL_DOWN_VELOCITY){rb2d.velocity = new Vector2(rb2d.velocity.x, RECOIL_DOWN_VELOCITY);}}public void CancelRecoilHorizonal(){cState.recoilingLeft = false;cState.recoilingRight = false;recoilSteps = 0;}

这里我为什么不贴完整的代码呢?因为其实小骑士的后坐力系统还和受伤系统有关系,所以我打算放到下面再完整的后坐力系统。

二、制作小骑士的生命系统和受伤系统

1.制作动画以及使用UNITY编辑器编辑

我们来个小骑士制作受伤动画和死亡动画和冻结动画吧。

老规矩,还是只添加偶数帧的sprite上去:这个是受伤Recoil动画

 这个是死亡Death动画:

最后是冻结Stun动画,是的它就一帧:

2.使用代码制作生命系统和受伤系统

制作完动画后当然要来到我们最爱的HeroAnimationController.cs中:

if (actorStates == ActorStates.no_input)
    {
        if (cState.recoilFrozen)
        {
        Play("Stun");
        }
        else if (cState.recoiling)
        {
        Play("Recoil");
        }
    }

using System;
using GlobalEnums;
using UnityEngine;public class HeroAnimationController : MonoBehaviour
{private HeroController heroCtrl;private HeroControllerStates cState;private tk2dSpriteAnimator animator;private PlayerData pd;private bool wasFacingRight;private bool playLanding;private bool playRunToIdle;//播放"Run To Idle"动画片段private bool playDashToIdle; //播放"Dash To Idle"动画片段private bool playBackDashToIdleEnd; //播放"Back Dash To Idle"动画片段(其实并不会播放)private bool changedClipFromLastFrame;public ActorStates actorStates { get; private set; }public ActorStates prevActorStates { get; private set; }private void Awake(){heroCtrl = HeroController.instance;cState = heroCtrl.cState;animator = GetComponent<tk2dSpriteAnimator>();}private void Start(){pd = PlayerData.instance;ResetAll();actorStates = heroCtrl.hero_state;if(heroCtrl.hero_state == ActorStates.airborne){animator.PlayFromFrame("Airborne", 7);return;}PlayIdle();}private void Update(){UpdateAnimation();if (cState.facingRight){wasFacingRight = true;return;}wasFacingRight = false;}private void UpdateAnimation(){changedClipFromLastFrame = false;if (playLanding){Play("Land");animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);playLanding = false;}if (playRunToIdle){Play("Run To Idle");animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);playRunToIdle = false;}if (playBackDashToIdleEnd){Play("Backdash Land 2");//处理animation播放完成后的事件(其实并不会播放)animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);playDashToIdle = false;}if (playDashToIdle){Play("Dash To Idle");//处理animation播放完成后的事件animator.AnimationCompleted = new Action<tk2dSpriteAnimator, tk2dSpriteAnimationClip>(AnimationCompleteDelegate);playDashToIdle = false;}if (actorStates == ActorStates.no_input){//TODO:if (cState.recoilFrozen){Play("Stun");}else if (cState.recoiling){Play("Recoil");}}else if (cState.dashing){if (heroCtrl.dashingDown){Play("Dash Down");}else{Play("Dash"); //通过cState.dashing判断是否播放Dash动画片段}}else if (cState.backDashing){Play("Back Dash");}else if(cState.attacking){if (cState.upAttacking){Play("UpSlash");}else if (cState.downAttacking){Play("DownSlash");}else if (!cState.altAttack){Play("Slash");}else{Play("SlashAlt");}}else if (actorStates == ActorStates.idle){//TODO:if (CanPlayIdle()){PlayIdle();}}else if (actorStates == ActorStates.running){if (!animator.IsPlaying("Turn")){if (cState.inWalkZone){if (!animator.IsPlaying("Walk")){Play("Walk");}}else{PlayRun();}}}else if (actorStates == ActorStates.airborne){if (cState.jumping){if (!animator.IsPlaying("Airborne")){animator.PlayFromFrame("Airborne", 0);}}else if (cState.falling){if (!animator.IsPlaying("Airborne")){animator.PlayFromFrame("Airborne", 7);}}else if (!animator.IsPlaying("Airborne")){animator.PlayFromFrame("Airborne", 3);}}//(其实并不会播放)else if (actorStates == ActorStates.dash_landing){animator.Play("Dash Down Land");}else if(actorStates == ActorStates.hard_landing){animator.Play("HardLand");}if (cState.facingRight){if(!wasFacingRight && cState.onGround && CanPlayTurn()){Play("Turn");}wasFacingRight = true;}else{if (wasFacingRight && cState.onGround && CanPlayTurn()){Play("Turn");}wasFacingRight = false;}ResetPlays();}private void AnimationCompleteDelegate(tk2dSpriteAnimator anim, tk2dSpriteAnimationClip clip){if(clip.name == "Land"){PlayIdle();}if(clip.name == "Run To Idle"){PlayIdle();}if(clip.name == "Backdash To Idle")//(其实并不会播放){PlayIdle();}if(clip.name == "Dash To Idle"){PlayIdle();}}private void Play(string clipName){if(clipName != animator.CurrentClip.name){changedClipFromLastFrame = true;}animator.Play(clipName);}private void PlayRun(){animator.Play("Run");}public void PlayIdle(){animator.Play("Idle");}public void StopAttack(){if(animator.IsPlaying("UpSlash") || animator.IsPlaying("DownSlash")){animator.Stop();}}public void FinishedDash(){playDashToIdle = true;}private void ResetAll(){playLanding = false;playRunToIdle = false;playDashToIdle = false;wasFacingRight = false;}private void ResetPlays(){playLanding = false;playRunToIdle = false;playDashToIdle = false;}public void UpdateState(ActorStates newState){if(newState != actorStates){if(actorStates == ActorStates.airborne && newState == ActorStates.idle && !playLanding){playLanding = true;}if(actorStates == ActorStates.running && newState == ActorStates.idle && !playRunToIdle && !cState.inWalkZone){playRunToIdle = true;}prevActorStates = actorStates;actorStates = newState;}}private bool CanPlayIdle(){return !animator.IsPlaying("Land") && !animator.IsPlaying("Run To Idle") && !animator.IsPlaying("Dash To Idle") && !animator.IsPlaying("Backdash Land") && !animator.IsPlaying("Backdash Land 2") && !animator.IsPlaying("LookUpEnd") && !animator.IsPlaying("LookDownEnd") && !animator.IsPlaying("Exit Door To Idle") && !animator.IsPlaying("Wake Up Ground") && !animator.IsPlaying("Hazard Respawn");}private bool CanPlayTurn(){return !animator.IsPlaying("Wake Up Ground") && !animator.IsPlaying("Hazard Respawn"); ;}}

然后到HeroAudioController.cs添加一个新的AudioSource:

public AudioSource takeHit;

case HeroSounds.TAKE_HIT:
            takeHit.Play();
            break;

using System.Collections;
using System.Collections.Generic;
using GlobalEnums;
using UnityEngine;public class HeroAudioController : MonoBehaviour
{private HeroController heroCtrl;private void Awake(){heroCtrl = GetComponent<HeroController>();}[Header("Sound Effects")]public AudioSource softLanding;public AudioSource hardLanding;public AudioSource jump;public AudioSource footStepsRun;public AudioSource footStepsWalk;public AudioSource falling;public AudioSource backDash;public AudioSource dash;public AudioSource takeHit;private Coroutine fallingCo;public void PlaySound(HeroSounds soundEffect){if(!heroCtrl.cState.isPaused){switch (soundEffect){case HeroSounds.FOOTSETP_RUN:if(!footStepsRun.isPlaying && !softLanding.isPlaying){footStepsRun.Play();return;}break;case HeroSounds.FOOTSTEP_WALK:if (!footStepsWalk.isPlaying && !softLanding.isPlaying){footStepsWalk.Play();return;}break;case HeroSounds.SOFT_LANDING:RandomizePitch(softLanding, 0.9f, 1.1f);softLanding.Play();break;case HeroSounds.HARD_LANDING:hardLanding.Play();break;case HeroSounds.JUMP:RandomizePitch(jump, 0.9f, 1.1f);jump.Play();break;case HeroSounds.BACK_DASH:backDash.Play();break;case HeroSounds.DASH:dash.Play();break;case HeroSounds.TAKE_HIT:takeHit.Play();break;case HeroSounds.FALLING:fallingCo = StartCoroutine(FadeInVolume(falling, 0.7f));falling.Play();break;default:break;}}}public void StopSound(HeroSounds soundEffect){if(soundEffect == HeroSounds.FOOTSETP_RUN){footStepsRun.Stop();return;}if (soundEffect == HeroSounds.FOOTSTEP_WALK){footStepsWalk.Stop();return;}switch (soundEffect){case HeroSounds.FALLING:falling.Stop();if(fallingCo != null){StopCoroutine(fallingCo);}return;default:return;}}public void StopAllSounds(){softLanding.Stop();hardLanding.Stop();jump.Stop();falling.Stop();backDash.Stop();dash.Stop();footStepsRun.Stop();footStepsWalk.Stop();}public void PauseAllSounds(){softLanding.Pause();hardLanding.Pause();jump.Pause();falling.Pause();backDash.Pause();dash.Pause();footStepsRun.Pause();footStepsWalk.Pause();}public void UnPauseAllSounds(){softLanding.UnPause();hardLanding.UnPause();jump.UnPause();falling.UnPause();backDash.UnPause();dash.UnPause();footStepsRun.UnPause();footStepsWalk.UnPause();}/// <summary>/// 音量淡入线性插值的从0到1/// </summary>/// <param name="src"></param>/// <param name="duration"></param>/// <returns></returns>private IEnumerator FadeInVolume(AudioSource src, float duration){float elapsedTime = 0f;src.volume = 0f;while (elapsedTime < duration){elapsedTime += Time.deltaTime;float t = elapsedTime / duration;src.volume = Mathf.Lerp(0f, 1f, t);yield return null;}}/// <summary>/// 随机旋转一个在和之间的pitch的值返回给audiosource/// </summary>/// <param name="src"></param>/// <param name="minPitch"></param>/// <param name="maxPitch"></param>private void RandomizePitch(AudioSource src, float minPitch, float maxPitch){float pitch = Random.Range(minPitch, maxPitch);src.pitch = pitch;}/// <summary>/// 重置audiosource的pitch/// </summary>/// <param name="src"></param>private void ResetPitch(AudioSource src){src.pitch = 1f;}}

 如果报错了自然是在GlobalEnums中我们没有添加这个新的herosounds数组:

using System;namespace GlobalEnums
{public enum ActorStates{grounded,idle,running,airborne,wall_sliding,hard_landing,dash_landing,no_input,previous}public enum AttackDirection{normal,upward,downward}public enum CollisionSide{top,left,right,bottom,other}public enum DamageMode{FULL_DAMAGE,HAZARD_ONLY,NO_DAMAGE}public enum HazardTypes{NON_HAZARD,SPIKES,ACID,LAVA,PIT}public enum HeroSounds{FOOTSETP_RUN,FOOTSTEP_WALK,SOFT_LANDING,HARD_LANDING,JUMP,BACK_DASH,DASH,FALLING,TAKE_HIT}public enum PhysLayers{DEFAULT,IGNORE_RAYCAST = 2,WATER = 4,UI,TERRAIN = 8,PLAYER,TRANSITION_GATES,ENEMIES,PROJECTILES,HERO_DETECTOR,TERRAIN_DETECTOR,ENEMY_DETECTOR,ITEM,HERO_ATTACK,PARTICLE,INTERACTIVE_OBJECT,HERO_BOX,BOUNCER = 24,SOFT_TERRAIN = 25}
}

 回到Unity编辑器中,在小骑士对应的脚本添加完新的AudioSource后,我们还要创建一个新的Layer就叫Hero Box,它用于专门处理和可交互物体(特别是敌人Enemy)的碰撞检测。

为小骑士新建一个子对象就叫HeroBox,把刚刚创建的Layer放上去,然后设置你觉得合适的碰撞大小:

创建一个同名脚本HeroBox.cs:

using System;
using GlobalEnums;
using UnityEngine;public class HeroBox : MonoBehaviour
{public static bool inactive;private HeroController heroCtrl;private GameObject damagingObject;private bool isHitBuffered;private int damageDealt;private int hazardType;private CollisionSide collisionSide;private void Start(){heroCtrl = HeroController.instance;}private void LateUpdate(){if (isHitBuffered){ApplyBufferedHit();}}private void OnTriggerEnter2D(Collider2D otherCollider){if (!inactive){CheckForDamage(otherCollider);}}private void OnTriggerStay2D(Collider2D otherCollider){if (!inactive){CheckForDamage(otherCollider);}}/// <summary>/// 通过两种方法检测受到伤害的方法/// 一种是通过otherCollider.gameObject中是否有一个名字叫"damages_hero"的playmakerFSM/// 另一种是通过otherCollider.gameObject是否有个叫DamageHero的脚本/// </summary>/// <param name="otherCollider"></param>private void CheckForDamage(Collider2D otherCollider){if (!FSMUtility.ContainsFSM(otherCollider.gameObject, "damages_hero")){DamageHero component = otherCollider.gameObject.GetComponent<DamageHero>();if (component != null){damageDealt = component.damageDealt;hazardType = component.hazardType;damagingObject = otherCollider.gameObject;collisionSide = ((damagingObject.transform.position.x > transform.position.x) ? CollisionSide.right : CollisionSide.left);if (!IsHitTypeBuffered(hazardType)){ApplyBufferedHit();return;}isHitBuffered = true;}return;}PlayMakerFSM fsm = FSMUtility.LocateFSM(otherCollider.gameObject, "damages_hero");int dealt = FSMUtility.GetInt(fsm, "damageDealt");int type = FSMUtility.GetInt(fsm, "hazardType");if (otherCollider.transform.position.x > transform.position.x){heroCtrl.TakeDamage(otherCollider.gameObject, CollisionSide.right, dealt, type);return;}heroCtrl.TakeDamage(otherCollider.gameObject, CollisionSide.left, dealt, type);}public static bool IsHitTypeBuffered(int hazardType){return hazardType == 0;}/// <summary>/// 应用缓冲后受击,就是执行HeroController的TakeDamage方法/// </summary>private void ApplyBufferedHit(){heroCtrl.TakeDamage(damagingObject, collisionSide, damageDealt, hazardType);isHitBuffered = false;}}

 我们先为每一个敌人添加一个新的脚本叫DamageHero.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class DamageHero : MonoBehaviour
{public int damageDealt = 1;public int hazardType = 1;public bool shadowDashHazard;public bool resetOnEnable;private int? initialValue;private void OnEnable(){if (resetOnEnable){if(initialValue == null){initialValue = new int?(damageDealt);return;}damageDealt = initialValue.Value;}}
}

回到HealthManager.cs中,如果执行Die()函数后就把damageHero里面的damageDealt设置为0

private DamageHero damageHero;

damageHero = GetComponent<DamageHero>();

if(damageHero != null)
    {
        damageHero.damageDealt = 0;
    }

完整的代码如下:

using System;
using System.Collections;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;public class HealthManager : MonoBehaviour, IHitResponder
{private BoxCollider2D boxCollider;private IHitEffectReciever hitEffectReceiver;private Recoil recoil;private tk2dSpriteAnimator animator;private tk2dSprite sprite;private DamageHero damageHero;[Header("Asset")][SerializeField] private AudioSource audioPlayerPrefab; //声音播放器预制体[Header("Body")][SerializeField] public int hp; //血量[SerializeField] public int enemyType; //敌人类型[SerializeField] private Vector3 effectOrigin; //生效偏移量public bool isDead;private int directionOfLastAttack; //最后一次受到攻击的方向private float evasionByHitRemaining; //剩余攻击下的逃避时间private const string CheckPersistenceKey = "CheckPersistence";public delegate void DeathEvent();public event DeathEvent OnDeath;protected void Awake(){boxCollider = GetComponent<BoxCollider2D>();hitEffectReceiver = GetComponent<IHitEffectReciever>();recoil = GetComponent<Recoil>();animator = GetComponent<tk2dSpriteAnimator>();sprite = GetComponent<tk2dSprite>();damageHero = GetComponent<DamageHero>();}protected void OnEnable(){StartCoroutine(CheckPersistenceKey);}protected void Start(){evasionByHitRemaining = -1f;}protected void Update(){evasionByHitRemaining -= Time.deltaTime;}public void Hit(HitInstance hitInstance){if (isDead){return;}if(evasionByHitRemaining > 0f) { return;}if(hitInstance.DamageDealt < 0f){return;}FSMUtility.SendEventToGameObject(hitInstance.Source, "DEALT DAMAGE", false);int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));if (IsBlockingByDirection(cardinalDirection, hitInstance.AttackType)){Invincible(hitInstance);return;}TakeDamage(hitInstance);}private void Invincible(HitInstance hitInstance){int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));directionOfLastAttack = cardinalDirection;FSMUtility.SendEventToGameObject(gameObject, "BLOCKED HIT", false);FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);if (!(GetComponent<DontClinkGates>() != null)){FSMUtility.SendEventToGameObject(gameObject, "HIT", false);if(hitInstance.AttackType == AttackTypes.Nail){if(cardinalDirection == 0){HeroController.instance.RecoilLeft();}else if(cardinalDirection == 2){HeroController.instance.RecoilRight();}}Vector2 v;Vector3 eulerAngles;if (boxCollider != null){switch (cardinalDirection){case 0:v = new Vector2(transform.GetPositionX() + boxCollider.offset.x - boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());eulerAngles = new Vector3(0f, 0f, 0f);break;case 1:v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Max(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y - boxCollider.size.y * 0.5f));eulerAngles = new Vector3(0f, 0f, 90f);break;case 2:v = new Vector2(transform.GetPositionX() + boxCollider.offset.x + boxCollider.size.x * 0.5f, hitInstance.Source.transform.GetPositionY());eulerAngles = new Vector3(0f, 0f, 180f);break;case 3:v = new Vector2(hitInstance.Source.transform.GetPositionX(), Mathf.Min(hitInstance.Source.transform.GetPositionY(), transform.GetPositionY() + boxCollider.offset.y + boxCollider.size.y * 0.5f));eulerAngles = new Vector3(0f, 0f, 270f);break;default:break;}}else{v = transform.position;eulerAngles = new Vector3(0f, 0f, 0f);}}evasionByHitRemaining = 0.15f;}public void TakeDamage(HitInstance hitInstance){Debug.LogFormat("Enemy Take Damage");int cardinalDirection = DirectionUtils.GetCardinalDirection(hitInstance.GetActualDirection(transform));directionOfLastAttack = cardinalDirection;FSMUtility.SendEventToGameObject(gameObject, "HIT", false);FSMUtility.SendEventToGameObject(hitInstance.Source, "HIT LANDED", false);FSMUtility.SendEventToGameObject(gameObject, "TOOK DAMAGE", false);if(recoil != null){recoil.RecoilByDirection(cardinalDirection,hitInstance.MagnitudeMultiplier);}switch (hitInstance.AttackType){case AttackTypes.Nail:if(hitInstance.AttackType == AttackTypes.Nail && enemyType !=3 && enemyType != 6){}Vector3 position = (hitInstance.Source.transform.position + transform.position) * 0.5f + effectOrigin;break;case AttackTypes.Generic:break;default:break;}if(hitEffectReceiver != null){hitEffectReceiver.ReceiverHitEffect(hitInstance.GetActualDirection(transform));}int num = Mathf.RoundToInt((float)hitInstance.DamageDealt * hitInstance.Multiplier);hp = Mathf.Max(hp - num, -50);if(hp > 0){}else{Die(new float?(hitInstance.GetActualDirection(transform)), hitInstance.AttackType, hitInstance.IgnoreInvulnerable);}}public void Die(float? v, AttackTypes attackType, bool ignoreInvulnerable){if (isDead){return;}if (sprite){sprite.color = Color.white;}FSMUtility.SendEventToGameObject(gameObject, "ZERO HP", false);isDead = true;if(damageHero != null){damageHero.damageDealt = 0;}SendDeathEvent();Destroy(gameObject); //TODO:}public void SendDeathEvent(){if (OnDeath != null){OnDeath();}}public bool IsBlockingByDirection(int cardinalDirection,AttackTypes attackType){switch (cardinalDirection){default:return false;}}protected IEnumerator CheckPersistence(){yield return null;if (isDead){gameObject.SetActive(false);}yield break;}}

 然后为FSMUtility添加几个新的静态方法:

using System;
using System.Collections.Generic;
using HutongGames.PlayMaker;
using UnityEngine;public static class FSMUtility
{private static List<List<PlayMakerFSM>> fsmListPool;private const int FsmListPoolSizeMax = 20;static FSMUtility(){fsmListPool = new List<List<PlayMakerFSM>>();}public static PlayMakerFSM LocateFSM(GameObject go, string fsmName){if (go == null){return null;}List<PlayMakerFSM> list = ObtainFsmList();go.GetComponents<PlayMakerFSM>(list);PlayMakerFSM result = null;for (int i = 0; i < list.Count; i++){PlayMakerFSM playMakerFSM = list[i];if (playMakerFSM.FsmName == fsmName){result = playMakerFSM;break;}}ReleaseFsmList(list);return result;}private static List<PlayMakerFSM> ObtainFsmList(){if (fsmListPool.Count > 0){List<PlayMakerFSM> result = fsmListPool[fsmListPool.Count - 1];fsmListPool.RemoveAt(fsmListPool.Count - 1);return result;}return new List<PlayMakerFSM>();}public static bool ContainsFSM(GameObject go, string fsmName){if (go == null){return false;}List<PlayMakerFSM> list = FSMUtility.ObtainFsmList();go.GetComponents<PlayMakerFSM>(list);bool result = false;for (int i = 0; i < list.Count; i++){if (list[i].FsmName == fsmName){result = true;break;}}FSMUtility.ReleaseFsmList(list);return result;}public static int GetInt(PlayMakerFSM fsm, string variableName){return fsm.FsmVariables.FindFsmInt(variableName).Value;}private static void ReleaseFsmList(List<PlayMakerFSM> fsmList){fsmList.Clear();if (fsmListPool.Count < FsmListPoolSizeMax){fsmListPool.Add(fsmList);}}public static PlayMakerFSM GetFSM(GameObject go){return go.GetComponent<PlayMakerFSM>();}public static GameObject GetSafe(this FsmOwnerDefault ownerDefault, FsmStateAction stateAction){if (ownerDefault.OwnerOption == OwnerDefaultOption.UseOwner){return stateAction.Owner;}return ownerDefault.GameObject.Value;}public static void SendEventToGameObject(GameObject go, string eventName, bool isRecursive = false){if (go != null){SendEventToGameObject(go, FsmEvent.FindEvent(eventName), isRecursive);}}public static void SendEventToGameObject(GameObject go, FsmEvent ev, bool isRecursive = false){if (go != null){List<PlayMakerFSM> list = ObtainFsmList();go.GetComponents<PlayMakerFSM>(list);for (int i = 0; i < list.Count; i++){list[i].Fsm.Event(ev);}ReleaseFsmList(list);if (isRecursive){Transform transform = go.transform;for (int j = 0; j < transform.childCount; j++){SendEventToGameObject(transform.GetChild(j).gameObject, ev, isRecursive);}}}}}

回到HeroController.cs处理报错的部分: 我们新生成一个方法就叫做TakeDamage():

private bool CanTakeDamage(){return damageMode != DamageMode.NO_DAMAGE && !cState.invulnerable && !cState.recoiling && !cState.dead;}public void TakeDamage(GameObject go,CollisionSide damageSide,int damageAmount,int hazardType){bool spawnDamageEffect = true;if (damageAmount > 0){if (CanTakeDamage()){if (damageMode == DamageMode.HAZARD_ONLY && hazardType == 1){return;}if (parryInvulnTimer > 0f && hazardType == 1){return;}proxyFSM.SendEvent("HeroCtrl-HeroDamaged");CancelAttack();if (cState.touchingWall){cState.touchingWall = false;}if (cState.recoilingLeft || cState.recoilingRight){CancelRecoilHorizonal();}audioCtrl.PlaySound(HeroSounds.TAKE_HIT);if (!takeNoDamage){playerData.TakeHealth(damageAmount);}if (damageAmount > 0 && OnTakenDamage != null){OnTakenDamage();}if (playerData.health == 0){StartCoroutine(Die());return;}if (hazardType == 2){Debug.LogFormat("Die From Spikes");return;}if (hazardType == 3){Debug.LogFormat("Die From Acid");return;}if (hazardType == 4){Debug.LogFormat("Die From Lava");return;}if (hazardType == 5){Debug.LogFormat("Die From Pit");return;}StartCoroutine(StartRecoil(damageSide, spawnDamageEffect, damageAmount));return;}else if (cState.invulnerable && !cState.hazardDeath){if(hazardType == 2){if (!takeNoDamage){playerData.TakeHealth(damageAmount);}proxyFSM.SendEvent("HeroCtrl-HeroDamaged");if(playerData.health == 0){StartCoroutine(Die());return;}audioCtrl.PlaySound(HeroSounds.TAKE_HIT);StartCoroutine(DieFromHazard(HazardTypes.SPIKES, (go != null) ? go.transform.rotation.z : 0f));return;}else if (hazardType == 3){             playerData.TakeHealth(damageAmount);proxyFSM.SendEvent("HeroCtrl-HeroDamaged");if (playerData.health == 0){StartCoroutine(Die());return;}audioCtrl.PlaySound(HeroSounds.TAKE_HIT);StartCoroutine(DieFromHazard(HazardTypes.ACID, 0f));return;}else if(hazardType == 4){Debug.LogFormat("Die From Lava");}}}}

我们还要一些新的受伤事件和变量要做:

    public delegate void TakeDamageEvent();
    public event TakeDamageEvent OnTakenDamage;
    public delegate void OnDeathEvent();
    public event OnDeathEvent OnDeath;

    public bool takeNoDamage; //不受到伤害
    public PlayMakerFSM damageEffectFSM; //负责的受伤效果playmakerFSM
    public DamageMode damageMode; //受伤类型
    private Coroutine takeDamageCoroutine; //受伤协程
    private float parryInvulnTimer;  //无敌时间
    public float INVUL_TIME;//无敌时间

    public float DAMAGE_FREEZE_DOWN;  //受伤冻结的上半程时间
    public float DAMAGE_FREEZE_WAIT; //受伤冻结切换的时间
    public float DAMAGE_FREEZE_UP;//受伤冻结的下半程时间

 来到PlayerData.cs中,我们需要创建几个新的变量,包括玩家的血量,是否阻止暂停(暂时用不上)public bool disablePause;

扣血函数:

public void TakeHealth(int amount)
    {
    if(amount > 0 && health == maxHealth && health != CurrentMaxHealth)
    {
        health = CurrentMaxHealth;
    }
    if(health - amount < 0)
    {
        health = 0;
        return;
    }
    health -= amount;
    }

using System;
using System.Collections.Generic;
using System.Reflection;
using GlobalEnums;
using UnityEngine;[Serializable]
public class PlayerData
{private static PlayerData _instance;public static PlayerData instance{get{if(_instance == null){_instance = new PlayerData();}return _instance;}set{_instance = value;}}public bool disablePause;public int health;public int maxHealth;public int maxHealthBase;public int nailDamage;public bool hasDash;public bool canDash;public bool hasBackDash;public bool canBackDash;public bool overcharmed;public bool gotCharm_31;public bool equippedCharm_31;public int CurrentMaxHealth{get{return maxHealth;}}protected PlayerData(){SetupNewPlayerData();}public void Reset(){SetupNewPlayerData();}private void SetupNewPlayerData(){disablePause = false;health = 5;maxHealth = 5;maxHealthBase = 5;nailDamage = 5;hasDash = true; //测试阶段先设置为true方便测试canDash = true;hasBackDash = false;canBackDash = false;overcharmed = false;gotCharm_31 = true;equippedCharm_31 = true;}public void TakeHealth(int amount){if(amount > 0 && health == maxHealth && health != CurrentMaxHealth){health = CurrentMaxHealth;}if(health - amount < 0){health = 0;return;}health -= amount;}public int GetInt(string intName){if (string.IsNullOrEmpty(intName)){Debug.LogError("PlayerData: Int with an EMPTY name requested.");return -9999;}FieldInfo fieldInfo = GetType().GetField(intName);if(fieldInfo != null){return (int)fieldInfo.GetValue(instance);}Debug.LogError("PlayerData: Could not find int named " + intName + " in PlayerData");return -9999;}public bool GetBool(string boolName){if (string.IsNullOrEmpty(boolName)){return false;}FieldInfo field = GetType().GetField(boolName);if (field != null){return (bool)field.GetValue(instance);}Debug.Log("PlayerData: Could not find bool named " + boolName + " in PlayerData");return false;}}

来到GameManager,我们创建一个新的协程FreezeMoment目的是设置新的TimeScale,而且它是分阶段的,从先到慢的 TimeScale再回到一般的TimeScale,功能很强大!

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class GameManager : MonoBehaviour
{private int timeSlowedCount;public bool TimeSlowed{get{return timeSlowedCount > 0;}}[SerializeField] public PlayerData playerData;private static GameManager _instance;public static GameManager instance{get{if(_instance == null){_instance = FindObjectOfType<GameManager>();}if (_instance == null){Debug.LogError("Couldn't find a Game Manager, make sure one exists in the scene.");}else if (Application.isPlaying){DontDestroyOnLoad(_instance.gameObject);}return _instance;}}private void Awake(){if(_instance != this){_instance = this;DontDestroyOnLoad(this);SetupGameRefs();return;}if(this != _instance){Destroy(gameObject);return;}SetupGameRefs();}private void SetupGameRefs(){playerData = PlayerData.instance;}public int GetPlayerDataInt(string intName){return playerData.GetInt(intName);}public bool GetPlayerDataBool(string boolName){return playerData.GetBool(boolName);}private IEnumerator SetTimeScale(float newTimeScale,float duration){float lastTimeScale = TimeController.GenericTimeScale;for (float timer = 0f; timer < duration; timer += Time.unscaledDeltaTime){float t = Mathf.Clamp01(timer / duration);SetTimeScale(Mathf.Lerp(lastTimeScale, newTimeScale, t));yield return null;}SetTimeScale(newTimeScale);}private void SetTimeScale(float newTimeScale){if(timeSlowedCount > 1){newTimeScale = Mathf.Min(newTimeScale, TimeController.GenericTimeScale);}TimeController.GenericTimeScale = ((newTimeScale > 0.01f) ? newTimeScale : 0f);}public IEnumerator FreezeMoment(float rampDownTime,float waitTime,float rampUpTime,float targetSpeed){timeSlowedCount++;yield return StartCoroutine(SetTimeScale(targetSpeed, rampDownTime));for (float timer = 0f; timer < waitTime; timer += Time.unscaledDeltaTime){yield return null;}yield return StartCoroutine(SetTimeScale(1f, rampUpTime));timeSlowedCount--;}
}

还需要新建一个新的静态类用过管理当前场景中的TimeScale,我们就叫他:TimeController:

using System;
using UnityEngine;public static class TimeController
{private static float slowMotionTimeScale = 1f;private static float pauseTimeScale = 1f;private static float platformBackgroundTimeScale = 1f;private static float genericTimeScale = 1f;public static float GenericTimeScale{get{return genericTimeScale;}set{SetTimeScaleFactor(ref genericTimeScale, value);}}private static void SetTimeScaleFactor(ref float field, float val){if (field != val){field = val;float num = slowMotionTimeScale * pauseTimeScale * platformBackgroundTimeScale * genericTimeScale;if (num < 0.01f){num = 0f;}Time.timeScale = num;}}
}

 回到HeroController.cs中,我们来制作无敌状态的方法()

private IEnumerator Invulnerable(float duration)
    {
        cState.invulnerable = true;
        yield return new WaitForSeconds(DAMAGE_FREEZE_DOWN);
        invPulse.StartInvulnerablePulse();
        yield return new WaitForSeconds(duration);
        invPulse.StopInvulnerablePulse();
        cState.invulnerable = false;
        cState.recoiling = false;
    }

这个invPulse用来制作无敌颜色和通常颜色的线性差值的变化

using System;
using UnityEngine;public class InvulnerablePulse : MonoBehaviour
{public Color invulColor;public float pulseDuration;private Color normalColor;private tk2dSprite sprite;private bool pulsing;private bool reverse;private float currentLerpTime;private void Start(){sprite = GetComponent<tk2dSprite>();normalColor = sprite.color;pulsing = false;currentLerpTime = 0f;}private void Update(){if (pulsing){if (!reverse){currentLerpTime += Time.deltaTime;if(currentLerpTime > pulseDuration){currentLerpTime = pulseDuration;reverse = true;}}else{currentLerpTime -= Time.deltaTime;if(currentLerpTime < 0f){currentLerpTime = 0f;reverse = false;}}float t = currentLerpTime / pulseDuration;sprite.color = Color.Lerp(normalColor, invulColor, t);}}public void StartInvulnerablePulse(){pulsing = true;currentLerpTime = 0f;}public void StopInvulnerablePulse(){pulsing = false;UpdateSpriteColor(normalColor);currentLerpTime = 0f;}public void UpdateSpriteColor(Color color){sprite.color = color;}}

而死亡函数和致命死亡函数仅仅是开始阶段,我们暂时只是将renderer.enabled = false;所以死亡动画什么的要到后面才能做到。 

 private IEnumerator Die(){if (OnDeath != null){OnDeath();}if (!cState.dead){playerData.disablePause = true;rb2d.velocity = Vector2.zero;CancelRecoilHorizonal();AffectedByGravity(false);HeroBox.inactive = true;rb2d.isKinematic = true;SetState(ActorStates.no_input);cState.dead = true;ResetMotion();ResetHardLandingTimer();renderer.enabled = false;gameObject.layer = 2;yield return null;}}private IEnumerator DieFromHazard(HazardTypes hazardType,float angle){if (!cState.hazardDeath){playerData.disablePause = true;SetHeroParent(null);SetState(ActorStates.no_input);cState.hazardDeath = true;ResetMotion();ResetHardLandingTimer();AffectedByGravity(false);renderer.enabled = false;gameObject.layer = 2;if(hazardType == HazardTypes.SPIKES){}else if(hazardType == HazardTypes.ACID){}yield return null;}}

最后我们来到Unity编辑器中,首先我们先来更改每一种Slash里面的PlayerMakerFSM的变量叫magnitudeMult的值都设置为1。 

 为Knight制作一个新的playmakerFSM叫proxyFSM:

 

 

这里有一个新的自定义行为叫SendEventToRegister.cs:

using System;
using HutongGames.PlayMaker;[ActionCategory("Hollow Knight")]
public class SendEventToRegister : FsmStateAction
{public FsmString eventName;public override void Reset(){eventName = new FsmString();}public override void OnEnter(){if(eventName.Value != ""){EventRegister.SendEvent(eventName.Value);}base.Finish();}
}

 我们再给小骑士添加一个新的脚本叫EventRegister.cs:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class EventRegister : MonoBehaviour
{public static Dictionary<string, List<EventRegister>> eventRegister = new Dictionary<string, List<EventRegister>>();[SerializeField] public string subscribedEvent = "";public delegate void RegisteredEvent();public event RegisteredEvent OnReceivedEvent;private void Awake(){SubscribeEvent(this);}private void OnDestroy(){UnsubscribeEvent(this);}public static void SendEvent(string eventName){if (eventName == ""){return;}if (eventRegister.ContainsKey(eventName)){foreach (EventRegister eventRegister in eventRegister[eventName]){eventRegister.ReceiveEvent();}}}public void ReceiveEvent(){FSMUtility.SendEventToGameObject(gameObject, this.subscribedEvent, false);if (this.OnReceivedEvent != null){this.OnReceivedEvent();}}public static void SubscribeEvent(EventRegister register){string key = register.subscribedEvent;List<EventRegister> list;if (eventRegister.ContainsKey(key)){list = eventRegister[key];}else{list = new List<EventRegister>();eventRegister.Add(key, list);}list.Add(register);}public static void UnsubscribeEvent(EventRegister register){string key = register.subscribedEvent;if (eventRegister.ContainsKey(key)){List<EventRegister> list = eventRegister[key];if (list.Contains(register)){list.Remove(register);}if (list.Count <= 0){eventRegister.Remove(key);}}}}

我们在小骑士的子对象Effects,给它添加一个新的子对象叫Damage Effect,用来制作受伤系统的效果:

首先从Leak开始介绍:

另一个粒子系统Hit Pt 1:

 

 另一个粒子系统Hit Pt 2只需要改变Transform即可:

Hit Crack:这个tk2dSprite和tk2dSpriteAnimation我感觉你们都会做了就不细说了

 然后是只剩一滴血的时候的Damage Effects:

 这个low health hit我使用animator制作的,你也可以使用tk2dSpriteAnimator来制作:

 然后是另一个粒子系统black particle burst:

为Hit Crack添加新的Playermaker FSM:简单,只需要把Wait的Time和动画的Clip Time的时间设置成相等即可

 low health hit effect也同理:

 

然给Damage Effect添加一个名字叫Knight Damage 的playmakerFSM:

事件如上,变量如下,特殊处理的变量的图片我都放出来了:

 每一种状态如下所示:

 

 

 

 

这里也需要一个新的自定义行为PlayerDataBoolTest .cs:

using UnityEngine;namespace HutongGames.PlayMaker.Actions
{[ActionCategory(ActionCategory.Logic)][Tooltip("Sends Events based on the value of a Boolean Variable.")]public class PlayerDataBoolTest : FsmStateAction{[RequiredField][Tooltip("GameManager reference, set this to the global variable GameManager.")]public FsmOwnerDefault gameObject;[RequiredField]public FsmString boolName;[Tooltip("Event to send if the Bool variable is True.")]public FsmEvent isTrue;[Tooltip("Event to send if the Bool variable is False.")]public FsmEvent isFalse;private bool boolCheck;public override void Reset(){gameObject = null;boolName = null;isTrue = null;isFalse = null;}public override void OnEnter(){GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);if (ownerDefaultTarget == null){return;}GameManager component = ownerDefaultTarget.GetComponent<GameManager>();if (component == null){return;}boolCheck = component.GetPlayerDataBool(boolName.Value);if (boolCheck){Fsm.Event(isTrue);}else{Fsm.Event(isFalse);}Finish();}}}

总结

回到Unity编辑器中,我们来小骑士的面板给它设置好参数,仅供参考:

我们先来测试敌人的后坐力,这里以crawler为例子,我们给它的recoil行为就是收到攻击后会执行Stun函数也即是播放动画并保持速度为0.

可以看到动画播放完成

再以cralwer为例子,我们让它来测试我们的小骑士后坐力行为和受伤系统以及小部分死亡系统

敌人与HeroBox发生碰撞检测后,播放小骑士Recoil动画,玩家被后坐力影响了速度,播放音乐,DamageEffect发生效果

 玩家在InvulnerablePulse无敌状态下不会再与敌人产生碰撞检测

当被撞到最后一滴血的时候,SetActive low health hit effect 设置为true,播放低血量状态下的hit effect,

 到最后当hp=0的时候,renderer.enabled 设置为false,玩家在此等待重生,至此我们今天的目标均已实现。、

下一期我们来做下小骑士的灵魂系统和法术系统,喜欢的话就蹲下明天八点看我能不能搞出来吧。本文一共五万多字,看完记得做下眼保健操 。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/149519.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

sqlserver 合并重复行数据,取有值的字段

我有这样的一个需求&#xff0c;先看数据 上面的记录&#xff0c;圈起来的 数据关键字段是重复的&#xff0c;但有的字段不一样&#xff0c; 我现在想合并为一条&#xff0c;特殊字段&#xff0c;取有值的 搜了很多行转列&#xff0c;都不是我需要的 不过有了启发&#xff0…

【自动驾驶】基于车辆几何模型的横向控制算法 | Pure Pursuit 纯跟踪算法详解与编程实现

写在前面&#xff1a; &#x1f31f; 欢迎光临 清流君 的博客小天地&#xff0c;这里是我分享技术与心得的温馨角落。&#x1f4dd; 个人主页&#xff1a;清流君_CSDN博客&#xff0c;期待与您一同探索 移动机器人 领域的无限可能。 &#x1f50d; 本文系 清流君 原创之作&…

从零开始学习Python

目录 从零开始学习Python 引言 环境搭建 安装Python解释器 选择IDE 基础语法 注释 变量和数据类型 变量命名规则 数据类型 运算符 算术运算符 比较运算符 逻辑运算符 输入和输出 控制流 条件语句 循环语句 for循环 while循环 循环控制语句 函数和模块 定…

一文学会 Java 8 的Predicates

​ 博客主页: 南来_北往 系列专栏&#xff1a;Spring Boot实战 前言 在这份详细的指南中&#xff0c;您将了解 Java Predicates&#xff0c;这是 Java 8 中一个新颖且有用的特性。本文解释了 Java Predicates 是什么以及如何在各种情况下使用它们。 在这份详尽的指南中…

JVM 几种经典的垃圾收集器

目录 前言 Serial Serial Old ParNew Parallel Scavenge Parallel Old CMS收集器 garbage first 收集器 前言 回顾一下之前的几种垃圾收集算法: JVM java主流的追踪式垃圾收集器-CSDN博客文章浏览阅读646次&#xff0c;点赞22次&#xff0c;收藏16次。简要介绍了几…

AI大模型教程 Prompt提示词工程 AI原生应用开发零基础入门到实战【2024超细超全,建议收藏】

在AGI&#xff08;通用人工智能&#xff09;时代&#xff0c;那些既精通AI技术、又具备编程能力和业务洞察力的复合型人才将成为最宝贵的资源。为此&#xff0c;我们提出了‘AI全栈工程师’这一概念&#xff0c;旨在更精准地描述这一复合型人才群体&#xff0c;而非过分夸大其词…

RocketMQ消费者消费的时候,宕机了,消息会丢失吗?

一个消息从生产者产生&#xff0c;到被消费者消费&#xff0c;主要经过这3个过程&#xff1a; 因此,本文将从以下这几个维度来回答: 生产者如何保证不丢消息 存储端如何保证不丢消息 消费者如何保证不丢消息 最后消费者消费的时候,宕机,消息会不会丢呢? 1. 生产者如何保证…

SaaS 软件转型计划

目录 一、转型目标 1、背景与趋势分析 2、转型策略与实施路径 3、预期成果与展望 二、现状分析 1、产品评估&#xff1a;从传统到SaaS的华丽转身 2、客户群体洞察&#xff1a;倾听需求&#xff0c;引领变革 3、销售渠道优化&#xff1a;拓宽路径&#xff0c;触达更多客…

如何高效绘制ER图?

在数据驱动的时代&#xff0c;实体-关系图&#xff08;ER图&#xff09;作为数据建模的核心工具&#xff0c;对于理解、设计和优化数据库结构至关重要。然而&#xff0c;传统的手绘或复杂软件绘制ER图方式往往效率低下且难以协作。幸运的是&#xff0c;ProcessOn在线绘图工具以…

潮玩宇宙大逃杀宝石游戏搭建开发

潮玩宇宙大逃杀的开发主要涉及以下方面&#xff1a; 1. 游戏概念和设计&#xff1a; 核心概念定义&#xff1a;确定以潮玩为主题的宇宙背景、游戏的基本规则和目标。例如&#xff0c;玩家在宇宙场景中参与大逃杀竞技&#xff0c;目标是成为最后存活的玩家。 玩法模式设计&a…

飞睿智能实时雷达活体探测传感器模块,智能家居静止检测实时感知人员有无

随着科技的飞速发展&#xff0c;我们的生活正在经历着未有的创新。在这个创新的浪潮中&#xff0c;实时雷达活体探测传感器模块的技术正逐渐崭露头角&#xff0c;以其独特的优势为我们的生活带来安全与便捷。今天&#xff0c;我们就来详细探讨一下这项技术&#xff0c;看看它是…

LeetCode Hot100 C++ 哈希 1.两数之和

LeetCode Hot100 C 1.两数之和 给定一个整数数组 nums 和一个整数目标值 target&#xff0c;请你在该数组中找出 和为目标值 target 的那 两个整数&#xff0c;并返回它们的数组下标。 你可以假设每种输入只会对应一个答案&#xff0c;并且你不能使用两次相同的元素。 你可以按…

银行业数据科学家的 6 条经验教训

一个扎心的现实教训是:数据科学并不像你所期望的那样。 原本希望在计算机科学、统计学和机器学习领域工作。运用新方法获得独特见解,实现一切自动化。简而言之,最终成了这个职业炒作的牺牲品。 我想和你们分享一下。希望我们能够摆脱炒作,提高你对数据科学家工作的理解。…

如何只用 CSS 制作网格?

来源&#xff1a;how-to-make-a-grid-like-graph-paper-grid-with-just-css 在看 用于打印到纸张的 CSS 这篇文章时&#xff0c;对其中的网格比较好奇&#xff0c;作者提供了 stackoverflow 的链接&#xff0c;就看到了来源的这个问题和众多回复。本文从里面挑选了一些个人比较…

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-09-24

计算机前沿技术-人工智能算法-大语言模型-最新研究进展-2024-09-24 1. Enriching Datasets with Demographics through Large Language Models: What’s in a Name? K AlNuaimi, G Marti, M Ravaut, A AlKetbi, A Henschel… - arXiv preprint arXiv …, 2024 通过大型语言…

9.23作业

仿照string类&#xff0c;自己手动实现 My_string 代码如下 MyString.h #ifndef MYSTRING_H #define MYSTRING_H #include <iostream> #include <cstring>using namespace std;class My_string { private:char *ptr; //指向字符数组的指针int size; …

【LeetCode:1014. 最佳观光组合 + 思维题】

&#x1f680; 算法题 &#x1f680; &#x1f332; 算法刷题专栏 | 面试必备算法 | 面试高频算法 &#x1f340; &#x1f332; 越难的东西,越要努力坚持&#xff0c;因为它具有很高的价值&#xff0c;算法就是这样✨ &#x1f332; 作者简介&#xff1a;硕风和炜&#xff0c;…

5G-A“用铲子挖金子”,为何在云南地区商用成功?

作者 | 曾响铃 文 | 响铃说 随着技术的成熟与应用&#xff0c;AI、5G-A、物联网等前沿技术领域在市场看来都属于“金矿”型产业&#xff0c;蕴藏着巨大的经济财富。然而&#xff0c;在如今的市场上&#xff0c;“挖金子”的不好过&#xff0c;反而是卖“铲子”的人赚得盆满钵…

MySQL --基本查询(下)

文章目录 3.Update3.1将孙悟空同学的数学成绩变更为 80 分3.2将曹孟德同学的数学成绩变更为 60 分&#xff0c;语文成绩变更为 70 分3.3将总成绩倒数前三的 3 位同学的数学成绩加上 30 分3.4将所有同学的语文成绩更新为原来的 2 倍 4.Delete4.1删除数据4.1.1删除孙悟空同学的考…

微软推迟在MDM设备上启用OOBE强制更新 因为IT管理员反馈称缺乏控制

微软很久之前就计划在 Windows 10/11 OOBE 期间强制下载更新&#xff0c;即若检测到系统本身属于旧版本例如并未安装最新累积更新&#xff0c;则在 OOBE 期间强制下载最新累积更新并自动安装。这种更新方式已经在面向消费者的设备上启用&#xff0c;而上周微软则是在适用于企业…