[Unity Demo]从零开始制作空洞骑士Hollow Knight第十五集:制作更多地图,更多敌人,更多可交互对象
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、第一个代表性场景
- 1.制作更多敌人
- 2.制作更多可交互对象
- 二、第二个代表性场景
- 1.制作更多敌人
- 2.制作更多可交互对象
- 三、第三个代表性场景
- 1.制作更多敌人
- 2.制作更多可交互对象
- 总结
前言
hello大家好久没见,之所以隔了这么久才更新并不是因为我又放弃了这个项目,而是接下来要制作的工作太忙碌了,每次我都花了很长的时间解决完一个部分,然后就没力气打开CSDN写文章就直接睡觉去了,现在终于有时间整理下我这半个月都做了什么内容
废话少说,接下来我将介绍我做的几个代表性场景,主要是如标题说的,制作更多地图,更多敌人,更多可交互对象
另外,我的Github已经更新了,想要查看最新的内容话请到我的Github主页下载工程吧:
GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!
一、第一个代表性场景
1.制作更多敌人
我们先把制作好创建吧,还是老规矩先用tk2dTilemap绘制好基础的地图样貌并添加上Collider:

然后堆叠素材添加上去,这样一个场景就做好了:

首先当然是从Town跳下井里的第一个场景Crossroads_01,这里我们设置Town到Crossroads_01的TransitionPoint为top2:

然后回到Crossroads中我们设置好全部的TransitionPoint,这个top1别管:

然后我们添加上敌人,其实这里敌人没什么要讲的因为都是我们之前就做过的,直接预制体拖上去就完事了

