[Unity Demo]从零开始制作空洞骑士Hollow Knight第五集:再制作更多的敌人
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作敌人另个爬虫Crawler
- 1.公式化导入制作另个爬虫Crawler素材
- 2.制作另个爬虫Crawler的Crawler.cs状态机
- 3.制作敌人另个爬虫Crawler的playmaker状态机
- 二、制作敌人飞虫Fly
- 1.公式化导入制作飞虫Fly素材
- 2.制作敌人飞虫Fly的playmaker状态机
- 总结
前言
如标题所示,最近感觉有些没活了,或者说是趁热打铁再制作更多的敌人,于是本期的主角就决定是两个大家刚进入空洞骑士就经常看到的敌人另一个爬虫Crawler和飞虫Fly。
一、制作敌人另一个爬虫Crawler
1.公式化导入制作另个爬虫Crawler素材
首先我们先完成Crawler的完整行为,第一步导入素材,分别为它制作tk2dspritecollection和tk2dspriteanimation,
然后就到了公式化设置一个敌人的时候了Rb2d , audiosource, boxcollider2d:
可以看到它不需要其它的子物体,因为只需要执行行走,死亡,转向的状态。
2.制作另一个爬虫Crawler的Crawler.cs状态机
创建同名函数给该游戏对象
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class Crawler : MonoBehaviour
{public float speed;[Space]private Transform wallCheck; //墙面检测的位置private Transform groundCheck; //地面检测的位置private Vector2 velocity; //记录速度private CrawlerType type;private Rigidbody2D body;private tk2dSpriteAnimator anim;private void Awake(){body = GetComponent<Rigidbody2D>();anim = GetComponent<tk2dSpriteAnimator>();}private void Start(){float z = transform.eulerAngles.z;//通过transform.eulerAngles.z来判断哪种类型的Crawlerif (z >= 45f && z <= 135f){type = CrawlerType.Wall;velocity = new Vector2(0f, Mathf.Sign(-transform.localScale.x) * speed);}else if (z >= 135f && z <= 225f){type = ((transform.localScale.y > 0f) ? CrawlerType.Roof : CrawlerType.Floor);velocity = new Vector2(Mathf.Sign(transform.localScale.x) * speed, 0f);}else if (z >= 225f && z <= 315f){type = CrawlerType.Wall;velocity = new Vector2(0f, Mathf.Sign(transform.localScale.x) * speed);}else{type = ((transform.localScale.y > 0f) ? CrawlerType.Floor : CrawlerType.Roof);velocity = new Vector2(Mathf.Sign(-transform.localScale.x) * speed, 0f);}//TODO:CrawlerType crawlerType = type;if(crawlerType != CrawlerType.Floor){if(crawlerType - CrawlerType.Roof <= 1){body.gravityScale = 0;//如果在墙面面上rb2d的重力就设置为1}}else{body.gravityScale = 1; //如果在地面上rb2d的重力就设置为1//TODO:}StartCoroutine(Walk());}/// <summary>/// 使用协程实现Walk函数,循环直至hit=true后挂起然后启用协程Turn()/// </summary>/// <returns></returns>private IEnumerator Walk(){for(; ; ){anim.Play("Walk");body.velocity = velocity;bool hit = false;while (!hit){if(CheckRayLocal(wallCheck.localPosition,transform.localScale.x > 0f ? Vector2.left : Vector2.right, 1f)){hit = true;break;}if (!CheckRayLocal(groundCheck.localPosition, transform.localScale.y > 0f ? Vector2.down : Vector2.up, 1f)){hit = true;break;}yield return null;}yield return StartCoroutine(Turn());yield return null;}}/// <summary>/// 使用协程实现转向函数/// </summary>/// <returns></returns>private IEnumerator Turn(){body.velocity = Vector2.zero;yield return StartCoroutine(anim.PlayAnimWait("Turn"));transform.SetScaleX(transform.localScale.x * -1f);velocity.x = velocity.x * -1f;velocity.y = velocity.y * -1f;}/// <summary>/// 发射射线,检测是否有LayerMask.GetMask("Terrain").collider/// </summary>/// <param name="originLocal"></param>/// <param name="directionLocal"></param>/// <param name="length"></param>/// <returns></returns>public bool CheckRayLocal(Vector3 originLocal, Vector2 directionLocal, float length){Vector2 vector = transform.TransformPoint(originLocal);Vector2 vector2 = transform.TransformDirection(directionLocal);RaycastHit2D raycastHit2D = Physics2D.Raycast(vector, vector2, length, LayerMask.GetMask("Terrain"));Debug.DrawLine(vector, vector + vector2 * length);return raycastHit2D.collider != null;}private enum CrawlerType{Floor,Roof,Wall}
}
回到Unity编辑器中,如果你的是开始往左走的话,记得设置speed为负数
只需要这个就行了。我们不需要挂载Lineofsightdetector.cs,这个就是敌人发现敌人到自己攻击范围的脚本
3.制作敌人另个爬虫Crawler的playmaker状态机
创建一个名字叫Crawler的playmaker状态机:
变量和事件如下所示:
同样我们还需要自定义脚本:
using System.Collections;
using UnityEngine;namespace HutongGames.PlayMaker.Actions
{[ActionCategory("Enemy AI")]public class WalkLeftRight : FsmStateAction{private Rigidbody2D body;private tk2dSpriteAnimator spriteAnimator;private Collider2D collider;public FsmOwnerDefault gameObject;public float walkSpeed; //移动速度public bool spriteFacesLeft; //sprite开始时是向左的吗public string groundLayer; //也就是Terrainpublic float turnDelay; //转向延时private float nextTurnTime; //下一次转身的时间[Header("Animation")]public FsmString walkAnimName; //walk的动画名字public FsmString turnAnimName; //turn的动画名字public FsmBool startLeft;public FsmBool startRight;public FsmBool keepDirection;private float scaleX_pos;private float scaleX_neg;private const float wallRayHeight = 0.5f; //检测墙壁的射线高度private const float wallRayLength = 0.1f; //检测墙壁的射线长度private const float groundRayLength = 1f; //检测地面的射线高度private GameObject target; //目标private Coroutine walkRoutine; //walk的协程private Coroutine turnRoutine; //turn的协程private bool shouldTurn; //应该转身了吗private float Direction{get{if (target){return Mathf.Sign(target.transform.localScale.x) * (spriteFacesLeft ? -1 : 1); //记录方向属性}return 0f;}}public override void OnEnter(){UpdateIfTargetChanged();SetupStartingDirection();walkRoutine = StartCoroutine(Walk());}/// <summary>/// 退出时停掉所有正在执行的协程/// </summary>public override void OnExit(){if(walkRoutine != null){StopCoroutine(walkRoutine);walkRoutine = null;}if (turnRoutine != null){StopCoroutine(turnRoutine);turnRoutine = null;}}/// <summary>/// 如果目标target发生变化后重新初始化/// </summary>private void UpdateIfTargetChanged(){GameObject ownerDefaultTarget = Fsm.GetOwnerDefaultTarget(gameObject);if(ownerDefaultTarget != target){target = ownerDefaultTarget;body = target.GetComponent<Rigidbody2D>();collider = target.GetComponent<Collider2D>();spriteAnimator = target.GetComponent<tk2dSpriteAnimator>();}}private IEnumerator Walk(){if (spriteAnimator){spriteAnimator.Play(walkAnimName.Value);}for(; ; ){if (body){Vector2 velocity = body.velocity;velocity.x = walkSpeed * Direction;body.velocity = velocity;if(shouldTurn || (CheckIsGrounded() && (CheckWall() || CheckFloor()) && Time.time >= nextTurnTime)){shouldTurn = false;nextTurnTime = Time.time + turnDelay;turnRoutine = StartCoroutine(Turn());yield return turnRoutine;}}yield return new WaitForFixedUpdate();}}private IEnumerator Turn(){Vector2 velocity = body.velocity;velocity.x = 0f;body.velocity = velocity;tk2dSpriteAnimationClip clipByName = spriteAnimator.GetClipByName(turnAnimName.Name);if(clipByName != null){float seconds = clipByName.frames.Length / clipByName.fps;//计算出动画播放的时间spriteAnimator.Play(clipByName);yield return new WaitForSeconds(seconds);}Vector3 localScale = target.transform.localScale;localScale.x *= -1f;target.transform.localScale = localScale;if (spriteAnimator){spriteAnimator.Play(walkAnimName.Value);}turnRoutine = null;}/// <summary>/// 检测是否接触到墙面/// </summary>/// <returns></returns>private bool CheckWall(){Vector2 vector = collider.bounds.center + new Vector3(0f, -(collider.bounds.size.y / 2f) + wallRayHeight);Vector2 vector2 = Vector2.right * Direction;float num = collider.bounds.center.x / 2f + wallRayLength;Debug.DrawLine(vector, vector + vector2 * num);return Physics2D.Raycast(vector, vector2, num, LayerMask.GetMask(groundLayer)).collider != null;}/// <summary>/// 检测是否接触到地板/// </summary>/// <returns></returns>private bool CheckFloor(){Vector2 vector = collider.bounds.center + new Vector3((collider.bounds.size.x / 2f + wallRayLength) * Direction, -(collider.bounds.size.y / 2f) + wallRayHeight);Debug.DrawLine(vector, vector + Vector2.down * groundRayLength);return !(Physics2D.Raycast(vector, Vector2.down, groundRayLength, LayerMask.GetMask(groundLayer)).collider != null);}/// <summary>/// 检测是否已经接触到地面/// </summary>/// <returns></returns>private bool CheckIsGrounded(){Vector2 vector = collider.bounds.center + new Vector3(0f,-(collider.bounds.center.y / 2f) + wallRayHeight);Debug.DrawLine(vector, vector + Vector2.down * groundRayLength);return Physics2D.Raycast(vector, Vector2.down, groundRayLength, LayerMask.GetMask(groundLayer)).collider != null;}/// <summary>/// 设置开始时GameObject的方向/// </summary>private void SetupStartingDirection(){if (target.transform.localScale.x < 0f){if (!spriteFacesLeft && startRight.Value){shouldTurn = true;}if (spriteFacesLeft && startLeft.Value){shouldTurn = true;}}else{if (spriteFacesLeft && startRight.Value){shouldTurn = true;}if (!spriteFacesLeft && startLeft.Value){shouldTurn = true;}}if (!startLeft.Value && !startRight.Value && !keepDirection.Value && UnityEngine.Random.Range(0f, 100f) <= 50f)//随机选择一边{shouldTurn = true;}startLeft.Value = false;startRight.Value = false;}public WalkLeftRight(){walkSpeed = 4f;groundLayer = "Terrain";turnDelay = 1f;}}}
整个PLAYmaker状态机如下所示:
二、制作敌人飞虫Fly
1.公式化导入制作飞虫Fly素材
这里就不过多赘述了,直接上图
这里需要注意到Fly有两个子游戏对象,用于检测是否碰到其它敌人需要转向,只不过这些都是后面设计要用的,所以先不管:
还有就是我们要开始导入PlayMaker Unity 2D用于检测场景中涉及到playmaker 2d的物理碰撞:
这里是我已经导入了所以是灰色的,你导入后会发现多生成了一个预制体:
再给我们fly添加一个脚本叫PlayMakerUnity2DProxy.cs:
2.制作敌人飞虫Fly的playmaker状态机
完成上述过程中就到了创建playmaker状态机环节,在这里我只用一个playmaker状态机完成Fly完整的循环:
变量和事件如下所示:
此时还需要继续自定义脚本,这就要用到上面提到的 playmaker 2d的物理碰撞了:
using UnityEngine;
using System.Collections.Generic;
using System;namespace HutongGames.PlayMaker.Actions
{[ActionCategory(ActionCategory.Physics)][Tooltip("Detect additional collisions between the Owner of this FSM and other object with additional raycasting.")]public class CheckCollisionSideEnter : FsmStateAction{[UIHint(UIHint.Variable)]public FsmBool topHit;[UIHint(UIHint.Variable)]public FsmBool rightHit;[UIHint(UIHint.Variable)]public FsmBool bottomHit;[UIHint(UIHint.Variable)]public FsmBool leftHit;public FsmEvent topHitEvent;public FsmEvent rightHitEvent;public FsmEvent bottomHitEvent;public FsmEvent leftHitEvent;public bool otherLayer;public int otherLayerNumber;public FsmBool ignoreTriggers;private PlayMakerUnity2DProxy _proxy;private Collider2D col2d;private const float RAYCAST_LENGTH = 0.08f;private List<Vector2> topRays;private List<Vector2> rightRays;private List<Vector2> bottomRays;private List<Vector2> leftRays;public override void Reset(){}public override void OnEnter(){col2d = Fsm.GameObject.GetComponent<Collider2D>();_proxy = Owner.GetComponent<PlayMakerUnity2DProxy>();if(_proxy == null){_proxy = Owner.AddComponent<PlayMakerUnity2DProxy>();}_proxy.AddOnCollisionEnter2dDelegate(new PlayMakerUnity2DProxy.OnCollisionEnter2dDelegate(DoCollisionEnter2D));}public override void OnUpdate(){ }public override void OnExit(){_proxy.RemoveOnCollisionEnter2dDelegate(new PlayMakerUnity2DProxy.OnCollisionEnter2dDelegate(DoCollisionEnter2D));}public new void DoCollisionEnter2D(Collision2D collision){if (!otherLayer){if(LayerMask.LayerToName(collision.gameObject.layer) == "Terrain"){CheckTouching(LayerMask.NameToLayer("Terrain"));return;}}else{CheckTouching(otherLayerNumber);}}private void CheckTouching(LayerMask layer){topRays = new List<Vector2>();topRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.max.y));topRays.Add(new Vector2(col2d.bounds.center.x, col2d.bounds.max.y));topRays.Add(col2d.bounds.max);rightRays = new List<Vector2>();rightRays.Add(col2d.bounds.max);rightRays.Add(new Vector2(col2d.bounds.max.x, col2d.bounds.center.y));rightRays.Add(new Vector2(col2d.bounds.max.x, col2d.bounds.min.y));bottomRays = new List<Vector2>();bottomRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.min.y));bottomRays.Add(new Vector2(col2d.bounds.center.x, col2d.bounds.min.y));bottomRays.Add(col2d.bounds.min);leftRays = new List<Vector2>();leftRays.Add(col2d.bounds.min);leftRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.center.y));leftRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.max.y));topHit.Value = false;rightHit.Value = false;bottomHit.Value = false;leftHit.Value = false;foreach (Vector2 v in topRays){RaycastHit2D raycastHit2D = Physics2D.Raycast(v, Vector2.up, RAYCAST_LENGTH, 1 << layer);if(raycastHit2D.collider != null && (!ignoreTriggers.Value || !raycastHit2D.collider.isTrigger)){topHit.Value = true;Fsm.Event(topHitEvent);break;}}foreach (Vector2 v2 in rightRays){RaycastHit2D raycastHit2D2 = Physics2D.Raycast(v2, Vector2.right, RAYCAST_LENGTH, 1 << layer);if (raycastHit2D2.collider != null && (!ignoreTriggers.Value || !raycastHit2D2.collider.isTrigger)){rightHit.Value = true;Fsm.Event(rightHitEvent);break;}}foreach (Vector2 v3 in bottomRays){RaycastHit2D raycastHit2D3 = Physics2D.Raycast(v3, Vector2.down, RAYCAST_LENGTH, 1 << layer);if(raycastHit2D3.collider != null && (!ignoreTriggers.Value || !raycastHit2D3.collider.isTrigger)){bottomHit.Value = true;Fsm.Event(bottomHitEvent);break;}}foreach (Vector2 v4 in leftRays){RaycastHit2D raycastHit2D4 = Physics2D.Raycast(v4, Vector2.left, RAYCAST_LENGTH, 1 << layer);if (raycastHit2D4.collider != null && (!ignoreTriggers.Value || !raycastHit2D4.collider.isTrigger)){leftHit.Value = true;Fsm.Event(leftHitEvent);break;}}}}}
using UnityEngine;
using System.Collections.Generic;
using System;namespace HutongGames.PlayMaker.Actions
{[ActionCategory(ActionCategory.Physics)][Tooltip("Detect additional collisions between the Owner of this FSM and other object with additional raycasting.")]public class CheckCollisionSide : FsmStateAction{[UIHint(UIHint.Variable)]public FsmBool topHit;[UIHint(UIHint.Variable)]public FsmBool rightHit;[UIHint(UIHint.Variable)]public FsmBool bottomHit;[UIHint(UIHint.Variable)]public FsmBool leftHit;public FsmEvent topHitEvent;public FsmEvent rightHitEvent;public FsmEvent bottomHitEvent;public FsmEvent leftHitEvent;public bool otherLayer;public int otherLayerNumber;public FsmBool ignoreTriggers;private PlayMakerUnity2DProxy _proxy;private Collider2D col2d;private const float RAYCAST_LENGTH = 0.08f;private List<Vector2> topRays;private List<Vector2> rightRays;private List<Vector2> bottomRays;private List<Vector2> leftRays;private bool checkUp;private bool checkRight;private bool checkBottom;private bool checkLeft;public override void Reset(){checkUp = false;checkRight = false;checkBottom = false;checkLeft = false;}public override void OnEnter(){col2d = Fsm.GameObject.GetComponent<Collider2D>();topRays = new List<Vector2>(3);rightRays = new List<Vector2>(3);bottomRays = new List<Vector2>(3);leftRays = new List<Vector2>(3);_proxy = Owner.GetComponent<PlayMakerUnity2DProxy>();if (_proxy == null){_proxy = Owner.AddComponent<PlayMakerUnity2DProxy>();}_proxy.AddOnCollisionStay2dDelegate(new PlayMakerUnity2DProxy.OnCollisionStay2dDelegate(DoCollisionStay2D));if(!topHit.IsNone || topHitEvent != null){checkUp = true;}else{checkUp = false;}if (!rightHit.IsNone || rightHitEvent != null){checkRight = true;}else{checkRight = false;}if (!bottomHit.IsNone || bottomHitEvent != null){checkBottom = true;}else{checkBottom = false;}if (!leftHit.IsNone || leftHitEvent != null){checkLeft = true;}else{checkLeft = false;}}public override void OnUpdate(){if(topHit.Value || rightHit.Value || bottomHit.Value || leftHit.Value){if (!otherLayer){CheckTouching(LayerMask.NameToLayer("Terrain"));return;}CheckTouching(otherLayerNumber);} }public override void OnExit(){_proxy.RemoveOnCollisionStay2dDelegate(new PlayMakerUnity2DProxy.OnCollisionStay2dDelegate(DoCollisionStay2D));}public new void DoCollisionStay2D(Collision2D collision){if (!otherLayer){if(collision.gameObject.layer == LayerMask.NameToLayer("Terrain")){CheckTouching(LayerMask.NameToLayer("Terrain"));return;}}else{CheckTouching(otherLayerNumber);}}public new void DoCollisionExit2D(Collision2D collision){topHit.Value = false;rightHit.Value = false;bottomHit.Value = false;leftHit.Value = false;}private void CheckTouching(LayerMask layer){if (checkUp){topRays.Clear();topRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.max.y));topRays.Add(new Vector2(col2d.bounds.center.x, col2d.bounds.max.y));topRays.Add(col2d.bounds.max);topHit.Value = false;for (int i = 0; i < 3; i++){RaycastHit2D raycastHit2D = Physics2D.Raycast(topRays[i], Vector2.up, RAYCAST_LENGTH, 1 << layer);if(raycastHit2D.collider != null && (!ignoreTriggers.Value || !raycastHit2D.collider.isTrigger)){topHit.Value = true;Fsm.Event(topHitEvent);break;}}}if (checkRight){rightRays.Clear();rightRays.Add(col2d.bounds.max);rightRays.Add(new Vector2(col2d.bounds.max.x, col2d.bounds.center.y));rightRays.Add(new Vector2(col2d.bounds.max.x, col2d.bounds.min.y));rightHit.Value = false;for (int i = 0; i < 3; i++){RaycastHit2D raycastHit2D2 = Physics2D.Raycast(rightRays[i], Vector2.right, RAYCAST_LENGTH, 1 << layer);if (raycastHit2D2.collider != null && (!ignoreTriggers.Value || !raycastHit2D2.collider.isTrigger)){rightHit.Value = true;Fsm.Event(rightHitEvent);break;}}}if (checkBottom){bottomRays.Clear();bottomRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.min.y));bottomRays.Add(new Vector2(col2d.bounds.center.x, col2d.bounds.min.y));bottomRays.Add(col2d.bounds.min);for (int i = 0; i < 3; i++){RaycastHit2D raycastHit2D3 = Physics2D.Raycast(bottomRays[i], Vector2.down, RAYCAST_LENGTH, 1 << layer);if (raycastHit2D3.collider != null && (!ignoreTriggers.Value || !raycastHit2D3.collider.isTrigger)){bottomHit.Value = true;Fsm.Event(bottomHitEvent);break;}}}if (checkLeft){leftRays.Clear();leftRays.Add(col2d.bounds.min);leftRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.center.y));leftRays.Add(new Vector2(col2d.bounds.min.x, col2d.bounds.max.y));for (int i = 0; i < 3; i++){RaycastHit2D raycastHit2D4 = Physics2D.Raycast(leftRays[i], Vector2.left, RAYCAST_LENGTH, 1 << layer);if (raycastHit2D4.collider != null && (!ignoreTriggers.Value || !raycastHit2D4.collider.isTrigger)){leftHit.Value = true;Fsm.Event(leftHitEvent);return;}}}}public enum CollisionSide { top,left,right,bottom,other}}}
using System;
using UnityEngine;namespace HutongGames.PlayMaker.Actions
{[ActionCategory(ActionCategory.Physics2D)][Tooltip("Sets the 2d Velocity of a Game Object, using an angle and a speed value. For the angle, 0 is to the right and the degrees increase clockwise.")]public class SetVelocityAsAngle : RigidBody2dActionBase{[RequiredField][CheckForComponent(typeof(Rigidbody2D))]public FsmOwnerDefault gameObject;[RequiredField]public FsmFloat angle;[RequiredField]public FsmFloat speed;private FsmFloat x;private FsmFloat y;public bool everyFrame;public override void Reset(){gameObject = null;angle = new FsmFloat{UseVariable = true};speed = new FsmFloat{UseVariable = true};everyFrame = false;}public override void Awake(){Fsm.HandleFixedUpdate = true;}public override void OnPreprocess(){Fsm.HandleFixedUpdate = true;}public override void OnEnter(){CacheRigidBody2d(Fsm.GetOwnerDefaultTarget(gameObject));DoSetVelocity();if (!everyFrame){Finish();}}public override void OnFixedUpdate(){DoSetVelocity();if (!everyFrame){Finish();}}private void DoSetVelocity(){if (rb2d == null)return;x = speed.Value * Mathf.Cos(angle.Value * 0.017453292f); //将角度转化为速度y = speed.Value * Mathf.Sin(angle.Value * 0.017453292f);Vector2 velocity;velocity.x = x.Value;velocity.y = y.Value;rb2d.velocity = velocity;}}}
整个Playmaker状态机如下所示:
完整图如下所示:
总结
首先我们来看看Crawler的转向效果能不能实现:
我们再来看看Fly的状态机能不能正常运行:
我们可以创建一个闭环的四边形,并给他一个"Terrain"Layer
完美运行,下一期我们来丰富一下玩家的行为吧。