Skip to content

Instantly share code, notes, and snippets.

@JavadocMD
Last active September 17, 2020 08:43
Show Gist options
  • Save JavadocMD/b9d87c639953e19ea1943f550907b9aa to your computer and use it in GitHub Desktop.
Save JavadocMD/b9d87c639953e19ea1943f550907b9aa 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);
}
}
}
@TokyoDan
Copy link

TokyoDan commented Aug 9, 2016

In CameraBobV2 the player.Walked.Subscribe lambda give me this error:

Assets/CameraBob.cs(25,41): error CS1660: Cannot convert lambda expression' to non-delegate typeUniRx.IObserver<UnityEngine.Vector3>'

@seamanmur
Copy link

Controlling the camera in the Player Controller increases code connectivity. Maybe it's better to subscribe the camera on MouseLook and rotate it from the camera script?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment