1
\$\begingroup\$

I am working on an interactive game to aid with anxiety management.

In one of the gameplay options, the player has to press buttons for a certain amount of time. Only after the required time has elapsed (with the player holding the buttons down), will the avatar move. So for example, the instruction will be "Press the buttons for 20 seconds to move the player". This part is working.

As this is an anxiety game, however, I want to incorporate some breathing activity/instruction into the gameplay. I have chosen to use the 4-7-8 second rule, whereby the player breathes in for 4 seconds, holds their breath for 7 seconds and exhales for 8 seconds. I have no idea how to incorporate something like this into my existing game. I understand that a second timer would be needed, but how would that be able to discern between the 4 seconds, 7 seconds and 8 seconds?

Attached is my timer script:

using UnityEngine;
using System.Collections;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class Timer : MonoBehaviour
{
    public int timeLeft;
    public Text countdownText;
    private bool timerStarted;

    int start;

    public void BeginTimer()
    {
        StartCoroutine(LoseTime());
    }

    void Awake()
    {
        start = timeLeft;
    }

    public void StopTimer()
    {
        StopAllCoroutines();
        Respawn.Instance.Player2.transform.position = Respawn.Instance.RespawnPoint2.transform.position;
        PlayerController.Instance.timerStarted = false;
        timeLeft = start;
        countdownText.text = "Time Left= " + start;
    }

    private IEnumerator LoseTime()
    {
        while (timeLeft > 0)
        {
            yield return new WaitForSeconds(1);
            timeLeft--;
            countdownText.text = "Time Left= " + timeLeft;
        }
       
        PlayerController.Instance.Character.GetComponent<Rigidbody2D>().AddForce(PlayerController.Instance.height, ForceMode2D.Impulse);
         //SphereController.Instance.Player.GetComponent<Renderer>().material;
        print("hi");
    }

    public void ChangeLevel()
    {
        SceneManager.LoadScene(SceneManager.GetActiveScene().buildIndex + 1);
    }
}

And my playercontroller script:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO.Ports;
using UnityEngine.UI;

public class PlayerController : MonoBehaviour
{
    public static PlayerController Instance { set; get; }
    public float Speed;
    public Vector2 height;
    public float xMax, xMin, yMin, yMax;
    public bool buttonPressed = false;

    public Rigidbody2D Character;
    public Timer timer;
    public Rigidbody2D _rigidbody;

    public bool timerStarted = false;

    private SerialPort sp = new SerialPort("\\\\.\\COM4", 9600);

    Animator anim;
    string value;
    int button;

    private void Awake()
    {
        FetchReferences();
        Instance = this;

        //print("hi");

        if (AvatarChooser.ava == 1)
        {
            anim = GameObject.FindWithTag("Player").GetComponent<Animator>();
            anim.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>("3");
            //print("hi");
        }

        if (AvatarChooser.ava == 2)
        {
            anim = GameObject.FindWithTag("Player").GetComponent<Animator>();
            anim.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>("1");
        }

        if (AvatarChooser.ava == 3)
        {
            anim = GameObject.FindWithTag("Player").GetComponent<Animator>();
            anim.runtimeAnimatorController = Resources.Load<RuntimeAnimatorController>("2");
        }
    }

    [ContextMenu("FetchReferences")]
    private void FetchReferences()
    {
        if (!Character) Character = GameObject.FindWithTag("Player").GetComponent<Rigidbody2D>();
        if (!timer) timer = GameObject.Find("Timer").GetComponent<Timer>();
    }

    private void Start()
    {
        if (!sp.IsOpen)
        {
            sp.Open(); // Open 
        }
        sp.ReadTimeout = 1;

        //print(AvatarChooser.ava);
    }

    private void Update()
    {
        if (sp.IsOpen)
        {
            try
            {

                value = sp.ReadLine(); //Read the information
                button = int.Parse(value);

                if (button == 9)/*(Input.GetKeyDown(KeyCode.Space))*/  //jump
                {
                    buttonPressed = true;

                   if (!timerStarted)
                    {

                        timerStarted = true;
                        timer.BeginTimer();
                    }
                }
                else
                {

                    timer.StopTimer();
                }
            }
            catch (System.Exception)
            {

            }
        }
    }


    private void FixedUpdate()
    {
        Character.position = new Vector3
                    (
                        Mathf.Clamp(GetComponent<Rigidbody2D>().position.x, xMin, xMax),
                        Mathf.Clamp(GetComponent<Rigidbody2D>().position.y, yMin, yMax)
                    );
    }
   
    void OnDisable()
    {
        sp.Close();
    }

}
\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

It looks like you're also reading your input from a specialized device of some kind. I'll show a conventional version with Unity's built-in input first, for simplicity, then mention how we can incorporate your custom input.

First we can set up a data structure you can use to associate a particular hold duration with a specific action:

// If you're using Unity 2020 you can skip this and use UnityEvent<float> directly.
[System.Serializable]
public class HoldEvent : UnityEvent<float> {}

[System.Serializable]
public struct HoldAction {
    public float targetDuration;
    public HoldEvent onHoldComplete;
}

This gives you a nice little inspector where you can wire up a function to call when the player releases the button after this hold.

Screenshot of hold action inspector

I gave it a float argument so you can pass the signed error value: if the player released half a second too early, we still call the function, but with -0.5f as the argument. 0.0f means they hit the target exactly, and greater than zero means they released late.

You can use this to give different effects and feedback for early/late releases - just be sure to give some tolerance for the limited update rate. eg. if (Mathf.Abs(errorSeconds) <= 2.0f/30.0f) will check if they were within about 2 frames of the target time.

Then we can make a script that takes an array of these hold actions, and steps through them in a sequence, 4-7-8-4-7-8... each time the player completes one hold, it advances to the next target duration automatically.

// Name of the button to read.
public string inputName;

// Ordered collection of holds you can set up in the inspector.
public HoldAction[] holdSequence;

// Which step of the 4-7-8 we're on now.
int _sequenceIndex;

// How many seconds we've been holding for.
float _holdDuration;


// This will return the current progress as a ratio versus the current target.
// 0 = not started, 1.0f = 100% complete, 1.1f = still holding 10% too long.
// You could use this to animate a progress bar or other feedback.
public float GetHoldProgress() {
    return (_holdDuration) /  (holdSequence[_sequenceIndex].targetDuration);
}

// If something breaks the player's rhythm, you can call this to start them back
// at the first hold in the sequence again.
public void ResetToStartOfSequence() {
    _sequenceIndex = 0;
}

void Update() {    
    // You can release and re-press the input on the same frame if you're super fast
    // or the framerate is sluggish, so we check for a release first:
    if(Input.GetButtonUp(inputName)) {

        // We call the appropriate method for this step in the sequence,
        // passing the error as our argument.
        var reaction = holdSequence[_sequenceIndex].onHoldComplete;
        if (reaction != null)
            reaction.Invoke(_holdDuration - holdSequence[_sequenceIndex].targetDuration);

        // Then advance to the next hold in sequence (wrapping around),
        // and zero our held duration to be ready to start the next hold.
        _sequenceIndex = (_sequenceIndex + 1) % holdSequence.Length;
        _holdDuration = 0;
    }

    // If the player is holding the button, we add time to our accumulator.
    if(Input.GetButton(inputName)) {
        _holdDuration += Time.unscaledDeltaTime;
    }
}

Here's what this can look like when all set up. I made a little Breathe script with a bunch of methods like public void Inhale(float error) { } to demonstrate how the events can be wired up.

Screenshot of script

To make this work with your custom input device, all you need to do is make a component that has a similar interface as the built-in Input system. Specifically, for this script we need to be able to read two different booleans: one that's true on the frame the button has been released, and one that's true as long as the button is pressed/down/held.

With that in place, you can add a reference to your custom input monitoring script, like

public SerialInputReader serialInput;

Then you can replace Input.GetButtonUp() with serialInput.ReleasedThisFrame() and Input.GetButton() with serialInput.IsPressed()

\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much. I will look into this solution; much appreciated @DMGregory \$\endgroup\$
    – user19964
    Commented Sep 9, 2020 at 19:55

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .