[Unity Demo]从零开始制作空洞骑士Hollow Knight第二十二集:制作游戏的对话系统

news2024/11/25 4:27:39

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

文章目录

  • 前言
  • 一、制作游戏的对话系统
    • 1.通过转移点的门讲解制作对话系统
    • 2.以游戏的石碑为例制作确认能力界面
  • 总结


前言     

  hello大家好久没见,之所以隔了这么久才更新并不是因为我又放弃了这个项目,而是接下来要制作的工作太忙碌了,每次我都花了很长的时间解决完一个部分,然后就没力气打开CSDN写文章就直接睡觉去了,而且还有就是我上上一期写了一万字的内容结果系统出BUG给我没保存直接气晕了,现在刚刚醒来终于有时间整理下我新制作的内容了,还有就是感谢兄弟们的评论支持,我看到评论区有个哥们说让我讲讲游戏的车站系统,我只能说哥们如果你想看的话我还没那么快写这部分的文章,如果你真想看的话可以到我的github下载,我刚好已上传到最新:

GitHub - ForestDango/Hollow-Knight-Demo: A new Hollow Knight Demo after 2 years!

        这一期我们就顺着上一期继续讲我们的HUD部分,那就是我们的对话系统,游戏中除了NPC,还有很多东西都是可以产生对话的,所以我们先从简单的物品举例,接下来在下一期我会讲解如何制作NPC系统。 


一、制作游戏的对话系统

1.通过转移点的门讲解制作对话系统

        别看这一篇只有两个小节,内容还是量大管饱的,所以我才决定分两期来讲对话系统和NPC系统。

        在第十四集中,我说过转移点除了上下左右以外还有一个门,也就是玩家要按额外的按键(比如上键)才能进入的,但你总不可能什么UI提示都不给让玩家不知道这是一个可以进的门,因此我才决定制作一个包含多个文字提示的对话框名字叫Arrow Prompt New,可以看到它非常简单,它有一个名字叫Prompt Cln的tk2dsprite和tk2dspriteAnimator,我们先去做好它:

 动画我就挑这期要用到的几个片段来讲吧:

下面是车站相关的动画:

 

这个是选择是与否的对话框的底部: 

 这个是选择是与否的对话框的头部: 

回到Arrow Prompt New来,我们来看看它的子对象,首先是一个阴影的Shadow会让这个对话提示框看起来更立体点,

然后Labels则是我上面说的显示哪个文字提示的文件夹,可以看到我已经制作了Enter进入,Inspect监视....Travel旅行等文字,然后它们使用的场合也是不一样的,以Enter为例,如果你进入门他就会选择这个文字而不是其它文字到对话框中,

然后Labels有一个脚本叫.cs可以根据时间参数渐变的显示文字和spriterenderer的内容:

using System;
using TMPro;
using UnityEngine;

public class FadeGroup : MonoBehaviour
{
    public SpriteRenderer[] spriteRenderers;
    public TextMeshPro[] texts;
    public InvAnimateUpAndDown[] animators;
    public float fadeInTime = 0.2f;
    public float fadeOutTime = 0.2f;
    public float fadeOutTimeFast = 0.2f;
    public float fullAlpha = 1f;
    public float downAlpha;
    public bool activateTexts;
    private int state;
    private float timer;
    private Color currentColour;
    private Color fadeOutColour = new Color(1f, 1f, 1f, 0f);
    private Color fadeInColour = new Color(1f, 1f, 1f, 1f);
    private float currentAlpha;
    public bool disableRenderersOnEnable;

    private void OnEnable()
    {
	if (disableRenderersOnEnable)
	{
	    DisableRenderers();
	}
    }

    private void Update()
    {
	if (state != 0)
	{
	    float t = 0f;
	    if (state == 1) //将所有spriteRenderers和texts的alpha设置为upalpha
	    {
		timer += Time.deltaTime;
		if (timer > fadeInTime)
		{
		    timer = fadeInTime;
		    state = 0;
		    for (int i = 0; i < spriteRenderers.Length; i++)
		    {
			if (spriteRenderers[i] != null)
			{
			    Color color = spriteRenderers[i].color;
			    color.a = fullAlpha;
			    spriteRenderers[i].color = color;
			}
		    }
		    for (int j = 0; j < texts.Length; j++)
		    {
			if (texts[j] != null)
			{
			    Color color2 = texts[j].color;
			    color2.a = fullAlpha;
			    texts[j].color = color2;
			}
		    }
		}
		t = timer / fadeInTime;
	    }
	    else if (state == 2) //将所有spriteRenderers和texts的alpha设置为downalpha
	    {
		timer -= Time.deltaTime;
		if (timer < 0f)
		{
		    timer = 0f;
		    state = 0;
		    if (downAlpha > 0f)
		    {
			for (int k = 0; k < spriteRenderers.Length; k++)
			{
			    if (spriteRenderers[k] != null)
			    {
				Color color3 = spriteRenderers[k].color;
				color3.a = downAlpha;
				spriteRenderers[k].color = color3;
			    }
			}
			for (int l = 0; l < texts.Length; l++)
			{
			    if (texts[l] != null)
			    {
				Color color4 = texts[l].color;
				color4.a = downAlpha;
				texts[l].color = color4;
			    }
			}
		    }
		    else
		    {
			DisableRenderers();
		    }
		}
		t = timer / fadeOutTime;
	    }
	    if (state != 0)
	    {
		currentAlpha = Mathf.Lerp(downAlpha, fullAlpha, t);
		for (int m = 0; m < spriteRenderers.Length; m++)
		{
		    if (spriteRenderers[m] != null)
		    {
			Color color5 = spriteRenderers[m].color;
			color5.a = currentAlpha;
			spriteRenderers[m].color = color5;
		    }
		}
		for (int n = 0; n < texts.Length; n++)
		{
		    if (texts[n] != null)
		    {
			Color color6 = texts[n].color;
			color6.a = currentAlpha;
			texts[n].color = color6;
		    }
		}
	    }
	}
    }
    
    /// <summary>
    /// 将所有的spriterender和text都设置为透明alpha = 0
    /// </summary>
    public void FadeUp()
    {
	timer = 0f;
	state = 1;
	for (int i = 0; i < spriteRenderers.Length; i++)
	{
	    if (spriteRenderers[i] != null)
	    {
		Color color = spriteRenderers[i].color;
		color.a = 0f;
		spriteRenderers[i].color = color;
		spriteRenderers[i].enabled = true;
	    }
	}
	for (int j = 0; j < texts.Length; j++)
	{
	    if (texts[j] != null)
	    {
		Color color2 = texts[j].color;
		color2.a = 0f;
		texts[j].color = color2;
		texts[j].gameObject.GetComponent<MeshRenderer>().SetActiveWithChildren(true);
	    }
	}
	for (int k = 0; k < animators.Length; k++)
	{
	    if (animators[k] != null)
	    {
		animators[k].AnimateUp();
	    }
	}
    }
    /// <summary>
    ///  将所有的spriterender和text都设置为透明alpha = 1
    /// </summary>
    public void FadeDown()
    {
	timer = fadeOutTime;
	state = 2;
	for (int i = 0; i < animators.Length; i++)
	{
	    if (animators[i] != null)
	    {
		animators[i].AnimateDown();
	    }
	}
    }

    /// <summary>
    ///  将所有的spriterender和text都快速的设置为透明alpha = 1
    /// </summary>
    public void FadeDownFast()
    {
	timer = fadeOutTimeFast;
	state = 2;
	for (int i = 0; i < animators.Length; i++)
	{
	    if (animators[i] != null)
	    {
		animators[i].AnimateDown();
	    }
	}
    }


    private void DisableRenderers()
    {
	for (int i = 0; i < spriteRenderers.Length; i++)
	{
	    if (spriteRenderers[i] != null)
	    {
		spriteRenderers[i].enabled = false;
	    }
	}
	for (int j = 0; j < texts.Length; j++)
	{
	    if (texts[j] != null)
	    {
		Color color = texts[j].color;
		color.a = 0f;
		texts[j].color = color;
		//texts[j].gameObject.GetComponent<MeshRenderer>().SetActiveWithChildren(false);
	    }
	}
    }
}

 回到Unity当中,我们添加好参数:

这个InvAnimateUpAndDown是在背包系统的时候才用到的脚本,不过不影响我们先创建好它:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class InvAnimateUpAndDown : MonoBehaviour
{
    public string upAnimation;
    public string downAnimation;
    public float upDelay;
    public int randomStartFrameSpriteMax;
    private tk2dSpriteAnimator spriteAnimator;
    private MeshRenderer meshRenderer;
    private float timer;
    private bool animatingDown;
    private bool readyingAnimUp;

    private void Awake()
    {
	spriteAnimator = GetComponent<tk2dSpriteAnimator>();
	meshRenderer = GetComponent<MeshRenderer>();
    }

    private void Update()
    {
	if(animatingDown && !spriteAnimator.Playing)
	{
	    meshRenderer.enabled = false;
	    animatingDown = false;
	}
	if(timer > 0f)
	{
	    timer -= Time.deltaTime;
	}
	if(readyingAnimUp && timer <= 0f)
	{
	    animatingDown = false;
	    meshRenderer.enabled = true;
	    if (randomStartFrameSpriteMax > 0)
	    {
		int frame = Random.Range(0, randomStartFrameSpriteMax);
		spriteAnimator.PlayFromFrame(upAnimation, frame);
	    }
	    else
	    {
		spriteAnimator.Play(upAnimation);
	    }
	    readyingAnimUp = false;
	}
    }

    public void AnimateUp()
    {
	readyingAnimUp = true;
	timer = upDelay;
    }

    public void AnimateDown()
    {
	spriteAnimator.Play(downAnimation);
	animatingDown = true;
    }

    public void ReplayUpAnim()
    {
	meshRenderer.enabled = true;
	spriteAnimator.PlayFromFrame(0);
    }


}

 让我门回到Arrow Prompt New当中,创建脚本PromptMarker .cs方便引用子对象的FadeGroup以及给playmakerFSM的行为调用。

using System.Collections;
using UnityEngine;

public class PromptMarker : MonoBehaviour
{
    public GameObject labels;
    private FadeGroup fadeGroup;
    private tk2dSpriteAnimator anim;
    private GameObject owner;
    private bool isVisible;

