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

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

文章目录


前言

大家好久不见,终于放完中秋假期可以回自己家里打代码了,上一期我介绍了为什么和如何导入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来管理陆地敌人的基本行为,同样我们还需要使用状态机来实现整个敌人状态机的切换。

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方向上的速度设置为0
	    if (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静态类来实现:

cs 复制代码
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函数,以后就可以在这里调用你想要用的方法了。

cs 复制代码
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有关的自定义脚本都继承自它,这样至少不会忘了要调用了:

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);
	    }
    }

}
cs 复制代码
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();
    }
}
cs 复制代码
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);
	    }
    }

}
cs 复制代码
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);
    }
}
cs 复制代码
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);
		}
	    }
	}
    }

}
cs 复制代码
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();
	}


    }

}
cs 复制代码
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();
	}



    }

}
cs 复制代码
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

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和position
	    if (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
	//防止数字超出枚举长度或者小于0
	if(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上书写写的便捷方法:

cs 复制代码
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,我就展示它沿着墙壁走路并转向的画面展示吧:

相关推荐
zxctsclrjjjcph18 分钟前
【C语言】猜数字游戏
c语言·redis·游戏
__water3 小时前
『功能项目』QFrameWorkBug修改器界面【65】
c#·unity引擎·bug修改器
ue星空4 小时前
虚幻引擎的三种输入模式和将控件显示到屏幕上
ue5·游戏引擎·虚幻·虚幻引擎
L W S4 小时前
归纳一下Invoke,beginInvoke,还有InvokeRequire
c#
鲤籽鲲5 小时前
C#设计模式之访问者模式
设计模式·c#·访问者模式
lzhdim5 小时前
C#控件开发能够阅读的书籍
开发语言·c#
神仙别闹5 小时前
基于C#+SQL Server(CS界面)学生选课及成绩查询管理系统
开发语言·数据库·c#
一 乐6 小时前
电子竞技信息交流平台|基于java的电子竞技信息交流平台系统小程序(源码+数据库+文档)
java·开发语言·数据库·vue.js·学习·游戏·小程序