
Forest of Firefly
Forest of Firefly
BACKGROUND
A dreamlike VR journey through a dark firefly forest, where players walk in circles to climb a mountain path, gather drifting fireflies with a magic wand, and paint luminous trails in the night sky.
INTERACTION WAYS
INTERACTION WAYS
Circular locomotion system
Ambient insect sounds
Holding a magic wand to draw fireflies
Create glowing strokes in the night forest.
DEVELOP TOOLS
DEVELOP TOOLS
Unity
Maya
C# Programming


01 Roaming in the Firefly Forest
01 Roaming in the Firefly Forest
01 Roaming in the Firefly Forest


02 Using wand catching fireflies
02 Using wand catching fireflies
02 Using wand catching fireflies


03 Get enough firefly energy
03 Get enough firefly energy
03 Get enough firefly energy


04 Drawing in the forest
04 Drawing in the forest
04 Drawing in the forest




public class EndlessRunnerEngine : MonoBehaviour { public ActionBasedController leftController; public ActionBasedController rightController; public Camera head; public TextMeshProUGUI debugText; public Boolean isRepeating = false; public Boolean showGuide=true; public GameObject[] LevelBlock; private GameObject[] quads; private Boolean lastQuadSetForward=true; private float offset_angle = 0; private float path_phi = 0; private int path_i = 0;//an integer counter of how many circles we have completed private float previous_phi; private float previous_path_phi; private float delta_path_phi; private int onBlock = 0;void Awake() { transform.position = Vector3.zero; transform.rotation = Quaternion.identity; transform.localScale = Vector3.one; if (showGuide) createSpaceGuide(); quads = new GameObject[4]; for (int i = 0; i < 4; i++) quads[i] = null; } void Start() { if (LevelBlock.Length == 0) { Debug.Log("Level Block array is empty!"); return; } setBlock(0, 0); setBlock(1, 1); setBlock(2, 2); setBlock(3, 3); } private void setBlock(int id) { setBlock(id, id); } private void setBlock(int blockID, int quadID) { if (LevelBlock.Length == 0) return; int qID = quadID % 4;if (qID < 0) qID += 4; int bID = blockID; if (isRepeating) bID = blockID % LevelBlock.Length; if (isRepeating && bID < 0) bID += LevelBlock.Length;Debug.Log("Set block:"+bID+" to quad:"+qID); GameObject old = quads[qID]; if (old != null) { old.SetActive(false); } if (bID < 0 || bID>=LevelBlock.Length) return; GameObject newobj = LevelBlock[bID]; quads[qID] = newobj; newobj.transform.SetParent(this.transform); newobj.transform.localRotation = Quaternion.Euler(0, -90 * (qID - 1), 0); newobj.transform.localPosition = Vector3.zero; newobj.SetActive(true); } void Update() { if (leftController == null || head == null) return; //Calculates where we are on our level (path_phi) calculatePathPhi(); //Debug.Log("Path Phi:"+path_phi); //Replaces quads with other level blocks based on path_phi. replaceQuads(); if(debugText!=null)debugText.text = "Circle:" + path_phi+ "\nOn Block:"+onBlock; }void calculatePathPhi() { //The localPosition of transform is the center of the circle. Vector2 v = new Vector2(head.transform.localPosition.x - transform.localPosition.x, head.transform.localPosition.z - transform.localPosition.z); v.Normalize(); float angle = Mathf.Atan2(v.y, v.x) / (2 * Mathf.PI); //phi is a number that goes from 0 to 0.999 around the circle float phi = angle - offset_angle; if (phi < 0) phi += 1; //if there is a jump from 0 to 1 or from 1 to 0 if (Mathf.Abs(previous_phi - phi) > 0.5) { if (previous_phi > phi) path_i += 1;//from 1 to 0, we go forward else path_i -= 1;//from 0 to 1, we go backwards } previous_phi = phi; path_phi = path_i + phi + 0.25f;//path_phi is a continuous number along the path that goes beyond 1 or even below 0 if you move backwards. //we added 0.25 so that you don't start at 0 in the beginning of the level. delta_path_phi = path_phi - previous_path_phi; previous_path_phi = path_phi; //This script sets the center of the circle when you click the left controller's trigger if (leftController.selectAction.action.WasPressedThisFrame()) { Vector3 pos = transform.localPosition; pos.x = leftController.transform.localPosition.x; pos.z = leftController.transform.localPosition.z; transform.localPosition = pos; transform.localRotation = Quaternion.Euler(0f, -angle * 360, 0f); offset_angle = angle; } } void replaceQuads() { int block = (int)Mathf.Floor(path_phi / 0.25f); float blockf = path_phi / 0.25f - block; if (delta_path_phi > 0 && (block > onBlock || !lastQuadSetForward) && blockf > 0.6) { lastQuadSetForward = true; onBlock = block;//forward setBlock(onBlock + 2); } else if (delta_path_phi < 0 && (block < onBlock || lastQuadSetForward) && blockf < 0.4) { lastQuadSetForward = false; onBlock = block;//backwards setBlock(onBlock-2); } } void createSpaceGuide() { float radius = 0.8f; float height = 0.73f; GameObject table = GameObject.CreatePrimitive(PrimitiveType.Cylinder); table.transform.SetParent(this.transform); //you need to move the cylinder up because the default pivot is at the center of the cylinder. table.transform.position = new Vector3(0, height * 0.5f, 0); // Scale: // Unity default cylinder radius = 0.5 → so scale factor = radius / 0.5 = radius * 2 // Unity default cylinder height = 2 → so scale factor = height / 2 = height * 0.5 table.transform.localScale = new Vector3(radius * 2, height * 0.5f, radius * 2);for (int i = 0; i < wallCount; i++) { // angle around the circle float angle = i * Mathf.PI * 2 / wallCount; float x = Mathf.Cos(angle) * arenaRadius; float z = Mathf.Sin(angle) * arenaRadius; GameObject wall = GameObject.CreatePrimitive(PrimitiveType.Plane); wall.transform.SetParent(this.transform); // Position plane wall.transform.localPosition = new Vector3(x, wallHeight * 0.5f, z); // Rotate plane to face inward wall.transform.LookAt(this.transform.position + new Vector3(0, wallHeight * 0.5f, 0)); // Plane is 10×10, so adjust scale to match wall height float wallWidth = (2 * Mathf.PI * arenaRadius) / wallCount; // approximate arc length wall.transform.localScale = new Vector3(wallWidth / 10f, 1f, wallHeight / 10f); // Rotate upright (Plane normal is +Y, so tilt 90° around X) wall.transform.Rotate(90, 0, 0); } float barHeight = 0.1f; GameObject bar = GameObject.CreatePrimitive(PrimitiveType.Cube); bar.transform.SetParent(this.transform); bar.transform.localPosition = new Vector3(0, barHeight * 0.5f, 0); bar.transform.localScale = new Vector3(2 * arenaRadius, barHeight, 0.1f); bar.GetComponent<Renderer>().material.color = Color.red; bar = GameObject.CreatePrimitive(PrimitiveType.Cube); bar.transform.SetParent(this.transform); bar.transform.localPosition = new Vector3(0, barHeight * 0.5f, 0); bar.transform.localScale = new Vector3(0.1f, barHeight, 2 * arenaRadius); }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; public class HandTouchFirefly : MonoBehaviour { bool isTouched = false; public InputActionReference triggerPressed = null; float triggerValue; GameObject touchedFirefly; public ParticleSystem particles; LineRenderer line; bool isLineActive = false; private AudioSource audioSource; public AudioClip destorySound; public FlockController flockController; void Start() { audioSource = gameObject.AddComponent<AudioSource>(); line = gameObject.AddComponent<LineRenderer>(); line.positionCount = 2; line.enabled = false; line.material = new Material(Shader.Find("Sprites/Default")); line.startColor = new Color(0f,1f,0f,0.5f); line.endColor = new Color(0f,1f,0f,1f); line.startWidth = 0.002f; line.endWidth = 0.002f; } void Update() { triggerValue = triggerPressed.action.ReadValue<float>(); if (isTouched && touchedFirefly != null) { if (!isLineActive) { isLineActive = true; line.enabled = true; } line.SetPosition(0, transform.position); line.SetPosition(1, touchedFirefly.transform.position); } else { if (isLineActive) { line.enabled = false; isLineActive = false; } } if (triggerValue >= 0.5f && isTouched) { isTouched = false; StartCoroutine(FlyToHand(touchedFirefly, transform)); particles.Stop(true, ParticleSystemStopBehavior.StopEmittingAndClear); particles.Play(); isLineActive = true; line.enabled = true; UIsyetem bar = FindObjectOfType<UIsyetem>(); if (bar != null) { bar.ShowAndAutoHide(0.5f); bar.AddProgress(); Debug.Log("ui WORKS"); } if (destorySound != null) { audioSource.PlayOneShot(destorySound); } } } void OnTriggerEnter(Collider other) { if (other.CompareTag("firefly")) { touchedFirefly = other.gameObject; isTouched = true; if (touchedFirefly.GetComponent<FireflyTouchCrystal>() == null) { var script = touchedFirefly.AddComponent<FireflyTouchCrystal>(); script.handScript = this; } Debug.Log("Firefly Touched"); } } void OnTriggerExit(Collider other) { if (other.CompareTag("firefly")) { isTouched = false; } } public void FireflyHitCrystal(GameObject firefly) { line.enabled = false; isLineActive = false; Destroy(firefly); if(firefly.TryGetComponent<FlockChild>(out FlockChild child)) { // flockController.RecycleChild(child); } } IEnumerator FlyToHand(GameObject firefly, Transform target) { if (firefly == null) yield break; MonoBehaviour[] scripts = firefly.GetComponents<MonoBehaviour>(); foreach (var s in scripts) s.enabled = false; float duration = 0.3f; float t = 0f; Vector3 startPos = firefly.transform.position; while (t < duration) { if (firefly == null) yield break; t += Time.deltaTime; firefly.transform.position = Vector3.Lerp(startPos, target.position, t / duration); yield return null; } if (firefly != null) firefly.transform.position = target.position; } }