Skip to content

Instantly share code, notes, and snippets.

@soundyogi
Forked from JavadocMD/UniRxCharacterV2.cs
Created August 14, 2017 18:52
Show Gist options
  • Save soundyogi/7e303e7a82229e66464ec1720d90ec88 to your computer and use it in GitHub Desktop.
Save soundyogi/7e303e7a82229e66464ec1720d90ec88 to your computer and use it in GitHub Desktop.
Developing a first person controller for Unity3D with UniRx: Part 2
using UnityEngine;
using UniRx;
using UniRx.Triggers;
// NOTE: Unity won't actually let you put two MonoBehaviours in one file.
// They're both listed here just for convenience.
namespace Assets.Scripts.v2 {
public class InputsV2 : MonoBehaviour {
// Singleton.
public static InputsV2 Instance { get; private set; }
public IObservable<Vector2> Movement { get; private set; }
public IObservable<Vector2> Mouselook { get; private set; }
public ReadOnlyReactiveProperty<bool> Run { get; private set; }
private void Awake() {
Instance = this;
// Hide the mouse cursor and lock it in the game window.
Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;
// Movement inputs tick on FixedUpdate
Movement = this.FixedUpdateAsObservable()
.Select(_ => {
var x = Input.GetAxis("Horizontal");
var y = Input.GetAxis("Vertical");
return new Vector2(x, y).normalized;
});
// Run while held.
Run = this.UpdateAsObservable()
.Select(_ => Input.GetButton("Fire3"))
.ToReadOnlyReactiveProperty();
// Mouse look ticks on Update
Mouselook = this.UpdateAsObservable()
.Select(_ => {
var x = Input.GetAxis("Mouse X");
var y = Input.GetAxis("Mouse Y");
return new Vector2(x, y);
});
}
}
public abstract class PlayerSignalsV2 : MonoBehaviour {
public abstract float StrideLength { get; }
public abstract IObservable<Vector3> Walked { get; }
}
[RequireComponent(typeof(CharacterController))]
public class PlayerControllerV2 : PlayerSignalsV2 {
public float walkSpeed = 5f;
public float runSpeed = 10f;
public float strideLength = 2.5f;
[Range(-90, 0)]
public float minViewAngle = -60f; // How much can the user look down (in degrees)
[Range(0, 90)]
public float maxViewAngle = 60f; // How much can the user look up (in degrees)
// Implement IPlayerSignalsV2
public override float StrideLength {
get { return strideLength; }
}
private Subject<Vector3> walked; // We get to see this as a Subject
public override IObservable<Vector3> Walked {
get { return walked; } // Everyone else sees it as an IObservable
}
private CharacterController character;
private Camera view;
private void Awake() {
character = GetComponent<CharacterController>();
view = GetComponentInChildren<Camera>();
walked = new Subject<Vector3>().AddTo(this);
}
private void Start() {
var inputs = InputsV2.Instance;
// Handle movement input (WASD-style) with run (Shift).
inputs.Movement
.Where(v => v != Vector2.zero) // We can ignore this if movement is zero.
.Subscribe(inputMovement => {
// Calculate velocity (direction * speed).
var inputVelocity = inputMovement * (inputs.Run.Value ? runSpeed : walkSpeed);
// Translate 2D velocity into 3D player coordinates.
var playerVelocity =
inputVelocity.x * transform.right + // x (+/-) corresponds to strafe right/left
inputVelocity.y * transform.forward; // y (+/-) corresponds to forward/back
// Apply movement.
var distance = playerVelocity * Time.fixedDeltaTime;
character.Move(distance);
// Signal that movement happened.
walked.OnNext(character.velocity * Time.fixedDeltaTime);
}).AddTo(this);
// Handle mouse input (free mouse look).
inputs.Mouselook
.Where(v => v != Vector2.zero) // We can ignore this if mouse look is zero.
.Subscribe(inputLook => {
// Translate 2D mouse input into euler angle rotations.
// inputLook.x rotates the character around the vertical axis (with + being right)
var horzLook = inputLook.x * Vector3.up;
transform.localRotation *= Quaternion.Euler(horzLook * Time.deltaTime);
// inputLook.y rotates the camera around the horizontal axis (with + being up)
var vertLook = inputLook.y * Time.deltaTime * Vector3.left;
var newQ = view.transform.localRotation * Quaternion.Euler(vertLook);
// We have to flip the signs and positions of min/max view angle here because the math
// uses the contradictory interpretation of our angles (+/- is down/up).
view.transform.localRotation = ClampRotationAroundXAxis(newQ, -maxViewAngle, -minViewAngle);
}).AddTo(this);
}
// Ripped straight out of the Standard Assets MouseLook script. (This should really be a standard function...)
private static Quaternion ClampRotationAroundXAxis(Quaternion q, float minAngle, float maxAngle) {
q.x /= q.w;
q.y /= q.w;
q.z /= q.w;
q.w = 1.0f;
float angleX = 2.0f * Mathf.Rad2Deg * Mathf.Atan(q.x);
angleX = Mathf.Clamp(angleX, minAngle, maxAngle);
q.x = Mathf.Tan(0.5f * Mathf.Deg2Rad * angleX);
return q;
}
}
[RequireComponent(typeof(Camera))]
public class CameraBobV2 : MonoBehaviour {
// IPlayerSignals reference configured in the Unity Inspector, since we can
// reasonably expect these game objects to be in the same hierarchy
public IPlayerSignalsV2 player;
public float walkBobMagnitude = 0.05f;
public float runBobMagnitude = 0.10f;
public AnimationCurve bob = new AnimationCurve(
new Keyframe(0.00f, 0f),
new Keyframe(0.25f, 1f),
new Keyframe(0.50f, 0f),
new Keyframe(0.75f, -1f),
new Keyframe(1.00f, 0f));
private Camera view;
private Vector3 initialPosition;
private void Awake() {
view = GetComponent<Camera>();
initialPosition = view.transform.localPosition;
}
private void Start() {
var distance = 0f;
player.Walked.Subscribe(w => {
// Accumulate distance walked (modulo stride length).
distance += w.magnitude;
distance %= player.StrideLength;
// Use distance to evaluate the bob curve.
var magnitude = InputsV2.Instance.Run.Value ? runBobMagnitude : walkBobMagnitude;
var deltaPos = magnitude * bob.Evaluate(distance / player.StrideLength) * Vector3.up;
// Adjust camera position.
view.transform.localPosition = initialPosition + deltaPos;
}).AddTo(this);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment