Unity C# Mathf.Abs()取绝对值性能测试

之前有人提到过取绝对值时 直接写三目运算符比用Mathf.Abs()效率高 没觉得能高太多 今天测了一下 真是不测不知道 一测吓一跳 直接写三目运算符比Mathf.Abs()效率高2-3倍 这性能差距有点不太合理啊! 看下源码发现 很多Mathf的方法就是多封装了一层Math里的方法 把double型转成float型了 即便很简单得方法也没有重新实现 官方有点偷懒了 所以性能差距才会这么大 以后要求性能高的地方要注意 老老实实写一遍 能提升不少性能 ABS效率对比 测试代码: using UnityEngine;using UnityEditor;using System.Diagnostics;/// <summary>/// 执行时间测试/// ZhangYu 2019-04-04/// </summary>public class TimeTest : MonoBehaviour { public int executeTimes = 1; private static Stopwatch watch; private void OnValidate() { times = executeTimes; } private static int times = 1; [MenuItem("CONTEXT/TimeTest/执行")] private static void Execute() { watch = new Stopwatch(); // 数据 float a = 1; // Mathf.Abs watch.Reset(); watch.Start(); for (int i = 0; i < times; i++) { a = Mathf.Abs(a); } watch.Stop(); string msgMathfAbs = string.Format("Mathf.Abs: {0}s", watch.Elapsed); // 自己实现Abs watch.Reset(); watch.Start(); for (int i = 0; i < times; i++) { a = MyAbs(a); } watch.Stop(); string msgMyAbs = string.Format("自定义Abs: {0}s", watch.Elapsed); // 三目运算符Abs watch.Reset(); watch.Start(); for (int i = 0; i < times; i++) { a = a < 0 ? -a : a; } watch.Stop(); string msg3Abs = string.Format("三目运算符Abs: {0}s", watch.Elapsed); print(msgMathfAbs); print(msgMyAbs); print(msg3Abs); } // == 执行次数:10000000 // Mathf.Abs // (1)0.2803558s // (2)0.2837749s // (3)0.2831089s // (4)0.2829929s // (5)0.2839846s // 自定义Abs // (1)0.2162217s // (2)0.2103635s // (3)0.2103390s // (4)0.2092863s // (5)0.2097648s private static float MyAbs(float a) { return a < 0 ? -a : a; } // 三目运算符Abs // (1)0.0893028s // (2)0.1000181s // (3)0.1017959s // (4)0.1001749s // (5)0.1005737s} Mathf.Abs()源码: ...

April 4, 2019 · 2 min · jiezi

属于 Unity 的 Flutter——UIWidgets

介绍UIWidgets 是 Unity 的一个插件包,是一个从 Google 的移动 UI 框架 Flutter 演变过来的 UI 框架。 相对于原生开发的高开发成本(不同平台都需要不同的一套代码),Flutter、React-Native 等这种跨平台 UI 框架应运而生。 Flutter 自 2018 年 3 月发布以来,社区不断壮大。由于 Flutter 自身设计理念的出色,Unity 中国已经着手将其移植过来。当然了,也因为这两个东西都非常的年轻,因此开发的时候都像开荒一样。框架图Flutter 有自己的一套渲染系统,那么 Unity 作为一个游戏引擎,底层的图形 API 用自己的一套东西就行了,因此移植过来更方便了。Flutter 框架结构UIWidgets 框架结构执行效率这里提一些基础的知识:Batch 就是 DrawCall 的另一种说法,了解渲染流水线的同学会知道流水线在 CPU 与 GPU 之间通信时,一般有三个步骤:把数据加载到显存中。设置渲染状态。调用 Draw CallDraw Call 就是一个调用命令,让 CPU 告诉 GPU 要怎么样用给定的渲染状态和输入的顶点信息来计算。Batch 里面装着顶点信息,也就是 DrawCall 中 GPU 需要的顶点信息。 DrawCall 可以在 Profiler 中看,Batches 可以在 Stats 窗口看,大家可以仔细看看上面动图(右键在新标签页打开图片)里面的数据变化。 在我随便写的一个例子中间,可以看到 Batches 数只有 1 。即使在有动画的时候 Batches 会多一点,但动画停止后 DrawCall 和 Batches 都马上下来了。这也有我这个应用写的太简单的原因,但是这种效率还是非常值得期待的。组件树学过前端的同学应该熟悉组件树,这里就不介绍了。 为了更高的渲染效率,Unity 采用了 Render Object Compositiing 的技术。 如果一个子树没有发生改变,Unity 就会将其渲染到一个离屏的 Render Texture 上缓存下来,需要的时候再将其贴到屏幕上。 相比之下,以前的做法是,Canvas 只要有 UI element 改动了,整个 Canvas 都需要重新绘制。即使也有一种优化做法是准备两个 Canvas 分别绘制动态 UI element 和 静态 UI element,但这样也存在很多手动管理的地方。 另外一方面,你可能也意识到了,我们不需要再管什么用同一个材质等等来优化图的合批,UIWidgets 会自动来管理这些事情。这方面也跟 FairyGUI 非常像,开发者能专注在生产效率上,让插件来管理麻烦的事情。优点能开发游戏以外的 APP游戏中的 UI新的用户体验不用管渲染过程,提升效率因为是 Unity 的插件,可以轻松加各种粒子效果和其他骚操作。一套代码能跑在游戏中、APP 中、网页中和 Unity 的 Editor 窗口中。(开发者还用其做了一个 Unity 中文文档的网站…一套代码能用在网页上和 APP 端,不过还在开发中)和 Flutter 的 API 几乎一样,可以参考 Flutter 教程来用 UIWidgets 搭应用。缺点无论是 Flutter 还是 UIWidgets 都还很年轻,有很多组件 UIWidgets 还没移植过来(GridView、Circle Avatar 等等)官方示例、文档还没完善开发时是开荒模式,所以可能忍不住直接转用 Flutter 去了…我的示例这里借用了 ミライ小町 的模型,所以代码仓库大小会比较大。(项目里面还有ミライ小町的跳舞动画 animation!)项目仓库:Latias94/UIWidget-Practice UIWidgets:UnityTech/UIWidgets 官方讲解录播:[官方直播] UIWidgets - 不止游戏!如何使用Unity开发跨平台应用 ...

