- Home /
Smooth open/close door script coroutine issue
I've just watched this video and grabbed the script from it. Here's an enhanced C# version of it. It works fine.
However, I decided that I want to make a Coroutine to open/close the door:
void Update()
{
if (Input.GetKeyDown(KeyCode.E) && inSight) {
open = !open;
StartCoroutine(DoorCoroutine(open));
}
}
IEnumerator DoorCoroutine(bool open)
{
Vector3 target = open ? openRot : defaultRot;
float t = 0;
while (t < 1) {
t += Time.deltaTime / duration;
cachedTransform.eulerAngles = Vector3.Slerp(cachedTransform.eulerAngles,
target,
t);
yield return null;
}
cachedTransform.eulerAngles = target;
}
It's doing the job well if I wait for it to finish, but the problem is, if I try to close/open the door while it's opening/closing, it doesn't act right. I understand that the problem is related to the fact, that when I call the closing coroutine while the opening one hasn't finished yet, they will conflict with each other and give wrong results.
I tried to stop the coroutine before calling it, didn't work. I also tried making the 't' variable a member field, not local, so that if the door hasn't finished rotating yet, and I tried to open/close it, 't' would resume and continue from where it left of. But it didn't quite work...
So, how can I write that coroutine properly? such that, if I open/close the door while it's closing/opening, it would react properly.
Thanks for any help.
EDIT:
Here's a video showing the door opening with and without the coroutine.
Answer by Statement · Dec 25, 2013 at 01:28 PM
Here's one way, stopping the Coroutine and restarting it:
void Update()
{
if (Input.GetKeyDown(KeyCode.E) && inSight)
ToggleDoor();
}
void ToggleDoor()
{
open = !open;
StopCoroutine("DoorCoroutine");
StartCoroutine("DoorCoroutine", open);
}
Here is one without Coroutines, using Animation instead:
void Update()
{
if (Input.GetKeyDown(KeyCode.E) && inSight)
ToggleDoor();
}
void ToggleDoor()
{
open = !open;
if (open)
animation.Play("Open");
else
animation.Play("Close");
}
Here's the really long way of achieving the same thing without having to use strings. I haven't tested it, but the code is so small so I'll take my chances of public embarrasment if it turns out to have bugs. :) If anything is wrong, I would suspect you may have to change the Current value to return null when you try to access it if it has been aborted, but I doubt it.
AbortableEnumerator doorCoroutine;
void ToggleDoor()
{
open = !open;
StopDoorCoroutine ();
StartDoorCoroutine ();
}
void StopDoorCoroutine ()
{
if (doorCoroutine != null)
doorCoroutine.Abort ();
}
void StartDoorCoroutine ()
{
doorCoroutine = new AbortableEnumerator (DoorCoroutine (open));
StartCoroutine (doorCoroutine);
}
And the code that enables this new behaviour:
public class AbortableEnumerator : IEnumerator
{
IEnumerator enumerator;
bool isAborted;
public AbortableEnumerator(IEnumerator enumerator)
{
this.enumerator = enumerator;
}
public void Abort()
{
isAborted = true;
}
bool IEnumerator.MoveNext ()
{
if (isAborted)
return false;
else
return enumerator.MoveNext ();
}
void IEnumerator.Reset ()
{
isAborted = false;
enumerator.Reset ();
}
object IEnumerator.Current
{
get { return enumerator.Current; }
}
}
What I really think your approach should have been if you were to do it purely with code, but this is my preference and not a rule. Take it for a spin. I believe it behaves pretty nicely, and you can adjust the rotational angles and how fast it should smooth/move. If your world expects that the original rotation should be taken into account, you'd have to fix that.
Create a blank GO, attach this script to it.
Add a cube child to it, offset: 0.5, 0.5, 0.0, rotation: 0, 0, 0, scale: 1, 2, 0.1.
Press "E"
Tweak values in inspector to your hearts content
And the code:
using UnityEngine;
using System.Collections;
public class SmallDoorExample : MonoBehaviour {
public float closedAngle = 0;
public float openedAngle = 90;
public float doorSwingSmoothingTime = 0.5f;
public float doorSwingMaxSpeed = 90;
private float targetAngle;
private float currentAngle;
private float currentAngularVelocity;
void Update ()
{
if (DoorWasInteractedWith ())
ToggleAngle ();
UpdateAngle ();
UpdateRotation ();
}
static bool DoorWasInteractedWith ()
{
return Input.GetKeyDown (KeyCode.E);
}
void ToggleAngle ()
{
if (targetAngle == openedAngle)
targetAngle = closedAngle;
else
targetAngle = openedAngle;
}
void UpdateAngle ()
{
currentAngle = Mathf.SmoothDamp (currentAngle,
targetAngle,
ref currentAngularVelocity,
doorSwingSmoothingTime,
doorSwingMaxSpeed);
}
void UpdateRotation ()
{
transform.localRotation = Quaternion.AngleAxis (currentAngle, Vector3.up);
}
}
If you want to abort a coroutine, you need to either call StopAllCoroutines or StopCoroutine. You can also sprinkle a lot of if (abort)
all over the code where the coroutine may resume but I don't think that is very neat at all.
I think you can check if a coroutine is running with IsInvoking("DoorCoroutine");
but make a test ins$$anonymous$$d of accepting this as I am both unsure and too lazy to test for you.
Alternatively, design your code differently. I am not sure if a coroutine would be the best solution to your problem. It is a solution, and it will work, but perhaps there are cleaner ways to do it. I tend to prefer coroutines when I want to chain together different kinds of timed actions. In this case it feels like you could have bool for animating the door, or even an actual animation. With an animation you get more control over the look of the animation, produces less code and (if valuable to you) means less chance for code bugs/less frequent code changes.
I tried to stop the coroutine before calling it, didn't work.
How were you trying to stop it?
you could actually do StartCoroutine(method()); avoiding string literals.
But then you can't stop it. You need to start it by a string so it has a name to remember it by when you want to stop it.
I didn't test your entire code but I did a simple debug log to ensure that the code would stop the coroutine mid-way.
But, this way I can't close/open the door while it's being opened/closed.
Yes you can, if you let the script run on update and move toward either the open rotation or closed rotation, what you can do is just simply change a member variable which is the target rotation.
// make targetRotation a member variable
if (open)
targetRotation = openRot;
else
targetRotation = defaultRot;
The trick is, the door should rotate until it has reached the targetRotation. You are free to change the targetRotation mid-way a rotation (it will instantly start -or continue, rather- rotating the other direction).
Edit: Ok, now I see what you meant by your video. Let me take some time looking through it.
Ok, it's clear from your video you are starting the coroutine wrong. You must use the string variant exactly like I shown you if you want to use StopCoroutine. If you don't want to do it this way, you need to make a decorator that is abortable which would involve more work, but I'll update my answer with a solution so you get a taste of what I mean.
Also a wiki page where you can improve on the code:
It's up to you. Perhaps you should ask a new question that focuses only on that once you got your code in place. You could for example accelerate the lerp factor over time to make it appear smooth, or maybe you want to use $$anonymous$$athf.SmoothDamp - I can't tell what effect you want but SmoothDamp doesn't sound like a bad idea.
Glad it did. I also felt like making yet another example, that does it without coroutines, and with SmoothDamp.
Your answer
Follow this Question
Related Questions
Rotation - Simple Question 0 Answers
beguin (but not wait for) the smoothing of a float in coroutine 1 Answer
Rotate smoothly an object when key is up 0 Answers
Smooth reset rotation problem 1 Answer