主要是要修复这个Zombie的bug,我们之前做的Walker.cs脚本有问题,现在让我们修复bug,问题在EndStopping中:我就说之前怎么会莫名其妙的转身,
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Walker : MonoBehaviour
{[Header("Structure")]//检测玩家的脚本一个不能少[SerializeField] private LineOfSightDetector lineOfSightDetector;[SerializeField] private AlertRange alertRange; //每一个敌人的四件公式化挂载rb2d,col2d,animator,audiosource,再加一个摄像头和hero位置private Rigidbody2D body;private Collider2D bodyCollider;private tk2dSpriteAnimator animator;private AudioSource audioSource;private Camera mainCamera;private HeroController hero;private const float CameraDistanceForActivation = 60f;private const float WaitHeroXThreshold = 1f; //距离玩家X方向上的极限距离值[Header("Configuration")][SerializeField] private bool ambush; //是否埋伏[SerializeField] private string idleClip; //idle的动画片段名字[SerializeField] private string turnClip; //turn的动画片段名字[SerializeField] private string walkClip; //walk的动画片段名字[SerializeField] private float edgeXAdjuster; //检测墙沿x上的增加值[SerializeField] private bool preventScaleChange; //是否防止x轴的localscale发生变化[SerializeField] private bool preventTurn; //是否阻止转向[SerializeField] private float pauseTimeMin; //停止不动的时间[SerializeField] private float pauseTimeMax;[SerializeField] private float pauseWaitMin; //走路的时间[SerializeField] private float pauseWaitMax;[SerializeField] private bool pauses; //是否需要静止状态[SerializeField] private float rightScale; //开始时的x轴方向[SerializeField] public bool startInactive; //开始时不活跃[SerializeField] private int turnAfterIdlePercentage; //Idle状态过后进入转身Turn状态的概率[SerializeField] private float turnPause; //设置转身的冷却时间[SerializeField] private bool waitForHeroX; //是否等待玩家X方向到位[SerializeField] private float waitHeroX; //等待玩家X方向距离[SerializeField] public float walkSpeedL; //向左走路的速度[SerializeField] public float walkSpeedR;//向右走路的速度[SerializeField] public bool ignoreHoles; //是否忽略洞[SerializeField] private bool preventTurningToFaceHero; //防止转向玩家的位置[SerializeField] private Walker.States state;[SerializeField] private Walker.StopReasons stopReason;private bool didFulfilCameraDistanceCondition; //暂时没有用到private bool didFulfilHeroXCondition; //暂时没有用到private int currentFacing;//Debug的时候可以在前面加个[SerializeField]private int turningFacing;//三个计时器且顾名思义private float walkTimeRemaining;private float pauseTimeRemaining;private float turnCooldownRemaining;protected void Awake(){body = GetComponent<Rigidbody2D>();bodyCollider = GetComponent<BoxCollider2D>();animator = GetComponent<tk2dSpriteAnimator>();audioSource = GetComponent<AudioSource>();}protected void Start(){mainCamera = Camera.main;hero = HeroController.instance;if(currentFacing == 0){currentFacing = ((transform.localScale.x * rightScale >= 0f) ? 1 : -1); //左边是-1,右边是1}if(state == States.NotReady){turnCooldownRemaining = -Mathf.Epsilon;BeginWaitingForConditions();}}/// <summary>/// 我们创建另一个状态机,分为四种状态,每一种都有Update和Stop的方法。/// </summary>protected void Update(){turnCooldownRemaining -= Time.deltaTime;switch (state){case States.WaitingForConditions:UpdateWaitingForConditions();break;case States.Stopped:UpdateStopping();break;case States.Walking:UpdateWalking();break;case States.Turning:UpdateTurning();break;default:break;}}/// <summary>/// 从Waiting状态进入开始移动状态(不一定是Walk也可能是Turn)/// </summary>public void StartMoving(){if(state == States.Stopped || state == States.WaitingForConditions){startInactive = false;int facing;if(currentFacing == 0){facing = UnityEngine.Random.Range(0, 2) == 0 ? -1 : 1;}else{facing = currentFacing;}BeginWalkingOrTurning(facing);}Update();}/// <summary>/// 在需要时取消转向/// </summary>public void CancelTurn(){if(state == States.Turning){BeginWalking(currentFacing);}}public void Go(int facing){turnCooldownRemaining = -Time.deltaTime;if(state == States.Stopped || state == States.Walking){BeginWalkingOrTurning(facing);}else if(state == States.Turning && currentFacing == facing){CancelTurn();}Update();}public void ReceiveGoMessage(int facing) //TODO:{if(state != States.Stopped || stopReason != StopReasons.Controlled){Go(facing);}}/// <summary>/// 被脚本StopWalker.cs调用,更改reason为controlled/// </summary>/// <param name="reason"></param>public void Stop(StopReasons reason){BeginStopped(reason);}/// <summary>/// 更改turningFacing和currentFacing,属于Turn状态的行为/// </summary>/// <param name="facing"></param>public void ChangeFacing(int facing){if(state == States.Turning){turningFacing = facing;currentFacing = -facing;return;}currentFacing = facing;}/// <summary>/// 开始进入等待状态/// </summary>private void BeginWaitingForConditions(){state = States.WaitingForConditions;didFulfilCameraDistanceCondition = false;didFulfilHeroXCondition = false;UpdateWaitingForConditions();}/// <summary>/// 在Update以及BeginWaitingForConditions两大函数中调用,更新等待状态下的行为/// </summary>private void UpdateWaitingForConditions(){if (!didFulfilCameraDistanceCondition && (mainCamera.transform.position - transform.position).sqrMagnitude < CameraDistanceForActivation * CameraDistanceForActivation){didFulfilCameraDistanceCondition = true;}if(didFulfilCameraDistanceCondition && !didFulfilHeroXCondition && hero != null && Mathf.Abs(hero.transform.position.x - waitHeroX) < WaitHeroXThreshold) //TODO:{didFulfilHeroXCondition = true;}if(didFulfilCameraDistanceCondition && (!waitForHeroX || didFulfilHeroXCondition) && !startInactive && !ambush){BeginStopped(StopReasons.Bored);StartMoving();}}/// <summary>/// 开始进入停止状态/// </summary>/// <param name="reason"></param>private void BeginStopped(StopReasons reason){state = States.Stopped;stopReason = reason;if (audioSource){audioSource.Stop();}if(reason == StopReasons.Bored){tk2dSpriteAnimationClip clipByName = animator.GetClipByName(idleClip);if(clipByName != null){animator.Play(clipByName);}body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f)); //相当于把x方向上的速度设置为0if (pauses){pauseTimeRemaining = UnityEngine.Random.Range(pauseTimeMin, pauseTimeMax);return;}EndStopping();}}/// <summary>/// 在Update中被调用,执行停止Stop状态的行为/// </summary>private void UpdateStopping(){if(stopReason == StopReasons.Bored){pauseTimeRemaining -= Time.deltaTime;if(pauseTimeRemaining <= 0f){EndStopping();}}}/// <summary>/// 终止停止状态/// </summary>private void EndStopping(){if(currentFacing == 0){BeginWalkingOrTurning(UnityEngine.Random.Range(0, 2) == 0 ? 1 : -1);return;}if(UnityEngine.Random.Range(0,100) < turnAfterIdlePercentage){BeginTurning(-currentFacing);return;}BeginWalking(currentFacing); //这里应该是开始行走Walk而不是开始转向Turn}/// <summary>/// 要不走路要不转身/// </summary>/// <param name="facing"></param>private void BeginWalkingOrTurning(int facing){if(currentFacing == facing){BeginWalking(facing);return;}BeginTurning(facing);}/// <summary>/// 开始进入Walking状态/// </summary>/// <param name="facing"></param>private void BeginWalking(int facing){state = States.Walking;animator.Play(walkClip);if (!preventScaleChange){transform.SetScaleX(facing * rightScale);}walkTimeRemaining = UnityEngine.Random.Range(pauseWaitMin, pauseWaitMax);if (audioSource){audioSource.Play();}body.velocity = new Vector2((facing > 0) ? walkSpeedR : walkSpeedL,body.velocity.y);}/// <summary>/// 在Update中被调用,动态执行Walking状态,根据情况决定是否要进入Turning状态或者Stopped状态/// </summary>private void UpdateWalking(){if(turnCooldownRemaining <= 0f){Sweep sweep = new Sweep(bodyCollider, 1 - currentFacing, Sweep.DefaultRayCount,Sweep.DefaultSkinThickness);if (sweep.Check(transform.position, bodyCollider.bounds.extents.x + 0.5f, LayerMask.GetMask("Terrain"))){BeginTurning(-currentFacing);return;}if (!preventTurningToFaceHero && (hero != null && hero.transform.GetPositionX() > transform.GetPositionX() != currentFacing > 0) && lineOfSightDetector != null && lineOfSightDetector.CanSeeHero && alertRange != null && alertRange.IsHeroInRange){BeginTurning(-currentFacing);return;}if (!ignoreHoles){Sweep sweep2 = new Sweep(bodyCollider, DirectionUtils.Down, Sweep.DefaultRayCount, 0.1f);if (!sweep2.Check((Vector2)transform.position + new Vector2((bodyCollider.bounds.extents.x + 0.5f + edgeXAdjuster) * currentFacing, 0f), 0.25f, LayerMask.GetMask("Terrain"))){BeginTurning(-currentFacing);return;}}}if (pauses){walkTimeRemaining -= Time.deltaTime;if(walkTimeRemaining <= 0f){BeginStopped(StopReasons.Bored);return;}}body.velocity = new Vector2((currentFacing > 0) ? walkSpeedR : walkSpeedL, body.velocity.y);}private void BeginTurning(int facing){state = States.Turning;turningFacing = facing;if (preventTurn){EndTurning();return;}turnCooldownRemaining = turnPause;body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f));animator.Play(turnClip);FSMUtility.SendEventToGameObject(gameObject, (facing > 0) ? "TURN RIGHT" : "TURN LEFT", false);}/// <summary>/// 在Update中被调用,执行Turning转身状态。/// </summary>private void UpdateTurning(){body.velocity = Vector2.Scale(body.velocity, new Vector2(0f, 1f));if (!animator.Playing){EndTurning();}}/// <summary>/// 被UpdateTurning()调用,当动画播放完成后切换到Walking状态。/// 被BeginTurning()调用,当preventTurn为true时就不再向下执行了。/// </summary>private void EndTurning(){currentFacing = turningFacing;BeginWalking(currentFacing);}/// <summary>/// 就清空turnCooldownRemaining/// </summary>public void ClearTurnCoolDown(){turnCooldownRemaining = -Mathf.Epsilon;}public enum States{NotReady,WaitingForConditions,Stopped,Walking,Turning}public enum StopReasons{Bored,Controlled}}
然后我们在面板中重新设置好参数:

2.制作更多可交互对象
这个场景里可交互的貌似只有这个杆,我们添加好它的顶部和底部并设置好位置,添加上layer然后新建脚本:BreakablePole.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class BreakablePole : MonoBehaviour,IHitResponder
{[SerializeField] private SpriteRenderer spriteRenderer;[SerializeField] private Sprite brokenSprite;[SerializeField] private float inertBackgroundThreshold;[SerializeField] private float inertForegroundThreshold;[SerializeField] private AudioSource audioSourcePrefab;[SerializeField] private RandomAudioClipTable hitClip;[SerializeField] private GameObject slashImpactPrefab;[SerializeField] private Rigidbody2D top;protected void Reset(){inertBackgroundThreshold = -1f;inertForegroundThreshold = -1f;}protected void Start(){float z = transform.position.z;if(z < inertBackgroundThreshold || z > inertForegroundThreshold){enabled = false;return;}}public void Hit(HitInstance damageInstance){int cardinalDirection = DirectionUtils.GetCardinalDirection(damageInstance.Direction);if (cardinalDirection != 2 && cardinalDirection != 0){return;}spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b,0f);Transform transform = Instantiate(slashImpactPrefab).transform;transform.eulerAngles = new Vector3(0f, 0f, Random.Range(340f, 380f));Vector3 localScale = transform.localScale;localScale.x = ((cardinalDirection == 2) ? -1f : 1f);localScale.y = 1f;hitClip.SpawnAndPlayOneShot(audioSourcePrefab, base.transform.position);if (top != null){top.gameObject.SetActive(true);float num = (cardinalDirection == 2) ? Random.Range(120, 140) : Random.Range(40, 60);top.transform.localScale = new Vector3(localScale.x, localScale.y, top.transform.localScale.z);top.velocity = new Vector2(Mathf.Cos(num * 0.017453292f), Mathf.Sin(num * 0.017453292f)) * 5f;top.transform.Rotate(new Vector3(0f, 0f, num));base.enabled = false;}}
}
然后设置好参数:

二、第二个代表性场景
1.制作更多敌人
然后就是大家喜闻乐见的长场景Crossroads_07,这个场景相当于一个区域的中转站,既可以向左走去虫爷爷和苍绿之境,或者打boss躁郁的毛里克,亦可以向右走去打假骑士和苍蝇之母,鹿角站,矿井等等,不过这些都是后话了,我们先来把地图做好,添加上对应的TranstionPoint:

其实做到这里我才意识到要把这四个 TranstionPoint做成预制体。
然后敌人自然是到处飞的苍蝇fly了:

2.制作可交互对象
为了让场景看起来生动,我们可以添加背景板里飞走的蚊子buzzer,用particlesystem来实现


第二个:

然后就是踩一下会发出声音的平台:
我们来给它们新建一个脚本LiftPlatform.cs:实现了个功能当角色踩上去时上下移动一下,播放粒子系统和声音:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class LiftPlatform : MonoBehaviour
{public GameObject part1;public GameObject part2;public ParticleSystem dustParticle;public AudioSource source;private float part1_start_y;private float part2_start_y;private int state;private float timer;private void Start(){part1_start_y = part1.transform.position.y;part2_start_y = part2.transform.position.y;}private void Update(){if(state == 1){if (timer < 0.125f){part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - timer * 0.75f, part1.transform.position.z);part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - timer * 0.75f, part2.transform.position.z);timer += Time.deltaTime;}else{part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - 0.09f, part1.transform.position.z);part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - 0.09f, part2.transform.position.z);state = 2;timer = 0.12f;}}if(state == 2){if (timer > 0f){part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y - timer * 0.75f, part1.transform.position.z);part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y - timer * 0.75f, part2.transform.position.z);timer -= Time.deltaTime;return;}part1.transform.position = new Vector3(part1.transform.position.x, part1_start_y, part1.transform.position.z);part2.transform.position = new Vector3(part2.transform.position.x, part2_start_y, part2.transform.position.z);state = 0;}}private void OnCollisionEnter2D(Collision2D collision){if(state == 0 && collision.collider.gameObject.layer != LayerMask.NameToLayer("Item") && collision.gameObject.layer != LayerMask.NameToLayer("Particle") && collision.gameObject.layer != LayerMask.NameToLayer("Enemies") && collision.GetSafeContact().Normal.y < 0.1f){source.pitch = Random.Range(0.85f, 1.15f);source.Play();dustParticle.Play();state = 0;timer = 0f;}}}