April 2, 2019 · 1 min · jiezi

Unity C# 3D世界坐标转2D屏幕坐标

让2D UI跟随3D物体移动或指示3D物体的位置该怎么做呢?关键代码:Camera.main.WorldToScreenPoint(target.position) + new Vector3(-Screen.width / 2, -Screen.height / 2);测试脚本:Position3DTo2DTest.csusing UnityEngine;/// <summary>/// 3D物体转2D屏幕坐标测试/// ZhangYu 2019-03-20/// </summary>public class Position3DTo2DTest : MonoBehaviour { public Transform target; // 3D目标 public Transform ui; // 2D UI private Vector3 originOff; // 当前UI系统(0,0)点 相对于屏幕左下角(0, 0)点的偏移量 private void Start () { originOff = new Vector3(-Screen.width / 2, -Screen.height / 2); Reposition(); } private void Update () { // 需要性能优化 仅在物体移动或相机移动后调用即可 Reposition(); } // 根据目标物体 重定位UI private void Reposition() { Vector3 position = Camera.main.WorldToScreenPoint(target.position) + originOff; position.z = 0; ui.localPosition = position; }}实现效果:血条UI悬浮在人物头顶当摄像机旋转角度时 依然有效 ...

March 20, 2019 · 1 min · jiezi

如何实现手游中的账户系统|游戏后端

作者:崔毅然在这篇文章中,我们使用 LeanCloud 作为后端来实现游戏内的账户系统。这篇文章以 Unity 游戏引擎中的 C# 语言为示例,主要讲解如何实现几种主流的登录方式,包括游客登录、游客账号升级、手机号验证码登录、用户名密码注册及登录。接入 SDK首先要接入 LeanCloud 的 SDK,接入方式可以参考文档。游客登录为了让玩家尽快体验游戏,每一个游戏都会有游客登录的功能。游客登录在 LeanCloud 中可以这样来实现:var user = await AVUser.LogInAnonymouslyAsync();调用上述代码成功后,LeanCloud 会自动生成一个游客用户登录,进入「控制台」 - 「存储」-「_User」表就可以看到表中新增了一条数据。在客户端登录的游客信息会一直被 SDK 存在本地,直到玩家删除游戏或主动退出登录。但就像所有游戏中的游客登录一样,当该游客退出登录后会丢失自己全部的游戏数据,为了保存游戏数据,需要将游客账号升级为正式账号。游客账号升级为了不丢失玩家的数据,我们会在游戏内建议玩家升级账号为正式玩家。例如绑定微信登录、绑定用户名密码及手机号,绑定成功后玩家就能以正式的登录方式获取到自己的游戏数据。升级为微信登录假设我们已经通过某些方法(例如使用 ShareSDK)拿到了微信的 openId、access_token、unionId 等,可以这样在 LeanCloud 中将游客账号关联到微信登录中:var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “openid”, “OPENID” },// openId 是用户在当前微信应用下的唯一 Id}; // unionId 是用户在整个微信内的唯一 Idvar unionId = “ox7NLs06ZGfdxbLiI0e0F1po78qE”; AVUserAuthDataLogInOption options = new AVUserAuthDataLogInOption{ UnionIdPlatform = “weixin”,// 这里指定用微信平台 AsMainAccount = true}; var user = AVUser.CurrentUser;// 绑定微信登录,第二个参数 weixinapp1是自定义的当前微信应用的标识await user.AssociateAuthDataAndUnionIdAsync(authData, “weixinapp1”, unionId, options);关联成功后,玩家以后就可以用微信登录了,登录代码见下文的第三方账户登录。绑定用户名、密码及手机号var currentUser = AVUser.CurrentUser;currentUser.Username = “username”;currentUser.Password = “password”;user.MobilePhoneNumber = “186xxxxxxxx”;await currentUser.SaveAsync();如果保存了手机号,保存成功后 LeanCloud 会自动向该手机号发送一条验证码,用户输入验证码后验证手机号:await AVUser.VerifyMobilePhoneAsync(“6位数字验证码”);手机号码验证成功后,该玩家以后就能以手机号登录了,这样就保证了游戏数据不会丢失。手机号+验证码登录、用户名及密码登录的代码见下文。手机号 + 验证码登录这种登录方式下,如果 _User 表中没有这个手机号,则视为新用户,会自动注册账号并登录;如果 _User 表中某个用户已经有了这个手机号(例如曾使用过该手机号登录,或通过游客账号升级绑定的信息),则直接登录。首先,调用发送登录验证码的接口:await AVCloud.RequestSMSCodeAsync(“18611111111”);然后使用验证码来登录var user = await AVUser.SignUpOrLogInByMobilePhoneAsync(“18611111111”, “6位短信验证码”);用户名 + 密码注册登录这种是最常见的登录方式,稍微有一点麻烦的是,需要玩家记住自己的用户名和密码。注册如果 _User 表中没有相应的用户名密码信息,例如从未注册过,也没有通过游客升级的方式增加用户名密码,需要先注册。var user = new AVUser();user.Username = “Tom”;user.Password = “cat!@#123”;await user.SignUpAsync();Debug.Log(user.Username);登录var user = await AVUser.LogInAsync(“username”, “password”);Debug.Log(user.Username);第三方登录微信或 QQ 登录可以让玩家更便捷的登录游戏。利用 LeanCloud 第三方登录的模块就可以完成这种场景。微信登录假设现在开发者已经通过某些方法(例如使用 ShareSDK)拿到了微信的 openId、access_token、unionId 等,无需注册就可以在 LeanCloud 中直接登录。如果游客已经升级绑定了微信信息,也可以通过这种方式来登录。var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “openid”, “OPENID” },// openId 是用户在当前微信应用下的唯一 Id};// unionId 是用户在整个微信内的唯一 Id var unionId = “ox7NLs06ZGfdxbLiI0e0F1po78qE”; AVUserAuthDataLogInOption options = new AVUserAuthDataLogInOption{ UnionIdPlatform = “weixin”,// 这里指定用微信平台 AsMainAccount = true}; // 绑定微信登录,第二个参数 weixinapp1 是自定义的当前微信应用的标识var user = await AVUser.LogInWithAuthDataAndUnionIdAsync(authData, “weixinapp1”, unionId, options);在 LogInWithAuthDataAndUnionIdAsync 这个方法中,第二个参数是自己定义的微信应用的名字,第三个参数 unionId 是用户在多个微信应用之间互通的唯一 id。如果我们有多个微信应用,就可以通过 unionId 登录来实现多个微信应用之间的账号互通。其他平台如果是其他平台,例如 facebook 是没有 unionId 的,这个时候只需要 access_token、expires_in、uid 三个自定义字段就可以了。var authData = new Dictionary<string, object> { { “access_token”, “ACCESS_TOKEN” }, { “expires_in”, 7200 }, { “uid”, “FACEBOOK_UID” },};var user = await AVUser.LogInWithAuthDataAsync(authData, “facebook”);由于 LeanCloud 默认只支持微信、QQ、新浪微博登录,因此对 Facebook 需要额外去设置一下唯一索引,设置唯一索引的方式非常简单,只需要进入控制台,在 _User 表中选择「其他」-「索引」,将 authData.facebook.uid 建立唯一索引,并且勾选上「允许缺失值」选项,这样 Facebook 登录也完成了。 ...

