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

news2024/9/20 3:44:30

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

文章目录

  • 前言
  • 一、制作敌人僵尸虫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方向上的速度设置为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静态类来实现:

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和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上书写写的便捷方法:

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.coloradmin.cn/o/2147775.html

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

相关文章

C语言程序设计(进阶)

肆意张扬的我们都不会是烂尾的诗集。 2.整型在内存中的存储 我们之前讲过一个变量的创建是要在内存中开辟空间的。空间的大小是根据不同类型而决定的。 2.1原码、反码、补码 数值有不同的表现形式&#xff1a;2进制、8进制、10进制、16进制 其中整数的2进制表示也有三种形式&…

毕业论文写作会用到的AI软件!一定不能错过的18个网站!(务必收藏)

AI毕业论文写作它可以提供论文摘要、大纲、选题确立等多种写作辅助&#xff0c;还能帮助我们完成开题报告、实验报告、辩论灵感等内容。无论是文章纠正、批改&#xff0c;还是改写降重&#xff0c;它都能轻松搞定。甚至连论文致谢、创新创业计划书等都能为我们提供帮助。 以下…

缓存穿透 问题(缓存空对象)

文章目录 1、缓存穿透2、缓存空对象3、AlbumInfoApiController --》getAlbumInfo()4、AlbumInfoServiceImpl --》getAlbumInfo()5、RedisConstant6、请求缓存不存在的数据 1、缓存穿透 2、缓存空对象 3、AlbumInfoApiController --》getAlbumInfo() GetMapping("getAlbumI…

基于RK3588,AI边缘模块,单片6TOPS,可集群堆叠,Mixtile Blade 3

Mixtile Blade 3 是一款经济实惠、节能的 SBC&#xff0c;围绕下一代 8 纳米瑞芯微 RK3588 处理器构建。它非常适合快速开发、AI 应用程序原型设计和边缘计算&#xff0c;允许您集群多个 Mixtile Blade 3 SBC 以扩展您的部署。 硬件布局正反面 开箱即用的 Mixtile Blade 3 是一…

Three.js学习笔记

Three.js是一款基于原生WebGL封装的Web 3D库,向外提供了许多的接口。 它可以运用在在小游戏、产品展示、物联网、数字孪生、智慧城市园区、机械、建筑、全景看房、GIS等各个领域。 npm install three https://threejs.org/docs/index.html#manual/en/introduction/Installatio…

java序列化对象后读取数据错误的问题

今天学到了对象的序列化&#xff0c;就是将对象写入到文件中去&#xff0c;大家要直到我们普通的输入输出文件只是把数据的值写入了文件&#xff0c;而没有把数据的类型与之绑定&#xff0c;比如我向文件中写入100&#xff0c;那么这是字符串”100“还是整数100还是高精度浮点数…

CSS入门笔记

目录 概述 组成 CSS 语法 常见的使用方式 CSS 优先级 CSS 选择器 1. 基本选择器 2. 属性选择器 3. 伪类选择器 4. 组合选择器 示例 优先级 边框样式与盒子模型 单个边框 边框轮廓&#xff08;Outline&#xff09; 盒子模型 模型介绍 边距设置 布局示例 文…

支付宝开放平台-开发者社区——AI 日报「9 月 19 日」

1️⃣ 保守派中间派原生派&#xff0c;谁将主导搜索引擎未来三十年&#xff1f; 机器之心&#xff5c;阅读原文 拥有智能索引库、专属知识库、混合大模型调度系统的 AI 原生搜索&#xff0c;能否成为正统&#xff0c;引领搜索引擎的下一个三十年&#xff1f;AI 正成为「端掉」…

数据处理与统计分析篇-day05-Pandas详解

创建DaraFrame对象 概述 DataFrame是一个表格型的结构化数据结构&#xff0c;它含有一组或多组有序的列&#xff08;Series&#xff09;&#xff0c;每列可以是不同的值类型&#xff08;数值、字符串、布尔值等&#xff09;。 DataFrame是Pandas中的最基本的数据结构对象&…

51单片机-DA(数字转模拟)

作者&#xff1a;Whappy 个人理解&#xff1a;将电压或电流信号进行等分或不等分&#xff08;高电平的电压范围和低电平的范围&#xff0c;如0-5v&#xff0c;0-1.8位低电平&#xff0c;3.8-5v为高电平&#xff09;&#xff0c;同样也是通过采样&#xff0c;量化等操作将不连续…

工业仪器仪表指针数据集

工业仪器仪表指针数据集: 可用于仪表指针识别、分割项目 分别含有1000张原图和标签 图片中有各类工业仪器表盘&#xff0c;指针。 工业仪器仪表指针数据集介绍 数据集名称 工业仪器仪表指针数据集&#xff08;Industrial Instrument Needle Dataset&#xff09; 数据集概述 …

Introduction to LLMs in Python

1、The Large Language Models (LLMs) Landscape 1.1、Introducing large language models 1.1.1、LLMs development lifecycle 1.1.2、Pre-training and fine-tuning We can often use a pre-trained LLM as a foundation and fine-tune it with our specific data, saving …

【笔记】2.1 半导体三极管(BJT,Bipolar Junction Transistor)

一、结构和符号 1. 三极管结构 常用的三极管的结构有硅平面管和锗合金管两种类型。各有PNP型和NPN型两种结构。 左图是NPN型硅平面三极管,右图是PNP型锗合金三极管。 从图中可见平面型三极管是先在一块大的金属板上注入杂质使之变成N型,然后再在中间注入杂质使之变成P型,…

从零开始讲DDR(2)——DDR的核心技术

我们知道DDR区分于之前的SDRAM最大的点就在于它可以做到“在时钟周期的上升沿和下降沿都能传输数据&#xff0c;这样它的传输速率就比SDRAM快了一倍”。其实要做到这点&#xff0c;背后需要的技术突破体现在很多层面&#xff1a; 一、双边沿触发技术&#xff08;Double Data Ra…

DPDK 简易应用开发之路 2:UDP数据包发送及实现

本机环境为 Ubuntu20.04 &#xff0c;dpdk-stable-20.11.10 发送数据包的通用步骤 初始化DPDK环境&#xff1a; 调用 rte_eal_init() 来初始化DPDK的EAL&#xff08;环境抽象层&#xff09;&#xff0c;这是所有DPDK应用程序的第一步&#xff0c;用于初始化硬件、内存和逻辑核…

希尔排序(ShellSort)详解

希尔排序的底层是插入排序&#xff0c; 不了解插入排序的友友可以先看我下面这篇文章&#xff1a; 插入排序详解-CSDN博客 思路 就整体而言&#xff0c;希尔排序不过是多用了几次插入排序&#xff0c; 预排序→插入排序&#xff0c; 预排序的作用是让数组元素更接近于有序…

FreeRtos中Task函数详解

前言&#xff1a;本篇笔记参考韦东山老师&#xff0c;B站视频链接放在最后。 Task任务基本概念 在使用FreeRtos的程序中&#xff0c;可以创建多个Task来完成程序功能&#xff0c;Task是轻量级的独立执行单元&#xff0c;被FreeRtos的调度器管理&#xff0c;每个任务有着自己的…

从零开始手搓Transformer#Datawhale组队学习Task1#

从零开始手搓Transformer 目录 缩放点积注意力DotProductAttention 多头注意力Multi-Head Attention 位置编码Position Encoder 前馈神经网络FFN 残差连接和层归一化&#xff08;Add&Norm&#xff09; 编码器Encoder 解码器Decoder 编码器-解码器Encoder-Decoder …

pg入门3—详解tablespaces2

pg默认的tablespace的location为空&#xff0c;那么如果表设置了默认的tablespace&#xff0c;数据实际上是存哪个目录的呢? 在 PostgreSQL 中&#xff0c;如果你创建了一个表并且没有显式指定表空间&#xff08;tablespace&#xff09;&#xff0c;或者表空间的 location 为…

ISSTA 2024盛大开幕:中国学者的录取数和投稿量均位列第一

随着夏日的尾声&#xff0c;全球软件测试领域的专家和学者齐聚在奥地利维也纳。共同参与这场科技盛宴——ISSTA 2024。这场国际会议正如火如荼地进行中&#xff0c;吸引了来自世界各地的专业人士参与。 会议实况&#xff1a; 9月16日与17日&#xff0c;大会安排了丰富的社交活…