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

InstantOC + LOD

原理 剔除遮挡+Lod,可以组合使用 相机每帧主动发射射线击中物体,击中后显示,并且重置周期 物体在周期后,主动向相机发射射线,判断是否需要隐藏 相机后的物体。减少DC(CPU压力),减少GPU剔除消耗;被遮挡的物体减少GPU渲染 IOCCam 每帧根据算法生成的随机点,向视锥体内发射若干条射线,击中以后,显示 并根据距离通过LODGroup切换高低模 参数:每帧发射的射线 不足:射线过多 改进:减少射线,延长周期(物体不主动发起射线) IOClod 初始化,根据相机距离和包围盒,初始化一个合适的Lod 剔除方法:Render enable设置为false或者修改阴影投射(一定距离内保持阴影,防止穿帮) 原版方案:周期从击中点向相机发射射线,被其他阻挡,则剔除 不足:随机的击中点,可能在侧面,或者后面,会自己把自己剔除掉 改进:击中点取2s中最近的击中点,作为击中点 LOD 运行时根据机型信息,设置IOClod的lod距离 cullDis,过远的直接隐藏,方法和上面一致 减少GPU需要处理的顶点数 使用 生成场景时给需要剔除的组件添加IOClod组件 指标 点面数 峰值点面数 优点 剔除不需要渲染的物体 根据距离切换LOD模型,甚至剔除 参考 IOC介绍

2022-11-12 · Dand