Make loop wait for action to complete - c#

Im working in Unity3d (this is more a C# question, so I doubt that is an issue). Im working on a movement system like you would find in Civilization. I have a loop setup so that you can move 2 squares per turn. This works fine. I click on a square 10 blocks away and it takes 5 turns to get there. Now im trying to make the pawn lerp between blocks. I have got lerp to work, problem is, it jumps from the current tile to the 1st tile, then transitions to the 2nd tile where its supposed to be. I used a coroutine to make this work instead of the update function (as the update would cause it to just lerp to the final destination instead of from current, to first, to second). So what im running into is the loop that goes through each move the pawn has, isnt waiting for the coroutine to complete before continuing its own loop. Here is the code
public void MoveNextTile()
{
float remainingMoves = moveSpeed;
while (remainingMoves > 0)
{
if (currentPath == null)
return;
//Get the cost from current tile to next tile
remainingMoves -= map.CostToEnterTile(currentPath[0].x, currentPath[0].y, currentPath[1].x, currentPath[1].y);
//Move us to the next tile in the sequence
toVector = map.TileCoordToWorldCoord(currentPath[1].x, currentPath[1].y);
Vector3 fromVec = transform.position;
StartCoroutine(MoveObject(fromVec, toVector, 1.0f));
//transform.position = map.TileCoordToWorldCoord(currentPath[1].x, currentPath[1].y);
//Remove the old current tile
this.tileX = currentPath[0].x;
this.tileY = currentPath[0].y;
currentPath.RemoveAt(0);
if (currentPath.Count == 1)
{
this.tileX = currentPath[0].x;
this.tileY = currentPath[0].y;
currentPath = null;
}
}
}
IEnumerator MoveObject(Vector3 source, Vector3 target, float overTime)
{
float startTime = Time.time;
while (Time.time < startTime + overTime)
{
transform.position = Vector3.Lerp(source, target, (Time.time - startTime) / overTime);
yield return null;
}
transform.position = target;
}
I know this is a noob question. I just never have needed to do this in C# before. Thank in advance for all the help

I suggest you research into how coroutines work.
Your coroutine doesn't execute fully and then return to complete the rest of your MoveNextTile function. It actually executes up until the first yield statement and then continues execution of MoveNextTile. Each subsequent frame will continue to run one 'step' of the coroutine until the next yield statement in an attempt to replicate asynchronous methods.
What you want to do is tell your program to explicitly wait for your coroutine to finish. To do so;
yield return StartCoroutine(MoveObject(fromVec, toVector, 1.0f));
Of course, you can only use this statement inside of an IEnumerator. So you would have to change your void MoveNextTile function to IEnumerator MoveNextTile. You end up with something as follows;
public IEnumerator MoveNextTile() {
// Ommited
while (remainingMoves > 0) {
//Ommited
yield return StartCoroutine(MoveObject(fromVec, toVector, 1.0f));
// Now MoveNextTile will continue after your MoveObject coroutine finishes.
}
}

Related

coroutine executes only once

I have a game where a car moves on a road, which has triggers along it. The purpose is to detect when the car enters those triggers and to do stuff depending on the trigger.
For the one I am having trouble, the camera is supposed to slowly move towards a second position which is behind and slightly above the car.
Here is what I tried:
private void OnTriggerEnter(Collider other)
{
if (other.attachedRigidbody.velocity.magnitude > 20.0f)
{
StartCoroutine(tst());
}
}
IEnumerator tst()
{
Camera cam = Camera.main;
Vector3 newPosition = cam.gameObject.transform.GetChild(0).position;
cam.transform.position = Vector3.MoveTowards(cam.transform.position, newPosition, camUnit);
yield return new WaitForSeconds(3);
}
The camUnit is equal 1f but the problem is that it doesn't move to the correct location, as in if I just assign it the new position the camera is in a different perspective than the code above and it is instant and not slow to move to the next position.
What am I doing wrong? Thank you in advance.
Directly after your yield you are exiting the Coroutine. With what you describe, you actually want this in some form of loop, and yield after doing what you need to do. Then, after the function resumes you decide whether you go again and yield again, or continue on towards the exit of the Coroutine.
bool isDone = false;
while(!isDone)
{
Camera cam = Camera.main;
Vector3 newPosition = cam.gameObject.transform.GetChild(0).position;
cam.transform.position = Vector3.MoveTowards(cam.transform.position, newPosition, camUnit);
yield return new WaitForSeconds(3);
isDone = EnsureCameraLocation(); // Do whatever check you need to do to figure out if the camera is where you want it yet
}
Note that the yield is inside the while statement which is what will make the Coroutine seamless.

Trouble understanding Delays and Coroutines

void start()
StartCoroutine(Text());
IEnumerator Text()
{
Debug.Log("Hello")
yield return new WaitForSeconds(3)
Debug.Log("ByeBye")
}
I understand the basic concept that this does but I don't get what anything means such as yield return new WaitforSeconds(3) and what StartCoroutine is and What an IEnumerator is.
Can anyone explain to me what they mean?
When you call a function, it runs to completion before returning. This effectively means that any action taking place in a function must happen within a single frame update; a function call can’t be used to contain a procedural animation or a sequence of events over time. As an example, consider the task of gradually reducing an object’s alpha (opacity) value until it becomes completely invisible.
void Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
}
}
As it stands, the Fade function will not have the effect you might expect. In order for the fading to be visible, the alpha must be reduced over a sequence of frames to show the intermediate values being rendered. However, the function will execute in its entirety within a single frame update. The intermediate values will never be seen and the object will disappear instantly.
It is possible to handle situations like this by adding code to the Update function that executes the fade on a frame-by-frame basis. However, it is often more convenient to use a coroutine for this kind of task.
A coroutine is like a function that has the ability to pause execution and return control to Unity but then to continue where it left off on the following frame. In C#, a coroutine is declared like this:
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return null;
}
}
It is essentially a function declared with a return type of IEnumerator and with the yield return statement included somewhere in the body. The yield return null line is the point at which execution will pause and be resumed the following frame. To set a coroutine running, you need to use the StartCoroutine function:
void Update()
{
if (Input.GetKeyDown("f"))
{
StartCoroutine("Fade");
}
}
You will notice that the loop counter in the Fade function maintains its correct value over the lifetime of the coroutine. In fact any variable or parameter will be correctly preserved between yields.
By default, a coroutine is resumed on the frame after it yields but it is also possible to introduce a time delay using WaitForSeconds:
IEnumerator Fade()
{
for (float ft = 1f; ft >= 0; ft -= 0.1f)
{
Color c = renderer.material.color;
c.a = ft;
renderer.material.color = c;
yield return new WaitForSeconds(.1f);
}
}
This can be used as a way to spread an effect over a period of time, but it is also a useful optimization. Many tasks in a game need to be carried out periodically and the most obvious way to do this is to include them in the Update function. However, this function will typically be called many times per second. When a task doesn’t need to be repeated quite so frequently, you can put it in a coroutine to get an update regularly but not every single frame. An example of this might be an alarm that warns the player if an enemy is nearby. The code might look something like this:
bool ProximityCheck()
{
for (int i = 0; i < enemies.Length; i++)
{
if (Vector3.Distance(transform.position, enemies[i].transform.position) < dangerDistance) {
return true;
}
}
return false;
}
If there are a lot of enemies then calling this function every frame might introduce a significant overhead. However, you could use a coroutine to call it every tenth of a second:
IEnumerator DoCheck()
{
for(;;)
{
ProximityCheck();
yield return new WaitForSeconds(.1f);
}
}
This would greatly reduce the number of checks carried out without any noticeable effect on gameplay.
Note: You can stop a Coroutine with StopCoroutine and StopAllCoroutines. A coroutines also stops when the GameObject it is attached to is disabled with SetActive(false). Calling Destroy(example) (where example is a MonoBehaviour instance) immediately triggers OnDisable and the coroutine is processed, effectively stopping it. Finally, OnDestroy is invoked at the end of the frame.
Coroutines are not stopped when disabling a MonoBehaviour by setting enabled to false on a MonoBehaviour instance.
Reference: https://docs.unity3d.com/Manual/Coroutines.html
Unity (ab)uses enumerators to build C# CoRoutines, because async / await didn't exist. When you write;
IEnumerator Text()
{
Debug.Log("Hello")
yield return new WaitForSeconds(3)
Debug.Log("ByeBye")
}
The compiler turns that into something like;
IEnumerator Text() => new StateMachine();
public class StateMachine : IEnumerable{
private int state = 0;
// plus any local variables moved to fields.
StateMachine(){}
public object Current { get; set; }
public bool MoveNext(){
switch(state){
case 0:
Debug.Log("Hello");
Current = new WaitForSeconds(3);
state = 1;
return true;
case 1:
Debug.Log("ByeBye");
return false;
}
}
}
Since the state of your function is now stored in fields on an object, your method can pause before it finishes. Unity will then look at the object you yield to decide when to call MoveNext().
Now that C# has async methods, which also cause your methods to be translated into state machines. It would be possible for a new version of unity to support them instead, like;
async Task Text()
{
Debug.Log("Hello")
await Something.WaitForSeconds(3)
Debug.Log("ByeBye")
}
But they would still have to support the old way of building CoRoutines.

Counting a full rotation with a circular drive

I've been raking my brain for the last day on how to calculate a full rotation with how to count a full rotation of an object along the X axis thats using the base circular drive from SteamVR.
I thought a simple 3d cube, with the mesh turned off in the the path of the rotation with collision code on it would be a barebone way of doing it, but it doesn't even seem to be registering the detection when the object hits the placed cubes, and i know its not because of me being stupid, as its recycled code from a working part of the project.
Below i have a small piece of code that basically detects when the object has reached the end of the rotation, and then increments the Count by one.
My main problem is that sometimes it manages to clock more than once, and if you can find the right spot, you can just keep it there and it'll keep on adding the count up by. Im wondering how i can stop it and increment only by one, until another full rotation has been made?
EDIT: to be more clear in case there is any confusion, Once the angle is clocked in between 359 and 360, i want it to increment once, whereas currently if you get the angle to sit anywhere in between 359-360 it will carry on adding one to the rotation count, despite no full rotation having been made, so im trying to figure out how to make my code only increment once, and once it does increment once it resets the position to zero, so therefore no more Increments can happen. It's a crank mechanism in VR, along the X axis.
Any help is appreciated, Thanks!
float Test;
float RotationCount = 0;
// Start is called before the first frame update
void Start()
{
// Test = transform.localRotation.eulerAngles.x;
}
// Update is called once per frame
void Update()
{
if (Test > 359 && Test < 360)
{
Debug.Log("Clocked");
count();
}
else
{
// Debug.Log("Nope");
}
if (Test == 0)
{
Debug.Log("Yes");
}
Test = transform.localRotation.eulerAngles.x;
}
void count()
{
RotationCount++;
}
sometimes it manages to clock more than once, and if you can find the right spot, you can just keep it there and it'll keep on adding the count up by.
well in
if (Test > 359 && Test < 360)
{
Debug.Log("Clocked");
count();
}
what happens if your angle is e.g. 359.5?
It is very difficult to just take a current rottaion and know whether it was turned more or less than a certain angle.
I'ld rather store the last rotation, compare it to the current one and add the difference to a variable. Than if the variable exceeds 360 a full rotation was done. Since I also don't like to calculate anything with Quaternion and eulerAngles, way simplier is to use the methods provided by Vector3.
For the local rotation around X (== transform.right) I would use the angle between the current and the last transfrm.up vector.
Something like
public class RotationCheck : MonoBehaviour
{
public int RotationCount;
public float rotatedAroundX;
public Vector3 lastUp;
public UnityEvent On3TimesRotated;
private void Awake()
{
rotatedAroundX = 0;
// initialize
lastUp = transform.up;
}
private void Update()
{
var rotationDifference = Vector3.SignedAngle(transform.up, lastUp, transform.right);
rotatedAroundX += rotationDifference;
if (rotatedAroundX >= 360.0f)
{
Debug.Log("One positive rotation done", this);
RotationCount++;
rotatedAroundX -= 360.0f;
}
else if (rotatedAroundX <= -360.0f)
{
Debug.Log("One negative rotation done", this);
RotationCount--;
rotatedAroundX += 360.0f;
}
// update last rotation
lastUp = transform.up;
// check for fire the event
if (RotationCount >= 3)
{
On3TimesRotated?.Invoke();
RotationCount = 0;
}
}
}
You can use a UnityEvent to get the same thing the Button uses for onClick so you can reference callbacks there via the inspector.
BUT if you don't care about the single rotations but actually only wnat the final RotationCount >= 3 I would actually use
private void Update()
{
var rotationDifference = Vector3.SignedAngle(transform.up, lastUp, transform.right);
rotatedAroundX += rotationDifference;
RotationCount = Mathf.RoundToInt(rotatedAroundX / 360.0f);
// update last rotation
lastUp = transform.up;
// check for fire the event
if (RotationCount >= 3)
{
On3TimesRotated?.Invoke();
RotationCount = 0;
rotatedAroundX = 0;
}
}
which directly reduces the value by one if rotated under the 360 mark instead of waiting for a full negative rotation
*as you can see instead of reaching 3 the RotationCount is reset to 0. This is where the On3TimesRotated event is/would get fired.

How can I make GameObjects translate across the screen smoothly in Unity?

Basically, I have a game where tickets appear at the top of the screen. When a ticket is completed, it gets destroyed, and if there's a gap between the remaining tickets, the tickets slide down to fill in the gap. I actually have it working just fine, but they kind of just jump to the positions and it doesn't look very pretty.
How can I move the objects to their target position smoothly? I've tried messing with Lerp and Smoothdamp, but both of those seem to be producing strange results (tickets go all over the place. Here's the relevant code I'm working with:
public void SlideRail()
{
ticketList.RemoveAll(x => x == null);
int count = ticketList.Count;
for (int i = destroyedIndex; i < ticketList.Count; i++)
{
if (ticketList[i] != null)
{
ticketList[i].transform.Translate(Vector3.left * 35);
ticketList[i].GetComponent<Ticket>().index--;
}
}
indexer = railPosition.IndexOf(ticketList.Last().GetComponent<Ticket>().item_pos);
up = false;
}
DestroyedIndex is the index of the object that was destroyed (so that we only move the objects after it in the list)
Up is a flag that activates SlideRail(), when its set to false the method ends and the next method continues.
SlideRail is then called in the Update() method:
void Update()
{
if (up && ticketList.Count > 2)
SlideRail();
if (Time.time > nextTicket)
{
nextTicket = Time.time + timeBetweenTickets;
StartCoroutine(WaitToPrint());
CreateTicket();
UpdatePosition();
}
}
I feel like I should be able to do this using Translate, but I must be missing something.
With movement like this you should usually include Time.deltaTime (as a factor in translate) to bind the movement to the frame rate. [Edit: Translate this sentence to framerate-independant, that's what I meant.]
Now, to the actual point, it looks like you do the movement in just one frame and thus you get this jump (you always set up to false at the end of SlideRail).
A possible way of doing this is to specify a target position upon trigger (task complete, need to push over) and than keep calling ticketList[i].transform.position = Vector3.MoveTowards(ticketList[i].transform.position, targetPosition, speed * Time.deltaTime); for each object (framed by a "reached target position" check, e.g. using if(Vector3.Distance(ticketList[i].transform.position, targetPosition) > someThreshold). You can add an else to this to directly set the position when in threshold range (make the threshold small so it's not visible, e.g. 0.1f), though you usually only need this for non-linear functions like lerp which slow down at the end and can stall. [Edit: Because of floating point imprecision it's actually mostly always a good idea to use a threshold.]
Using iTween, it can be simplified into a one line call:
iTween.MoveTo(someGameObject, iTween.Hash("y", targetY,
"islocal", true,"easetype", iTween.EaseType.spring,
"time", 0.2f));
I've wrote this piece of code for myself before, maybe it helps you.
using System;
using System.Collections;
using UnityEngine;
public class MoveController : MonoBehaviour
{
public float moveTime;
public AnimationCurve moveSpeedCurve;
public AnimationCurve movePositionCurve;
public void StartMoving(Vector2 destination, Action action = null)
{
StartCoroutine(Move(destination, action));
}
IEnumerator Move(Vector3 destination, Action action = null)
{
float currentTime = 0;
float perc = 0;
Vector3 currentPos = transform.localPosition;
while (perc != 1)
{
currentTime += Time.deltaTime;
if (currentTime > moveTime)
{
currentTime = moveTime;
}
perc = moveSpeedCurve.Evaluate(currentTime / moveTime);
transform.localPosition = Vector2.LerpUnclamped(currentPos, destination, movePositionCurve.Evaluate(perc));
yield return null;
}
if (action != null)
action();
}
}

How can I get the camera to pause on 3 objects on the x axis?

I am a new student working on a class project. I have 1 script attached to the camera in my only scene. I want the camera to pause over the 1st object, scroll to the 2nd object and pause then scroll to the 3rd object and pause then end. Putting this code in the UPDATE, the camera never stops. Here in the START, it hesitates around 15 sec and then it goes right to the last object, then the function stops. Note the delay set for 10 seconds. I tried putting the code in a function and calling the function from START… but no good. What am I doing wrong? HELP ME OB1....
One more thing... Is START the best place to play sound?
using UnityEngine;
using System.Collections;
// I want the camera to pause over the 1st object, scroll to the 2nd object and pause
// then scroll to the 3rd object and pause then end. Putting this code in the UPDATE
// the camera never stops. Here in the START, it hesitates around 15 sec and then it
// goes right to the last object, then the function stops. Note the delay set for 10
// seconds.
public class CameraControl : MonoBehaviour
{
public float speed; // How fast to move the camera
public int moves; // How many moves to make
public float MyWait; // How long to pause over object
// Use this for initialization
void Start()
{
StartCoroutine(MyDelay());
for (int y = 1; y <= 2; y++) // go to the next two objects
{
for (int i = 1; i <= moves; i++) // Move the camera to the next position
{
Camera.main.transform.Translate(new Vector3(1.0f, 0.0f, 0.0f) * speed * Time.deltaTime);
Debug.LogFormat("moves = {0} ", i);
}
StartCoroutine(MyDelay());
}
}
IEnumerator MyDelay()
{
yield return new WaitForSeconds(10.0f);
}
}
Try placing this code on your camera and place all the game objects you'd like the camera to move to in the Objects list. If you'd like the camera to be a little further back so it can see the object, create a new Vector3 instead of simply giving the exact position and then give that new Vector3 the x, y and z of the iterating object and then add distance to which ever axis you'd like for the camera to be distanced from the object.
public float MyWait = 5; // How long to pause over object
public float speed = 5f; // How fast to move the camera
public List<GameObject> Objects; //List of each object for the camera to go to
void Start()
{
StartCoroutine(MoveToObject(0));
}
IEnumerator MoveToObject(int iteratingObject)
{
//Wait for however many seconds
yield return new WaitForSeconds(MyWait);
bool atDestination = false;
//Move the camera until at destination
while (!atDestination)
{
yield return new WaitForFixedUpdate();
transform.position = Vector3.MoveTowards(transform.position, Objects[iteratingObject].transform.position, Time.deltaTime * speed);
if (transform.position == Objects[iteratingObject].transform.position)
atDestination = true;
}
//Continue iterating until moved over all objects in list
if(iteratingObject != Objects.Count - 1)
StartCoroutine(MoveToObject(iteratingObject + 1));
}
I think you're going to need to put some code in the Update function for this to work smoothly.
Time.deltaTime will only really make sense in an Update function, using it here and trying to do everything in the Start function won't work. Also setting the Translate transform will instantly set the position to the given value. Look up linear interpolation (lerp).
I would suggest you have a member that you use to track the current state, i.e. which object you're looking at, but an Enum of states might be easier to read.
Then you can keep a member for how long you've been in that state, which you can increase in the Update.
Then within the Update you can check whether it is time to change state or update your moving camera.
Good luck!

Categories

Resources