An accurate physics-based model will feel muddy. Players are used to pressing a direction input and seeing their character move immediately without waiting for forces to propagate through the physics simulation. That’s why CharacterController exists. Responsive player controls aren’t physically accurate. You can do something similar with a rigidbody character by setting it kinematic. If you bypass physics for your player, though, you need to simulate parts of it yourself such as gravity.
Stepping back to the bigger picture, consider using layered FSMs. It’ll make your controller much simpler and more extensible. They work like the layered FSMs in an Animator Controller except at a higher conceptual level, encapsulating states such as crouching, falling, reloading, etc. The active character states will control the animator controller.
For example, you might have a base layer with these states:
-
Idle
-
Moving
-
Sprinting
-
Climbing
-
Jumping
-
Falling
-
etc.
When the character is in a state, you only need to check the inputs and transitions that are valid in that state. For example, in the base layer’s Falling state my character can’t sprint, crouch, jump, climb, etc. The only transitions are landing or dying. So the implementation of this state is really short and clear: keep playing the falling animation, and check for landing or dying. If the character becomes grounded, transition to the Landing state. When in the Landing state, check for the landing animation to finish. When it’s done, transition to the Idle state.
Higher layers (such as upper body) will probably need to check transitions based on input and on lower states. For example, say the character’s upper body layer is in the Reloading state. When the base layer changes to Falling, the upper body layer might transition to a FlappingArms state. The FlappingArms state might exclude any transitions to combat-related upper body states such as firing or reloading. When the base layer changes to Landing, transition the upper body to its upper body Idle state.
Also, use a virtual controller. Don’t read input directly (e.g., no Input.GetKey) except for in scripts whose only purpose is to read input. Your script layers should look something like this:
- [PlayerInput] (reads Input.GetKey() etc., sets virtual controller inputs)
- [VirtualController]
- [HighLevelCharacterController] (reads virtual controller, maintains FSM)
- [HighLevelAnimationController]
- [Animator]
This isolates the character controller from the input method. If you end up using a gamepad, or an Ouya controller, or AI control, or some other input method, you only need to swap out [PlayerInput] and everything below it will work fine without needing any modification.