March 7, 2019 · 2 min · jiezi

Unity C# 自定义事件系统

在Unity中 用C#实现自定义的事件系统用法:EventUtil.AddListener(“事件名称”, 回调方法) // 添加事件监听器EventUtil.RemoveListener(“事件名称”, 回调方法) // 移除事件监听器EventUtil.DispatchEvent(“事件名称”, 不定长参数…) // 派发事件实现效果:事件工具EventUtil.csnamespace Pers.ZY.Events { /// <summary> /// 事件工具 /// <para>ZhangYu 2019-03-04</para> /// </summary> public static class EventUtil { /// <summary> 事件派发器 </summary> private static EventDispatcher dispatcher = new EventDispatcher(); /// <summary> 添加事件监听器 </summary> /// <param name=“eventType”>事件类型</param> /// <param name=“eventHandler”>事件处理器</param> public static void AddListener(string eventType, EventListener.EventHandler eventHandler) { dispatcher.AddListener(eventType, eventHandler); } /// <summary> 移除事件监听器 </summary> /// <param name=“eventType”>事件类型</param> /// <param name=“eventHandler”>事件处理器</param> public static void RemoveListener(string eventType, EventListener.EventHandler eventHandler) { dispatcher.RemoveListener(eventType, eventHandler); } /// <summary> 是否已经拥有该类型的事件 </summary> /// <param name=“eventType”>事件类型</param> public static bool HasListener(string eventType) { return dispatcher.HasListener(eventType); } /// <summary> 派发事件 </summary> /// <param name=“eventType”>事件类型</param> public static void DispatchEvent(string eventType, params object[] args) { dispatcher.DispatchEvent(eventType, args); } /// <summary> 清理所有事件监听器 </summary> public static void Clear() { dispatcher.Clear(); } }}事件派发器EventDispatcher.csusing System.Collections.Generic;namespace Pers.ZY.Events { /// <summary> /// 事件派发器 /// <para>ZhangYu 2019-03-05</para> /// </summary> public class EventDispatcher { /// <summary> 事件Map </summary> private Dictionary<string, EventListener> dic = new Dictionary<string, EventListener>(); /// <summary> 添加事件监听器 </summary> /// <param name=“eventType”>事件类型</param> /// <param name=“eventHandler”>事件处理器</param> public void AddListener(string eventType, EventListener.EventHandler eventHandler) { EventListener invoker; if (!dic.TryGetValue(eventType, out invoker)) { invoker = new EventListener(); dic.Add(eventType, invoker); } invoker.eventHandler += eventHandler; } /// <summary> 移除事件监听器 </summary> /// <param name=“eventType”>事件类型</param> /// <param name=“eventHandler”>事件处理器</param> public void RemoveListener(string eventType, EventListener.EventHandler eventHandler) { EventListener invoker; if (dic.TryGetValue(eventType, out invoker)) invoker.eventHandler -= eventHandler; } /// <summary> 是否已经拥有该类型的事件 </summary> /// <param name=“eventType”>事件类型</param> public bool HasListener(string eventType) { return dic.ContainsKey(eventType); } /// <summary> 派发事件 </summary> /// <param name=“eventType”>事件类型</param> public void DispatchEvent(string eventType, params object[] args) { EventListener invoker; if (dic.TryGetValue(eventType, out invoker)) { EventArgs evt; if (args == null || args.Length == 0) { evt = new EventArgs(eventType); } else { evt = new EventArgs(eventType, args); } invoker.Invoke(evt); } } /// <summary> 清理所有事件监听器 </summary> public void Clear() { foreach (EventListener value in dic.Values) { value.Clear(); } dic.Clear(); } }}事件监听器EventListener.csnamespace Pers.ZY.Events { /// <summary> /// 事件监听器 /// <para>ZhangYu 2019-03-05</para> /// </summary> public class EventListener { /// <summary> 事件处理器委托 </summary> public delegate void EventHandler(EventArgs eventArgs); /// <summary> 事件处理器集合 </summary> public EventHandler eventHandler; /// <summary> 调用所有添加的事件 </summary> public void Invoke(EventArgs eventArgs) { if (eventHandler != null) eventHandler.Invoke(eventArgs); } /// <summary> 清理所有事件委托 </summary> public void Clear() { eventHandler = null; } }}事件数据EventArgs.csnamespace Pers.ZY.Events { /// <summary> 事件参数 /// <para>ZhangYu 2019-03-05</para> /// </summary> public class EventArgs { /// <summary> 事件类型 </summary> public readonly string type; /// <summary> 事件参数 </summary> public readonly object[] args; public EventArgs(string type) { this.type = type; } public EventArgs(string type, params object[] args) { this.type = type; this.args = args; } }}事件派发测试using UnityEngine;using Pers.ZY.Events;/// <summary> 派发事件测试 </summary>public class EventDispatchTest : MonoBehaviour { public void Call() { EventUtil.DispatchEvent(EventHandleTest.ON_CLICK); EventUtil.DispatchEvent(EventHandleTest.ON_CLICK2, “参数1”); } }事件接收测试using UnityEngine;using Pers.ZY.Events;/// <summary> 处理事件测试 </summary>public class EventHandleTest : MonoBehaviour { // 定义事件名称 public const string ON_CLICK = “ON_CLICK”; // 定义事件名称 public const string ON_CLICK2 = “ON_CLICK2”; private void Start () { // 添加监听器 if (!EventUtil.HasListener(ON_CLICK)) EventUtil.AddListener(ON_CLICK, OnClick); if (!EventUtil.HasListener(ON_CLICK2)) EventUtil.AddListener(ON_CLICK2, OnClick2); } // 处理点击事件 public void OnClick(EventArgs evt) { print(evt.type); print(evt.args); } // 带参数的点击事件 public void OnClick2(EventArgs evt) { print(evt.type); print(evt.args[0]); } // 移除监听器 private void OnDestroy() { EventUtil.RemoveListener(ON_CLICK, OnClick); EventUtil.RemoveListener(ON_CLICK2, OnClick2); }} ...

March 5, 2019 · 3 min · jiezi

Unity C# 计算导弹抛物线弹道和转向

在三维空间中,利用抛物线公式计算弹道,得到一个发射初速度,让导弹打击到指定地点效果:脚本使用:只需指定目标点即可可以通过Hight调整导弹的飞行高度可以通过Gravity调整导弹的飞行速度通过以下两个脚本实现。工具脚本计算弹道,Missile脚本每帧更新导弹位置PhysicsUtil.csusing UnityEngine;/// <summary> 物理计算工具/// <para>ZhangYu 2018-05-10</para>/// </summary>public static class PhysicsUtil { /**findInitialVelocity * Finds the initial velocity of a projectile given the initial positions and some offsets * @param Vector3 startPosition - the starting position of the projectile * @param Vector3 finalPosition - the position that we want to hit * @param float maxHeightOffset (default=0.6f) - the amount we want to add to the height for short range shots. We need enough clearance so the * ball will be able to get over the rim before dropping into the target position * @param float rangeOffset (default=0.11f) - the amount to add to the range to increase the chances that the ball will go through the rim * @return Vector3 - the initial velocity of the ball to make it hit the target under the current gravity force. * * Vector3 tt = findInitialVelocity (gameObject.transform.position, target.transform.position); Rigidbody rigidbody = gameObject.GetComponent<Rigidbody> (); Debug.Log (tt); rigidbody.AddForce(tt*rigidbody.mass,ForceMode.Impulse); */ public static Vector3 GetParabolaInitVelocity(Vector3 from, Vector3 to, float gravity = 9.8f, float heightOff = 0.0f, float rangeOff = 0.11f) { // get our return value ready. Default to (0f, 0f, 0f) Vector3 newVel = new Vector3(); // Find the direction vector without the y-component /// /找到未经y分量的方向矢量// Vector3 direction = new Vector3(to.x, 0f, to.z) - new Vector3(from.x, 0f, from.z); // Find the distance between the two points (without the y-component) //发现这两个点之间的距离(不y分量)// float range = direction.magnitude; // Add a little bit to the range so that the ball is aiming at hitting the back of the rim. // Back of the rim shots have a better chance of going in. // This accounts for any rounding errors that might make a shot miss (when we don’t want it to). range += rangeOff; // Find unit direction of motion without the y component Vector3 unitDirection = direction.normalized; // Find the max height // Start at a reasonable height above the hoop, so short range shots will have enough clearance to go in the basket // without hitting the front of the rim on the way up or down. float maxYPos = to.y + heightOff; // check if the range is far enough away where the shot may have flattened out enough to hit the front of the rim // if it has, switch the height to match a 45 degree launch angle //if (range / 2f > maxYPos) // maxYPos = range / 2f; if (maxYPos < from.y) maxYPos = from.y; // find the initial velocity in y direction /// /发现在y方向上的初始速度// float ft; ft = -2.0f * gravity * (maxYPos - from.y); if (ft < 0) ft = 0f; newVel.y = Mathf.Sqrt(ft); // find the total time by adding up the parts of the trajectory // time to reach the max //发现的总时间加起来的轨迹的各部分// //时间达到最大// ft = -2.0f * (maxYPos - from.y) / gravity; if (ft < 0) ft = 0f; float timeToMax = Mathf.Sqrt(ft); // time to return to y-target //时间返回到y轴的目标// ft = -2.0f * (maxYPos - to.y) / gravity; if (ft < 0) ft = 0f; float timeToTargetY = Mathf.Sqrt(ft); // add them up to find the total flight time //把它们加起来找到的总飞行时间// float totalFlightTime; totalFlightTime = timeToMax + timeToTargetY; // find the magnitude of the initial velocity in the xz direction /// /查找的初始速度的大小在xz方向// float horizontalVelocityMagnitude = range / totalFlightTime; // use the unit direction to find the x and z components of initial velocity //使用该单元的方向寻找初始速度的x和z分量// newVel.x = horizontalVelocityMagnitude * unitDirection.x; newVel.z = horizontalVelocityMagnitude * unitDirection.z; return newVel; } /// <summary> 计算抛物线物体在下一帧的位置 </summary> /// <param name=“position”>初始位置</param> /// <param name=“velocity”>移动速度</param> /// <param name=“gravity”>重力加速度</param> /// <param name=“time”>飞行时间</param> /// <returns></returns> public static Vector3 GetParabolaNextPosition(Vector3 position, Vector3 velocity, float gravity, float time) { velocity.y += gravity * time; return position + velocity * time; }}Missile.csusing UnityEngine;/// <summary>/// 抛物线导弹/// <para>计算弹道和转向</para>/// <para>ZhangYu 2019-02-27</para>/// </summary>public class Missile : MonoBehaviour { public Transform target; // 目标 public float hight = 16f; // 抛物线高度 public float gravity = -9.8f; // 重力加速度 private Vector3 position; // 我的位置 private Vector3 dest; // 目标位置 private Vector3 velocity; // 运动速度 private float time = 0; // 运动时间 private void Start() { dest = target.position; position = transform.position; velocity = PhysicsUtil.GetParabolaInitVelocity(position, dest, gravity, hight, 0); transform.LookAt(PhysicsUtil.GetParabolaNextPosition(position, velocity, gravity, Time.deltaTime)); } private void Update() { // 计算位移 float deltaTime = Time.deltaTime; position = PhysicsUtil.GetParabolaNextPosition(position, velocity, gravity, deltaTime); transform.position = position; time += deltaTime; velocity.y += gravity * deltaTime; // 计算转向 transform.LookAt(PhysicsUtil.GetParabolaNextPosition(position, velocity, gravity, deltaTime)); // 简单模拟一下碰撞检测 if (position.y <= dest.y) enabled = false; }} ...

