Unity-简易对象池

在Unity中我们经常会用到对象池,使用对象池无非就是解决两个问题: 一是减少 new 时候寻址造成的消耗,该消耗的原因是内存碎片。 二是减少 Object.Instantiate 时内部进行序列化和反序列化而造成的CPU消耗。 设计: 从字面上理解对象池,池的意思就是容器。我们可以从池中获取一个对象(一条鱼),也可以向池中放入一个对象(一条鱼)。获取的操作我们叫Allocate(分配),而放入一个对象我们叫Recycle(回收)。所以我们可以定义池的接口为如下: public interface IPool<T> { T Allocate(); bool Recycle(T obj); } 为什么要用泛型呢?如何实现一个精简并且灵活的对象池。这个灵活很大一部分是通过泛型体现的。 池是容器的意思,在C#中可以是List,Queue或者Stack甚至是数组。所以对象池本身要维护一个容器。本篇我们选取Stack来作为池容器,原因是当我们在Allocate和Recycle时并不关心缓存的存储的顺序,只要求缓存对象的地址是连续的。代码如下所示: using System.Collections.Generic; public abstract class Pool<T> : IPool<T> { ... protected Stack<T> mCacheStack = new Stack<T>(); ... } Pool是个抽象类,实现一个精简并且灵活的对象池。这个灵活很大一部分是通过抽象类体现的。 现在对象的存取和缓存接口都设计好了,那么这些对象是从哪里来的呢?我们分析下,创建对象我们知道有两种方式,反射构造方法和new一个对象。对象池的一个重要功能就是缓存,要想实现缓存就要求对象可以在对象池内部进行创建。所以我们要抽象出一个对象的工厂,代码如下所示: public interface IObjectFactory<T> { T Create(); } 为什么要用工厂? 实现一个精简并且灵活的对象池。这个灵活很大一部分是通过工厂体现的。 OK,现在对象的创建,存取,缓存的接口都设计好了。下面放出Pool的全部代码。 using System.Collections.Generic; public abstract class Pool<T> : IPool<T> { #region ICountObserverable /// <summary> /// Gets the current count. /// </summary> /// <value>The current count.</value> public int CurCount { get { return mCacheStack.Count; } } #endregion protected IObjectFactory<T> mFactory; protected Stack<T> mCacheStack = new Stack<T>(); /// <summary> /// default is 5 /// </summary> protected int mMaxCount = 5; public virtual T Allocate() { return mCacheStack.Count == 0 ? mFactory.Create() : mCacheStack.Pop(); } public abstract bool Recycle(T obj); } 对象池实现 首先要实现一个对象的创建器,代码如下所示: ...

March 1, 2022 · 2 min · LiuYingbo

Unity-射线

Unity射线系统 Demo展示 UI+Physical射线测试: FPS自定义射线测试: UGUI射线工具 实现功能,鼠标点击UI,返回鼠标点击的UI对象; 需要使用到鼠标点击事件-PointerEventData; 关键API:EventSystem.current.RaycastAll(); 参数为鼠标点击事件,和接受射线返回结果集合; public static GameObject RaycastUI() { if (EventSystem.current == null) return null; //鼠标点击事件 PointerEventData pointerEventData = new PointerEventData(EventSystem.current); //设置鼠标位置 pointerEventData.position = Input.mousePosition; //射线检测返回结果 List<RaycastResult> results = new List<RaycastResult>(); //检测UI EventSystem.current.RaycastAll(pointerEventData, results); //返回最上层ui if (results.Count > 0) return results[0].gameObject; else return null; } Physcial射线工具 从摄像机发射射线,方向为,摄像机——鼠标位置; 可以获取射线碰撞到的3D物品的大部分信息: 可以活着hit.collider;意味着可以获取碰撞点的位置,物体等信息; 用来做鼠标点击地面控制人物位移; public static GameObject RaycastPhysical() { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; bool isHit = Physics.Raycast((Ray) ray, out hit); if (isHit) { Debug.Log(hit.collider.name); return hit.collider.gameObject; //检测到碰撞,就把检测到的点记录下来 } return null; } 测试代码: public class Test : MonoBehaviour { void Update() { if (Input.GetMouseButtonUp(0)) { GameObject temp = RayCastTool.RaycastUI(); if (temp.CompareTag("Pic")) { temp.GetComponent<Image>().color = Color.red; } } if (Input.GetMouseButtonUp(1)) { GameObject temp = RayCastTool.RaycastPhysical(); temp.GetComponent<Renderer>().material.color = Color.red; } } } FPS射线测试 自定义射线的起始点Origin,方向,以及射线长度; ...

March 1, 2022 · 1 min · LiuYingbo

二叉树

二叉树 什么是二叉树 简单地理解,满足以下两个条件的树就是二叉树: 本身是有序树; 树中包含的各个节点的度不能超过 2,即只能是 0、1 或者 2; 二叉树性质 二叉树中,第 i 层最多有 2i-1 个结点。 如果二叉树的深度为 K,那么此二叉树最多有 2K-1 个结点。 二叉树中,终端结点数(叶子结点数)为 n0,度为 2 的结点数为 n2,则 n0=n2+1。 性质 3 的计算方法为:对于一个二叉树来说,除了度为 0 的叶子结点和度为 2 的结点,剩下的就是度为 1 的结点(设为 n1),那么总结点 n=n0+n1+n2。 同时,对于每一个结点来说都是由其父结点分支表示的,假设树中分枝数为 B,那么总结点数 n=B+1。而分枝数是可以通过 n1 和 n2 表示的,即 B=n1+2n2。所以,n 用另外一种方式表示为 n=n1+2n2+1。 两种方式得到的 n 值组成一个方程组,就可以得出 n0=n2+1。 满二叉树 如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。 满二叉树除了满足普通二叉树的性质,还具有以下性质: 满二叉树中第 i 层的节点数为 2n-1 个。 深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2k-1。 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。 具有 n 个节点的满二叉树的深度为 log2(n+1)。 完全二叉树 如果二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为完全二叉树。 ...

February 25, 2022 · 3 min · LiuYingbo

Unity-Asset资源

Unity资源文件夹介绍 编辑时 在Asset文件下存在Resources和SteamingAsset文件夹; Resources 只读不可修改,打包时直接写死,没有办法通过热更新替换资源; 可以存放任何格式的资源; 通过Resources.Load加载; 一般只存登录UI,congfig配置等不会修改的文件; //加载 GameObject go = Resources.Load<GameObject>(path); Sprite sp = Resouces.Load(path,typeof(sprite)) as sprite; TextAsset txt = Resources.Load(path) as TextAsset; SteamingAssets 存放打包后Unity打包后的资源,也就是我说的AssetBundle(ab包); 可读可写,打包后移动端不能使用File类读取(用过www读取streamingAssetspath数据拷贝到persistentDataPath); 安装后 Application.dataPath 包含游戏数据文件夹的路径,app程序包安装路径(不常用); 1.UnityEditor——默认Assets——可读可写 using StreamReader/StreamWriter/异步 2.Andriod——data/app/xxx.apk——不可读不可写 3.IOS——Application/…..app/Data——可读不可写(读没意义) Application.streamingAssetsPath 数据流换成目录,外部数据文件(二进制,AssetsBundle.csv、数据裸露不加密); 1.除了Andriod都可读可写,使用WWW或者System.IO都可以; 2.Andriod只读,只能用WWW读写——第三方压缩/解压库实现读写(待研究); 3.安装包资源目录,不可修改; Application.presistentDataPath 持久化存储目录,应用更新、覆盖安装都不会清除; 1.全平台可读可写——stream和file都可以读写; 2.热更新资源存放目录; Application.temporaryCachePath 临时换成目录,可读可写(只有ios常用); AssetBundle 资源压缩文件(ab包),非路径,一种压缩文件的格式; AssetBundle Asset Bundle: AssetBundle 是一个存档文件,包含可在运行时加载的特定于平台的资源(模型、纹理、预制件、音频剪辑甚至整个场景)。AssetBundle 可以表达彼此之间的依赖关系;例如 AssetBundle A 中的材质可以引用 AssetBundle B 中的纹理。为了通过网络进行有效传递,可以根据用例要求选用内置算法来压缩 AssetBundle(LZMA 和 LZ4)。 AssetBundle 可用于可下载内容(DLC),减小初始安装大小,加载针对最终用户平台优化的资源,以及减轻运行时内存压力。 AssetBundle 中有什么: “AssetBundle”可以指两种不同但相关的东西。 首先是磁盘上的实际文件。对于这种情况,我们称之为 AssetBundle 存档,在本文档中简称“存档”。存档可以被视为一个容器,就像文件夹一样,可以在其中包含其他文件。这些附加文件包含两种类型: 序列化文件和资源文件。序列化文件包含分解为各个对象并写入此单个文件的资源。资源文件只是为某些资源(纹理和音频)单独存储的二进制数据块,允许我们有效地在另一个线程上从磁盘加载它们。 其次是通过代码进行交互以便从特定存档加载资源的实际 AssetBundle 对象。此对象包含一个映射,即从已添加到此存档的资源的所有文件路径到按需加载的资源所包含的对象之间的映射。 为 AssetBundle 分配资源 要为 AssetBundle 分配指定资源,请按照下列步骤操作: ...

February 20, 2022 · 6 min · LiuYingbo

C#数据结构与算法

数据结构与算法 什么是数据结构与算法? 在学习数据结构与算法的时候,常常会思考一个问题:什么是数据结构与算法? 这里比较找到了一个比较好的答案。 从宏观来讲: 数据结构是一组数据的存储结构。 算法就是操作数据的方法。 数据结构为算法服务,算法要作用在特定的数据结构中。 从狭义上来讲,就是数据结构与算法这门学科就是去学习比较经典的数据结构和算法。 有点像学习设计模式一样,学习 23 种设计模式。 如何学习数据结构与算法? 说到设计模式,学习数据结构与算法与学习设计模式非常相似的。 我们知道设计模式有 23 种,而一般一个开发者学习设计模式的时候,往往会对 23 个设计模式都从头到尾学一遍,但是学完之后发现除了记了个名字之外没啥卵用,就连名字也不一定记得住。 那么是以为 23 种设计模式的核心并不是设计模式本身,而是 23 种设计模式所遵循的 SOLID 原则,即六大设计原则,而开发者一开始最应该掌握的是六大设计原则,而不是设计模式,六大设计原则是核心,而 23 种设计模式算是 23 种对于六大设计原则的案例一样,一定要先搞清楚这一点。否则学习就会事倍功半,当然如果有一定的编码经验再去学设计模式更好一些。 学习数据结构与算法也是一样的,数据结构与算法的核心是时间复杂度和空间复杂度,就像对于设计模式中的六大设计原则一样,掌握了时间复杂度和空间复杂度的评估,那么剩下的经典的常用的数据结构和算法就是一个个案例而已,而不用死记硬背,当然有一定的编码经验再去学习会更好一些。 这就是数据结构与算法的学习思路。 为什么要学习数据结构与算法? 国内大厂的面试比较爱考,但是考得不是很难 Google、Facebook、微软等更爱考,而且非常难。 可以评估代码性能,写出高性能的代码 开发时选择合适的数据结构 比较有名的基础框架,都柔和了很多基础数据解耦股和算法的设计思想 阅读源码的时候可以减少阻碍(比如遇到一些算法的实现) 提高编程能力 思维提升 简历上可以写一个精通数据结构与算法 数据结构与算法是不是高智商开发者的专属? 当然不是,数据结构与算法的常用的知识点不多,而且也不需要高智商。重点还是熟能生巧。 数据结构与算法学科的解决问题重点是什么? 更省更快地存储和处理数据的问题 时间复杂度 时间复杂度的定义很简单: 算法的执行时间与数据规模之间的增长关系 时间复杂度的全称其实叫做:渐进时间复杂度,我们就叫时间复杂度即可。 时间复杂度的定义难得的清晰,不像一些设计模式这样的定义非常抽象。 OK,既然时间复杂度的定义这么简单,那么我们也顺便说一下空间复杂度吧,空间复杂度的定义如下: 算法的存储空间与数据规模之间的增长关系。 空间复杂度的定义也非常直白。 List 的遍历,代码如下: var list = new List<string>(){"a","b","c"}; foreach (var s in list) { Debug.Log(s); } 代码中的 list 的数据规模是 3,其实就是数据量,那么这一段代码所执行的时间就是, Debug.Log(s) 所执行的时间 x 3。 ...

February 10, 2022 · 6 min · LiuYingbo

Unity协程

yield yield 实质是一个语法糖,它让程序员能够更方便的去使用迭代器,通过 yield 你可以直接使用迭代器操作而不需要去实现 IEnumerable 和 IEnumerator,也不需要一个临时的 Collection 来完成迭代。 yield 有两种格式声明 yield return <expression>; yield break; using System.Collections; using UnityEngine; namespace UniRxLesson { public class YieldExample : MonoBehaviour { private void Start() { foreach (var empty in FiveTimes()) { Debug.Log("A"); } } private IEnumerable FiveTimes() { for (var i = 0; i < 5; i++) { yield return string.Empty; } } } } 输出结果为 A A A A A 每一次 foreach 的循环中都会调用迭代器方法,当 yield return 被执行到时,表达式的值会被返回,同时当前的函数的上下文信息被保存下来。下一次循环执行之时会重新从上一次停止的位置继续执行。你也可以使用 yield break 来终止迭代过程。 ...

February 9, 2022 · 3 min · LiuYingbo

Unity-编辑器扩展

一、菜单栏拓展 Menultem [Menultem("Test/CreatePanel/func1 %_Q"),true,1] 1、路径+快捷键,是否启用,排列顺序 2、配合Selection.Object选中物体文件夹时启用 3、方法必须是静态方法 [MenuItem("Test/OpenPanel %_Q",true)] public static bool IsSelected() { if (Selection.objects.Length > 0) return true; else return false; } [MenuItem("Test/OpenPanel %_Q",false)] public static void OpenPanel() { Undo.DestroyObjectImmediate(object); } 4、快捷键 符号 字符 % Ctrl # Shift & Alt LEFT/RIGHT/UP/DOWN 方向键 F1-F12 F功能键 _g 字母g 二、鼠标右键拓展 CONTEXT [MenuItem("CONTEXT/组件名/方法名(按钮名)")] 方法前加这句特性,可以在组件的鼠标右键菜单中添加相应的按钮方法,方法必须有参数MenuCommand [MenuItem("CONTEXT/Rigidbody/Clear")] static void ClearMassAndGravity(MenuCommand cmd) { Rigidbody rgd = cmd.context as Rigidbody; rgd.mass = 0.1f; rgd.useGravity = false; } ...

January 29, 2022 · 2 min · LiuYingbo

回溯算法

什么是回溯法 回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 在二叉树系列中,我们已经不止一次,提到了回溯,例如 二叉树:以为使用了递归,其实还隐藏着回溯。 回溯是递归的副产品,只要有递归就会有回溯。 回溯法的效率 回溯法的性能如何呢,这里要和大家说清楚了,「虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法」。 因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案」,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。 那么既然回溯法并不高效为什么还要用它呢? 因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。 回溯法解决的问题 回溯法,一般可以解决如下几种问题: 组合问题:N个数里面按一定规则找出k个数的集合 排列问题:N个数按一定规则全排列,有几种排列方式 切割问题:一个字符串按一定规则有几种切割方式 子集问题:一个N个数的集合里有多少符合条件的子集 棋盘问题:N皇后,解数独等等 组合是不强调元素顺序的,排列是强调元素顺序」。 例如:{1, 2} 和 {2, 1} 在组合上,就是一个集合,因为不强调顺序,而要是排列的话,{1, 2} 和 {2, 1} 就是两个集合了。 记住组合无序,排列有序,就可以了。 如何理解回溯法 所有回溯法的问题都可以抽象为树形结构! 因为回溯法解决的都是在集合中递归查找子集,集合的大小就构成了树的宽度,递归的深度,都构成的树的深度。 递归就要有终止条件,所以必然是一颗高度有限的树(N叉树)。 回溯法模板 回溯函数模板返回值以及参数 在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。 回溯算法中函数返回值一般为void。 再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。 但后面的回溯题目的讲解中,为了方便大家理解,我在一开始就帮大家把参数确定下来。 回溯函数伪代码如下: void backtracking(参数) 回溯函数终止条件 既然是树形结构,那么我们在讲解 二叉树的递归的时候,就知道遍历树形结构一定要有终止条件。 所以回溯也有要终止条件。 什么时候达到了终止条件,树中就可以看出,一般来说搜到叶子节点了,也就找到了满足条件的一条答案,把这个答案存放起来,并结束本层递归。 所以回溯函数终止条件伪代码如下: if (终止条件) { 存放结果; return; } 回溯搜索的遍历过程 回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。 如图: 注意图中,集合大小和孩子的数量是相等的! 回溯函数遍历过程伪代码如下: for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { 处理节点; backtracking(路径,选择列表); // 递归 回溯,撤销处理结果 } for循环就是遍历集合区间,可以理解一个节点有多少个孩子,这个for循环就执行多少次。 ...

January 25, 2022 · 1 min · LiuYingbo

IOC

简介 IOC简介 IOC全称为Invertuon Of Control,也就是控制反转。 控制反转是一种设计思想。而不是具体的技术。 IOC这种设计思想有很多方式实现,最常见的实现方式就是DI。 DI简介 DI的全称是Dependency Injection,也就是依赖注入,是IOC思想的一种实现。 依赖 什么是依赖?看下面这段代码: public class A { public B b = new B(); } 上述代码中就是A依赖B。 我再这里理解为,A对象里面持有B对象,如果B对象不存在,那么A就无法成立,所以是A对象依赖B。 注入 什么是注入呢?看下面这段代码: public class A { public B b = null; } public class B {} void Main() { var a = new A(); var b = new B(); a.b = b; } 从上面可以看出总共有两个对象,分别是a和b。可以看出现在的A对象是依赖B对象的。 **a.b=b;**这句代码就是将b对象注入到a对象的b成员中。 有时候可以把注入就理解成设置值。 依赖注入也就是吧某个对象依赖的对象进行赋值。 DI QF中的IOC QF是一种框架,是作者最近正在学习的一种框架,这个框架不要求会,我们就拿QF中的DI来进行详细的了解一下DI。 我们在使用DI方案的时候一般都离不开DI容器这个概念。育德时候DI容易也叫做IOC容器。就是:DIContainer 和 IOCContainer。 下面我们来看看QF中是如何使用IOC的。 using System.Collections; using System.Collections.Generic; using UnityEngine; namespace QF.Master.Example { public class ServiceA { public void Say() { Debug.Log("I am ServiceA:" + this.GetHashCode()); } } public class IOCExample : MonoBehaviour { // 声明为需要注入的对象 [Inject] public ServiceA A {get;set;} void Start () { // 创建实例容器 var container = new QFrameworkContainer(); // 注册类型 container.Register<ServiceA>(); // 注入对象(会自动查找 Inject Atrributet的对象) container.Inject(this); // 注入之后,就可以直接使用 A 对象了 A.Say(); } } } 上面是完整的案例,我们只要仔细看下面的代码: ...

January 15, 2022 · 4 min · LiuYingbo

动态规划

动态规划为什么重要? 从面试的角度看,动态规划是正规算法面试中无论如何都逃不掉的必考题。 其实最主要的原因就是动态规划非常适合面试,因为动态规划没办法「背」。 我们很多求职者其实是通过背题来面试的,而之前这个做法屡试不爽,什么翻转二叉树、翻转链表,快排、归并、冒泡一顿背,基本上也能在面试中浑水摸鱼过去,其实这哪是考算法能力、算法思维,这就是考谁的备战态度好,愿意花时间去背题而已,把连背都懒得背的筛出去就完事了。 但是随着互联网遇冷,人才供给进一步过热,背题的人越来越多,面试的门槛被增加了,因此这个时候需要一种非常考验算法思维、变化多端而且容易设计的题目类型,动态规划就完美符合这个要求。 比如 LeetCode 中有1261道算法类题目,其中动态规划题目占据了近200道,动态规划能占据总题目的 1/6 的比例,可见其火热程度。 更重要的是,动态规划的题目难度以中高难度为主 所以,既然我们已经知道这是算法面试的必考题了,我们怎么准备都不为过。 什么是动态规划 动态规划(Dynamic programming,简称DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。 态规划与分治方法类似,都是通过组合子问题的解来来求解原问题的。再来了解一下什么是分治方法,以及这两者之间的差别,分治方法将问题划分为互不相交的子问题,递归的求解子问题,再将它们的解组合起来,求出原问题的解。而动态规划与之相反,动态规划应用与子问题重叠的情况,即不同的子问题具有公共的子子问题(子问题的求解是递归进行的,将其划分为更小的子子问题)。在这种情况下,分治方法会做许多不必要的工作,他会反复求解那些公共子子问题。而动态规划对于每一个子子问题只求解一次,将其解保存在一个表格里面,从而无需每次求解一个子子问题时都重新计算,避免了不必要的计算工作。 动态规划题目的特点 从「钱」讲起 我们可以算一下,按照贪心算法的策略,我们先拿出3个最大面值的7,再拿出一个面值5然后就没有办法继续了。 这里就有问题了,贪心算法的弊端在这种特殊面值钱币面前展露无疑,原因就在于「只顾眼前,无大局观」,在先拿出最大的 7 面值的硬币后就彻底把周旋余地堵死了,因为剩下的 21 要想凑足付出的代价是非常高的,我们需要依次拿出4个面值为2的硬币。 改进计算策略 那么既然贪心算法已经不适用于这种场景了,我们应该如何改变计算策略呢? 当我们面试过程中遇到这种问题时,如果一时没有思路,也要想到一种万能算法–暴力破解。 我们分析一下上述题目,它的问题其实是「给定一组面额的硬币,我们用现有的币值凑出27最少需要多少个币」。 那么假设最后一个硬币为a[k]的话,那么剩下27 - a[k],这个时候问题又变成了,我们凑出 27 - a[k]最少需要多少个币 问题可以不断被分解为「我们用现有的币值凑出 n 最少需要多少个币」,比如我们用 f(n) 函数代表 「凑出 n 最少需要多少个币」. 把「原有的大问题逐渐分解成类似的但是规模更小的子问题」这就是最优子结构,我们可以通过自底向上的方式递归地从子问题的最优解逐步构造出整个问题的最优解。 这个时候我们分别假设 2、5、7 三种面值的币分别为最后一个硬币的情况: 最后一枚硬币的面额为 7: min = f(20) + 1 最后一枚硬币的面额为 5: min = f(22) + 1 最后一枚硬币的面额为 2: min = f(25) + 1 这个时候大家发现问题所在了吗?最少找零 min 与 f(20)、f(22)、f(25) 三个函数解中的最小值是有关的,毕竟后面的「+1」是大家都有的。 ...

January 15, 2022 · 2 min · LiuYingbo