GamePlay

对话框

上一章说的输入控制器里,还有对对话框输入的控制,这小节就看看对话框的整体实现方式。

对话框输入控制

先看一下对话框输入控制,我对其有一点点修改。

        void DialogControl(){model.player.nextMoveCommand = Vector3.zero;if (Input.GetKeyDown(KeyCode.LeftArrow))// model.dialog.FocusButton(-1);curButton = -1;else if (Input.GetKeyDown(KeyCode.RightArrow))// model.dialog.FocusButton(+1);curButton = 1;model.dialog.FocusButton(curButton);if (Input.GetKeyDown(KeyCode.Return))model.dialog.SelectActiveButton();}

可以看到调用了model.dialog.FocusButton(curButton);model.dialog.SelectActiveButton();,前者是调用button 的选中响应,后者调用button 的点击响应。

先看FocusButton 做了什么,代码如下,selectedButton 是当前选中button的下标,用direction 控制下标切换,执行上一个button的Exit() 方法和当前button 的Enter() 方法。

        public void FocusButton(int direction){if (buttonCount > 0){if (selectedButton < 0) selectedButton = 0;buttons[selectedButton].Exit();selectedButton += direction;selectedButton = Mathf.Clamp(selectedButton, 0, buttonCount - 1);buttons[selectedButton].Enter();}}

再看SelectActiveButton,代码如下。

        public void SelectActiveButton(){if (buttonCount > 0){if (selectedButton >= 0){model.input.ChangeState(InputController.State.CharacterControl);buttons[selectedButton].Click();selectedButton = -1;}}else{//there are no buttons, just Hide when required.model.input.ChangeState(InputController.State.CharacterControl);Hide();}}

对话框分为有按钮的和没按钮的,对于有按钮的,会调用这个按钮的点击事件Click(),转换到角色控制状态。(ps:在对话控制状态时是没法处理移动输入的。)

对于没有按钮的对话框,直接隐藏对话框,转换到角色控制状态。

对话框控制器

上面讲的两个方法是属于对话控制器的一部分,这小节看看这个控制器的整体实现。

控制器的属性变量如下。

        public DialogLayout dialogLayout;   // UI Viewpublic System.Action<int> onButton; // Button点击事件public int selectedButton = 0;  // 当前选中的buttonpublic int buttonCount = 0; // button 总数SpriteButton[] buttons; // 当前对话框的button数组Camera mainCamera;GameModel model = Schedule.GetModel<GameModel>();SpriteUIElement spriteUIElement;

Awake() 时,代码如下,给button赋值,设置button的点击事件,onClickEvent 是自定义的一个事件,下一小节讲button的时候会说。这里为什么能用+=?指路Action的多播:委托的多播

        void Awake(){dialogLayout.gameObject.SetActive(false);buttons = dialogLayout.buttons;dialogLayout.buttonA.onClickEvent += () => OnButton(0);dialogLayout.buttonB.onClickEvent += () => OnButton(1);dialogLayout.buttonC.onClickEvent += () => OnButton(2);spriteUIElement = GetComponent<SpriteUIElement>();mainCamera = Camera.main;}

之后是Show() 方法,用来显示对话框。这个方法有好几个重载,不过都差不多,看一个就行了。要说的都在下面的注释里。

        public void Show(Vector3 position, string text, string buttonA, string buttonB, string buttonC){UserInterfaceAudio.OnShowDialog();   // 播放声音var d = dialogLayout;   d.gameObject.SetActive(true);   // 显示UId.SetText(text, buttonA, buttonB, buttonC);  // 设置UI元素SetPosition(position); // 设置显示位置model.input.ChangeState(InputController.State.DialogControl); // 显示对话框时进入对话框控制状态buttonCount = 3;  selectedButton = -1;}void SetPosition(Vector3 position)    // 通过spriteUIElement 来设置位置{var screenPoint = mainCamera.WorldToScreenPoint(position);position = spriteUIElement.camera.ScreenToViewportPoint(screenPoint);spriteUIElement.anchor = position;}

这个对话控制器,使用了两个组件,SpriteButtonDialogLayout,前者是自定义button,后者是对话框UI,接下来说说这两个。

自定义button

这个简单,直接放代码。

namespace RPGM.UI
{public class SpriteButton : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler{public SpriteRenderer spriteRenderer;public TextMeshPro textMeshPro;public Vector2 Size => spriteRenderer.size;public event System.Action onClickEvent;public void Enter(){textMeshPro.color = Color.yellow;UserInterfaceAudio.OnButtonEnter();}public void Exit(){textMeshPro.color = Color.white;UserInterfaceAudio.OnButtonExit();}public void Click(){if (onClickEvent != null) onClickEvent();textMeshPro.color = Color.white;UserInterfaceAudio.OnButtonClick();}public void OnPointerClick(PointerEventData eventData) => Click();public void OnPointerEnter(PointerEventData eventData) => Enter();public void OnPointerExit(PointerEventData eventData) => Exit();public void SetText(string text) => textMeshPro.text = text;void Reset(){spriteRenderer = GetComponent<SpriteRenderer>();textMeshPro = GetComponentInChildren<TextMeshPro>();}}
}

这个button 包含一个精灵和一个文本。

IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler 是需要处理进入、退出、点击事件时用到的,需要重载它们的OnPointerClick() 方法,

当进入的时候把颜色设为黄色,退出的时候设为白色,效果就是上面的对话框截图所示。这里的点击事件,播放点击音效,调用了Action onClickEvent,这个就是button 的自定义事件

对话框UI DialogLayout

这个就是对UI的操作,没啥特别需要注意的,注意看看UI界面的调整吧

        void Update(){PositionIcon();ScaleBackgroundToFitText();PositionButtons();}void PositionIcon(){if (iconRenderer.sprite != null) // 设置icon 的位置{var p = new Vector3(1, 0, 0);// p.x -= iconRenderer.size.x * 0.5f;iconRenderer.transform.localPosition = p;}}void PositionButtons()    // 设置几个按钮的位置{var s = (Vector3)spriteRenderer.size;  // 这个sprite 必须是Tiled 才有这个sizebuttonA.transform.localPosition = new Vector3(-0.1f - buttonA.Size.x * 0.5f, (-s.y * 0.5f) - buttonA.Size.y * 0.5f - 0.05f, 0);buttonB.transform.localPosition = new Vector3(+0.1f + buttonB.Size.x * 0.5f, (-s.y * 0.5f) - buttonB.Size.y * 0.5f - 0.05f, 0);buttonC.transform.localPosition = new Vector3(0, (-s.y * 0.5f) - buttonC.Size.y * 0.5f - 0.1f - buttonC.Size.y, 0);}void ScaleBackgroundToFitText()  // 修改对话框的大小{var s = (Vector2)textMeshPro.bounds.size;s += Vector2.one * padding;s.x = Mathf.Max(minSize.x, s.x);s.y = Mathf.Max(minSize.y, s.y);spriteRenderer.size = s;}

Update() 里摆icon 位置,根据文字多少调整对话框大小,摆button位置。

云朵系统

在游戏运行时地面会有云朵形状的阴影飘过,这节看看这是怎么实现的。

直接看代码,想说的都在注释里

namespace RPGM.Gameplay
{/// <summary>/// Performs batch translation of cloud transforms, resetting the transform position when/// the cloud transform position exceeds the resetRadius distance to the controller./// Automatically collects and animates all it's child transforms./// </summary>public class CloudSystem : MonoBehaviour{public Vector3 windDirection = Vector2.left;    // 风向,也是云移动的方向public float windSpeed = 1;public float minSpeed = 0.5f;public float resetRadius = 100;     // 云移动限制在一个圆内,这个变量是圆的半径Transform[] clouds;float[] speeds;void Start(){clouds = new Transform[transform.childCount];   // 获得所有子组件,在Hierarchy 里挂好speeds = new float[transform.childCount];for (var i = 0; i < transform.childCount; i++)  // 对于每朵云,设一个随机速度{clouds[i] = transform.GetChild(i);speeds[i] = Random.value;   // Returns a random float within [0.0..1.0] }}void Update(){var r2 = resetRadius * resetRadius;for (var i = 0; i < speeds.Length; i++){var cloud = clouds[i];var speed = Mathf.Lerp(minSpeed, windSpeed, speeds[i]); // 每帧插值随机一个速度cloud.position += windDirection * speed;    if (cloud.localPosition.sqrMagnitude > r2)  // 当云移动的距离大于半径时,云的位置取反{// cloud.position = -cloud.position;   // 原代码是这个,这样是把世界位置取反,效果不对cloud.localPosition = -cloud.localPosition; // 把相对位置取反,实际就是变到圆形的对角}}}/// <summary>/// gizmos 工具/// </summary>void OnDrawGizmos()     {Gizmos.DrawWireSphere(transform.position, resetRadius);}}
}

其中有两点需要注意的,一个是OnDrawGizmos() 的用法。这个我理解主要是在编辑器页面画辅助线的,例如这个例子,就是画了一个以transform.position 为圆心,resetRadius 为半径的圆,也就是能可视化的知道云朵的移动范围,效果如下:

云朵只能在白圈内移动,Gizmos 还有一些其他用法,如改变脚本图标等,可以自行了解。

另一个是云朵位置的变换。给定一个随机速度移动,移动到圆形边界的时候就把相对位置取反,变到对角之后重新移动,如下图所示。

动画控制器

这个Demo 实现了一个批量帧序列动画控制器,为了让同一种物体动画统一更新。在这个Demo 中主要用于背景草木的动画。

不多说,都在注释里。

namespace RPGM.Mechanics
{public class AnimationBatchController : MonoBehaviour{/// <summary>/// 把同一种动画挂在同一父物体下,实现批量帧序列切换,如背景树的摇动/// </summary>[System.Serializable]public class AnimationBatch {public int frame;           // 当前帧数public Transform sceneParent;   // 父物体public Sprite[] animationFrames;    // 动画序列帧,在编辑器里挂上public SpriteRenderer[] spriteRenderers; // 动画序列帧对应的render组件}public float frameRate = 12;    // 帧速率,每秒12帧public AnimationBatch[] batches;    // 控制的动画列表public float nextFrameTime = 0; // 下一帧的时间void OnEnable(){foreach (var batch in batches)  // 获得所有sprite 的render 组件{if (batch.sceneParent != null){batch.spriteRenderers = batch.sceneParent.GetComponentsInChildren<SpriteRenderer>();}}}void Update(){//if it's time for the next frame...if (Time.time - nextFrameTime > (1f / frameRate))   // 帧时间到了的话,切换下一帧{foreach (var batch in batches){AnimateBatch(batch);}//calculate the time of the next frame.nextFrameTime += 1f / frameRate;}}void AnimateBatch(AnimationBatch batch){//update all tokens with the next animation frame.var renderers = batch.spriteRenderers;var frames = batch.animationFrames;var count = frames.Length;var frameIndex = batch.frame;   // 动画序列的当前帧for (var i = 0; i < batch.spriteRenderers.Length; i++)  // 更新每一个render 的sprite,也就是更换每一个物体的图片,注意这个批处理,是处理同一种物体的每一个。{var sr = renderers[i];sr.sprite = frames[(frameIndex + i) % count];}batch.frame += 1;   }}
}

动画状态回调

StateMachineBehaviour 用于动画状态时自定义一些行为,比如进入动画播放音效,这个Demo 中就使用它实现了行走时播放脚步声音效的功能。

继承StateMachineBehaviour 的脚本只能挂到Animator 里的状态上。

其他的,老规矩,该说的都在代码里。

namespace RPGM.Gameplay
{/// <summary>/// Triggers footsteps sounds during playback of an animation state./// </summary>public class FootstepTimer : StateMachineBehaviour{[Range(0, 1)]public float leftFoot, rightFoot;   // 假设这两个值分别是0.1 和0.6public AudioClip[] clips;float lastNormalizedTime;int clipIndex = 0;override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex){var t = stateInfo.normalizedTime % 1;   // normalizedTime 范围是 0-1 0是动画开始,1是动画结束,这个取余去掉整数部分// Debug.Log(stateInfo.normalizedTime);// Debug.Log(t);if (lastNormalizedTime < leftFoot && t >= leftFoot)  // 上次时间小于leftFoot 并且这帧动画时间大于leftFoot 时,播放clips[clipIndex] 音效,并切换下一音效{UserInterfaceAudio.PlayClip(clips[clipIndex]);clipIndex = (clipIndex + 1) % clips.Length;}if (lastNormalizedTime < rightFoot && t >= rightFoot)   // 处理右脚,也就是一个动画里会切换两次音效,分别是左右脚{UserInterfaceAudio.PlayClip(clips[clipIndex]);clipIndex = (clipIndex + 1) % clips.Length;}lastNormalizedTime = t;}}
}

音乐控制

实现当前播放音乐的切换,没啥说的直接看代码吧。

OnEnable() 时添加两个组件,A 用来播放音乐,B 用来备用,实现音效渐变消失的效果。

Update() 里就是实现的两个音源的声音变化,A 在crossFadeTime 时间内逐渐变为1,B 逐渐变为0,也就是声音逐渐消失。

CrossFade() 里切换音源。

代码如下:

namespace RPGM.Gameplay
{public class MusicController : MonoBehaviour{public AudioMixerGroup audioMixerGroup;     // 音效混合组public AudioClip audioClip;public float crossFadeTime = 3;    // 音源渐变需要的时间AudioSource audioSourceA, audioSourceB;float audioSourceAVolumeVelocity, audioSourceBVolumeVelocity;public void CrossFade(AudioClip audioClip)      // 切换音源,永远播放A,B用来实现渐变消失{var t = audioSourceA;audioSourceA = audioSourceB;audioSourceB = t;audioSourceA.clip = audioClip;audioSourceA.Play();}void Update()   // 两个音源,一个渐变消失,一个渐变响起{audioSourceA.volume = Mathf.SmoothDamp(audioSourceA.volume, 1f, ref audioSourceAVolumeVelocity, crossFadeTime, 1);audioSourceB.volume = Mathf.SmoothDamp(audioSourceB.volume, 0f, ref audioSourceBVolumeVelocity, crossFadeTime, 1);}void OnEnable(){audioSourceA = gameObject.AddComponent<AudioSource>();audioSourceA.spatialBlend = 0;  // 设置此 AudioSource 受 3D 空间化计算(衰减、多普勒等)影响的程度。0.0 使声音全 2D,1.0 使其全 3D。audioSourceA.clip = audioClip;audioSourceA.loop = true;audioSourceA.outputAudioMixerGroup = audioMixerGroup;audioSourceA.Play();audioSourceB = gameObject.AddComponent<AudioSource>();audioSourceB.spatialBlend = 0;audioSourceB.loop = true;audioSourceB.outputAudioMixerGroup = audioMixerGroup;}}
}

【Creator Kit - RPG 代码分析】(4)-游戏玩法-对话框、云朵系统、帧序列动画控制器、动画状态回调、音乐控制相关推荐

  1. 【Creator Kit - RPG 代码分析】(2)-游戏玩法-背包系统

    GamePlay 这篇开始来讲这个教程代码实现的游戏玩法逻辑 背包系统 效果 先看这个背包的UI 效果图 基本单元item 每个物体都有一个基本单元 Item namespace RPGM.Gamep ...

  2. 【Creator Kit - RPG 代码分析】(1)-核心框架、单例、定时事件

    Creator Kit - RPG 简介 Unity 官方的几个教程代码之一,适合入门学习. 实现了多个模块,本系列就逐步学习一下这个项目的源码. Core 核心模块,主要是实现一些框架层功能,这里主 ...

  3. python拼图游戏代码的理解_有意思的JS(1)拼图游戏 玩法介绍及其代码实现

    我是你们的索儿呀,很幸运我的文章能与你相见,愿萌新能直观的感受到Javascript的趣味性,愿有一定基础者有所收获,愿大佬不吝赐教 拼图游戏是一张图片分为若干块,打乱次序,将其中一块变为空白块,其只 ...

  4. 拼图游戏 玩法介绍及其代码实现(有意思的JS 一)

    我是你们的索儿呀,很幸运我的文章能与你相见 愿萌新能直观的感受到Javascript的趣味性,愿有一定基础者有所收获,愿大佬不吝赐教 拼图游戏是一张图片分为若干块,打乱次序,将其中一块变为空白块,其只 ...

  5. 50个新的游戏玩法机制

    50个新的游戏玩法机制  Hero86 https://www.jianshu.com/p/c244075cac63 游戏玩法创新 我这里所说的游戏设置是指游戏提供给玩家的挑战,以及玩家为了应对这些挑 ...

  6. ar ebs 销售订单关闭_本周大新闻|《哈利波特》AR游戏玩法公开,谷歌关闭VR影视部门...

    本周,AR方面,专利显示苹果可能会推出AR/VR混合设计的头显:Vuzix将与Verizon合作,通过运营商渠道分销:Niantic<哈利波特:巫师联盟>AR游戏玩法公开,比<精灵宝 ...

  7. 我的世界有宠物系统的服务器,我的世界:养宠物,玩科技,全新的游戏玩法等你来...

    原标题:我的世界:养宠物,玩科技,全新的游戏玩法等你来 Mc我的世界包含了很多玩法,正因为有这些不同的玩法,才能包容这么多性格的玩家.确实,mc的玩法很自由,而且有很多种方式,可以自己玩,可以邀请朋友 ...

  8. 在Unity中构建Pong克隆:UI和游戏玩法

    In Part 1 of this tutorial – the Retro Revolution: Building a Pong Clone in Unity – we created a Pon ...

  9. 一款科幻题材基地建设策略游戏——太空避难所中文版 附游戏玩法

    space haven中文称之为太空避难所,是由Bugbyte Ltd. 制作并发行一款以太空科幻为背景的模拟经营游戏,让玩家能够到科幻风格的基地建设的内容以及紧张刺激的战斗.在游戏中,玩家将率领一群 ...

最新文章

  1. linux Mysql 安装
  2. R语言ggplot2可视化:在可视化结果图的四个角落(左上、左下、右上、右下)添加标签实战
  3. HDU 5119 Happy Matt Friends(DP || 高斯消元)
  4. 2019年“计算法学”夏令营即日起接收报名申请
  5. 10.22 tcpdump:监听网络流量
  6. 在集设把优秀的设计合集,轻松追寻设计灵感
  7. c# 将doc转换为docx
  8. oracle生成存储过程示例,oracle创建简单存储过程示例
  9. PowerDesigner工具箱palette关了如何重新打开
  10. 鼠标onfocus或onblur效果
  11. 线性代数及其应用(第三版)1.4节习题解答
  12. pta c语言编程答案,PTA 程序设计 单选题-期末复习
  13. 写给非网工的CCNA教程(4)聊聊ping命令后的原理(续)
  14. python3 荣誉证书(奖状)批量打印
  15. 双硬盘双win10互不干扰_笔者详解win10系统双硬盘经常提示“盘符交错”的技巧...
  16. Verilog0.2:跑通第一个Vivado工程
  17. DOM 树的解析渲染
  18. UVA 11384 Help is needed for Dexter (递归函数)
  19. 如何理解卷积神经网络中的通道(channel)
  20. 移动端车架号vin码识别SDK

热门文章

  1. Linux权限:权限的概念及管理、粘滞位
  2. C++ boost协程技术介绍
  3. Monkey Patching in Go
  4. Cesium获取模型(Primitive)的位置与方向
  5. 中软国际有限公司c语言笔试,【求助】中软国际C++程序员笔试题
  6. windows下的广告病毒
  7. 北交计算机文化基础在线作业答案,19秋北交《计算机文化基础》在线作业二【满分答案】...
  8. BlackBerry 10 BlackBerry OS 7 1 手机通过蓝牙串口读取Arduino 蓝牙传过来的温度
  9. 王者荣耀在该服务器上未获取角色信息,王者荣耀:游戏载入的界面信息,你知道吗?...
  10. GEE:绘制累积降雨量折线图、降雨量均值折线图