February 28, 2019 · 4 min · jiezi

Unity C# 打包AssetBundle与场景

Unity2018已经把打包过程简化很多了我们只需要关心两个API:1.BuildPipline.BuildAssetBundles() 打包AssetBundle2.BuildPipline.BuildPlayer() 打包场景1.打包AssetBundle先在资源的Inspector面板最下方 填写资源所属的AssetBundle名称和后缀(后缀可以不填)再利用BuildPipeline.BuildAssetBundles()进行打包2.打包Scene利用BuildPipeline.BuildPlayer()进行打包为方便使用 先把要打包的场景放入指定的文件夹 通过脚本批量打包3.脚本批量打包4.打包完毕5.加载测试6.打包和测试脚本AssetBundleBuilder.csusing UnityEngine;using UnityEditor;using System.IO;/// <summary>/// 资源包打包工具/// <para>打包AssetBundle和场景(Unity 2018.2.20)</para>/// <para>ZhangYu 2019-02-26</para>/// </summary>public class AssetBundleBuilder{ [MenuItem(“打包/Windows/资源包和场景”)] public static void BuildAbsAndScenesWindows() { BuildAbsAndScenes(BuildTarget.StandaloneWindows); } [MenuItem(“打包/Android/资源包和场景”)] public static void BuildAbsAndScenesAndroid() { BuildAbsAndScenes(BuildTarget.Android); } [MenuItem(“打包/IOS/资源包和场景”)] public static void BuildAbsAndScenesIOS() { BuildAbsAndScenes(BuildTarget.iOS); } [MenuItem(“打包/Windows/资源包”)] public static void BuildAbsWindows() { BuildAssetBundles(BuildTarget.StandaloneWindows); } [MenuItem(“打包/Android/资源包”)] public static void BuildAbsAndroid() { BuildAssetBundles(BuildTarget.Android); } [MenuItem(“打包/IOS/资源包”)] public static void BuildAbsIOS() { BuildAssetBundles(BuildTarget.iOS); } [MenuItem(“打包/Windows/场景”)] public static void BuildScenesWindows() { BuildScenes(BuildTarget.StandaloneWindows); } [MenuItem(“打包/Android/场景”)] public static void BuildScenesAndroid() { BuildScenes(BuildTarget.Android); } [MenuItem(“打包/IOS/场景”)] public static void BuildScenesIOS() { BuildScenes(BuildTarget.iOS); } // 打包AssetBundle和Scenes public static void BuildAbsAndScenes(BuildTarget platform) { BuildAssetBundles(platform); BuildScenes(platform); } // 打包AssetBundles private static void BuildAssetBundles(BuildTarget platform) { // 输出路径 string outPath = Application.streamingAssetsPath + “/Abs”; if (!Directory.Exists(outPath)) Directory.CreateDirectory(outPath); EditorUtility.DisplayProgressBar(“信息”, “打包资源包”, 0f); BuildPipeline.BuildAssetBundles(outPath, BuildAssetBundleOptions.DeterministicAssetBundle, platform); AssetDatabase.Refresh(); Debug.Log(“所有资源包打包完毕”); } // 打包Scenes private static void BuildScenes(BuildTarget platform) { // 指定场景文件夹和输出路径 string scenePath = Application.dataPath + “/AbResources/Scenes”; string outPath = Application.streamingAssetsPath + “/Abs/”; if (Directory.Exists(scenePath)) { // 创建输出文件夹 if (!Directory.Exists(outPath)) Directory.CreateDirectory(outPath); // 查找指定目录下的场景文件 string[] scenes = GetAllFiles(scenePath, “.unity”); for (int i = 0; i < scenes.Length; i++) { string url = scenes[i].Replace("\", “/”); int index = url.LastIndexOf("/"); string scene = url.Substring(index + 1, url.Length - index - 1); string msg = string.Format(“打包场景{0}”, scene); EditorUtility.DisplayProgressBar(“信息”, msg, 0f); scene = scene.Replace(".unity", “.scene”); Debug.Log(string.Format(“打包场景{0}到{1}”, url, outPath + scene)); BuildPipeline.BuildPlayer(scenes, outPath + scene, BuildTarget.StandaloneWindows, BuildOptions.BuildAdditionalStreamedScenes); AssetDatabase.Refresh(); } EditorUtility.ClearProgressBar(); Debug.Log(“所有场景打包完毕”); } } /// <summary> 获取文件夹和子文件夹下所有指定类型文件 </summary> private static string[] GetAllFiles(string directory, params string[] types) { if (!Directory.Exists(directory)) return new string[0]; string searchTypes = (types == null || types.Length == 0) ? “.*” : string.Join("|", types); string[] names = Directory.GetFiles(directory, searchTypes, SearchOption.AllDirectories); return names; }}LoadTest.csusing UnityEngine;using UnityEngine.SceneManagement;public class LoadTest : MonoBehaviour { private void Start () { LoadAB(); LoadScene(); } // 加载资源包 private void LoadAB() { // 资源包路径 string path = Application.streamingAssetsPath + “/Abs/test.ab”; // WWW下载 Http http = gameObject.AddComponent<Http>(); http.get(path, OnLoadABComplete); } // 加载场景 private void LoadScene() { // 资源包路径 string path = Application.streamingAssetsPath + “/Abs/Test.scene”; // WWW下载 Http http = gameObject.AddComponent<Http>(); http.get(path, OnLoadSceneComplete); } // 加载AssetBundle完毕 private void OnLoadABComplete(WWW www) { // 实例化预制 AssetBundle ab = www.assetBundle; Object prefab = ab.LoadAsset(“Test”); GameObject instance = (GameObject)Instantiate(prefab); DontDestroyOnLoad(instance); } // 加载场景完毕 private void OnLoadSceneComplete(WWW www) { // 必须写www.assetBundle这句 这样场景才能被读取到 AssetBundle ab = www.assetBundle; SceneManager.LoadScene(“Test”); }} ...

February 27, 2019 · 2 min · jiezi

Unity 开源双端框架 ET 中初尝热更新技术

ET 框架简介正所谓时势造英雄,在 Web 开发领域或者传统软件开发领域中,人们把经过千锤百炼的代码总结出一套开发框架,从而提高开发效率,让开发者能更专注于业务本身。对于游戏领域而言,不同游戏需求的东西也不一样:有的游戏对性能有着苛刻要求,有的游戏需要快速地迭代出来,有的游戏需要联网热更新等等。因此不同的游戏框架应运而生。例如:Game Framework 是一个基于 Unity 引擎的游戏框架,主要对游戏开发过程中常用模块进行了封装,很大程度地规范开发过程、加快开发速度并保证产品质量。QFramework 一套渐进式的快速开发框架。框架内部积累了多个项目的在各个技术方向的解决方案。Entitas 一套基于 C# 和 Unity 的实体组件系统。Entities Unity 官方的实体组件系统实现,不过还是 Beta 版本,详细介绍可以查看官网。StrangeIoC 一套基于 C# 和 Unity 的控制反转 (Inversion-of-Control) 框架。今天介绍的是 ET 框架。ET是一个开源的游戏客户端(基于unity3d)服务端双端框架,服务端是使用C# .net core开发的分布式游戏服务端,其特点是开发效率高,性能强,双端共享逻辑代码,客户端服务端热更机制完善,同时支持可靠udp tcp websocket协议,支持服务端3D recast寻路等等ET 框架能让我们只用 C# 就能搞定前后端,热更新方面也采用了基于 C# 的 IL 运行时——ILRuntime, 贯彻了 “珍爱生命,远离 Lua” 这句话。目前自己接触的大多是客户端部分,因此服务器方面不做介绍。框架文件结构ET 官网 本身给了很多介绍,我们可以克隆 Git 仓库到本地。下面来看看每个文件夹的作用:客户端文件结构本文主要来介绍客户端,因此进入到 Unity 文件夹,文件夹结构如下:当前 Master 分支目前需要 Unity 2018.3 以上版本。使用之前需要参考下官方的 运行指南。在 VS 中重新编译,或者 Rider Rebuild 一下项目。Scene 选择 ScenesInit.unity,点 Play 按钮应该就能成功运行,看到登陆界面。组件设计ET 框架使用了组件的设计,一切都是实体(Entity)和组件(Component),官方文档 组件设计 介绍的很详细。看完文档,我们来看看项目代码的启动入口。这个 Init.cs 文件,在 Model 文件夹下。可能有同学注意到 Hotfix 文件夹下也有一个 Init.cs 文件,而且这两个文件夹的结构大同小异,两边都有一些相同的文件,而它们只是命名空间不一样。这是因为我们用到 ILRuntime,而 ILRuntime 最好不要跨域继承。Model/Init.cs 文件中private async ETVoid StartAsync(){ try { … // 添加了组件,就赋予了各种功能。 // 例如加了 Timer 组件,就有了计时功能 Game.Scene.AddComponent<TimerComponent>(); … // 下载热更用的 AssetBundle 包 await BundleHelper.DownloadBundle(); // 加载热更用的dll等文件,调用 Hotfix/Init.cs Game.Hotfix.LoadHotfixAssembly(); // 加载配置 Game.Scene.GetComponent<ResourcesComponent>().LoadBundle(“config.unity3d”); Game.Scene.AddComponent<ConfigComponent>(); // 加载后卸载相应的 AB 包 Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle(“config.unity3d”); Game.Scene.AddComponent<OpcodeTypeComponent>(); Game.Scene.AddComponent<MessageDispatcherComponent>(); Game.Hotfix.GotoHotfix(); Game.EventSystem.Run(EventIdType.TestHotfixSubscribMonoEvent, “TestHotfixSubscribMonoEvent”); } …}通过组件设计,可以轻易地加载组件和卸载组件,例如我可以写一个心跳包组件来每隔30秒发送一个心跳包到服务器,当我需要这个组件的时候,可以直接 AddComponent,不需要的时候可以 RemoveComponent 移除组件。登陆界面的热更新启动过程接下来看到 Hotfix/Init.cs 文件中public static void Start(){ try { // 注册热更层回调 ETModel.Game.Hotfix.Update = () => { Update(); }; ETModel.Game.Hotfix.LateUpdate = () => { LateUpdate(); }; ETModel.Game.Hotfix.OnApplicationQuit = () => { OnApplicationQuit(); }; … // 加载热更配置 ETModel.Game.Scene.GetComponent<ResourcesComponent>().LoadBundle(“config.unity3d”); Game.Scene.AddComponent<ConfigComponent>(); ETModel.Game.Scene.GetComponent<ResourcesComponent>().UnloadBundle(“config.unity3d”); UnitConfig unitConfig = (UnitConfig)Game.Scene.GetComponent<ConfigComponent>().Get(typeof(UnitConfig), 1001); Log.Debug($“config {JsonHelper.ToJson(unitConfig)}”); // 发送事件来启动界面 Game.EventSystem.Run(EventIdType.InitSceneStart); } …}来看看发送的事件,代码在Hotfix\Module\Demo\UI\UILogin\System\InitSceneStart_CreateLoginUI.csnamespace ETHotfix{ // 用 Attribute 来注册事件 [Event(EventIdType.InitSceneStart)] public class InitSceneStart_CreateLoginUI: AEvent { public override void Run() { UI ui = UILoginFactory.Create(); // 这里就是启动登陆界面的地方,界面可以直接 add 或者 remove Game.Scene.GetComponent<UIComponent>().Add(ui); } }}再来看看一个界面是怎么生成的,代码在 Hotfix\Module\Demo\UI\UILogin\System\UILoginFactory.cspublic static UI Create(){ … ResourcesComponent resourcesComponent = ETModel.Game.Scene.GetComponent<ResourcesComponent>(); // 让资源组件读取登陆界面的 AB 包 resourcesComponent.LoadBundle(UIType.UILogin.StringToAB()); // 从 AB 包拿到登陆界面的 GameObject GameObject bundleGameObject = (GameObject) resourcesComponent.GetAsset(UIType.UILogin.StringToAB(), UIType.UILogin); GameObject gameObject = UnityEngine.Object.Instantiate(bundleGameObject); UI ui = ComponentFactory.Create<UI, string, GameObject>(UIType.UILogin, gameObject, false); // 添加登陆界面组件 ui.AddComponent<UILoginComponent>(); return ui; …}来看看登陆界面组件,代码在 Hotfix\Module\Demo\UI\UILogin\Component\UILoginComponent.cspublic class UILoginComponent: Component{ private GameObject account; private GameObject loginBtn; public void Awake() { // 通过引用来获取 UI 组件,再为其添加点击事件 ReferenceCollector rc = this.GetParent<UI>().GameObject.GetComponent<ReferenceCollector>(); loginBtn = rc.Get<GameObject>(“LoginBtn”); loginBtn.GetComponent<Button>().onClick.Add(OnLogin); this.account = rc.Get<GameObject>(“Account”); } public void OnLogin() { // 有兴趣可以再进去看看 OnLoginAsync,其中登陆的 Session 连接了服务器地址 127.0.0.1:10002 LoginHelper.OnLoginAsync(this.account.GetComponent<InputField>().text).Coroutine(); }}服务器地址存在了 Tools 菜单中的全局配置,上面的资源路径则是热更新服务器的地址。游戏运行后,在 Hierarchy 界面中也可以看到组件的结构,其中 uilogin.unity3d 就是登陆界面的 AB 包引用:这就是通过热更新逻辑生成的界面,也就是说,上面的代码让我们可以通过热更新来给应用加载各种界面和改写页面跳转逻辑,当然还可以通过热更来增加修改游戏逻辑和功能。如果不喜欢这种页面加载方式,可以考虑不使用 Hotfix/Init.cs 中的 Game.Scene.AddComponent<UIComponent>(); 这个 UIComponent,而使用其他 UI 组件,例如主仓库中的 FairyGUI 分支,让 FairyGUI 来单独负责 UI 界面。这里也可以看出基于组件的框架的灵活性,我以后也会出文章单独介绍 FairyGUI。热更新切换首先看看作者的介绍:7.客户端热更新一键切换因为ios的限制,之前unity热更新一般使用lua,导致unity3d开发人员要写两种代码,麻烦的要死。之后幸好出了ILRuntime库,利用ILRuntime库,unity3d可以利用C#语言加载热更新dll进行热更新。ILRuntime一个缺陷就是开发时候不支持VS debug,这有点不爽。ET框架使用了一个预编译指令ILRuntime,可以无缝切换。平常开发的时候不使用ILRuntime,而是使用Assembly.Load加载热更新动态库,这样可以方便用VS单步调试。在发布的时候,定义预编译指令ILRuntime就可以无缝切换成使用ILRuntime加载热更新动态库。这样开发起来及其方便,再也不用使用狗屎lua了8.客户端全热更新客户端可以实现所有逻辑热更新,包括协议,config,ui等等预编译指令指的就是在 Player Setting 中,上图右下角箭头指着的地方。当前有两个预编译指令,通常在开发中,可以只填写 NET452,这样可以得到完整的堆栈信息来调试程序。还有一个预编译指令 ASYNC,加上后,应用就会从前面填写的热更新服务器下载热更包,该指令在后文会提到。在国内环境下,手机游戏热更新的需求较强烈。市场上手机系统普遍分成 Android 和 iOS 阵营,其中 iOS 不支持 JIT 热更,因此 ET 框架给了两种选择:ILRuntime 热更新和 Mono 热更新。两者概念可以参考文末的参考链接,在这里不多说。体验热更新体验热更新之前,先把项目切到 Android 平台。按照下图配置 Mono 热更新:确保 Scripting Backend 为 Mono,下面预编译宏去掉 ILRuntime,加上 ASYNC,按下回车键执行变更。ASYNC 说明我们现在的热更新资源从资源服务器中获取,这里的热更新资源包括 Res 文件夹、Bundles 文件夹、Hotfix 文件夹中的代码等。在这个例子中,登陆界面的代码就已经写在热更新文件夹中了,我们将尝试通过热更新来展示登陆界面。点击 Play 按钮,会有两个报错:第二个 Log 信息展示了应用想要获取资源的热更资源服务器地址,这个地址可以在 Tools 菜单的全局配置中找到。报错信息提示找不到终端主机。报错是理所当然的,因为我们还没有启动本地服务器。首先要生成热更新文件,在 Tools 菜单中点击打包工具,如下图所示:平台选择当前的 Android 平台,目前不需要打包应用,所以无视第一个单选按钮。前面的思维导图提到了 ET 根目录的 Release 文件夹存的就是热更新资源文件。打包工具也会把打包后的资源放在 Release 文件夹下。而第二个按钮指的是是否把打包的热更新资源也放在应用中,目前也不需要选择。开启热更新后,应用会比较服务器和本地应用的 Version 文件,计算文件差异后才会下载相关的热更新资源文件。如果勾选了第二个按钮,打包工具将会把资源也复制到 Assets/StreamingAssets 文件夹下,同时更新 Version 文件,这样我们将不能测试下载热更包的过程。点击开始打包后,热更文件就生成了:再点击 Tools 菜单中的 web 资源服务器开启映射了 Release 文件夹的本地文件服务器。点击 Play 按钮,应用通过下载热更新资源,生成了登陆界面,也把热更资源下载到了应用中,也就是 Assets/StreamingAssets 文件夹。重新启动 web 资源服务器清除 log 信息,再次运行应用,会发现没有再次下载热更新资源。因为对比了 Version 文件后,应用本地的文件已经不需要更新了。至此,我们完成了一次完整的热更新。总结ET 框架给了我们一种统一的开发体验,提供了方便的热更新切换和调试方案,这足以支撑起一些小游戏的开发需求,有需要的同学可以了解下 ET 框架~2019 年立了个 Flag:周更技术博客,欢迎督促和交流,也欢迎常来我博客 萤火之森 逛!参考一些新潮的Unity热更方案Mono和IL2CPP选哪个更合适?Unity实现c#热更新方案探究(一)关于热更新,大家现在都是怎么实现的? 中网友 gx 的回答 ...

January 9, 2019 · 2 min · jiezi