Wednesday, August 21, 2013

Turncoat Dev Diary: Prototyping Player Game Mechanics, Episode II


As I started working on the basics of movement, I decided that my prototyping model needed a little something extra. I want my character controller to support "physics bones", which are bones that aren't pre-animated, but instead are controlled by the physics engine. You might use physics bones if a character has a pony tail, for example, so that the pony tail moves naturally. If a character has an item hanging from their belt, you might put a physics bone on it to make the item bounce around as the character walks. You can also use physics bones to fake cloth and hair physics. The results aren't as good as you get from true physical simulations, but those are often too processor intensive to do in real time, especially on mobile devices. You can get surprisingly good results by faking more complex simulations using a number of constrained physics bones.


In order to ensure physics bones work with my controller, I went back and added a pony tail to my prototyping character. Well, more like a pony-spike-out-the-back-of-the-head, but it'll work.


Because these bones are not standard parts of a bipedal character, they're not part of any of the animations I have. By default, bones that aren't part of an animation just move along with the nearest ancestor bone that is part of the animation. In our case, that's the head bone, and the result is less than realistic.


To get the pony tail to behave the way I want it to behave, I have to add colliders to each of the new bones and make them all part of the physics system. It was a little tedious getting the colliders set up for the pony tail. I have no idea why Unity places colliders, by default, at the head of bones rather than at the mid-point of the bone. While it might make some sense on paper since the head of the bone is its pivot point, in practice, you're almost always going to want the collider to be placed so it covers the length of the bone. If the colliders defaulted to the midpoint of the bone (halfway between the tail and head), it would take a lot less time to set up physics bones and skeleton colliders.


Once I placed all the colliders and added rigid body physics to each of the bones, I fired it up to try it out. The bones were definitely affected by gravity, just not in the way I wanted; the pony tail fell right to the ground and bounced around on the floor because I forgot to connect the bones to each other with joints.


Joint components are Unity's way of telling the physics engine that certain things should stick together. There are several types of joints in Unity, but the one I want here is called a Character Joint. Adding a character joint to each of the pony tail bones will keep them connected to each other, but will allow them to swing and twist within set limits. After some playing around, I came up with these values for my pony tail.


If this were a real model, and not just a prototyping one, I'd probably spend a lot more time tweaking these values to get them just right. Since all I really care about right now whether parts of the character can interact with the physics engine properly, it doesn't make sense to spend a lot of time tweaking these parameters. Testing it out, it looks okay. Or, at least, it looks okay as I can tell without being able to move the character around.


I guess I know what I need to do next: basic movement controls. I'm eventually going to work on touch controls, since this will be released on iPad, but for now, I'm just going to use keyboard and joystick to get the animations working. Translating these types of controls to touch is, I think, going to be a fairly time-consuming task, so I want to tackle that by itself separately.

Unity's Mecanim system lets you automatically transition between different animations or even combine animations by building a state machine. You can pass different parameters into this state machine from your code and set it up to transition between various animations based on the values you pass in.

I started with a state machine provided by Mixamo with one of the motion packs that I bought from the Unity Asset Store, but I had to fix quite a few things to get it functioning to my satisfaction, then I expanded on it to get the basics of movement covered. This state machine includes stand, turn in place, walk forward, walk backward, run forward, run backward, jump in place, and jump while walking.

Here's what it looks like in Unity's editor:


I'll have to add stealth movement, crawling, and taking cover to my state machine later, but I want to get these basics working well before I start on the more complex parts. Down in the lower left corner, you can see the parameters that can be passed into my state machine. Here's how a parameter gets passed in from code:

Animator anim = GetComponent();   
float vertical = Input.GetAxis("Vertical");
anim.SetFloat("Speed", vertical);   

Pretty straightforward. Just grab a reference to the animator component that represents this state machine and use SetFloat, SetBool, SetInteger, etc. to pass in whatever value is needed.

The orange Idle state in the middle of the picture above is orange because it's the default state. That means that when the level starts, the character will immediately start animating using the Idle animation, which is just a short looping animation of standing still. Using an animation for the idle state is more natural looking than just having the character stay frozen in place when not moving.

Every white line in the state machine is a transition that defines when the character should switch from one animation to a different animation.  For example, if the Speed parameter becomes greater than 0.1, the character will automatically transitions from the Idle animation to Forward Locomotion because that's the condition I've specified for it:


This ability to move between animations based on parameters is pretty neat in and of itself, but this system is even more powerful than that. Forward Locomotion isn't actually an animation the way Idle is. It's what Unity calls a Blend Tree, which is a grouping of animations that can be interpolated together to create new animations based on input parameters, to create new animations. This allows you to take, for example, a walking and a running animation, and blend them together to create a fast walk animation. Here's the blend tree called Forward Locomotion that gets fired when a character starts moving forward:


This blend tree takes two parameters - Run and Direction. The Direction parameter goes from -1.0, representing turning as far left as possible, to 1.0, which represents turning as far to the right as possible.  Run, similarly, goes from -1.0 representing full backwards motion, to 1.0, which is full forward motion. This particular tree will only ever get Run values between 0.0 and 1.0, however, because there's a separate tree for handling backwards movement.

This tree will automatically create new animations by mixing the run and walk animations together based on the value of the Run parameter. If Run is 0.0, the character will walk. If Run is 1.0, they will run. For any value in-between, it will mix the two animations to create something between a run and a walk. Similarly, the tree will also interpolate between the walking forward and walking left or right animations based on the Direction value so that the character turns left or right naturally. All the hard work of interpolating between multiple animations is done for you. You just pass in two parameters in and everything else gets handled for you based on the way the state machine is set up.

It's a pretty great system, and I'm mostly happy with the basic locomotion as it exists right now. There's a few things I don't like, however.

The first is that the arc left and arc right animations I have from Mixamo all seem to have a problem. I'm not exactly sure what the root cause of the problem is because I don't have access to the source animations, but whenever you transition into running or walking all the way to the left or right, the camera starts moving in a jerky, accordion-like manner that just looks terrible. It's not as noticeable with the walk animations, but when you start running, it's really noticeable.

But that's not a problem with the controller or state machine, it's a problem with the animations themselves, and I can replace those at any point. I'm not going to worry about this problem for now. I'm just going to make a note to find replacement animations that don't have this problem or else to buy the full version of these animations and fix the problem myself. If I can't do either of those, then I'll get rid of the Direction part of the blend tree and turn the character left and right in code, which is the traditional way of turning a character that uses an animated walk cycle.

The other problem that I notice is that there's no way to do a standing forward jump. The state machine jumps forward if you're already moving forward and jumps straight up in place otherwise. If the forward button is pressed (or the joystick is pushed forward), but you haven't hit that 0.1 Speed threshold yet, you jump straight up, which doesn't feel like correct behavior to me.
Are you wondering why there's both Speed and RunSpeed represents the actual value taken from the Y-axis of the joystick (if not using a joystick, Speed is 1.0 if the forward button is pressed, 0.0 otherwise). Run, on the other hand, is a calculated acceleration value that builds up over time based on SpeedSpeed is used to determine when to transition to a forward or backward movement, but Run is used to interpolate between the walk and run animations in a natural manner.
My first thought for fixing this was to simply change the transition to jump forward if Speed is greater than 0.0 rather than 0.1, but I realized that 0.1 threshold wasn't the problem. The problem is that there's currently no transition that goes directly from Idle to Running Jump. To fix this, I need to add a new transition from Idle to Running Jump, and then make sure the transitions to the jump states are never ambiguous. I also needed to add transitions back from Running Jump to Idle if the character's not longer moving forward, otherwise there's a little stutter step as the state machine has to go back first to ForwardLocomotion and then to Idle. Here's what my state machine looked like after tweaking the basic movement:


Happy with the basic movements, I decided to take my character out for a stroll around the level. She knocked over some barrels, which told me she was interacting with the physics engine appropriately, but when I got to the stairs or the ramp, she wouldn't climb.

Making my character part of the physics system isn't enough to get her to walk up stairs or slopes. The physics engine will keep her from walking through walls, but it won't handle changes in elevation. Dealing with that requires some logic.

There's two possible ways to fix this. I can write code to handle elevation changes needed to properly traverse stairs and slopes, or I can use the provided CharacterController class, which already contains code to deal with slopes and stairs.

My favorite kind of code is the kind of code you don't have to write at all, so I decided to try using the provided class before setting off to write my own physics-friendly replacement. Unfortunately, this particular class is the one I talked about in the last post that turns your character into a giant floating pill in the physics engine. Once I added a CharacterController component to my character, it instantly started doing wacky things like walking on air.

I added the character to the same Player Collision layer as the bone colliders. Because that layer is set to not interact with itself, it stopped the problem. Suddenly, my character could walk up stairs, climb slopes, and just generally move around the level pretty naturally. But, there were new problems. For one thing, my physics bones in the pony tail weren't bouncing off the character's back, they were passing right through them. For another, I was back to interacting with the physics objects like the barrels on my test level, as if I was a giant valium pill instead of bipedal creature.

I tried moving the physics bones in the pony tail to a different layer. That caused them to begin bouncing off the other bone colliders, which is good, but it also caused them to interact with the character controller, which is bad, because it puts us back to crazy stuttery walk-on-air time.

All is not lost, though. Taking a step back to think about what should interact with what, I realized there was a way to make this work. I don't want any of the bone colliders - neither the physics bones, nor the regular ones - to interact with the CharacterController because that causes weird, undesirable behavior. I also don't want the CharacterController interacting with props or moveable items because I went through all the effort of setting up bone colliders to do that. I do, however, want the CharacterController to interact with the terrain, but I don't really want all the bone colliders to do that because it would be redundant.

That means all I need to do to get this working is to add a few more layers. If I move the CharacterController to a different layer than the bone colliders and put the physics bones back on the same layer as the rest of the bone colliders, then create separate layers for the terrain and moveable objects, I can then use the physics preferences to make everything work correctly.


With these settings, terrain is handled by the character controller, props are handled by the bone colliders and ne'er the two shall meet. Props and terrain still interact with each other, which is necessary because the props would fall through the floor if they didn't.

This seems like it should work. Let's see if it does.


Ahhhh....


That's not a bad start as far as I'm concerned. Time to start working on more advanced movement, like taking cover, as well as switching between first and third person perspective. But not today.

Next Up: Prototyping Player Game Mechanics, Episode III
Previous: Prototyping Player Game Mechanics, Episode I

No comments:

Post a Comment