mirror of
https://github.com/MirrorNetworking/Mirror.git
synced 2024-11-18 02:50:32 +00:00
player look and move
This commit is contained in:
parent
1c46530f68
commit
0790b99007
358
Assets/Mirror/Examples/Shooter/Scripts/PlayerLook.cs
Normal file
358
Assets/Mirror/Examples/Shooter/Scripts/PlayerLook.cs
Normal file
@ -0,0 +1,358 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace Mirror.Examples.Shooter
|
||||||
|
{
|
||||||
|
public class PlayerLook : MonoBehaviour
|
||||||
|
{
|
||||||
|
[Header("Components")]
|
||||||
|
public PlayerMovement movement;
|
||||||
|
#pragma warning disable CS0109 // member does not hide accessible member
|
||||||
|
new Camera camera;
|
||||||
|
#pragma warning restore CS0109 // member does not hide accessible member
|
||||||
|
|
||||||
|
[Header("Camera")]
|
||||||
|
public float XSensitivity = 2;
|
||||||
|
public float YSensitivity = 2;
|
||||||
|
public float MinimumX = -90;
|
||||||
|
public float MaximumX = 90;
|
||||||
|
|
||||||
|
// head position is useful for raycasting etc.
|
||||||
|
public Transform firstPersonParent;
|
||||||
|
public Vector3 headPosition => firstPersonParent.position;
|
||||||
|
public Transform freeLookParent;
|
||||||
|
|
||||||
|
Vector3 originalCameraPosition;
|
||||||
|
|
||||||
|
public KeyCode freeLookKey = KeyCode.LeftAlt;
|
||||||
|
|
||||||
|
// the layer mask to use when trying to detect view blocking
|
||||||
|
// (this way we dont zoom in all the way when standing in another entity)
|
||||||
|
// (-> create a entity layer for them if needed)
|
||||||
|
public LayerMask viewBlockingLayers;
|
||||||
|
public float zoomSpeed = 0.5f;
|
||||||
|
public float distance = 0;
|
||||||
|
public float minDistance = 0;
|
||||||
|
public float maxDistance = 7;
|
||||||
|
|
||||||
|
[Header("Physical Interaction")]
|
||||||
|
[Tooltip("Layers to use for raycasting. Check Default, Walls, Player, Zombie, Doors, Interactables, Item, etc. Uncheck IgnoreRaycast, AggroArea, Water, UI, etc.")]
|
||||||
|
public LayerMask raycastLayers = Physics.DefaultRaycastLayers;
|
||||||
|
|
||||||
|
// camera offsets. Vector2 because we only want X (left/right) and Y (up/down)
|
||||||
|
// to be modified. Z (forward/backward) should NEVER be modified because
|
||||||
|
// then we could look through walls when tilting our head forward to look
|
||||||
|
// downwards, etc. This can be avoided in the camera positioning logic, but
|
||||||
|
// is way to complex and not worth it at all.
|
||||||
|
[Header("Offsets - Standing")]
|
||||||
|
public Vector2 firstPersonOffsetStanding = Vector2.zero;
|
||||||
|
public Vector2 thirdPersonOffsetStanding = Vector2.up;
|
||||||
|
public Vector2 thirdPersonOffsetStandingMultiplier = Vector2.zero;
|
||||||
|
|
||||||
|
[Header("Offsets - Crouching")]
|
||||||
|
public Vector2 firstPersonOffsetCrouching = Vector2.zero;
|
||||||
|
public Vector2 thirdPersonOffsetCrouching = Vector2.up / 2;
|
||||||
|
public Vector2 thirdPersonOffsetCrouchingMultiplier = Vector2.zero;
|
||||||
|
// scale offset by distance so that 100% zoom in = first person and
|
||||||
|
// zooming out puts camera target slightly above head for easier aiming
|
||||||
|
public float crouchOriginMultiplier = 0.65f;
|
||||||
|
|
||||||
|
[Header("Offsets - Crawling")]
|
||||||
|
public Vector2 firstPersonOffsetCrawling = Vector2.zero;
|
||||||
|
public Vector2 thirdPersonOffsetCrawling = Vector2.up;
|
||||||
|
public Vector2 thirdPersonOffsetCrawlingMultiplier = Vector2.zero;
|
||||||
|
// scale offset by distance so that 100% zoom in = first person and
|
||||||
|
// zooming out puts camera target slightly above head for easier aiming
|
||||||
|
public float crawlOriginMultiplier = 0.65f;
|
||||||
|
|
||||||
|
[Header("Offsets - Swimming")]
|
||||||
|
public Vector2 firstPersonOffsetSwimming = Vector2.zero;
|
||||||
|
public Vector2 thirdPersonOffsetSwimming = Vector2.up;
|
||||||
|
public Vector2 thirdPersonOffsetSwimmingMultiplier = Vector2.zero;
|
||||||
|
// scale offset by distance so that 100% zoom in = first person and
|
||||||
|
// zooming out puts camera target slightly above head for easier aiming
|
||||||
|
public float swimOriginMultiplier = 0.65f;
|
||||||
|
|
||||||
|
// look directions /////////////////////////////////////////////////////////
|
||||||
|
// * for first person, all we need is the camera.forward
|
||||||
|
//
|
||||||
|
// * for third person, we need to raycast where the camera looks and then
|
||||||
|
// calculate the direction from the eyes.
|
||||||
|
// BUT for animations we actually only want camera.forward because it
|
||||||
|
// looks strange if we stand right in front of a wall, camera aiming above
|
||||||
|
// a player's head (because of head offset) and then the players arms
|
||||||
|
// aiming at that point above his head (on the wall) too.
|
||||||
|
// => he should always appear to aim into the far direction
|
||||||
|
// => he should always fire at the raycasted point
|
||||||
|
// in other words, if we want 1st and 3rd person WITH camera offsets, then
|
||||||
|
// we need both the FAR direction and the RAYCASTED direction
|
||||||
|
//
|
||||||
|
// * we also need to sync it over the network to animate other players.
|
||||||
|
// => we compress it as far as possible to save bandwidth. syncing it via
|
||||||
|
// rotation bytes X and Y uses 2 instead of 12 bytes per observer(!)
|
||||||
|
//
|
||||||
|
// * and we can't only calculate and store the values in Update because
|
||||||
|
// ShoulderLookAt needs them live in LateUpdate, Update is too far behind
|
||||||
|
// and would cause the arms to be lag behind a bit.
|
||||||
|
//
|
||||||
|
public Vector3 lookDirectionFar
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return camera.transform.forward;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//[SyncVar, HideInInspector] Vector3 syncedLookDirectionRaycasted; not needed atm, see lookPositionRaycasted comment
|
||||||
|
public Vector3 lookDirectionRaycasted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// same for local and other players
|
||||||
|
// (positionRaycasted uses camera || syncedDirectionRaycasted anyway)
|
||||||
|
return (lookPositionRaycasted - headPosition).normalized;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the far position, directionFar projected into nirvana
|
||||||
|
public Vector3 lookPositionFar
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
Vector3 position = camera.transform.position;
|
||||||
|
return position + lookDirectionFar * 9999f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// the raycasted position is needed for lookDirectionRaycasted calculation
|
||||||
|
// and for firing, so we might as well reuse it here
|
||||||
|
public Vector3 lookPositionRaycasted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// raycast based on position and direction, project into nirvana if nothing hit
|
||||||
|
// (not * infinity because might overflow depending on position)
|
||||||
|
RaycastHit hit;
|
||||||
|
return Utils.RaycastWithout(camera.transform.position, camera.transform.forward, out hit, Mathf.Infinity, gameObject, raycastLayers)
|
||||||
|
? hit.point
|
||||||
|
: lookPositionFar;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
camera = Camera.main;
|
||||||
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Start()
|
||||||
|
{
|
||||||
|
// set camera parent to player
|
||||||
|
camera.transform.SetParent(transform, false);
|
||||||
|
|
||||||
|
// look into player forward direction, which was loaded from the db
|
||||||
|
camera.transform.rotation = transform.rotation;
|
||||||
|
|
||||||
|
// set camera to head position
|
||||||
|
camera.transform.position = headPosition;
|
||||||
|
|
||||||
|
// remember original camera position
|
||||||
|
originalCameraPosition = camera.transform.localPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
// escape unlocks cursor
|
||||||
|
if (Input.GetKeyDown(KeyCode.Escape))
|
||||||
|
{
|
||||||
|
Cursor.lockState = CursorLockMode.None;
|
||||||
|
}
|
||||||
|
// mouse click locks cursor
|
||||||
|
else if (Input.GetMouseButtonDown(0))
|
||||||
|
{
|
||||||
|
Cursor.lockState = CursorLockMode.Locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
// only while alive and while cursor is locked, otherwise we are in a UI
|
||||||
|
if (Cursor.lockState == CursorLockMode.Locked)
|
||||||
|
{
|
||||||
|
// calculate horizontal and vertical rotation steps
|
||||||
|
float xExtra = Input.GetAxis("Mouse X") * XSensitivity;
|
||||||
|
float yExtra = Input.GetAxis("Mouse Y") * YSensitivity;
|
||||||
|
|
||||||
|
// use mouse to rotate character
|
||||||
|
// (but use camera freelook parent while climbing so player isn't rotated
|
||||||
|
// while climbing)
|
||||||
|
// (no free look in first person)
|
||||||
|
if (movement.state == MoveState.CLIMBING ||
|
||||||
|
(Input.GetKey(freeLookKey) && distance > 0))
|
||||||
|
{
|
||||||
|
// set to freelook parent already?
|
||||||
|
if (camera.transform.parent != freeLookParent)
|
||||||
|
InitializeFreeLook();
|
||||||
|
|
||||||
|
// rotate freelooktarget for horizontal, rotate camera for vertical
|
||||||
|
freeLookParent.Rotate(new Vector3(0, xExtra, 0));
|
||||||
|
camera.transform.Rotate(new Vector3(-yExtra, 0, 0));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// set to player parent already?
|
||||||
|
if (camera.transform.parent != transform)
|
||||||
|
InitializeForcedLook();
|
||||||
|
|
||||||
|
// rotate character for horizontal, rotate camera for vertical
|
||||||
|
transform.Rotate(new Vector3(0, xExtra, 0));
|
||||||
|
camera.transform.Rotate(new Vector3(-yExtra, 0, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update camera position after everything else was updated
|
||||||
|
void LateUpdate()
|
||||||
|
{
|
||||||
|
// clamp camera rotation automatically. this way we can rotate it to
|
||||||
|
// whatever we like in Update, and LateUpdate will correct it.
|
||||||
|
camera.transform.localRotation = Utils.ClampRotationAroundXAxis(camera.transform.localRotation, MinimumX, MaximumX);
|
||||||
|
|
||||||
|
// zoom after rotating, otherwise it won't be smooth and would overwrite
|
||||||
|
// each other.
|
||||||
|
|
||||||
|
// zoom should only happen if not in a UI right now
|
||||||
|
if (!Utils.IsCursorOverUserInterface())
|
||||||
|
{
|
||||||
|
float step = Utils.GetZoomUniversal() * zoomSpeed;
|
||||||
|
distance = Mathf.Clamp(distance - step, minDistance, maxDistance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate target and zoomed position
|
||||||
|
if (distance == 0) // first person
|
||||||
|
{
|
||||||
|
// we use the current head bone position as origin here
|
||||||
|
// -> gets rid of the idle->run head change effect that was odd
|
||||||
|
// -> gets rid of upper body culling issues when looking downwards
|
||||||
|
Vector3 headLocal = transform.InverseTransformPoint(headPosition);
|
||||||
|
Vector3 origin = Vector3.zero;
|
||||||
|
Vector3 offset = Vector3.zero;
|
||||||
|
|
||||||
|
if (movement.state == MoveState.CROUCHING)
|
||||||
|
{
|
||||||
|
origin = headLocal * crouchOriginMultiplier;
|
||||||
|
offset = firstPersonOffsetCrouching;
|
||||||
|
}
|
||||||
|
else if (movement.state == MoveState.CRAWLING)
|
||||||
|
{
|
||||||
|
origin = headLocal * crawlOriginMultiplier;
|
||||||
|
offset = firstPersonOffsetCrawling;
|
||||||
|
}
|
||||||
|
else if (movement.state == MoveState.SWIMMING)
|
||||||
|
{
|
||||||
|
origin = headLocal;
|
||||||
|
offset = firstPersonOffsetSwimming;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
origin = headLocal;
|
||||||
|
offset = firstPersonOffsetStanding;
|
||||||
|
}
|
||||||
|
|
||||||
|
// set final position
|
||||||
|
Vector3 target = transform.TransformPoint(origin + offset);
|
||||||
|
camera.transform.position = target;
|
||||||
|
}
|
||||||
|
else // third person
|
||||||
|
{
|
||||||
|
Vector3 origin = Vector3.zero;
|
||||||
|
Vector3 offsetBase = Vector3.zero;
|
||||||
|
Vector3 offsetMult = Vector3.zero;
|
||||||
|
|
||||||
|
if (movement.state == MoveState.CROUCHING)
|
||||||
|
{
|
||||||
|
origin = originalCameraPosition * crouchOriginMultiplier;
|
||||||
|
offsetBase = thirdPersonOffsetCrouching;
|
||||||
|
offsetMult = thirdPersonOffsetCrouchingMultiplier;
|
||||||
|
}
|
||||||
|
else if (movement.state == MoveState.CRAWLING)
|
||||||
|
{
|
||||||
|
origin = originalCameraPosition * crawlOriginMultiplier;
|
||||||
|
offsetBase = thirdPersonOffsetCrawling;
|
||||||
|
offsetMult = thirdPersonOffsetCrawlingMultiplier;
|
||||||
|
}
|
||||||
|
else if (movement.state == MoveState.SWIMMING)
|
||||||
|
{
|
||||||
|
origin = originalCameraPosition * swimOriginMultiplier;
|
||||||
|
offsetBase = thirdPersonOffsetSwimming;
|
||||||
|
offsetMult = thirdPersonOffsetSwimmingMultiplier;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
origin = originalCameraPosition;
|
||||||
|
offsetBase = thirdPersonOffsetStanding;
|
||||||
|
offsetMult = thirdPersonOffsetStandingMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 target = transform.TransformPoint(origin + offsetBase + offsetMult * distance);
|
||||||
|
Vector3 newPosition = target - (camera.transform.rotation * Vector3.forward * distance);
|
||||||
|
|
||||||
|
// avoid view blocking (only third person, pointless in first person)
|
||||||
|
// -> always based on original distance and only overwrite if necessary
|
||||||
|
// so that we dont have to zoom out again after view block disappears
|
||||||
|
// -> we cast exactly from cam to target, which is the crosshair position.
|
||||||
|
// if anything is inbetween then view blocking changes the distance.
|
||||||
|
// this works perfectly.
|
||||||
|
float finalDistance = distance;
|
||||||
|
RaycastHit hit;
|
||||||
|
Debug.DrawLine(target, camera.transform.position, Color.white);
|
||||||
|
if (Physics.Linecast(target, newPosition, out hit, viewBlockingLayers))
|
||||||
|
{
|
||||||
|
// calculate a better distance (with some space between it)
|
||||||
|
finalDistance = Vector3.Distance(target, hit.point) - 0.1f;
|
||||||
|
Debug.DrawLine(target, hit.point, Color.red);
|
||||||
|
}
|
||||||
|
else Debug.DrawLine(target, newPosition, Color.green);
|
||||||
|
|
||||||
|
// set final position
|
||||||
|
camera.transform.position = target - (camera.transform.rotation * Vector3.forward * finalDistance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool InFirstPerson()
|
||||||
|
{
|
||||||
|
return distance == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// free look mode //////////////////////////////////////////////////////////
|
||||||
|
public bool IsFreeLooking()
|
||||||
|
{
|
||||||
|
return camera != null && // camera isn't initialized while loading players in charselection
|
||||||
|
camera.transform.parent == freeLookParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeFreeLook()
|
||||||
|
{
|
||||||
|
camera.transform.SetParent(freeLookParent, false);
|
||||||
|
freeLookParent.localRotation = Quaternion.identity; // initial rotation := where we look at right now
|
||||||
|
}
|
||||||
|
|
||||||
|
public void InitializeForcedLook()
|
||||||
|
{
|
||||||
|
camera.transform.SetParent(transform, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// debugging ///////////////////////////////////////////////////////////////
|
||||||
|
void OnDrawGizmos()
|
||||||
|
{
|
||||||
|
if (camera == null) return;
|
||||||
|
|
||||||
|
// draw camera forward
|
||||||
|
Gizmos.color = Color.white;
|
||||||
|
Gizmos.DrawLine(headPosition, camera.transform.position + camera.transform.forward * 9999f);
|
||||||
|
|
||||||
|
// draw all the different look positions
|
||||||
|
Gizmos.color = Color.blue;
|
||||||
|
Gizmos.DrawLine(headPosition, lookPositionFar);
|
||||||
|
Gizmos.color = Color.cyan;
|
||||||
|
Gizmos.DrawLine(headPosition, lookPositionRaycasted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Examples/Shooter/Scripts/PlayerLook.cs.meta
Normal file
11
Assets/Mirror/Examples/Shooter/Scripts/PlayerLook.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 3f380ddcf720745159256fd28850e60b
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
797
Assets/Mirror/Examples/Shooter/Scripts/PlayerMovement.cs
Normal file
797
Assets/Mirror/Examples/Shooter/Scripts/PlayerMovement.cs
Normal file
@ -0,0 +1,797 @@
|
|||||||
|
// based on Unity's FirstPersonController & ThirdPersonController scripts
|
||||||
|
using UnityEngine;
|
||||||
|
using Controller2k;
|
||||||
|
using Random = UnityEngine.Random;
|
||||||
|
|
||||||
|
namespace Mirror.Examples.Shooter
|
||||||
|
{
|
||||||
|
// MoveState as byte for minimal bandwidth (otherwise it's int by default)
|
||||||
|
// note: distinction between WALKING and RUNNING in case we need to know the
|
||||||
|
// difference somewhere (e.g. for endurance recovery)
|
||||||
|
public enum MoveState : byte {IDLE, WALKING, RUNNING, CROUCHING, CRAWLING, AIRBORNE, CLIMBING, SWIMMING}
|
||||||
|
|
||||||
|
[RequireComponent(typeof(CharacterController2k))]
|
||||||
|
[RequireComponent(typeof(AudioSource))]
|
||||||
|
public class PlayerMovement : MonoBehaviour
|
||||||
|
{
|
||||||
|
// components to be assigned in inspector
|
||||||
|
[Header("Components")]
|
||||||
|
public Animator animator;
|
||||||
|
public CharacterController2k controller;
|
||||||
|
public AudioSource feetAudio;
|
||||||
|
public PlayerLook look;
|
||||||
|
// the collider for the character controller. NOT the hips collider. this
|
||||||
|
// one is NOT affected by animations and generally a better choice for state
|
||||||
|
// machine logic.
|
||||||
|
public CapsuleCollider controllerCollider;
|
||||||
|
#pragma warning disable CS0109 // member does not hide accessible member
|
||||||
|
new Camera camera;
|
||||||
|
#pragma warning restore CS0109 // member does not hide accessible member
|
||||||
|
|
||||||
|
[Header("State")]
|
||||||
|
public MoveState state = MoveState.IDLE;
|
||||||
|
[HideInInspector] public Vector3 moveDir;
|
||||||
|
|
||||||
|
// it's useful to have both strafe movement (WASD) and rotations (QE)
|
||||||
|
// => like in WoW, it more fun to play this way.
|
||||||
|
[Header("Rotation")]
|
||||||
|
public float rotationSpeed = 150;
|
||||||
|
|
||||||
|
[Header("Walking")]
|
||||||
|
public float walkSpeed = 5;
|
||||||
|
public float walkAcceleration = 15; // set to maxint for instant speed
|
||||||
|
public float walkDeceleration = 20; // feels best if higher than acceleration
|
||||||
|
|
||||||
|
[Header("Running")]
|
||||||
|
public float runSpeed = 8;
|
||||||
|
[Range(0f, 1f)] public float runStepLength = 0.7f;
|
||||||
|
public float runStepInterval = 3;
|
||||||
|
public float runCycleLegOffset = 0.2f; //specific to the character in sample assets, will need to be modified to work with others
|
||||||
|
public KeyCode runKey = KeyCode.LeftShift;
|
||||||
|
float stepCycle;
|
||||||
|
float nextStep;
|
||||||
|
|
||||||
|
[Header("Crouching")]
|
||||||
|
public float crouchSpeed = 1.5f;
|
||||||
|
public float crouchAcceleration = 5; // set to maxint for instant speed
|
||||||
|
public float crouchDeceleration = 10; // feels best if higher than acceleration
|
||||||
|
public KeyCode crouchKey = KeyCode.C;
|
||||||
|
bool crouchKeyPressed;
|
||||||
|
|
||||||
|
[Header("Crawling")]
|
||||||
|
public float crawlSpeed = 1;
|
||||||
|
public float crawlAcceleration = 5; // set to maxint for instant speed
|
||||||
|
public float crawlDeceleration = 10; // feels best if higher than acceleration
|
||||||
|
public KeyCode crawlKey = KeyCode.V;
|
||||||
|
bool crawlKeyPressed;
|
||||||
|
|
||||||
|
[Header("Swimming")]
|
||||||
|
public float swimSpeed = 4;
|
||||||
|
public float swimAcceleration = 15; // set to maxint for instant speed
|
||||||
|
public float swimDeceleration = 20; // feels best if higher than acceleration
|
||||||
|
public float swimSurfaceOffset = 0.25f;
|
||||||
|
Collider waterCollider;
|
||||||
|
bool inWater => waterCollider != null; // standing in water / touching it?
|
||||||
|
bool underWater; // deep enough in water so we need to swim?
|
||||||
|
[Range(0, 1)] public float underwaterThreshold = 0.9f; // percent of body that need to be underwater to start swimming
|
||||||
|
public LayerMask canStandInWaterCheckLayers = Physics.DefaultRaycastLayers; // set this to everything except water layer
|
||||||
|
|
||||||
|
[Header("Jumping")]
|
||||||
|
public float jumpSpeed = 7;
|
||||||
|
[HideInInspector] public float jumpLeg;
|
||||||
|
bool jumpKeyPressed;
|
||||||
|
|
||||||
|
[Header("Falling")]
|
||||||
|
public float airborneAcceleration = 15; // set to maxint for instant speed
|
||||||
|
public float airborneDeceleration = 20; // feels best if higher than acceleration
|
||||||
|
public float fallMinimumMagnitude = 9; // walking down steps shouldn't count as falling and play no falling sound.
|
||||||
|
public float fallDamageMinimumMagnitude = 13;
|
||||||
|
public float fallDamageMultiplier = 2;
|
||||||
|
[HideInInspector] public Vector3 lastFall;
|
||||||
|
bool sprintingBeforeAirborne; // don't allow sprint key to accelerate while jumping. decision has to be made before that.
|
||||||
|
|
||||||
|
[Header("Climbing")]
|
||||||
|
public float climbSpeed = 3;
|
||||||
|
Collider ladderCollider;
|
||||||
|
|
||||||
|
[Header("Mounted")]
|
||||||
|
public float mountedRotationSpeed = 100;
|
||||||
|
public float mountedAcceleration = 15; // set to maxint for instant speed
|
||||||
|
public float mountedDeceleration = 20; // feels best if higher than acceleration
|
||||||
|
|
||||||
|
[Header("Physics")]
|
||||||
|
public float gravityMultiplier = 2;
|
||||||
|
|
||||||
|
// we need to remember the last accelerated xz speed without gravity etc.
|
||||||
|
// (using moveDir.xz.magnitude doesn't work well with mounted movement)
|
||||||
|
float horizontalSpeed;
|
||||||
|
|
||||||
|
// helper property to check grounded with some tolerance. technically we
|
||||||
|
// aren't grounded when walking down steps, but this way we factor in a
|
||||||
|
// minimum fall magnitude. useful for more tolerant jumping etc.
|
||||||
|
// (= while grounded or while velocity not smaller than min fall yet)
|
||||||
|
public bool isGroundedWithinTolerance =>
|
||||||
|
controller.isGrounded || controller.velocity.y > -fallMinimumMagnitude;
|
||||||
|
|
||||||
|
[Header("Sounds")]
|
||||||
|
public AudioClip[] footstepSounds; // an array of footstep sounds that will be randomly selected from.
|
||||||
|
public AudioClip jumpSound; // the sound played when character leaves the ground.
|
||||||
|
public AudioClip landSound; // the sound played when character touches back on ground.
|
||||||
|
|
||||||
|
void Awake()
|
||||||
|
{
|
||||||
|
camera = Camera.main;
|
||||||
|
}
|
||||||
|
|
||||||
|
// input directions ////////////////////////////////////////////////////////
|
||||||
|
Vector2 GetInputDirection()
|
||||||
|
{
|
||||||
|
// get input direction while alive and while not typing in chat
|
||||||
|
// (otherwise 0 so we keep falling even if we die while jumping etc.)
|
||||||
|
float horizontal = Input.GetAxis("Horizontal");
|
||||||
|
float vertical = Input.GetAxis("Vertical");
|
||||||
|
return new Vector2(horizontal, vertical).normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector3 GetDesiredDirection(Vector2 inputDir)
|
||||||
|
{
|
||||||
|
// always move along the camera forward as it is the direction that is being aimed at
|
||||||
|
return transform.forward * inputDir.y + transform.right * inputDir.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
// movement state machine //////////////////////////////////////////////////
|
||||||
|
bool EventJumpRequested()
|
||||||
|
{
|
||||||
|
// only while grounded, so jump key while jumping doesn't start a new
|
||||||
|
// jump immediately after landing
|
||||||
|
// => and not while sliding, otherwise we could climb slides by jumping
|
||||||
|
// => not even while SlidingState.Starting, so we aren't able to avoid
|
||||||
|
// sliding by bunny hopping.
|
||||||
|
// => grounded check uses min fall tolerance so we can actually still
|
||||||
|
// jump when walking down steps.
|
||||||
|
return isGroundedWithinTolerance &&
|
||||||
|
controller.slidingState == SlidingState.NONE &&
|
||||||
|
jumpKeyPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventCrouchToggle()
|
||||||
|
{
|
||||||
|
return crouchKeyPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventCrawlToggle()
|
||||||
|
{
|
||||||
|
return crawlKeyPressed;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventFalling()
|
||||||
|
{
|
||||||
|
// use minimum fall magnitude so walking down steps isn't detected as
|
||||||
|
// falling! otherwise walking down steps would show the fall animation
|
||||||
|
// and play the landing sound.
|
||||||
|
return !isGroundedWithinTolerance;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventLanded()
|
||||||
|
{
|
||||||
|
return controller.isGrounded;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventUnderWater()
|
||||||
|
{
|
||||||
|
// we can't really make it player position dependent, because he might
|
||||||
|
// swim to the surface at which point it might be detected as standing
|
||||||
|
// in water but not being under water, etc.
|
||||||
|
if (inWater) // in water and valid water collider?
|
||||||
|
{
|
||||||
|
// raycasting from water to the bottom at the position of the player
|
||||||
|
// seems like a very precise solution
|
||||||
|
Vector3 origin = new Vector3(transform.position.x,
|
||||||
|
waterCollider.bounds.max.y,
|
||||||
|
transform.position.z);
|
||||||
|
float distance = controllerCollider.height * underwaterThreshold;
|
||||||
|
Debug.DrawLine(origin, origin + Vector3.down * distance, Color.cyan);
|
||||||
|
|
||||||
|
// we are underwater if the raycast doesn't hit anything
|
||||||
|
return !Utils.RaycastWithout(origin, Vector3.down, out RaycastHit hit, distance, gameObject, canStandInWaterCheckLayers);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventLadderEnter()
|
||||||
|
{
|
||||||
|
return ladderCollider != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EventLadderExit()
|
||||||
|
{
|
||||||
|
// OnTriggerExit isn't good enough to detect ladder exits because we
|
||||||
|
// shouldn't exit as soon as our head sticks out of the ladder collider.
|
||||||
|
// only if we fully left it. so check this manually here:
|
||||||
|
return ladderCollider != null &&
|
||||||
|
!ladderCollider.bounds.Intersects(controllerCollider.bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to apply gravity based on previous Y direction
|
||||||
|
float ApplyGravity(float moveDirY)
|
||||||
|
{
|
||||||
|
// apply full gravity while falling
|
||||||
|
if (!controller.isGrounded)
|
||||||
|
// gravity needs to be * Time.fixedDeltaTime even though we multiply
|
||||||
|
// the final controller.Move * Time.fixedDeltaTime too, because the
|
||||||
|
// unit is 9.81m/s²
|
||||||
|
return moveDirY + Physics.gravity.y * gravityMultiplier * Time.fixedDeltaTime;
|
||||||
|
// if grounded then apply no force. the new OpenCharacterController
|
||||||
|
// doesn't need a ground stick force. it would only make the character
|
||||||
|
// slide on all uneven surfaces.
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to get move or walk speed depending on key press & endurance
|
||||||
|
float GetWalkOrRunSpeed()
|
||||||
|
{
|
||||||
|
bool runRequested = Input.GetKey(runKey);
|
||||||
|
return runRequested ? runSpeed : walkSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ApplyFallDamage()
|
||||||
|
{
|
||||||
|
// measure only the Y direction. we don't want to take fall damage
|
||||||
|
// if we jump forward into a wall because xz is high.
|
||||||
|
float fallMagnitude = Mathf.Abs(lastFall.y);
|
||||||
|
if(fallMagnitude >= fallDamageMinimumMagnitude)
|
||||||
|
{
|
||||||
|
int damage = Mathf.RoundToInt(fallMagnitude * fallDamageMultiplier);
|
||||||
|
Debug.LogWarning("Fall Damage: " + damage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// acceleration can be different when accelerating/decelerating
|
||||||
|
float AccelerateSpeed(Vector2 inputDir, float currentSpeed, float targetSpeed, float acceleration)
|
||||||
|
{
|
||||||
|
// desired speed is between 'speed' and '0'
|
||||||
|
float desiredSpeed = inputDir.magnitude * targetSpeed;
|
||||||
|
|
||||||
|
// accelerate speed
|
||||||
|
return Mathf.MoveTowards(currentSpeed, desiredSpeed, acceleration * Time.fixedDeltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// rotate with QE keys
|
||||||
|
void RotateWithKeys()
|
||||||
|
{
|
||||||
|
float horizontal2 = Input.GetAxis("Horizontal2");
|
||||||
|
transform.Rotate(Vector3.up * horizontal2 * rotationSpeed * Time.fixedDeltaTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
void EnterLadder()
|
||||||
|
{
|
||||||
|
// make player look directly at ladder forward. but we also initialize
|
||||||
|
// freelook manually already to overwrite the initial rotation, so
|
||||||
|
// that in the end, the camera keeps looking at the same angle even
|
||||||
|
// though we did modify transform.forward.
|
||||||
|
// note: even though we set the rotation perfectly here, there's
|
||||||
|
// still one frame where it seems to interpolate between the
|
||||||
|
// new and the old rotation, which causes 1 odd camera frame.
|
||||||
|
// this could be avoided by overwriting transform.forward once
|
||||||
|
// more in LateUpdate.
|
||||||
|
look.InitializeFreeLook();
|
||||||
|
transform.forward = ladderCollider.transform.forward;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveState UpdateIDLE(Vector2 inputDir, Vector3 desiredDir)
|
||||||
|
{
|
||||||
|
// QE key rotation
|
||||||
|
RotateWithKeys();
|
||||||
|
|
||||||
|
// decelerate from last move (e.g. from jump)
|
||||||
|
// (moveDir.xz can be set to 0 to have an interruption when landing)
|
||||||
|
horizontalSpeed = AccelerateSpeed(inputDir, horizontalSpeed, 0, walkDeceleration);
|
||||||
|
moveDir.x = desiredDir.x * horizontalSpeed;
|
||||||
|
moveDir.y = ApplyGravity(moveDir.y);
|
||||||
|
moveDir.z = desiredDir.z * horizontalSpeed;
|
||||||
|
|
||||||
|
if (EventFalling())
|
||||||
|
{
|
||||||
|
sprintingBeforeAirborne = false;
|
||||||
|
return MoveState.AIRBORNE;
|
||||||
|
}
|
||||||
|
else if (EventJumpRequested())
|
||||||
|
{
|
||||||
|
// start the jump movement into Y dir, go to jumping
|
||||||
|
// note: no endurance>0 check because it feels odd if we can't jump
|
||||||
|
moveDir.y = jumpSpeed;
|
||||||
|
sprintingBeforeAirborne = false;
|
||||||
|
PlayJumpSound();
|
||||||
|
return MoveState.AIRBORNE;
|
||||||
|
}
|
||||||
|
else if (EventCrouchToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.5f, true, true, false))
|
||||||
|
return MoveState.CROUCHING;
|
||||||
|
}
|
||||||
|
else if (EventCrawlToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
return MoveState.CRAWLING;
|
||||||
|
}
|
||||||
|
else if (EventLadderEnter())
|
||||||
|
{
|
||||||
|
EnterLadder();
|
||||||
|
return MoveState.CLIMBING;
|
||||||
|
}
|
||||||
|
else if (EventUnderWater())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
return MoveState.SWIMMING;
|
||||||
|
}
|
||||||
|
else if (inputDir != Vector2.zero)
|
||||||
|
{
|
||||||
|
return MoveState.WALKING;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveState UpdateWALKINGandRUNNING(Vector2 inputDir, Vector3 desiredDir)
|
||||||
|
{
|
||||||
|
// QE key rotation
|
||||||
|
RotateWithKeys();
|
||||||
|
|
||||||
|
// walk or run?
|
||||||
|
float speed = GetWalkOrRunSpeed();
|
||||||
|
|
||||||
|
// move with acceleration (feels better)
|
||||||
|
horizontalSpeed = AccelerateSpeed(inputDir, horizontalSpeed, speed, inputDir != Vector2.zero ? walkAcceleration : walkDeceleration);
|
||||||
|
moveDir.x = desiredDir.x * horizontalSpeed;
|
||||||
|
moveDir.y = ApplyGravity(moveDir.y);
|
||||||
|
moveDir.z = desiredDir.z * horizontalSpeed;
|
||||||
|
|
||||||
|
if (EventFalling())
|
||||||
|
{
|
||||||
|
sprintingBeforeAirborne = speed == runSpeed;
|
||||||
|
return MoveState.AIRBORNE;
|
||||||
|
}
|
||||||
|
else if (EventJumpRequested())
|
||||||
|
{
|
||||||
|
// start the jump movement into Y dir, go to jumping
|
||||||
|
// note: no endurance>0 check because it feels odd if we can't jump
|
||||||
|
moveDir.y = jumpSpeed;
|
||||||
|
sprintingBeforeAirborne = speed == runSpeed;
|
||||||
|
PlayJumpSound();
|
||||||
|
return MoveState.AIRBORNE;
|
||||||
|
}
|
||||||
|
else if (EventCrouchToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.5f, true, true, false))
|
||||||
|
{
|
||||||
|
// limit speed to crouch speed so we don't decelerate from run speed
|
||||||
|
// to crouch speed (hence crouching too fast for a short time)
|
||||||
|
horizontalSpeed = Mathf.Min(horizontalSpeed, crouchSpeed);
|
||||||
|
return MoveState.CROUCHING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventCrawlToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
{
|
||||||
|
// limit speed to crawl speed so we don't decelerate from run speed
|
||||||
|
// to crawl speed (hence crawling too fast for a short time)
|
||||||
|
horizontalSpeed = Mathf.Min(horizontalSpeed, crawlSpeed);
|
||||||
|
return MoveState.CRAWLING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventLadderEnter())
|
||||||
|
{
|
||||||
|
EnterLadder();
|
||||||
|
return MoveState.CLIMBING;
|
||||||
|
}
|
||||||
|
else if (EventUnderWater())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
return MoveState.SWIMMING;
|
||||||
|
}
|
||||||
|
// go to idle after fully decelerating (y doesn't matter)
|
||||||
|
else if (moveDir.x == 0 && moveDir.z == 0)
|
||||||
|
{
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressStepCycle(inputDir, speed);
|
||||||
|
return speed == walkSpeed ? MoveState.WALKING : MoveState.RUNNING;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveState UpdateCROUCHING(Vector2 inputDir, Vector3 desiredDir)
|
||||||
|
{
|
||||||
|
// QE key rotation
|
||||||
|
RotateWithKeys();
|
||||||
|
|
||||||
|
// move with acceleration (feels better)
|
||||||
|
horizontalSpeed = AccelerateSpeed(inputDir, horizontalSpeed, crouchSpeed, inputDir != Vector2.zero ? crouchAcceleration : crouchDeceleration);
|
||||||
|
moveDir.x = desiredDir.x * horizontalSpeed;
|
||||||
|
moveDir.y = ApplyGravity(moveDir.y);
|
||||||
|
moveDir.z = desiredDir.z * horizontalSpeed;
|
||||||
|
|
||||||
|
if (EventFalling())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
sprintingBeforeAirborne = false;
|
||||||
|
return MoveState.AIRBORNE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventJumpRequested())
|
||||||
|
{
|
||||||
|
// stop crouching when pressing jump key. this feels better than
|
||||||
|
// jumping from the crouching state.
|
||||||
|
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventCrouchToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventCrawlToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
{
|
||||||
|
// limit speed to crawl speed so we don't decelerate from run speed
|
||||||
|
// to crawl speed (hence crawling too fast for a short time)
|
||||||
|
horizontalSpeed = Mathf.Min(horizontalSpeed, crawlSpeed);
|
||||||
|
return MoveState.CRAWLING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventLadderEnter())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
EnterLadder();
|
||||||
|
return MoveState.CLIMBING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventUnderWater())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.SWIMMING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressStepCycle(inputDir, crouchSpeed);
|
||||||
|
return MoveState.CROUCHING;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveState UpdateCRAWLING(Vector2 inputDir, Vector3 desiredDir)
|
||||||
|
{
|
||||||
|
// QE key rotation
|
||||||
|
RotateWithKeys();
|
||||||
|
|
||||||
|
// move with acceleration (feels better)
|
||||||
|
horizontalSpeed = AccelerateSpeed(inputDir, horizontalSpeed, crawlSpeed, inputDir != Vector2.zero ? crawlAcceleration : crawlDeceleration);
|
||||||
|
moveDir.x = desiredDir.x * horizontalSpeed;
|
||||||
|
moveDir.y = ApplyGravity(moveDir.y);
|
||||||
|
moveDir.z = desiredDir.z * horizontalSpeed;
|
||||||
|
|
||||||
|
if (EventFalling())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
sprintingBeforeAirborne = false;
|
||||||
|
return MoveState.AIRBORNE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventJumpRequested())
|
||||||
|
{
|
||||||
|
// stop crawling when pressing jump key. this feels better than
|
||||||
|
// jumping from the crawling state.
|
||||||
|
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventCrouchToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.5f, true, true, false))
|
||||||
|
{
|
||||||
|
// limit speed to crouch speed so we don't decelerate from run speed
|
||||||
|
// to crouch speed (hence crouching too fast for a short time)
|
||||||
|
horizontalSpeed = Mathf.Min(horizontalSpeed, crouchSpeed);
|
||||||
|
return MoveState.CROUCHING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventCrawlToggle())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventLadderEnter())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
EnterLadder();
|
||||||
|
return MoveState.CLIMBING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (EventUnderWater())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.SWIMMING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ProgressStepCycle(inputDir, crawlSpeed);
|
||||||
|
return MoveState.CRAWLING;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveState UpdateAIRBORNE(Vector2 inputDir, Vector3 desiredDir)
|
||||||
|
{
|
||||||
|
// QE key rotation
|
||||||
|
RotateWithKeys();
|
||||||
|
|
||||||
|
// max speed depends on what we did before jumping/falling
|
||||||
|
float speed = sprintingBeforeAirborne ? runSpeed : walkSpeed;
|
||||||
|
|
||||||
|
// move with acceleration (feels better)
|
||||||
|
horizontalSpeed = AccelerateSpeed(inputDir, horizontalSpeed, speed, inputDir != Vector2.zero ? airborneAcceleration : airborneDeceleration);
|
||||||
|
moveDir.x = desiredDir.x * horizontalSpeed;
|
||||||
|
moveDir.y = ApplyGravity(moveDir.y);
|
||||||
|
moveDir.z = desiredDir.z * horizontalSpeed;
|
||||||
|
|
||||||
|
if (EventLanded())
|
||||||
|
{
|
||||||
|
// apply fall damage only in AIRBORNE->Landed.
|
||||||
|
// (e.g. not if we run face forward into a wall with high velocity)
|
||||||
|
ApplyFallDamage();
|
||||||
|
PlayLandingSound();
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
else if (EventLadderEnter())
|
||||||
|
{
|
||||||
|
EnterLadder();
|
||||||
|
return MoveState.CLIMBING;
|
||||||
|
}
|
||||||
|
else if (EventUnderWater())
|
||||||
|
{
|
||||||
|
// rescale capsule
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 0.25f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.SWIMMING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return MoveState.AIRBORNE;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveState UpdateCLIMBING(Vector2 inputDir, Vector3 desiredDir)
|
||||||
|
{
|
||||||
|
// finished climbing?
|
||||||
|
if (EventLadderExit())
|
||||||
|
{
|
||||||
|
// player rotation was adjusted to ladder rotation before.
|
||||||
|
// let's reset it, but also keep look forward
|
||||||
|
transform.rotation = Quaternion.Euler(0, transform.rotation.eulerAngles.y, 0);
|
||||||
|
ladderCollider = null;
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// interpret forward/backward movement as upward/downward
|
||||||
|
// note: NO ACCELERATION, otherwise we would climb really fast when
|
||||||
|
// sprinting towards a ladder. and the actual climb feels way too
|
||||||
|
// unresponsive when accelerating.
|
||||||
|
moveDir.x = inputDir.x * climbSpeed;
|
||||||
|
moveDir.y = inputDir.y * climbSpeed;
|
||||||
|
moveDir.z = 0;
|
||||||
|
|
||||||
|
// make the direction relative to ladder rotation. so when pressing right
|
||||||
|
// we always climb to the right of the ladder, no matter how it's rotated
|
||||||
|
moveDir = ladderCollider.transform.rotation * moveDir;
|
||||||
|
Debug.DrawLine(transform.position, transform.position + moveDir, Color.yellow, 0.1f, false);
|
||||||
|
|
||||||
|
return MoveState.CLIMBING;
|
||||||
|
}
|
||||||
|
|
||||||
|
MoveState UpdateSWIMMING(Vector2 inputDir, Vector3 desiredDir)
|
||||||
|
{
|
||||||
|
// ladder under / above water?
|
||||||
|
if (EventLadderEnter())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
EnterLadder();
|
||||||
|
return MoveState.CLIMBING;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// not under water anymore?
|
||||||
|
else if (!EventUnderWater())
|
||||||
|
{
|
||||||
|
// rescale capsule if possible
|
||||||
|
if (controller.TrySetHeight(controller.defaultHeight * 1f, true, true, false))
|
||||||
|
{
|
||||||
|
return MoveState.IDLE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QE key rotation
|
||||||
|
RotateWithKeys();
|
||||||
|
|
||||||
|
// move with acceleration (feels better)
|
||||||
|
horizontalSpeed = AccelerateSpeed(inputDir, horizontalSpeed, swimSpeed, inputDir != Vector2.zero ? swimAcceleration : swimDeceleration);
|
||||||
|
moveDir.x = desiredDir.x * horizontalSpeed;
|
||||||
|
moveDir.z = desiredDir.z * horizontalSpeed;
|
||||||
|
|
||||||
|
// gravitate toward surface
|
||||||
|
if (waterCollider != null)
|
||||||
|
{
|
||||||
|
float surface = waterCollider.bounds.max.y;
|
||||||
|
float surfaceDirection = surface - controller.bounds.min.y - swimSurfaceOffset;
|
||||||
|
moveDir.y = surfaceDirection * swimSpeed;
|
||||||
|
}
|
||||||
|
else moveDir.y = 0;
|
||||||
|
|
||||||
|
return MoveState.SWIMMING;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use Update to check Input
|
||||||
|
void Update()
|
||||||
|
{
|
||||||
|
if (!jumpKeyPressed) jumpKeyPressed = Input.GetButtonDown("Jump");
|
||||||
|
if (!crawlKeyPressed) crawlKeyPressed = Input.GetKeyDown(crawlKey);
|
||||||
|
if (!crouchKeyPressed) crouchKeyPressed = Input.GetKeyDown(crouchKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CharacterController movement is physics based and requires FixedUpdate.
|
||||||
|
// (using Update causes strange movement speeds in builds otherwise)
|
||||||
|
void FixedUpdate()
|
||||||
|
{
|
||||||
|
// get input and desired direction based on camera and ground
|
||||||
|
Vector2 inputDir = GetInputDirection();
|
||||||
|
Vector3 desiredDir = GetDesiredDirection(inputDir);
|
||||||
|
Debug.DrawLine(transform.position, transform.position + desiredDir, Color.blue);
|
||||||
|
Debug.DrawLine(transform.position, transform.position + desiredDir, Color.cyan);
|
||||||
|
|
||||||
|
// update state machine
|
||||||
|
if (state == MoveState.IDLE) state = UpdateIDLE(inputDir, desiredDir);
|
||||||
|
else if (state == MoveState.WALKING) state = UpdateWALKINGandRUNNING(inputDir, desiredDir);
|
||||||
|
else if (state == MoveState.RUNNING) state = UpdateWALKINGandRUNNING(inputDir, desiredDir);
|
||||||
|
else if (state == MoveState.CROUCHING) state = UpdateCROUCHING(inputDir, desiredDir);
|
||||||
|
else if (state == MoveState.CRAWLING) state = UpdateCRAWLING(inputDir, desiredDir);
|
||||||
|
else if (state == MoveState.AIRBORNE) state = UpdateAIRBORNE(inputDir, desiredDir);
|
||||||
|
else if (state == MoveState.CLIMBING) state = UpdateCLIMBING(inputDir, desiredDir);
|
||||||
|
else if (state == MoveState.SWIMMING) state = UpdateSWIMMING(inputDir, desiredDir);
|
||||||
|
else Debug.LogError("Unhandled Movement State: " + state);
|
||||||
|
|
||||||
|
// cache this move's state to detect landing etc. next time
|
||||||
|
if (!controller.isGrounded) lastFall = controller.velocity;
|
||||||
|
|
||||||
|
// move depending on latest moveDir changes
|
||||||
|
Debug.DrawLine(transform.position, transform.position + moveDir * Time.fixedDeltaTime, Color.magenta);
|
||||||
|
controller.Move(moveDir * Time.fixedDeltaTime); // note: returns CollisionFlags if needed
|
||||||
|
|
||||||
|
// calculate which leg is behind, so as to leave that leg trailing in the jump animation
|
||||||
|
// (This code is reliant on the specific run cycle offset in our animations,
|
||||||
|
// and assumes one leg passes the other at the normalized clip times of 0.0 and 0.5)
|
||||||
|
float runCycle = Mathf.Repeat(animator.GetCurrentAnimatorStateInfo(0).normalizedTime + runCycleLegOffset, 1);
|
||||||
|
jumpLeg = (runCycle < 0.5f ? 1 : -1);// * move.z;
|
||||||
|
|
||||||
|
// reset keys no matter what
|
||||||
|
jumpKeyPressed = false;
|
||||||
|
crawlKeyPressed = false;
|
||||||
|
crouchKeyPressed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnGUI()
|
||||||
|
{
|
||||||
|
// show data next to player for easier debugging. this is very useful!
|
||||||
|
if (Debug.isDebugBuild)
|
||||||
|
{
|
||||||
|
// project player position to screen
|
||||||
|
Vector3 center = controllerCollider.bounds.center;
|
||||||
|
Vector3 point = camera.WorldToScreenPoint(center);
|
||||||
|
|
||||||
|
// in front of camera and in screen?
|
||||||
|
if (point.z >= 0 && Utils.IsPointInScreen(point))
|
||||||
|
{
|
||||||
|
GUI.color = new Color(0, 0, 0, 0.5f);
|
||||||
|
GUILayout.BeginArea(new Rect(point.x, Screen.height - point.y, 150, 200));
|
||||||
|
|
||||||
|
// some info for all players, including local
|
||||||
|
GUILayout.Label("grounded=" + controller.isGrounded);
|
||||||
|
GUILayout.Label("groundedTol=" + isGroundedWithinTolerance);
|
||||||
|
GUILayout.Label("lastFall=" + lastFall);
|
||||||
|
GUILayout.Label("sliding=" + controller.slidingState);
|
||||||
|
|
||||||
|
GUILayout.EndArea();
|
||||||
|
GUI.color = Color.white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayLandingSound()
|
||||||
|
{
|
||||||
|
feetAudio.clip = landSound;
|
||||||
|
feetAudio.Play();
|
||||||
|
nextStep = stepCycle + .5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayJumpSound()
|
||||||
|
{
|
||||||
|
feetAudio.clip = jumpSound;
|
||||||
|
feetAudio.Play();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ProgressStepCycle(Vector3 inputDir, float speed)
|
||||||
|
{
|
||||||
|
if (controller.velocity.sqrMagnitude > 0 && (inputDir.x != 0 || inputDir.y != 0))
|
||||||
|
{
|
||||||
|
stepCycle += (controller.velocity.magnitude + (speed*(state == MoveState.WALKING ? 1 : runStepLength)))*
|
||||||
|
Time.fixedDeltaTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stepCycle > nextStep)
|
||||||
|
{
|
||||||
|
nextStep = stepCycle + runStepInterval;
|
||||||
|
PlayFootStepAudio();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void PlayFootStepAudio()
|
||||||
|
{
|
||||||
|
if (!controller.isGrounded) return;
|
||||||
|
|
||||||
|
// pick & play a random footstep sound from the array,
|
||||||
|
// excluding sound at index 0
|
||||||
|
int n = Random.Range(1, footstepSounds.Length);
|
||||||
|
feetAudio.clip = footstepSounds[n];
|
||||||
|
feetAudio.PlayOneShot(feetAudio.clip);
|
||||||
|
|
||||||
|
// move picked sound to index 0 so it's not picked next time
|
||||||
|
footstepSounds[n] = footstepSounds[0];
|
||||||
|
footstepSounds[0] = feetAudio.clip;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTriggerEnter(Collider co)
|
||||||
|
{
|
||||||
|
// touching ladder? then set ladder collider
|
||||||
|
if (co.CompareTag("Ladder"))
|
||||||
|
ladderCollider = co;
|
||||||
|
// touching water? then set water collider
|
||||||
|
else if (co.CompareTag("Water"))
|
||||||
|
waterCollider = co;
|
||||||
|
}
|
||||||
|
|
||||||
|
void OnTriggerExit(Collider co)
|
||||||
|
{
|
||||||
|
// not touching water anymore? then clear water collider
|
||||||
|
if (co.CompareTag("Water"))
|
||||||
|
waterCollider = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4ab763efbf7234d96aedc8f08d29df1f
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
393
Assets/Mirror/Examples/Shooter/Scripts/Utils.cs
Executable file
393
Assets/Mirror/Examples/Shooter/Scripts/Utils.cs
Executable file
@ -0,0 +1,393 @@
|
|||||||
|
// This class contains some helper functions.
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Events;
|
||||||
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityEngine.AI;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Mirror.Examples.Shooter
|
||||||
|
{
|
||||||
|
// serializable events
|
||||||
|
[Serializable] public class UnityEventGameObject : UnityEvent<GameObject> {}
|
||||||
|
|
||||||
|
public class Utils
|
||||||
|
{
|
||||||
|
// is any of the keys UP?
|
||||||
|
public static bool AnyKeyUp(KeyCode[] keys)
|
||||||
|
{
|
||||||
|
// avoid Linq.Any because it is HEAVY(!) on GC and performance
|
||||||
|
foreach (KeyCode key in keys)
|
||||||
|
if (Input.GetKeyUp(key))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is any of the keys DOWN?
|
||||||
|
public static bool AnyKeyDown(KeyCode[] keys)
|
||||||
|
{
|
||||||
|
// avoid Linq.Any because it is HEAVY(!) on GC and performance
|
||||||
|
foreach (KeyCode key in keys)
|
||||||
|
if (Input.GetKeyDown(key))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is any of the keys PRESSED?
|
||||||
|
public static bool AnyKeyPressed(KeyCode[] keys)
|
||||||
|
{
|
||||||
|
// avoid Linq.Any because it is HEAVY(!) on GC and performance
|
||||||
|
foreach (KeyCode key in keys)
|
||||||
|
if (Input.GetKey(key))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is a 2D point in screen?
|
||||||
|
public static bool IsPointInScreen(Vector2 point)
|
||||||
|
{
|
||||||
|
return 0 <= point.x && point.x <= Screen.width &&
|
||||||
|
0 <= point.y && point.y <= Screen.height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance between two ClosestPoints
|
||||||
|
// this is needed in cases where entites are really big. in those cases,
|
||||||
|
// we can't just move to entity.transform.position, because it will be
|
||||||
|
// unreachable. instead we have to go the closest point on the boundary.
|
||||||
|
//
|
||||||
|
// Vector3.Distance(a.transform.position, b.transform.position):
|
||||||
|
// _____ _____
|
||||||
|
// | | | |
|
||||||
|
// | x==|======|==x |
|
||||||
|
// |_____| |_____|
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Utils.ClosestDistance(a.collider, b.collider):
|
||||||
|
// _____ _____
|
||||||
|
// | | | |
|
||||||
|
// | |x====x| |
|
||||||
|
// |_____| |_____|
|
||||||
|
//
|
||||||
|
public static float ClosestDistance(Collider a, Collider b)
|
||||||
|
{
|
||||||
|
// return 0 if both intersect or if one is inside another.
|
||||||
|
// ClosestPoint distance wouldn't be > 0 in those cases otherwise.
|
||||||
|
if (a.bounds.Intersects(b.bounds))
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
// Unity offers ClosestPointOnBounds and ClosestPoint.
|
||||||
|
// ClosestPoint is more accurate. OnBounds often doesn't get <1 because
|
||||||
|
// it uses a point at the top of the player collider, not in the center.
|
||||||
|
// (use Debug.DrawLine here to see the difference)
|
||||||
|
return Vector3.Distance(a.ClosestPoint(b.transform.position),
|
||||||
|
b.ClosestPoint(a.transform.position));
|
||||||
|
}
|
||||||
|
|
||||||
|
// CastWithout functions all need a backups dictionary. this is in hot path
|
||||||
|
// and creating a Dictionary for every single call would be insanity.
|
||||||
|
static Dictionary<Transform, int> castBackups = new Dictionary<Transform, int>();
|
||||||
|
|
||||||
|
// raycast while ignoring self (by setting layer to "Ignore Raycasts" first)
|
||||||
|
// => setting layer to IgnoreRaycasts before casting is the easiest way to do it
|
||||||
|
// => raycast + !=this check would still cause hit.point to be on player
|
||||||
|
// => raycastall is not sorted and child objects might have different layers etc.
|
||||||
|
public static bool RaycastWithout(Vector3 origin, Vector3 direction, out RaycastHit hit, float maxDistance, GameObject ignore, int layerMask=Physics.DefaultRaycastLayers)
|
||||||
|
{
|
||||||
|
// remember layers
|
||||||
|
castBackups.Clear();
|
||||||
|
|
||||||
|
// set all to ignore raycast
|
||||||
|
foreach (Transform tf in ignore.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
castBackups[tf] = tf.gameObject.layer;
|
||||||
|
tf.gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
|
||||||
|
}
|
||||||
|
|
||||||
|
// raycast
|
||||||
|
bool result = Physics.Raycast(origin, direction, out hit, maxDistance, layerMask);
|
||||||
|
|
||||||
|
// restore layers
|
||||||
|
foreach (KeyValuePair<Transform, int> kvp in castBackups)
|
||||||
|
kvp.Key.gameObject.layer = kvp.Value;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool LinecastWithout(Vector3 start, Vector3 end, out RaycastHit hit, GameObject ignore, int layerMask=Physics.DefaultRaycastLayers)
|
||||||
|
{
|
||||||
|
// remember layers
|
||||||
|
castBackups.Clear();
|
||||||
|
|
||||||
|
// set all to ignore raycast
|
||||||
|
foreach (Transform tf in ignore.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
castBackups[tf] = tf.gameObject.layer;
|
||||||
|
tf.gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
|
||||||
|
}
|
||||||
|
|
||||||
|
// raycast
|
||||||
|
bool result = Physics.Linecast(start, end, out hit, layerMask);
|
||||||
|
|
||||||
|
// restore layers
|
||||||
|
foreach (KeyValuePair<Transform, int> kvp in castBackups)
|
||||||
|
kvp.Key.gameObject.layer = kvp.Value;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool SphereCastWithout(Vector3 origin, float sphereRadius, Vector3 direction, out RaycastHit hit, float maxDistance, GameObject ignore, int layerMask=Physics.DefaultRaycastLayers)
|
||||||
|
{
|
||||||
|
// remember layers
|
||||||
|
castBackups.Clear();
|
||||||
|
|
||||||
|
// set all to ignore raycast
|
||||||
|
foreach (Transform tf in ignore.GetComponentsInChildren<Transform>(true))
|
||||||
|
{
|
||||||
|
castBackups[tf] = tf.gameObject.layer;
|
||||||
|
tf.gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
|
||||||
|
}
|
||||||
|
|
||||||
|
// raycast
|
||||||
|
bool result = Physics.SphereCast(origin, sphereRadius, direction, out hit, maxDistance, layerMask);
|
||||||
|
|
||||||
|
// restore layers
|
||||||
|
foreach (KeyValuePair<Transform, int> kvp in castBackups)
|
||||||
|
kvp.Key.gameObject.layer = kvp.Value;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pretty print seconds as hours:minutes:seconds(.milliseconds/100)s
|
||||||
|
public static string PrettySeconds(float seconds)
|
||||||
|
{
|
||||||
|
TimeSpan t = TimeSpan.FromSeconds(seconds);
|
||||||
|
string res = "";
|
||||||
|
if (t.Days > 0) res += t.Days + "d";
|
||||||
|
if (t.Hours > 0) res += " " + t.Hours + "h";
|
||||||
|
if (t.Minutes > 0) res += " " + t.Minutes + "m";
|
||||||
|
// 0.5s, 1.5s etc. if any milliseconds. 1s, 2s etc. if any seconds
|
||||||
|
if (t.Milliseconds > 0) res += " " + t.Seconds + "." + (t.Milliseconds / 100) + "s";
|
||||||
|
else if (t.Seconds > 0) res += " " + t.Seconds + "s";
|
||||||
|
// if the string is still empty because the value was '0', then at least
|
||||||
|
// return the seconds instead of returning an empty string
|
||||||
|
return res != "" ? res : "0s";
|
||||||
|
}
|
||||||
|
|
||||||
|
// hard mouse scrolling that is consistent between all platforms
|
||||||
|
// Input.GetAxis("Mouse ScrollWheel") and
|
||||||
|
// Input.GetAxisRaw("Mouse ScrollWheel")
|
||||||
|
// both return values like 0.01 on standalone and 0.5 on WebGL, which
|
||||||
|
// causes too fast zooming on WebGL etc.
|
||||||
|
// normally GetAxisRaw should return -1,0,1, but it doesn't for scrolling
|
||||||
|
public static float GetAxisRawScrollUniversal()
|
||||||
|
{
|
||||||
|
float scroll = Input.GetAxisRaw("Mouse ScrollWheel");
|
||||||
|
if (scroll < 0) return -1;
|
||||||
|
if (scroll > 0) return 1;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// two finger pinch detection
|
||||||
|
// source: https://docs.unity3d.com/Manual/PlatformDependentCompilation.html
|
||||||
|
public static float GetPinch()
|
||||||
|
{
|
||||||
|
if (Input.touchCount == 2)
|
||||||
|
{
|
||||||
|
// Store both touches.
|
||||||
|
Touch touchZero = Input.GetTouch(0);
|
||||||
|
Touch touchOne = Input.GetTouch(1);
|
||||||
|
|
||||||
|
// Find the position in the previous frame of each touch.
|
||||||
|
Vector2 touchZeroPrevPos = touchZero.position - touchZero.deltaPosition;
|
||||||
|
Vector2 touchOnePrevPos = touchOne.position - touchOne.deltaPosition;
|
||||||
|
|
||||||
|
// Find the magnitude of the vector (the distance) between the touches in each frame.
|
||||||
|
float prevTouchDeltaMag = (touchZeroPrevPos - touchOnePrevPos).magnitude;
|
||||||
|
float touchDeltaMag = (touchZero.position - touchOne.position).magnitude;
|
||||||
|
|
||||||
|
// Find the difference in the distances between each frame.
|
||||||
|
return touchDeltaMag - prevTouchDeltaMag;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// universal zoom: mouse scroll if mouse, two finger pinching otherwise
|
||||||
|
public static float GetZoomUniversal()
|
||||||
|
{
|
||||||
|
if (Input.mousePresent)
|
||||||
|
return GetAxisRawScrollUniversal();
|
||||||
|
else if (Input.touchSupported)
|
||||||
|
return GetPinch();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the cursor is over a UI or OnGUI element right now
|
||||||
|
// note: for UI, this only works if the UI's CanvasGroup blocks Raycasts
|
||||||
|
// note: for OnGUI: hotControl is only set while clicking, not while zooming
|
||||||
|
public static bool IsCursorOverUserInterface()
|
||||||
|
{
|
||||||
|
// IsPointerOverGameObject check for left mouse (default)
|
||||||
|
if (EventSystem.current.IsPointerOverGameObject())
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// IsPointerOverGameObject check for touches
|
||||||
|
for (int i = 0; i < Input.touchCount; ++i)
|
||||||
|
if (EventSystem.current.IsPointerOverGameObject(Input.GetTouch(i).fingerId))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// OnGUI check
|
||||||
|
return GUIUtility.hotControl != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// random point on NavMesh for item drops, etc.
|
||||||
|
public static Vector3 RandomUnitCircleOnNavMesh(Vector3 position, float radiusMultiplier)
|
||||||
|
{
|
||||||
|
// random circle point
|
||||||
|
Vector2 r = UnityEngine.Random.insideUnitCircle * radiusMultiplier;
|
||||||
|
|
||||||
|
// convert to 3d
|
||||||
|
Vector3 randomPosition = new Vector3(position.x + r.x, position.y, position.z + r.y);
|
||||||
|
|
||||||
|
// raycast to find valid point on NavMesh. otherwise return original one
|
||||||
|
if (NavMesh.SamplePosition(randomPosition, out NavMeshHit hit, radiusMultiplier * 2, NavMesh.AllAreas))
|
||||||
|
return hit.position;
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// random point on NavMesh that has no obstacles (walls) between point and center
|
||||||
|
// -> useful because items shouldn't be dropped behind walls, etc.
|
||||||
|
public static Vector3 ReachableRandomUnitCircleOnNavMesh(Vector3 position, float radiusMultiplier, int solverAttempts)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < solverAttempts; ++i)
|
||||||
|
{
|
||||||
|
// get random point on navmesh around position
|
||||||
|
Vector3 candidate = RandomUnitCircleOnNavMesh(position, radiusMultiplier);
|
||||||
|
|
||||||
|
// check if anything obstructs the way (walls etc.)
|
||||||
|
if (!NavMesh.Raycast(position, candidate, out NavMeshHit hit, NavMesh.AllAreas))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise return original position if we can't find any good point.
|
||||||
|
// in that case it's best to just drop it where the entity stands.
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
// can Collider A 'reach' Collider B?
|
||||||
|
// e.g. can monster reach player to attack?
|
||||||
|
// can player reach item to pick up?
|
||||||
|
// => NOTE: we only try to reach the center vertical line of the collider.
|
||||||
|
// this is not a perfect 'is collider reachable' function that checks
|
||||||
|
// any point on the collider. it is perfect for monsters and players
|
||||||
|
// though, because they are rather vertical
|
||||||
|
public static bool IsReachableVertically(Collider origin, Collider other, float maxDistance)
|
||||||
|
{
|
||||||
|
// we need to find the closest collider points first, because using
|
||||||
|
// maxDistance for checks between collider.center points is meaningless
|
||||||
|
// for monsters with huge colliders.
|
||||||
|
// (we use ClosestPointOnBounds for all other attack range checks too)
|
||||||
|
Vector3 originClosest = origin.ClosestPoint(other.transform.position);
|
||||||
|
Vector3 otherClosest = other.ClosestPoint(origin.transform.position);
|
||||||
|
|
||||||
|
// linecast from origin to other to decide if reachable
|
||||||
|
// -> we cast from origin center/top to all center/top/bottom of other
|
||||||
|
// aka 'can origin attack any part of other with head or hands?'
|
||||||
|
Vector3 otherCenter = new Vector3(otherClosest.x, other.bounds.center.y, otherClosest.z); // closest centered at y
|
||||||
|
Vector3 otherTop = otherCenter + Vector3.up * other.bounds.extents.y;
|
||||||
|
Vector3 otherBottom = otherCenter + Vector3.down * other.bounds.extents.y;
|
||||||
|
|
||||||
|
Vector3 originCenter = new Vector3(originClosest.x, origin.bounds.center.y, originClosest.z); // origin centered at y
|
||||||
|
Vector3 originTop = originCenter + Vector3.up * origin.bounds.extents.y;
|
||||||
|
|
||||||
|
// maxDistance is from origin center to any other point.
|
||||||
|
// -> it's not meant from origin head to other feet, in which case we
|
||||||
|
// could reach objects that are too far above us, e.g. a monster
|
||||||
|
// could reach a player standing on the battle bus.
|
||||||
|
// -> in other words, the origin head checks should be reduced by size/2
|
||||||
|
// since they start further away from the hips
|
||||||
|
float originHalf = origin.bounds.size.y / 2;
|
||||||
|
|
||||||
|
// reachable if there is nothing between us and the other collider
|
||||||
|
// -> check distance too, e.g. monsters attacking upwards
|
||||||
|
//
|
||||||
|
// NOTE: checking 'if nothing is between' is the way to go, because
|
||||||
|
// monster and player main colliders have IgnoreRaycast layers, so
|
||||||
|
// checking 'if linecast reaches other collider' wouldn't work.
|
||||||
|
// (this is also faster, since we only Linecast if dist <= ...)
|
||||||
|
//
|
||||||
|
// NOTE: this can be done shorter with just Linecast || Linecast || ...
|
||||||
|
// but color coded DrawLines are significantly(!) easier to debug!
|
||||||
|
//
|
||||||
|
// IMPORTANT: we do NOT have to ignore any colliders manually because
|
||||||
|
// the monster/player main colliders are on IgnoreRaycast
|
||||||
|
// layers, and all the body part colliders are triggers!
|
||||||
|
if (Vector3.Distance(originCenter, otherCenter) <= maxDistance &&
|
||||||
|
!Physics.Linecast(originCenter, otherCenter, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore))
|
||||||
|
{
|
||||||
|
Debug.DrawLine(originCenter, otherCenter, Color.white);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else Debug.DrawLine(originCenter, otherCenter, Color.gray);
|
||||||
|
|
||||||
|
if (Vector3.Distance(originCenter, otherTop) <= maxDistance &&
|
||||||
|
!Physics.Linecast(originCenter, otherTop, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore))
|
||||||
|
{
|
||||||
|
Debug.DrawLine(originCenter, otherTop, Color.white);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else Debug.DrawLine(originCenter, otherTop, Color.gray);
|
||||||
|
|
||||||
|
if (Vector3.Distance(originCenter, otherBottom) <= maxDistance &&
|
||||||
|
!Physics.Linecast(originCenter, otherBottom, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore))
|
||||||
|
{
|
||||||
|
Debug.DrawLine(originCenter, otherBottom, Color.white);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else Debug.DrawLine(originCenter, otherBottom, Color.gray);
|
||||||
|
|
||||||
|
if (Vector3.Distance(originTop, otherCenter) <= maxDistance - originHalf &&
|
||||||
|
!Physics.Linecast(originTop, otherCenter, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore))
|
||||||
|
{
|
||||||
|
Debug.DrawLine(originTop, otherCenter, Color.white);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else Debug.DrawLine(originTop, otherCenter, Color.gray);
|
||||||
|
|
||||||
|
if (Vector3.Distance(originTop, otherTop) <= maxDistance - originHalf &&
|
||||||
|
!Physics.Linecast(originTop, otherTop, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore))
|
||||||
|
{
|
||||||
|
Debug.DrawLine(originTop, otherTop, Color.white);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else Debug.DrawLine(originTop, otherTop, Color.gray);
|
||||||
|
|
||||||
|
if (Vector3.Distance(originTop, otherBottom) <= maxDistance - originHalf &&
|
||||||
|
!Physics.Linecast(originTop, otherBottom, Physics.DefaultRaycastLayers, QueryTriggerInteraction.Ignore))
|
||||||
|
{
|
||||||
|
Debug.DrawLine(originTop, otherBottom, Color.white);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
else Debug.DrawLine(originTop, otherBottom, Color.gray);
|
||||||
|
|
||||||
|
// no point was reachable
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// clamp a rotation around x axis
|
||||||
|
// (e.g. camera up/down rotation so we can't look below character's pants etc.)
|
||||||
|
// original source: Unity's standard assets MouseLook.cs
|
||||||
|
public static Quaternion ClampRotationAroundXAxis(Quaternion q, float min, float max)
|
||||||
|
{
|
||||||
|
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, min, max);
|
||||||
|
q.x = Mathf.Tan (0.5f * Mathf.Deg2Rad * angleX);
|
||||||
|
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
11
Assets/Mirror/Examples/Shooter/Scripts/Utils.cs.meta
Normal file
11
Assets/Mirror/Examples/Shooter/Scripts/Utils.cs.meta
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4816473ade13e4dc8b4b4a9717676357
|
||||||
|
MonoImporter:
|
||||||
|
externalObjects: {}
|
||||||
|
serializedVersion: 2
|
||||||
|
defaultReferences: []
|
||||||
|
executionOrder: 0
|
||||||
|
icon: {instanceID: 0}
|
||||||
|
userData:
|
||||||
|
assetBundleName:
|
||||||
|
assetBundleVariant:
|
Loading…
Reference in New Issue
Block a user