There isn’t a whole lot to explain really. Well there is actually a TON to explain, but without specific cases it’s hard to explain that. The pattern of multithreading I describe is really specific to a specific way the game internally functions.
Basically this is all you do at the base.
using UnityEngine;
using System.Collections;
using System.Threading;
public class ThreadedGameLogicObject : MonoBehaviour
{
private Thread thread;
private Transform thisTransform;
private Vector3 currentPos;
private void Start()
{
thisTransform = this.transform;
thread = new Thread(ThreadUpdate);
thread.Start();
}
private void Update()
{
thisTransform.position = currentPos;
}
//I used FixedUpdate function to modulate the speed at which the threads execute
//there is potentially like a dozen different ways to do this, and this one might not be ideal
//my game used no physics, so I could change the fixed timestep to whatever I needed to make this work
private void FixedUpdate()
{
updateThread = true;
}
private bool runThread = true;
private bool updateThread;
private void ThreadUpdate()
{
while (runThread)
{
if (updateThread)
{
updateThread = false;
currentPos += 1f; //some crazy ass function that takes forever to do here
}
}
}
private void OnApplicationQuit()
{
EndThreads();
}
//This must be called from OnApplicationQuit AND before the loading of a new level.
//Threads spawned from this class must be ended when this class is destroyed in level changes.
public void EndThreads()
{
runThread = false;
//you could use thread.abort() but that has issues on iOS
while (thread.IsAlive())
{
//simply have main loop wait till thread ends
}
}
}
Then you just have to really think about how your laying out your functions.
simple types are Atomic, but processes are not atomic
So for example, lets assume globalFloatArray is something accessed by many different threads:
This simple line IS atomic:
float myVar = globalFloatArray[10];
However this is not:
if ( globalFloatArray[10] != null )
float localFloat = globalFloatArray[10];
//use localFloat in operation
Because between doing the null check on floatArray[10] and actually attempting to read out the variable, it is possible that floatArray[10] could have been set to null by another thread.
The standard C# way you to deal with this, if you search google, you will come up with something like this:
lock(lockObject)
{
if ( globalFloatArray[10] != null )
float localFloat = globalFloatArray[10];
//do your stuff with local float
}
Which is atomic. However I found you must absolutely avoid locking variables accessed from the main thread. If your alternate thread locks a variable, then your main thread tries to access it during the lock, your main thread will wait until it is unlocked. This will cause frame stutter. There is serious overhead to locking a variable, it does not happen quickly. It should be avoided.
What can be done is you can program in a way that you never need to do anything but simple reads and writes to simple types.
So in the example above you could do something like this
float localFloat = globalFloatArray[10]
if (localFloat != null)
//do your stuff with localFloat
globalFloatArray[10] = localFloat;
This is atomic, accomplishes the same thing essentially, and doesn’t use locks. Multiple threads can all be reading in and out of globalFloatArray at a full 60 fps in this manner. But you have to think about how to set that up so that it is atomic without locks.
The game in my profile, enrgy, uses this base design pattern. It seems to run without any issues on Windows, OSX and iOS.
However when you get into it, you may find that not everything can be adapted to this type of pattern very easily. Some things may be impossible. It also must be kept in mind that the thread updating your transform position is out of sync with your update thread. So ideally you alternate thread is executing faster than your Update loop, and for each cycle of the Update loop, your alternate thread will have done something to make things move. But it could be possible depending on how much code you put in your alternate thread that your alternate thread executes slower than your main update loop. At which point your alternate thread may only be outputting a new position only 15 times a second, while your main update loop is updating 60 times a second. You will have to interpolate the output from your alternate thread.
enrgy has two alternate threads running. One which goes faster than the update loop, and one that goes about 1/4 the speed of the update loop. The thread that goes 1/4 the speed is basically the path finding algorithm, it is a very intense calculation, it uses up the entire second CPU on the iPad, and it runs at full speed with no time limits or pauses, constantly updating the path finding grid based on user input. The second alternate thread moves all the particles, this one can run twice the speed of Update loop, and this one is time constrained to go at a certain rate. This thread smoothly moves the particles along the grid that the path finding algorithm laid out, and also does all the game logic of having one particle attack another particle, or test if a particle is hitting anything.
The main thread and Update loop then reads all these particle positions out of a huge global array, clumps together nearby particles to be represented by a single larger particle, then uploads them into a ParticleSystem for display.
Runs reliably as far as I can tell.