At the time and until recently, doing so seemed very difficult for someone without a vast knowledge of network experience. Even some of the biggest companies still have problems when it comes to fighting game netcode.
Regardless, I know, if I ever wanted to add it to Battle High 2 or to make it for a future title, that I would want a rollback netcode solution. I know GGPO exists, but it's C++ library (making it very difficult to integrate into Unity3D), expensive to get a license, and last time I checked, not even openly available, especially to Unity devs, anymore.
I had experimented with doing it myself using uNet, Unity's built-in networking library, but the results were far from perfect and rather buggy.
Anyway, a few months ago, I was browsing the Unity Asset Store and came across TrueSync by Exit Games. The first thing I read, which appealed to me was that it used a Rollback Engine and since I had some experience using Photon, the networking library TrueSync is based on, I decided to give it a whirl.
Though the library is not finished, I'm still impressed and have been able to get a pretty decent prototype -- at least to me.
Here's a video showing off the tool to record information I'll be discussing shortly as well as an example fighter:
TrueSync has a few aspects which were interesting to work with in Unity3D.
Fixed PointsThe goal of TrueSync is to keep players in-sync. There are two aspects of base Unity3D development that make this difficult: floating point precision and determinism. Because different computers interpret floating points differently, there can be problems with syncing. To fix this, TrueSync provides its own structs such as FP (Fixed Point) and TSVector. They work similarly to floats and Vector3's, but with the goal to be consistent between machines.
Overall, a lot of Unity3D components have TrueSync versions such as TSTransforms used to replace Transforms and track their position, rotation, and scale; however, some aspects of Unity are still nondeterministic such as the Animator and can make certain types of game, such as a 3D fighter, rather difficult to make.
Anyway, this blog goes over some aspects of TrueSync and workarounds I used to get the game working and still synced (at least to my knowledge as I haven't tested it a ton with other players.)
OnSyncInputTrueSyncBehaviours created by the TrueSyncManager, the MonoBehaviors that manages the TrueSync scene use a method called OnSyncInput. This method is used to take player input, send it to the opponent and then perform a rollback if it has changed.
I feel sending the less values over for this, the better, so I just send a single integer and then use a bitmask to see how it has changed.
AddTracking AttributeAnother important aspects of TrueSync is the AddTracking attribute. TSTransform positions and rotations are tracked; this attribute is important because when the game rolls back, these parameters will be taken into account. Overall, anything that changes on the player and is important for both players to know needs this attribute; however, I wish I knew if there was a limit or warning if you try to track too many variables.
Animation & PlayablesAnyway, since I was doing a 3D fighting game prototype, I had to figure out a way to make TrueSync do the following:
- Translate and rotate the character using animations' root motion
- Figure out where certain limbs were on certain frames for hitbox detection
In 5.5, the advantage is that it allows me to set a time directly instead of letting the animator playout, which can cause problems with floating points precision errors.
To get my game working, I had to do the following:
- Store an array of TSVectors based on frame for translation and rotation
- I did this with a tool that plays out each playable, recording the changes in root position and rotation to TSVectors and TSQuaternions respectively.
- Store a similar array of TSVectors for the position of different limbs.
Vector3 diff = transform.position - prePos;
prePos = transform.position;
diff = transform.InverseTransformVector(diff) * 60f;
fighterInfo[currentInfo].velocityByFrame.Add(new TSVector(diff.x, diff.y, diff.z));
I record the velocity change each frame by using transform.InvertseTransformVector and store it in an array.
For rotation, I use the following:
Quaternion newQuat = Quaternion.FromToRotation(preForward, transform.forward);
preForward = transform.forward;
Vector3 v = newQuat.eulerAngles;
fighterInfo[currentInfo].rotationByFrameEuler.Add(new TSVector(v.x, v.y, v.z));
Then, during OnSyncBehaviour, the TrueSync's behaviour method, I translate and rotate these back to my characters by using the following:
TSVector vel = tsTransform.rotation * states[currentAnimIndex].velocityByFrame[currentFrame] * TrueSyncManager.DeltaTime;
tsTransform.position += vel;
An important thing to note is I also don't let the Animator playing the Playable use root motion. Anyway, because currentFrame uses the AddTracking attribute, when the game rollbacks because of new inputs, my character's positions and rotations are fixed and recalculated.
For keeping track of joints and other areas that need to be tracked for hit spheres, I use the following:
Transform t = anim.GetBoneTransform(fighterInfo[currentInfo].hitBoxPoints[i].boneRef);
Vector3 iP = 0.5f * (t.transform.position + t.GetChild(0).transform.position) - transform.position;
iP = transform.InverseTransformVector(iP);
I get the bone's position and average it with the first child so it feels more center (for example, I get the middle of the hand instead of the wrists. I then subtract the root position from this and use InverseTransformVector. In the game, when I want to get this position back, when testing hit collision, I use the following:
TSVector hurtBoxPos = tsTransform.position + tsTransform.rotation * currentState.hitBoxPoints.trackedPoint[currentFrame];
It's similar to my velocity, but I add the current transform position first.
Overall, this allowed me to get points that in a non-networked game I could probably get more easily, but this allowed me to get this rather accurately.
Local MultiplayerThis is another issue I was having and wasn't addressed, but most if not all fighting games have the ability to be played locally offline. By default, TrueSync doesn't really provide this. Photon does have an offline mode, so when I want a local match, I set offline mode on and instantiate a second player. This player uses a different key during OnSyncInput; otherwise, both players would mimic each other the entire time since it's the same machine.
A Work in ProgressTrueSync is still a work in progress. For example, the provided physics are still rather problematic. I tried using them at first but characters were sinking through the floor, going through walls, and all other sorts of strange behaviour. To be honest, I always struggle when I use an engine's provided physics, so I went with my own solutions instead -- using sphere intersection checking, distance checking, etc.
Also, it's built off Photon, which is great, but there's a lot about Photon I still need to make myself familiar with, for example, how to cleanly exit a room? Is it usable for Xbox One games and if so, what do I need to do? Also, Photon is not free; unless I'm okay with only 20 people being able to play at a time, which I'm not, I'd need to pay to get more eventually; unless, I'm willing to host my own server, which quite frankly I'm not.
Will I use this to port Battle High 2 to an online mutliplayer solution? Maybe! The issue is this prototype is going smoothly because I started working with online in mind from the start. I did not do this with Battle High 2 A+; however, this does not mean I can't do such a thing, but I know there is a lot more work involved.
Finally, I uploaded the protoype to itch.io; if you'd like to try it, feel free! I'd love to know if the online worked for you! (I know, from a fighting game perspective there is still a TON of work to do).