【Creator Kit - RPG 代码分析】(4)-游戏玩法-对话框、云朵系统、帧序列动画控制器、动画状态回调、音乐控制
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;}
这个对话控制器,使用了两个组件,SpriteButton
和DialogLayout
,前者是自定义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)-游戏玩法-对话框、云朵系统、帧序列动画控制器、动画状态回调、音乐控制相关推荐
- 【Creator Kit - RPG 代码分析】(2)-游戏玩法-背包系统
GamePlay 这篇开始来讲这个教程代码实现的游戏玩法逻辑 背包系统 效果 先看这个背包的UI 效果图 基本单元item 每个物体都有一个基本单元 Item namespace RPGM.Gamep ...
- 【Creator Kit - RPG 代码分析】(1)-核心框架、单例、定时事件
Creator Kit - RPG 简介 Unity 官方的几个教程代码之一,适合入门学习. 实现了多个模块,本系列就逐步学习一下这个项目的源码. Core 核心模块,主要是实现一些框架层功能,这里主 ...
- python拼图游戏代码的理解_有意思的JS(1)拼图游戏 玩法介绍及其代码实现
我是你们的索儿呀,很幸运我的文章能与你相见,愿萌新能直观的感受到Javascript的趣味性,愿有一定基础者有所收获,愿大佬不吝赐教 拼图游戏是一张图片分为若干块,打乱次序,将其中一块变为空白块,其只 ...
- 拼图游戏 玩法介绍及其代码实现(有意思的JS 一)
我是你们的索儿呀,很幸运我的文章能与你相见 愿萌新能直观的感受到Javascript的趣味性,愿有一定基础者有所收获,愿大佬不吝赐教 拼图游戏是一张图片分为若干块,打乱次序,将其中一块变为空白块,其只 ...
- 50个新的游戏玩法机制
50个新的游戏玩法机制 Hero86 https://www.jianshu.com/p/c244075cac63 游戏玩法创新 我这里所说的游戏设置是指游戏提供给玩家的挑战,以及玩家为了应对这些挑 ...
- ar ebs 销售订单关闭_本周大新闻|《哈利波特》AR游戏玩法公开,谷歌关闭VR影视部门...
本周,AR方面,专利显示苹果可能会推出AR/VR混合设计的头显:Vuzix将与Verizon合作,通过运营商渠道分销:Niantic<哈利波特:巫师联盟>AR游戏玩法公开,比<精灵宝 ...
- 我的世界有宠物系统的服务器,我的世界:养宠物,玩科技,全新的游戏玩法等你来...
原标题:我的世界:养宠物,玩科技,全新的游戏玩法等你来 Mc我的世界包含了很多玩法,正因为有这些不同的玩法,才能包容这么多性格的玩家.确实,mc的玩法很自由,而且有很多种方式,可以自己玩,可以邀请朋友 ...
- 在Unity中构建Pong克隆:UI和游戏玩法
In Part 1 of this tutorial – the Retro Revolution: Building a Pong Clone in Unity – we created a Pon ...
- 一款科幻题材基地建设策略游戏——太空避难所中文版 附游戏玩法
space haven中文称之为太空避难所,是由Bugbyte Ltd. 制作并发行一款以太空科幻为背景的模拟经营游戏,让玩家能够到科幻风格的基地建设的内容以及紧张刺激的战斗.在游戏中,玩家将率领一群 ...
最新文章
- linux Mysql 安装
- R语言ggplot2可视化:在可视化结果图的四个角落(左上、左下、右上、右下)添加标签实战
- HDU 5119 Happy Matt Friends(DP || 高斯消元)
- 2019年“计算法学”夏令营即日起接收报名申请
- 10.22 tcpdump:监听网络流量
- 在集设把优秀的设计合集,轻松追寻设计灵感
- c# 将doc转换为docx
- oracle生成存储过程示例,oracle创建简单存储过程示例
- PowerDesigner工具箱palette关了如何重新打开
- 鼠标onfocus或onblur效果
- 线性代数及其应用(第三版)1.4节习题解答
- pta c语言编程答案,PTA 程序设计 单选题-期末复习
- 写给非网工的CCNA教程(4)聊聊ping命令后的原理(续)
- python3 荣誉证书(奖状)批量打印
- 双硬盘双win10互不干扰_笔者详解win10系统双硬盘经常提示“盘符交错”的技巧...
- Verilog0.2:跑通第一个Vivado工程
- DOM 树的解析渲染
- UVA 11384 Help is needed for Dexter (递归函数)
- 如何理解卷积神经网络中的通道(channel)
- 移动端车架号vin码识别SDK
热门文章
- Linux权限:权限的概念及管理、粘滞位
- C++ boost协程技术介绍
- Monkey Patching in Go
- Cesium获取模型(Primitive)的位置与方向
- 中软国际有限公司c语言笔试,【求助】中软国际C++程序员笔试题
- windows下的广告病毒
- 北交计算机文化基础在线作业答案,19秋北交《计算机文化基础》在线作业二【满分答案】...
- BlackBerry 10 BlackBerry OS 7 1 手机通过蓝牙串口读取Arduino 蓝牙传过来的温度
- 王者荣耀在该服务器上未获取角色信息,王者荣耀:游戏载入的界面信息,你知道吗?...
- GEE:绘制累积降雨量折线图、降雨量均值折线图