端午三天假,刚过完端午就被老板拉过去加班去了,端午三天假加了两天班,好了不吐槽了。记录一下Unity通过TouchScript插件中TUIO协议的使用以及代码的简单分析。

先说一下项目的大致情况,对方通过TUIO协议发送Blob格式的消息,发送的Blob消息中的面积(Area)是一个识别的重要信息,但TouchScript中返回的是Pointer类,但这个类中并没有我需要的消息。后来分析了一下代码的流向最终拿到了需要的信息。

TuioInput.cs

首先分析一下Tuio的输入,先看一下Unity函数:

/// <inheritdoc />
protected override void OnEnable()
{base.OnEnable();screenWidth = Screen.width;screenHeight = Screen.height;cursorProcessor = new CursorProcessor();cursorProcessor.CursorAdded += OnCursorAdded;cursorProcessor.CursorUpdated += OnCursorUpdated;cursorProcessor.CursorRemoved += OnCursorRemoved;blobProcessor = new BlobProcessor();blobProcessor.BlobAdded += OnBlobAdded;blobProcessor.BlobUpdated += OnBlobUpdated;blobProcessor.BlobRemoved += OnBlobRemoved;objectProcessor = new ObjectProcessor();objectProcessor.ObjectAdded += OnObjectAdded;objectProcessor.ObjectUpdated += OnObjectUpdated;objectProcessor.ObjectRemoved += OnObjectRemoved;connect();
}/// <inheritdoc />
protected override void OnDisable()
{disconnect();base.OnDisable();
}

在OnEnable函数中,首先记录了一下屏幕的宽高,其次new了三个处理类,分别处理鼠标、Blob和物体的,并且分别注册了处理类的回调,当添加时、当更新时、当移除时,最后调用了connect连接函数。按下F12追踪一下connect函数;

private void connect()
{if (!Application.isPlaying) return;if (server != null) disconnect();server = new TuioServer(TuioPort);server.Connect();updateInputs();
}

可以看到此处新建一个TuioServer类,并调用器自身的Connect函数,最后调用updateInputs函数。按下F12看一下TuioServer;

namespace TUIOsharp
{public class TuioServer{public TuioServer();public TuioServer(int port);public int Port { get; }public event EventHandler<ExceptionEventArgs> ErrorOccured;public void AddDataProcessor(IDataProcessor processor);public void Connect();public void Disconnect();public void RemoveAllDataProcessors();public void RemoveDataProcessor(IDataProcessor processor);}
}

这里可以看到TuioServer类中有一个添加处理器的函数AddDataProcessor和移除处理器的函数RemoveDataProcessor,这个下边会说到。我们再看一下updateInputs函数;

private void updateInputs()
{if (server == null) return;if ((supportedInputs & InputType.Cursors) != 0) server.AddDataProcessor(cursorProcessor);else server.RemoveDataProcessor(cursorProcessor);if ((supportedInputs & InputType.Blobs) != 0) server.AddDataProcessor(blobProcessor);else server.RemoveDataProcessor(blobProcessor);if ((supportedInputs & InputType.Objects) != 0) server.AddDataProcessor(objectProcessor);else server.RemoveDataProcessor(objectProcessor);
}

可以看到再updateInputs函数内部,把再OnEnable函数中新建的三个处理类添加进TuioServer中。好的,我们再看一下TuioInput注册三个处理类的回调函数(由于对方是发送Blob消息的,这里只看一下Blob的回调函数,其他类似);

private void OnBlobAdded(object sender, TuioBlobEventArgs e)
{var entity = e.Blob;lock (this){var x = entity.X * screenWidth;var y = (1 - entity.Y) * screenHeight;var touch = internalAddObject(new Vector2(x, y));updateBlobProperties(touch, entity);blobToInternalId.Add(entity, touch);}
}

首先看一下参数TuioBlobEventArgs;

