[Unity Demo]从零开始制作空洞骑士Hollow Knight第四集:制作更多的敌人

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

文章目录

  • 前言
  • 一、制作敌人僵尸虫Zombie
    • 1.公式化导入制作僵尸虫Zombie素材
    • 2.制作僵尸虫Zombie的Walker.cs状态机
    • 3.制作敌人僵尸虫的playmaker状态机
  • 二、制作敌人爬虫Climber
    • 1.公式化导入制作爬虫Climber素材
    • 2.制作虫Climber的Climber.cs状态机
  • 总结


前言

        大家好久不见,终于放完中秋假期可以回自己家里打代码了,上一期我介绍了为什么和如何导入2D toolkit以及制作一个完整的敌人(就是那只蚊子包括了精灵,动画,状态机行为以及脚本)现在不妨趁热打铁制作更多的敌人。

        (PS:上面的内容是我还没开始做的时候就写好的,本以为是个很简单的事情但其实工作量还是很大,所以确保你能一步步根据我的想法理解我在干什么)

        这期我们制作的敌人当然都是来自遗忘十字路的僵尸虫zombie以及沿着墙壁爬行和旋转的爬虫Climber

一、制作敌人僵尸虫Zombie

1.公式化导入制作僵尸虫Zombie素材

        首先我们先完成僵尸虫的完整行为,第一步导入素材,分别为它制作tk2dspritecollection和tk2dspriteanimation,

 其实这些播放时间Clip time不一定要跟我一样,你觉得多少合理就填多少,因为这是一个僵尸虫所以我会让它的播放时间相对长点看起来更像一个僵尸。

 然后就是公式化设置一个敌人的:

经典子对象Attack Range还要给它一个检测玩家位置的脚本Alert Range.cs,这个我们上一期讲到过

再给它一个粒子系统,让僵尸虫在除了静止状态以外播放这个粒子系统。

 2.制作僵尸虫Zombie的Walker.cs状态机

我们可以先想想它会有什么状态,经过我的分析它会有初始化Initialize,Idle站立,走路Walk,转向Turn,发现alert,攻击系统可以分为多个阶段:进入范围后的准备攻击attack anticipate,攻击attack lunge,攻击冷却时间attack cooldown,受伤Hurt,死亡Dead,所以我们需要在playmaker fsm中创建state,然后分别想想它在每一个状态中会发生什么行为action。

同样他也需要每一个敌人都应该有的Lineofsightdetector.cs,这个就是敌人发现敌人到自己攻击范围的脚本

除此之外,我们还要想想僵尸虫和上期的蚊子有什么区别,答案是一个是陆地上的一个是天空的,在地上意味着更多的射线检测(如检测碰墙后转向),更多的状态(比如。。没有比如,这是我自己感觉的),所以我们要给它创建一个基类脚本的就叫Walker.cs来管理陆地敌人的基本行为,同样我们还需要使用状态机来实现整个敌人状态机的切换。

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; //防止转向玩家的位置private Walker.States state;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的方法。/// 我们通过States来展示当前的状态是什么,并在该状态下都有哪些行为需要完成/// </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;}}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();}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){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);}public void ChangeFacing(int facing){if(state == States.Turning){turningFacing = facing;currentFacing = -facing;return;}currentFacing = facing;}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(); //开始进入移动状态}}
//开始进入停止移动状态,如果原因是bored则还有其它处理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;}EndStoppping();}}/// <summary>/// 在Update中被调用,执行停止Stop的状态/// </summary>private void UpdateStopping(){if(stopReason == StopReasons.Bored){pauseTimeRemaining -= Time.deltaTime;if(pauseTimeRemaining <= 0f){EndStoppping();}}}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;}BeginTurning(currentFacing);}private void BeginWalkingOrTurning(int facing){if(currentFacing == facing){BeginWalking(facing);return;}BeginTurning(facing);}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();}Debug.LogFormat("facing = " + facing);body.velocity = new Vector2((facing > 0) ? walkSpeedR : walkSpeedL,body.velocity.y);}/// <summary>/// 在Update中被调用,执行Walking状态/// </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(transform.position + new Vector3((bodyCollider.bounds.extents.x + 0.5f + edgeXAdjuster) * (float)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}}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 * (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;}
}

可以看到,每一种状态都有Begin,Update和End开头的三大方法分别负责进入,更新行为,终止一个状态。 

 其中结构体Sweep负责我们提供正确方向的射线检测以及发射多少条射线保证能在一个范围的扫射,纳闷怎么保证正确方向的射线检测呢,这里我们用DirectionalUtils静态类来实现:

using System;
using UnityEngine;public static class DirectionUtils
{public const int Right = 0;public const int Up = 1;public const int Left = 2;public const int Down = 3;public static int GetCardinalDirection(float degrees){return DirectionUtils.NegSafeMod(Mathf.RoundToInt(degrees / 90f), 4); //取一个最接近degrees / 90的整数,再获取它最靠近的方向}public static int NegSafeMod(int val, int len){return (val % len + len) % len;}public static int GetX(int cardinalDirection){int num = cardinalDirection % 4;if (num == 0){return Up;}if (num != 2){return Right;}return -1;}public static int GetY(int cardinalDirection){int num = cardinalDirection % 4;if (num == 1){return Up;}if (num != 3){return Right;}return -1;}}

同样我们还需要更安全的静态函数获取对象变量的静态类Extensions:

 这个类相当于小小型Mathf函数,以后就可以在这里调用你想要用的方法了。        

using System;
using System.Collections;
using UnityEngine;public static  class Extensions 
{public static void SetScaleX(this Transform t, float newXScale){t.localScale = new Vector3(newXScale, t.localScale.y, t.localScale.z);}public static float GetPositionX(this Transform t){return t.position.x;}public static Vector2 MultiplyElements(this Vector2 self, Vector2 other){Vector2 result = self;result.x *= other.x;result.y *= other.y;return result;}}

回到Unity中,这里我就随便设置一下仅供参考,你们可以按照自己的想法自己填,但有些东西不能动的,详见代码段上面的注释(比如你不能勾选这个Prevent Scale Change,不然它的Scale就不能* -1就不会转向了)

 

 3.制作敌人僵尸虫的playmaker状态机

        你可能会想,我不是都通过Walker.cs代码制作了僵尸虫的状态机了吗,还有playmaker啥事吗?其实,我有一些状态是想通过playmaker来实现,就是前面一直没设计攻击状态,现在的僵尸虫只是一套空壳而已,它并不会在玩家进入alert range后来攻击方向,这就需要我们的设计感了。

首先添加变量和事件:

 本期我们还需要自定义playmaker.actions下面的脚本,你可以注意到我已经在Walker.cs中放了一些public类型的方法来供这些自定义脚本直接调用。

而且这些自定义脚本的功能通常都比较简单,一般是应用一个方法进入一个状态机,所以我们可以先创建一个抽象类,让所有跟Walker.cs有关的自定义脚本都继承自它,这样至少不会忘了要调用了:

using HutongGames.PlayMaker;
using UnityEngine;[ActionCategory("Hollow Knight")]
public abstract class WalkerAction : FsmStateAction
{public FsmOwnerDefault target;public bool everyFrame;private Walker walker;protected abstract void Apply(Walker walker);public override void Reset(){base.Reset();target = new FsmOwnerDefault();everyFrame = false;}public override void OnEnter(){base.OnEnter();GameObject safe = target.GetSafe(this);if(safe != null){walker = safe.GetComponent<Walker>();if(walker != null){Apply(walker);}}else{walker = null;}if (!everyFrame){Finish();}}public override void OnUpdate(){base.OnUpdate();if(walker != null){Apply(walker);}}}
using HutongGames.PlayMaker;[ActionCategory("Hollow Knight")]
public class StartWalker : WalkerAction
{public FsmBool walkRight;public override void Reset(){base.Reset();walkRight = new FsmBool{UseVariable = true};}/// <summary>/// 调用了walker的两个方法,如果不存在walkright就按原计划接着走路/// 如果存在则根据方向判断行走的方向/// </summary>/// <param name="walker"></param>protected override void Apply(Walker walker){if (walkRight.IsNone){walker.StartMoving();}else{walker.Go(walkRight.Value ? 1 : -1);}walker.ClearTurnCoolDown();}
}
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);}}}
using HutongGames.PlayMaker;[ActionCategory("Hollow Knight")]
public class StopWalker : WalkerAction
{/// <summary>/// 调用walker.cs的Stop函数并原因为controlled/// </summary>/// <param name="walker"></param>protected override void Apply(Walker walker){walker.Stop(Walker.StopReasons.Controlled);}
}

 

using System;
using UnityEngine;namespace HutongGames.PlayMaker.Actions
{
//随机播放一个audioclip[ActionCategory(ActionCategory.Audio)]public class AudioPlayRandom : FsmStateAction{[RequiredField][CheckForComponent(typeof(AudioSource))][Tooltip("The GameObject with an AudioSource component.")]public FsmGameObject gameObject;[CompoundArray("Audio Clips", "Audio Clip", "Weight")]public AudioClip[] audioClips;[HasFloatSlider(0f, 1f)]public FsmFloat[] weights;public FsmFloat pitchMin;public FsmFloat pitchMax;private AudioSource audio;public AudioPlayRandom(){pitchMin = 1f;pitchMax = 2f;}public override void Reset(){gameObject = null;audioClips = new AudioClip[3];weights = new FsmFloat[]{1f,1f,1f};pitchMin = 1f;pitchMax = 1f;}public override void OnEnter(){DoPlayRandomClip();Finish();}private void DoPlayRandomClip(){if (audioClips.Length == 0){return;}audio = gameObject.Value.GetComponent<AudioSource>();int randomWeightedIndex = ActionHelpers.GetRandomWeightedIndex(weights);if (randomWeightedIndex != -1){AudioClip audioClip = audioClips[randomWeightedIndex];if (audioClip != null){float pitch = UnityEngine.Random.Range(pitchMin.Value, pitchMax.Value);audio.pitch = pitch;audio.PlayOneShot(audioClip);}}}}}
using UnityEngine;namespace HutongGames.PlayMaker.Actions
{
//开始播放一个粒子系统[ActionCategory("Particle System")][Tooltip("Set particle emission on or off on an object with a particle emitter")]public class PlayParticleEmitter : FsmStateAction{[RequiredField][Tooltip("The particle emitting GameObject")]public FsmOwnerDefault gameObject;public FsmInt emit;public override void Reset(){gameObject = null;emit = new FsmInt(0);}public override void OnEnter(){if(gameObject != null){GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);if (ownerDefaultTarget != null){ParticleSystem component = ownerDefaultTarget.GetComponent<ParticleSystem>();if (component && !component.isPlaying && emit.Value <= 0){component.Play();}else if (emit.Value > 0){component.Emit(emit.Value);}}}Finish();}}}

 

using UnityEngine;namespace HutongGames.PlayMaker.Actions
{
//停止播放一个粒子系统
[ActionCategory("Particle System")]
[Tooltip("Set particle emission on or off on an object with a particle emitter")]public class StopParticleEmitter : FsmStateAction{[RequiredField][Tooltip("The particle emitting GameObject")]public FsmOwnerDefault gameObject;public override void Reset(){gameObject = null;}public override void OnEnter(){if (gameObject != null){GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);if (ownerDefaultTarget != null){ParticleSystem component = ownerDefaultTarget.GetComponent<ParticleSystem>();if (component && component.isPlaying){component.Stop();}}}Finish();}}}
using System;
using UnityEngine;namespace HutongGames.PlayMaker.Actions
{[ActionCategory(ActionCategory.Transform)][Tooltip("Sets the Scale of a Game Object. To leave any axis unchanged, set variable to 'None'.")]public class FlipScale : FsmStateAction{[RequiredField][Tooltip("The GameObject to scale.")]public FsmOwnerDefault gameObject;public bool flipHorizontally;public bool flipVertically;[Tooltip("Repeat every frame.")]public bool everyFrame;[Tooltip("Perform in LateUpdate. This is useful if you want to override the position of objects that are animated or otherwise positioned in Update.")]public bool lateUpdate;public override void Reset(){flipHorizontally = false;flipVertically = false;everyFrame = false;}public override void OnEnter(){DoFlipScale();if (!everyFrame){Finish();}}public override void OnUpdate(){if (!lateUpdate){DoFlipScale();}	}public override void OnLateUpdate(){if (lateUpdate){DoFlipScale();}if (!everyFrame){Finish();}}private void DoFlipScale(){GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);if (ownerDefaultTarget == null){return;}Vector3 localScale = ownerDefaultTarget.transform.localScale;if (flipHorizontally){localScale.x = -localScale.x;}if (flipVertically){localScale.y = -localScale.y;}ownerDefaultTarget.transform.localScale = localScale;}}}

 制作完自定义脚本后我们可以给我们状态机添加状态了:

 

 

 ​​​​​​​

