Check out the code below the videos
Check out the video above to hear the explanation and see the code in action.
Time Breakdown:
0:48 - Download the Sample Assets package
1:35 - Importing only what we need
2:52 - Setting up our scene with the prefabs and settings we'll need
4:43 - Demonstration of touch joystick as-is before we edit it
5:47 - Fixing the joystick so it doesn't snap to the bottom right corner
6:55 - Quick tip: Adding custom Task tokens for comments
8:02 - Clamping the joysticks in a circle rather than a square
10:00 - Using the joystick as a slider instead
10:43 - Explanation of how the joystick GetAxis and how you can use it for other purpose, not just a FPS controller
13:13 - Adding the Look joystick in Unity
14:33 - Difference between GetAxis and GetAxisRaw
15:25 - Editing RotateView to work with our touch joystick
18:57 - Checking and limiting camera angles
23:42 - Drawn explaination of camera angles and when we need to limit them
28:50 - Demonstration of the look joystick working in Unity
30:47 - (Optional) Dampening the vertical look speed slightly
31:43 - Demonstration of everything working on Android
32:50 - Outro, thanks for watching :D
1:35 - Importing only what we need
2:52 - Setting up our scene with the prefabs and settings we'll need
4:43 - Demonstration of touch joystick as-is before we edit it
5:47 - Fixing the joystick so it doesn't snap to the bottom right corner
6:55 - Quick tip: Adding custom Task tokens for comments
8:02 - Clamping the joysticks in a circle rather than a square
10:00 - Using the joystick as a slider instead
10:43 - Explanation of how the joystick GetAxis and how you can use it for other purpose, not just a FPS controller
13:13 - Adding the Look joystick in Unity
14:33 - Difference between GetAxis and GetAxisRaw
15:25 - Editing RotateView to work with our touch joystick
18:57 - Checking and limiting camera angles
23:42 - Drawn explaination of camera angles and when we need to limit them
28:50 - Demonstration of the look joystick working in Unity
30:47 - (Optional) Dampening the vertical look speed slightly
31:43 - Demonstration of everything working on Android
32:50 - Outro, thanks for watching :D
Check out the video above to hear the explanation and see the code in action.
/* * This script is from the 4.6 Sample Assets with edits from Devin Curry * Search for changes tagged with the //DCURRY comment * Watch the tutorial here: www.Devination.com */ using UnityEngine; using UnityEngine.EventSystems; using UnitySampleAssets.CrossPlatformInput; public class Joystick : MonoBehaviour , IPointerUpHandler , IPointerDownHandler , IDragHandler { public int MovementRange = 100; public enum AxisOption { // Options for which axes to use Both, // Use both OnlyHorizontal, // Only horizontal OnlyVertical // Only vertical } public AxisOption axesToUse = AxisOption.Both; // The options for the axes that the still will use public string horizontalAxisName = "Horizontal";// The name given to the horizontal axis for the cross platform input public string verticalAxisName = "Vertical"; // The name given to the vertical axis for the cross platform input private Vector3 startPos; private bool useX; // Toggle for using the x axis private bool useY; // Toggle for using the Y axis private CrossPlatformInputManager.VirtualAxis horizontalVirtualAxis; // Reference to the joystick in the cross platform input private CrossPlatformInputManager.VirtualAxis verticalVirtualAxis; // Reference to the joystick in the cross platform input void Start () {//DCURRY changed this to Start from OnEnable startPos = transform.position; CreateVirtualAxes (); } private void UpdateVirtualAxes (Vector3 value) { var delta = startPos - value; delta.y = -delta.y; delta /= MovementRange; if(useX) horizontalVirtualAxis.Update (-delta.x); if(useY) verticalVirtualAxis.Update (delta.y); } private void CreateVirtualAxes() { // set axes to use useX = (axesToUse == AxisOption.Both || axesToUse == AxisOption.OnlyHorizontal); useY = (axesToUse == AxisOption.Both || axesToUse == AxisOption.OnlyVertical); // create new axes based on axes to use if (useX) horizontalVirtualAxis = new CrossPlatformInputManager.VirtualAxis(horizontalAxisName); if (useY) verticalVirtualAxis = new CrossPlatformInputManager.VirtualAxis(verticalAxisName); } public void OnDrag(PointerEventData data) { Vector3 newPos = Vector3.zero; if (useX) { int delta = (int) (data.position.x - startPos.x);//DCURRY deleted clamp newPos.x = delta; } if (useY) { int delta = (int)(data.position.y - startPos.y);//DCURRY deleted clamp newPos.y = delta; } //DCURRY added ClampMagnitude transform.position = Vector3.ClampMagnitude( new Vector3(newPos.x , newPos.y , newPos.z), MovementRange) + startPos; UpdateVirtualAxes (transform.position); } public void OnPointerUp(PointerEventData data) { transform.position = startPos; UpdateVirtualAxes (startPos); } public void OnPointerDown (PointerEventData data) { } void OnDisable () { // remove the joysticks from the cross platform input if (useX) { horizontalVirtualAxis.Remove(); } if (useY) { verticalVirtualAxis.Remove(); } } }
And this is the modified FirstPersonController script
/* * This script is from the 4.6 Sample Assets with edits from Devin Curry * Search for changes tagged with the //DCURRY comment * Watch the tutorial here: www.Devination.com */ using UnityEngine; using UnitySampleAssets.CrossPlatformInput; using UnitySampleAssets.Utility; namespace UnitySampleAssets.Characters.FirstPerson { [RequireComponent(typeof (CharacterController))] [RequireComponent(typeof (AudioSource))] public class FirstPersonController : MonoBehaviour { //////////////////////// exposed privates /////////////////////// [SerializeField] private bool _isWalking; [SerializeField] private float walkSpeed; [SerializeField] private float lookSpeed = 4;//DCURRY add [SerializeField] private float runSpeed; [SerializeField] [Range(0f, 1f)] private float runstepLenghten; [SerializeField] private float jumpSpeed; [SerializeField] private float stickToGroundForce; [SerializeField] private float _gravityMultiplier; [SerializeField] private MouseLook _mouseLook; [SerializeField] private bool useFOVKick; [SerializeField] private FOVKick _fovKick = new FOVKick(); [SerializeField] private bool useHeadBob; [SerializeField] private CurveControlledBob _headBob = new CurveControlledBob(); [SerializeField] private LerpControlledBob _jumpBob = new LerpControlledBob(); [SerializeField] private float _stepInterval; [SerializeField] private AudioClip[] _footstepSounds; // an array of footstep sounds that will be randomly selected from. [SerializeField] private AudioClip _jumpSound; // the sound played when character leaves the ground. [SerializeField] private AudioClip _landSound; // the sound played when character touches back on ground. ///////////////// non exposed privates ///////////////////////// private Camera _camera; private bool _jump; private float _yRotation; private CameraRefocus _cameraRefocus; private Vector2 _input; private Vector3 _moveDir = Vector3.zero; private CharacterController _characterController; private CollisionFlags _collisionFlags; private bool _previouslyGrounded; private Vector3 _originalCameraPosition; private float _stepCycle = 0f; private float _nextStep = 0f; private bool _jumping = false; // Use this for initialization private void Start() { _characterController = GetComponent<CharacterController>(); _camera = Camera.main; _originalCameraPosition = _camera.transform.localPosition; _cameraRefocus = new CameraRefocus(_camera, transform, _camera.transform.localPosition); _fovKick.Setup(_camera); _headBob.Setup(_camera, _stepInterval); _stepCycle = 0f; _nextStep = _stepCycle/2f; _jumping = false; } // Update is called once per frame private void Update() { RotateView(); // the jump state needs to read here to make sure it is not missed if (!_jump) _jump = CrossPlatformInputManager.GetButtonDown("Jump"); if (!_previouslyGrounded && _characterController.isGrounded) { StartCoroutine(_jumpBob.DoBobCycle()); PlayLandingSound(); _moveDir.y = 0f; _jumping = false; } if (!_characterController.isGrounded && !_jumping && _previouslyGrounded) { _moveDir.y = 0f; } _previouslyGrounded = _characterController.isGrounded; } private void PlayLandingSound() { audio.clip = _landSound; audio.Play(); _nextStep = _stepCycle + .5f; } private void FixedUpdate() { float speed; GetInput(out speed); // always move along the camera forward as it is the direction that it being aimed at Vector3 desiredMove = _camera.transform.forward*_input.y + _camera.transform.right*_input.x; // get a normal for the surface that is being touched to move along it RaycastHit hitInfo; Physics.SphereCast(transform.position, _characterController.radius, Vector3.down, out hitInfo, _characterController.height/2f); desiredMove = Vector3.ProjectOnPlane(desiredMove, hitInfo.normal).normalized; _moveDir.x = desiredMove.x*speed; _moveDir.z = desiredMove.z*speed; if (_characterController.isGrounded) { _moveDir.y = -stickToGroundForce; if (_jump) { _moveDir.y = jumpSpeed; PlayJumpSound(); _jump = false; _jumping = true; } } else { _moveDir += Physics.gravity*_gravityMultiplier; } _collisionFlags = _characterController.Move(_moveDir*Time.fixedDeltaTime); ProgressStepCycle(speed); UpdateCameraPosition(speed); } private void PlayJumpSound() { audio.clip = _jumpSound; audio.Play(); } private void ProgressStepCycle(float speed) { if (_characterController.velocity.sqrMagnitude > 0 && (_input.x != 0 || _input.y != 0)) _stepCycle += (_characterController.velocity.magnitude + (speed*(_isWalking ? 1f : runstepLenghten)))* Time.fixedDeltaTime; if (!(_stepCycle > _nextStep)) return; _nextStep = _stepCycle + _stepInterval; PlayFootStepAudio(); } private void PlayFootStepAudio() { if (!_characterController.isGrounded) return; // pick & play a random footstep sound from the array, // excluding sound at index 0 int n = Random.Range(1, _footstepSounds.Length); audio.clip = _footstepSounds[n]; audio.PlayOneShot(audio.clip); // move picked sound to index 0 so it's not picked next time _footstepSounds[n] = _footstepSounds[0]; _footstepSounds[0] = audio.clip; } private void UpdateCameraPosition(float speed) { Vector3 newCameraPosition; if (!useHeadBob) return; if (_characterController.velocity.magnitude > 0 && _characterController.isGrounded) { _camera.transform.localPosition = _headBob.DoHeadBob(_characterController.velocity.magnitude + (speed*(_isWalking ? 1f : runstepLenghten))); newCameraPosition = _camera.transform.localPosition; newCameraPosition.y = _camera.transform.localPosition.y - _jumpBob.Offset(); } else { newCameraPosition = _camera.transform.localPosition; newCameraPosition.y = _originalCameraPosition.y - _jumpBob.Offset(); } _camera.transform.localPosition = newCameraPosition; _cameraRefocus.SetFocusPoint(); } private void GetInput(out float speed) { // Read input float horizontal = CrossPlatformInputManager.GetAxisRaw("Horizontal"); float vertical = CrossPlatformInputManager.GetAxisRaw("Vertical"); bool waswalking = _isWalking; #if !MOBILE_INPUT // On standalone builds, walk/run speed is modified by a key press. // keep track of whether or not the character is walking or running _isWalking = !Input.GetKey(KeyCode.LeftShift); #endif // set the desired speed to be walking or running speed = _isWalking ? walkSpeed : runSpeed; _input = new Vector2(horizontal, vertical); // normalize input if it exceeds 1 in combined length: if (_input.sqrMagnitude > 1) _input.Normalize(); // handle speed change to give an fov kick // only if the player is going to a run, is running and the fovkick is to be used if (_isWalking != waswalking && useFOVKick && _characterController.velocity.sqrMagnitude > 0) { StopAllCoroutines(); StartCoroutine(!_isWalking ? _fovKick.FOVKickUp() : _fovKick.FOVKickDown()); } } private void RotateView() { //DCURRY added else for mobile input #if !MOBILE_INPUT Vector2 mouseInput = _mouseLook.Clamped(_yRotation, transform.localEulerAngles.y); _camera.transform.localEulerAngles = new Vector3(-mouseInput.y, _camera.transform.localEulerAngles.y, _camera.transform.localEulerAngles.z); transform.localEulerAngles = new Vector3(0, mouseInput.x, 0); #else Vector2 mouseInput = new Vector2(CrossPlatformInputManager.GetAxisRaw("HorizontalLook"), CrossPlatformInputManager.GetAxisRaw("VerticalLook")); float camX = _camera.transform.localEulerAngles.x; if((camX > 280 && camX <= 360) || (camX >= 0 && camX < 80) || (camX >= 80 && camX < 180 && mouseInput.y > 0) || (camX > 180 && camX <= 280 && mouseInput.y < 0)) { _camera.transform.localEulerAngles += new Vector3(-mouseInput.y * lookSpeed * .7f, _camera.transform.localEulerAngles.y, _camera.transform.localEulerAngles.z); } transform.localEulerAngles += new Vector3(0, mouseInput.x * lookSpeed, 0); #endif // handle the roation round the x axis on the camera _yRotation = mouseInput.y; _cameraRefocus.GetFocusPoint(); } private void OnControllerColliderHit(ControllerColliderHit hit) { Rigidbody body = hit.collider.attachedRigidbody; if (body == null || body.isKinematic) return; //dont move the rigidbody if the character is on top of it if (_collisionFlags == CollisionFlags.CollidedBelow) return; body.AddForceAtPosition(_characterController.velocity*0.1f, hit.point, ForceMode.Impulse); } } }