0%

前言

因为自己本身有每天中午观看tiktok半小时需求,晚上偶尔也看半小时左右。前段时间换了新手机,忘记了整个流程,所以也是查阅搜索了相关博客、油管视频,现总结如下。声明:本人纯属个人使用探索,为记录自己需要内容或学习感悟,不涉及商业用途,如有侵权,请告知并会删除。

所需工具

一部iPhone,一个美区appleid,iTunes(老版本),QuantumultX(或是ShadowRocket等),爱思助手(或是iMazing用于安装ipa),Fiddler(软件抓包),版本号对应版本id查找工具。

下载对应版本TikTok

1.先安装老版本iTunes,在iTunes–智能刷机–其他工具,选择最后一个支持应用商店的iTunes下载并安装。登录自己的美区账号,找到tiktok

2.打开旧版应用下载工具,先找到对应的 软件即tiktok,然后右键查看历史版本,找到我们所需的17.8.1版本,得知版本id为838412699

3.安装并打开Fiddler,复制代码bpu MZBuy.woa 将其填入左下黑色输入框内并按enter。然后在iTunes里点击下载tiktok,这时fiddler软件就会有响应反应,找到含字母T的红色图标点击选中,选择Inspectors-TextView,此时就会看到当前版本tiktok对应的版本id。我们要做的就是把第二步查找的对应版本id在此时复制替换掉,然后点击Run to Completion按钮。这个时候就等待iTunes慢慢下载完成。

4.安装对应TikTok。在上述第三步下载完之后,在iTunes资料库里有下载完的ipa文件,可以右键选择的Windows资源管理器中显示。打开爱思助手,选择我的设备–应用游戏–导入安装,然后找到刚才下载完的ipa,此时便已安装好对应版本TikTok

5.避免地区限制。在QuantumultX软件里编辑配置,找打rewrite_local一栏,将如下代码复制粘贴

1
2
3
4
(?<=_region=)CN(?=&) url 307 JP
(?<=&mcc_mnc=)4 url 307 2
^(https?:\/\/(tnc|dm)[\w-]+\.\w+\.com\/.+)(\?)(.+) url 302 $1$3
(?<=\d\/\?\w{7}_\w{4}=)1[6-9]..(?=.?.?&) url 307 17

然后在mitm一栏

1
hostname = *.tiktokv.com, *byteoversea.com, *tik-tokapi.com

当然首先mitm证书得配置好

至此,整个titok对应版本软件安装完并且解除限制,如果有问题再仔细看看是哪步出了问题。也可以对比网上其他教程查看。

ref:https://github.com/giterwen/TikTok_Need

整个红点管理采用树形结构,大概分为三部分:

1.redPointModule,该moudle主要负责红点的绑定和移除监听管理

2.redPointEvents,该模块主要是定义各个系统模块功能红点的事件,以及枚举红点类型,红点依赖的profile声明。

3.CommonUI_RedPoint,红点的实体类,是作为一个cell嵌套到各个红点依赖的实例内部,这个实体cell和普通cell无异,由UI管理。

一、先说用法,以主界面邮件为例

a.绑定

1
2
3
4
5
6
7
8
9
10
11
12
function layout:OnAppear()

......

if self._redPointIns ~= nil then
game.modules.RedPointModule:Rebind(self._redPointIns)
else
self._redPointIns = game.modules.RedPointModule:Bind({ event = {game.redPointEvent.MAIL_RED,
game.redPointEvent.TASK_MAIN_RED},widget = {self.BtnMail.transform.gameObject,
self.BtnMission.transform.gameObject},target = self })
end
end

b.移除

1
2
3
4
5
6
function layout:OnDisappear()
......
if self._redPointIns ~= nil then
game.modules.RedPointModule:Unbind(self)
end
end

由于主界面红点需求较多,故上述例子是以一个table一次绑定的,简化代码。

在绑定的时候,需要判断是否绑定过,如果绑定过,只需要调用Rebind,这个API是支持动态替换参数的,具体可以看cell部分代码,如果参数没变,则什么都不传,

内部会再次注册该红点关心的事件。

界面DisApper的时候,记得调用Unbind,这里会移除该红点监听的所有事件,并且移除redPointModule管理的红点实例。

注:为什么要有refresh的设计,因为我们界面是有两个状态,apper与disapper,二者是相辅相成的,当界面apper后,在disapper,这个时候会调用unBind,会移除掉红点的所有监听,但是红点cell可能并未销毁,还在target的cell列表中,故重新apper的时候,只需要刷新红点信息,监听事件即可。各个模块的红点cell需要大家像普通cell一样,自己管理下,其实也不需要做什么,在destyoy的时候置nil

cell中的绑定实例:

image

二、讲下核心,红点对应的事件

还有一个地方需要处理,就是红点对应的事件。具体如下图:

这个事件需要在绑定的时候传入,这是重点,需要自己在redPointEvents声明。有两个参数比较重要,特说明一下。

1.CombineEvents:这个参数是红点树的核心,它主要是记录该红点的依赖节点,即子红点。填写的是redPointevents的枚举事件值,需要各个模块自己酌情判断,以免冗余。

注:这里有个建议,就是当根红点只依赖一个子红点,且无其他任何逻辑,就可以简化掉,直接让根红点绑定子红点的事件,而不需要额外在注册这个根红点事件。

2.CheckProfilesKey:该参数即上述注解,需要注意的是,这个参数传入的是红点配置key,至于该红点关心哪些profile,甚至指定profile的指定key,需要在RedPointEvents.RedPointConfig里边声明。

三、最后说下绑定的参数

这里没啥要多说的,就在呼应一下之前的主界面例子,支持一次绑定多个。注意,是以widget为判断基准的,其他参数可为数组,也可以是具体值,如果为数组,就要与widget一一对应。target肯定不是数组。

四、补充

1、CheckShow的参数问题:这个参数在绑定的时候,有个param,传入什么,会原封不动传入CheckShow中,同时在红点内部产生refresh的时候,也会将该参数原封不动传入CheckShow,故该红点绑定的时候传入的是哪个参数,CheckShow的时候就能收到这个参数,不会发生错乱问题,和被谁依赖无关。

2、根红点依赖子红点,但子红点需要参数的问题:这个时候,就需要自己在子红点的CheckShow中处理无参情况。比如角色入口红点依赖内部各个角色的红点,这个时候,内部每个角色的红点绑定同一个redPointEvent,CheckShow中根据参数处理显示逻辑,那么跟红点依赖这个红点,自然无参可传,这个时候,就需要这个redPointEvent的CheckShow处理无参情况,也就是判断所有角色。

使用动画系统时,有两种控制人物移动的方式:

1.使用动画中的位移

好处是:人物的脚步会跟地面贴合,不会出现滑步的问题(人物的移动距离比步子大或者小),控制简单

缺点是:比较依赖动画的制作,程序控制性不高

2.使用代码控制人物的位移

好处:可控性高

缺点:容易出现滑步,控制复杂

修改Animator Controller中的参数:

设置参数的办法是使用SetInteger、SetFloat、SetBool、SetTrigger

bool参数和Trigger参数的区别:

bool参数和trigger参数很像,都是代表布尔值,但是trigger参数只能被设为true,一旦被transition使用,就会自动被设为false。bool类型一般用于持续的状态,比如角色是否趴下。而trigger一般用于使用一次就会恢复的状态,比如开枪,开枪动画播放完以后,会自动恢复到之前的动作

parameter的id:

设置parameter的时候设置的是一个字符串的名称,但是在Unity内部是有一个数字id跟它对应的,使用Animator.StringToHash这个API可以将字符串的参数名转为数字id。使用数字id的代码运行效率会稍微高一些

其他SetFloat、SetBool、SetTrigger都类似,但是唯一不同的是SetFoat还有额外的两个重载方法:

1
2
public void SetFloat(string name, float value, float dampTime, float deltaTime);
public void SetFloat(int id, float value, float dampTime, float deltaTime);

dampTime 阻尼时间,deltaTime 时间增量

SetFloat的那个damp用法:

damp翻译过来一般是阻尼的意思,你可以理解为缓行。这样Fload值会渐变过去,而不是一下子变成设置的Float值,这个在有些情况下很有用,比如人物的速度。玩家按下W的时候,应该是一个逐渐从0到最大速度的过程,而不应该一下从0到最大速度,这时候就可以用到damp。

效果为如下公式:

总结:

  1. Animator中可以设置参数,用来控制Transition的变化
  2. Has Exit Time也是transition切换的一个条件,只有transition的所有条件都满足时才会进行切换
  3. 在代码中可以使用Animator类中的SetXXX方法控制参数,进而控制状态的转换
关于生成Layer层的写法:

写法1:

1
2
3
4
var layers = productController.layers;
layers[1].defaultWeight = 1;
layers[1].blendingMode = AnimatorLayerBlendingMode.Additive;
productController.layers = layers;

写法2:

1
2
3
4
5
6
7
8
9
var layers = productController.layers;
var layer = new UnityEditor.Animations.AnimatorControllerLayer
{
name = "Face",
defaultWeight = 1f,
blendingMode = AnimatorLayerBlendingMode.Additive,
stateMachine = new UnityEditor.Animations.AnimatorStateMachine()
};
productController.AddLayer(layer);

写法1能够正常导出显示并序列化,但是写法2可以导出显示但无法正常序列化

行业报告 http://report.seedsufe.com/#/report
世界之声 https://aporee.org/maps/
商用图片 https://www.pexels.com/zh-cn/
抠图 https://www.remove.bg/
抠视频 https://www.unscreen.com/
打字比赛 https://play.typeracer.com/
空投 https://airportal.cn/
宝贝DJ http://www.bbdj.com/
色彩 https://color.adobe.com/zh/create/color-wheel
3D模型 https://sketchfab.com/
核爆 https://nuclearsecrecy.com/nukemap/
约稿 https://www.mihuashi.com/artists
虚拟博物馆 https://virtual.mauritshuis.nl/index.html?lang=en&startscene=21
尺寸 https://www.dimensions.com/
迪士尼化 https://toonme.com/
动态图表 https://hanabi.data-viz.cn/index?lang=zh-CN
中国营养 https://www.fatsecret.cn/%E7%83%AD%E9%87%8F%E8%90%A5%E5%85%BB/
发现 https://www.producthunt.com/
找音乐 https://www.tunefind.com/
996身临其境 https://imisstheoffice.eu/
森林之声 https://www.tree.fm/
西方电音 https://www.traxsource.com/dj-top-10s
猜真假 https://landing.adobe.com/en/na/products/creative-cloud/69308-real-or-photoshop/index.html
魔性 https://patatap.com/
中华珍宝馆 https://www.ltfc.net/exhibit/recent
英语游戏 https://www.merriam-webster.com/

这几天由于分支开发,发现主干迁移开辟的新分支上部分预制体挂载的timeline Binding的Animator引用丢失了

想到去对比版本管理器上提交的日志是否有改动差异,结果发现并没有。然后将不同版本的角色预制体拖进VSCode对比相关文件,发现一模一样,根本没差异。

这很令人奇怪,明明预制体的信息和提交都没发生变化,但在最新的分支工程却丢失了引用资源,但是同丢失引用资源inspector下会是missing value显示。

在Google下通过搜索”unity timeline bind”关键词,查到一篇似乎有帮助的博客

于是就开启Debug模式对比了一下两者差异,发现分支工程中的animator绑定的value字段为missing value。

这个结果有些意思,默认下是为空,但是Debug模式下确是引用为空,而且prefab以文本形式打开确实有引用的guid的。这时,旁边负责角色资源预制体工具生成的同学想到了什么,当即把两者所用的FBX文件分别拖入场景中,发现分支工程里的模型文件并没有生成Animator,于是去找引擎大佬咨询是不是模型资源导入做了特殊的后处理操作。经过引擎大佬的查看,是由于此模型文件Rig设置中Avatar Definition改为Copy From Other Avatar,引用了其他模型文件中的avatar,从而自己这里就不再创建生成avatar,进而也就不会自动创建AnimatorController了。

顺便也给我解释了这样改动的原因是同一角色的预制体有多份模型文件的话这样就就可以复用avatar而不用再多一份内存开销。

总结至此所有的困惑与问题都已解决,预制体资源文件对比相同,但是角色的animator默认是由带有avatar的模型文件生成的,如果想要不由fbx生成ac,那么就将其删掉,主动在prefab上挂载生成新的,那么这个ac引用就属于预制体本身引用关系,而跟fbx文件就无引用关系了。

RectMaskD的基本原理就是CanvasRenderer的EnableRectClipping方法

基本原理比较简单,复杂点在于其上层逻辑比较复杂,今天就按逻辑顺序进行分析。
1)启动时通过ClipperRegistry.Register(this);将自己注册到RectMask2D的管理类ClipperRegistry中,便于后续统一调用。
2)启动的同时通过 MaskUtilities.Notify2DMaskStateChanged(this)通知所有子游戏物体(继承IClippable,后续简称子Clippable)重新更新Clipp状态(通过UpdateClipParent重新确定影响自身Clip的RectMask2D);由于考虑到会存在多个Canvas以及RectMask2D的情况,所以子Clippable在得到重新更新状态通知时,会调用MaskUtilities.GetRectMaskForClippable方法重新确认RectMask2D。确认后每个子Clippable将自己添加到相应的RectMask2D维护的列表中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void Notify2DMaskStateChanged(Component mask)
{
var components = ListPool<Component>.Get();
mask.GetComponentsInChildren(components);
for (var i = 0; i < components.Count; i++)
{
if (components[i] == null || components[i].gameObject == mask.gameObject)
continue;

var toNotify = components[i] as IClippable;
if (toNotify != null)
toNotify.RecalculateClipping();
}
ListPool<Component>.Release(components);
}

以上两步为逻辑层控制实现子游戏物体mask的基础。后续是实现mask的方法。

3)当Canvas更新时会调用ClipperRegistry的cull方法进行剔除(即实现遮罩),如下所示。cull方法会通知所有的RectMask2D进行PerformClipping。

1
2
3
4
5
6
7
public void Cull()
{
for (var i = 0; i < m_Clippers.Count; ++i)
{
m_Clippers[i].PerformClipping();
}
}

4)当RectMask2D收到PerformClipping命令时,先获取所有父类有效的RectMask2D。这是为了后续计算遮罩的范围Rect。因为当有两个RectMask2D时,裁切范围是两个共同作用的区域。然后采用Clipping.FindCullAndClipWorldRect方法计算裁切区域。通过名字也可以知道,计算出来的rect为world级别的(其实就是对应的Canvas下的坐标值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static Rect FindCullAndClipWorldRect(List<RectMask2D> rectMaskParents, out bool validRect)
{
if (rectMaskParents.Count == 0)
{
validRect = false;
return new Rect();
}

var compoundRect = rectMaskParents[0].canvasRect;
for (var i = 0; i < rectMaskParents.Count; ++i)
compoundRect = RectIntersect(compoundRect, rectMaskParents[i].canvasRect);

var cull = compoundRect.width <= 0 || compoundRect.height <= 0;
if (cull)
{
validRect = false;
return new Rect();
}

Vector3 point1 = new Vector3(compoundRect.x, compoundRect.y, 0.0f);
Vector3 point2 = new Vector3(compoundRect.x + compoundRect.width, compoundRect.y + compoundRect.height, 0.0f);
validRect = true;
return new Rect(point1.x, point1.y, point2.x - point1.x, point2.y - point1.y);
}

其中比较有用的一个方法是计算两个rect的相交范围:

1
2
3
4
5
6
7
8
9
10
private static Rect RectIntersect(Rect a, Rect b)
{
float xMin = Mathf.Max(a.x, b.x);
float xMax = Mathf.Min(a.x + a.width, b.x + b.width);
float yMin = Mathf.Max(a.y, b.y);
float yMax = Mathf.Min(a.y + a.height, b.y + b.height);
if (xMax >= xMin && yMax >= yMin)
return new Rect(xMin, yMin, xMax - xMin, yMax - yMin);
return new Rect(0f, 0f, 0f, 0f);
}

5)当确定了裁切范围后,RectMask2D通知自己维护的IClippable列表成员进行裁切,然后每个IClippable列表成员调用 canvasRenderer.EnableRectClipping(clipRect);进行裁切。

以上为基本流程,真是代码中会考虑其他一些状况。比如第五步并非一定会进行裁切,而是会根据条件选择裁切或者不进行裁切。

此文来分析以下Raycaster。虽然Unity的Raycaster等一些组件跟ui放在一起,但是很多属于事件系统。

在事件系统中,Raycaster用来获取获取当前交互位置(如鼠标位置)对应的游戏物体,其使用位置在EventSystem中的RaycastAll方法。而RaycasterAll方法却是InputModule调用的。

基本流程

所有的Caycaster都继承子BaseRayster,启动时自动加入到RaycasterManager中,然后由EventSystem调用。目前存在PhysicRaycaster和Physics2DRaycaster以及GraphicRaycaster,分别为3D、2D和ui检测的Raycaster。2d和3D的Raycaster基本相同,差异在于检测时调用raycast,检测的对象不同。基本流程如下所示:

PhysicRaycaster

PhysicRaycater和Physic2DRaycaster两者均为“三维实体”,有别于UI元素。流程如下:
1)生成射线检测的ray以及计算在camera裁剪平面内的射线距离,以此来舍弃camera空间外的物体。
2)其次进行射线检测,但是射线检测时与我们常规使用Physics.Raycast方法不同,采用的是反射的方法。此部分涉及到变量m_MaxRayIntersections,此变量表示是否由射线检测的结果的数量限制。正常使用都是只返回一个结果,但是此处是返回多个结果,然后进行深度判断。
3)根据深度对结果进行排序,然后将结果添加到RaycasterResult列表中。

GraphicRaycaster

GraphicRaycaster用来进行ui检测,虽然也使用到了射线,但其当前ui并不是通过射线检测出来的,射线检测只是为了进行2D和3D物体的遮挡距离计算用的。流程如下:
1)获取canvas对应的Graphic,因为UI图像显示的核心是Graphic类,而Graphic类都注册在了CanvasRegistry中,所以通过CanvasRegistry获取所有的Canvas。
2)通过EventData的屏幕坐标,处理多屏幕的问题,在移动端是不存在此问题的。
3)通过射线检测,找到距离camera最近的2d或者3d物体,并计算距离,用来判断ui是否被遮挡。
4)通过判断点击位置是否在ui的rect范围内来确定那个ui是点击ui,并通过计算三角形法则计算点击距离。然后用3)中计算的距离计算遮挡。如果canvas是overlay,则其始终在最前方,所以不存在遮挡问题。
5)生成RaycastReuslt列表。

射线检测方法

此处进行对使用反射进行射线检测的方法进行说明,代码中通过注释说明是为了避免模块之间的强引用关系(即不需要引入相关模块的使用),所以通过反射方法来获取。也正因为如此,可以直接拷贝出来使用。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
using System;
using System.Collections.Generic;
using System.Reflection;

namespace UnityEngine.UI
{
internal class ReflectionMethodsCache
{
public delegate bool Raycast3DCallback(Ray r, out RaycastHit hit, float f, int i);
public delegate RaycastHit2D Raycast2DCallback(Vector2 p1, Vector2 p2, float f, int i);
public delegate RaycastHit[] RaycastAllCallback(Ray r, float f, int i);
public delegate RaycastHit2D[] GetRayIntersectionAllCallback(Ray r, float f, int i);
public delegate int GetRayIntersectionAllNonAllocCallback(Ray r, RaycastHit2D[] results, float f, int i);
public delegate int GetRaycastNonAllocCallback(Ray r, RaycastHit[] results, float f, int i);

// We call Physics.Raycast and Physics2D.Raycast through reflection to avoid creating a hard dependency from
// this class to the Physics/Physics2D modules, which would otherwise make it impossible to make content with UI
// without force-including both modules.
public ReflectionMethodsCache()
{
var raycast3DMethodInfo = typeof(Physics).GetMethod("Raycast", new[] {typeof(Ray), typeof(RaycastHit).MakeByRefType(), typeof(float), typeof(int)});
if (raycast3DMethodInfo != null)
raycast3D = (Raycast3DCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(Raycast3DCallback), raycast3DMethodInfo);

var raycast2DMethodInfo = typeof(Physics2D).GetMethod("Raycast", new[] {typeof(Vector2), typeof(Vector2), typeof(float), typeof(int)});
if (raycast2DMethodInfo != null)
raycast2D = (Raycast2DCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(Raycast2DCallback), raycast2DMethodInfo);

var raycastAllMethodInfo = typeof(Physics).GetMethod("RaycastAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (raycastAllMethodInfo != null)
raycast3DAll = (RaycastAllCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(RaycastAllCallback), raycastAllMethodInfo);

var getRayIntersectionAllMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionAll", new[] {typeof(Ray), typeof(float), typeof(int)});
if (getRayIntersectionAllMethodInfo != null)
getRayIntersectionAll = (GetRayIntersectionAllCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRayIntersectionAllCallback), getRayIntersectionAllMethodInfo);

var getRayIntersectionAllNonAllocMethodInfo = typeof(Physics2D).GetMethod("GetRayIntersectionNonAlloc", new[] { typeof(Ray), typeof(RaycastHit2D[]), typeof(float), typeof(int) });
if (getRayIntersectionAllNonAllocMethodInfo != null)
getRayIntersectionAllNonAlloc = (GetRayIntersectionAllNonAllocCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRayIntersectionAllNonAllocCallback), getRayIntersectionAllNonAllocMethodInfo);

var getRaycastAllNonAllocMethodInfo = typeof(Physics).GetMethod("RaycastNonAlloc", new[] { typeof(Ray), typeof(RaycastHit[]), typeof(float), typeof(int) });
if (getRaycastAllNonAllocMethodInfo != null)
getRaycastNonAlloc = (GetRaycastNonAllocCallback)UnityEngineInternal.ScriptingUtils.CreateDelegate(typeof(GetRaycastNonAllocCallback), getRaycastAllNonAllocMethodInfo);
}

public Raycast3DCallback raycast3D = null;
public RaycastAllCallback raycast3DAll = null;
public Raycast2DCallback raycast2D = null;
public GetRayIntersectionAllCallback getRayIntersectionAll = null;
public GetRayIntersectionAllNonAllocCallback getRayIntersectionAllNonAlloc = null;
public GetRaycastNonAllocCallback getRaycastNonAlloc = null;

private static ReflectionMethodsCache s_ReflectionMethodsCache = null;

public static ReflectionMethodsCache Singleton
{
get
{
if (s_ReflectionMethodsCache == null)
s_ReflectionMethodsCache = new ReflectionMethodsCache();
return s_ReflectionMethodsCache;
}
}
};

}

使用时直接调用如下六个回调即可:

1
2
3
4
5
6
7
8
9
10
11
12
//获取射线检测到的第一个3D物体
public Raycast3DCallback raycast3D = null;
//返回检测到的所有3D物体
public RaycastAllCallback raycast3DAll = null;
//获取射线检测到的第一个2D物体
public Raycast2DCallback raycast2D = null;
//返回检测到的所有2D游戏物体
public GetRayIntersectionAllCallback getRayIntersectionAll = null;
//返回检测到的所需2D游戏物体,即返回前n个结果,n可以自己定义
public GetRayIntersectionAllNonAllocCallback getRayIntersectionAllNonAlloc = null;
//返回检测到的所需3D游戏物体,即返回前n个结果,n可以自己定义
public GetRaycastNonAllocCallback getRaycastNonAlloc = null;


具体操作如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using UnityEngine;
using UnityEditor;
public class NodeEditor : EditorWindow
{
Rect window1;
Rect window2;
[MenuItem("Window/Node editor")]
static void ShowEditor()
{
NodeEditor editor = EditorWindow.GetWindow<NodeEditor>();
editor.Init();
}
public void Init()
{
window1 = new Rect(10, 10, 100, 100);
window2 = new Rect(210, 210, 100, 100);
}
void OnGUI()
{
DrawNodeCurve(window1, window2);
BeginWindows();
window1 = GUI.Window(1, window1, DrawNodeWindow, "Window 1");
window2 = GUI.Window(2, window2, DrawNodeWindow, "Window 2");
EndWindows();
}
void DrawNodeWindow(int id)
{
GUI.DragWindow();
}
void DrawNodeCurve(Rect start, Rect end)
{
Vector3 startPos = new Vector3(start.x + start.width, start.y + start.height / 2, 0);
Vector3 endPos = new Vector3(end.x, end.y + end.height / 2, 0);
Vector3 startTan = startPos + Vector3.right * 50;
Vector3 endTan = endPos + Vector3.left * 50;
Color shadowCol = new Color(0, 0, 0, 0.06f);
for (int i = 0; i < 3; i++) // Draw a shadow
Handles.DrawBezier(startPos, endPos, startTan, endTan, shadowCol, null, (i + 1) * 5);
Handles.DrawBezier(startPos, endPos, startTan, endTan, Color.black, null, 1);
}
}

圆环的另一种实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using UnityEngine;
using UnityEngine.UI;

[ExecuteAlways]
//离线生成圆环 图片 缓存在预制体中
public class CircleSprite : MonoBehaviour
{
//空白区域色值
[Header("空白区域色值")]
public Color emptyColor = new Color(0, 0, 0, 0);
//圆环区域色值
[Header("圆环区域色值")]
public Color circleColor = Color.white;
//圆环内径/外径
[Header("圆环内径")]
public int minRadius = 40;
[Header("圆环外径[圆形尺寸]")]
public int maxRadius = 50;
//扇形角度
[Header("扇形角度")]
public float circleAngle = 90;

Color lastColor = Color.black;
Color lastCircleColor = Color.black;
int lastMinRadius = 0;
int lastMaxRadius = 0;
float lastCircleAngle = 0;

private void Update()
{
if (Application.isPlaying) return;
if (lastColor != emptyColor || lastCircleColor != circleColor || minRadius != lastMinRadius || maxRadius != lastMaxRadius || circleAngle != lastCircleAngle || circleAngle != lastCircleAngle)
{
circleAngle = Mathf.Clamp(circleAngle, 0, 361);
minRadius = minRadius > maxRadius ? maxRadius : minRadius;
lastColor = emptyColor;
lastCircleColor = circleColor;
lastMinRadius = minRadius;
lastMaxRadius = maxRadius;
lastCircleAngle = circleAngle;
var sprite = CreateSprite(minRadius, maxRadius, circleAngle / 2, circleColor);
sprite.name = "cirlce";
this.GetComponent<Image>().sprite = sprite;
}
}


/// <summary>
/// 绘制扇形圆环,生成Sprite
/// </summary>
/// <param name="minRadius">圆环内径,值为0即是实心圆</param>
/// <param name="maxRadius">圆环外径</param>
/// <param name="circleAngle">1/2扇形弧度,值>=180度即是整园</param>
Sprite CreateSprite(int minRadius, int maxRadius, float halfAngle, Color circleColor)
{
//图片尺寸
int spriteSize = maxRadius * 2;
//创建Texture2D
Texture2D texture2D = new Texture2D(spriteSize, spriteSize);
//图片中心像素点坐标
Vector2 centerPixel = new Vector2(spriteSize / 2, spriteSize / 2);
//
Vector2 tempPixel;
float tempAngle, tempDisSqr;
if (halfAngle > 0 && halfAngle < 360)
{
//遍历像素点,绘制扇形圆环
for (int x = 0; x < spriteSize; x++)
{
for (int y = 0; y < spriteSize; y++)
{
//以中心作为起点,获取像素点向量
tempPixel.x = x - centerPixel.x;
tempPixel.y = y - centerPixel.y;
//是否在半径范围内
tempDisSqr = tempPixel.sqrMagnitude;
if (tempDisSqr >= minRadius * minRadius && tempDisSqr <= maxRadius * maxRadius)
{
//是否在角度范围内
tempAngle = Vector2.Angle(Vector2.up, tempPixel);
if (tempAngle < halfAngle || tempAngle > 360 - halfAngle)
{
//设置像素色值
texture2D.SetPixel(x, y, circleColor);
continue;
}
}
//设置为透明
texture2D.SetPixel(x, y, emptyColor);
}
}
}
else
{
//遍历像素点,绘制圆环
for (int x = 0; x < spriteSize; x++)
{
for (int y = 0; y < spriteSize; y++)
{
//以中心作为起点,获取像素点向量
tempPixel.x = x - centerPixel.x;
tempPixel.y = y - centerPixel.y;
//是否在半径范围内
tempDisSqr = tempPixel.sqrMagnitude;
if (tempDisSqr >= minRadius * minRadius && tempDisSqr <= maxRadius * maxRadius)
{
//设置像素色值
texture2D.SetPixel(x, y, circleColor);
continue;
}
//设置为透明
texture2D.SetPixel(x, y, emptyColor);
}
}
}
texture2D.Apply();
//创建Sprite
return Sprite.Create(texture2D, new Rect(0, 0, spriteSize, spriteSize), new Vector2(0.5f, 0.5f));
}
}

圆环的一种实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Sprites;
using System;
using System.Collections;
using UnityEngine.Serialization;

//[AddComponentMenu("UI/Circle Image")]
public class CircleImage : MaskableGraphic, ISerializationCallbackReceiver, ILayoutElement, ICanvasRaycastFilter
{

[FormerlySerializedAs("m_Frame")]
[SerializeField]
private Sprite m_Sprite;
public Sprite sprite { get { return m_Sprite; } set { if (SetPropertyUtilityExt.SetClass(ref m_Sprite, value)) SetAllDirty(); } }

[NonSerialized]
private Sprite m_OverrideSprite;
public Sprite overrideSprite { get { return m_OverrideSprite == null ? sprite : m_OverrideSprite; } set { if (SetPropertyUtilityExt.SetClass(ref m_OverrideSprite, value)) SetAllDirty(); } }

[Header("圆形或扇形填充比例")]
[Range(0, 1)]
public float fillPercent = 1f;
[Header("是否填充圆形")]
public bool fill = true;
[Header("圆环宽度")]
public float thickness = 5;
[Header("圆形精度")]
[Range(3, 100)]
public int segements = 20;

private List<Vector3> innerVertices;
private List<Vector3> outterVertices;

// Use this for initialization
void Awake()
{
innerVertices = new List<Vector3>();
outterVertices = new List<Vector3>();
}

// Update is called once per frame
void Update()
{
this.thickness = (float)Mathf.Clamp(this.thickness, 0, rectTransform.rect.width / 2);
}



protected override void OnPopulateMesh(VertexHelper vh)
{
vh.Clear();

innerVertices.Clear();
outterVertices.Clear();

float degreeDelta = (float)(2 * Mathf.PI / segements);
int curSegements = (int)(segements * fillPercent);

float tw = rectTransform.rect.width;
float th = rectTransform.rect.height;
float outerRadius = rectTransform.pivot.x * tw;
float innerRadius = rectTransform.pivot.x * tw - thickness;

Vector4 uv = overrideSprite != null ? DataUtility.GetOuterUV(overrideSprite) : Vector4.zero;

float uvCenterX = (uv.x + uv.z) * 0.5f;
float uvCenterY = (uv.y + uv.w) * 0.5f;
float uvScaleX = (uv.z - uv.x) / tw;
float uvScaleY = (uv.w - uv.y) / th;

float curDegree = 0;
UIVertex uiVertex;
int verticeCount;
int triangleCount;
Vector2 curVertice;

if (fill) //圆形
{
curVertice = Vector2.zero;
verticeCount = curSegements + 1;
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);

for (int i = 1; i < verticeCount; i++)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curVertice = new Vector2(cosA * outerRadius, sinA * outerRadius);
curDegree += degreeDelta;

uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);

outterVertices.Add(curVertice);
}

triangleCount = curSegements * 3;
for (int i = 0, vIdx = 1; i < triangleCount - 3; i += 3, vIdx++)
{
vh.AddTriangle(vIdx, 0, vIdx + 1);
}
if (fillPercent == 1)
{
//首尾顶点相连
vh.AddTriangle(verticeCount - 1, 0, 1);
}
}
else//圆环
{
verticeCount = curSegements * 2;
for (int i = 0; i < verticeCount; i += 2)
{
float cosA = Mathf.Cos(curDegree);
float sinA = Mathf.Sin(curDegree);
curDegree += degreeDelta;

curVertice = new Vector3(cosA * innerRadius, sinA * innerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
innerVertices.Add(curVertice);

curVertice = new Vector3(cosA * outerRadius, sinA * outerRadius);
uiVertex = new UIVertex();
uiVertex.color = color;
uiVertex.position = curVertice;
uiVertex.uv0 = new Vector2(curVertice.x * uvScaleX + uvCenterX, curVertice.y * uvScaleY + uvCenterY);
vh.AddVert(uiVertex);
outterVertices.Add(curVertice);
}

triangleCount = curSegements * 3 * 2;
for (int i = 0, vIdx = 0; i < triangleCount - 6; i += 6, vIdx += 2)
{
vh.AddTriangle(vIdx + 1, vIdx, vIdx + 3);
vh.AddTriangle(vIdx, vIdx + 2, vIdx + 3);
}
if (fillPercent == 1)
{
//首尾顶点相连
vh.AddTriangle(verticeCount - 1, verticeCount - 2, 1);
vh.AddTriangle(verticeCount - 2, 0, 1);
}
}

}

public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
{
Sprite sprite = overrideSprite;
if (sprite == null)
return true;

Vector2 local;
RectTransformUtility.ScreenPointToLocalPointInRectangle(rectTransform, screenPoint, eventCamera, out local);
return Contains(local, outterVertices, innerVertices);
}

private bool Contains(Vector2 p, List<Vector3> outterVertices, List<Vector3> innerVertices)
{
var crossNumber = 0;
RayCrossing(p, innerVertices, ref crossNumber);//检测内环
RayCrossing(p, outterVertices, ref crossNumber);//检测外环
return (crossNumber & 1) == 1;
}

/// <summary>
/// 使用RayCrossing算法判断点击点是否在封闭多边形里
/// </summary>
/// <param name="p"></param>
/// <param name="vertices"></param>
/// <param name="crossNumber"></param>
private void RayCrossing(Vector2 p, List<Vector3> vertices, ref int crossNumber)
{
for (int i = 0, count = vertices.Count; i < count; i++)
{
var v1 = vertices[i];
var v2 = vertices[(i + 1) % count];

//点击点水平线必须与两顶点线段相交
if (((v1.y <= p.y) && (v2.y > p.y))
|| ((v1.y > p.y) && (v2.y <= p.y)))
{
//只考虑点击点右侧方向,点击点水平线与线段相交,且交点x > 点击点x,则crossNumber+1
if (p.x < v1.x + (p.y - v1.y) / (v2.y - v1.y) * (v2.x - v1.x))
{
crossNumber += 1;
}
}
}
}



/// <summary>
/// Image's texture comes from the UnityEngine.Image.
/// </summary>
public override Texture mainTexture
{
get
{
return overrideSprite == null ? s_WhiteTexture : overrideSprite.texture;
}
}

public float pixelsPerUnit
{
get
{
float spritePixelsPerUnit = 100;
if (sprite)
spritePixelsPerUnit = sprite.pixelsPerUnit;

float referencePixelsPerUnit = 100;
if (canvas)
referencePixelsPerUnit = canvas.referencePixelsPerUnit;

return spritePixelsPerUnit / referencePixelsPerUnit;
}
}

// /// <summary>
// /// 子类需要重写该方法来自定义Image形状
// /// </summary>
// /// <param name="vh"></param>
// protected override void OnPopulateMesh(VertexHelper vh)
// {
// base.OnPopulateMesh(vh);
// }

#region ISerializationCallbackReceiver
public void OnAfterDeserialize()
{
}

//
// 摘要:
// Implement this method to receive a callback after unity serialized your object.
public void OnBeforeSerialize()
{
}
#endregion

#region ILayoutElement
public virtual void CalculateLayoutInputHorizontal() { }
public virtual void CalculateLayoutInputVertical() { }

public virtual float minWidth { get { return 0; } }

public virtual float preferredWidth
{
get
{
if (overrideSprite == null)
return 0;
return overrideSprite.rect.size.x / pixelsPerUnit;
}
}

public virtual float flexibleWidth { get { return -1; } }

public virtual float minHeight { get { return 0; } }

public virtual float preferredHeight
{
get
{
if (overrideSprite == null)
return 0;
return overrideSprite.rect.size.y / pixelsPerUnit;
}
}

public virtual float flexibleHeight { get { return -1; } }

public virtual int layoutPriority { get { return 0; } }
#endregion

#region ICanvasRaycastFilter
// public virtual bool IsRaycastLocationValid(Vector2 screenPoint, Camera eventCamera)
// {
// return true;
// }
#endregion



}

internal static class SetPropertyUtilityExt
{
public static bool SetColor(ref Color currentValue, Color newValue)
{
if (currentValue.r == newValue.r && currentValue.g == newValue.g && currentValue.b == newValue.b && currentValue.a == newValue.a)
return false;

currentValue = newValue;
return true;
}

public static bool SetStruct<T>(ref T currentValue, T newValue) where T : struct
{
if (currentValue.Equals(newValue))
return false;

currentValue = newValue;
return true;
}

public static bool SetClass<T>(ref T currentValue, T newValue) where T : class
{
if ((currentValue == null && newValue == null) || (currentValue != null && currentValue.Equals(newValue)))
return false;

currentValue = newValue;
return true;
}
}