namespace TUIOsharp.DataProcessors
{public class TuioBlobEventArgs : EventArgs{public TuioBlob Blob;public TuioBlobEventArgs(TuioBlob blob);}
}

在追踪一下TuioBlob类;

namespace TUIOsharp.Entities
{public class TuioBlob : TuioEntity{public TuioBlob(int id);public TuioBlob(int id, float x, float y, float angle, float width, float height, float area, float velocityX, float velocityY, float rotationVelocity, float acceleration, float rotationAcceleration);public float Angle { get; }public float Width { get; }public float Height { get; }public float Area { get; }public float RotationVelocity { get; }public float RotationAcceleration { get; }public void Update(float x, float y, float angle, float width, float height, float area, float velocityX, float velocityY, float rotationVelocity, float acceleration, float rotationAcceleration);}
}

好的,在这个类中可以看到有很多信息,坐标、角度、宽度、高度等一些列信息,我需要的面积也在其中,下一步就是怎么取出数据了,由于没怎么用过TUIO,也没研究过TouchScript关于这块的内容走了很多岔子,这就不提了。关于这个类的一些参考可以看一下TUIO官网的说明,链接在这:http://www.tuio.org/?specification

接着看OnBlobAdded函数,在函数内部可以看到x值乘以了缓存的屏幕宽度、y值乘以了缓存的屏幕高度,可以断定传过来的xy是归一化后的数字(即介于0-1之间),同时y轴翻转;紧接着调用internalAddObject函数,并传参xy,看一下internalAddObject函数;

private ObjectPointer internalAddObject(Vector2 position)
{var pointer = objectPool.Get();pointer.Position = remapCoordinates(position);pointer.Buttons |= Pointer.PointerButtonState.FirstButtonDown | Pointer.PointerButtonState.FirstButtonPressed;addPointer(pointer);pressPointer(pointer);return pointer;
}

在这里更新了一下位置,随后调用addPointer和pressPointer两个函数,这里看一下addPointer函数;

/// <summary>
/// Adds the pointer to the system.
/// </summary>
/// <param name="pointer">The pointer to add.</param>
protected virtual void addPointer(Pointer pointer)
{manager.INTERNAL_AddPointer(pointer);
}

这里的函数实际调用了父类InputSource的函数,TuioInput类继承InputSource类,函数内部又调用了manager.INTERNAL_AddPointer函数,这里的manager是TouchManagerInstance类,一个比较核心的类。看一下INTERNAL_AddPointer函数;

