b8ab71969a
Finished lockpicking. tied it to the Attributes system. when you pick a lock you gain dexterity, the higher your dexterity, the easier it is to pick locks.
525 lines
19 KiB
C#
525 lines
19 KiB
C#
using UnityEditor;
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
/*
|
|
* Thank you for using my Lockpicking asset! I hope it works great in your game. You can extend it to suit your game by
|
|
* manipulating some of the code below. For instance, if your player can have a various level of "lockpicking" skill,
|
|
* you may consider multiplying the value of lockGive by their skill, so that a higher skilled player would find it
|
|
* easier to open a lock.
|
|
*
|
|
* Enjoy!
|
|
*/
|
|
|
|
namespace Lockpicking
|
|
{
|
|
[RequireComponent(typeof(LockEmissive))]
|
|
public class Keyhole : MonoBehaviour
|
|
{
|
|
// Events
|
|
UnityEvent lockpickBroke = new UnityEvent();
|
|
UnityEvent lockOpen = new UnityEvent();
|
|
|
|
[Header("Player Input")]
|
|
public float openPressure = 0f;
|
|
public float lockpickPressure = 0f;
|
|
|
|
|
|
[Header("Speed Settings")]
|
|
[Tooltip("Speed of the lockpick when input value is full.")]
|
|
[Range(1f, 720f)] public float turnSpeedLockpick = 25f;
|
|
|
|
[Tooltip("Speed of the entire keyhole when input value is full.")]
|
|
[Range(1f, 720f)] public float turnSpeedKeyhole = 25f;
|
|
|
|
[Tooltip("Speed at which the lock will return to normal when the input value is 0.")]
|
|
[Range(1f, 720f)] public float returnSpeedKeyhole = 150f;
|
|
|
|
[Tooltip("Maximum shake distance per shake change.")]
|
|
[SerializeField] private float maxShake = 0.5f;
|
|
|
|
[Tooltip("Amount of time between shake changes when shaking.")]
|
|
[SerializeField] private float shakeTime = 0.1f;
|
|
|
|
|
|
[Header("Pick Settings")]
|
|
[Tooltip("Starting angle of the lock pick.")]
|
|
[SerializeField] private Vector3 _pickAnglesDefault;
|
|
|
|
[Tooltip("Minimum angle the lock pick can travel to.")]
|
|
[SerializeField] private float _pickAngleMin;
|
|
|
|
[Tooltip("Maximum angle the lock pick can travel to.")]
|
|
[SerializeField] private float _pickAngleMax;
|
|
|
|
|
|
[Header("Keyhole Settings")]
|
|
[Tooltip("Starting angle of the keyhole.")]
|
|
[SerializeField] private float _keyholeAngleDefault = 0;
|
|
|
|
[Tooltip("Maximum angle of the keyhole. At this angle, the lock will open.")]
|
|
[SerializeField] private float _keyholeAngleMax = 0;
|
|
|
|
|
|
[Header("Lock Settings")]
|
|
[Tooltip("If true, lock details will be randomized on awake")]
|
|
public bool resetOnAwake = true;
|
|
|
|
[Tooltip("Minimum angle the lock can be set to.")]
|
|
[Range(0f, 180f)] public float minLockAngle = -90f;
|
|
|
|
[Tooltip("Maximum angle the lock can be set to.")]
|
|
[Range(0f, 180f)] public float maxLockAngle = 90f;
|
|
|
|
[Tooltip("Minimum distance (plus and minus) from the lock angle that the lock will open.")]
|
|
[Range(1f, 180f)] public float minGiveAmount = 1f;
|
|
|
|
[Tooltip("Maximum distance (plus and minus) from the lock angle that the lock will open.")]
|
|
[Range(1f, 180f)] public float maxGiveAmount = 45f;
|
|
|
|
[Tooltip("Minimum distance for the pick to be in for the lock will turn partially.")]
|
|
[Range(5f, 180f)] public float minCloseDistance = 5f;
|
|
|
|
[Tooltip("Maximum distance for the pick to be in for the lock will turn partially.")]
|
|
[Range(5f, 180f)] public float maxCloseDistance = 20f;
|
|
|
|
[Tooltip("Amount of time to ignore player input after a lock pick breaks.")]
|
|
[Range(0f, 5f)] public float breakPause = 2f;
|
|
|
|
|
|
[Header("Lock Details")]
|
|
[Tooltip("True if the lock is already open (unlocked).")]
|
|
[SerializeField] private bool _lockIsOpen;
|
|
public bool LockIsOpen
|
|
{
|
|
get => _lockIsOpen;
|
|
set => _lockIsOpen = value;
|
|
}
|
|
[Tooltip("The exact angle the lock is set to.")]
|
|
float _lockAngle;
|
|
public float LockAngle
|
|
{
|
|
get => _lockAngle;
|
|
set => _lockAngle = Mathf.Clamp(value, minLockAngle, maxLockAngle);
|
|
}
|
|
[Tooltip("The distance to/from the LockAngle the lock pick needs to be in for the lock to open successfully.")]
|
|
[SerializeField] private float _lockGive;
|
|
public float LockGive
|
|
{
|
|
get => _lockGive;
|
|
set => _lockGive = Mathf.Clamp(value, 1, maxGiveAmount);
|
|
}
|
|
[Tooltip("If the lock pick is within this distance to the angle range which the lock will open, the lock will turn partially when an open attempt is made.")]
|
|
[SerializeField] private float _closeDistance;
|
|
public float CloseDistance
|
|
{
|
|
get => _closeDistance;
|
|
set => _closeDistance = Mathf.Clamp(value, 5, maxCloseDistance);
|
|
}
|
|
[Tooltip("The amount of time before a lock pick breaks when the lock is unable to be opened, but the player is attempting to open it.")]
|
|
[Range(0f, 5f)] public float breakTime = 1f;
|
|
|
|
|
|
[Header("Animation Trigger Strings")]
|
|
public string openTrigger = "OpenPadlock";
|
|
public string closeTrigger = "ClosePadlock";
|
|
public string lockPickBreakTrigger = "BreakLockpick1";
|
|
public string lockPickInsertTrigger = "InsertLockpick";
|
|
|
|
// Private animation hashes
|
|
private int _openTrigger;
|
|
private int _closeTrigger;
|
|
private int _lockpickBreakTrigger;
|
|
private int _lockpickInsertTrigger;
|
|
|
|
[Header("Plumbing")]
|
|
public GameObject keyhole; // The keyhole with lockpick A that turns the entire keyhole object to open it
|
|
public GameObject lockpickObject; // The lockpick that turns to match the secret lockAngle
|
|
public Animator lockpickAnimator; // Animator on the lockpick in the lockpickObject
|
|
private Animator _padlockAnimator;
|
|
public GameObject padlock1; // Link to the padlock 1 game object
|
|
public GameObject button;
|
|
private LockEmissive _lockEmissive; // Link to the lockEmissive script on this object
|
|
public LocksetAudio audioTurnClick;
|
|
public LocksetAudio audioSqueek;
|
|
public LocksetAudio audioOpen;
|
|
public LocksetAudio audioJiggle;
|
|
public LocksetAudio audioJiggle2;
|
|
public LocksetAudio audioJiggle3;
|
|
public LocksetAudio audioPadlockOpen;
|
|
public LocksetAudio audioPadlockJiggle;
|
|
public LocksetAudio audioLockpickBreak;
|
|
public LocksetAudio audioLockpickEnter;
|
|
public LocksetAudio audioLockpickClick;
|
|
|
|
// Audio Settings
|
|
[Range(0f, 1f)] public float clickVolumeMin = 0.1f;
|
|
[Range(0f, 1f)] public float clickVolumeMax = 0.4f;
|
|
[Range(0, 100)] public int clickChance = 100;
|
|
[Range(0f, 15f)] public float clickRate = 10f;
|
|
[Range(0f, 1f)] public float squeekVolumeMin = 0.1f;
|
|
[Range(0f, 1f)] public float squeekVolumeMax = 0.4f;
|
|
[Range(0, 100)] public int squeekChance = 50;
|
|
[Range(0f, 360f)] public float squeekRate = 20f;
|
|
|
|
// Private variables
|
|
private float breakCounter; // Counter for taking a break after a broken lockpick
|
|
private float breakTimeCounter;
|
|
private float shakeTimer; // Counter for the shake Time
|
|
private bool isShaking; // Whether we are currently shaking or not
|
|
private Vector3 preshakeLockpick; // Saves the pre-shake angles
|
|
private Vector3 preshakeKeyhole; // Saves the pre-shake angles
|
|
private float _lockpickAnglePrev;
|
|
private float squeekTimer = 0f;
|
|
public bool buttonDown;
|
|
|
|
public float BreakTimeCounter => breakTimeCounter;
|
|
public float LockPickAngle => GetAngle(LockpickAngles.z);
|
|
public float KeyholeAngle => GetAngle(KeyholeAngles.z);
|
|
|
|
|
|
void OnValidate()
|
|
{
|
|
LockAngle = LockAngle;
|
|
LockGive = LockGive;
|
|
CloseDistance = CloseDistance;
|
|
|
|
_pickAngleMin = Mathf.Clamp(_pickAngleMin, minLockAngle, maxLockAngle);
|
|
_pickAngleMax = Mathf.Clamp(_pickAngleMax, minLockAngle, maxLockAngle);
|
|
|
|
minGiveAmount = Mathf.Clamp(minGiveAmount, 1f, 360f);
|
|
maxGiveAmount = Mathf.Clamp(maxGiveAmount, 1f, 360f);
|
|
minCloseDistance = Mathf.Clamp(minCloseDistance, 5f, 360f);
|
|
maxCloseDistance = Mathf.Clamp(maxCloseDistance, 5f, 360f);
|
|
}
|
|
|
|
public void EditorOnValidate()
|
|
{
|
|
#if UNITY_EDITOR
|
|
EditorUtility.SetDirty(this);
|
|
#endif
|
|
OnValidate();
|
|
}
|
|
|
|
void Awake()
|
|
{
|
|
squeekTimer = squeekRate;
|
|
|
|
_pickAnglesDefault = LockpickAngles;
|
|
_lockEmissive = GetComponent<LockEmissive>();
|
|
|
|
_padlockAnimator = padlock1.GetComponent<Animator>();
|
|
_closeTrigger = Animator.StringToHash(closeTrigger);
|
|
_openTrigger = Animator.StringToHash(openTrigger);
|
|
_lockpickBreakTrigger = Animator.StringToHash(lockPickBreakTrigger);
|
|
_lockpickInsertTrigger = Animator.StringToHash(lockPickInsertTrigger);
|
|
|
|
if (resetOnAwake)
|
|
ResetLock();
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
PassValuesToEmissiveScript();
|
|
|
|
if (BreakingForAnimation())
|
|
return;
|
|
|
|
HandlePlayerInput();
|
|
}
|
|
|
|
private void HandlePlayerInput()
|
|
{
|
|
if (openPressure > 0)
|
|
{
|
|
TryToTurnKeyhole();
|
|
}
|
|
else
|
|
{
|
|
StopShaking();
|
|
ReturnKeyholeToDefaultPosition();
|
|
TurnLockpick(turnSpeedLockpick * lockpickPressure);
|
|
}
|
|
}
|
|
|
|
private bool BreakingForAnimation()
|
|
{
|
|
if (breakCounter > 0f)
|
|
{
|
|
breakCounter -= Time.deltaTime;
|
|
ReturnKeyholeToDefaultPosition();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void ReturnKeyholeToDefaultPosition()
|
|
{
|
|
TurnKeyhole(-returnSpeedKeyhole);
|
|
}
|
|
|
|
private void TryToTurnKeyhole()
|
|
{
|
|
if (LockCanTurn)
|
|
{
|
|
TurnKeyhole(turnSpeedKeyhole * openPressure);
|
|
|
|
if (KeyholeTurnValue <= 0 && LockpickIsInPosition)
|
|
OpenLock();
|
|
}
|
|
else
|
|
{
|
|
Shake();
|
|
}
|
|
}
|
|
|
|
private void PassValuesToEmissiveScript()
|
|
{
|
|
_lockEmissive.breakpointValue = Mathf.Clamp(breakTimeCounter / breakTime, 0, 1);
|
|
_lockEmissive.successValue = Mathf.Clamp(KeyholeTurnValue, 0, 1);
|
|
}
|
|
|
|
private bool LockpickIsInPosition =>
|
|
LockPickAngle < _lockAngle + _lockGive && LockPickAngle > _lockAngle - _lockGive;
|
|
|
|
private void Shake()
|
|
{
|
|
// If we are not already shaking, save the original rotations.
|
|
if (!isShaking)
|
|
{
|
|
if (audioPadlockJiggle && padlock1.activeSelf)
|
|
{
|
|
audioPadlockJiggle.PlayLoop();
|
|
}
|
|
else if (audioJiggle && !padlock1.activeSelf)
|
|
{
|
|
audioJiggle.PlayLoop();
|
|
audioJiggle2?.PlayLoop();
|
|
audioJiggle3?.PlayLoop();
|
|
}
|
|
preshakeKeyhole = KeyholeAngles;
|
|
preshakeLockpick = LockpickAngles;
|
|
isShaking = true;
|
|
}
|
|
|
|
// Check breakTimeCounter to stop shaking at the right time
|
|
breakTimeCounter += Time.deltaTime;
|
|
if (breakTimeCounter > breakTime)
|
|
{
|
|
StopShaking();
|
|
BreakLockpick();
|
|
return;
|
|
}
|
|
|
|
shakeTimer -= Time.deltaTime;
|
|
if (shakeTimer <= 0)
|
|
{
|
|
// Start with the current values
|
|
Vector3 newShakeKeyhole = preshakeKeyhole;
|
|
Vector3 newShakeLockpick = preshakeLockpick;
|
|
|
|
// Add some modification
|
|
newShakeKeyhole.z += Random.Range(-maxShake, maxShake);
|
|
newShakeLockpick.z += Random.Range(-maxShake, maxShake);
|
|
|
|
// Set the value + modification
|
|
SetKeyholeAngles(newShakeKeyhole);
|
|
SetLockpickAngles(newShakeLockpick);
|
|
|
|
// Reset the timer
|
|
shakeTimer = shakeTime;
|
|
}
|
|
}
|
|
|
|
private void StopShaking()
|
|
{
|
|
if (isShaking)
|
|
{
|
|
if (audioPadlockJiggle && padlock1.activeSelf)
|
|
{
|
|
audioPadlockJiggle.StopLoop();
|
|
}
|
|
else if (audioJiggle && !padlock1.activeSelf)
|
|
{
|
|
audioJiggle.StopLoop();
|
|
audioJiggle2?.StopLoop();
|
|
audioJiggle3?.StopLoop();
|
|
}
|
|
SetKeyholeAngles(preshakeKeyhole);
|
|
SetLockpickAngles(preshakeLockpick);
|
|
isShaking = false;
|
|
}
|
|
}
|
|
|
|
private void SetKeyholeAngles(Vector3 value)
|
|
{
|
|
keyhole.transform.localEulerAngles = value;
|
|
}
|
|
|
|
private void SetLockpickAngles(Vector3 value)
|
|
{
|
|
lockpickObject.transform.localEulerAngles = value;
|
|
}
|
|
|
|
private void BreakLockpick()
|
|
{
|
|
if (audioLockpickBreak)
|
|
{
|
|
audioLockpickBreak.PlayOnce();
|
|
}
|
|
breakCounter = breakPause; // Set so we can't do any actions for a short time
|
|
breakTimeCounter = 0f; // Reset the breakCounter
|
|
ResetLockpickPosition(); // Reset the lockpick position
|
|
lockpickAnimator.SetTrigger(_lockpickBreakTrigger); // Play the break animation
|
|
lockpickBroke.Invoke(); // Invoke this event in case other scripts are listening
|
|
|
|
if (audioLockpickEnter)
|
|
{
|
|
audioLockpickEnter.DelayPlay(1f);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call this when the lock is open successfully.
|
|
/// </summary>
|
|
public void OpenLock()
|
|
{
|
|
if (!_lockIsOpen)
|
|
{
|
|
if (audioOpen && !padlock1.activeSelf)
|
|
{
|
|
audioOpen.PlayOnce();
|
|
}
|
|
else if (audioPadlockOpen && padlock1.activeSelf)
|
|
{
|
|
audioPadlockOpen.PlayOnce();
|
|
}
|
|
|
|
// Only run this on the padlock 1
|
|
if (padlock1.activeSelf)
|
|
_padlockAnimator.SetTrigger(_openTrigger);
|
|
|
|
// Invoke the event for any other scripts that are listening
|
|
lockOpen.Invoke();
|
|
|
|
_lockIsOpen = true;
|
|
}
|
|
}
|
|
|
|
private void DoSqueekAudio(float speed)
|
|
{
|
|
if (audioSqueek)
|
|
{
|
|
if (squeekRate > 0)
|
|
{
|
|
squeekTimer -= Mathf.Abs(speed) * Time.deltaTime;
|
|
if (squeekTimer <= 0)
|
|
{
|
|
if (Random.Range(0, 100) < squeekChance)
|
|
{
|
|
audioSqueek.PlayAudioClip(Random.Range(squeekVolumeMin, squeekVolumeMax));
|
|
}
|
|
squeekTimer = squeekRate;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private float GetAngle(float eulerAngle)
|
|
{
|
|
float angle = eulerAngle;
|
|
angle %= 360;
|
|
if (angle > 180)
|
|
angle -= 360;
|
|
return angle;
|
|
}
|
|
|
|
private void TurnLockpick(float speed)
|
|
{
|
|
// If we are at or outside of our max range, return
|
|
if (LockPickAngle >= _pickAngleMax && speed > 0 || LockPickAngle <= _pickAngleMin && speed < 0)
|
|
return;
|
|
|
|
// Set the new angle
|
|
Vector3 newAngle = new Vector3(LockpickAngles.x, LockpickAngles.y,
|
|
LockpickAngles.z + speed * Time.deltaTime);
|
|
SetLockpickAngles(newAngle);
|
|
|
|
DoClickAudio(speed, newAngle);
|
|
}
|
|
|
|
private void DoClickAudio(float speed, Vector3 newAngle)
|
|
{
|
|
float angleMod = newAngle.z % clickRate;
|
|
float prevMod = _lockpickAnglePrev % clickRate;
|
|
|
|
if ((speed > 0 && angleMod < prevMod) || (speed < 0 && angleMod > prevMod))
|
|
{
|
|
audioTurnClick.PlayAudioClip(Random.Range(clickVolumeMin, clickVolumeMax));
|
|
}
|
|
|
|
_lockpickAnglePrev = newAngle.z;
|
|
}
|
|
|
|
private Vector3 LockpickAngles => lockpickObject.transform.localEulerAngles;
|
|
|
|
private void TurnKeyhole(float speed)
|
|
{
|
|
// If we are at or outside of our max range, return
|
|
if (KeyholeAngle >= _keyholeAngleMax && speed > 0 || KeyholeAngle <= _keyholeAngleDefault && speed < 0)
|
|
return;
|
|
|
|
// Set the new angle
|
|
SetKeyholeAngles(new Vector3(KeyholeAngles.x, KeyholeAngles.y, KeyholeAngles.z + speed * Time.deltaTime));
|
|
|
|
DoSqueekAudio(speed);
|
|
}
|
|
|
|
private Vector3 KeyholeAngles => keyhole.transform.localEulerAngles;
|
|
|
|
public float KeyholeTurnValue => (_keyholeAngleMax - KeyholeAngle) / (_keyholeAngleMax - _keyholeAngleDefault);
|
|
|
|
public void SetLock(float newLockAngle, float newLockGive, float newCloseDistance)
|
|
{
|
|
_lockAngle = newLockAngle;
|
|
_lockGive = newLockGive;
|
|
_closeDistance = newCloseDistance;
|
|
|
|
if (audioLockpickEnter)
|
|
{
|
|
audioLockpickEnter.DelayPlay(0.7f);
|
|
}
|
|
|
|
ResetLockpickPosition();
|
|
lockpickAnimator.SetTrigger(_lockpickInsertTrigger); // Play the animation
|
|
}
|
|
|
|
public void SetLock(float lockAngleMin, float lockAngleMax, float lockGiveMin,
|
|
float lockGiveMax, float closeDistanceMin, float closeDistanceMax)
|
|
{
|
|
SetLock(Random.Range(lockAngleMin, lockAngleMax),
|
|
Random.Range(lockGiveMin, lockGiveMax),
|
|
Random.Range(closeDistanceMin, closeDistanceMax));
|
|
}
|
|
|
|
public void ResetLock()
|
|
{
|
|
LockIsOpen = false;
|
|
SetLock(minLockAngle, maxLockAngle,
|
|
minGiveAmount, maxGiveAmount,
|
|
minCloseDistance, maxCloseDistance);
|
|
}
|
|
|
|
public void ResetLockpickPosition()
|
|
{
|
|
SetLockpickAngles(_pickAnglesDefault);
|
|
if (padlock1.activeSelf)
|
|
_padlockAnimator.SetTrigger(_closeTrigger);
|
|
}
|
|
|
|
public bool LockCanTurn =>
|
|
!(LockPickAngle < GetAngle(_lockAngle) - _lockGive - (_closeDistance * KeyholeTurnValue)) &&
|
|
!(LockPickAngle > GetAngle(_lockAngle) + _lockGive + (_closeDistance * KeyholeTurnValue));
|
|
}
|
|
} |