提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 一、制作游戏的对话系统
- 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"); //播放动画Uptransform.SetPositionZ(0f); //设置好z轴位置fadeGroup.FadeUp(); //fadegroup脚本设置alpha 0 -> 1isVisible = 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设计。
结束