    private void Awake()
    {
	anim = GetComponent<tk2dSpriteAnimator>();
	if (labels)
	{
	    fadeGroup = labels.GetComponent<FadeGroup>();
	}
    }

    private void Start()
    {
	if (GameManager.instance)
	{
	    GameManager.instance.UnloadingLevel += RecycleOnLevelLoad;
	}
    }
    private void OnDestroy()
    {
	if (GameManager.instance)
	{
	    GameManager.instance.UnloadingLevel -= RecycleOnLevelLoad;
	}
    }

    private void RecycleOnLevelLoad()
    {
	if (gameObject.activeSelf)
	{
	    gameObject.Recycle();
	}
    }

    private void OnEnable()
    {
	anim.Play("Blank"); //开始时设置动画为Blank空白的
    }

    private void Update()
    {
	if (isVisible && (!owner || !owner.activeInHierarchy))
	{
	    Hide();
	}
    }

    public void SetLabel(string labelName)
    {
	if (labels)
	{
	    foreach (object obj in labels.transform)
	    {
		Transform transform = (Transform)obj;
		transform.gameObject.SetActive(transform.name == labelName);
	    }
	}
    }

    /// <summary>
    /// 被playmaker的行为调用
    /// </summary>
    public void Show()
    {
	anim.Play("Up"); //播放动画Up
	transform.SetPositionZ(0f); //设置好z轴位置
	fadeGroup.FadeUp(); //fadegroup脚本设置alpha 0 -> 1
	isVisible = true; //设置为可视
    }

    /// <summary>
    /// 被playmaker的行为调用
    /// </summary>
    public void Hide()
    {
	anim.Play("Down");
	fadeGroup.FadeDown();
	owner = null; //空引用
	StartCoroutine(RecycleDelayed(fadeGroup.fadeOutTime)); //延迟销毁
	isVisible = false;
    }

    /// <summary>
    /// 延时销毁
    /// </summary>
    /// <param name="delay"></param>
    /// <returns></returns>
    private IEnumerator RecycleDelayed(float delay)
    {
	yield return new WaitForSeconds(delay);
	gameObject.Recycle();
	yield break;
    }

    public void SetOwner(GameObject obj)
    {
	owner = obj;
    }

}

 OK我们已经制作完了一个对话系统所需要的UI界面,接下来开始讲讲一个门的转移点的逻辑处理,用的当然是我们最爱的playmakerFSM了。

来到一个需要门的场景,我们创建好一个door,在它的脚本TransitionPoint.cs勾选上Is A Door!不然的话玩家一碰到碰撞箱就直接进去了。

 然后设置好它的子对象Prompt Marker,这个是标记我们上面讲过的Arrow Prompt new生成的位置。

第一个playmakerFSM叫:Set Compass Point,这个设置游戏地图指南针的标记位置,是属于我还没做到的领域,就先搭个架子放着先:

 重点当然是第二个playmakerFSM:Door Control我们先添加好事件以及变量

以下两个变量是记下与这个门对应的转移点的场景名“Room_temple”和转移点名字“left1” 

 这个是记录使用Arrow Prompt New使用的是哪一个文字,这里我们使用的是Enter

 先来看看第一个状态Wait for enter scene,这个行为是为了当玩家完全完成了进入场景的所有处理以后,再来执行剩下的状态。

代码内容很简单,就是制作一个委托订阅事件GameManager.instance.OnFinishedEnteringScene这个事件。 

using HutongGames.PlayMaker;
using UnityEngine;

[ActionCategory("Hollow Knight")]
public class WaitForFinishedEnteringScene : FsmStateAction
{
    [RequiredField]
    public FsmEvent sendEvent;

    public override void Reset()
    {
	sendEvent = null;
    }

    public override void OnEnter()
    {
	if (!GameManager.instance)
	{
	    Finish();
	    return;
	}
	if (!GameManager.instance.HasFinishedEnteringScene)
	{
	    GameManager.EnterSceneEvent temp = null;
	    temp = delegate ()
	    {
		Fsm.Event(sendEvent);
		GameManager.instance.OnFinishedEnteringScene -= temp;
		Finish();
	    };
	    GameManager.instance.OnFinishedEnteringScene += temp;
	    return;
	}
	Fsm.Event(sendEvent);
    }

}

初始化:

如果当前场景是在遗忘十字路当中,要判断playerdata的变量visitedCrossroads,是否之前就拜访过遗忘十字路。

没有的话就要等10秒,有的话就要等3.5秒: 

这是正常情况下等待的时间:

检测tag为player的游戏对象是否碰到碰撞箱,碰到的话就发送IN RANGE事件 

还有一个名字叫HidePromptMarker的脚本用来在玩家远离这个碰撞箱的时候隐藏我们的Arrow Prompt New

using System;
using HutongGames.PlayMaker;

[ActionCategory("Hollow Knight")]
public class HidePromptMarker : FsmStateAction
{
    [UIHint(UIHint.Variable)]
    public FsmGameObject storedObject;

    public override void Reset()
    {
	storedObject = null;
    }

    public override void OnEnter()
    {
	if (storedObject.Value)
	{
	    PromptMarker component = storedObject.Value.GetComponent<PromptMarker>();
	    if (component)
	    {
		component.Hide();
		storedObject.Value = null;
	    }
	}
	Finish();
    }

}

关键的点来了,在范围当中我们就显示Arrow Prompt New,这里新自定义一个行为叫ShowPromptMarker

通过调用Prompt Marker.cs当中的三个方法来显示我们的HUD提示框。

        component.SetLabel(labelName.Value);
        component.SetOwner(Owner);
        component.Show(); 

using HutongGames.PlayMaker;
using UnityEngine;


[ActionCategory("Hollow Knight")]

public class ShowPromptMarker : FsmStateAction
{
    public FsmGameObject prefab;
    public FsmString labelName;
    [UIHint(UIHint.Variable)]
    public FsmGameObject spawnPoint;
    [UIHint(UIHint.Variable)]
    public FsmGameObject storeObject;

    public override void Reset()
    {
	prefab = new FsmGameObject();
	labelName = new FsmString();
	spawnPoint = new FsmGameObject();
	storeObject = new FsmGameObject();
    }

    public override void OnEnter()
    {
	if (prefab.Value && spawnPoint.Value)
	{
	    GameObject gameObject;
	    if (storeObject.Value)
	    {
		gameObject = storeObject.Value;
	    }
	    else
	    {
		gameObject = prefab.Value.Spawn();
		storeObject.Value = gameObject;
	    }
	    gameObject.transform.position = spawnPoint.Value.transform.position;
	    PromptMarker component = gameObject.GetComponent<PromptMarker>();
	    if (component)
	    {
		component.SetLabel(labelName.Value);
		component.SetOwner(Owner);
		component.Show();
	    }
	}
	base.Finish();
    }

}

然后就是监听玩家是否按下上键和下键:

using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory("Controls")]
    [Tooltip("Listens for an action button press (using HeroActions InControl mappings).")]
    public class ListenForUp : FsmStateAction
    {
	[Tooltip("Where to send the event.")]
	public FsmEventTarget eventTarget;
	public FsmEvent wasPressed;
	public FsmEvent wasReleased;
	public FsmEvent isPressed;
	public FsmEvent isNotPressed;

	[UIHint(UIHint.Variable)]
	public FsmBool isPressedBool;

	public bool stateEntryOnly;

	private GameManager gm;
	private InputHandler inputHandler;

	public override void Reset()
	{
	    eventTarget = null;
	}

	public override void OnEnter()
	{
	    gm = GameManager.instance;
	    inputHandler = gm.GetComponent<InputHandler>();
	    CheckForInput();
	    if (stateEntryOnly)
	    {
		Finish();
	    }
	}


	public override void OnUpdate()
	{
	    CheckForInput();
	}

	private void CheckForInput()
	{
	    if (!gm.isPaused)
	    {
		if (inputHandler.inputActions.up.WasPressed)
		{
		    Fsm.Event(wasPressed);
		}
		if (inputHandler.inputActions.up.WasReleased)
		{
		    Fsm.Event(wasReleased);
		}
		if (inputHandler.inputActions.up.IsPressed)
		{
		    Fsm.Event(isPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = true;
		    }
		}
		if (!inputHandler.inputActions.up.IsPressed)
		{
		    Fsm.Event(isNotPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = false;
		    }
		}
	    }
	}

    }
}
using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{

    [ActionCategory("Controls")]
    [Tooltip("Listens for an action button press (using HeroActions InControl mappings).")]
    public class ListenForDown : FsmStateAction
    {

	[Tooltip("Where to send the event.")]
	public FsmEventTarget eventTarget;
	public FsmEvent wasPressed;
	public FsmEvent wasReleased;
	public FsmEvent isPressed;
	public FsmEvent isNotPressed;

	[UIHint(UIHint.Variable)]
	public FsmBool isPressedBool;

	public bool stateEntryOnly;

	private GameManager gm;
	private InputHandler inputHandler;

	public override void Reset()
	{
	    eventTarget = null;
	}

	public override void OnEnter()
	{
	    gm = GameManager.instance;
	    inputHandler = gm.GetComponent<InputHandler>();
	    CheckForInput();
	    if (stateEntryOnly)
	    {
		Finish();
	    }
	}

	public override void OnUpdate()
	{
	    CheckForInput();
	}

	private void CheckForInput()
	{
	    if (!gm.isPaused)
	    {
		if (inputHandler.inputActions.down.WasPressed)
		{
		    Fsm.Event(wasPressed);
		}
		if (inputHandler.inputActions.down.WasReleased)
		{
		    Fsm.Event(wasReleased);
		}
		if (inputHandler.inputActions.down.IsPressed)
		{
		    Fsm.Event(isPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = true;
		    }
		}
		if (!inputHandler.inputActions.down.IsPressed)
		{
		    Fsm.Event(isNotPressed);
		    if (!isPressedBool.IsNone)
		    {
			isPressedBool.Value = false;
		    }
		}
	    }
	}
    }
}

最后就是当玩家离开范围的时候就回到Idle状态。

下一个状态Can Enter?就是判断玩家当前情况下能否使用UI互动。这里通过行为CallMethodProper调用HeroController的方法CanInteract并且存储变量Can Interact.

public bool CanInteract()
    {
        return CanInput() && hero_state != ActorStates.no_input && !gm.isPaused && !cState.dashing && !cState.backDashing && !cState.attacking && !controlReqlinquished && !cState.hazardDeath && !cState.hazardRespawning && !cState.recoilFrozen && !cState.recoiling && !cState.transitioning && cState.onGround;
    }

如果不能互动的话,就等一帧回到In Range状态:

我们还要判断新场景是否是White_Palace_01,这个暂时用不到哈 

我们现在肯定是做不到白色宫殿的,只能走左边这条路了,首先是转移Audio Snapshot

发送事件SET COMPASS POINT给另一个playmakerFSM

小骑士播放动画Exit

进入的状态下:玩家停止其它动画播放,停止输入,向主摄像机发送事件FADE OUT场景淡入, 

 

最后就是开始场景转移,这也在14集我们讲教学关卡打那个巨型岩石的时候讲到过。

using UnityEngine;

namespace HutongGames.PlayMaker.Actions
{
    [ActionCategory("Game Manager")]
    [Tooltip("Perform a generic scene transition.")]
    public class BeginSceneTransition : FsmStateAction
    {
	public FsmString sceneName;
	public FsmString entryGateName;
	public FsmFloat entryDelay;
	[ObjectType(typeof(GameManager.SceneLoadVisualizations))]
	public FsmEnum visualization;

	public bool preventCameraFadeOut;

	public override void Reset()
	{
	    sceneName = "";
	    entryGateName = "left1";
	    entryDelay = 0f;
	    visualization = new FsmEnum
	    {
		Value = GameManager.SceneLoadVisualizations.Default
	    };
	    preventCameraFadeOut = false;
	}

	public override void OnEnter()
	{
	    GameManager unsafeInstance = GameManager.instance;
	    if (unsafeInstance == null)
	    {
		LogError("Cannot BeginSceneTransition() before the game manager is loaded.");
	    }
	    else
	    {
		unsafeInstance.BeginSceneTransition(new GameManager.SceneLoadInfo
		{
		    SceneName = sceneName.Value,
		    EntryGateName = entryGateName.Value,
		    EntryDelay = entryDelay.Value,
		    Visualization = (GameManager.SceneLoadVisualizations)visualization.Value,
		    PreventCameraFadeOut = true,
		    WaitForSceneTransitionCameraFade = !preventCameraFadeOut,
		    AlwaysUnloadUnusedAssets = false
		});
	    }
	    Finish();
	}


    }

}

OK我们成功了扩展了一个新的转移点类型,也即是门,也制作了最简单的一种对话系统,那就乘胜追击制作一个游戏的石碑的完整行为以及确认能力界面。 

2.以游戏的石碑为例制作确认能力界面

        其实游戏的石碑我这样说有点陌生,其实就是这个东西,它的英文名字叫Tut_tablet_top:它只会出现在一些特定的关卡里。

来看看它的子对象有什么:

靠近的时候的粒子系统: 

 

最后那个子对象Focus_prompt_temp我打算放到确认界面来讲。

首先来看看Tut_tablet_top的两个playmakerFSM,第一个就很简单Tablet Control,当玩家靠近和原理这个石碑的时候播放的一些粒子效果:

判断距离是否在10以内。 

讲一下这个FadeColorFader行为:也是渐变的方式让自己或者自己的子对象要不淡入要不淡出。

using System;
using HutongGames.PlayMaker;
using UnityEngine;

[ActionCategory("Hollow Knight")]
public class FadeColorFader : FsmStateAction
{
    public FsmOwnerDefault target;
    [ObjectType(typeof(FadeType))]
    public FsmEnum fadeType;
    public FsmBool useChildren;

    public override void Reset()
    {
	target = null;
	fadeType = null;
	useChildren = new FsmBool(true);
    }

    public override void OnEnter()
    {
	GameObject safe = target.GetSafe(this);
	if (safe)
	{
	    ColorFader[] array;
	    if (useChildren.Value)
	    {
		array = safe.GetComponentsInChildren<ColorFader>();
	    }
	    else
	    {
		array = new ColorFader[]
		{
		    safe.GetComponent<ColorFader>()
		};
	    }
	    ColorFader[] array2 = array;
	    for (int i = 0; i < array2.Length; i++)
	    {
		array2[i].Fade((FadeType)fadeType.Value == FadeType.UP);
	    }
	}
	base.Finish();
    }

    public enum FadeType
    {
	UP,
	DOWN
    }
}

 可以看到这个ColorFader.cs脚本啊,其实核心代码就这几段:

for (float elapsed = 0f; elapsed < time; elapsed += Time.deltaTime)
    {
        Color color = Color.Lerp(from, to, elapsed / time) * initialColour;

    }

设置好这三个参数随便玩玩的Color to, float time, float delay

using System;
using System.Collections;
using TMPro;
using UnityEngine;

public class ColorFader : MonoBehaviour
{
    public Color downColour = new Color(1f, 1f, 1f, 0f);
    public float downTime = 0.4f;
    public Color upColour = new Color(1f, 1f, 1f, 1f);
    public float upDelay;
    public float upTime = 0.4f;
    private Color initialColour;
    public bool useInitialColour = true;

    private SpriteRenderer spriteRenderer;
    private TextMeshPro textRenderer;
    private tk2dSprite tk2dSprite;
    private bool setup;

    private Coroutine fadeRoutine;

    public delegate void FadeEndEvent(bool up); 
    public event FadeEndEvent OnFadeEnd;

    private void Reset()
    {
	foreach (PlayMakerFSM playMakerFSM  in GetComponents<PlayMakerFSM>())
	{
	    if ((playMakerFSM.FsmTemplate ? playMakerFSM.FsmTemplate.name : playMakerFSM.FsmName) == "color_fader")
	    {
		downColour = playMakerFSM.FsmVariables.GetFsmColor("Down Colour").Value;
		downTime = playMakerFSM.FsmVariables.GetFsmFloat("Down Time").Value;
		upColour = playMakerFSM.FsmVariables.GetFsmColor("Up Colour").Value;
		upDelay = playMakerFSM.FsmVariables.GetFsmFloat("Up Delay").Value;
		upTime = playMakerFSM.FsmVariables.GetFsmFloat("Up Time").Value;
		return;
	    }
	}
    }

    private void Start()
    {
	Setup();
    }

    private void Setup()
    {
	if (!setup)
	{
	    setup = true;
	    if (!spriteRenderer)
	    {
		spriteRenderer = GetComponent<SpriteRenderer>();
	    }
	    if (spriteRenderer)
	    {
		initialColour = (useInitialColour ? spriteRenderer.color : Color.white);
		spriteRenderer.color = downColour * initialColour;
		return;
	    }
	    if (!textRenderer)
	    {
		textRenderer = GetComponent<TextMeshPro>();
	    }
	    if (textRenderer)
	    {
		initialColour = (useInitialColour ? textRenderer.color : Color.white);
		textRenderer.color = downColour * initialColour;
		return;
	    }
	    if (!tk2dSprite)
	    {
		tk2dSprite = GetComponent<tk2dSprite>();
	    }
	    if (tk2dSprite)
	    {
		initialColour = (useInitialColour ? tk2dSprite.color : Color.white);
		tk2dSprite.color = downColour * initialColour;
	    }
	}
    }

    public void Fade(bool up)
    {
	Setup();
	if (fadeRoutine != null)
	{
	    StopCoroutine(fadeRoutine);
	}
	if (up)
	{
	    fadeRoutine = StartCoroutine(Fade(upColour, upTime, upDelay));
	    return;
	}
	fadeRoutine = StartCoroutine(Fade(downColour, downTime, 0f));
    }

    private IEnumerator Fade(Color to, float time, float delay)
    {
	if (!spriteRenderer)
	{
	    spriteRenderer = GetComponent<SpriteRenderer>();
	}
	Color from = spriteRenderer ? spriteRenderer.color : (textRenderer ? textRenderer.color : (tk2dSprite ? tk2dSprite.color : Color.white));
	if (delay > 0f)
	{
	    yield return new WaitForSeconds(upDelay);
	}
	for (float elapsed = 0f; elapsed < time; elapsed += Time.deltaTime)
	{
	    Color color = Color.Lerp(from, to, elapsed / time) * initialColour;
	    if (spriteRenderer)
	    {
		spriteRenderer.color = color;
	    }
	    else if (textRenderer)
	    {
		textRenderer.color = color;
	    }
	    else if (tk2dSprite)
	    {
		tk2dSprite.color = color;
	    }
	    yield return null;
	}
	if (spriteRenderer)
	{
	    spriteRenderer.color = to * initialColour;
	}
	else if (textRenderer)
	{
	    textRenderer.color = to * initialColour;
	}
	else if (tk2dSprite)
	{
	    tk2dSprite.color = to * initialColour;
	}
	if (OnFadeEnd != null)
	{
	    OnFadeEnd(to == upColour);
	}
    }

}

然后我们来看看另一个playmakerFSM名字叫Inspection,这个其实和我们下一期要讲到的NPC系统中所使用的逻辑行为控制的playmakerFSM很相似, 我们来看看它的变量和事件

由于这个石碑是确认玩家能使用回血也就是Focus Prompt,记得勾上 

第一个状态就是等下一帧: 

初始化阶段,新建一个游戏对象叫Arrow Prompt并存储到变量Prompt中,对它的playmakerFSMPrompt Control,设置它的Prompt Name为“Inspect”  ,删除子对象prompt marker因为这个坐标已经没有用了

你可能会好奇,这个Arrow Prompt和我上一节讲到的Arrow Prompt New有什么区别吗,        其实它们的大部分内容都是一样的,只是这个Arrow Prompt使用playmakerFSM来控制的。

我们来制作这个playmakerFSM:

 第二步就是获取子对象Labels并隐藏它的全部子对象,

初始化阶段将自身的动画设置为Blank空白 

当外界发送UP事件给它以后,播放Up动画,开始shade的renderer,还有对label的fade group up

完成后进入UP状态:

直到外界发送事件,进入Go Down状态:

这里还有一个公共事件:注意别漏了这个行为Goto Previous State

回到石碑当中,接下来就是判断玩家是否到能检视对话的距离了:

在In Range状态,就提到了我上面讲的发送给Arrow Prompt的UP事件 还有就是监听按键了

接下来是判断能否互动的状态,这里我用的是HeroController的CanInput()方法

 public bool CanInput()
    {
        return acceptingInput;
    }

相比于转移点,这里的条件还要苛刻的多,它这里有获取了玩家是否正在执行这五种状态:攻击,向上攻击,向下攻击,冲刺和后撤步(这个没做),还要判断玩家是否在地面上,如果这些有一个不满足,都要到达取消状态 

取消然后回到In Range状态:

 到达夺取控制权的状态:玩家不能发送输入,停止动画改变,发送事件DOWN给arrow prompt

中间这五个是为了玩家在聆听NPC对话的时候出现在一个正常的位置,是下一期讲NPC要用到的,这里我们直接跳过到达Prompt Up状态: 

还有这个全球变量Dialogue Text也不用管它, 是下一篇要用到对话框的UI

判断当前对话是否是focus prompt,这里当然是是的。 

激活我们的子对象Focus_prompt_temp,并让它Fade Up自己的子对象 

这个状态不用管

这里需要注意,玩家受到伤害后会直接跳到Set Bool状态

最后全部完成了后,就让玩家播放动画:TurnFromBG

 重新获得动画的输入控制

然后回到Idle状态:

然后就是制作确认能力界面了,其实说起来有点不明所以,其实就是这个告诉玩家你有能回血的能力了。

        这些都太简单了,就是制作几个Text Mesh的UI啊,唯一需要注意的是控制好每一个有Color Fader.cs脚本的变量Up Delay,也就是延迟显示的时间。

        至此我们完成了一个完整的游戏对话系统,来总结处看看效果吧。

        其实这期很多都没讲好,因为这里面的逻辑处理和NPC系统的逻辑系统有众多相似之处,所以只能等到下一期再给大家细细的讲了。


总结

首先来看看门,这个门是离得近和离得远的显示

当按下特定按键的时候,开始转移场景,

再从传送点回到门:

  

然后是石碑,可以看到啊它离得近和离得远的显示,

当按下按键的时候,玩家停止接受控制和动画改变,然后就是显示石碑的内容和确认菜单界面,这个对话框我要到下一期才能讲到,然后就是丢脸给大伙看我这答辩般的UI设计。

结束

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

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

相关文章

ChatGPT 桌面版发布了,如何安装?

本章教程教大家如何进行安装。 一、下载安装包 官网地址地址&#xff1a;https://openai.com/chatgpt/desktop/ 支持Windows和MacOS操作系统 二、安装步骤 Windows用户下载之后&#xff0c;会有一个exe安装包&#xff0c;点击运行安装即可。 注意事项&#xff0c;如果Windows操…

Excel求和如何过滤错误值

一、问题的提出 平时&#xff0c;我们在使用Excel时&#xff0c;最常用的功能就是求和了&#xff0c;一说到求和你可能想到用sum函数&#xff0c;但是如果sum的求和区域有#value #Div等错误值怎么办&#xff1f;如下图&#xff0c;记算C列中工资的总和。 直接用肯定会报错&…

librdns一个开源DNS解析库

原文地址&#xff1a;librdns一个开源DNS解析库 – 无敌牛 欢迎参观我的个人博客&#xff1a;无敌牛 – 技术/著作/典籍/分享等 介绍 librdns是一个开源的异步多功能插件式的解析器&#xff0c;用于DNS解析。 源代码地址&#xff1a;GitHub - vstakhov/librdns: Asynchrono…

成都睿明智科技有限公司怎么样可靠不?

在这个日新月异的数字时代&#xff0c;电商行业如同一股不可阻挡的洪流&#xff0c;席卷着每一个消费者的生活。而抖音&#xff0c;作为短视频与电商完美融合的典范&#xff0c;更是为无数商家开辟了一片全新的蓝海。在这片充满机遇与挑战的海洋中&#xff0c;成都睿明智科技有…

栈的应用,力扣394.字符串解码力扣946.验证栈序列力扣429.N叉树的层序遍历力扣103.二叉树的锯齿形层序遍历

目录 力扣394.字符串解码 力扣946.验证栈序列 力扣429.N叉树的层序遍历 力扣103.二叉树的锯齿形层序遍历 力扣394.字符串解码 看见括号&#xff0c;由内而外&#xff0c;转向用栈解决。使用两个栈处理&#xff0c;一个用String,一个用Integer 遇到数字:提取数字放入到数字栈…

Vue3移动端-点餐项目

目录 一、项目构建 1、使用了vite构建vue3项目&#xff0c;其中支持如下图 2、其余路由配置、axios封装、组件封装 都与 后台管理系统 方式一致 二、Vant组件引入 1、安装 &#xff08;vue3使用vant4&#xff09;Vant官方文档 2、全局引入main.ts/js 3、如果是基于 Vite 的…

分词器的概念(通俗易懂版)

什么是分词器&#xff1f;分词器&#xff08;Tokenizer&#xff09;是自然语言处理&#xff08;NLP&#xff09;中的一个关键组件&#xff0c;它的主要功能是将文本数据分解成更小的单元&#xff0c;这些单元可以是单词、子词&#xff08;subword&#xff09;、字符或其他有意义…

【JavaEE进阶】 JavaScript

本节⽬标 了解什么是JavaScript, 学习JavaScript的常⻅操作, 以及使⽤JQuery完成简单的⻚⾯元素操作. 一. 初识 JavaScript 1.JavaScript 是什么 JavaScript (简称 JS), 是⼀个脚本语⾔, 解释型或即时编译型的编程语⾔. 虽然它是作为开发Web⻚⾯的脚本语⾔⽽出名&#xff0c;…

浮点数的表示—IEEE754标准

浮点数的表示—IEEE754标准 引言 我们知道&#xff0c;在计算机中&#xff0c;数字以0和1组成的二进制序列来表示。但是&#xff0c;对于非常大的数字以及非常接近0的数字&#xff0c;简单的存储方式往往会造成精度的丢失。 为了解决这个问题&#xff0c;提供更高效的浮点数…

uniapp vue2项目迁移vue3项目

uniapp vue2项目迁移vue3项目&#xff0c;必须适配的部分 一、main.js 创建应用实例 // 之前 - Vue 2 import Vue from vue import App from ./App Vue.config.productionTip false // vue3 不再需要 App.mpType app // vue3 不再需要 const app new Vue({ ...App }) …

计算机网络(14)ip地址超详解

先看图&#xff1a; 注意看第三列蓝色标注的点不会改变&#xff0c;A类地址第一个比特只会是0&#xff0c;B类是10&#xff0c;C类是110&#xff0c;D类是1110&#xff0c;E类是1111. IPv4地址根据其用途和网络规模的不同&#xff0c;分为五个主要类别&#xff08;A、B、C、D、…

Unity DOTS中的Entity

Unity DOTS中的Entity 在DOTS中entity往往只被看作一个ID&#xff0c;用来查找component&#xff0c;但实际上Unity为了有效地管理entity&#xff0c;在背后还做了一些其他的工作。首先是Entity类本身的定义&#xff0c;它的确跟一个ID差不多&#xff0c;只包含了两个int类型的…

SpringBoot实现单文件上传

一、在springBoot项目中的pom.xml添加依赖。 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency> 二、在资源目录下的static目录下中创建一个upload.html的表单文件…

CNN—LeNet:从0开始神经网络学习,实战MNIST和CIFAR10~

文章目录 前言一、CNN与LeNet介绍二、LeNet组成及其名词解释2.1 输入2.2 卷积层2.3池化层2.4 全连接层2.5 总结 三、MNIST实战3.1 构建神经网络3.2 数据处理3.3 &#xff08;模板&#xff09;设置优化器&#xff0c;损失函数&#xff0c;使用gpu(如果是N卡有cuda核心)&#xff…

PVE系统中风扇驱动安装——linux 硬件驱动安装(IT8613E为例)

本文提供全流程命令代码,IT8613E的Github下载地址,pve头文件官方下载地址 对网卡驱动感兴趣的可以看这篇文章 linux系统下 usb网卡的驱动安装_0bda:a192-CSDN博客文章浏览阅读1.5w次,点赞16次,收藏72次。本文介绍如何通过lsusb查找USB网卡vid:pid,使用google搜索驱动信息…

美国人工智能国家安全备忘录核心解读(下)

文章目录 三、美国国内和国际人工智能治理策略1.保证AI政策有效执行的协调措施2.推进AI治理格局的优势地位&#xff08;1&#xff09;对于美国盟友&#xff1a;试图向盟友保证其将从美国的战略中获益。&#xff08;2&#xff09;对于美国的战略竞争对手&#xff1a;介绍了超越竞…

工具学习_Docker

0. Docker 简介 Docker 是一个开源平台&#xff0c;旨在帮助开发者构建、运行和交付应用程序。它通过容器化技术将应用程序及其所有依赖项打包在一个标准化的单元&#xff08;即容器&#xff09;中&#xff0c;使得应用程序在任何环境中都能保持一致的运行效果。Docker 提供了…

红黑树模拟实现STL中的map与set

1.map 在C标准模板库(STL)中&#xff0c;std::map是一种非常实用且强大的容器&#xff0c;它提供了键值对的存储机制。这使得std::map成为处理具有唯一关键的关联数据的理想选择。 1.1 map的特性 1、键值对存储&#xff1a;std::map通过键值对的形式存储数据&#xff0c;其中…

【数据结构专栏】二叉搜索树(Binary Search Tree)的剖析?

文章目录 &#x1f9e8;前言1、二叉搜索树的基本概念&#xff1f;2、二叉搜索树的节点结构组成&#xff1f;3、二叉搜索树的插入操作&#xff1f;4、二叉搜索树的删除操作&#xff1f;5、二叉搜索树的遍历&#xff1f; 6、二叉搜索树的性能分析&#xff1f; &#x1f389;完整代…

FastApi学习第三天:两表联查

两表联查 在 FastAPI 中&#xff0c;使用 Tortoise ORM 查询两表联查&#xff08;通常是通过外键关系进行联接&#xff09;是非常简单的。可以使用 select_related 或 prefetch_related 来执行联表查询&#xff0c;它们类似于 Django ORM 的 select_related 和 prefetch_relate…