Skip to content

Instantly share code, notes, and snippets.

@juaxix
Created August 12, 2018 23:17
Show Gist options
  • Save juaxix/bada8c3a436a6d7595c4edd674db21b3 to your computer and use it in GitHub Desktop.
Save juaxix/bada8c3a436a6d7595c4edd674db21b3 to your computer and use it in GitHub Desktop.
OutOfSpace- LudumDare 42
using System;
using UnityEngine;
using System.Collections.Generic;
using System.Linq;
using DG.Tweening;
using UnityEngine.Events;
using Random = UnityEngine.Random;
public class Game : MonoBehaviour
{
public Transform[] personSpawnPoints;
private static Game instance;
public GameObject[] peoplePrefabs;
public int peopleToGenerate = 30;
[Range(0.1f,100f)]
[Tooltip("In seconds")]
public float frequenceToGeneratePeople = 0.4f;
public Player _player;
private List<Person> _persons = new List<Person>();
private bool _allpersonsspawned;
public bool AllPersonsAreSpawned { get { return _allpersonsspawned; } }
[SerializeField] private UnityEvent OnUIGameOverCalled = null;
[SerializeField] private UnityEngine.UI.Text _GameOverText = null;
[SerializeField] private UnityEngine.UI.Text _StatusText = null;
[SerializeField] private UnityEvent OnUIPersonArrested = null;
private Camera _camera;
private Transform _cameraTransform;
public Camera PlayerCamera {get { return _camera; }}
public Transform PlayerCameraTransform { get { return _cameraTransform; }}
private static bool firstRun = true;
public static Game Instance
{
get { return instance != null ? instance : (instance = FindObjectOfType<Game>()); }
}
void Awake()
{
instance = this;
if (_player==null) _player = FindObjectOfType<Player>();
_camera = Camera.main;
_cameraTransform = _camera.GetComponent<Transform>();
GetComponent<AudioSource>();
#if !UNITY_EDITOR
if (firstRun)
{
firstRun = false;
_audioSource.Play();
}
#endif
}
public void GeneratePerson()
{
if (peoplePrefabs.Length == 0)
{
Debug.Log("No people prefabs");
return;
}
int randomIndex = Random.Range(0, peoplePrefabs.Length);
GameObject goperson = Instantiate(peoplePrefabs[randomIndex]) as GameObject;
if (goperson==null)
{
Debug.LogError("Error creating person with prefab index " + randomIndex);
return;
}
_persons.Add(goperson.GetComponent<Person>());
_persons.Last().ChangeState(Person.PersonalSpaceState.WALKING);
}
void AllPersonsSpawned()
{
_allpersonsspawned = true;
}
public void OnPlayerEnteringPersonPrivateSpace(Player player,Person person, Vector3 position)
{
switch (person._state)
{
case Person.PersonalSpaceState.IDLE:
break;
case Person.PersonalSpaceState.WALKING:
break;
case Person.PersonalSpaceState.AFFRAID:
break;
case Person.PersonalSpaceState.INJURIED:
break;
case Person.PersonalSpaceState.ATTACKING:
break;
case Person.PersonalSpaceState.ARRESTED:
break;
default:
throw new ArgumentOutOfRangeException();
}
}
public void OnPlayerExitingPersonPrivateSpace(Player player, Person person, Vector3 point)
{
}
void Start()
{
DOVirtual.DelayedCall(0.4f, GeneratePerson)
.SetLoops(peopleToGenerate)
.SetDelay(frequenceToGeneratePeople)
.OnComplete(AllPersonsSpawned);
}
void Update()
{
for (int i = 0; i < _persons.Count; i++)
{
Person person = _persons[i];
person.UpdateAggroLevel();
person.UpdateAnimationSpeed();
switch (person._state)
{
case Person.PersonalSpaceState.IDLE:
person.ChangeState(Person.PersonalSpaceState.WALKING);
break;
case Person.PersonalSpaceState.WALKING:
if (person._navMeshAgent.velocity.magnitude <= 0.1f &&
Vector3.Distance(person._transform.position, person._navMeshAgent.destination) < 0.2f)
{
person.ChangeState(Person.PersonalSpaceState.IDLE);
}
break;
case Person.PersonalSpaceState.AFFRAID:
break;
case Person.PersonalSpaceState.INJURIED:
break;
case Person.PersonalSpaceState.ATTACKING:
break;
case Person.PersonalSpaceState.ARRESTED:
break;
}
}
}
public void OnPlayerMadeNormalPersonInjuried(Person person)
{
if (_GameOverText != null)
{
_GameOverText.text = "Invaded the personal space of an innocent!";
}
if (OnUIGameOverCalled != null)
{
OnUIGameOverCalled.Invoke();
}
}
public void OnPlayerArrestedPerson(Person person)
{
if (_StatusText != null)
{
_StatusText.text = "Person arrested";
}
if (OnUIPersonArrested != null)
{
OnUIPersonArrested.Invoke();
}
}
}
using UnityEngine;
using System.Collections;
/// MouseLook rotates the transform based on the mouse delta.
/// Minimum and Maximum values can be used to constrain the possible rotation
/// To make an FPS style character:
/// - Create a capsule.
/// - Add the MouseLook script to the capsule.
/// -> Set the mouse look to use LookX. (You want to only turn character but not tilt it)
/// - Add FPSInputController script to the capsule
/// -> A CharacterMotor and a CharacterController component will be automatically added.
/// - Create a camera. Make the camera a child of the capsule. Reset it's transform.
/// - Add a MouseLook script to the camera.
/// -> Set the mouse look to use LookY. (You want the camera to tilt up and down like a head. The character already turns.)
[AddComponentMenu("Camera-Control/Mouse Look")]
public class MouseLook : MonoBehaviour {
public enum RotationAxes { MouseXAndY = 0, MouseX = 1, MouseY = 2 }
public RotationAxes axes = RotationAxes.MouseXAndY;
public float sensitivityX = 15F;
public float sensitivityY = 15F;
public float minimumX = -360F;
public float maximumX = 360F;
public float minimumY = -60F;
public float maximumY = 60F;
float rotationY = 0F;
void Update ()
{
if (axes == RotationAxes.MouseXAndY)
{
float rotationX = transform.localEulerAngles.y + Input.GetAxis("Mouse X") * sensitivityX;
rotationY += Input.GetAxis("Mouse Y") * sensitivityY;
rotationY = Mathf.Clamp (rotationY, minimumY, maximumY);
transform.localEulerAngles = new Vector3(-rotationY, rotationX, 0);
}
else if (axes == RotationAxes.MouseX)
{
transform.Rotate(0, Input.GetAxis("Mouse X") * sensitivityX, 0);
}
else
{
rotationY += Input.GetAxis("Mouse Y") * sensitivityY;
rotationY = Mathf.Clamp (rotationY, minimumY, maximumY);
transform.localEulerAngles = new Vector3(-rotationY, transform.localEulerAngles.y, 0);
}
}
void Start ()
{
// Make the rigid body not change rotation
if (GetComponent<Rigidbody>())
GetComponent<Rigidbody>().freezeRotation = true;
}
}
using System;
using UnityEngine;
using System.Runtime.CompilerServices;
using Random = UnityEngine.Random;
[assembly: InternalsVisibleTo("Game")]
[RequireComponent(typeof(NavMeshAgent))]
[RequireComponent(typeof(Animator))]
[RequireComponent(typeof(AudioSource))]
public class Person : MonoBehaviour
{
[SerializeField]
private Animator _animator;
[SerializeField]
internal NavMeshAgent _navMeshAgent;
[SerializeField]
internal GameObject _FearAura = null;
[SerializeField]
internal GameObject _AngryAura = null;
[SerializeField]
internal GameObject _NormalAura = null;
[SerializeField]
internal GameObject _GoodAura = null;
[SerializeField]
internal AudioSource _audioSource;
[SerializeField]
internal AudioClip _fearSound = null;
[SerializeField]
internal float _walkSpeed = 1.55f;
[SerializeField]
internal float _minimumDistanceToWalk = 40.0f;
[SerializeField]
internal float _walkAcceleration = 8.0f;
[SerializeField]
internal float _runSpeed = 5.0f;
[SerializeField]
internal float _runAcceleration = 9.0f;
[Tooltip("Increment each frame the personal space is entered by the player")]
[SerializeField]
internal float _aggroIncrement = 0.15f;
internal float _aggroLevel;
public Transform _aggroSliderGroup = null;
public Transform _aggroSliderQuad = null;
//[SerializeField]
//internal float _aggroSliderDistanceToCamera = 3.0f;
public enum TypesOfPersons
{
NORMAL,
CRIMINAL
}
public enum PersonalSpaceState
{
IDLE,
WALKING,
AFFRAID,
INJURIED,
ATTACKING,
ARRESTED
}
[SerializeField]
internal PersonalSpaceState _state = PersonalSpaceState.IDLE;
private TypesOfPersons _type = TypesOfPersons.NORMAL;
internal bool _invaded;
internal Transform _transform;
void Awake ()
{
if (_animator == null) _animator = GetComponent<Animator>();
if (_navMeshAgent == null) _navMeshAgent = GetComponent<NavMeshAgent>();
if (_audioSource == null) _audioSource = GetComponent<AudioSource>();
_state = PersonalSpaceState.IDLE;
_transform = GetComponent<Transform>();
_type = (Random.Range(1, 6) >= 5) ? TypesOfPersons.CRIMINAL : TypesOfPersons.NORMAL;
if (_type == TypesOfPersons.CRIMINAL) _aggroIncrement *= 2.0f;
SetActiveAura(null);
SetAggroLevel(0.0f);
WarpToRandomPosition();
enabled = false;
}
internal void SetAggroLevel(float aggrolevel)
{
_aggroLevel = Mathf.Clamp01(aggrolevel);
_aggroSliderQuad.localScale = new Vector3(_aggroLevel, 0.1f, 1.0f);
if (aggrolevel >= 1.0f)
{
_aggroLevel = 0.0f;
switch (_type)
{
case TypesOfPersons.NORMAL:
switch (_state)
{
case PersonalSpaceState.WALKING:
ChangeState(PersonalSpaceState.AFFRAID);
break;
case PersonalSpaceState.AFFRAID:
ChangeState(PersonalSpaceState.INJURIED);
break;
}
break;
case TypesOfPersons.CRIMINAL:
switch (_state)
{
case PersonalSpaceState.WALKING:
ChangeState(PersonalSpaceState.AFFRAID);
break;
case PersonalSpaceState.AFFRAID:
ChangeState(PersonalSpaceState.ATTACKING);
break;
case PersonalSpaceState.ATTACKING:
ChangeState(PersonalSpaceState.ARRESTED);
break;
}
break;
}
} else if (aggrolevel == 0.0f)
{
switch (_type)
{
case TypesOfPersons.NORMAL:
switch (_state)
{
case PersonalSpaceState.IDLE:
break;
case PersonalSpaceState.WALKING:
break;
case PersonalSpaceState.AFFRAID:
break;
case PersonalSpaceState.INJURIED:
break;
case PersonalSpaceState.ATTACKING:
break;
case PersonalSpaceState.ARRESTED:
break;
default:
throw new ArgumentOutOfRangeException();
}
break;
case TypesOfPersons.CRIMINAL:
switch (_state)
{
case PersonalSpaceState.IDLE:
break;
case PersonalSpaceState.WALKING:
break;
case PersonalSpaceState.AFFRAID:
break;
case PersonalSpaceState.INJURIED:
break;
case PersonalSpaceState.ATTACKING:
break;
case PersonalSpaceState.ARRESTED:
break;
default:
throw new ArgumentOutOfRangeException();
}
break;
}
}
}
void WarpToRandomPosition()
{
int startIndex = Random.Range(0, Game.Instance.personSpawnPoints.Length);
_transform.position = Game.Instance.personSpawnPoints[startIndex].position;
_navMeshAgent.Warp(_transform.position);
}
bool GoToRandomPosition()
{
int endIndex;
do
{
endIndex = Random.Range(0, Game.Instance.personSpawnPoints.Length);
} while (Vector3.Distance(Game.Instance.personSpawnPoints[endIndex].position, _transform.position) >
_minimumDistanceToWalk);
return _navMeshAgent.SetDestination(Game.Instance.personSpawnPoints[endIndex].position);
}
internal void UpdateAnimationSpeed()
{
_animator.SetFloat("speed",_navMeshAgent.velocity.magnitude);
}
internal void UpdateAggroLevel()
{
if (_invaded)
{
SetAggroLevel(_aggroIncrement * Time.deltaTime + _aggroLevel);
}
else
{
if (_aggroLevel > 0.0f)
SetAggroLevel(_aggroLevel - _aggroIncrement * Time.deltaTime);
}
_aggroSliderGroup.rotation = Quaternion.LookRotation(Game.Instance.PlayerCameraTransform.position - _aggroSliderGroup.position);
}
public void ChangeState(PersonalSpaceState newState)
{
if (newState == _state) return;
switch (newState)
{
case PersonalSpaceState.WALKING:
_navMeshAgent.speed = _walkSpeed;
_navMeshAgent.acceleration = _walkAcceleration;
bool result = GoToRandomPosition();
if (!result)
{
ChangeState(PersonalSpaceState.IDLE);
}
break;
case PersonalSpaceState.AFFRAID:
SetActiveAura(_FearAura);
if (_type == TypesOfPersons.CRIMINAL)
{
GoToRandomPosition();
_navMeshAgent.speed = _runSpeed;
_navMeshAgent.acceleration = _walkAcceleration;
}
else
{
_navMeshAgent.Stop(true);
}
_audioSource.PlayOneShot(_fearSound);
break;
case PersonalSpaceState.ATTACKING:
SetActiveAura(_AngryAura);
_navMeshAgent.Stop(true);
_animator.SetBool("attacking", true);
break;
case PersonalSpaceState.INJURIED:
SetActiveAura(_GoodAura);
_navMeshAgent.Stop(true);
Game.Instance.OnPlayerMadeNormalPersonInjuried(this);
break;
case PersonalSpaceState.ARRESTED:
SetActiveAura(_FearAura);
_navMeshAgent.Stop(true);
Game.Instance.OnPlayerArrestedPerson(this);
break;
}
_state = newState;
}
private void SetActiveAura(GameObject aura)
{
_FearAura.SetActive(aura == _FearAura);
_AngryAura.SetActive(aura == _AngryAura);
_NormalAura.SetActive(aura == _NormalAura);
_GoodAura.SetActive(aura == _GoodAura);
}
public void OnTriggerEnter(Collider otherCollider)
{
Debug.Log("I am invaded " + otherCollider.name);
if (otherCollider.GetComponent<Player>())
{
Game.Instance.OnPlayerEnteringPersonPrivateSpace(
otherCollider.GetComponent<Player>(),
this,
otherCollider.ClosestPointOnBounds(_transform.position)
);
_invaded = true;
}
}
public void OnTriggerExit(Collider otherCollider)
{
if (otherCollider.GetComponent<Player>())
{
Game.Instance.OnPlayerExitingPersonPrivateSpace(
otherCollider.GetComponent<Player>(),
this,
otherCollider.ClosestPointOnBounds(_transform.position)
);
_invaded = false;
}
}
}
using UnityEngine;
using System.Collections;
[RequireComponent (typeof(CharacterMotor))]
[AddComponentMenu ("Character/Platform Input Controller")]
public class PlatformInputController : MonoBehaviour
{
// This makes the character turn to face the current movement speed per default.
public bool autoRotate = true;
public float maxRotationSpeed = 360.0f;
private CharacterMotor motor;
void Awake () {
motor = GetComponent<CharacterMotor>();
}
void Update () {
// Get the input vector from keyboard or analog stick
Vector3 directionVector = new Vector3(Input.GetAxis("Horizontal"), Input.GetAxis("Vertical"), 0);
if (directionVector != Vector3.zero) {
// Get the length of the directon vector and then normalize it
// Dividing by the length is cheaper than normalizing when we already have the length anyway
float directionLength = directionVector.magnitude;
directionVector = directionVector / directionLength;
// Make sure the length is no bigger than 1
directionLength = Mathf.Min(1, directionLength);
// Make the input vector more sensitive towards the extremes and less sensitive in the middle
// This makes it easier to control slow speeds when using analog sticks
directionLength = directionLength * directionLength;
// Multiply the normalized direction vector by the modified length
directionVector = directionVector * directionLength;
}
// Rotate the input vector into camera space so up is camera's up and right is camera's right
directionVector = Camera.main.transform.rotation * directionVector;
// Rotate input vector to be perpendicular to character's up vector
Quaternion camToCharacterSpace = Quaternion.FromToRotation(-Camera.main.transform.forward, transform.up);
directionVector = (camToCharacterSpace * directionVector);
// Apply the direction to the CharacterMotor
motor.inputMoveDirection = directionVector;
motor.inputJump = Input.GetButton("Jump");
// Set rotation to the move direction
if (autoRotate && directionVector.sqrMagnitude > 0.01)
{
Vector3 newForward = ConstantSlerp(
transform.forward,
directionVector,
maxRotationSpeed * Time.deltaTime
);
newForward = ProjectOntoPlane(newForward, transform.up);
transform.rotation = Quaternion.LookRotation(newForward, transform.up);
}
}
Vector3 ProjectOntoPlane (Vector3 v, Vector3 normal )
{
return v - Vector3.Project(v, normal);
}
public Vector3 ConstantSlerp (Vector3 from , Vector3 to , float angle) {
float value = Mathf.Min(1, angle / Vector3.Angle(from, to));
return Vector3.Slerp(from, to, value);
}
}
using UnityEngine;
public class Player : MonoBehaviour
{
public ThirdPersonController playerController;
public ThirdPersonCamera playerCamera;
}
using UnityEngine;
using System.Collections;
public class ThirdPersonCamera : MonoBehaviour
{
public Transform cameraTransform = null;
private Transform _target = null;
// The distance in the x-z plane to the target
public float distance = 7.0f;
// the height we want the camera to be above the target
public float height = 3.0f;
public float angularSmoothLag = 0.3f;
public float angularMaxSpeed = 15.0f;
public float heightSmoothLag = 0.3f;
public float snapSmoothLag = 0.2f;
public float snapMaxSpeed = 720.0f;
public float clampHeadPositionScreenSpace = 0.75f;
public float lockCameraTimeout = 0.2f;
private Vector3 headOffset = Vector3.zero;
private Vector3 centerOffset = Vector3.zero;
private float heightVelocity = 0.0f;
private float angleVelocity = 0.0f;
private bool snap = false;
private ThirdPersonController controller;
private float targetHeight = 100000.0f;
void Awake ()
{
if(!cameraTransform && Camera.main)
cameraTransform = Camera.main.transform;
if(!cameraTransform) {
Debug.Log("Please assign a camera to the ThirdPersonCamera script.");
enabled = false;
}
_target = transform;
if (_target)
{
controller = _target.GetComponent<ThirdPersonController>();
}
if (controller)
{
Collider characterController = _target.GetComponent<Collider>();
centerOffset = characterController.bounds.center - _target.position;
headOffset = centerOffset;
headOffset.y = characterController.bounds.max.y - _target.position.y;
}
else
Debug.Log("Please assign a target to the camera that has a ThirdPersonController script attached.");
Cut(_target, centerOffset);
}
public void DebugDrawStuff ()
{
Debug.DrawLine(_target.position, _target.position + headOffset);
}
public float AngleDistance (float a, float b)
{
a = Mathf.Repeat(a, 360);
b = Mathf.Repeat(b, 360);
return Mathf.Abs(b - a);
}
public void Apply (Transform dummyTarget,Vector3 dummyCenter)
{
// Early out if we don't have a target
if (!controller)
return;
Vector3 targetCenter = _target.position + centerOffset;
Vector3 targetHead = _target.position + headOffset;
// DebugDrawStuff();
// Calculate the current & target rotation angles
float originalTargetAngle = _target.eulerAngles.y;
float currentAngle = cameraTransform.eulerAngles.y;
// Adjust real target angle when camera is locked
float targetAngle = originalTargetAngle;
// When pressing Fire2 (alt) the camera will snap to the target direction real quick.
// It will stop snapping when it reaches the target
if (Input.GetButton("Fire2"))
snap = true;
if (snap)
{
// We are close to the target, so we can stop snapping now!
if (AngleDistance (currentAngle, originalTargetAngle) < 3.0)
snap = false;
currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, snapSmoothLag, snapMaxSpeed);
}
// Normal camera motion
else
{
if (controller.GetLockCameraTimer () < lockCameraTimeout)
{
targetAngle = currentAngle;
}
// Lock the camera when moving backwards!
// * It is really confusing to do 180 degree spins when turning around.
if (AngleDistance (currentAngle, targetAngle) > 160 && controller.IsMovingBackwards ())
targetAngle += 180;
currentAngle = Mathf.SmoothDampAngle(currentAngle, targetAngle, ref angleVelocity, angularSmoothLag, angularMaxSpeed);
}
// When jumping don't move camera upwards but only down!
if (controller.IsJumping ())
{
// We'd be moving the camera upwards, do that only if it's really high
float newTargetHeight = targetCenter.y + height;
if (newTargetHeight < targetHeight || newTargetHeight - targetHeight > 5)
targetHeight = targetCenter.y + height;
}
// When walking always update the target height
else
{
targetHeight = targetCenter.y + height;
}
// Damp the height
float currentHeight = cameraTransform.position.y;
currentHeight = Mathf.SmoothDamp (currentHeight, targetHeight, ref heightVelocity, heightSmoothLag);
// Convert the angle into a rotation, by which we then reposition the camera
Quaternion currentRotation = Quaternion.Euler (0f, currentAngle, 0f);
// Set the position of the camera on the x-z plane to:
// distance meters behind the target
cameraTransform.position = targetCenter;
cameraTransform.position += currentRotation * Vector3.back * distance;
// Set the height of the camera
cameraTransform.position = new Vector3(cameraTransform.position.x, currentHeight, cameraTransform.position.z);
// Always look at the target
SetUpRotation(targetCenter, targetHead);
}
void LateUpdate ()
{
Apply (transform, Vector3.zero);
}
public void Cut (Transform dummyTarget,Vector3 dummyCenter )
{
float oldHeightSmooth = heightSmoothLag;
float oldSnapMaxSpeed = snapMaxSpeed;
float oldSnapSmooth = snapSmoothLag;
snapMaxSpeed = 10000f;
snapSmoothLag = 0.001f;
heightSmoothLag = 0.001f;
snap = true;
Apply (transform, Vector3.zero);
heightSmoothLag = oldHeightSmooth;
snapMaxSpeed = oldSnapMaxSpeed;
snapSmoothLag = oldSnapSmooth;
}
public void SetUpRotation (Vector3 centerPos,Vector3 headPos)
{
// Now it's getting hairy. The devil is in the details here, the big issue is jumping of course.
// * When jumping up and down we don't want to center the guy in screen space.
// This is important to give a feel for how high you jump and avoiding large camera movements.
//
// * At the same time we dont want him to ever go out of screen and we want all rotations to be totally smooth.
//
// So here is what we will do:
//
// 1. We first find the rotation around the y axis. Thus he is always centered on the y-axis
// 2. When grounded we make him be centered
// 3. When jumping we keep the camera rotation but rotate the camera to get him back into view if his head is above some threshold
// 4. When landing we smoothly interpolate towards centering him on screen
Vector3 cameraPos = cameraTransform.position;
Vector3 offsetToCenter = centerPos - cameraPos;
// Generate base rotation only around y-axis
Quaternion yRotation = Quaternion.LookRotation(new Vector3(offsetToCenter.x, 0, offsetToCenter.z));
Vector3 relativeOffset = Vector3.forward * distance + Vector3.down * height;
cameraTransform.rotation = yRotation * Quaternion.LookRotation(relativeOffset);
// Calculate the projected center position and top position in world space
Ray centerRay = cameraTransform.GetComponent<Camera>().ViewportPointToRay(new Vector3(.5f, 0.5f, 1f));
Ray topRay = cameraTransform.GetComponent<Camera>().ViewportPointToRay(new Vector3(.5f, clampHeadPositionScreenSpace, 1));
Vector3 centerRayPos = centerRay.GetPoint(distance);
Vector3 topRayPos = topRay.GetPoint(distance);
float centerToTopAngle = Vector3.Angle(centerRay.direction, topRay.direction);
float heightToAngle = centerToTopAngle / (centerRayPos.y - topRayPos.y);
float extraLookAngle = heightToAngle * (centerRayPos.y - centerPos.y);
if (extraLookAngle < centerToTopAngle)
{
extraLookAngle = 0.0f;
}
else
{
extraLookAngle = extraLookAngle - centerToTopAngle;
cameraTransform.rotation *= Quaternion.Euler(-extraLookAngle, 0, 0);
}
}
public Vector3 GetCenterOffset ()
{
return centerOffset;
}
}
using UnityEngine;
using System.Collections;
[RequireComponent(typeof(CharacterController))]
public class ThirdPersonController : MonoBehaviour
{
public float walkMaxAnimationSpeed = 0.75f;
public float trotMaxAnimationSpeed = 1.0f;
public float runMaxAnimationSpeed = 1.0f;
public float jumpAnimationSpeed = 1.15f;
public float landAnimationSpeed = 1.0f;
private int personLayer = 0;
private Animator _animator;
public enum CharacterState {
Idle = 0,
Walking = 1,
Trotting = 2,
Running = 3,
Jumping = 4,
}
private CharacterState _characterState;
// The speed when walking
public float walkSpeed = 2.0f;
public float trotSpeed = 4.0f;
public float runSpeed = 6.0f;
public float inAirControlAcceleration = 3.0f;
public float jumpHeight = 0.5f;
public float gravity = 20.0f;
public float speedSmoothing = 10.0f;
public float rotateSpeed = 500.0f;
public float trotAfterSeconds = 3.0f;
public bool canJump = true;
private float jumpRepeatTime = 0.05f;
private float jumpTimeout = 0.15f;
private float groundedTimeout = 0.25f;
// The camera doesnt start following the target immediately but waits for a split second to avoid too much waving around.
private float lockCameraTimer = 0.0f;
// The current move direction in x-z
private Vector3 moveDirection = Vector3.zero;
// The current vertical speed
private float verticalSpeed = 0.0f;
// The current x-z move speed
private float moveSpeed = 0.0f;
// The last collision flags returned from controller.Move
private CollisionFlags collisionFlags;
// Are we jumping? (Initiated with jump button and not grounded yet)
private bool jumping = false;
private bool jumpingReachedApex = false;
// Are we moving backwards (This locks the camera to not do a 180 degree spin)
private bool movingBack = false;
// Is the user pressing any keys?
private bool isMoving = false;
// When did the user start walking (Used for going into trot after a while)
private float walkTimeStart = 0.0f;
// Last time the jump button was clicked down
private float lastJumpButtonTime = -10.0f;
// Last time we performed a jump
private float lastJumpTime = -1.0f;
// the height we jumped from (Used to determine for how long to apply extra jump power after jumping.)
private float lastJumpStartHeight = 0.0f;
private Vector3 inAirVelocity = Vector3.zero;
private float lastGroundedTime = 0.0f;
private bool isControllable = true;
private CharacterController controller = null;
void Awake ()
{
personLayer = LayerMask.NameToLayer("Person");
moveDirection = transform.TransformDirection(Vector3.forward);
_animator = GetComponent<Animator>();
if(!_animator)
Debug.Log("The character you would like to control doesn't have animations. Moving her might look weird.");
controller = GetComponent<CharacterController>();
}
public void UpdateSmoothedMovementDirection ()
{
Transform cameraTransform = Camera.main.transform;
bool grounded = IsGrounded();
// Forward vector relative to the camera along the x-z plane
Vector3 forward = cameraTransform.TransformDirection(Vector3.forward);
forward.y = 0;
forward = forward.normalized;
// Right vector relative to the camera
// Always orthogonal to the forward vector
Vector3 right = new Vector3(forward.z, 0, -forward.x);
float v = Input.GetAxisRaw("Vertical");
float h = Input.GetAxisRaw("Horizontal");
// Are we moving backwards or looking backwards
if (v < -0.2)
movingBack = true;
else
movingBack = false;
bool wasMoving = isMoving;
isMoving = Mathf.Abs (h) > 0.1 || Mathf.Abs (v) > 0.1;
// Target direction relative to the camera
Vector3 targetDirection = h * right + v * forward;
// Grounded controls
if (grounded)
{
// Lock camera for short period when transitioning moving & standing still
lockCameraTimer += Time.deltaTime;
if (isMoving != wasMoving)
lockCameraTimer = 0.0f;
// We store speed and direction seperately,
// so that when the character stands still we still have a valid forward direction
// moveDirection is always normalized, and we only update it if there is user input.
if (targetDirection != Vector3.zero)
{
// If we are really slow, just snap to the target direction
if (moveSpeed < walkSpeed * 0.9 && grounded)
{
moveDirection = targetDirection.normalized;
}
// Otherwise smoothly turn towards it
else
{
moveDirection = Vector3.RotateTowards(moveDirection, targetDirection, rotateSpeed * Mathf.Deg2Rad * Time.deltaTime, 1000);
moveDirection = moveDirection.normalized;
}
}
// Smooth the speed based on the current target direction
float curSmooth = speedSmoothing * Time.deltaTime;
// Choose target speed
//* We want to support analog input but make sure you cant walk faster diagonally than just forward or sideways
float targetSpeed = Mathf.Min(targetDirection.magnitude, 1.0f);
_characterState = CharacterState.Idle;
// Pick speed modifier
if (Input.GetKey (KeyCode.LeftShift) || Input.GetKey (KeyCode.RightShift))
{
targetSpeed *= runSpeed;
_characterState = CharacterState.Running;
}
else if (Time.time - trotAfterSeconds > walkTimeStart)
{
targetSpeed *= trotSpeed;
_characterState = CharacterState.Trotting;
}
else
{
targetSpeed *= walkSpeed;
_characterState = CharacterState.Walking;
}
moveSpeed = Mathf.Lerp(moveSpeed, targetSpeed, curSmooth);
// Reset walk time start when we slow down
if (moveSpeed < walkSpeed * 0.3)
walkTimeStart = Time.time;
}
// In air controls
else
{
// Lock camera while in air
if (jumping)
{
lockCameraTimer = 0.0f;
}
if (isMoving)
inAirVelocity += targetDirection.normalized * Time.deltaTime * inAirControlAcceleration;
}
}
public void ApplyJumping ()
{
// Prevent jumping too fast after each other
if (lastJumpTime + jumpRepeatTime > Time.time)
return;
if (IsGrounded()) {
// Jump
// - Only when pressing the button down
// - With a timeout so you can press the button slightly before landing
if (canJump && Time.time < lastJumpButtonTime + jumpTimeout) {
verticalSpeed = CalculateJumpVerticalSpeed (jumpHeight);
SendMessage("DidJump", SendMessageOptions.DontRequireReceiver);
_animator.SetTrigger("Jump");
}
}
}
public void ApplyGravity ()
{
if (isControllable) // don't move player at all if not controllable.
{
// Apply gravity
//float jumpButton = Input.GetButton("Jump");
// When we reach the apex of the jump we send out a message
if (jumping && !jumpingReachedApex && verticalSpeed <= 0.0)
{
jumpingReachedApex = true;
SendMessage("DidJumpReachApex", SendMessageOptions.DontRequireReceiver);
}
if (IsGrounded ())
verticalSpeed = 0.0f;
else
verticalSpeed -= gravity * Time.deltaTime;
}
}
public float CalculateJumpVerticalSpeed (float targetJumpHeight)
{
// From the jump height and gravity we deduce the upwards speed
// for the character to reach at the apex.
return Mathf.Sqrt(2 * targetJumpHeight * gravity);
}
public void DidJump ()
{
jumping = true;
jumpingReachedApex = false;
lastJumpTime = Time.time;
lastJumpStartHeight = transform.position.y;
lastJumpButtonTime = -10;
_characterState = CharacterState.Jumping;
}
void Update() {
if (!isControllable)
{
// kill all inputs if not controllable.
Input.ResetInputAxes();
}
if (Input.GetButtonDown ("Jump"))
{
lastJumpButtonTime = Time.time;
}
UpdateSmoothedMovementDirection();
// Apply gravity
// - extra power jump modifies gravity
// - controlledDescent mode modifies gravity
ApplyGravity ();
// Apply jumping logic
ApplyJumping ();
// Calculate actual motion
Vector3 movement = moveDirection * moveSpeed + new Vector3 (0, verticalSpeed, 0) + inAirVelocity;
movement *= Time.deltaTime;
// Move the controller
collisionFlags = controller.Move(movement);
// ANIMATION sector
_animator.SetFloat("speed", controller.velocity.sqrMagnitude);
if(_characterState == CharacterState.Jumping)
{
_animator.SetFloat("speed", movement.magnitude);
/*if (!jumpingReachedApex) {
_animator.speed = jumpAnimationSpeed;
} else {
_animator.speed = -landAnimationSpeed;
}*/
}
else
{
if(controller.velocity.sqrMagnitude < 0.1)
{
_animator.SetFloat("speed",0.0f);
}
else
{
if(_characterState == CharacterState.Running)
{
_animator.speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, runMaxAnimationSpeed);
}
else if(_characterState == CharacterState.Trotting) {
_animator.speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, trotMaxAnimationSpeed);
}
else if(_characterState == CharacterState.Walking) {
_animator.speed = Mathf.Clamp(controller.velocity.magnitude, 0.0f, walkMaxAnimationSpeed);
}
}
}
// ANIMATION sector
// Set rotation to the move direction
if (IsGrounded())
{
transform.rotation = Quaternion.LookRotation(moveDirection);
}
else
{
var xzMove = movement;
xzMove.y = 0;
if (xzMove.sqrMagnitude > 0.001)
{
transform.rotation = Quaternion.LookRotation(xzMove);
}
}
// We are in jump mode but just became grounded
if (IsGrounded())
{
lastGroundedTime = Time.time;
inAirVelocity = Vector3.zero;
if (jumping)
{
jumping = false;
SendMessage("DidLand", SendMessageOptions.DontRequireReceiver);
}
}
}
/*public void OnControllerColliderHit (ControllerColliderHit hit )
{
Debug.DrawRay(hit.point, hit.normal);
if (hit.moveDirection.y > 0.01)
return;
}*/
public float GetSpeed () {
return moveSpeed;
}
public bool IsJumping () {
return jumping;
}
public bool IsGrounded () {
return (collisionFlags & CollisionFlags.CollidedBelow) != 0;
}
public Vector3 GetDirection () {
return moveDirection;
}
public bool IsMovingBackwards () {
return movingBack;
}
public float GetLockCameraTimer ()
{
return lockCameraTimer;
}
public bool IsMoving ()
{
return Mathf.Abs(Input.GetAxisRaw("Vertical")) + Mathf.Abs(Input.GetAxisRaw("Horizontal")) > 0.5;
}
public bool HasJumpReachedApex ()
{
return jumpingReachedApex;
}
public bool IsGroundedWithTimeout ()
{
return lastGroundedTime + groundedTimeout > Time.time;
}
public void Reset ()
{
gameObject.tag = "Player";
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment