Wednesday, July 20, 2016

Golem Jox 2: A Fighting Game Unet Experiment

During my spare time for the last couple of weeks I've been working on a fighting game prototype in Unity 3D involving primitive 3D shapes.  The following video shows the progression of this prototype:



I had the following two goals while working on this:
  • Create a 2.5D fighting game that utilizes Unity3D's Animator class
  • Make the game work online using Unity's new(er) high level networking API
The game idea, entitled Golem Jox 2, is based off an old jam week project I did at Schell Games a few years ago.  The overall design is that by switching various body parts, legs, arms, etc., the player would have different moves and attacks available.  Though this would be a balancing nightmare -- a nightmare I'd at least attempt to live through -- it could have served as a unique way to allow players to create a fighter that fits their particular play style.  Unfortunately, I didn't get time to integrate the UI needed to allow for swapping.
What I did get to focus on, instead, was the networking aspect using UNet, Unity3D's networking API.  This post is about that game prototype, some lessons I learned, approaches, etc.

Deterministic Approach

So as much as I like Unity3D, one issue I have is that it doesn't feel precise at times, meaning if the game is running at a certain frame rate on one machine it'll yield results different from those of a machine that runs better or slower.  There are ways to resolve this issue, such as using Time.deltaTime, but sometimes there can even be issues with this such as floating point precision issues.
With this goal in mind, my first task with this was to create a way to update the game in a way that would be consistent regardless (or nearly regardless) of any slowdown.  To accomplish this, I used something that resembled the following in my Update method:

// The amount of time, in seconds, that must elapsed before updating the game.
float frameRate = 1f/60f;

// The current frame the game is on.
uint currentFrame = 0;

void Update
{
     time += Time.deltaTime;
     while  (time >= frameRate)
     {
          time -= frameRate;
          currentFrame++;
          ManagedUpdate();
     }
}

Now, this is similar to FixedUpdate, which is supposed to be consistent; however, I suspect that sometimes a frame can be skipped, or the game can be running slow and the value of currentFrame wouldn't increment when I want.  I'll explain the importance of currentFrame later.  Anyway, within the method, ManagedUpdate, I update the players, their physics, any other collision testing I need to do.  I even manage their Animators, which can done by disabling the Animator Component and calling animator.Update(frameRate).  This allows me to know that the Animator is updating consistently and if a few frames need to be skipped, these are skipped in the Animator.

Animator + Mecanim


When Mecanim / Animator, Unity3D's updated solution for playing animations, was announced, my initial thoughts were very skeptic.  I was having some issues and the editor could be better for it; however, its ability to allow for animation retargeting, animation blending, and IK functionalities made it something I really wanted to do for this game.
The following is what the initial state machine of my Animator Controller graph looked like:

I'd show more if Unity let you zoom out...

For this prototype, I used a lot of asset store animations such as http://u3d.as/v2w and http://u3d.as/bQP.  What was great about this was that, because of humanoid targeting, I could use a variety of animations and have them blend smoothly with little to no popping.  Another aspect that Mecanim made useful was StateMachineBehaviours.  StateMachineBehaviours have entry, exit, update, movement, and inverse kinematic methods that can be overwritten.  I usually just used entry, exit, and update.  Originally, I tried using internal variables within each StateMachineBehaviour such as tracking the frame and assigning an exit frame.  There were some issues when you try and enter the same state and use this method, so I decided to use a parameter instead.  Also, it's important to note that Exits happen after Enter if a transition is utilized.
In addition, prototyping is a great time to just experiment with fun things in general.  I did this with the smear effect, which was a vertex shader I found on Twitter:

UNet

So, for a long time, online multiplayer has been very intimidating to me.  In a previous game, Battle High 2 A+, I didn't even attempt to try and do it as I felt I wouldn't be able to do the job properly.  Now, using UNet, I decided to try my hand at prototyping online multiplayer with this.  My goal wasn't just to use UNet's basic functionality though such as SyncVars and NetworkTransforms, but instead just send Commands and use RPC Client calls to try and simulate a rollback system similar to GGPO -- I knew I wasn't going to get anything that sophisticated in less than a week, but I still attempted it for my prototype.
The main goal when doing this is that the local player should feel like their inputs are immediately respected and reacted to.  The problem with this is that when the player immediately changes state, this needs to be sent to the server and then to the opposing client.  Naturally, there will be delay, even if just a few frames and this needs to be dealt with properly.
This is where the currentFrame variable came into play.  My thinking was that I would record the frame when the player changes state, send it over the network and compare the opposing client's current frame with the received frame and then fast-forward that character that many frames.  This, surprisingly, actually got me decent results.  They were FAR from perfect, but I was able to get the game running over the network.  Note, I did this with a StateMachineBehaviour that called a Command upon entry depending on the state.
Most issues would arise when the two players would get out of sync; for example, if the delay was bad -- about 8 or more frames -- some attacks would not register on the opponent's side.  One reason for this could have been that, when I received the values, I wasn't checking attacks when doing the fast forward process; just fast-forwarding the character to make sure the positioning was accurate.  I also wasn't rewinding the entire game this many frames; something that could have resolved some issues.
Anyway, after only three days of focusing on the online aspect, I had something playable over the internet.  It wasn't perfect, but people were able to play the game on two separate machines with relative accuracy.

Conclusion 

In conclusion, I learned a lot doing this prototype.  To be honest, I'm unsure whether or not I'll continue working on this, but the hope is that when I start a new fighting game project, I can carry over the lessons I learned from doing this.  I'd like to also try to use more of UNet's functionality to try and make the online components such as SyncVars and NetworkTransforms to try and make the networking more reliable.
In addition, I'm unsure that I'll be able to use this approach if I ever try to add networking to Battle High 2 A+, but it could be possible in the future.  My only issue is that because I'm using Unity3D's networking solution and am not a seasoned network programmer, that whatever work I ever do, especially in this genre where online play is highly scrutinized, won't be great.  This, however, does take practice and that practice will only come by making (and eventually releasing) games that try and utilize this functionality.
Anyway, if you'd like to play this game, you can download the build here:  https://mattrified.itch.io/golem-jox-2 .  Enjoy and please let me know your thoughts.

No comments:

Post a Comment