当前位置: 首页 > news >正文

[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第十五集:制作更多地图,更多敌人,更多可交互对象

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、第一个代表性场景 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的存储引擎包括&#xff1a; InnoDB&#xff0c;MyISAM&#xff0c;Memory&#xff08;Heap&#xff09;&#xff0c;Archive&#xff0c;CSV&#xff0c;NDB Cluster 常用的只有前两个。 InnoDB与MyISAM的区别&#xff1a; InnoDB 简介&#xff1a;Inn…...

英语单词之社会生活之聚会

一些关于聚会的单词和短语 句子 English中文What’s the plan?计划是什么&#xff1f;I’m going out with some friends.我要跟几个朋友一起出去。I don’t really feel like going out.我不是很想出去。What time suits you ?你什么时间合适&#xff1f;Where shall we m…...

Qt - 地图相关 —— 1、加载百度在线地图(附源码)

效果图 开始加载地图 1、百度地图开发者网站中注册,获取密钥 2、进入开发文档中 将下图内容保存到本地文件中,文件名为"index.html"文件即可。接着将内容中的“您的密钥”改为刚刚创建应用出来的AK密钥即可。 然后双击打开若在浏览器中正常看到下图右侧地图则说明没…...

Elasticsearch 简单使用

Elasticsearch 安装和基本操作 一、引言 Elasticsearch 是一个基于 Lucene 构建的开源分布式搜索引擎&#xff0c;提供了实时的搜索和数据分析能力。它广泛应用于日志分析、全文搜索、数据可视化等场景。本文将详细介绍 Elasticsearch 的安装步骤及基本操作&#xff0c;包括索…...

基于SpringBoot+Vue+uniapp微信小程序的垃圾分类系统的详细设计和实现(源码+lw+部署文档+讲解等)

项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念&#xff0c;提供了一套默认的配置&#xff0c;让开发者可以更专注于业务逻辑而不是配置文件。Spring Boot 通过自动化配置和约…...

基于深度学习的车辆车型检测识别系统(YOLOV5)

界面图&#xff1a; 项目简介&#xff1a; 网络&#xff1a;深度学习网络 yoloV5 软件&#xff1a;PycharmAnaconda 环境&#xff1a;python3.8 opencv PyQt5 torch1.9 文件&#xff1a;训练集8000张图片 测试集1000张图片 系统包含所有文件夹 环境文件 UI文件 功能&a…...

Java开发中知识点整理

正则表达式 测试网址 Git 分支和主分支有冲突 先checkout origin/分支把origin/master pull进本地分支 修改冲突MergeCommit and Push...

【css-在一个元素中设置font-size和实际渲染字体大小不一致】

首先&#xff0c;这个不是bug&#xff0c;是Chromium内核提高移动端文本可读性的一个特性&#xff0c;叫做这个特性被称做「Text Autosizer」&#xff0c;又称「Font Boosting」、「Font Inflation」 解决方案&#xff1a; 使用-webkit-text-size-adjust 给元素设置 -webkit-te…...

LabVIEW提高开发效率技巧----用户权限控制

在LabVIEW开发中&#xff0c;用户权限控制是一个重要的设计模块&#xff0c;尤其在多用户系统中&#xff0c;它可以确保数据安全并控制不同用户的操作权限。为了实现用户权限控制&#xff0c;可以通过角色与权限管理模块来进行设计和实施。以下将从多个角度详细说明如何在LabVI…...

如何快速学会盲打

今天就来给大家分享一下如何快速学会盲打 盲打的基本方法和步骤 手指放置&#xff1a;将双手放在键盘上&#xff0c;左手食指放在F键上&#xff0c;右手食指放在J键上&#xff0c;其他手指分别放在相邻的键位上。熟悉键盘布局&#xff1a;学习26个字母的位置&#xff0c;以及…...

如何通过外链组合套餐提升外贸网站的整体表现?

在SEO优化中&#xff0c;单一的外链形式很难覆盖所有需求&#xff0c;特别是对于那些竞争激烈的行业。通过高低搭配的外链组合套餐成为越来越多企业的选择 简单来说&#xff0c;外链组合套餐是将不同质量、不同类型的外链进行合理搭配&#xff0c;从而最大化地提升网站的多维度…...

MySQL—事务

目录 1.事务的简介&#xff1a; 2.使用事务 2.1 开启事务 2.2 自动提交 2.3 使用范围 2.4 事务的属性 1.事务的简介&#xff1a; 介绍事务之前&#xff0c;我们先来看一个经典的场景&#xff1a;银行转账。 假如a想要把自己的账户上的10万块钱转到b账户上&#xff0c;这…...

二、PyCharm基本设置

PyCharm基本设置 前言一、设置中文汉化二、设置代码字体颜色三、设置鼠标滚轮调整字体大小四、修改 PyCharm 运行内存4.1 方式一4.1 方式二 五、显示 PyCharm 运行时内存六、设置代码模版配置的参数有&#xff1a; 七、PyCharm设置背景图总结 前言 为了让我们的 PyCharm 更好用…...

SSH流量秒变HTTPS —— 筑梦之路

背景说明 很多时候对外开放仅有80 443端口&#xff0c;若想要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 的步骤&#xff1a; 找到 Kafka 的启动脚本&#xff0c;通常在 Kafka 安装目录的 bin 子目录下 编辑启动脚本&#xff08;例如 kafka-server-start.sh&#xff09;&#xff0c;在其中设置 JMX 参数。 在启动脚本中添加以下环境变量设置&a…...

G1(Garbage First)垃圾回收实战

GC过程 G1&#xff08;Garbage First&#xff09;是JVM中的一种垃圾回收器&#xff0c;设计用于处理具有大堆内存的应用程序&#xff0c;减少GC停顿时间&#xff0c;并提供更可预测的垃圾回收性能。G1的垃圾回收过程主要分为以下几个阶段&#xff1a; 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…...

C#学习笔记(十)

C#学习笔记&#xff08;十&#xff09; 第七章 对象的构造方法与实例方法一、对象的构造方法1. 构造方法初识2. 构造方法的创建3. this关键字4. 构造方法的规范和重载4.1 构造方法的规范 5. 对象初始化器5.1 对象初始化器和构造方法的区别 二、对象的实例方法1. 简单应用2.实例…...

出手!快手可灵开源版,AI视频生成整合包!

在2024年&#xff0c;人工智能领域迎来了一位新星——AI视频生成技术。在这场技术革命中&#xff0c;快手推出的可灵AI无疑是最耀眼的明星之一。然而&#xff0c;其高昂的年费让不少用户望而却步&#xff0c;毕竟数千元的开销对于普通人来说是个不小的负担。 幸运的是&#xff…...

【Linux】进程池

目录 进程池 进程池的概念&#xff1a; 手搓进程池&#xff1a; 1、创建信道和子进程 2、通过channel控制子进程 3、回收管道和子进程 进程池 进程池的概念&#xff1a; 定义一个池子&#xff0c;在里面放上固定数量的进程&#xff0c;有需求来了&#xff0c;就拿一个池中…...

实验23:DA呼吸灯实验

电路硬件: 实现功能: 代码: public.h #ifndef _public_H #define _public_H#include "reg52.h" //#include "key.h"typedef unsigned int u16; typedef unsigned char u8;void delay_10us(u16 n); void delay_ms(u16 ms);#endif public.c #include …...

安科瑞智慧能源管理系统EMS3.0在浙江某能源集团有限公司的应用

安科瑞戴婷 Acrel-Fanny 一、项目背景 浙江某能源集团有限公司位于浙江省宁波前湾新区&#xff0c;主营业务范围包括了储能技术服务&#xff0c;光伏风力发电技术服务&#xff0c;充电桩技术服务&#xff0c;新能源项目的施工以及为企业提供配电房运维服务。 随着新能源的兴…...

线性代数学习

1.标量由只有一个元素的张量表示 import torchx torch.tensor([3,0]) y torch.tensor([2,0])x y, x * y, x / y, x**y 2.可以将向量视为标量值组成的列表 x torch.arange(4) x 3.通过张量的索引访问任一元素 x[3] 4.访问张量长度 len(x) 5.只有一个轴的张量&#xff0c…...

FineReport 数据显示格式

原始 修改 选择「单元格元素>格式」&#xff0c;选择「日期型」&#xff0c;改成 「yyyy 年 MM 月 dd 日」&#xff0c;如下图所示&#xff1a; 注&#xff1a;若列表中没有 yyyy 年 MM 月 dd 日 格式&#xff0c;可手动输入 选择运货费数据列单元格&#xff0c;选择「单元…...

leetcode.204.计数质数

#中等#枚举 给定整数 n &#xff0c;返回 所有小于非负整数 n 的质数的数量 。 埃氏筛 枚举没有考虑到数与数的关联性&#xff0c;因此难以再继续优化时间复杂度。接下来我们介绍一个常见的算法&#xff0c;该算法由希腊数学家厄拉多塞&#xff08;Eratosthenes&#xff09;提…...

Mysql环境安装

1&#xff0c;下载压缩包 下载压缩包解压 2&#xff0c;配置环境变量 i&#xff0c;高级系统设置-->环境变量-->系统变量-->path-->添加mysql的bin目录路径 ii&#xff0c;新建my.ini文件 basedir:MYSQL的路径 datadir&#xff1a;这个data路径不用手动创建&am…...

请问平面仓系统的盘点如何做?

盘点流程 一、盘点任务生成 手动发起&#xff1a;仓库管理人员可以根据实际需要&#xff0c;在系统中手动发起库存盘点任务。例如&#xff0c;定期进行全盘、抽盘或者在发现库存数据异常时发起盘点。自动触发&#xff1a;系统可以设置自动触发盘点的条件&#xff0c;如每隔一…...