打开场景:Tutorial World Streamer - Advanced_Game_Float_Fix_Looped_Safe_Place

只显示一个:

选中这个物体之后:

提示将这个streamer上的scenecollection添加到build settings中:

点击运行之后得到:

用方向键控制人物的行走,在scene视图可以看到地图的加载和卸载的过程:

下面就来研究下,到底是哪个脚本负责人物的移动,哪个脚本负责监测人物的移动,进行新场景的加载,旧场景的卸载。

首先第一个问题,人物的移动:
在人物的身上有两个脚本:
ThirdPersonUserControl
ThirdPersonCharacter

ThirdPersonUserControl
代码量不多:

 private void Start(){// get the transform of the main cameraif (Camera.main != null){m_Cam = Camera.main.transform;}else{Debug.LogWarning("Warning: no main camera found. Third person character needs a Camera tagged \"MainCamera\", for camera-relative controls.");// we use self-relative controls in this case, which probably isn't what the user wants, but hey, we warned them!}// get the third person character ( this should never be null due to require component )m_Character = GetComponent<ThirdPersonCharacter>();}

很简单,获取主相机,获取ThirdPersonCharacter脚本组件。

 private void Update(){if (!m_Jump){m_Jump = CrossPlatformInputManager.GetButtonDown("Jump");}}

判断是否点击了跳跃的按键,用于跳跃。
这个按键的设置在:

默认是按空格键就跳跃,这里我们可以改为4测试下:

此时,按键盘的数字4就可以完成跳跃了。

 private void FixedUpdate()
{// read inputsfloat h = CrossPlatformInputManager.GetAxis("Horizontal");float v = CrossPlatformInputManager.GetAxis("Vertical");bool crouch = Input.GetKey(KeyCode.C);// calculate move direction to pass to characterif (m_Cam != null){// calculate camera relative direction to move:m_CamForward = Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1)).normalized;m_Move = v*m_CamForward + h*m_Cam.right;}else{// we use world-relative directions in the case of no main cameram_Move = v*Vector3.forward + h*Vector3.right;}
#if !MOBILE_INPUT
// walk speed multiplierif (Input.GetKey(KeyCode.LeftShift)) m_Move *= 0.5f;
#endif// pass all parameters to the character control scriptm_Character.Move(m_Move, crouch, m_Jump);m_Jump = false;
}

读取键盘的输入,h——鼠标水平方向移动了多少;v——鼠标垂直方向上移动了多少。
crouch——按键c,用于控制是否匍匐前进。

Vector3.Scale(m_Cam.forward, new Vector3(1, 0, 1));这个代码,是把y方向上归0,就是控制只在xz平面移动。
m_Move = vm_CamForward + hm_Cam.right; 这个是根据水平距离和垂直距离重新计算前进的方向。

在else中,是主相机为null的情况下,使用世界的foward和right作为前进的方向。

#if !MOBILE_INPUT// walk speed multiplierif (Input.GetKey(KeyCode.LeftShift)) m_Move *= 0.5f;
#endif

如果是在非移动端,则要进行速度的减半处理。

最后使用: m_Character.Move(m_Move, crouch, m_Jump);
进行移动。

此时我们唯一的方法,只有进到ThirdPersonCharacter中进行一步一步跟踪,请您告诉我不这样做还能有什么方法。
答案是有的,但是我们选择不进行这个研究,我们只要知道人物的在走动就可了。这不是我们的主要关注点。
也就是说上面的两个脚本控制了人物的行走,对于场景的加载没有丝毫的涉及,所以我们再次回到第二个问题。
哪个脚本负责检测人物的位置,因为只有人物的位置发生了变化,才可能导致新场景的加载和旧场景的卸载。

我们到这个物体:_Streamer_Major_Terrains_Lod0

上面有两个脚本:Streamer和TerrainNeighbours

我们即使隐藏掉这个两个脚本,进行人物移动之后,依然可以正常的加载场景和卸载场景,为啥???????

经过测试,我们在运行之前就把TerrainNeighbours给disable掉,发现依然可以进行正常的新场景的加载。所以我们最终将问题锁定在Streamer中。

在streamer中其实也是很简单的800多行代码,我们下面逐行攻破。

首先它是个MonoBehaviour类。

Awake方法:

void Awake ()
{if (spawnedPlayer) {player = null;}xPos = int.MinValue;yPos = int.MinValue;zPos = int.MinValue;}

这个是想干嘛呢?首先判断是否是要等到player加载之后再赋值,也就是说如果需要等到,那么此时player置为null。否则直接就是用Inspector面板上拖拽的player。

xPos = int.MinValue;
yPos = int.MinValue;
zPos = int.MinValue;

初始化人物当前的位置,都是int的最小值。

 void Start (){if (sceneCollection != null) {PrepareScenesArray ();xLimity = sceneCollection.xLimitsy;xLimitx = sceneCollection.xLimitsx;xRange = xLimity + Mathf.Abs (xLimitx) + 1;yLimity = sceneCollection.yLimitsy;yLimitx = sceneCollection.yLimitsx;yRange = yLimity + Mathf.Abs (yLimitx) + 1;zLimity = sceneCollection.zLimitsy;zLimitx = sceneCollection.zLimitsx;zRange = zLimity + Mathf.Abs (zLimitx) + 1;StartCoroutine (PositionChecker ());canUnload = true;} elseDebug.LogError ("No scene collection in streamer");}

首先判断SceneCollection是否为null,这个scenecollection就是场景的分割的信息集合。

因为在分割scene之后,会生成一个预制体,这个预制体上面有脚本记录,它记录了分割了多少个场景,场景的名字是哪些,分割的大小等信息。

ok,有了个分割信息总览之后,后面才能正确的加载场景呀。

 void PrepareScenesArray (){scenesArray = new Dictionary<int[], SceneSplit> (new IntArrayComparer ());foreach (var sceneName in sceneCollection.names) {int posX = 0;int posY = 0;int posZ = 0;SceneNameToPos (sceneCollection, sceneName, out posX, out posY, out posZ);SceneSplit sceneSplit = new SceneSplit ();sceneSplit.posX = posX;sceneSplit.posY = posY;sceneSplit.posZ = posZ;sceneSplit.sceneName = sceneName.Replace (".unity", "");scenesArray.Add (new int[] {posX,posY,posZ}, sceneSplit);}}

这个函数是想干嘛?其实也很简单,就是把所有names进行一个一个分析,举例:
Advanced_Work_Scene_Terrains_Lod0_x-1_z0.unity
这个场景的名字如上,SceneNameToPos (sceneCollection, sceneName, out posX, out posY, out posZ);
通过这个方法,

首先把Advanced_Work_Scene_Terrains_Lod0_x-1_z0.unity
去除掉prefix scene和.unity得到:_x-1_z0
然后以_分割,得到x-1和z0

foreach (var item in values) {if (item [0] == 'x') {posX = int.Parse (item.Replace ("x", ""));}if (item [0] == 'y') {posY = int.Parse (item.Replace ("y", ""));}if (item [0] == 'z') {posZ = int.Parse (item.Replace ("z", ""));}}

解析得到x=-1,z=0
最终得到x=-1,y=0,z=0

最终把:

SceneSplit sceneSplit = new SceneSplit ();
sceneSplit.posX = posX;
sceneSplit.posY = posY;
sceneSplit.posZ = posZ;
sceneSplit.sceneName = sceneName.Replace (".unity", "");
scenesArray.Add (new int[] {posX,posY,posZ}, sceneSplit);

最终把以posX,posY,posZ作为key,sceneSplit作为value添加到 public Dictionary<int[],SceneSplit> scenesArray;中去。

这样做的目的是啥,后面会体会到,其实你也能猜到了,我们能拿到的是人物的坐标,然后根据这个坐标去拿对应的场景。这个人物坐标是三维的所以,这里以posX,posY,posZ作为key是合乎情理,也是设计巧妙的地方。

接下来几个变量的赋值,会很让人费解:

 xLimity = sceneCollection.xLimitsy;xLimitx = sceneCollection.xLimitsx;xRange = xLimity + Mathf.Abs (xLimitx) + 1;yLimity = sceneCollection.yLimitsy;yLimitx = sceneCollection.yLimitsx;yRange = yLimity + Mathf.Abs (yLimitx) + 1;zLimity = sceneCollection.zLimitsy;zLimitx = sceneCollection.zLimitsx;zRange = zLimity + Mathf.Abs (zLimitx) + 1;

很简单,我们只要看懂一个即可,比如对x的操作。

 xLimity = sceneCollection.xLimitsy;xLimitx = sceneCollection.xLimitsx;xRange = xLimity + Mathf.Abs (xLimitx) + 1;

这个还是要看的sceneCollection的信息:

xlimitsX,是-3,xlimitsy是4,那么xRange = xLimity + Mathf.Abs (xLimitx) + 1;的结果是8。

其他的y和z也是这么计算的。至于什么意义,现在不知道呀呀呀呀。

紧接着是这个启动协程,去检测人物的位置。

StartCoroutine (PositionChecker ());
IEnumerator PositionChecker (){while (true) {if (spawnedPlayer && player == null && !string.IsNullOrEmpty (playerTag)) {GameObject playerGO = GameObject.FindGameObjectWithTag (playerTag);if (playerGO != null)player = playerGO.transform;}if (streamerActive && player != null) {CheckPositionTiles ();} else if (loadedScenes.Count > 0) {UnloadAllScenes ();xPos = int.MinValue;yPos = int.MinValue;zPos = int.MinValue;}yield return new WaitForSeconds (positionCheckTime);}}

搞不懂,作者为啥要在start方法中去启动协程去检测位置,而不是在update中去检测呢?傻傻分不清呀。
ok,那就协程吧,协程中是个while(true)死循环,我说呢,其实还是要一直检测人物的位置,但是但是,yield return new WaitForSeconds (positionCheckTime);这个是能控制多久检测一次人物的位置。而update我们知道是每帧都检测,而每帧差不多是0.02秒,那么这样就可以控制检测人物位置的频次了, public float positionCheckTime = 0.1f;
也就是说,差不多5帧检测一次位置,0.1f/0.02f=5

while中上来就判断是否有检测的物体——player,如果使用的使用的是spawn的方式,那么则要去寻找tag的player,总之就是找场景中的玩家,有了玩家,才有能有位置呀。

if (streamerActive && player != null) {CheckPositionTiles ();}

判断脚本是否enable,并且角色不为空,才去检测位置。否则:

else if (loadedScenes.Count > 0) {UnloadAllScenes ();xPos = int.MinValue;yPos = int.MinValue;zPos = int.MinValue;}

否则,则去判断是否有已经加载的场景,如果有那么则去卸载掉,并且恢复默认的位置。这个很好理解。

下面重点是CheckPositionTiles ();函数的分析。

 public void CheckPositionTiles (){Vector3 pos = player.position;pos -= currentMove;int xPosCurrent = (sceneCollection.xSize != 0) ? (int)(Mathf.FloorToInt (pos.x / sceneCollection.xSize)) : 0;int yPosCurrent = (sceneCollection.ySize != 0) ? (int)(Mathf.FloorToInt (pos.y / sceneCollection.ySize)) : 0;int zPosCurrent = (sceneCollection.zSize != 0) ? (int)(Mathf.FloorToInt (pos.z / sceneCollection.zSize)) : 0;if (xPosCurrent != xPos || yPosCurrent != yPos || zPosCurrent != zPos) {xPos = xPosCurrent;yPos = yPosCurrent;zPos = zPosCurrent;SceneLoading ();Invoke ("SceneUnloading", destroyTileDelay);if (worldMover != null) {worldMover.CheckMoverDistance (xPosCurrent, yPosCurrent, zPosCurrent);}}}

第一个问题是,为啥pos.x/sceneCollection.xSize?
比如我们人现在到了110的位置,而场景的分割的大小为100*100,这里是xz轴,y轴不考虑。
那么110的x,所在第几个块呢?110/100=1.1,向下取整,得到1,所以x的索引就是1。
同理对于y和z也是这么计算。

也就是说经过上面的三个除法,得到的地形块的索引。

紧接着是if判断,如果x,y,z任何一个和当前的索引不同,那么我们则认为需要去加载新的场景了。
于是到了SceneLoading()方法。

 if (showLoadingScreen && loadingStreamer != null) {showLoadingScreen = false;if (tilesLoaded >= tilesToLoad) {tilesToLoad = int.MaxValue;tilesLoaded = 0;}}

这里的代码,我不知道干嘛的,但是它没有任何作用。

int tilesToLoadNew = 0;

统计需要加载新的场景的个数titlesToLoadNew = 0;

if (!useLoadingRangeMin)

如果没有使用ring stream,什么是ring stream,就是人物中心开始像四周一圈不加载,但是形成环状的加载区域,参考我之前的学习笔记。

如果没有使用ring stream,那么useLoadingRangMin为false,则!之后得到true,于是执行if块。

int x = xPos;
int y = yPos;
int z = zPos;int[] sceneID = new int[] {x,y,z}; //当前的地块索引为xyz
float xMoveLimit = 0;
int xDeloadLimit = 0;float yMoveLimit = 0;
int yDeloadLimit = 0;float zMoveLimit = 0;
int zDeloadLimit = 0; //这六个变量我不懂你的意思是啥现在

下面的if是判断是否是循环大世界,无限大世界,无限就是循环实现的。

if(looping)
{}

如果是无限大世界,那么则执行if内容。

if (sceneCollection.xSplitIs)
{int xFinal = mod ((x + Mathf.Abs (xLimitx)), xRange) + xLimitx;xDeloadLimit = (int)Math.Ceiling ((x - xLimity) / (float)xRange) * xRange;xMoveLimit = xDeloadLimit * sceneCollection.xSize;sceneID [0] = xFinal;
}

模函数:

int mod (int x, int m)
{return (x % m + m) % m;
}

上面这段代码又如此简单,首先判断是否对地形的x方向进行了分割,如果分割了,那么则执行if。
int xFinal = mod(); //xFinal为最终的索引
mod的第一个参数,x+Mathf.Abs(xLimitx),就是当前的索引值,加上低限的绝对值
然后,去余上xRange,比如xLimitX=-3,xLimitY = 4,那么xRange=7;
那么取绝对值之后为3,而x=-1,那么-1+3=2,然后mod(2,7)=2
然后再加上xLimitx,2-3=-1
所以x=-1,最后得到xFinal = -1
而如果是x=-5呢,-5+3=-2,mod(-2,7)=5,5-3=2,所以最终的xFinal=2

其实很好懂了吧,其实就是计算,当前的x最终是落在-3和4之间的哪个块。也就是无论你多大的x,我们最终都会将其映射到-3到4这个范围内,做到无限循环。

我们可以做如下的输出:


可以看到xFinal在-3到4,为周期进行循环。当达到了4之后,有从-3开始循环了。
而注意到x是一直增大的,一直到8,然后我们又回到了7,发现xFinal也回到了-1。

ok计算好了scene的索引之后,判断是否有这个scene:

if (scenesArray.ContainsKey (sceneID))
{SceneSplit split = scenesArray [sceneID];if (!split.loaded) {split.loaded = true;split.posXLimitMove = xMoveLimit;split.xDeloadLimit = xDeloadLimit;split.posYLimitMove = yMoveLimit;split.yDeloadLimit = yDeloadLimit;split.posZLimitMove = zMoveLimit;split.zDeloadLimit = zDeloadLimit;scenesToLoad.Add (split);loadedScenes.Add (split);tilesToLoadNew++;}}

还记得我们有个dictionary保存了分割之后的所有的场景信息,所以使用其检测是否有当前计算出来的索引。如果有,并且这个场景没有被加载过,则将其split.loaded = true。

然后就是对我们不明确的两个参数进行x和y和z的赋值,最后加入的要加载的列表中,和已经加载的列表中,然后累加titlesToLoadNew。

上面是使用无线循环的情况,如果不是使用无限循环则,直接进入的是这个for循环。

for (int x = -(int)loadingRange.x + xPos; x <= (int)loadingRange.x + xPos; x++) {for (int y = -(int)loadingRange.y + yPos; y <= (int)loadingRange.y + yPos; y++) {for (int z = -(int)loadingRange.z + zPos; z <= (int)loadingRange.z + zPos; z++) {

3个for就直接吓死我了。

请您不要惧怕,我们来细细讲解。其实很好理解,以xPos为中心,加载周围的地块。
第一层是x方向的左右边界:int x = -(int)loadingRange.x + xPos; x <= (int)loadingRange.x + xPos; x++
第二层是y方向的左右边界:int y = -(int)loadingRange.y + yPos; y <= (int)loadingRange.y + yPos; y++
第二层是z方向的左右边界:int z = -(int)loadingRange.z + zPos; z <= (int)loadingRange.z + zPos; z++

你可懂了,如此简单。

if (useLoadingRangeMin)
if (x - xPos >= -loadingRangeMin.x && x - xPos <= loadingRangeMin.x &&y - yPos >= -loadingRangeMin.y && y - yPos <= loadingRangeMin.y &&z - zPos >= -loadingRangeMin.z && z - zPos <= loadingRangeMin.z) {continue;
}

如果使用了环形流,那么则要检测在-loadingRangeMin.x和loadingRangeMin.x之间的不用去加载。
这个很好理解。

其余的代码,和上面的一样,就是给定xyz的作为id,如果使用循环大世界,那么则计算循环后的xFinal,yFinal,zFinal,最终判断是否有这个场景,是否加载过这个场景,如果都没有,则加入的要加载的场景列表中去。

     tilesToLoad = tilesToLoadNew;initialized = true;}

初始化结束。

那么我们的问题来了,上面的知识准备了要加载的场景,而真正的加载的地方在哪里呢?

 void Update (){LoadLevelAsyncManage ();}

在update方法中,我们看到了LoadLevelAsyncManage 方法。

void LoadLevelAsyncManage (){if (scenesToLoad.Count > 0 && currentlySceneLoading <= 0) {if (LoadingProgress < 1 || sceneLoadFramesNextWaited && sceneLoadFrameNext <= 0) {sceneLoadFramesNextWaited = false;sceneLoadFrameNext = sceneLoadWaitFrames;while (currentlySceneLoading < maxParallelSceneLoading && scenesToLoad.Count > 0) {SceneSplit split = scenesToLoad [0];//if (!Application.is || Application.isWebPlayer && Application.CanStreamedLevelBeLoaded (split.sceneName)) {scenesToLoad.Remove (split);currentlySceneLoading++;//Application.LoadLevelAdditiveAsync ();SceneManager.LoadSceneAsync (split.sceneName, LoadSceneMode.Additive);//}}} else {sceneLoadFramesNextWaited = true;sceneLoadFrameNext--;}}}

再次振作精神,看其究竟干了啥?
首先是判断是否有要加载的场景,如果有,再判断,是否当前加载的场景数量是否小于等于0????why?

如果都成立,则进入第二个if判断:

public float LoadingProgress {get{ return  (tilesToLoad > 0) ? tilesLoaded / (float)tilesToLoad : 1; }}

判断加载的进度。当前已经加载的,和要加载的比值。如果小于1

         if (LoadingProgress < 1 || sceneLoadFramesNextWaited && sceneLoadFrameNext <= 0) {

此时,我确定你被作者绕晕了,原因是代码写的有点混乱,它的本意是只要有没有加载的场景就去加载,并且同时加载的场景有个上限。

所以,我读懂之后,将其简化下为,只要有场景未加载,并且没有超过1个,则要进行新场景的加载。
修改之后为:

 void LoadLevelAsyncManage (){if (scenesToLoad.Count > 0 && currentlySceneLoading <= 0){SceneSplit split = scenesToLoad [0];scenesToLoad.Remove (split);currentlySceneLoading++;SceneManager.LoadSceneAsync (split.sceneName, LoadSceneMode.Additive);}}

这里只要加载了一个场景,那么currentlySceneLoading++;变为1,下一次update之后也不会进入加载新场景了。保证了每帧只去加载一个场景。

那哪里是取减去这个currentlySceneLoading的呢?当然是在场景加载好之后了,我们找到:

public void AddSceneGO (string sceneName, GameObject sceneGO){……tilesLoaded++;currentlySceneLoading--;……}

这个函数在哪里被调用的呢?
是在SceneSplitManager类中的Start方法调用AddToStreamer,进而调用Streamer方法中的AddSceneGO犯法:

这样当一个场景正在被加载,则不能加载其他的场景;而当场景加载完毕之后,才能加载其他的未被加载的场景。

下面我看看下,当一个场景被加载完之后,是如何被加入到内存的。

public void AddSceneGO (string sceneName, GameObject sceneGO)
{int posX = 0;int posY = 0;int posZ = 0;SceneNameToPos (sceneCollection, sceneName, out posX, out posY, out posZ);int[] posInt = new int[] { posX, posY, posZ };if (scenesArray.ContainsKey (posInt)) {scenesArray [posInt].sceneGo = sceneGO;//Debug.Log (currentMove + " " + new Vector3 (scenesArray [posInt].posXLimitMove, 0, 0));sceneGO.transform.position += currentMove + new Vector3 (scenesArray [posInt].posXLimitMove, scenesArray [posInt].posYLimitMove, scenesArray [posInt].posZLimitMove);}tilesLoaded++;currentlySceneLoading--;if (terrainNeighbours)terrainNeighbours.CreateNeighbours ();
}

定义了posX,posY,posZ,传入的参数是sceneName,然后通过SceneNameToPos方法将得到xyz的坐标依次为key,去检查是否包含这个key,如果包含,则将scenesArray[key].sceneGo = sceneGo;
sceneGo就是那个块,就是地形块的父亲物体:

int[] posInt = new int[posX, posY, posZ];if (scenesArray.ContainsKey (posInt)) {scenesArray [posInt].sceneGo = sceneGO;

然后是:

int xFinal = mod ((x + Mathf.Abs (xLimitx)), xRange) + xLimitx;
xDeloadLimit = (int)Math.Ceiling ((x - xLimity) / (float)xRange) * xRange;
xMoveLimit = xDeloadLimit * sceneCollection.xSize;

这个代码什么意思?x是人物的位置除以地形块的size.x得到的值;并且是向下取整的。
当且仅当使用循环的时候,才去进行xFinal的计算,以及xDeloadLimit和xMoveLimit的计算。
其他情况xDeloadLimit和xMoveLimit都是默认为0。

x-xLimity,是当前的x减去加载范围的最大值,比如下面:


x此时为-5,而x加载范围的最大值为4,那么-5-4=-9
-9/8=-1…r
那么向上取整得到-1
-1xRange=-18,得到的是-8
所以xMoveLimit = xDeloadLimit * sceneCollection.xSize;=》-8*100=-800

行了,你胜利了,这段代码的意思,我看不懂了。你成功了,行了吗,够了吗?

World Streamer学习4相关推荐

  1. World Streamer学习2

    reference--World Streamer Manual.pdf chapter4--world streamer details and settings Streaming solutio ...

  2. World Streamer学习5

    上一个篇的最后,我们已经彻底被源码中的计算物体坐标的地方搞晕了,这个真的不太好懂,幸好的断点下的地方比较准确,才促使这个问题得以解决. 首先,我们在这个地方输出一个log: 可以看到当角色的位置达到了 ...

  3. World Streamer学习1

    1.首先找到插件包,可以淘宝买个便宜的. 2.导入到unity项目:World Streamer v1.9.6.unitypackage 3.文件夹: 4.读World Streamer Manual ...

  4. 计算机操作系统 - 目录1

    目录 概述 进程管理 死锁 内存管理 设备管理 链接 参考资料 Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Pre ...

  5. 计算机操作系统 - 目录

    计算机操作系统 概述 进程管理 死锁 内存管理 设备管理 链接 参考资料 Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hal ...

  6. 香侬科技Service Streamer:加速深度学习Web服务、极大提高GPU利用率。| 百万人学AI评选

    2020 无疑是特殊的一年,而 AI 在开年的这场"战疫"中表现出了惊人的力量.站在"新十年"的起点上,CSDN[百万人学AI]评选活动正式启动.本届评选活动在 ...

  7. FFMPEG开源音视频项目学习汇总

    ~非常感谢雷霄骅老师的无私帮助,本文转载自:http://blog.csdn.net/leixiaohua1020/article/details/42658139~       本文汇总一下自己视音 ...

  8. Openvino学习之openvino2022.1版安装配置

    Openvino学习之openvino2022.1版安装配置 文章目录 Openvino学习之openvino2022.1版安装配置 前言 一.从安装角度看新版本的变化 二.安装 1.官网地址 2.安 ...

  9. ethereum-etl学习3

    ethereum-etl学习3 > ethereumetl stream --start-block 500000 -e block,transaction,log,token_transfer ...

最新文章

  1. TreeSet集合排序方式二:定制排序Comparator
  2. Java获取各种常用时间方法
  3. 用计算机模拟光子行为,光量子玻色—费米模拟系统的设计与实现
  4. mpvue 从零开始 女友的衣装 1 pages
  5. runC爆严重安全漏洞,主机可被攻击!使用容器的快打补丁
  6. oracle scn隐藏参数,Oracle隐含参数scn不一致启动
  7. 深度学习环境搭建之Anaconda安装keras
  8. c语言中 d 1是啥意思,空开D/C是什么意思?终于有人把它说清楚了!
  9. 宣化市大专计算机学校,2018张家口专科大学有哪些 最新大专院校名单
  10. Guava cache
  11. Python 中的 if __name__ == ‘__main__‘ 该如何理解
  12. 云卓遥控器+DIY高清摄像机的方案(完美接入原系统)
  13. AR:Unity与iOS交互(入门篇)
  14. 回文树 / 自动机模板
  15. VS2013 启动时遇到空白窗口
  16. Linux下编译OpenSSL
  17. 在线ai伪原创文章生成助手
  18. LDA Effect Size分析 LEfSe详解
  19. 西瓜书.第五章(神经网络)重点最全整理+课后习题
  20. 安利一波gif录制工具

热门文章

  1. ESD的防护要求和器件注意事项
  2. html5 游戏制作教程,【整理】一步一步学做HTML5游戏教程
  3. 全网最佳优惠券使用算法
  4. linux运维工程师工作职责
  5. Instruction set mismatch
  6. 免费的外文文献搜索下载工具推荐
  7. [源码和文档分享]基于Python实现的论坛帖子情感分析
  8. java中文拼音转换(maven)
  9. java将中文转为拼音
  10. Linux 解析 ip 的各种命令