Saturday, April 19, 2014

Interpretted Scripting in Unity

I spent a little while earlier this year trying to devise a good way to handle attack scripting in Phantom Mansion in an extensible, designer-friendly, mod-friendly way.

I decided to look into interpreting scripts at run-time. This would put a little bit more faith in the people who will use this system than I generally like to, but I think it compares favorably with the other options, where I either have to anticipate everything this tool might be used for or be on-call 24/7 for the lifetime of the project implementing tiny changes because a designer wanted, say, a slightly different targeting behavior on an attack.

I made some progress on this recently. There's a wonderful library called Jurassic that allows a C# program to compile and execute Javascript code at run-time. You can expose C# functions and objects to Javascript with a little boilerplate code, which isn't too much work, and this keeps it properly encapsulated.

Initialization looks like this:

 ScriptEngine engine; 

 engine = new ScriptEngine(); 

 engine.EnableExposedClrTypes = true; 

To register a function, the code looks like this:

 engine.SetGlobalFunction("SetState", new System.Action<string>(jsSetState)); 

 #region JS Functions 
      public void jsSetState(string newState) 
      { 
           state = newState; 
      } 
 #endregion  

The jsSetState function is a C# function that sets a C#-side member variable string called State. The engine.SetGlobalFunction call hooks that function to a JS-side function called SetState. We can call that function with Engine.Execute(); which executes an arbitrary string as Javascript code. This code will set the state variable to "This is the new state string."

 engine.Execute("SetState("This is the new state string.");"); 

There's still a lot of work to do on this system. Right now the extent of the supporting architecture for it is a very simple state machine setup, which is what I intend to base the attack system on, but it's very rudimentary right now. I need to write a lot more gameplay hooks, and probably do about seven polish passes to make this really usable. Unfortunately, all of those gameplay hooks are in a really grizzly part of the Phantom Mansion code that I'm never going to touch again unless I'm rewriting it, and rewriting the entire attack system of Phantom Mansion was a little beyond scope. This was, after all, primarily a research/experimentation project.

I think I've found something that can, one day, be the foundation of a really good attack scripting system for Phantom Mansion, but we've got a long road ahead of us, and I am very, very tired.

Building a Fight Stick

I just built a (prototype of) a fight stick.








It was a process.


For a while, everything went off without a hitch. The controller is built around an Arduino Leonardo, which has a couple of very important features. First, the new AVR microcontroller embedded in it supports USB communication directly, without the need for a second chip. This means that the Leonardo can act as a USB host and emulate Human Interface Devices natively, given the right software support. (You can do this with older Arduinos, but you need to reprogram the USB communication chip that is usually used for programming the Arduino. So every time you need to change your code, you need to flash the original software to the communications chip, use that to program the Arduino with your new code, and then re-flash the communications chip with your modified, HID-emulation bootloader, which is a really awkward hack.)



The controller uses Sanwa pushbuttons, which are pretty standard, and started off with a rare, complex joystick called the Ultrastik 360. The Ultrastik is a magnetic analog stick in an arcade joystick form factor.

The most interesting part of this project was the physical act of building the stick. Stripping wires, crimping connectors, cutting holes in cardboard boxes with a dull X-Acto Knife... There's something about a new wiring harness that makes me really happy, I can't quite explain why.


The actual circuitry is pretty simple. Every button or directional input has a pin on the Arduino. These pins are connected to power through a small resistor ("pulled high") and connected to a button, which is connected to ground. This means that, when the button is pressed, the input pin is connected to ground with less resistance than to power, the pin reads low, and we know our button is pressed.


The software problems are significant, but fortunately I didn't have to solve any of them. Writing a new HID Report Descriptor, and the drivers attached to that, is a tremendous amount of very low-level, technical work. Fortunately, the kind people over at the UnoJoy! Project have done it all for me, and it's all open-source, well-documented, and performant. Give them all your pageviews.

I'm still fortunate in that I didn't have to make any modifications to the HID Report Descriptor they gave me. 13 buttons, a D-Pad, and six analog axes as a hard limit could cause trouble for some people, but it was plenty for me.

My first hitch came when it was time to wire up the Ultrastik. It's a very sophisticated piece of hardware, capable of appearing as a Joystick in windows on it's own. It has a nine-pin communication header, which can either be used to read eight buttons (and communicate their state to Windows), or to output four digital values (as an eight-way stick) and two analog values (as an analog stick) to an external source. The ultrastik autodetects which mode to enter when it powers on, based on whether the digital output lines are pulled high or not. Unfortunately, this didn't work, for reasons I still haven't determined. Current theories are:

1. My Arduino isn't pushing quite enough power to the Ultrastik, and I should probably invest in a decent multimeter.

2. I have a bad Ultrastik. I don't think this is the case, but it's a possibility I haven't been able to rule out entirely.



In any case, I couldn't get the UltraStik into output mode. I spent a while talking to Andy at Ultrastik, he was very responsive and helpful, a real pleasure to work with. (As far as I can tell, Andy is UltraStik; they're modified Sanwa joysticks, and it's a very niche market, so I believe he makes them more-or-less by hand.) I sent him some questions, and he gave me more detailed information about the automatic mode selection. When I still had problems, he sent me modified Ultrastik firmware to force the stick into Output mode. Unfortunately, after I installed it, it bricked my stick, and I'm sending it back to Ultrastik for repairs. It's an unfortunate situation, and I feel a little bad writing it here. If someone finds this while researching Ultrastik: I actually recommend it. The hardware is great, it worked well out of the box for most applications (mine is very fringe, most people will use Input Mode) and for people like me with atypical applications, Andy's very helpful. This problem isn't yet resolved, and has run into complications, but I firmly believe that given more time we will, or at least could, make it work.

Unfortunately, I ran out of time. It's the end of the year, and I have a stick that doesn't work. Fortunately, one of my friends, who was assisting with this project, found an old Atari controller in a box in his roommate's closet. These controllers aren't very nice to handle, but they're great to work with. I still mourn the demise of parallel interfaces; they're so intuitively hackable. In any case, wiring up an Atari controller is super simple, and provided some semblance of functionality. You can play flash games with Joy2Key. You can play Super Meat Boy, but it's hard. I can't condone Skullgirls, the Atari stick is just too sticky.


 Here's an awkward video that didn't embed quite right. It's very late, I'm sorry.

Friday, February 28, 2014

Tightly Controlled Physics Forces in Unity




This post pertains to an ongoing struggle of mine.

Unity has a character controller. This is very useful if you want to precisely control a character through movement deltas.  Unity also has a fully-fledged physics system, which is very useful if you want to control a character with physically-simulated forces and have it interact realistically with the world.

Unfortunately, this is an either-or situation. As useful as it would be, you can't apply a direct movement delta to a rigid body, or apply a knockback force to a character controller. This comes up more often than you might think. Any game with knock-back from explosions ("rocket jumping") uses a mix of physics simulation and direct-driven character movement, in general.

I'm working on a prototype (currently in the sort of limbo where it may never see any sort of public release) of a physics-heavy FPS where every weapon has different movement tricks attached to it. (Similar in concept to the traditional rocket jump, but obviously very different for each weapon.) I needed to be able to do this.

Unfortunately, I still kind of can't. I built three (well, two and a half) controllers, and while two of them work to some extend, neither of them work as well as I need them to, which is why I'm temporary shelving the prototype.

My first controller tried to precisely control a rigidbody using physics forces.

 if (rigidbody.velocity.HorizontalComponent().sqrMagnitude < speed * speed)  
           {  
                rigidbody.AddForce(movement - rigidbody.velocity.HorizontalComponent(), ForceMode.VelocityChange);  
           }  
           else  
           {  
                Debug.Log(influenceMove * Time.fixedDeltaTime);  
                rigidbody.velocity.Influence(influenceMove * Time.fixedDeltaTime);  
           }  
           if (Input.GetKeyDown(KeyCode.Space))  
                rigidbody.AddForce(Vector3.up * jumpPower, ForceMode.VelocityChange);  

This immediately ran into some problems. I still didn't have precise control, and although that could be managed, the controller was jumpy on some slopes, and the constant application of force let the player stick to near-vertical walls.  This is more or less the solution I had used in the past, it's acceptable for much simpler applications, but it simply didn't meet my needs here.

My second (attempted) controller tried to basically re-implement a primitive version of Unity's character controller using CapsuleCast and SweepTest to check collisions.

 RaycastHit[] hits;  
           hits = rigidbody.SweepTestAll(movement);  
           float hitDistance = Mathf.Infinity;  
           foreach (RaycastHit hit in hits)  
           {  
                Debug.Log(hit.collider.gameObject.name + " at " + hit.distance);  
                if (hit.distance < hitDistance)  
                     hitDistance = hit.distance;  
           }  
           //Debug.Log(hitDistance);  
           Debug.Log("Moving " + movement.magnitude * Time.deltaTime);  
           if (hits.Length == 0 || hitDistance > movement.magnitude * Time.deltaTime)  
           {  
                Debug.Log("FullMove");  
                transform.Translate(movement * Time.deltaTime);  
           }  
           else  
           {  
                Debug.Log("TruncatedMove");  
                transform.Translate(movement.normalized * hitDistance);  
           }  

 Unfortunately, these Unity functions don't work quite as advertised. Rather than checking if my capsule will collide with anything along the ray that represents the sweep path, SweepTest and CapsuleCast just cast a single ray from the center of the capsule to the destination point, and if the ray is interrupted, it adjusts the hit point so that the capsule will touch the object that was hit, rather than being stuck in it. This isn't a useless function, but it doesn't do what I need it to here, and as far as I could tell after several hours in the Unity documentation, there is no function that does what I need, which left this approach dead in the water.  It's certainly possible to do, but it isn't easy enough to do in two weeks.

My third controller was the closest to being successful. Instead of trying to build a character controller around rigidbody physics, I tried to build primitive rigidbody physics around a character controller.
 public class PhysicsAccumulator : MonoBehaviour {  
      CharacterController controller;  
      Dictionary<string, PhysicsForce> forces = new Dictionary<string,PhysicsForce>();  
      public PhysicsForce Force(string name)  
      {  
           if (ForceExists(name))  
                return forces[name];  
           else  
                return null;  
      }  
      public bool ForceExists(string name)  
      {  
           if(forces.ContainsKey(name))  
                return true;  
           else  
                return false;  
      }  
      public bool AddForce(string name, PhysicsForce force)  
      {  
           if (ForceExists(name))  
                return false;  
           forces[name] = force;  
           return true;  
      }  
      public bool RemoveForce(string name)  
      {  
           if (!ForceExists(name))  
                return false;  
           forces.Remove(name);  
           return true;  
      }  
      // Use this for initialization  
      void Start () {  
           controller = GetComponent<CharacterController>();  
           forces.Add("Controller", new ControllerForce(controller, 4));  
           forces.Add("BunnyHop", new JumpForce(controller, 7f, 5, 10));  
           forces["BunnyHop"].FlatDamping = 3;  
           forces.Add("Gravity", new VectorLockedForce());  
           forces["Gravity"].Acceleration = new Vector3(0, -15f, 0);  
           ((VectorLockedForce)forces["Gravity"]).LockDirection = Vector3.down;  
           forces.Add("Rocket", new PhysicsForce());  
           forces["Rocket"].LinearDamping = 1;  
           forces["Rocket"].Aerial = true;  
      }  
      // Update is called once per frame  
      void LateUpdate () {  
           foreach (KeyValuePair<string, PhysicsForce> f in forces)  
                f.Value.Update(Time.deltaTime);  
           Vector3 netForce = Vector3.zero;  
           foreach (KeyValuePair<string, PhysicsForce> f in forces)  
                netForce += f.Value.Displacement;  
           controller.Move(netForce);  
      }  
      void OnControllerColliderHit(ControllerColliderHit hit)  
      {  
           foreach (KeyValuePair<string, PhysicsForce> f in forces)  
           {  
                f.Value.HandleHit(hit);  
           }  
      }  
 }  

I built a simple physics force registry and set it up with an array of forces representing both my "control" forces for movement and jumping and various environmental forces for things like gravity and rocket knockback. This worked perfectly until I tried to implement spherical planet gravity for the player. Character controllers in Unity have to be axis-aligned, which makes using them on a sphere impossible. This wasn't technically something I felt I needed going in, but the point of this experiment was to find a way to avoid being constrained by the limitations of these two systems in the future, and this is at least one limitation I have yet to overcome.

I think my next attempt, whenever that might be, will probably involve attempting to solve a problem I encountered before any of the controllers mentioned here. It is possible to drive a rigidbody's velocity directly, but it effectively overrides any meaningful physics motion. (You can't, for example, drive a vehicle off of a jump believably if you reset it's velocity every physics frame.) There might be a better approach to this method, but that's a task for the future.

Friday, November 15, 2013

Particle Effects

(I made a bunch of them.)










(The fighting.loulessing.com build hasn't worked for a while, the live build is using a windows-only controller library that isn't compatible with the web build. I'll post a download soon.)

Saturday, November 2, 2013

Art Style Recap

I'd like to quickly recap about a month and a half of art style work.

Before I go further, you should check out Liz's Blog, because she goes over this all in much more detail, and has a much better perspective.

Our gameplay can be flavored as basically anything. This is a blessing and a curse. We have basically total creative freedom, but there's a very limited amount our gameplay can do for our art and vice versa. None of our mechanics mandate anything about our art, this isn't a game "about" anything, and as such nearly any theme or art style we choose is going to on some level feel arbitrary.

There's a reason most fighting games have stories that basically revolve around "There are people. They fight because reasons." The simple fact of the gameplay is that it isn't really simulating anything except in the most abstract sense, and if you're not going to take the obvious "Martial arts + some gimmick" theme, you're sort of at the end of what makes sense, and it's best not to shine too much of a spotlight on that fact.

One of our first theme ideas was also our longest-lived. We considered a game about chefs, having a food fight on some sort of demented parody of a cooking show. It was a lot of fun, kind of zany, and more reliant on comedy than I personally felt comfortable with. Comedy is risky. I think I'm a fairly funny person, and my team are definitely funny people, but things go wrong with projects like this all the time. The fact is that we're an inexperienced, undermanned, underfunded team under a lot of time pressure, and a fair amount of other external pressure. Lots of things under these circumstances end up being pretty-good-but-not-great, and nothing fails less gracefully under those conditions than humor. If you try to be beautiful and don't quite make it you wind up interesting-looking. If you try to be exciting and don't quite make it, you end up campy. If you try to be funny and don't quite make it, you end up making people cringe, and we really didn't want to do that.

We went through a dozen other art styles. Surreal monsters, levels based on different historical art styles, weird little crystal robot birds (which I never understood but I really really miss,) wizards, origami people... This was an elaborate process.

The last iteration of it is the one I have the clearest memories of. We were down to two concepts. One of them was the chef one, and the other one was less defined. The less defined one was an aesthetic design involving ghostly figures in old clothing, wearing masks and fighting in a theater. Nobody was certain what they were, but we all thought it was very cool and very compelling.

So we spent a week trying to figure out what they could be. We constructed this elaborate narrative about spirits fighting their way out of purgatory, wearing masks and wielding powers symbolic of the things they did in life. We justified a whole lot of things under it, and sort of made it work.

Then we decided that was dumb and we were going to do a game about ghosts, and I literally couldn't be happier about that decision. It was a last-minute decision (literally; we set a deadline to pin down our art style and the specifics of the ghost theme weren't brought up until the day of that meeting) and I think it was a perfect decision. We've finally found a compelling artistic direction that the whole team likes. It's not too silly, it's not too pretentious, it's not too boring, it's not too dumb, it's not too difficult, it's not too restrictive, and it provides some inspiration in the form of two new mechanics that I'll talk about in my next post. Plus, we all really like ghosts. I'm all about ghosts. Remy, one of our designers, actually is a ghost. We're all looking forward to working with ghost story and horror tropes for the next few weeks while we get our art built, in-game, and ready to go. Personally, I really want a move that makes you throw your own head, and until you pick your head back up your body can move normally, but all your attacks originate from where your head landed.

(Also, we have some art in-game.)

Monday, October 14, 2013

The Difference Between Doing Something Quickly and Doing It Well

I'd like to talk, likely at length, about the difference between doing something quickly and doing it well.
I feel that they each have their place. Up until this point, nearly everything in this project has been done quickly. That kind of quick and dirty (actually incredibly sloppy) code has gotten us where we are, which is a very good place to be. We are 'ahead,' and more importantly, our game is fun. (This isn't nominally a competition, but it almost definitely is in practice.)

However, doing something well and doing it quickly are different things.

For instance, I redid our movement a while back.

It used to work by a fairly simple process. The code read the horizontal value of an analog stick, multiplied that by a constant and the time that has passed since the last frame, and moved the character that distance. When the player jumped, it applied a constant vertical force as long as the player held the jump button and hadn't run out of remaining jump time. The player got their jump time back when they hit another object while falling. When the player wasn't jumping, they fell at a constant speed.

It looked (in short) like this:

      // Update is called once per frame  
      void Update()  
      {  
           Vector3 stickPos = Vector3.zero;  
           stickPos.x = Input.GetAxis(playerNumber + "Horizontal");  
           stickPos.y = Input.GetAxis(playerNumber + "Vertical");  
           Vector3 moveDir = Vector3.zero;  
           if (!Input.GetButton(playerNumber + "Root"))  
           {  
                moveDir.x = stickPos.x * speed * Time.deltaTime;  
                moveDir.y = 0;  
                //if (Input.GetButton(playerNumber + "Jump") || (stickPos.y >= tapJumpThreshold && lastStickPos.y < tapJumpThreshold))  
                if (Input.GetButton(playerNumber + "Jump") || (stickPos.y >= tapJumpThreshold))  
                {  
                     if (remainingJumpGas > Time.deltaTime)  
                     {  
                          remainingJumpGas -= Time.deltaTime;  
                          moveDir.y = jump * Time.deltaTime;  
                     }  
                     else  
                     {  
                          moveDir.y = jump * remainingJumpGas;  
                          remainingJumpGas = 0;  
                     }  
                }  
           }  
           moveDir.y += gravity * Time.deltaTime;  
           controller.Move(moveDir);  
           if (stickPos.x < -switchFacingThreshold)  
                facing = false;  
           if (stickPos.x > switchFacingThreshold)  
                facing = true;  
           lastStickPos = stickPos;  
      }  


      void OnControllerColliderHit(ControllerColliderHit hit)  
      {  
           if(hit.moveDirection == Vector3.down)  
                remainingJumpGas = jumpGas;  
      }  

This code worked fine, but it didn't feel very good. The movement felt slow, stiff, and unnatural. Moving while jumping was particularly painful, and jumps felt very floaty.

The reworked process is a little more complicated.

The new horizontal movement code generates three numbers every frame.
First, a target velocity, defined by the horizontal position of the movement stick.
Second, a maximum horizontal acceleration, defined in terms of how many seconds it takes the player to accelerate from a stop to full speed, that differs when the player is in the air. The sign of this value is determined by the horizontal position of the movement stick, and if the stick is within the dead zone, this value is set to zero.
Third, a damping value that represents how quickly the player will stop if they are allowed to glide (that is to say "they receive no input") that also differs when the player is in the air. The sign of this value is always opposite the sign of the current velocity (it always accelerates toward zero, so to speak) and if the current velocity is zero, this value is also set to zero.

From the target velocity and the current velocity, it generates a target velocity change. Then, the horizontal movement code adds either the horizontal acceleration value, or the damping value, or neither (if the desired velocity change is zero) or both (if the player is trying to change direction) to the current velocity. It does not always apply the entire velocity change from applicable values, if the change would exceed the desired change it applies only the desired change in velocity.

The new jump process is equally complex.

Gravity is handled as an acceleration force that will always accelerate the player downward. When the player presses the jump button, if they are grounded, it resets their vertical velocity to a constant "standing jump" upward value. If they are not grounded and still have air jumps, it resets their vertical velocity to a constant "air jump" upward value, and drops their remaining air jumps by one. Air Jumping also immediately sets the player's horizontal velocity to their target velocity, which helps compensate for low aerial control, and makes air jumps feel very impactful. At any point during this process, if the player releases the jump button while moving upward, they will end the jump early (reset their vertical velocity to zero) which allows for short hops on the ground to dodge projectiles without having to float for too long. There is also an experimental mechanic where an air jump aimed downward will cause the player to "Ground Pound" and fall very quickly. This is probably going to be removed, or reworked into a draftable dash move, it doesn't seem to play very well right now. I've never seen someone do it because they wanted to, but people do it by accident and get mad sometimes.

(Design aside: Low aerial control means that jumping is a risk. It makes your movements more predictable in the air, which is otherwise a very powerful place to be. You move faster when jumping or falling, and are harder to hit with horizontal attacks, which are by far the most prevalent kind of projectile in the game right now. A limited number of air jumps mean that you can still do a physical dodge (that is, maneuver out of the way of a projectile, rather than use your dodge move to negate its damage.) However, air jumping to dodge a projectile usually means that you won't be going where you want to go. You need your air jumps to land where you want to land most of the time, or at least to maintain your air time and vertical position, so using them for other things often results in other consequences. Positioning is fairly important, and it's pretty easy for a good player to land at least one or two free hits on a player who is landing too close to them. On a whole, I think this system feels very good for both players involved, every action involved in it feels organic, every jump represents a test of skill for one player, and a chance to outplay that person for the other player.)

This code looks  more like this:

(I apologize for it still not being in a particularly consistent style. Stylistic consistency is nice, but I'm the only programmer on this project and it's not a priority for me right now.)

     // Update is called once per frame  
     void Update()  
     {  
          float linearDamping, acceleration;  
          if (grounded)  
          {  
               linearDamping = maxVelocity / timeToStop;  
               acceleration = maxVelocity / timeToMaxSpeed;  
          }  
          else  
          {  
               linearDamping = maxVelocity / airTimeToStop;  
               acceleration = maxVelocity / airTimeToMaxSpeed;  
          }  
          float targetVelocity = maxVelocity * Input.GetAxis(playerNumber + "Horizontal");  
          if (Input.GetButton(playerNumber + "Root"))  
               targetVelocity = 0;  
          float horizontalDirection = 0;  
          if (Input.GetAxis(playerNumber + "Horizontal") <= -horizontalDeadZone)  
          {  
               horizontalDirection = -1;  
               _facing = false;  
          }  
          if (Input.GetAxis(playerNumber + "Horizontal") >= horizontalDeadZone)  
          {  
               horizontalDirection = 1;  
               _facing = true;  
          }  
          float accelerationValue = acceleration * horizontalDirection * Time.deltaTime;  
          float linearDampingValue = linearDamping * -1 * Mathf.Sign(currentVelocity.x) * Time.deltaTime;  
          float desiredVelocityChange = targetVelocity - currentVelocity.x;  
          if (desiredVelocityChange < 0)  
          {  
               if (linearDampingValue < 0)  
               {  
                    if (linearDampingValue < desiredVelocityChange)  
                         linearDampingValue = desiredVelocityChange; //Not allowed to damp more than the desired velocity change  
                    if (currentVelocity.x > 0 && linearDampingValue < currentVelocity.x * -1)  
                         linearDampingValue = currentVelocity.x * -1; //Not allowed to damp past zero  
                    currentVelocity.x += linearDampingValue;  
                    desiredVelocityChange = targetVelocity - currentVelocity.x; //We have to recalculate this every time we change the current velocity, because reasons, but it will never change sign so we don't have to break out of the loop.  
               }  
               if (accelerationValue < 0)  
               {  
                    if (accelerationValue < desiredVelocityChange)  
                         accelerationValue = desiredVelocityChange; //Not allowed to accelerate past the desired change  
                    currentVelocity.x += accelerationValue;  
               }  
          }  
          else if (desiredVelocityChange > 0)  
          {  
               if (linearDampingValue > 0)  
               {  
                    if (linearDampingValue > desiredVelocityChange)  
                         linearDampingValue = desiredVelocityChange; //Not allowed to damp more than the desired velocity change  
                    if (currentVelocity.x < 0 && linearDampingValue > currentVelocity.x * -1)  
                         linearDampingValue = currentVelocity.x * -1; //Not allowed to damp past zero  
                    currentVelocity.x += linearDampingValue;  
                    desiredVelocityChange = targetVelocity - currentVelocity.x; //We have to recalculate this every time we change the current velocity, because reasons, but it will never change sign so we don't have to break out of the loop.  
               }  
               if (accelerationValue > 0)  
               {  
                    if (accelerationValue > desiredVelocityChange)  
                         accelerationValue = desiredVelocityChange; //Not allowed to accelerate past the desired change  
                    currentVelocity.x += accelerationValue;  
               }  
          }  
          else  
          {  
               //Velocity is right where it needs to be.  
          }  
          if (currentVelocity.x > maxVelocity)  
               currentVelocity.x = maxVelocity;  
          if (currentVelocity.x < -maxVelocity)  
               currentVelocity.x = -maxVelocity;  
          if (!specialFall)  
          {  
               currentVelocity.y += -gravity * Time.deltaTime;  
               if (currentVelocity.y < -terminalVelocity)  
               currentVelocity.y = -terminalVelocity;  
          }  
          if (grounded)  
          {  
               if (Input.GetButtonDown(playerNumber + "Jump") || (Input.GetAxis(playerNumber + "Vertical") >= tapJumpThreshold && lastStickPos.y < tapJumpThreshold && !Input.GetButton(playerNumber + "Root")))  
               {  
                    currentVelocity.y = jumpStrength;  
                    grounded = false;  
               }  
          }  
          else  
          {  
               if (airJumpsRemaining == airJumpCount)  
               {  
                    if (Input.GetButtonUp(playerNumber + "Jump"))  
                         if (currentVelocity.y > 0)  
                              currentVelocity.y = 0;  
               }  
               //Air Jump Code  
               if (airJumpsRemaining > 0)  
               {  
                    if (Input.GetButtonDown(playerNumber + "Jump"))  
                    {  
                         airJumpsRemaining--;  
                         if (Input.GetAxis(playerNumber + "Vertical") > crashThreshold)  
                         {  
                              currentVelocity.y = airJumpStrength;  
                              specialFall = false;  
                         }  
                         else  
                         {  
                              currentVelocity.y = -specialFallVelocity;  
                              specialFall = true;  
                         }  
                         currentVelocity.x = targetVelocity;  
                    }  
                    if (Input.GetAxis(playerNumber + "Vertical") >= tapJumpThreshold && lastStickPos.y < tapJumpThreshold && !Input.GetButton(playerNumber + "Root"))  
                    {  
                         airJumpsRemaining--;  
                         currentVelocity.y = airJumpStrength;  
                    }  
               }  
          }  
          controller.Move(new Vector3(currentVelocity.x, currentVelocity.y, 0) * Time.deltaTime);  
          lastStickPos = new Vector2(Input.GetAxis(playerNumber + "Horizontal"), Input.GetAxis(playerNumber + "Vertical"));  
     }  


     void OnControllerColliderHit(ControllerColliderHit hit)  
     {  
          if (hit.moveDirection == Vector3.down)  
          {  
               airJumpsRemaining = airJumpCount;  
               grounded = true;  
               specialFall = false;  
          }  
     }  

As you can see, it's about four times as long, and the process is many times more complicated. In addition, the first implementation took about ninety minutes counting the time I spent looking up how character controllers in Unity are supposed to work. The second one took two or three days of overtime work to experiment with, discover how to do, implement, and debug. So, what do we get for that effort?
  • Super snappy jumps
  • Jumps with many uses, from short hops over projectiles to dodges to rapid direction changes
  • Horizontal movement that feels natural and responsive
  • Horizontal movement that still allows very precise control of speed
  • A very configurable system that can be easily tuned by designer.
  • Movement that is fun to play with and feels great

Which is really quite a lot, and it was definitely worth the effort to do again correctly.

Going forward, I'm doing something similar with our attack editing systems. Right now, adding a new attack takes me about thirty-five minutes, and I'm the only one on the team who can do it. It involves manually creating a prefab and two scripts, adding the new attack to the player prefab, manually adding the attack to the player attack script in four different places, and manually adding the attack to the draft screen (usually making the draft screen larger to fit it, editing that prefab too.) It's a nightmare.

I'm slowly taking on the process of making a new GUI-based editor to simplify the scripting of new attacks. In my dreams, we could integrate this editor into the game as a mod tool and ship with it.


Right now this tool only exists in my head, all I have is this mock-up that I spent far too long on. But it's one of my major goals going forward. I'll talk about it in more detail in the future, I have some things to say about tools programming and tools design.

And as always, you can play at fighting.loulessing.com.