Basic Player Controller Development
The player controller is the first component written in any project, and the first one you'll rewrite if the architecture was chosen hastily. "Basic" doesn't mean "simple": a well-designed 3D character controller includes minimum six interacting systems—input, ground detection, velocity management, states, animation, and camera.
Choosing the Foundation: CharacterController vs Rigidbody
CharacterController is Unity's kinematic primitive. Its Move(Vector3 motion) method moves the capsule with collision resolution, but doesn't participate in the physics engine: forces don't act on it, it doesn't push Rigidbody objects (without custom code) and doesn't receive impulses. For most action games this is a plus: movement is predictable and doesn't depend on physicsStep.
Rigidbody is a full physics object. Control via velocity or AddForce allows natural world interactions: character rolls downhill, explosions push them, they interact with Joint objects. The price is control complexity: without proper PhysicMaterial (frictionCombine = Minimum, dynamicFriction = 0 on capsule) the character gets stuck on geometry edges.
Practical rule: CharacterController for platformers and action-RPG, Rigidbody for games with physically significant environment (racing, shooters with ragdoll interaction, VR).
Controller Code Structure
A typical mistake—one monolithic PlayerController : MonoBehaviour at 800 lines mixing input, physics, animation, and state logic. Reusing such a component is impossible.
Correct decomposition:
-
PlayerInputHandlerreadsInput System(new InputSystem viaPlayerInputcomponent or directlyInputAction), writes toPlayerInputDatastruct:moveDirection,jumpPressed,sprintHeld,aimPosition -
PlayerMovement : MonoBehaviourreadsPlayerInputData, manages movement and velocity -
PlayerAnimationController : MonoBehaviourreads velocity and states, managesAnimatorparameters -
PlayerCameraController : MonoBehaviourindependently from movement, works withCinemachine Virtual Camera
Data between components passes via shared PlayerState struct or events—not direct component references.
Ground Detection and Jump Management
CharacterController.isGrounded returns false descending inclined surface on some frames—a Unity engine bug present since 5. Reliable solution: additional Physics.SphereCast downward from capsule center with radius 0.9 * capsuleRadius and distance groundCheckDistance. Result is cached in isGrounded flag used everywhere.
Jump is implemented via direct vertical velocity management in Vector3 velocity buffer:
if (isGrounded && jumpPressed)
velocity.y = Mathf.Sqrt(jumpHeight * -2f * gravity);
velocity.y += gravity * Time.deltaTime;
characterController.Move(velocity * Time.deltaTime);
Gravity is applied each frame via deltaTime—this gives physically correct free-fall acceleration. The gravity value lives in MovementSettings ScriptableObject and can differ from Physics.gravity.y for artistic feel control.
Animator Integration
Animator is managed via parameters, not direct Play() calls. Parameters update in PlayerAnimationController each frame:
-
Speed(float)—magnitude of horizontal velocity, normalized to maxSpeed -
IsGrounded(bool)—from ground detection -
VerticalVelocity(float)—velocity.y, used for blending fall/jump animations
For locomotion use Blend Tree by Speed parameter: Idle → Walk → Run. This is smoother than three separate states with threshold transitions, and requires no manual transition condition tuning.
For character rotation toward movement direction—Quaternion.RotateTowards(current, target, rotationSpeed * Time.deltaTime), not LookAt(): the latter teleports rotation in one frame.
Camera: Cinemachine FreeLook
For 3D TPS controller CinemachineFreeLook with three rigs (Top, Middle, Bottom)—standard choice. Camera follows CameraTarget—empty transform smoothly following the character via SmoothDamp. This prevents camera jitter on uneven geometry.
CinemachineCollider extension resolves camera penetration into geometry—mandatory for any 3D game with enclosed spaces.
Timeline Guidelines
| Complexity | Components | Timeframe |
|---|---|---|
| Simple 2D | Movement, jump, sprite flip | 1–3 days |
| 3D basic | CharacterController, jump, Cinemachine, Blend Tree | 4–7 days |
| 3D full | + dash, crouch, wall interactions, camera lock-on | 2–3 weeks |
| Network replication | + Netcode for GameObjects / Mirror sync | +1–3 weeks |
Development Process
Start with prototype without animations: just capsule, movement, jump, Camera Follow. Takes a day and lets you nail feel before animators invest time. After feel approval—Animator integration, then edge cases: moving platforms, inclined surfaces, scene transitions keeping speed.





