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


前言     

  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设计。

结束

Logo

分享前沿Unity技术干货和开发经验,精彩的Unity活动和社区相关信息

更多推荐