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 { //[ExecuteInEditMode] [RequireComponent(typeof(LockEmissive))] public class Cipher : MonoBehaviour { // Events UnityEvent lockOpen = new UnityEvent(); [Header("Speed Settings")] [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; [Tooltip("Speed of the wheel when turning.")] public float turnSpeed = 180f; [Header("Lock Setup")] [Tooltip("Number of symbols on the wheel. Should be spaced evenly.")] public int symbolCount = 18; [Tooltip("How close do we have to be to perfectly aligned for the placement of a wheel to count?")] public float closeEnough = 3f; [Tooltip("If true, wheels will \"snap\" into a new random spot on setup. If false, they will rotate to those spots.")] public bool quickReset; [Tooltip("If true, the active wheel will be highlighted using the emissive map of the material.")] public bool highlightActiveWheel = true; [Tooltip("If true, wheels will move to the closest spot when the player stops input movement.")] public bool moveToClosestSpot; [Header("Lock Details")] [Tooltip("When true, the lock has been opened.")] public bool isOpen; [Tooltip("When true, the lock will reset itself on awake. Set this to false if you are choosing a specific combination.")] public bool resetOnAwake; [Header("Animation Trigger Strings")] public string openTrigger = "Open"; public string closeTrigger = "Close"; // Private animation hashes private int _openTrigger; private int _closeTrigger; // Audio Settings [Range(0f, 1f)] public float clickVolumeMin = 0.1f; [Range(0f, 1f)] public float clickVolumeMax = 0.4f; [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, 15f)] public float squeekRate = 3.5f; // Private variables private int _activeWheel; public float[] unlockAngle; // All the unlock angles for each wheel public float[] startAngle; // All the starting angles for each wheel public float[] turnedDistance; // amount each wheel has been turned public float[] turnedDistancePrev; // amount each wheel has been turned public bool[] isMoving; // Records whether the wheels are moving public bool playAudioOnSetup = true; private bool isReady; // WHen true, player can interact private bool isShaking; // When true, the lock is shaking (trying to be opened but in the wrong combination) private Vector3 preshake; // Saves the pre-shake angles private float shakeTimer; // Counter for the shake Time private bool _visualizeSolution; private bool _visualizeStart; private float _speed; private float squeekTimer = 0f; private float getReadyPostReadyTimer = 0.3f; [Header("Plumbing")] public GameObject[] wheels; public LocksetAudio audioClick; public LocksetAudio audioSqueek; public LocksetAudio audioJiggle; public LocksetAudio audioJiggle2; public LocksetAudio audioJiggle3; public LocksetAudio audioOpen; private Animator animator; private LockEmissive lockEmissive; private LocksetAudio _locksetAudio; public int ActiveWheel => _activeWheel; public bool VisualizeSolution { get { return _visualizeSolution; } set { _visualizeSolution = value; } } public bool VisualizeStart { get { return _visualizeStart; } set { _visualizeStart = value; } } public float ActiveWheelStart { get { return startAngle[ActiveWheel]; } set { startAngle[ActiveWheel] = value; } } public float ActiveWheelSolution { get { return unlockAngle[ActiveWheel]; } set { unlockAngle[ActiveWheel] = value; } } void OnValidate() { #if UNITY_EDITOR if (unlockAngle.Length != wheels.Length ) { Debug.Log("Updating Lengths"); unlockAngle = new float[wheels.Length]; startAngle = new float[wheels.Length]; turnedDistance = new float[wheels.Length]; isMoving = new bool[wheels.Length]; } if (_visualizeSolution) SetRotationToSolution(); else if (_visualizeStart) SetRotationToStart(); else SetRotationToZero(); for (int i = 0; i < wheels.Length; i++) { unlockAngle[i] = ClosestSpot(unlockAngle[i]); } #endif } public void EditorOnValidate() { #if UNITY_EDITOR EditorUtility.SetDirty(this); #endif OnValidate(); } private void SetRotationToSolution() { for (int i = 0; i < wheels.Length; i++) { Vector3 eulerAngles = wheels[i].transform.localEulerAngles; eulerAngles.x = unlockAngle[i]; wheels[i].transform.localEulerAngles = eulerAngles; } } private void SetRotationToStart() { for (int i = 0; i < wheels.Length; i++) { Vector3 eulerAngles = wheels[i].transform.localEulerAngles; eulerAngles.x = startAngle[i]; wheels[i].transform.localEulerAngles = eulerAngles; } } private void SetRotationToZero() { for (int i = 0; i < wheels.Length; i++) { wheels[i].transform.localEulerAngles = new Vector3(); } } void Awake() { squeekTimer = squeekRate; _locksetAudio = GetComponent(); animator = GetComponent(); lockEmissive = GetComponent(); if (!highlightActiveWheel) lockEmissive.highlightRenderer = null; _openTrigger = Animator.StringToHash(openTrigger); _closeTrigger = Animator.StringToHash(closeTrigger); if (resetOnAwake) { ResetLock(); } } public float DistanceLeft(int i) { if (turnedDistance.Length >= i + 1) { float left = (unlockAngle[i] - turnedDistance[i]); if (left < -350) left += 360; if (left > 350) left -= 360; return left; } return 9999; } void Update() { // Do actions to get ready, if we are not ready if (!isReady) { GetReady(); if (playAudioOnSetup) DoAudio(); return; } if (getReadyPostReadyTimer > 0) getReadyPostReadyTimer -= Time.deltaTime; if (moveToClosestSpot) MoveToClosestSpot(); ResetMovement(); if (highlightActiveWheel) { lockEmissive.SetHighlightRenderer(wheels[_activeWheel].GetComponent()); } DoAudio(); } private void DoAudio() { for (int i = 0; i < turnedDistance.Length; i++) { float turnSpeed = 0f; if (turnedDistance[i] != turnedDistancePrev[i]) { if (getReadyPostReadyTimer <= 0 || playAudioOnSetup) { if (audioSqueek) { // Squeek if (squeekRate > 0) { squeekTimer -= Time.deltaTime; if (squeekTimer <= 0) { if (Random.Range(0, 100) < squeekChance) { audioSqueek.PlayAudioClip(Random.Range(squeekVolumeMin, squeekVolumeMax)); } squeekTimer = squeekRate; } } } if (audioClick) { // Clicks turnSpeed = turnedDistance[i] - turnedDistancePrev[i]; float turnedMod = turnedDistance[i] % (360 / symbolCount); float turnedModPrev = turnedDistancePrev[i] % (360 / symbolCount); if ((turnSpeed > 0 || turnSpeed < -350) && (turnedMod < turnedModPrev || turnedMod > 0 && turnedModPrev < 0)) { audioClick.PlayAudioClip(Random.Range(clickVolumeMin, clickVolumeMax)); } else if ((turnSpeed < 0 || turnSpeed > 350) && (turnedMod > turnedModPrev || turnedMod < 0 && turnedModPrev > 0)) { audioClick.PlayAudioClip(Random.Range(clickVolumeMin, clickVolumeMax)); } else if (turnedMod == 0) { audioClick.PlayAudioClip(Random.Range(clickVolumeMin, clickVolumeMax)); } } } turnedDistancePrev[i] = turnedDistance[i]; } } } public int NextWheelIndex => (_activeWheel + 1) < wheels.Length ? (_activeWheel + 1) : 0; public int PrevWheelIndex => (_activeWheel - 1) < 0 ? (wheels.Length - 1) : (_activeWheel - 1); public void SelectWheel(int value) { _activeWheel = value < wheels.Length && value >= 0 ? value : 0; } public void ResetMovement() { for (int i = 0; i < wheels.Length; i++) { isMoving[i] = false; } } public void MoveToClosestSpot() { for (int i = 0; i < wheels.Length; i++) { // Only run this if the wheel is not moving (i.e. being moved by the player) if (!isMoving[i]) { if (turnedDistance[i] < 0) { turnedDistance[i] += 360; } /* float smallestDistance = 360f; float closestRotation = 0f; for (int d = 0; d < 360 / symbolCount; d++) { float diff = d * (360 / symbolCount); if (Mathf.Abs(Mathf.Abs(turnedDistance[i]) - diff) < smallestDistance) { smallestDistance = Mathf.Abs(Mathf.Abs(turnedDistance[i]) - diff); closestRotation = diff; } } MoveTowards(i, closestRotation); */ MoveTowards(i, ClosestSpot(Mathf.Abs(turnedDistance[i]))); } } } private float ClosestSpot(float value) { float smallestDistance = 360f; float closestRotation = 0f; for (int d = -360; d < 360 / symbolCount; d++) { float diff = d * (360 / symbolCount); if (Mathf.Abs(value - diff) < smallestDistance) { smallestDistance = Mathf.Abs(value - diff); closestRotation = diff; } } return closestRotation; } public void TryOpen() { if (!isShaking) preshake = transform.localEulerAngles; if (!isOpen) { for (int i = 0; i < wheels.Length; i++) { if (Mathf.Abs(DistanceLeft(i)) > closeEnough) { Shake(); return; } } isOpen = true; animator.SetTrigger(_openTrigger); audioOpen.PlayOnce(); // Invoke the event for any other scripts that are listening lockOpen.Invoke(); } } public void StopShaking() { transform.localEulerAngles = preshake; isShaking = false; if (audioJiggle) { audioJiggle.StopLoop(); audioJiggle2?.StopLoop(); audioJiggle3?.StopLoop(); } } private void Shake() { if (!isShaking) isShaking = true; shakeTimer -= Time.deltaTime; if (shakeTimer <= 0) { // Start with the current values Vector3 newShake = preshake; // Add some modification //newShake.z += Random.Range(-maxShake, maxShake); newShake.x += Random.Range(-maxShake, maxShake); newShake.y += Random.Range(-maxShake, maxShake); // Set the value + modification transform.localEulerAngles = newShake; // Reset the timer shakeTimer = shakeTime; } if (audioJiggle) { audioJiggle.PlayLoop(); audioJiggle2?.PlayLoop(); audioJiggle3?.PlayLoop(); } } public void GetReady() { int readyCount = 0; for (int i = 0; i < wheels.Length; i++) { int speed = 1; float distanceLeft = startAngle[i] - turnedDistance[i]; if (_visualizeSolution) { distanceLeft = unlockAngle[i] - turnedDistance[i]; } if (distanceLeft < 0) { speed = -1; } float turnAmount = speed * turnSpeed * Time.deltaTime; if (Mathf.Abs(distanceLeft) < Mathf.Abs(turnAmount)) { turnAmount = distanceLeft; readyCount++; } if (quickReset || _visualizeSolution) { turnAmount = distanceLeft; readyCount++; } SetTurnedDistance(i, turnAmount); RotateWheel(wheels[i].transform, turnAmount); } if (readyCount == wheels.Length) isReady = true; } public void MoveActiveWheel(int speed) { MoveWheel(_activeWheel, speed); } public void MoveWheel(int i, int speed, float destination = 999f) { if (isReady) { isMoving[i] = true; // Mark this wheel as currently moving float turnAmount = speed * turnSpeed * Time.deltaTime; if (destination != 999) { if (Mathf.Abs(destination - turnedDistance[i]) < Mathf.Abs(turnAmount)) { turnAmount = destination - turnedDistance[i]; } } SetTurnedDistance(i, turnAmount); RotateWheel(wheels[i].transform, turnAmount); } } private void RotateWheel(Transform transform, float value) { transform.Rotate(value, 0.0f, 0.0f, Space.Self); } public void MoveTowards(int i, float closestRotation) { if (turnedDistance[i] > closestRotation) MoveWheel(i, -1, closestRotation); else MoveWheel(i, 1, closestRotation); } public void SetTurnedDistance(int i, float distance) { turnedDistance[i] += distance; if (turnedDistance[i] > 360) turnedDistance[i] -= 360; if (turnedDistance[i] < -360) turnedDistance[i] += 360; } public void ResetLock() { Debug.Log("ResetLock"); isOpen = false; isReady = false; animator.SetTrigger(_closeTrigger); // Shuffle each wheel for (int i = 0; i < wheels.Length; i++) { unlockAngle[i] = RandomAngle; startAngle[i] = RandomAngle; } } private float RandomAngle => (Random.Range(0, symbolCount) * (360 / symbolCount) - 180); } }