提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作游戏的对话系统
- 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设计。
结束