player look and move

This commit is contained in:
mischa 2023-12-13 16:27:12 +01:00
parent 1c46530f68
commit 0790b99007
6 changed files with 1581 additions and 0 deletions

View 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);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 3f380ddcf720745159256fd28850e60b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4ab763efbf7234d96aedc8f08d29df1f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View 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;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4816473ade13e4dc8b4b4a9717676357
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: