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.

Tuesday, October 1, 2013

Platform Combat!

After a great deal of internal debate, we have decided to move forward with the prototype I have been working on, instead of the one about moles. This makes me happy.

My team's trying to turn it into a game about chefs having a food fight, which I think is interesting, but I'm not entirely sold. I'm all for humor, but I don't think that's as funny as everybody else seems to. Maybe I lack a proper appreciation of puns.

We have support for up to four players, although playing free-for-all on anywhere but Temple turns into a hot mess pretty quickly. We desperately need more 4 player stages, we're all sick of playing nothing but temple.

I've also reworked movement, which I'll write about in-depth later tonight, and added seven new moves, bringing us up to a total of thirteen.

We have a new team name! We are now Quadratic Magic Industries. I like this name. It's hard to say but pleasant to hear, I think.

We haven't begun to think of a name for the game, but we're actively searching for a good description of the genre. Right now we're working with "combat platformer." I'd like to get away from the associations of fighting games, with characters, combo lists, thousands of frames of animation, giant scope issues, etc. A team tried and basically failed to make a traditional fighter here last year, which is part of why I don't want us to be associated with the fighting genre, but it's really mostly because I don't think the term is accurate.




We've been recruiting people for a competitive testing initiative. In the next few weeks, we're going to try to build the infrastructure to support at least a handful of expert players. I think this will be valuable going forward (Random testers are great for first impressions and bug hunting, but I don't think they're a good substitute for people who actually understand how to play a game.) Plus, as a semi-competitive gamer myself, I think it's exciting.

All in all, it's been a very successful week-and-change.

 I've added links to my teammates blogs in the sidebar, you should check them out!

We've registered quadraticmagic.com, but right now it's a GoDaddy "Under Construction" page so you probably shouldn't go there. I'm going to try to get Trenholme to point it to the prototype in the meantime.

The prototype, as always, resides at fighting.loulessing.com. It should be updated to the new version (1-4 players, 13 moves) any minute now.

I got sick of looking at a pink screen for our main menu, so I thew something together for it. here's a still of that background. Have a wallpaper.


Thursday, September 19, 2013

Stages!


I've made some quick updates to the prototype.

Move drafting is finished, there are now six moves that are reasonably balanced. (Reasonably balanced means that every move except the blue Line Shot was called OP by at least one person in testing, and I think every move was called trash at least once as well.)

We have five stages now, which is partly an attempt to stop the disengage problems we were having.

This is Classic. We were having problems with players engaging to melee so they could hit every move, and then being unable to disengage. We've since realized that this is at least partly because this stage is a subtle pit, and the one platform provides a roof that effectively makes it so that if two people meet in the middle of the map, the only way for one of them to leave is exactly the way they came, which involves moving uphill.

This is Lava Platforms. It doesn't have that issue, but testers don't seem to like it. Maybe it's too dangerous, maybe it's too hard to navigate without blink. (Right now you can't jump through the platforms from beneath.)







This is Rage Cage. It's our least subtle, and most innovative, attempt to solve the disengage issue. Quite simply, there's a wall across the middle of the stage. You can't close to melee at all without Blink. This stage tested extremely well, so we may continue with this idea. I think at very least we'll ship with some 'Cage' stages if we move forward with this idea.



This is Final Destination. Named in reference to Super Smash Bros. Melee, it is the simplest possible stage, more or less, and it's amazing how much difference not having the platform makes. You can dodge attacks with jumps. You can run away, you can jump over the other player, you can reposition yourself in ways that aren't possible at all on Classic. Right now I'm pretty sure Classic is just terrible.


This is Giant Temple. It is more or less the platform layout (and loosely scale) of Hyrule Temple, again from Super Smash Bros. Melee. I wanted to put in a big stage with room to explore and fight in different environments, but I didn't want to spend too long building it (I am not a level designer, nor did I have one on hand) so I swiped an existing map layout. It plays surprisingly well, and is the only stage where all six attacks feel equally powerful to me. Blinking downward through platforms for an un-chaseable escape is a lot of fun. This stage is roughly four times as large as the others, in case scale is unclear.

This prototype is playable online now, go to fighting.loulessing.com to play. It requires two players and two Xbox 360 controllers plugged in. It still controls like crap, I'm going to rework movement before Tuesday.