There are noticable stutters in the visuals (especially noticable when moving the camera), and the Unity Profiler makes it clear why:
First Thread problems.
Drilling down shows that it’s all in the updates that call the SQL database. We just can’t keep Unity waiting for Update to finish while we query the big relational monster and sort
through the response. The obvious solution is to do it all in a different thread, but what’s the Unity-appropriate way of doing that?
This blog post
seems to have the answer, but as usual we’ll do some experimenting with it first.
Here’s a nice fake task to take up some time and CPU, calculating the largest prime under 50000 in a most inefficient way:
Then we comment out the actual updates of the map and NPCs, just calling ComplexJob on every scheduled Map update:
Ok then.
That’s pretty clear, the spikes taking nearly a second for a single frame. Interestingly, when I doubled a to 100k, I stopped seeing the spikes on the Profiler; everything pauses, but the Profiler just doesn’t show it.
Now let’s implement the code for pushing that call onto a separate thread (a lot of this is verbatim from the blog mentioned above).
internalclassThreadAdapter:MonoBehaviour{privatevolatileboolwaitCall=true;publicstaticintx=0;//this will hold the reference to delegate which will be//executed on ui threadprivatevolatileActiontheAction;publicvoidAwake(){DontDestroyOnLoad(gameObject);this.name="ThreadAdapter-"+(x++);}publicIEnumeratorStart(){while(waitCall){yieldreturnnewWaitForSeconds(.05f);}theAction();Destroy(gameObject);}publicvoidExecuteOnUi(Actionaction){this.theAction=action;waitCall=false;}}
publicvoidBlah(boolresult){print("Blah says "+result);}// see http://blog.yamanyar.com/2015/05/unity-creating-c-thread-with-callback.htmlpublicdelegatevoidJobResultHandler(boolresult);publicstaticvoidDoJobAsync(stringsomeParameter,JobResultHandlerjobResultHandler=null){//If callback is null; we do not need unity adapter, otherwise we need to create it in ui thread.ThreadAdapteradapter=jobResultHandler==null?null:CreateUnityAdapter();System.Threading.ThreadPool.QueueUserWorkItem(//jobResultHandler is a function reference; which will be called by UIThread with result datanewSystem.Threading.WaitCallback(ExecuteJob),newobject[]{someParameter,adapter,jobResultHandler});}privatestaticvoidExecuteJob(objectstate){object[]array=stateasobject[];stringsomeParameter=(string)array[0];ThreadAdapteradapter=array[1]asThreadAdapter;JobResultHandlercallback=array[2]asJobResultHandler;//... time consuming job is performed here...boolresult=ComplexJob(someParameter);// if adapter is not null; callback is also not null.if(adapter!=null){adapter.ExecuteOnUi(delegate{callback(result);});}}/// <summary>/// Must be called from an ui thread/// </summary>/// <returns>The unity adapter.</returns>internalstaticThreadAdapterCreateUnityAdapter(){GameObjectgameObject=newGameObject();returngameObject.AddComponent<ThreadAdapter>();}
Does it work?
In this experiment, at least, it completely resolves things!
publicstaticDictionary<String,Cell>UpdateCells(Dictionary<String,Cell>oldCells,stringmapName){// load the cell information from the Cell tableList<Cell>cellsList=DAL.DBWorld.GetCellList(mapName);if(cellsList==null){Utils.Log("Failed to get cell list from DB?",Utils.LogType.SystemFailure);returnoldCells;}varnewCells=oldCells;foreach(stringtempKeyinnewCells.Keys){newCells[tempKey].IsMapPortal=false;}stringkey="";foreach(CelliCellincellsList){key=iCell.X.ToString()+","+iCell.Y.ToString()+","+iCell.Z.ToString();if(newCells.ContainsKey(key)){newCells[key].Segue=iCell.Segue;newCells[key].Description.Trim();newCells[key].Description=iCell.Description+" "+newCells[key].Description;newCells[key].Description.Trim();newCells[key].cellLock=iCell.cellLock;newCells[key].IsMapPortal=iCell.IsMapPortal;if(iCell.IsMapPortal)Utils.Log("Cell "+key+" is a map portal.",Utils.LogType.SystemFailure);newCells[key].IsTeleport=iCell.IsTeleport;newCells[key].IsSingleCustomer=iCell.IsSingleCustomer;}}returnnewCells;}
I’ve clearly been doing too much functional programming lately, since I felt the need to return a new dictionary, even when the one coming in was passed by reference. Another refactoring to-do.
We’ll use a little flag to know when the other thread is done (another to-do: find out the C# concurrency idiom for this sort of thing):
voidUpdate(){if(updateComplete&&Time.time>nextDBUpdate){print("Time to read from the DB! "+Time.time);nextDBUpdate=Time.time+dbUpdateDelta;updateComplete=false;UpdateCellContents();}}
voidUpdateIsComplete(Dictionary<String,Cell>newCells){foreach(stringcellkeyinnewCells.Keys){// so we can make sure changes are actually seen, since we currently do nothing with them.if(newCells[cellkey].IsMapPortal)print("newCell "+cellkey+" is a map portal.");}ourMap.cells=newCells;updateComplete=true;}publicstaticvoidDoUpdateAsync(Dictionary<String,Cell>param,UpdateResultHandlerjobResultHandler=null){//If callback is null; we do not need unity adapter, otherwise we need to create it in ui thread.ThreadAdapteradapter=jobResultHandler==null?null:CreateUnityAdapter();System.Threading.ThreadPool.QueueUserWorkItem(//jobResultHandler is a function reference; which will be called by UIThread with result datanewSystem.Threading.WaitCallback(ExecuteUpdateJob),newobject[]{param,adapter,jobResultHandler});}privatestaticvoidExecuteUpdateJob(objectstate){object[]array=stateasobject[];varparam=(Dictionary<String,Cell>)array[0];ThreadAdapteradapter=array[1]asThreadAdapter;UpdateResultHandlercallback=array[2]asUpdateResultHandler;//... time consuming job is performed here...Dictionary<String,Cell>result=Map.UpdateCells(param,"Island of Kesmai");// if adapter is not null; callback is also not null.if(adapter!=null){adapter.ExecuteOnUi(delegate{callback(result);});}}publicdelegatevoidUpdateResultHandler(Dictionary<String,Cell>result);voidUpdateCellContents(){// old way:// ourMap.UpdateCells();// updateComplete = true;// new way:UpdateResultHandlerjrh=newUpdateResultHandler(UpdateIsComplete);DoUpdateAsync(ourMap.cells,jrh);}
Since we don’t currently have code for relocating portals or anything, we have to just do some DB updates and watch the log to see that they are reflected.
The profiler now shows the foreach loop in UpdateIsComplete being the only expensive thing happening on the Unity UI thread. Yay!
Tomorrow we make NPC updates cheap, and then think about doing some substantive Cell updates (and why we need to).
I’m an unapologetic Media Molecule fan. Of all the studios I admire, they’re doing some of the most interesting stuff. I ran a fansite during the early days of LittleBigPlanet, and
have had the opportunity to meet with a number of the Molecules over the years. I’m occasionally tempted to work on a fansite for their latest project, Dreams, but know I don’t have the
time. Luckily others are doing more/better than I would, anyway.
This analysis by MrStampy83 covers just about everything I have noticed,
including many of the same hopes and concerns that I have. I was really happy to see some
confirmation that he’s on the right track.