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;
    }   
}