Flashlight
- Top-Down Survival Horror Game, Unity
- Flashlight Mechanic: The flashlight attracts monsters, requiring strategic use to navigate and manipulate their movements.
- Showcase: Featured in NYU Game Center 2019 Halloween Showcase
contributions
- 01Puzzle and Level Design
- 02Light Mechanic with dynamic field-of-view system that visualizes sight, detects and handles targets in real time
- 03Atmospheric Audio Implementation
Level Design
- Setting in abandoned house with tight, confined spaces
- Circular Level Design with natural, non-linear paths
- Wires as visual guides and puzzle clues
FOV Implementation
- Initialization of view angle and mesh creation
- Real-time sight area visualization
- Target identification within field of view
- Status updates when targets become visible
FieldOfView.cs
| 1 | using System.Collections; |
| 2 | using System.Collections.Generic; |
| 3 | using UnityEngine; |
| 4 | |
| 5 | public class FieldOfView : MonoBehaviour |
| 6 | { |
| 7 | public static bool on = false; |
| 8 | public float viewRadius; |
| 9 | [Range(0,360)] |
| 10 | public float viewAngle; |
| 11 | public float viewAngleAmount; |
| 12 | public LayerMask targetMask; |
| 13 | public LayerMask obstacleMask; |
| 14 | [HideInInspector] |
| 15 | public List<Transform> visibleTargets = new List<Transform>(); |
| 16 | public float meshResolution; |
| 17 | public int edgeResolveIterations; |
| 18 | public float edgeDstThreshold; |
| 19 | public MeshFilter viewMeshFilter; |
| 20 | public Transform monster; |
| 21 | |
| 22 | Mesh viewMesh; |
| 23 | |
| 24 | private void Start() |
| 25 | { |
| 26 | viewAngle = 0; |
| 27 | viewMesh = new Mesh(); |
| 28 | viewMesh.name = "View Mesh"; |
| 29 | viewMeshFilter.mesh = viewMesh; |
| 30 | StartCoroutine("FindTargetsWithDelay", .2f); |
| 31 | } |
| 32 | |
| 33 | IEnumerator FindTargetsWithDelay(float delay) |
| 34 | { |
| 35 | while (true) |
| 36 | { |
| 37 | yield return new WaitForSeconds(delay); |
| 38 | FindVisibleTargets(); |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | private void Update() |
| 43 | { |
| 44 | if (Input.GetKeyDown(KeyCode.Mouse0)) |
| 45 | { |
| 46 | if (on) |
| 47 | { |
| 48 | on = false; |
| 49 | viewAngle = 0; |
| 50 | } |
| 51 | else if (!on) |
| 52 | { |
| 53 | on = true; |
| 54 | viewAngle = viewAngleAmount; |
| 55 | } |
| 56 | } |
| 57 | SetStayToFalse(); |
| 58 | } |
| 59 | |
| 60 | private void LateUpdate() |
| 61 | { |
| 62 | DrawFieldOfView(); |
| 63 | } |
| 64 | |
| 65 | void SetStayToFalse() |
| 66 | { |
| 67 | if (visibleTargets.Count > 0) |
| 68 | { |
| 69 | for (int i = 0; i < visibleTargets.Count; i++) |
| 70 | { |
| 71 | visibleTargets[i].gameObject.GetComponent<StoreEnemyTarget>().enemyTarget.GetComponent<EnemyTargetSetter>().activated = true; |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | void FindVisibleTargets() |
| 77 | { |
| 78 | visibleTargets.Clear(); |
| 79 | Collider2D[] targetsInViewRadius = Physics2D.OverlapCircleAll(transform.position, viewRadius, targetMask); |
| 80 | |
| 81 | for (int i = 0; i < targetsInViewRadius.Length; i++) |
| 82 | { |
| 83 | Transform target = targetsInViewRadius[i].transform; |
| 84 | Vector3 dirToTarget = (target.position - transform.position).normalized; |
| 85 | |
| 86 | if (Vector3.Angle(transform.up, dirToTarget) < viewAngle / 2) |
| 87 | { |
| 88 | float dstToTarget = Vector3.Distance(transform.position, target.position); |
| 89 | |
| 90 | if (!Physics2D.Raycast(transform.position, dirToTarget, dstToTarget, obstacleMask)) |
| 91 | { |
| 92 | visibleTargets.Add(target); |
| 93 | } |
| 94 | } |
| 95 | } |
| 96 | } |
| 97 | |
| 98 | void DrawFieldOfView() |
| 99 | { |
| 100 | int stepCount = Mathf.RoundToInt(viewAngle * meshResolution); |
| 101 | float stepAngleSize = viewAngle / stepCount; |
| 102 | List<Vector3> viewPoints = new List<Vector3>(); |
| 103 | ViewCastInfo oldViewCast = new ViewCastInfo(); |
| 104 | |
| 105 | for (int i = 0; i <= stepCount; i++) |
| 106 | { |
| 107 | float angle = transform.eulerAngles.y - viewAngle / 2 + stepAngleSize * i; |
| 108 | ViewCastInfo newViewCast = ViewCast(angle); |
| 109 | |
| 110 | if (i > 0) |
| 111 | { |
| 112 | bool edgeDstThresholdExceeded = Mathf.Abs(oldViewCast.dst - newViewCast.dst) > edgeDstThreshold; |
| 113 | |
| 114 | if (oldViewCast.hit != newViewCast.hit || (oldViewCast.hit && newViewCast.hit && edgeDstThresholdExceeded)) |
| 115 | { |
| 116 | Edgeinfo edge = FindEdge(oldViewCast, newViewCast); |
| 117 | if (edge.pointA != Vector3.zero) { viewPoints.Add(edge.pointA); } |
| 118 | if (edge.pointB != Vector3.zero) { viewPoints.Add(edge.pointB); } |
| 119 | } |
| 120 | } |
| 121 | |
| 122 | viewPoints.Add(newViewCast.point); |
| 123 | oldViewCast = newViewCast; |
| 124 | } |
| 125 | |
| 126 | int vertexCount = viewPoints.Count + 1; |
| 127 | Vector3[] vertices = new Vector3[vertexCount]; |
| 128 | int[] triangles = new int[(vertexCount - 2) * 3]; |
| 129 | vertices[0] = Vector3.zero; |
| 130 | |
| 131 | for (int i = 0; i < vertexCount - 1; i++) |
| 132 | { |
| 133 | vertices[i + 1] = transform.InverseTransformPoint(viewPoints[i]); |
| 134 | if (i < vertexCount - 2) |
| 135 | { |
| 136 | triangles[i * 3] = 0; |
| 137 | triangles[i * 3 + 1] = i + 1; |
| 138 | triangles[i * 3 + 2] = i + 2; |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | viewMesh.Clear(); |
| 143 | viewMesh.vertices = vertices; |
| 144 | viewMesh.triangles = triangles; |
| 145 | viewMesh.RecalculateNormals(); |
| 146 | } |
| 147 | |
| 148 | public struct ViewCastInfo |
| 149 | { |
| 150 | public bool hit; |
| 151 | public Vector3 point; |
| 152 | public float dst; |
| 153 | public float angle; |
| 154 | |
| 155 | public ViewCastInfo(bool _hit, Vector3 _point, float _dst, float _angle) |
| 156 | { |
| 157 | hit = _hit; point = _point; dst = _dst; angle = _angle; |
| 158 | } |
| 159 | } |
| 160 | |
| 161 | public struct Edgeinfo |
| 162 | { |
| 163 | public Vector3 pointA; |
| 164 | public Vector3 pointB; |
| 165 | |
| 166 | public Edgeinfo(Vector3 _pointA, Vector3 _pointB) |
| 167 | { |
| 168 | pointA = _pointA; pointB = _pointB; |
| 169 | } |
| 170 | } |
| 171 | } |
Audio Design
Ambiance:
Background Depression
Player Interaction Cues:
Door Closes
Light Torch Open
Locked
Player Footstep
Monster Sounds:
Monster Roar 1
Monster Roar 2