整体的playmaker图如下所示:

完整的一个僵尸虫状态机通过代码和playmaker就这样实现了!

 

二、制作敌人爬虫Climber

1.公式化导入制作爬虫Climber素材

 第一步当然是导入素材,分别为它制作tk2dspritecollection和tk2dspriteanimation,

 然后就是公式化三件套给它整上去:

注意,这个小怪的特色就是沿着墙壁走动,当到达墙壁边缘的时候翻转90度(顺or逆)所以我们给它创建一堵墙并给Layer"Terrain"还有boxcollider2d。 

 2.制作虫Climber​​​​​​​的Climber.cs一个完整的状态机

然后我们来做这个爬虫Climber,同样我们先分析他有几种可能的状态,首先肯定有Intial,walk,turn,Stun,dead。这个爬虫最有特色的地方是它会沿着一个墙体特定的选转,而且它的重力gravity绝对为0,毕竟不能掉下去,然后我们就可以编写它的脚本了

由于这个敌人的设定是不会主动攻击的,所以应该不需要Lineofsightdetector.cs的脚本来检测玩家的位置在哪里。

其实我们可以尝试只用代码来实现的完整状态机,不妨创建一个Climber.cs

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;}//TODO: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}
}

同样还需要到Extensions上书写写的便捷方法:

using System;
using System.Collections;
using UnityEngine;public static  class Extensions 
{public static void SetScaleX(this Transform t, float newXScale){t.localScale = new Vector3(newXScale, t.localScale.y, t.localScale.z);}public static void SetRotation2D(this Transform t,float rotation){Vector3 eulerAngles = t.eulerAngles;eulerAngles.z = rotation;t.eulerAngles = eulerAngles;}public static IEnumerator PlayAnimWait(this tk2dSpriteAnimator self, string anim){tk2dSpriteAnimationClip clipByName = self.GetClipByName(anim);self.Play(clipByName);yield return new WaitForSeconds(clipByName.Duration);yield return new WaitForEndOfFrame();yield break;}public static float GetPositionX(this Transform t){return t.position.x;}public static Vector2 MultiplyElements(this Vector2 self, Vector2 other){Vector2 result = self;result.x *= other.x;result.y *= other.y;return result;}}

 这里那个clipByName.Duration可能会报错因为我们的tk2dSpriteAnimationClip没有Duration属性,我们直接给它添加一个即可:

回到Unity编辑器中,我们直接填上参数:

 


总结

最终实现的游戏效果如下所示:

可以见到僵尸虫在我们没到达检测范围时的状态:

 

它会进入Walk,Stop,Turn状态:

玩家进入攻击范围时attack anticipate并lunge,还有冷却时间cooldown:

 

玩家离开它的攻击范围后:

由于我还没做小骑士attack的相关行为已经血量相关的代码,所以我们暂时看不到它死亡dead的状态。

然后是爬虫climber,我就展示它沿着墙壁走路并转向的画面展示吧:

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

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

相关文章

Gradio 自定义组件

如何使用 Gradio 自定义组件&#xff0c;Gradio 前端使用 Svelte&#xff0c;后端使用的 Python。如何自定义一个组件呢&#xff1f;Gadio 提供了类似于脚手架的命令&#xff0c;可以生成需要开发组件的前后和后端代码。 创建组件 运行如下命令&#xff0c;gradio 会自动生成…

鹏鼎控股社招校招入职SHL综合能力测评:高分攻略及真题题库解析答疑

鹏鼎控股&#xff08;深圳&#xff09;股份有限公司&#xff0c;成立于1999年4月29日&#xff0c;是一家专注于印制电路板&#xff08;PCB&#xff09;的设计、研发、制造与销售的高新技术企业。公司总部位于中国广东省深圳市&#xff0c;并在全球多个地区设有生产基地和服务中…

FinGPT金融大模型

FinGPT仓库https://github.com/AI4Finance-Foundation/FinGPT 功能&#xff1a; Adviser。根据新闻判断市场情绪&#xff08;积极、消极、中性&#xff09;&#xff0c;给出投资建议。Quantitative Trading。定制属于自己的金融助手。叫它关注某几个股票、监测消息等。可以直…

计算机网络33——文件系统

1、chmod 2、chown 需要有root权限 3、link 链接 4、unlink 创建临时文件&#xff0c;用于非正常退出 5、vi vi可以打开文件夹 ../是向外一个文件夹 6、ls ls 可以加很多路径&#xff0c;路径可以是文件夹&#xff0c;也可以是文件 ---------------------------------…

5.C_数据结构_树

概述 树的逻辑结构&#xff1a; 树中任何节点都可以有0个或多个直接后继节点&#xff0c;但最多只有一个直接前驱节点。根节点没有直接前驱节点&#xff0c;叶节点没有直接后继节点。 相关名词&#xff1a; 空树&#xff1a;树中没有节点节点的度数&#xff1a;一个节点的…

【纯小白论文代码带读】医学图像分割MASDF-Net(问题产生及解决)

论文链接&#xff1a;https://www.semanticscholar.org/paper/MASDF-Net%3A-A-Multi-Attention-Codec-Network-with-and-Fu-Deng/6ab609eb93dfd12596032174ca9603712f5c050a 代码链接&#xff1a;https://github.com/Rayicer/TransFuse 初见面代码&#xff1a; Q&am…

【字符函数】strcpy函数(字符串复制函数)+strcat函数(字符串追加)+strcmp函数(字符串比较)【笔记】

1.复制函数--------------strcpy函数 函数使用 char*strcpy&#xff08;char* destination, const char* source&#xff09; strcpy函数用于拷贝字符串&#xff0c;即将一个字符串中的内容拷贝到另一个字符串中&#xff08;会覆盖原字符串内容&#xff09;。它的参数是两个指…

大数据新视界 --大数据大厂之Kubernetes与大数据:容器化部署的最佳实践

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

Java键盘输入语句

编程输入语句 1.介绍:在编程中&#xff0c;需要接受用户输入的数据&#xff0c;就可以使用键盘输入语句来获取。 2.步骤&#xff1a; 1&#xff09;导入该类的所在包&#xff0c;java.util.* 2)创建该类对象&#xff08;声明变量&#xff09; 3&#xff09;调用里面的功能 3…

面试官:什么是CAS?存在什么问题?