internal void INTERNAL_AddPointer(Pointer pointer)
{lock (pointerLock){pointer.INTERNAL_Init(nextPointerId);pointersAdded.Add(pointer);#if TOUCHSCRIPT_DEBUGpLogger.Log(pointer, PointerEvent.IdAllocated);
#endifnextPointerId++;}
}

在这里调用了Pointer类自身的INTERNAL_Init函数,这里就不看了,INTERNAL_Init函数内部更新了一下Id并 记录了一下位置,;在调用Pointer类自身的INTERNAL_Init函数后,将其添加至pointersAdded这个list中,并自动更新下一个Id,关于pointersAdded这里先不深究,等一下再说;接着看一下pressPointer函数;

/// <summary>
/// Mark the pointer as touching the surface.
/// </summary>
/// <param name="pointer">The pointer.</param>
protected virtual void pressPointer(Pointer pointer)
{if (pointer == null) return;manager.INTERNAL_PressPointer(pointer.Id);
}

和上边相同调用了manager的函数,看一下INTERNAL_PressPointer函数;

internal void INTERNAL_PressPointer(int id)
{lock (pointerLock){Pointer pointer;if (!idToPointer.TryGetValue(id, out pointer)){// This pointer was added this frameif (!wasPointerAddedThisFrame(id, out pointer)){// No pointer with such id
#if TOUCHSCRIPT_DEBUGif (DebugMode)Debug.LogWarning("TouchScript > Pointer with id [" + id +"] is requested to PRESS but no pointer with such id found.");
#endifreturn;}}
#if TOUCHSCRIPT_DEBUGif (!pointersPressed.Add(id))if (DebugMode)Debug.LogWarning("TouchScript > Pointer with id [" + id +"] is requested to PRESS more than once this frame.");
#elsepointersPressed.Add(id);
#endif}
}

在这里可以看到函数首先会从idToPointer这个字典中尝试获取Pointer,由于取反,当字典中存在时将会执行

pointersPressed.Add(id);

这条语句,将id添加至pointersPressed中;当字典不存在时,会调用wasPointerAddedThisFrame进行一次判断(应该是判断是否是新添加的Pointer),同样取反,true->return,false->添加至pointersPressed。wasPointerAddedThisFrame函数内部如下:

private bool wasPointerAddedThisFrame(int id, out Pointer pointer)
{pointer = null;foreach (var p in pointersAdded){if (p.Id == id){pointer = p;return true;}}return false;
}

通过foreach进行判断,有意思的是遍历是pointersAdded,上边分析INTERNAL_AddPointer函数时,最终的Pointer被添加的就是pointersAdded。

好的,经过上边一串有点长的函数调用分析,终于把OnBlobAdded函数内部中的internalAddObject函数分析完毕,请滚动一下鼠标重新看一下OnBlobAdded函数,接下来要继续分析下边的执行语句;调用updateBlobProperties函数,并把一些数据添加到blobToInternalId字典中;

private void updateBlobProperties(ObjectPointer obj, TuioBlob target)
{obj.Width = target.Width;obj.Height = target.Height;obj.Angle = target.Angle;
}

updateBlobProperties函数内部更新了一些属性,如上代码所见,我们是不是可以把Area(面积)更新一下???这样我们就可以拿到自己想要的数据了,事实证明这样是可以的,我最终也是这样解决的,理论上这篇博客已经给出了开始问题的解决方案,只需要订阅TouchManager的相应事件即可,比如TouchManager.Instance.PointersAdded、TouchManager.Instance.PointersUpdated、TouchManager.Instance.PointersRemoved……在回调时把相应的Pointer转换为ObjectPointer即可拿到area(ObjectPointer时是Pointer的子类)。但最终我继续分析了其他代码,把Pointer在TouchScript内部流通给搞明白了。所以,我们继续分析;

OnBlobUpdated函数:

private void OnBlobUpdated(object sender, TuioBlobEventArgs e)
{var entity = e.Blob;lock (this){ObjectPointer touch;if (!blobToInternalId.TryGetValue(entity, out touch)) return;var x = entity.X * screenWidth;var y = (1 - entity.Y) * screenHeight;touch.Position = remapCoordinates(new Vector2(x, y));updateBlobProperties(touch, entity);updatePointer(touch);}
}

在这里会首先对blobToInternalId字典尝试获取ObjectPointer(在OnBlobAdded函数最后把OnBlobAdded添加到了blobToInternalId字典中),如果获取成功,会计算位置,并通过remapCoordinates最终调用函数把位置重新映射一下(这里就补贴其他代码了,如果有兴趣请自行查看,下同),调用updateBlobProperties(上边分析过了)更新属性,最终调用updatePointer函数,updatePointer函数内部调用manager.INTERNAL_UpdatePointer函数,该函数如下:

internal void INTERNAL_UpdatePointer(int id)
{lock (pointerLock){Pointer pointer;if (!idToPointer.TryGetValue(id, out pointer)){// This pointer was added this frameif (!wasPointerAddedThisFrame(id, out pointer)){// No pointer with such id
#if TOUCHSCRIPT_DEBUGif (DebugMode) Debug.LogWarning("TouchScript > Pointer with id [" + id + "] is requested to MOVE to but no pointer with such id found.");
#endifreturn;}}pointersUpdated.Add(id);}
}

和INTERNAL_PressPointer函数类似,只不过最后添加的是pointersUpdated而不是pointersPressed。

OnBlobRemoved函数:

private void OnBlobRemoved(object sender, TuioBlobEventArgs e)
{var entity = e.Blob;lock (this){ObjectPointer touch;if (!blobToInternalId.TryGetValue(entity, out touch)) return;blobToInternalId.Remove(entity);releasePointer(touch);removePointer(touch);}
}

同样的先在blobToInternalId中尝试获取ObjectPointer,如果获取成功,则从blobToInternalId移除,且调用releasePointer函数和removePointer,这两个函数最终调用了TouchManagerInstance的INTERNAL_ReleasePointer函数和INTERNAL_RemovePointer函数;其内部实现如下:

/// <inheritdoc />
internal void INTERNAL_ReleasePointer(int id)
{lock (pointerLock){Pointer pointer;if (!idToPointer.TryGetValue(id, out pointer)){// This pointer was added this frameif (!wasPointerAddedThisFrame(id, out pointer)){// No pointer with such id
#if TOUCHSCRIPT_DEBUGif (DebugMode)Debug.LogWarning("TouchScript > Pointer with id [" + id +"] is requested to END but no pointer with such id found.");
#endifreturn;}}
#if TOUCHSCRIPT_DEBUGif (!pointersReleased.Add(id))if (DebugMode)Debug.LogWarning("TouchScript > Pointer with id [" + id +"] is requested to END more than once this frame.");
#elsepointersReleased.Add(id);
#endif}
}/// <inheritdoc />
internal void INTERNAL_RemovePointer(int id)
{lock (pointerLock){Pointer pointer;if (!idToPointer.TryGetValue(id, out pointer)){// This pointer was added this frameif (!wasPointerAddedThisFrame(id, out pointer)){// No pointer with such id
#if TOUCHSCRIPT_DEBUGif (DebugMode)Debug.LogWarning("TouchScript > Pointer with id [" + id +"] is requested to REMOVE but no pointer with such id found.");
#endifreturn;}}
#if TOUCHSCRIPT_DEBUGif (!pointersRemoved.Add(pointer.Id))if (DebugMode)Debug.LogWarning("TouchScript > Pointer with id [" + id +"] is requested to REMOVE more than once this frame.");
#elsepointersRemoved.Add(pointer.Id);
#endif}
}

这两个函数内部极为相似,不同的是最后是添加的不是同一个HashSet。

TouchManagerInstance.cs

先看一下几个比较眼熟的字段:

private List<Pointer> pointers = new List<Pointer>(30);
private HashSet<Pointer> pressedPointers = new HashSet<Pointer>();
private Dictionary<int, Pointer> idToPointer = new Dictionary<int, Pointer>(30);// Upcoming changes
private List<Pointer> pointersAdded = new List<Pointer>(10);
private HashSet<int> pointersUpdated = new HashSet<int>();
private HashSet<int> pointersPressed = new HashSet<int>();
private HashSet<int> pointersReleased = new HashSet<int>();
private HashSet<int> pointersRemoved = new HashSet<int>();
private HashSet<int> pointersCancelled = new HashSet<int>();

我们看一下它的Update函数:

private void Update()
{sendFrameStartedToPointers();updateInputs();updatePointers();
}

这里只看一下updatePointers函数:

private void updatePointers()
{IsInsidePointerFrame = true;if (frameStartedInvoker != null) frameStartedInvoker.InvokeHandleExceptions(this, EventArgs.Empty);// need to copy buffers since they might get updated during executionList<Pointer> addedList = null;List<int> updatedList = null;List<int> pressedList = null;List<int> releasedList = null;List<int> removedList = null;List<int> cancelledList = null;lock (pointerLock){if (pointersAdded.Count > 0){addedList = pointerListPool.Get();addedList.AddRange(pointersAdded);pointersAdded.Clear();}if (pointersUpdated.Count > 0){updatedList = intListPool.Get();updatedList.AddRange(pointersUpdated);pointersUpdated.Clear();}if (pointersPressed.Count > 0){pressedList = intListPool.Get();pressedList.AddRange(pointersPressed);pointersPressed.Clear();}if (pointersReleased.Count > 0){releasedList = intListPool.Get();releasedList.AddRange(pointersReleased);pointersReleased.Clear();}if (pointersRemoved.Count > 0){removedList = intListPool.Get();removedList.AddRange(pointersRemoved);pointersRemoved.Clear();}if (pointersCancelled.Count > 0){cancelledList = intListPool.Get();cancelledList.AddRange(pointersCancelled);pointersCancelled.Clear();}}var count = pointers.Count;for (var i = 0; i < count; i++){pointers[i].INTERNAL_UpdatePosition();}if (addedList != null){updateAdded(addedList);pointerListPool.Release(addedList);}if (updatedList != null){updateUpdated(updatedList);intListPool.Release(updatedList);}if (pressedList != null){updatePressed(pressedList);intListPool.Release(pressedList);}if (releasedList != null){updateReleased(releasedList);intListPool.Release(releasedList);}if (removedList != null){updateRemoved(removedList);intListPool.Release(removedList);}if (cancelledList != null){updateCancelled(cancelledList);intListPool.Release(cancelledList);}if (frameFinishedInvoker != null) frameFinishedInvoker.InvokeHandleExceptions(this, EventArgs.Empty);IsInsidePointerFrame = false;
}

首先看一下pointersAdded,在这里把pointersAdded装进addedList中;

if (pointersAdded.Count > 0)
{addedList = pointerListPool.Get();addedList.AddRange(pointersAdded);pointersAdded.Clear();
}

最后调用updateAdded函数:

if (addedList != null)
{updateAdded(addedList);pointerListPool.Release(addedList);
}

看一下updateAdded函数:

private void updateAdded(List<Pointer> pointers)
{samplerUpdateAdded.Begin();var addedCount = pointers.Count;var list = pointerListPool.Get();for (var i = 0; i < addedCount; i++){var pointer = pointers[i];list.Add(pointer);this.pointers.Add(pointer);idToPointer.Add(pointer.Id, pointer);#if TOUCHSCRIPT_DEBUGpLogger.Log(pointer, PointerEvent.Added);
#endiftmpPointer = pointer;layerManager.ForEach(_layerAddPointer);tmpPointer = null;#if TOUCHSCRIPT_DEBUGif (DebugMode) addDebugFigureForPointer(pointer);
#endif}if (pointersAddedInvoker != null)pointersAddedInvoker.InvokeHandleExceptions(this, PointerEventArgs.GetCachedEventArgs(list));pointerListPool.Release(list);samplerUpdateAdded.End();
}

可以看到最终这些Pointer被添加进idToPointer和pointers中,在函数最后几行进行了回调,完成了一次触摸点从接收到回调的一个完整流程。

在看一下pointersUpdated:

if (pointersUpdated.Count > 0)
{updatedList = intListPool.Get();updatedList.AddRange(pointersUpdated);pointersUpdated.Clear();
}

被添加进updatedList中,接着往下看:

if (updatedList != null)
{updateUpdated(updatedList);intListPool.Release(updatedList);
}

最终调用了updateUpdated函数:

private void updateUpdated(List<int> pointers)
{samplerUpdateUpdated.Begin();var updatedCount = pointers.Count;var list = pointerListPool.Get();for (var i = 0; i < updatedCount; i++){var id = pointers[i];Pointer pointer;if (!idToPointer.TryGetValue(id, out pointer)){
#if TOUCHSCRIPT_DEBUGif (DebugMode)Debug.LogWarning("TouchScript > Id [" + id +"] was in UPDATED list but no pointer with such id found.");
#endifcontinue;}list.Add(pointer);#if TOUCHSCRIPT_DEBUGpLogger.Log(pointer, PointerEvent.Updated);
#endifvar layer = pointer.GetPressData().Layer;if (layer != null) layer.INTERNAL_UpdatePointer(pointer);else{tmpPointer = pointer;layerManager.ForEach(_layerUpdatePointer);tmpPointer = null;}#if TOUCHSCRIPT_DEBUGif (DebugMode) addDebugFigureForPointer(pointer);
#endif}if (pointersUpdatedInvoker != null)pointersUpdatedInvoker.InvokeHandleExceptions(this, PointerEventArgs.GetCachedEventArgs(list));pointerListPool.Release(list);samplerUpdateUpdated.End();
}

这里和updateAdded函数类似,不过它是在idToPointer中尝试获取(idToPointer承载了大部分的工作),最后回调一次PointersUpdated。

最后分析的是pointersRemoved:

if (pointersRemoved.Count > 0)
{removedList = intListPool.Get();removedList.AddRange(pointersRemoved);pointersRemoved.Clear();
}

添加至removedList:

if (removedList != null)
{updateRemoved(removedList);intListPool.Release(removedList);
}

调用updateRemoved函数:

private void updateRemoved(List<int> pointers)
{samplerUpdateRemoved.Begin();var removedCount = pointers.Count;var list = pointerListPool.Get();for (var i = 0; i < removedCount; i++){var id = pointers[i];Pointer pointer;if (!idToPointer.TryGetValue(id, out pointer)){
#if TOUCHSCRIPT_DEBUGif (DebugMode) Debug.LogWarning("TouchScript > Id [" + id + "] was in REMOVED list but no pointer with such id found.");
#endifcontinue;}idToPointer.Remove(id);this.pointers.Remove(pointer);pressedPointers.Remove(pointer);list.Add(pointer);#if TOUCHSCRIPT_DEBUGpLogger.Log(pointer, PointerEvent.Removed);
#endiftmpPointer = pointer;layerManager.ForEach(_layerRemovePointer);tmpPointer = null;#if TOUCHSCRIPT_DEBUGif (DebugMode) removeDebugFigureForPointer(pointer);
#endif}if (pointersRemovedInvoker != null)pointersRemovedInvoker.InvokeHandleExceptions(this, PointerEventArgs.GetCachedEventArgs(list));removedCount = list.Count;for (var i = 0; i < removedCount; i++){var pointer = list[i];pointer.InputSource.INTERNAL_DiscardPointer(pointer);}pointerListPool.Release(list);samplerUpdateRemoved.End();
}

同样在idToPointer中尝试获取,最后完成回调。

到这里这篇博客就基本结束了,也不总结什么了,想研究的自己去看代码琢磨琢磨就清楚了。最后说一下事件的大致调用顺序

add-press-update-released-remove

另外Cancell没找到。

就这样,本人水平有限,如果有错误,欢迎大佬指正,谢谢!

Unity-TouchScripts中使用TUIO的记录和简单的代码分析相关推荐

  1. 文献—Emergent simplicity in microbial community assembly——中使用的交叉互养模型的代码分析

    本文对Emergent simplicity in microbial community assembly--中使用的交叉互养模型的代码分析 原始文献Goldford J E , Lu N , Ba ...

  2. unity 3d 中paint in 3d插件的简单使用

    首先去AssetStore搜一下paint in 3d 接下来步入正题 新建个工程 将下载下来的包导入unity中 导入后,随便打开一个示例场景 本文打开的是MousePainting 运行后可以左键 ...

  3. Unity 3D中的射线与碰撞检测

    创建一条射线Ray需要指明射线的起点(origin)和射线的方向(direction).这两个参数也是Ray的成员变量.注意,射线的方向在设置时如果未单位化,Unity 3D会自动进行单位归一化处理. ...

  4. Unity 3D中的射线与碰撞检测 1

    创建一条射线Ray需要指明射线的起点(origin)和射线的方向(direction).这两个参数也是Ray的成员变量.注意,射线的方向在设置时如果未单位化,Unity 3D会自动进行单位归一化处理. ...

  5. Unity 3D 中的专业“术语表”。

    这是unity手册中的内容.具体可以参考此链接:Unity 用户手册 (2019.4 LTS) - Unity 手册 目录 2D 术语 2D 物理术语 AI 术语 Analytics 术语 动画术语 ...

  6. Unity游戏优化[第二版]学习记录6

    以下内容是根据Unity 2020.1.01f版本进行编写的 Unity游戏优化[第二版]学习记录6 第6章 动态图形 一.管线渲染 1.GPU前端 2.GPU后端 3.光照和阴影 4.多线程渲染 5 ...

  7. Unity 项目中资源管理(续)

    转载自:https://zhuanlan.zhihu.com/p/28324190 上次和大家分享了Unity项目中的资源管理主要讲资源配置以及资源配置工具,Unity资源配置在资源管理中处于基础地位 ...

  8. Unity游戏优化[第二版]学习记录4

    Unity游戏优化[第二版]学习记录4 第4章 着手处理艺术资源 一.音频 1.导入音频文件 2.加载音频文件 3.编码格式与品质级别 4. 音频性能增强 二.纹理文件 1.纹理压缩格式 2.纹理性能 ...

  9. awk 分隔符_awk 中的字段、记录和变量 | Linux 中国

    这个系列的第二篇,我们会学习字段,记录和一些非常有用的 Awk 变量.-- Seth Kenlon Awk 有好几个变种:最早的 awk,是 1977 年 AT&T 贝尔实验室所创.它还有一些 ...

  10. 获取mysql可行方法_Mysql学习Java实现获得MySQL数据库中所有表的记录总数可行方法...

    <Mysql学习Java实现获得MySQL数据库中所有表的记录总数可行方法>要点: 本文介绍了Mysql学习Java实现获得MySQL数据库中所有表的记录总数可行方法,希望对您有用.如果有 ...

最新文章

  1. HashMap,LinkedHashMap,TreeMap的有序性
  2. c 清除 html标签,13.4. 去除HTML的标签tag:htmlRemoveTag
  3. linux服务器的日志管理
  4. 软件工程模块开发卷宗_数据科学家应该了解的软件工程实践
  5. 关于遍历字典的二三事
  6. CENTOS7 修改 网卡名称为eth0的配置方法
  7. 2022起重机司机(限门式起重机)理论题库模拟考试平台操作
  8. 服务器系统备份还原到虚拟机,一秒还原,一秒备份,系统重装「新手学识4」虚拟机--时光倒流...
  9. 蜻蜓安全编写插件模块 webcrack 实践
  10. Python练手项目:计算机自动还原魔方(4)还原底部两层+顶面
  11. ueditor php上传word,ueditor百度编辑器上传PDF并显示
  12. 计算机无法关闭密码保护共享,xp系统怎么关闭密码保护共享
  13. 2020 dns排名_2020年最快的dns是多少_动漫台
  14. Python爬取某宝菠萝数据,并可视化分析销量
  15. 成长的烦恼:如何面对失败常态化的人生
  16. 如何看待 Kotlin 成为 Android 官方支持开发语言
  17. React 基础----1
  18. 关于云数据管理的复兴之路是怎样的?
  19. Mac Pro install peel
  20. 37互娱笔试智力题--猜帽子问题分析

热门文章

  1. python文件是乱码怎么办_python写入文件乱码怎么办
  2. 运维面试和笔试常见问题
  3. srs之服务搭建+OBS推流(简单记录)
  4. Java零基础到进阶(真的零基础,也可以当笔记看~)
  5. 谷歌浏览器安装stylish插件笔记
  6. neo4j中实现关键路径算法
  7. matlab求解线性规划问题的实例代码,matlab 求解线性规划问题
  8. SQL Server2008详细安装步骤(超详细步骤)
  9. 项目集与项目群、项目组合的区别
  10. 2.8数据-paddlepaddle数据集uci_housing