人物移动模块

一、概述 游戏中角色移动分为了主动和被动,主动主要由玩家通过摇杆或者键盘进行操作,被动移动主要应用于自动寻路,通过寻路点行动。 二、主动移动 移动组件 移动组件主要用到Unity的移动组件Character Controller,主要属性如下 Height角色的高度,通常和现实中的人物一样设置为2米左右。 Radius角色的半径,用于控制人物的胖瘦。 Center设置角色中心点的位置。 Slope Limit限制角色能爬的最大坡度。通常设置为90度以下,这样角色就不会走到墙上。 Step Offset移动步长。通常2米左右的人移动步长设置在0.1到0.4米. Skin Width皮肤厚度。如果这个值太小角色容易被卡住,太大角色容易抖动。通常将这个数据设为0.01到角色半径的10%之间。 Min Move Distance最小移动距离。官方推荐把这个值设为0。 isGrounded可以获取角色当前是否在地面。 velocity可以获取角色当前的速度向量。 移动方法主要用到Move,Move方法需要自己实现重力的效果,看具体项目需求 //用摇杆控制方向,当按下空格键时跳起。 using UnityEngine; using System.Collections; public class ExampleClass : MonoBehaviour { public float speed = 6.0F; public float jumpSpeed = 8.0F; public float gravity = 20.0F; private Vector3 moveDirection = Vector3.zero; void Update() { CharacterController controller = GetComponent<CharacterController>(); if (controller.isGrounded) { moveDirection = GetTargetPos(nowPos) - nowPos; // 跳跃 if (Input.GetButton("Jump")) moveDirection.y = jumpSpeed; } //Move方法需要自己写重力效果 moveDirection.y -= gravity * Time.deltaTime; //移动控制器 controller.Move(moveDirection * Time.deltaTime); } } 每帧目标点的计算 速度模拟模拟现实中人物移动的特征,起步时加速,计算代码如下 Vector3 GetTargetPos(Vector3 pos) { //计算实时速度,addSpeed为加速度 curMoveSpeed = Mathf.MoveTowards(curMoveSpeed, MaxSpeed, addSpeed * deltaTime); //计算终点,curMoveDir由输入决定 pos += curMoveSpeed * curMoveDir * deltaTime } 动画状态机 移动时需要动作配合,动作主要由Animator组件实现,通过设置不同的参数,实现状态之间的转换,还可以细化,加上准备跳跃,跳跃落地等等。 通过SetTrigger进行不同运动状态的切换,通过SetBool进行移动和静止状态间的切换。 管理组件 主动移动的管理组件主要功能如下 接收输入方向 状态机维护当前角色状态(普通,跳跃),设置对应动画状态 计算下一帧移动方向 三、自动寻路 自动寻路主要是计算出起始点到目标点的一连串的中间点,每次走一小段直线,从而完成寻路的过程,逻辑比较简单, 如果使用Unity自带的Nav Mesh Agent 可以使用参考https://bbs.huaweicloud.com/blogs/303788。 计算寻路点 计算寻路点主要用到Unity的NavMesh类中的CalculatePath方法,得到寻路点以后只要逐个进行寻路即可。 ...

2022-12-12 · Dand

UGUI对比FGUI

前言 UI是游戏中很重要的一部分,工作大多是繁复的,所以有一套功能强大,操作简单直观的UI解决方案是非常有必要的。 上半年一直在用FGUI,和之前用的UGUI相比下来,各有优缺点,对比分析记录一下。 一、介绍 UGUI UGUI是Unity原生提供的UI方案,提供了基本的UI组件,如图片,文本,按钮,复选框,进度条等等,正是因为UGUI是面向普遍开发,所以没有封装很多高级的组件,比如虚拟列表。 FGUI FairyGUI(下来简称FGUI)号称易上手,直观,零代码,有一个独立的编辑器,封装了很多高级的UI组件,用起来不需要自己造轮子。 二、编辑器使用体验 UGUI 优点:稳定。 缺点:因为集成在Unity中,资源管理不太方便,需要项目定好规范,编写工具提交效率。 FGUI 优点:有单独的编辑器,所有资源统一管理,美术策划也能很快上手使用。 缺点:不稳定,经常会莫名卡住,导出的时候经常报错。 三、基础组件 基础组件上面因为FGUI实现很多的高级功能,所以使用起来体验要么一样,要么好点。 文本 FGUI文本支持ubb语法,富文本支持图文混排,超链接等等,自动大小的文本。 列表 FGUI实现了虚拟列表,树列表。 字体 字体上FGUI提供了工具制作美术字,只要把资源拖进去专用的编辑器就能很快做出一个美术字体。 动效系统 两者都有动效系统,UGUI上叫Animation,功能上差不多,都很强大。 四、对齐系统 对齐系统这一块,FGUI是很大的亮点。 UGUI UGUI的锚点系统以中心点为基准,对父节点对齐,提供了上下左右,拉伸对齐方式。 **优点:**简单直观UI默认的中心点在中心,符合人的正常思维。 缺点:不够灵活;同级UI之前的关联对齐要依赖其他布局组件;缺乏针对同级元素的对齐工具,不过可以自己拓展实现,可以参考我的另一篇文章。 FGUI 优点:FGUI的对齐系统叫做关联系统,第一个优点是不局限与与父节点对齐,可以对任意两个元素进行对齐。第二个优点是在拓展了对齐的维度,可以以上下左右边缘为基准,可以以上下左右为基准拉伸,复杂一点,但是学会以后可以很方便实现一些效果,比如一张图片随着文字的变长而往右移动。 缺点:垂直布局和水平布局功能太单一;锚点在左上角各种反人类。 五、FGUI特色功能 资源引用查找工具 可以轻易找到组件(图片)和其他组件的引用关系。 支持多国语言 原生支持多国语言,可以导出xml维护即可。 控制器系统 控制器系统一般用于控制显示一些UI元素的状态,可以配合动效和Tab使用,UGUI一般要通过代码控制或者自己实现一个类似的系统。 多平台 FGUI可以一套UI,应用到不同的游戏引擎去,比如Unity和Unreal,对于后期可能更换引擎的项目,节省掉很多成本。 插入3D物体 支持在UI层中插入任何3D物体,例如模型、粒子、骨骼动画等,UGUI中要通过修改特效和UI的渲染顺序实现,比较麻烦。 六、拓展性 两者都有拓展性,FGUI的拓展性在于代码是开源的,可以在底层定制适合项目的功能,但是编辑器是不开源的,所以编辑器里面的东西改不了,虽然编辑器提供了脚本拓展支持,但能实现的东西不多。 UGUI的拓展性在于可以轻松地对编辑器进行拓展,也可以加上各种效果,一般也不会动到底层的如网格重构和渲染的代码,真想修改的话可能要和Unity进行合作然后项目搞一个魔改版的Unity。 七、总结 总的来说两种UI解决方案都可以商用,不会有特别大的问题。 要是策划美术拼UI的话,可以选择FGUI,相反要是程序负责的话,两者都可以,但个人感觉Unity上的操作更加熟悉一点,程序员也可以在平时的UI流程中,吸收一些FGUI的思想,做些工具提高UI的开发效率。

2022-11-18 · Dand

UGUI对齐工具

前言 上半年的大半年时间都在用FGUI,最近又用回了UGUI,发现FGUI的一些工具还是很高效的,比如对齐工具,想着说移植到UGUI里面去,上网搜了一圈还真有,原理并不复杂,就是进行一些数学计算出最终的位置。 不过也没有完全适合项目需求,针对原项目基础上添加了三个功能,下面介绍一下。 一、撤销功能 原项目的工具在修改后,习惯性就按下Ctrl+Z,发现并不能撤销原来的操作,查询了Unity的API后,发现有个Undo的类,可以实现撤销功能,有很多方法,主要用到了RecordObject,代码如下 private static void SetTranPos(Transform tran, Vector3 pos) { Undo.RecordObject(tran, "modify posstion"); tran.position = pos; } 二、Selection.GameObjects不是按选择顺序的 API中的Selection.GameObjects用于获取当前选中的物件,数组的顺序和我选择的顺序无关,所以不能实现灵活的对齐,查询网上文章可以用Selection.Objects替代,里面的第一个元素就是Ctrl选中的第一个元素,以此类推,代码如下 //按选中顺序获取GameObjects private static GameObject[] GetOrderedSelctionObjs() { return Selection.objects.OfType<GameObject>().ToArray(); } 三、添加对父节点对齐 功能就是和标题一样,原项目是没有的,代码如下 //如果只选中一个,对父节点对齐 if (rects.Count == 1) { if (rects[0].parent != null && rects[0].parent.GetComponent<RectTransform>() != null) { rects.Insert(0, rects[0].parent.GetComponent<RectTransform>()); } } 四、代码 UGUIAlign using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEngine; public enum AlignType { Top = 1, Left = 2, Right = 3, Bottom = 4, HorizontalCenter = 5, //水平居中 VerticalCenter = 6, //垂直居中 Horizontal = 7, //横向分布 Vertical = 8, //纵向分布 } public class UGUIAlign : Editor { [MenuItem("GameObject/UI/Align/Left 【左对齐】")] static void AlignLeft() { Align(AlignType.Left); } [MenuItem("GameObject/UI/Align/HorizontalCenter 【水平居中】")] static void AlignHorizontalCenter() { Align(AlignType.HorizontalCenter); } [MenuItem("GameObject/UI/Align/Right 【右对齐】")] static void AlignRight() { Align(AlignType.Right); } [MenuItem("GameObject/UI/Align/Top 【顶端对齐】")] static void AlignTop() { Align(AlignType.Top); } [MenuItem("GameObject/UI/Align/VerticalCenter 【垂直居中】")] static void AlignVerticalCenter() { Align(AlignType.VerticalCenter); } [MenuItem("GameObject/UI/Align/Bottom 【底端对齐】")] static void AlignBottom() { Align(AlignType.Bottom); } [MenuItem("GameObject/UI/Align/Horizontal 【横向分布】")] static void AlignHorizontal() { Align(AlignType.Horizontal); } [MenuItem("GameObject/UI/Align/Vertical 【纵向分布】")] static void AlignVertical() { Align(AlignType.Vertical); } public static void Align(AlignType type) { List<RectTransform> rects = new List<RectTransform>(); GameObject[] objects = GetOrderedSelctionObjs(); if (objects != null && objects.Length > 0) { for (int i = 0; i < objects.Length; i++) { RectTransform rect = objects[i].GetComponent<RectTransform>(); if (rect != null) rects.Add(rect); } } //如果只选中一个,对父节点对齐 if (rects.Count == 1) { if (rects[0].parent != null && rects[0].parent.GetComponent<RectTransform>() != null) { rects.Insert(0, rects[0].parent.GetComponent<RectTransform>()); } } if (rects.Count > 1) { Align(type, rects); } } //按选中顺序获取GameObjects private static GameObject[] GetOrderedSelctionObjs() { return Selection.objects.OfType<GameObject>().ToArray(); } public static void Align(AlignType type, List<RectTransform> rects) { RectTransform tenplate = rects[0]; float w = tenplate.sizeDelta.x * tenplate.lossyScale.x; float h = tenplate.sizeDelta.y * tenplate.lossyScale.y; float x = tenplate.position.x - tenplate.pivot.x * w; float y = tenplate.position.y - tenplate.pivot.y * h; switch (type) { case AlignType.Top: for (int i = 1; i < rects.Count; i++) { RectTransform trans = rects[i]; float th = trans.sizeDelta.y * trans.lossyScale.y; Vector3 pos = trans.position; pos.y = y + h - th + trans.pivot.y * th; SetTranPos(trans, pos); } break; case AlignType.Left: for (int i = 1; i < rects.Count; i++) { RectTransform trans = rects[i]; float tw = trans.sizeDelta.x * trans.lossyScale.x; Vector3 pos = trans.position; pos.x = x + tw * trans.pivot.x; SetTranPos(trans, pos); } break; case AlignType.Right: for (int i = 1; i < rects.Count; i++) { RectTransform trans = rects[i]; float tw = trans.sizeDelta.x * trans.lossyScale.x; Vector3 pos = trans.position; pos.x = x + w - tw + tw * trans.pivot.x; SetTranPos(trans, pos); } break; case AlignType.Bottom: for (int i = 1; i < rects.Count; i++) { RectTransform trans = rects[i]; float th = trans.sizeDelta.y * trans.lossyScale.y; Vector3 pos = trans.position; pos.y = y + th * trans.pivot.y; SetTranPos(trans, pos); } break; case AlignType.HorizontalCenter: for (int i = 1; i < rects.Count; i++) { RectTransform trans = rects[i]; float tw = trans.sizeDelta.x * trans.lossyScale.x; Vector3 pos = trans.position; pos.x = x + 0.5f * w - 0.5f * tw + tw * trans.pivot.x; SetTranPos(trans, pos); } break; case AlignType.VerticalCenter: for (int i = 1; i < rects.Count; i++) { RectTransform trans = rects[i]; float th = trans.sizeDelta.y * trans.lossyScale.y; Vector3 pos = trans.position; pos.y = y + 0.5f * h - 0.5f * th + th * trans.pivot.y; SetTranPos(trans, pos); } break; case AlignType.Horizontal: float minX = GetMinX(rects); float maxX = GetMaxX(rects); rects.Sort(SortListRectTransformByX); float distance = (maxX - minX) / (rects.Count - 1); for (int i = 1; i < rects.Count - 1; i++) { RectTransform trans = rects[i]; Vector3 pos = trans.position; pos.x = minX + i * distance; SetTranPos(trans, pos); } break; case AlignType.Vertical: float minY = GetMinY(rects); float maxY = GetMaxY(rects); rects.Sort(SortListRectTransformByY); float distanceY = (maxY - minY) / (rects.Count - 1); for (int i = 1; i < rects.Count - 1; i++) { RectTransform trans = rects[i]; Vector3 pos = trans.position; pos.y = minY + i * distanceY; SetTranPos(trans, pos); } break; } } private static void SetTranPos(Transform tran, Vector3 pos) { Undo.RecordObject(tran, "modify posstion"); tran.position = pos; } private static int SortListRectTransformByX(RectTransform r1, RectTransform r2) { float w = r1.sizeDelta.x * r1.lossyScale.x; float x1 = r1.position.x - r1.pivot.x * w; w = r2.sizeDelta.x * r2.lossyScale.x; float x2 = r2.position.x - r2.pivot.x * w; if (x1 >= x2) return 1; else return -1; } private static int SortListRectTransformByY(RectTransform r1, RectTransform r2) { float w = r1.sizeDelta.y * r1.lossyScale.y; float y1 = r1.position.y - r1.pivot.y * w; w = r2.sizeDelta.y * r2.lossyScale.y; float y2 = r2.position.y - r2.pivot.y * w; if (y1 >= y2) return 1; else return -1; } private static float GetMinX(List<RectTransform> rects) { if (null == rects || rects.Count == 0) return 0; RectTransform tenplate = rects[0]; float minx = tenplate.position.x; float tempX = 0; for (int i = 1; i < rects.Count; i++) { tempX = rects[i].position.x; if (tempX < minx) minx = tempX; } return minx; } private static float GetMaxX(List<RectTransform> rects) { if (null == rects || rects.Count == 0) return 0; RectTransform tenplate = rects[0]; float maxX = tenplate.position.x; float tempX = 0; for (int i = 1; i < rects.Count; i++) { tempX = rects[i].position.x; if (tempX > maxX) maxX = tempX; } return maxX; } private static float GetMinY(List<RectTransform> rects) { if (null == rects || rects.Count == 0) return 0; RectTransform tenplate = rects[0]; float minY = tenplate.position.y; float tempX = 0; for (int i = 1; i < rects.Count; i++) { tempX = rects[i].position.y; if (tempX < minY) minY = tempX; } return minY; } private static float GetMaxY(List<RectTransform> rects) { if (null == rects || rects.Count == 0) return 0; RectTransform tenplate = rects[0]; float maxY = tenplate.position.y; float tempX = 0; for (int i = 1; i < rects.Count; i++) { tempX = rects[i].position.y; if (tempX > maxY) maxY = tempX; } return maxY; } } 五、代码下载 https://github.com/dandkong/UGUIAlign ...

2022-11-16 · Dand

《游戏逻辑思想》学习笔记

前言 这本书忘记从哪里下载了,和别的技术书籍不太一样,大多的内容都比较贴合项目开发,所以有一定的参考价值。 里面有几个章节觉得有收获的,主要是框架的设计,以及一些解决问题的思路,大致做了笔记或者摘录。 一、基础内容交流 代码规范 可读代码:由于动态语言的类型灵活性,可以在变量前加上变量类型简称 正确使用断言与返回 注意什么时候可以为空,不要盲目返回 可拓展接口使用 多参数,下面演示了三种写法,最终应该在保留必要参数的情况下,可选参数做成table addModulePower(nModuleId, nPower); addModulePower(nModuleId,nPower,bSyncMsg,bSendEvent, bOnlyBoss) if( tOption && tOption.bSyncMsg){ //做这个参数该干的事情 } 调试的思维与逻辑 正向和逆向思维,逆向更快 不易复现的bug,埋下日志,下次使用 培养敏锐的异常反应 注意生命周期的创建与销毁 代码修改与重构 我们在项目中秉持一个原则,如果有个接口让你不舒服,比如说多传了几个参数,那么我们一定要提出来,那一定是接口的设计不够简单或者没有提供更简单的接口形式。 优雅的使用外部代码 我们的一个原则就是要尽可能少去直接和引擎进行交互,而是更多的进行局部缓存,把战场拉回到更加通用的逻辑里面。 修改后的代码为: let levelSlider = Core.createBitmapByName("slider_png"); let nSliderX = 0; if(XXX){ nSliderX += 6; } if(XXX){ nSliderX += 8; } if(XXX){ nSliderX -= 2; } levelSlider.x -= nSliderX; 选择简单的接口/参数以及尽量少的使用/调用底层接口就是我们所谓的正确的代码使用方式。 代码审查 取出一个管理器对象。然后直接访问了它的成员函数,这是非常不应该行为。第一个是这个成员不应该是公有的,而应该是私有的,它的公有性质破坏了类的封装。 从面试的角度看面试 基础能力,逻辑能力,硬核能力 如何应对代码错误 作为项目的主程,还需要统一思想。这个过程包括要求大家遵循统一的代码命名等,这也是规避错误的一个重要手段。越是相似的代码风格,代码的阅读速度就会越快。 二、逻辑设计模式式讨论 分层设计 事件的派发遵循从下往上:比如M驱动V 依赖性越强的越靠下层 变化的放上层,底层可以互相依赖,逻辑层不允许相互依赖 主动和被动 被动模式,依赖事件,及时,性能消耗少,当被动模式不在能支持复杂逻辑时,可以考虑主动模式 主动模式,依赖轮询,有点像ESC的系统层监听实体层的感觉了。优点在于可以监听多种条件 阻塞和非阻塞 主要用于资源加载,阻塞速度快 统一与非统一 效率没有打到一定程度的影响时,推荐考虑统一性 三、框架设计初步 基类 存活状态,唯一ID 框架代码结构 子类关注的基类接口一定是个空的实现,意味着子类不需要考虑去调用父类同名的接口。 框架设计 配置化编程:消除重复代码,如协议监听,按钮监听等等 自动化平衡处理:如果没有销毁,帮忙擦下屁股 机制有助于实现全局性的功能,尤其处理大规模需要重复代码的东西 框架拓展的思路 基础能力,封装类,继承到框架中 四、逻辑设计原理 缓存的设计 缓存存放的东西有限,不能所有的东西都放缓存。 缓存应该具备清理功能。 缓存系统应该具备一定的匹配能力。 缓存具备最小保留数量以及预先创建的能力 分线漫谈 分线,是将玩家划分到不同的频道中,不同频道的玩家互不可见,且不会互相同步消息。分线在程序方面主要用于减少网络包,在策划层面会有一些其他的应用。分线是基于场景的,我们的可见性,以及消息同步默认以场景为单位。 ...

2022-11-12 · Dand

捏体型方案

总览 捏脸主要是通过复制一份骨骼(称为编辑骨骼)的Transform值,带动蒙皮来实现体型的变化,主要技术点在实现方式,编辑器开发,骨骼数据的组织方式,数据的存取。 主要的类和方法 CommonCustomizeDNA 自定义部位可以改变的值,包括Transform的各个分量,共9个,加一个整体缩放 public XXX { scaleX, rotaionX ... } CommonCustomizeDNAConfig 自定义部位可以改变的值范围,包括Transform的各个分量,共9个,加一个整体缩放 public XXX { scaleXmin, scaleYmin, ... } BodyCustomizeDNA 预定义的可调节部分,可能作用于单根骨骼,也可以一个参数作用于多个骨骼,也可以改变整体缩放(单独处理) 还包括了自定义的DNA属性 //在z轴方向缩放,改骨骼scale值 [LabelOverride("胸腔前后","上身")] [Range(160,60)] public upperBodyFB; //偏移 [LabelOverride("胸部左右","上身")] [Range(160,60)] public bustOffsetLR; BodyCustomizableSlot 数据类,可编辑的最小单位,主要存数据,包括操作的细项属性,原骨骼,编辑的骨骼 BodyCustomizableController 定义每个槽位的信息,包括了名字,操作的骨骼名字,编辑模式(改变自身,改变自身和子对象,改变自身以及非BIP骨骼) 初始化做了 根据固定槽位列表,找到对应的bone 整合固定的槽位列表和自定义的槽位列表 根据上面的数据,按模式生成编辑骨骼,生成一份一模一样的骨骼(Transform信息也一致),放到原骨骼下面,如果编辑模式为修改自己以及子对象,则把原骨骼的子对象都放到编辑骨骼下面。生成一份新的骨骼放在原骨骼的子节点,编辑骨骼可以受到动画和自定义值的双重影响,其子节点能受到自定义值得影响。 把编辑骨骼替换原有骨骼整合到蒙皮中,mesh.bones,使其能够影响蒙皮 ApplyDna方法,主要把配置的值(BodyCustomizeDNA)赋值到可编辑骨骼中 根据BodyCustomizeDNA值对BodyCustomizableSlot的编辑骨骼进行三维度缩放(放大缩小效果) BodyCustomizeDNA的配置值直接对特定BodyCustomizableSlot的编辑骨骼进行旋转处理(偏移效果) 处理自定义槽位的编辑骨骼旋转缩放位置,以及镜像编辑骨骼的旋转缩放位置 LateUpdate中处理某些父子骨骼的位置关系(脖子和头),子骨骼跟着父编辑骨骼走,主要针对BIP骨骼会被anim控制,也希望能受到父编辑骨骼的影响。 体型变化实现原理 分三种操作类型 1. 整体缩放 改根节点缩放 2. 骨骼节点的各方向缩放 直接调scale的分量 3. 骨骼节点偏移 左右:直接改旋转值的Y值 localRotation = Quertnion.Euler(0,左右偏移值,0) 上下 localPosition = Quertnion.Euler(0,0,0)*Vetor3.up*上下偏移值 //好像就是等于y值。。 数据存取方式 JSON进行存储,打包时用prefab索引起来,读取时也用prefab,后续可以打成二进制,比较省 高维控制 实际上就是把多个BodyCustomizableSlot打包成一个集合,可以初始化值,后面再做一下统一的缩放。比如改胖瘦,会同时改动腰部,胸部,大腿手臂等节点。 捏脸实现 底层逻辑和捏体型一致,不同点 可以指定部位镜像处理,比如,改了左眼会同时改变右眼 一个部位有多个维度可以调整,代码预先设置好可以调整的细项,比如,对眼睛可以进行旋转平移拉伸共九个维度,加一个整体的缩放 体型可以调整的范围比较粗略,脸部每个部位的细项大多可以调整 几个问题 问:怎么避免受到动画控制器影响 ...

2022-11-12 · Dand