大家好&#xff0c;我是大明哥&#xff0c;一个专注「死磕 Java」系列创作的硬核程序员。 回答 CAS&#xff0c;Compare And Swap&#xff0c;即比较并交换&#xff0c;它一种无锁编程技术的核心机制。其工作方式分为两步&#xff1a; 比较&#xff1a;它首先会比较内存中的某…

C++的初阶模板和STL

C的初阶模板和STL 回顾之前的内存管理&#xff0c;我们还要补充一个概念&#xff1a;内存池 也就是定位new会用到的场景&#xff0c;内存池只会去开辟空间。 申请内存也就是去找堆&#xff0c;一个程序中会有很多地方要去找堆&#xff0c;这样子效率会很低下&#xff0c;为了…

vue之我不会 计算属性 vuex 路由 插槽

一、计算属性 例子&#xff1a; 注意&#xff1a;调用计算属性时&#xff0c;不可以带括号&#xff0c;那样调用的就是方法&#xff0c;如&#xff1a;以下调用fullName时不可funnName() <div id"root">姓&#xff1a;<input type"text" v-model&…

化妆风格识别系统源码分享

化妆风格识别检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vis…

[2025]基于微信小程序慢性呼吸系统疾病的健康管理(源码+文档+解答)

博主介绍&#xff1a; ✌我是阿龙&#xff0c;一名专注于Java技术领域的程序员&#xff0c;全网拥有10W粉丝。作为CSDN特邀作者、博客专家、新星计划导师&#xff0c;我在计算机毕业设计开发方面积累了丰富的经验。同时&#xff0c;我也是掘金、华为云、阿里云、InfoQ等平台…

新手学习python第九天——加速学习

大家周末好&#xff0c;今天是周六北京时间07&#xff1a;50达到实验室&#xff0c;刚刚复习完昨天的内容&#xff0c;今天感冒有所好转&#xff0c;下午课题组有聚餐还是开心的&#xff0c;但今天的学习内容也不要落下。 ————08&#xff1a;24————开始学习———— 1…

SpringCloud微服务实现服务降级的最佳实践

Spring Cloud是一种用于快速构建分布式系统的框架&#xff0c;它提供了许多有用的功能&#xff0c;其中包括服务降级。 服务降级是一种保护机制&#xff0c;它可以在面临高并发或故障时保持服务的稳定性。当系统资源不足或服务出现故障时&#xff0c;服务降级可以通过关闭一些功…

为什么AI在广告投放上受追捧,创意上却饱受非议

AI代表着人类科技的未来&#xff0c;这已经是营销圈的共识&#xff0c;从网络上各个机构的解读来看&#xff0c;AI的奇点似乎正在临近。 AI人工智能对人类社会的震撼有两次标志性的事件&#xff1a;一次是AlphaGo战胜李世石&#xff0c; 我相信大多数人了解人工智能的开始&…

为什么是华为最先做出三折叠?这些黑科技硬核门槛缺一不可

一款起售价19999的手机&#xff0c;预约人数竟达到了600万&#xff0c;全球首款三折叠手机Mate XT到底有什么魔力&#xff0c;可以做到还未上市就引爆市场&#xff1f;看完这篇文章&#xff0c;你就知道何谓“科技新物种”。 9月7日12:08&#xff0c;华为Mate XT非凡大师开启预…

技术贴:电脑端企业微信双开教程!

软件双开的实现&#xff0c;很多小伙伴用的都是修改注册表的方式&#xff0c;这里我再介绍一个办法&#xff1a; 电脑桌面先新建一个 txt 文档&#xff0c;将下方命令全部复制&#xff0c;粘贴在 txt 文件中。 reg add HKEY_CURRENT_USER\Software\Tencent\WXWork /v multi_i…

C++第十二节课 模板初阶和string引入

一、函数模板 我们不需要写具体的函数&#xff0c;而是写这个函数的模板&#xff0c;编译器会根据模板生成对应的函数&#xff1b; template<typename T> template<class T> 两者的作用是等效的&#xff01; 用模板完成的功能有时候也叫泛型编程&#xff1b; …