三、第三个代表性场景
1.制作更多的敌人
其实这个场景我就想将新制作的敌人的:没错就是这个拿骨钉盾牌的僵尸虫

我们回到tk2dSprite和tk2danimator给它创建好:



你只需要记住,A是anticipate攻击准备阶段的动画,L是Lunge突刺动画,S是攻击时的动画,CD是冷却时候的动画, 然后剩下的dddd。这个Bump就是反弹骨钉攻击的动画。OK说的差不多了,然后Unsheild和sheild动画就是反过来的。
然后就是添加相应的脚本到场景中:




s上面的脚本我在之前的文章都讲过了,除了这个EnemyDeathEffects:
using System;
using HutongGames.PlayMaker;
using UnityEngine;
using UnityEngine.Audio;public class EnemyDeathEffects : MonoBehaviour
{[SerializeField] private GameObject corpsePrefab;[SerializeField] private bool corpseFacesRight;[SerializeField] private float corpseFlingSpeed;[SerializeField] public Vector3 corpseSpawnPoint;[SerializeField] private string deathBroadcastEvent;[SerializeField] private Vector3 effectOrigin;[SerializeField] private bool lowCorpseArc;[SerializeField] private EnemyDeathTypes enemyDeathType;[SerializeField] protected AudioSource audioPlayerPrefab;[SerializeField] protected AudioEvent enemyDeathSwordAudio;[SerializeField] protected AudioEvent enemyDamageAudio;[SerializeField] protected AudioClip enemyDeathSwordClip;[SerializeField] protected AudioClip enemyDamageClip;[SerializeField] private AudioMixerSnapshot audioSnapshotOnDeath;[SerializeField] protected GameObject deathWaveInfectedPrefab;[SerializeField] protected GameObject deathWaveInfectedSmallPrefab;[SerializeField] private bool recycle;[SerializeField] private bool rotateCorpse; //尸体需要旋转吗[SerializeField] protected GameObject dustPuffMedPrefab;[SerializeField] protected GameObject deathPuffLargePrefab;protected GameObject corpse;private bool didFire;[HideInInspector]public bool doKillFreeze = true;protected void Start(){PreInstantiate();}public void PreInstantiate(){if(!corpse && corpsePrefab){corpse = Instantiate(corpsePrefab, transform.position + corpseSpawnPoint, Quaternion.identity, transform);tk2dSprite[] componentInChildrens = corpse.GetComponentsInChildren<tk2dSprite>(true);for (int i = 0; i < componentInChildrens.Length; i++){componentInChildrens[i].ForceBuild();}corpse.SetActive(false);}}public void RecieveDeathEvent(float? attackDirection, bool resetDeathEvent = false, bool spellBurn = false, bool isWatery = false){if (didFire)return;didFire = true;if(corpse != null){EmitCorpse(attackDirection, isWatery, spellBurn);}if (!isWatery){EmitEffects();}if (doKillFreeze){GameManager.instance.FreezeMoment(1);}if (enemyDeathType == EnemyDeathTypes.Infected || enemyDeathType == EnemyDeathTypes.LargeInfected || enemyDeathType == EnemyDeathTypes.SmallInfected || enemyDeathType == EnemyDeathTypes.Uninfected ){EmitEssence();}if (audioSnapshotOnDeath != null){audioSnapshotOnDeath.TransitionTo(2f);}if (!string.IsNullOrEmpty(deathBroadcastEvent)){Debug.LogWarningFormat(this, "Death broadcast event '{0}' not implemented!", new object[]{deathBroadcastEvent});}if (resetDeathEvent){FSMUtility.SendEventToGameObject(gameObject, "CENTIPEDE DEATH", false);didFire = false;return;}if (recycle){PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(gameObject, "health_manager_enemy");if(playMakerFSM != null){playMakerFSM.FsmVariables.GetFsmBool("Activated").Value = false;}HealthManager component2 = GetComponent<HealthManager>();if(component2 != null){component2.SetIsDead(false);}didFire = false;//TODO:return;}Destroy(gameObject);}private void EmitCorpse(float? attackDirection, bool isWatery, bool spellBurn){if (corpse == null)return;corpse.transform.SetParent(null);corpse.transform.SetPositionZ(UnityEngine.Random.Range(-0.08f, -0.09f));corpse.SetActive(true);PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(corpse, "corpse");if(playMakerFSM != null){FsmBool fsmBool = playMakerFSM.FsmVariables.GetFsmBool("spellBurn");if(fsmBool!= null){fsmBool.Value = false;}}Corpse component = corpse.GetComponent<Corpse>();if (component){component.Setup(isWatery, spellBurn);}if (isWatery){return;}corpse.transform.SetRotation2D(rotateCorpse ? transform.GetRotation2D():0f);if(Mathf.Abs(transform.eulerAngles.z) >= 45f){Collider2D component2 = GetComponent<Collider2D>();Collider2D component3 = corpse.GetComponent<Collider2D>();if(!rotateCorpse && component2 && component3){Vector3 b = component2.bounds.center - component3.bounds.center;b.z = 0f;corpse.transform.position += b;}}float d = 1f;if(attackDirection == null){d = 0f;}int cardinalDirection = DirectionUtils.GetCardinalDirection(attackDirection.GetValueOrDefault());Rigidbody2D component4 = corpse.GetComponent<Rigidbody2D>();if(component4 != null && !component4.isKinematic){float num = corpseFlingSpeed;float num2;switch (cardinalDirection){case 0:num2 = lowCorpseArc ? 10f : 60f;corpse.transform.SetScaleX(corpse.transform.localScale.x * (corpseFacesRight ? -1f : 1f) * Mathf.Sign(transform.localScale.x));break;case 1:num2 = UnityEngine.Random.Range(75f, 105f);num *= 1.3f;break;case 2:num2 = lowCorpseArc ? 170f : 120f;corpse.transform.SetScaleX(corpse.transform.localScale.x * (corpseFacesRight ? 1f : -1f) * Mathf.Sign(transform.localScale.x));break;case 3:num2 = 270f;break;default:num2 = 90f;break;}component4.velocity = new Vector2(Mathf.Cos(num2 * 0.017453292f), Mathf.Sin(num2 * 0.017453292f)) * num * d;}}private void EmitEffects(){EnemyDeathTypes enemyDeathTypes = enemyDeathType;if(enemyDeathTypes == EnemyDeathTypes.Infected){EmitInfectedEffects();return;}if (enemyDeathTypes == EnemyDeathTypes.SmallInfected){EmitSmallInfectedEffects();return;}if (enemyDeathTypes != EnemyDeathTypes.LargeInfected){Debug.LogWarningFormat(this, "Enemy death type {0} not implemented!", new object[]{enemyDeathType});return;}EmitLargeInfectedEffects();}private void EmitLargeInfectedEffects(){AudioEvent audioEvent = default(AudioEvent);audioEvent.Clip = enemyDeathSwordClip;audioEvent.PitchMin = 0.75f;audioEvent.PitchMax = 0.75f;audioEvent.Volume = 1f;audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);audioEvent = default(AudioEvent);audioEvent.Clip = enemyDamageClip;audioEvent.PitchMin = 0.75f;audioEvent.PitchMax = 0.75f;audioEvent.Volume = 1f;audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);if(corpse != null){SpriteFlash component = corpse.GetComponent<SpriteFlash>();if(component != null){component.flashInfected();}}if (!(deathPuffLargePrefab == null)){Instantiate(deathPuffLargePrefab, transform.position + effectOrigin, Quaternion.identity);}ShakeCameraIfVisible("AverageShake");if (!(deathWaveInfectedPrefab == null)){GameObject gameObject = Instantiate(deathWaveInfectedPrefab, transform.position + effectOrigin, Quaternion.identity);gameObject.transform.SetScaleX(2f);gameObject.transform.SetScaleY(2f);}GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 75, 80, 20f, 25f, 0f, 360f, null);}private void EmitSmallInfectedEffects(){AudioEvent audioEvent = default(AudioEvent);audioEvent.Clip = enemyDeathSwordClip;audioEvent.PitchMin = 1.2f;audioEvent.PitchMax = 1.4f;audioEvent.Volume = 1f;audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);audioEvent = default(AudioEvent);audioEvent.Clip = enemyDamageClip;audioEvent.PitchMin = 1.2f;audioEvent.PitchMax = 1.4f;audioEvent.Volume = 1f;audioEvent.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);if (deathWaveInfectedSmallPrefab != null){GameObject gameObject = Instantiate(deathWaveInfectedSmallPrefab, transform.position + effectOrigin,Quaternion.identity);Vector3 localScale = gameObject.transform.localScale;localScale.x = 0.5f;localScale.y = 0.5f;gameObject.transform.localScale = localScale;}GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 8, 10, 15f, 20f, 0, 360, null);}private void EmitInfectedEffects(){EmitSound();if(corpse != null){SpriteFlash component = corpse.GetComponent<SpriteFlash>();if(component != null){component.flashInfected();}}GameObject gameObject = Instantiate(deathWaveInfectedPrefab, transform.position + effectOrigin, Quaternion.identity);gameObject.transform.SetScaleX(1.25f);gameObject.transform.SetPositionY(1.25f);GlobalPrefabDefaults.Instance.SpawnBlood(transform.position + effectOrigin, 8, 10, 15f, 20f, 0, 360, null);Instantiate(dustPuffMedPrefab, transform.position + effectOrigin, Quaternion.identity);ShakeCameraIfVisible("EnemyKillShake");}private void EmitSound(){enemyDeathSwordAudio.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);enemyDamageAudio.SpawnAndPlayOneShot(audioPlayerPrefab, transform.position);}private void EmitEssence(){//TODO:和梦之钉有关的PlayerData playerData = GameManager.instance.playerData;if (!playerData.hasDreamNail){return; }}protected void ShakeCameraIfVisible(string eventName){Renderer renderer = GetComponent<Renderer>();if (renderer == null){renderer = GetComponentInChildren<Renderer>();}if (renderer != null && renderer.isVisible){GameCameras.instance.cameraShakeFSM.SendEvent(eventName);}}}
在面板中添加好参数后,我们就到HealthManager中:
public class HealthManager : MonoBehaviour, IHitResponder
{private EnemyDeathEffects enemyDeathEffects;protected void Awake(){enemyDeathEffects = GetComponent<EnemyDeathEffects>();}public void Die(float? attackDirection, AttackTypes attackType, bool ignoreEvasion){if (isDead){ return;}if (sprite){sprite.color = Color.white;}FSMUtility.SendEventToGameObject(gameObject, "ZERO HP", false);if (hasSpecialDeath){NonFatalHit(ignoreEvasion);return;}isDead = true;if(damageHero != null){damageHero.damageDealt = 0;}if(battleScene != null && !notifiedBattleScene){PlayMakerFSM playMakerFSM = FSMUtility.LocateFSM(battleScene, "Battle Control");if(playMakerFSM != null){FsmInt fsmInt = playMakerFSM.FsmVariables.GetFsmInt("Battle Enemies");if(fsmInt != null){fsmInt.Value--;notifiedBattleScene = true;}}}if (enemyDeathEffects != null){if (attackType == AttackTypes.Generic){enemyDeathEffects.doKillFreeze = false;}enemyDeathEffects.RecieveDeathEvent(attackDirection, deathReset, attackType == AttackTypes.Spell, false);}SendDeathEvent();Destroy(gameObject); //TODO:}
}
还有我们将蚊子那期介绍的老朋友攻击距离检测,警戒距离检测
还有类似于玩家的slash的polygon collider2d,别忘了给它们添加上damagehero脚本

还有一个就是当僵尸虫进入冲刺状态上播放的粒子系统dust:


接下来就到了我们老朋友playmakerFSM登场了,老规矩我先贴出来变量和事件然后逐个讲状态:



第一个状态当然是初始化了:

每一帧都检测玩家是否到可视范围和攻击范围了

判断玩家位置:


我们先做好playmakerFSM自定义脚本:
using HutongGames.PlayMaker;
using UnityEngine;[ActionCategory("Hollow Knight")]
public class SetInvincible : FsmStateAction
{[UIHint(UIHint.Variable)]public FsmOwnerDefault target;public FsmBool Invincible;public FsmInt InvincibleFromDirection;public override void Reset(){target = new FsmOwnerDefault();Invincible = null;InvincibleFromDirection = null;}public override void OnEnter(){GameObject safe = target.GetSafe(this);if (safe != null){HealthManager component = safe.GetComponent<HealthManager>();if (component != null){if (!Invincible.IsNone){component.IsInvincible = Invincible.Value;}if (!InvincibleFromDirection.IsNone){component.InvincibleFromDirection = InvincibleFromDirection.Value;}}}Finish();}}
using HutongGames.PlayMaker;
using UnityEngine;[ActionCategory("Hollow Knight")]
public class SetWalkerFacing : WalkerAction
{public FsmBool walkRight;public FsmBool randomStartDir;public override void Reset(){base.Reset();walkRight = new FsmBool{UseVariable = true};randomStartDir = new FsmBool();}/// <summary>/// 调用Walker.cs中的ChangeFacing函数来改变朝向/// </summary>/// <param name="walker"></param>protected override void Apply(Walker walker){if (randomStartDir.Value){walker.ChangeFacing((Random.Range(0, 2) == 0) ? -1 : 1);return;}if (!walkRight.IsNone){walker.ChangeFacing(walkRight.Value ? 1 : -1);}}}



剩下三个都差不多,你只需要设置好tk2d动画,Lung1 Speed和Lung2 Speed的正负,判断不同方向的事件,以及sheild格挡的方向:


触发BLOCKED HIT的事件该执行的状态:


这个BLOCKED HIT的事件在HealthManager.cs中会触发的,让我们回到HealthManager.cs脚本中:
public 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;}
那么这个方法在哪里被调用呢,当然是我们的HIT方法了,如果你还有印象,这个就是继承接口IHitResponder要实现的方法:
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);}
判断当前攻击方向是否格挡:
[Header("Invincible")][SerializeField] private bool invincible;[SerializeField] private int invincibleFromDirection;public bool IsBlockingByDirection(int cardinalDirection,AttackTypes attackType){
//法术攻击无法格挡if(attackType == AttackTypes.Spell && gameObject.CompareTag("Spell Vulnerable")){return false;}
//不是无敌无法格挡if (!invincible){return false;}
//没有确切的方向无法格挡if(invincibleFromDirection == 0){return true;}switch (cardinalDirection){case 0:{int num = invincibleFromDirection;if (num <= 5){if (num != 1 && num != 5){return false;}}else if (num != 8 && num != 10){return false;}return true;} case 1:{int num = invincibleFromDirection;return num == 2 || num - 5 <= 4;}case 2:{int num = invincibleFromDirection;if (num <= 6){if (num != 3 && num != 6){return false;}}else if (num != 9 && num != 11){return false;}return true;}case 3:{int num = invincibleFromDirection;return num == 4 || num - 7 <= 4;}default:return false;}}
格挡以后自然是要对玩家发起攻击了:
首先进入准备阶段:

向前冲刺阶段:


攻击阶段,在这里就要打开我们创建的slash的碰撞箱了,同时将僵尸虫的速度位置为0

冷却阶段,关闭碰撞箱:

然后突然虚晃一枪接着对玩家发动二段攻击,没有准备阶段直接进入冲刺攻击阶段

二阶段的冷却:

三阶段的再次冲刺攻击阶段:

三阶段的攻击阶段:

三阶段的冷却阶段,应该不叫冷却而是叫停止攻击阶段:

重置walker状态:

除了自动攻击我们还有主动攻击阶段,内容我就不赘述了直接上图:



还有就是玩家离开可视范围和攻击距离发送LEFT RANGE事件,把举起的盾牌放下来


总结
我们来看看上述讲到的三个场景的效果(上面的UI先别管我之后会完成的):
虫子的转身没问题的



平台没问题的
然后僵尸盾牌虫:直接攻击Attack 1


它在反击后的攻击是水平方向的,因此我可以下劈它

由于我没做格挡时的动画,虽然不明显但是还是能看到敌人并没有因为我的攻击而受到伤害。。


三段攻击也有,但我忘了截屏了就先这样吧,下一期我们来制作更多的敌人和更多的场景
相关文章:
[Unity Demo]从零开始制作空洞骑士Hollow Knight第十五集:制作更多地图,更多敌人,更多可交互对象
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言一、第一个代表性场景 1.制作更多敌人2.制作更多可交互对象二、第二个代表性场景 1.制作更多敌人2.制作更多可交互对象三、第三个代表性场景 1.制作更多敌人2.制…...
在Openshift上安装MetalLB
1.部署MetalLB Operator 2.部署AddressPool addresses 必须和ocp节点在同一网段 apiVersion: metallb.io/v1beta1 kind: AddressPool metadata:name: metallb-ipaddressnamespace: metallb-system spec:addresses:- 192.168.1.51-192.168.1.60- 192.168.1.61-192.168.1.70aut…...
mysql其他对象
一、存储引擎 mysql的存储引擎包括: InnoDB,MyISAM,Memory(Heap),Archive,CSV,NDB Cluster 常用的只有前两个。 InnoDB与MyISAM的区别: InnoDB 简介:Inn…...
英语单词之社会生活之聚会
一些关于聚会的单词和短语 句子 English中文What’s the plan?计划是什么?I’m going out with some friends.我要跟几个朋友一起出去。I don’t really feel like going out.我不是很想出去。What time suits you ?你什么时间合适?Where shall we m…...
Qt - 地图相关 —— 1、加载百度在线地图(附源码)
效果图 开始加载地图 1、百度地图开发者网站中注册,获取密钥 2、进入开发文档中 将下图内容保存到本地文件中,文件名为"index.html"文件即可。接着将内容中的“您的密钥”改为刚刚创建应用出来的AK密钥即可。 然后双击打开若在浏览器中正常看到下图右侧地图则说明没…...
Elasticsearch 简单使用
Elasticsearch 安装和基本操作 一、引言 Elasticsearch 是一个基于 Lucene 构建的开源分布式搜索引擎,提供了实时的搜索和数据分析能力。它广泛应用于日志分析、全文搜索、数据可视化等场景。本文将详细介绍 Elasticsearch 的安装步骤及基本操作,包括索…...
基于SpringBoot+Vue+uniapp微信小程序的垃圾分类系统的详细设计和实现(源码+lw+部署文档+讲解等)
项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念,提供了一套默认的配置,让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…...
基于深度学习的车辆车型检测识别系统(YOLOV5)
界面图: 项目简介: 网络:深度学习网络 yoloV5 软件:PycharmAnaconda 环境:python3.8 opencv PyQt5 torch1.9 文件:训练集8000张图片 测试集1000张图片 系统包含所有文件夹 环境文件 UI文件 功能&a…...
Java开发中知识点整理
正则表达式 测试网址 Git 分支和主分支有冲突 先checkout origin/分支把origin/master pull进本地分支 修改冲突MergeCommit and Push...
【css-在一个元素中设置font-size和实际渲染字体大小不一致】
首先,这个不是bug,是Chromium内核提高移动端文本可读性的一个特性,叫做这个特性被称做「Text Autosizer」,又称「Font Boosting」、「Font Inflation」 解决方案: 使用-webkit-text-size-adjust 给元素设置 -webkit-te…...
LabVIEW提高开发效率技巧----用户权限控制
在LabVIEW开发中,用户权限控制是一个重要的设计模块,尤其在多用户系统中,它可以确保数据安全并控制不同用户的操作权限。为了实现用户权限控制,可以通过角色与权限管理模块来进行设计和实施。以下将从多个角度详细说明如何在LabVI…...
如何快速学会盲打
今天就来给大家分享一下如何快速学会盲打 盲打的基本方法和步骤 手指放置:将双手放在键盘上,左手食指放在F键上,右手食指放在J键上,其他手指分别放在相邻的键位上。熟悉键盘布局:学习26个字母的位置,以及…...
如何通过外链组合套餐提升外贸网站的整体表现?
在SEO优化中,单一的外链形式很难覆盖所有需求,特别是对于那些竞争激烈的行业。通过高低搭配的外链组合套餐成为越来越多企业的选择 简单来说,外链组合套餐是将不同质量、不同类型的外链进行合理搭配,从而最大化地提升网站的多维度…...
MySQL—事务
目录 1.事务的简介: 2.使用事务 2.1 开启事务 2.2 自动提交 2.3 使用范围 2.4 事务的属性 1.事务的简介: 介绍事务之前,我们先来看一个经典的场景:银行转账。 假如a想要把自己的账户上的10万块钱转到b账户上,这…...
二、PyCharm基本设置
PyCharm基本设置 前言一、设置中文汉化二、设置代码字体颜色三、设置鼠标滚轮调整字体大小四、修改 PyCharm 运行内存4.1 方式一4.1 方式二 五、显示 PyCharm 运行时内存六、设置代码模版配置的参数有: 七、PyCharm设置背景图总结 前言 为了让我们的 PyCharm 更好用…...
SSH流量秒变HTTPS —— 筑梦之路
背景说明 很多时候对外开放仅有80 443端口,若想要ssh服务器是比较困难的。这里介绍使用opensslHAProxy绕过限制。 解决思路 把SSH流量伪装成HTTPS流量,从443端口传输。 OpenSSL: 一个强大的开源加密工具包,我们用它来给SSH流量加密,让它看起来像HTTPS。 HAProxy…...
tkinter Listbox 列表框实现多列对齐排列并绑定下拉框和滚动条
from tkinter import * from tkinter import ttk, Button, Canvas, Listbox, Entry, LabelFrame, IntVar, Checkbutton, messageboximport win32print root Tk() root.title("tkinter Listbox 列表框实现多列对齐排列") root.geometry(550x450)def callback2(t, eve…...
Kafka 启用 JMX
以下是在 Kafka 服务启动时启用 JMX 的步骤: 找到 Kafka 的启动脚本,通常在 Kafka 安装目录的 bin 子目录下 编辑启动脚本(例如 kafka-server-start.sh),在其中设置 JMX 参数。 在启动脚本中添加以下环境变量设置&a…...
G1(Garbage First)垃圾回收实战
GC过程 G1(Garbage First)是JVM中的一种垃圾回收器,设计用于处理具有大堆内存的应用程序,减少GC停顿时间,并提供更可预测的垃圾回收性能。G1的垃圾回收过程主要分为以下几个阶段: 1. 年轻代垃圾回收&…...
ESP32-IDF 通用定时器 GPTimer
目录 一、基本介绍1、配置结构体1.1 gptimer_config_t1.2 gptimer_event_callbacks_t1.3 gptimer_alarm_config_t 2、常用 API2.1 gptimer_new_timer2.2 gptimer_del_timer2.3 gptimer_set_raw_count2.4 gptimer_get_raw_count2.5 gptimer_get_resolution2.6 gptimer_get_captu…...
龙虎榜——20250610
上证指数放量收阴线,个股多数下跌,盘中受消息影响大幅波动。 深证指数放量收阴线形成顶分型,指数短线有调整的需求,大概需要一两天。 2025年6月10日龙虎榜行业方向分析 1. 金融科技 代表标的:御银股份、雄帝科技 驱动…...
【Linux】shell脚本忽略错误继续执行
在 shell 脚本中,可以使用 set -e 命令来设置脚本在遇到错误时退出执行。如果你希望脚本忽略错误并继续执行,可以在脚本开头添加 set e 命令来取消该设置。 举例1 #!/bin/bash# 取消 set -e 的设置 set e# 执行命令,并忽略错误 rm somefile…...
java 实现excel文件转pdf | 无水印 | 无限制
文章目录 目录 文章目录 前言 1.项目远程仓库配置 2.pom文件引入相关依赖 3.代码破解 二、Excel转PDF 1.代码实现 2.Aspose.License.xml 授权文件 总结 前言 java处理excel转pdf一直没找到什么好用的免费jar包工具,自己手写的难度,恐怕高级程序员花费一年的事件,也…...
iPhone密码忘记了办?iPhoneUnlocker,iPhone解锁工具Aiseesoft iPhone Unlocker 高级注册版分享
平时用 iPhone 的时候,难免会碰到解锁的麻烦事。比如密码忘了、人脸识别 / 指纹识别突然不灵,或者买了二手 iPhone 却被原来的 iCloud 账号锁住,这时候就需要靠谱的解锁工具来帮忙了。Aiseesoft iPhone Unlocker 就是专门解决这些问题的软件&…...
数据链路层的主要功能是什么
数据链路层(OSI模型第2层)的核心功能是在相邻网络节点(如交换机、主机)间提供可靠的数据帧传输服务,主要职责包括: 🔑 核心功能详解: 帧封装与解封装 封装: 将网络层下发…...
全面解析各类VPN技术:GRE、IPsec、L2TP、SSL与MPLS VPN对比
目录 引言 VPN技术概述 GRE VPN 3.1 GRE封装结构 3.2 GRE的应用场景 GRE over IPsec 4.1 GRE over IPsec封装结构 4.2 为什么使用GRE over IPsec? IPsec VPN 5.1 IPsec传输模式(Transport Mode) 5.2 IPsec隧道模式(Tunne…...
AI,如何重构理解、匹配与决策?
AI 时代,我们如何理解消费? 作者|王彬 封面|Unplash 人们通过信息理解世界。 曾几何时,PC 与移动互联网重塑了人们的购物路径:信息变得唾手可得,商品决策变得高度依赖内容。 但 AI 时代的来…...
Xen Server服务器释放磁盘空间
disk.sh #!/bin/bashcd /run/sr-mount/e54f0646-ae11-0457-b64f-eba4673b824c # 全部虚拟机物理磁盘文件存储 a$(ls -l | awk {print $NF} | cut -d. -f1) # 使用中的虚拟机物理磁盘文件 b$(xe vm-disk-list --multiple | grep uuid | awk {print $NF})printf "%s\n"…...
NXP S32K146 T-Box 携手 SD NAND(贴片式TF卡):驱动汽车智能革新的黄金组合
在汽车智能化的汹涌浪潮中,车辆不再仅仅是传统的交通工具,而是逐步演变为高度智能的移动终端。这一转变的核心支撑,来自于车内关键技术的深度融合与协同创新。车载远程信息处理盒(T-Box)方案:NXP S32K146 与…...
【电力电子】基于STM32F103C8T6单片机双极性SPWM逆变(硬件篇)
本项目是基于 STM32F103C8T6 微控制器的 SPWM(正弦脉宽调制)电源模块,能够生成可调频率和幅值的正弦波交流电源输出。该项目适用于逆变器、UPS电源、变频器等应用场景。 供电电源 输入电压采集 上图为本设计的电源电路,图中 D1 为二极管, 其目的是防止正负极电源反接, …...
