Search Unity

Activate objects/code by holding key for n seconds

Discussion in 'Scripting' started by Sendatsu_Yoshimitsu, Sep 17, 2014.

  1. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    There are a lot of ways to do this badly, but I'm trying to work out an efficient way for any object marked as interactable to require the player to approach it and hold the Use key for n seconds. Releasing the key before the countdown expires does nothing, and hitting the key again starts the countdown from the beginning. Right now, the best pseudocoded logic I have goes as follows:

    Code (csharp):
    1.  
    2. void ActivateMe(){
    3. //Whatever happens when the object is activated goes here
    4. }
    5.  
    6. float timeToUse= 2;
    7. if (input.GetButtonDown("Use"){
    8. StartCoroutine("CountDown");
    9. }
    10.  
    11. if (input.GetButtonUp("Use"){
    12. //Terminate the CountDown coroutine somehow
    13. }
    14.  
    15. IEnumerator CountDown(gameObject activeObject){
    16. float countdown = timeToUse;
    17. bool countdownRunning = true;
    18. while (countdownRunning){
    19. countdown -= time.DeltaTime;
    20. if (countdown <= 0){
    21.      ActivateMe();
    22.     countdownRunning = false;
    23. }
    24. yield return null;
    25. }
    26. }
    27.  
    However, there are two obvious problems I'm stuck on:

    1) what is the "cleanest" way to terminate my coroutine early if the player releases the key before the countdown runs out? I could add to the conditional loop in my CountDown and make it "If StopEarly = true break, else //do coroutine stuff", but that seems brute force-y and I don't like making two checks every frame.

    2) How do I efficiently keep every single object with this script attached to it from triggering simultaneously every time Use is pressed? I have fairly large levels and could potentially have dozens or hundreds of interactable objects, so I don't want to manually test for distance every time, or do something silly like put a trigger collider around the player and use ontriggerenter to activate a CanUse boolean or something equally awkward.
     
    Last edited: Sep 17, 2014
  2. Glockenbeat

    Glockenbeat

    Joined:
    Apr 24, 2012
    Posts:
    669
    I guess you are making your own life harder as it needs to be here. As you said there are ugly ways and there are nice ways, but in between there is a huge gray zone. Why not work with a bool flag? On the other hand there is StopCoroutine() which you can use here since you are starting the coroutines with string names anyways (haven't used that before though).
    http://docs.unity3d.com/ScriptReference/MonoBehaviour.StopCoroutine.html

    As for your second problem you will of course need to determine which exact object was clicked, otherwise all of them will fire simultaneously.
    I usually attach a collider to those objects as well (which can also be automated via RequireComponent attribute on the class) and then do a raycast at the mouse position via ScreenPointToRay against a specific layer - the object of course needs to reside in that layer. That way you can check if that specific object was clicked.

    However if you implement that directly in that class you may of course end up with dozens and hundreds of interactable objects doing raycasts, so it's preferred to have a manager class for that stuff which detects the object which was actually clicked and then calls a public method on those.
     
  3. Krysalgir

    Krysalgir

    Joined:
    Aug 30, 2010
    Posts:
    95
    You could use a simple WaitForSeconds in your coroutine instead of a while().

    Code (csharp):
    1.  
    2. void ActivateMe(){
    3. //Whatever happens when the object is activated goes here
    4. }
    5.  
    6. float timeToUse= 2;
    7. bool countdownRunning= false;
    8.  
    9. void Update() {
    10.     if (input.GetButtonDown("Use") && !countdownRunning) {
    11.         StartCoroutine("CountDown", timetoUse);
    12.     }
    13.  
    14.     if (!input.GetButtonDown("Use") && countdownRunning) {
    15.         Stopcoroutine("CountDown");
    16.         countdownRunning = false;
    17.     }
    18. }
    19.  
    20. IEnumerator CountDown(float time){
    21.     countdownRunning = true;
    22.     yield return new WaitforSeconds(time);
    23.     if(countdownRunning)
    24.         ActivateMe();
    25.  
    26.     countdownRunning = false;
    27. }
    28.  
    29.  
    If you feel rather confident in your coding skills, you could also create a Timer class (and some timer manager) that could be completely separated from your specific script, and be used basically anywhere in your project :)
     
    Last edited: Sep 17, 2014
  4. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    Hmm alright, I wasn't aware that StopCoroutine() worked when you use string names to start them, thank you!

    And Krysalgir, that looks awesome and way cleaner than the way I was doing it, but one reason I was using the while() loop is because I have a visual indicator of progress, a sprite that gradually does a radial fill as time ticks down to indicate how close you are. If I'm executing it with while(bool), I can just add a number to my fill every frame, but is there an obvious way I'm missing to do the same thing if I'm doing it with a single WaitforSeconds?

    Finally, I'm assuming there's no way around this, but the way I implemented object use this morning was to cast a ray of a length corresponding to your interaction range toward the aiming reticule when the player hits Use, if it returns a hit I run a conditional to check if it's useable, and if it is, I sendmessage the object's ActivateMe() function. Ideally I'd like to give the player some form of visual feedback to indicate that what they're looking at is useable, I already have an icon and glow that I can toggle, but right now they're incredibly inefficient- the best way I could think of was to use a raycast to message whichever object I'm looking at, and I'm really uncomfortable using a ray every single frame, is this another case of overcomplicating a potentially simple process?
     
    Last edited: Sep 17, 2014
  5. JoeStrout

    JoeStrout

    Joined:
    Jan 14, 2011
    Posts:
    9,859
    A single ray per frame is unlikely to be a problem, especially if you limit the length of the ray (i.e. use a line cast rather than a ray cast). I'd say go for it, and if you find out through profiling later that this is a bottleneck, then refactor it then.

    Best,
    - Joe
     
  6. Krysalgir

    Krysalgir

    Joined:
    Aug 30, 2010
    Posts:
    95
    If you want some counter, you can add a float, for something like

    Code (csharp):
    1.  
    2. void ActivateMe(){
    3. //Whatever happens when the object is activated goes here
    4. }
    5.  
    6. float timeToUse= 2;
    7. public float currentCDTimer = 0.0f;
    8. bool countdownRunning= false;
    9.  
    10. void Update() {
    11.     if (input.GetButtonDown("Use")) && !countdownRunning) {
    12.         if(!countdownRunning)
    13.             StartCoroutine("CountDown", timetoUse);
    14.         else {
    15.             currentCDTimer += Time.deltaTime;
    16.             if(currentCDTimer > timeToUSe)
    17.                 currentCDTimer = timeToUse;
    18.         }
    19.     }
    20.  
    21.     if (!input.GetButtonDown("Use") && countdownRunning) {
    22.         Stopcoroutine("CountDown");
    23.         countdownRunning = false;
    24.         currentCDTimer = 0.0f;
    25.     }
    26. }
    27.  
    28. IEnumerator CountDown(float time){
    29.     countdownRunning = true;
    30.     currentCDTimer = 0.0f;
    31.     yield return new WaitforSeconds(time);
    32.     if(countdownRunning)
    33.         ActivateMe();
    34.  
    35.     countdownRunning = false;
    36. }
    37.  
    38.  
     
  7. Sendatsu_Yoshimitsu

    Sendatsu_Yoshimitsu

    Joined:
    May 19, 2014
    Posts:
    691
    That is enormously helpful, thank you so much for your patient help! :)
     
  8. Krysalgir

    Krysalgir

    Joined:
    Aug 30, 2010
    Posts:
    95
    You're welcome ;)