using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TMPro; using UnityEngine; using UnityEngine.XR.Interaction.Toolkit; [Serializable] public class BoolRow { public List cells = new(); public BoolRow(List cells) { this.cells = cells; } } [Serializable] public class ShapeScannerConfiguration { public int columns = 6; public float requiredCorrectPercentage = 80.0f; public List rows = new(); public ShapeScannerConfiguration(int columns, float requiredCorrectPercentage, List rows) { this.columns = columns; this.requiredCorrectPercentage = requiredCorrectPercentage; this.rows = rows; } } public class ShapeScanner : MonoBehaviour { public delegate void OnConfigCompletedDelegate(); public event OnConfigCompletedDelegate OnConfigCompleted; public delegate void OnAllCompletedDelegate(); public event OnAllCompletedDelegate OnAllCompleted; [Header("Static References")] public List configurations = new(); public ShapeScannerRay rayPrefab; public Transform raySpawnCorner1; public Transform raySpawnCorner2; public Transform rayParent; public TextMeshProUGUI currentConfigurationDisplay; public TextMeshProUGUI correctPercentageDisplay; public ParticleSystem particles; public XRInteractionManager interactionManager; [Header("Materials")] public Material requiredAndActive; public Material requiredAndPassive; public Material notRequiredAndActive; public Material notRequiredAndPassive; [Header("On Completion")] public LineRenderer lineRenderer; public Transform lineStart; public Transform lineEnd; public MeshRenderer activatableRenderer; public Material activatableActiveMaterial; public HingeJoint doorJoint; public Collider doorInvisibleWall; private List existingRays; private float raySpawnDistanceX; private float raySpawnDistanceZ; private int currentConfiguration; private int totalRayCount; private int correctRayCount; private bool isCompleted; // Start is called before the first frame update void Start() { existingRays = new List(); raySpawnDistanceX = raySpawnCorner2.localPosition.x - raySpawnCorner1.localPosition.x; raySpawnDistanceZ = raySpawnCorner2.localPosition.z - raySpawnCorner1.localPosition.z; lineRenderer.enabled = false; isCompleted = false; currentConfiguration = 0; InitializeConfiguration(configurations[0]); UpdateCurrentConfigurationDisplay(currentConfiguration); } // Update is called once per frame void Update() { if (lineRenderer.enabled) { lineRenderer.SetPosition(0, lineStart.position); lineRenderer.SetPosition(1, lineEnd.position); } } private void InitializeConfiguration(ShapeScannerConfiguration configuration) { // Create rays int rayRowCount = configuration.rows.Count; for (int i = 0; i < rayRowCount; i++) { float rayPosZ = raySpawnCorner1.localPosition.z + i * raySpawnDistanceZ / (rayRowCount - 1); for (int j = 0; j < rayRowCount; j++) { float rayPosX = raySpawnCorner1.localPosition.x + j * raySpawnDistanceX / (rayRowCount - 1); Vector3 rayPos = new Vector3(rayPosX, 0, rayPosZ); ShapeScannerRay ray = Instantiate(rayPrefab, rayParent); ray.transform.localPosition = rayPos; existingRays.Add(ray); bool rayCollisionRequired = configuration.rows[i].cells[j]; if (rayCollisionRequired) { ray.Initialize(this, rayCollisionRequired, requiredAndActive, requiredAndPassive); } else { ray.Initialize(this, rayCollisionRequired, notRequiredAndActive, notRequiredAndPassive); } } } // Count total rays and required collision rays totalRayCount = configuration.rows.SelectMany(row => row.cells).Count(); correctRayCount = configuration.rows.SelectMany(row => row.cells).Count(cell => !cell); float percentage = CalculateCorrectPercentage(); UpdateDisplay(percentage); } private void OnConfigurationComplete() { // Destroy colliding scannable objects HashSet allCollidingObjects = new(); foreach (ShapeScannerRay ray in existingRays) { allCollidingObjects.UnionWith(ray.GetCollidingObjects()); } foreach (GameObject collidingObject in allCollidingObjects) { // Release player holding it var grab = collidingObject.GetComponent(); if (grab != null && grab.isSelected) { grab.enabled = false; } collidingObject.transform.position = Vector3.zero; Destroy(collidingObject); } // Destroy all existing rays foreach (ShapeScannerRay ray in existingRays) { Destroy(ray.gameObject); } existingRays.Clear(); correctRayCount = 0; // Play sound effect and emit particles AudioManager.Instance.PlayAttachedInstance(FMODEvents.Instance.ShapeScannerSuccess, gameObject); if (!particles.isPlaying) particles.Play(); particles.Emit(100); OnConfigCompleted?.Invoke(); } private float CalculateCorrectPercentage() { return (float) correctRayCount / totalRayCount * 100; } public async void IncrementCorrectRayCount() { correctRayCount++; float correctPercentage = CalculateCorrectPercentage(); UpdateDisplay(correctPercentage); if (isCompleted) { return; } if (correctPercentage >= configurations[currentConfiguration].requiredCorrectPercentage) { OnConfigurationComplete(); await Task.Delay(1000); UpdateCurrentConfigurationDisplay(currentConfiguration + 1); if (currentConfiguration + 1 < configurations.Count) { currentConfiguration++; InitializeConfiguration(configurations[currentConfiguration]); } else { // Initialize configuration with no rays requiring collision List rows = Enumerable.Repeat(new BoolRow(Enumerable.Repeat(false, 6).ToList()), 6).ToList(); ShapeScannerConfiguration configuration = new ShapeScannerConfiguration(6, 100, rows); InitializeConfiguration(configuration); OnCompleteAll(); } } } public void DecrementCorrectRayCount() { correctRayCount--; UpdateDisplay(CalculateCorrectPercentage()); } private void UpdateDisplay(float percentage) { correctPercentageDisplay.text = Mathf.Round(percentage).ToString() + " %"; } private void UpdateCurrentConfigurationDisplay(int confNumber) { currentConfigurationDisplay.text = confNumber.ToString() + " / " + configurations.Count; } private void OnCompleteAll() { isCompleted = true; // Change card reader color to green activatableRenderer.material = activatableActiveMaterial; // Enable door to be opened JointLimits doorJointLimits = doorJoint.limits; doorJointLimits.min = -90f; doorJointLimits.max = 90f; doorJoint.limits = doorJointLimits; // Disable invisible wall doorInvisibleWall.enabled = false; // Create line renderer from shape scanner to door card reader lineRenderer.enabled = true; OnAllCompleted?.Invoke(); } }