提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作小骑士基本的攻击行为Attack
- 1.制作动画以及使用UNITY编辑器编辑
- 2.使用代码实现扩展新的落地行为和重落地行为
- 3.使用状态机实现击中敌人造成伤害机制
- 二、为敌人制作生命系统
- 三、为敌人制作受伤系统
- 1.使用代码制作受伤系统
- 2.制作受伤特效
- 总结
前言
警告:此篇文章难度较高而且复杂繁重,非常不适合刚刚入门的或者没看过我前几期的读者,我做了一整天才把明显的bug给解决了真的快要睁不开眼了,因此请读者如果在阅读后感到身体不适请立刻退出这篇文章,本期主要涉及的内容是:制作小骑士基本的攻击行为Attack以及为敌人制作生命系统和受伤系统,我已经把内容上传到我的github空洞骑士demo资产中,欢迎大家下载后在Unity研究。
GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!
一、制作小骑士基本的攻击行为Attack
1.制作动画以及使用UNITY编辑器编辑
我们来为小骑士添加几个新的tk2dSprite和tk2dSpriteAnimation:
我们把Knight文件夹中所有和Slash有关的文件夹里面的Sprite全部拖进去,然后点击Apply:
然后到Animation中,我们每一个种类的Slash都只要前六张图像,Clip Time统一设置成0.3
除此之外我们还做刀光效果也就是SlashEffect,为每一种 SlashEffect创建自己单独的tk2dSprite和tk2dSpriteAnimation:
同样,我们找到 Knight文件夹中所有和Slashffect有关的文件夹一个个拖上去:
在Animation中我们只要0,2,4三张图像,ClipTime设置成0.2:
万事准备OK后我们就给小骑士创建好这样的Attack子对象:
一定要记得给Attacks下的每一个子对象设置Layer为HeroAttack:
选择好HeroAttack可以交互的层级:
然后为每一种Slash Effect添加如下图所示的组件:
那些脚本你们先别管(比如里面的NailSlash.cs),我后续都会讲的。我们先设置好PolygonCollider2D的参数,这里有个小技巧,使用tk2dSprite的这个显示刀光的图片,然后对着这个大小调整好碰撞箱大小,注意要勾选isTrigger,调整好后
调整好后记得关上MeshRenderer和 PolygonCollider2D,我们只在需要的时候用到它们:
关于Slash的子对象Clash Tink目前还用不上,我们先把它放一边以后用的时候再扩展:
然后其它三个如上同理,最后打开Gizmos后效果如下所示:
2.使用代码实现扩展新的落地行为和重落地行为
我们每添加一个行为就要到HeroActions中添加一个PlayerAction,这次是attack
重大失误!!! 这个moveVector = CreateTwoAxisPlayerAction(left, right, down, up);它的顺序应该是-x,x-y,y。我之前倒数两个参数位置搞反了,所以检测的y轴输入时反的,现在已经更改过来了,只能说还好Debug发现的早。
using System;
using InControl;
public class HeroActions : PlayerActionSet
{
public PlayerAction left;
public PlayerAction right;
public PlayerAction up;
public PlayerAction down;
public PlayerTwoAxisAction moveVector;
public PlayerAction attack;
public PlayerAction jump;
public PlayerAction dash;
public HeroActions()
{
left = CreatePlayerAction("Left");
left.StateThreshold = 0.3f;
right = CreatePlayerAction("Right");
right.StateThreshold = 0.3f;
up = CreatePlayerAction("Up");
up.StateThreshold = 0.3f;
down = CreatePlayerAction("Down");
down.StateThreshold = 0.3f;
moveVector = CreateTwoAxisPlayerAction(left, right, down, up); //重大失误!!!
moveVector.LowerDeadZone = 0.15f;
moveVector.UpperDeadZone = 0.95f;
attack = CreatePlayerAction("Attack");
jump = CreatePlayerAction("Jump");
dash = CreatePlayerAction("Dash");
}
}
然后就到InputHandler.cs中添加一行代码: AddKeyBinding(inputActions.attack, "Z");
using System;
using System.Collections;
using System.Collections.Generic;
using GlobalEnums;
using InControl;
using UnityEngine;
public class InputHandler : MonoBehaviour
{
public InputDevice gameController;
public HeroActions inputActions;
public void Awake()
{
inputActions = new HeroActions();
}
public void Start()
{
MapKeyboardLayoutFromGameSettings();
if(InputManager.ActiveDevice != null && InputManager.ActiveDevice.IsAttached)
{
}
else
{
gameController = InputDevice.Null;
}
Debug.LogFormat("Input Device set to {0}.", new object[]
{
gameController.Name
});
}
private void MapKeyboardLayoutFromGameSettings()
{
AddKeyBinding(inputActions.up, "UpArrow");
AddKeyBinding(inputActions.down, "DownArrow");
AddKeyBinding(inputActions.left, "LeftArrow");
AddKeyBinding(inputActions.right, "RightArrow");
AddKeyBinding(inputActions.attack, "Z");
AddKeyBinding(inputActions.jump, "X");
AddKeyBinding(inputActions.dash, "D");
}
private static void AddKeyBinding(PlayerAction action, string savedBinding)
{
Mouse mouse = Mouse.None;
Key key;
if (!Enum.TryParse(savedBinding, out key) && !Enum.TryParse(savedBinding, out mouse))
{
return;
}
if (mouse != Mouse.None)
{
action.AddBinding(new MouseBindingSource(mouse));
return;
}
action.AddBinding(new KeyBindingSource(new Key[]
{
key
}));
}
}
来到HeroControllerState部分,我们添加几个新的状态:
public bool attacking;
public bool altAttack;
public bool upAttacking;
public bool downAttacking;
[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 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;
preventDash = false;
preventBackDash = false;
dashCooldown = false;
backDashCooldown = false;
isPaused = false;
}
回到HeroAnimationController.cs中,我们为attack攻击判断哪种攻击类型:
if(cState.attacking)
{
if (cState.upAttacking)
{
Play("UpSlash");
}
else if (cState.downAttacking)
{
Play("DownSlash");
}
else if (!cState.altAttack)
{
Play("Slash");
}
else
{
Play("SlashAlt");
}
}
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:
}
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"); ;
}
}
回到HeroController.cs当中,我们来为攻击添加完整的行为状态控制机:
[SerializeField] private NailSlash slashComponent; //决定使用哪种攻击的NailSlash
[SerializeField] private PlayMakerFSM slashFsm;//决定使用哪种攻击的PlayMakerFSM
public NailSlash normalSlash;
public NailSlash altetnateSlash;
public NailSlash upSlash;
public NailSlash downSlash;
public PlayMakerFSM normalSlashFsm;
public PlayMakerFSM altetnateSlashFsm;
public PlayMakerFSM upSlashFsm;
public PlayMakerFSM downSlashFsm;
private bool attackQueuing; //是否开始攻击计数步骤
private int attackQueueSteps; //攻击计数步骤
private float attack_time;
private float attackDuration; //攻击状态持续时间,根据有无护符来决定
private float attack_cooldown;
private float altAttackTime; //当时间超出可按二段攻击的时间后,cstate.altattack就会为false
public float ATTACK_DURATION; //无护符时攻击状态持续时间
public float ATTACK_COOLDOWN_TIME; //攻击后冷却时间
public float ATTACK_RECOVERY_TIME; //攻击恢复时间,一旦超出这个时间就退出攻击状态
public float ALT_ATTACK_RESET; //二段攻击重置时间
private int ATTACK_QUEUE_STEPS = 5; //超过5步即可开始攻击
private float NAIL_TERRAIN_CHECK_TIME = 0.12f;
在Update()中我们当攻击时间超过attackDuration后重置攻击,并开启冷却倒计时attack_cooldown :
else if (hero_state != ActorStates.no_input)
{
LookForInput();
if(cState.attacking && !cState.dashing)
{
attack_time += Time.deltaTime;
if(attack_time >= attackDuration)
{
ResetAttacks();
animCtrl.StopAttack();
}
}
}
if(attack_cooldown > 0f)
{
attack_cooldown -= Time.deltaTime;
}
在方法LookForQueueInput()中我们判断是否按下攻击键:
if(inputHandler.inputActions.attack.IsPressed && attackQueueSteps <= ATTACK_QUEUE_STEPS && CanAttack() && attackQueuing)
{
Debug.LogFormat("Start Do Attack");
DoAttack();
}
以及进入attackQueuing:
if(inputHandler.inputActions.attack.WasPressed)
{
if (CanAttack())
{
DoAttack();
}
else
{
attackQueueSteps = 0;
attackQueuing = true;
}
}
在Update()中我们直接++
if(attackQueuing)
{
attackQueueSteps++;
}
如果没按下攻击键就attackQueuing = false;
if (!inputHandler.inputActions.attack.IsPressed)
{
attackQueuing = false;
}
当然还有Attack(),DoAttack(),CanAttack(),CancelAttack()等等方法构成完整的攻击行为:
private bool CanAttack()
{
return hero_state != ActorStates.no_input && hero_state != ActorStates.hard_landing && hero_state != ActorStates.dash_landing && attack_cooldown <= 0f && !cState.attacking && !cState.dashing;
}
private void DoAttack()
{
attack_cooldown = ATTACK_COOLDOWN_TIME;
if(vertical_input > Mathf.Epsilon)
{
Attack(AttackDirection.upward);
StartCoroutine(CheckForTerrainThunk(AttackDirection.upward));
return;
}
if(vertical_input >= -Mathf.Epsilon)
{
Attack(AttackDirection.normal);
StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
return;
}
if(hero_state != ActorStates.idle && hero_state != ActorStates.running)
{
Attack(AttackDirection.downward);
StartCoroutine(CheckForTerrainThunk(AttackDirection.downward));
return;
}
Attack(AttackDirection.normal);
StartCoroutine(CheckForTerrainThunk(AttackDirection.normal));
}
private void Attack(AttackDirection attackDir)
{
if(Time.timeSinceLevelLoad - altAttackTime > ALT_ATTACK_RESET)
{
cState.altAttack = false;
}
cState.attacking = true;
attackDuration = ATTACK_DURATION;
if (attackDir == AttackDirection.normal)
{
if (!cState.altAttack)
{
slashComponent = normalSlash;
slashFsm = normalSlashFsm;
cState.altAttack = true;
}
else
{
slashComponent = altetnateSlash;
slashFsm = altetnateSlashFsm;
cState.altAttack = false;
}
}
else if (attackDir == AttackDirection.upward)
{
slashComponent = upSlash;
slashFsm = upSlashFsm;
cState.upAttacking = true;
}
else if (attackDir == AttackDirection.downward)
{
slashComponent = downSlash;
slashFsm = downSlashFsm;
cState.downAttacking = true;
}
if(attackDir == AttackDirection.normal && cState.facingRight)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 0f;
}
else if (attackDir == AttackDirection.normal && !cState.facingRight)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 180f;
}
else if (attackDir == AttackDirection.upward)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 90f;
}
else if (attackDir == AttackDirection.downward)
{
slashFsm.FsmVariables.GetFsmFloat("direction").Value = 270f;
}
altAtt