Dec 02, 2018
3D Movement is Hard
I’m a programmer of over a decade with 3D math & gamedev experience. It took me 8+ hours over 4+ weekends to code basic movement with a 3rd person camera and double-jumping to work in Unity. Countless failures & dead-ends. Background doesn’t matter; gamedev takes practice.
I even cheated! It’s not nearly as from-scratch as it could be! It has both a RigidBody, PlayerController, AND uses InControl for the annoying parts. It even uses Transform.LookAt and Transform.Rotate! I referred to other people’s code to get started with jumping! I’ve implemented the basic parts of PlayerController in the past. RigidBody is harder to do from scratch. Control coding is extremely frustrating in Unity without a framework. There were plenty of things here I thought I knew how to do but when I sat down to do it I felt totally lost.
Here’s what I’ve got so far. It’s far from perfect, but that’s kinda the point.
using UnityEngine;
using UnityEngine.SceneManagement;
using InControl;
public class PlayerInput : MonoBehaviour
{
private MyDebugActions debugActions;
private MyCharacterActions characterActions;
private CharacterController controller;
// General Movement variables
public float Speed = 10f;
public float Gravity = -1f;
private Vector3 velocity = Vector3.zero;
// Jumping variables
public float JumpPower = 20f;
public int MaxJumps = 2;
public float AirJumpCoolDown = 0f;
public float GroundedJumpCoolDown = 0f;
private int jumpCount = 0;
private float nextJumpAllowed = 0f;
private bool wasInAir = false;
// Camera variables
public Transform PlayerCamera;
public Transform CameraFocusPoint;
public float CameraDistance = 15f;
public float CameraSpeed = 10f;
public float CameraHeightOffset = 1.5f;
private Vector2 cameraPitchYaw = Vector3.zero;
private void Start()
{
controller = GetComponent<CharacterController>();
velocity = Vector3.zero;
debugActions = new MyDebugActions();
debugActions.ResetScene.AddDefaultBinding( Key.R );
debugActions.ResetScene.AddDefaultBinding( Key.Q );
characterActions = new MyCharacterActions();
characterActions.Up.AddDefaultBinding( Key.UpArrow );
characterActions.Up.AddDefaultBinding( Key.W );
characterActions.Up.AddDefaultBinding( InputControlType.DPadUp );
characterActions.Down.AddDefaultBinding( Key.DownArrow );
characterActions.Down.AddDefaultBinding( Key.S );
characterActions.Down.AddDefaultBinding( InputControlType.DPadDown );
characterActions.Left.AddDefaultBinding( Key.LeftArrow );
characterActions.Left.AddDefaultBinding( Key.A );
characterActions.Left.AddDefaultBinding( InputControlType.DPadLeft );
characterActions.Right.AddDefaultBinding( Key.RightArrow );
characterActions.Right.AddDefaultBinding( Key.D );
characterActions.Right.AddDefaultBinding( InputControlType.DPadRight );
characterActions.CameraUp.AddDefaultBinding( Mouse.PositiveY );
characterActions.CameraUp.AddDefaultBinding( Key.I );
characterActions.CameraDown.AddDefaultBinding( Mouse.NegativeY );
characterActions.CameraDown.AddDefaultBinding( Key.K );
characterActions.CameraLeft.AddDefaultBinding( Mouse.NegativeX );
characterActions.CameraLeft.AddDefaultBinding( Key.J );
characterActions.CameraRight.AddDefaultBinding( Mouse.PositiveX );
characterActions.CameraRight.AddDefaultBinding( Key.L );
characterActions.Jump.AddDefaultBinding( Key.Space );
characterActions.Jump.AddDefaultBinding( InputControlType.Action1 );
characterActions.Interact.AddDefaultBinding( Key.X );
characterActions.Interact.AddDefaultBinding( InputControlType.Action2 );
}
private void DebugUpdate()
{
if (debugActions.ResetScene.WasPressed)
{
SceneManager.LoadScene(SceneManager.GetActiveScene().GetHashCode());
}
if (debugActions.Quit.WasPressed)
{
Application.Quit();
}
}
private void Update()
{
DebugUpdate();
// Update camera angles. Movement is relative to the direction the camera is facing.
UpdateCamera( characterActions.CameraMove.Value.x, characterActions.CameraMove.Value.y );
// Reset velocity while on ground.
// When we're falling, we need to be able to keep gravity between frames.
if (controller.isGrounded)
{
velocity = Vector3.zero;
}
// Update velocity along the X/Z axes
UpdateMove( characterActions.Move.Value.x, characterActions.Move.Value.y );
// Change player rotation if we're moving
if (velocity.sqrMagnitude > 0.1f)
{
Vector3 xzVelocity = new Vector3(velocity.x, 0, velocity.z);
transform.LookAt(transform.position + xzVelocity.normalized); // update angle before giving a y value.
}
// Update velocity along the Y axis
UpdateJump(characterActions.Jump.WasPressed);
// Interaction
if (characterActions.Interact.WasPressed)
{
PerformInteract();
}
// Apply velocity
controller.Move(velocity * Time.deltaTime);
}
private void PerformInteract()
{
Debug.Log("Interact");
}
private void UpdateCamera(float leftRight, float upDown)
{
if (!PlayerCamera || !CameraFocusPoint)
{
return;
}
CameraFocusPoint.localPosition = Vector3.up * CameraHeightOffset;
// TODO this should do a trace instead of my haxx
PlayerCamera.parent = null;
cameraPitchYaw.y += leftRight * CameraSpeed * Time.deltaTime;
cameraPitchYaw.x += upDown * CameraSpeed * Time.deltaTime;
cameraPitchYaw.x = Mathf.Clamp(cameraPitchYaw.x, -40, 40);
// note the up-down angles are not working the way I expect. the rotate about the wrong axis
CameraFocusPoint.eulerAngles = Vector3.zero;
CameraFocusPoint.Rotate(Vector3.up, cameraPitchYaw.y); // leftRight
CameraFocusPoint.Rotate(Vector3.right, cameraPitchYaw.x); // upDown
PlayerCamera.position = CameraFocusPoint.position + CameraFocusPoint.forward * CameraDistance;
PlayerCamera.LookAt(CameraFocusPoint);
}
private void UpdateJump(bool pressed)
{
// if we hit the ground reset double jump count
bool canJump = true;
bool isAirJump = false;
if (controller.isGrounded)
{
// reset air jumps
jumpCount = 0;
isAirJump = false;
// Prevent bunnyhop upon landing
if (wasInAir)
{
nextJumpAllowed = Time.time + GroundedJumpCoolDown;
}
}
else
{
isAirJump = true;
}
// if we've hit our max jump count since we last touched the ground, don't jump
if (jumpCount >= MaxJumps)
{
canJump = false;
}
// if either of the cooldowns are active, don't jump
if (Time.time < nextJumpAllowed)
{
canJump = false;
}
if (pressed)
{
if (canJump)
{
// Prevent air jumping too soon after this
nextJumpAllowed = Time.time + AirJumpCoolDown;
// If we're jumping in the air, make sure we don't have to work against gravity
if (isAirJump)
{
velocity.y = 0;
}
jumpCount++;
velocity.y += JumpPower;
}
}
velocity.y += Gravity;
wasInAir = !controller.isGrounded;
}
private void UpdateMove( float leftRight, float forwardBack )
{
// Get the direction we're trying to move relative to the camera angles
Vector3 direction = PlayerCamera.forward * forwardBack + PlayerCamera.right * leftRight;
// Eliminate the Y-component so we don't apply velocity along the Y axis. That's what jumping is for!
direction.y = 0;
// Normalize after removing Y so we don't get a non-unit vector when we multiply by speed.
direction = direction.normalized;
// It's important not to pollute the Y component, so make a temp variable for velocity along the X-Z plane.
Vector3 xzVelocity = direction * Speed;
// Controls are tight - set velocity directly.
velocity.x = xzVelocity.x;
velocity.z = xzVelocity.z;
}
}
You're welcome to contact me though.