I'd like to move up and down a certain part of a 2D-enemy-spaceship via DoTween's DOMoveY-shortcut. So far so good - it almost works... but when I try to change the value of the transitionTarget during gameplay the animation doesn't change accordingly.
So the problem seems to be that the code doesn't update the Tweener. Now I'd like to know what do I have to change of my code so that the Tweener gets updated when I change the value (via inspector) of the transitionTarget during gameplay?
This is the code:
public float transitionTarget = 0f;
private Tweener transitionTweener;
private bool toggleTransition = true;
void Start()
{
TransitionTween(transitionTarget);
transitionTweener.OnRewind(() => {
Debug.Log("<<- Transition played backward and completed!");
toggleTransition = true;
});
transitionTweener.OnComplete(() => {
Debug.Log("->> Transition played and completed!");
if (toggleTransition) toggleTransition = false;
else toggleTransition = true;
});
}
void TransitionTween(float targetY) {
transitionTweener = this.transform.DOMoveY(targetY, 3f, false)
.SetEase(Ease.Linear)
.SetAutoKill(false)
.SetId("tran")
.Pause();
}
void Update() {
if (toggleTransition) {
transitionTweener.PlayForward();
}
else {
transitionTweener.PlayBackwards();
}
}
Solution 1:
Assuming that it suffices for you to have the updated transitionTarget take effect at the moment when the animation-cycle starts, there is a simple solution to your problem
utilizing the ChangeEndValue method.
Moreover I suggest a simplification:
Instead of rewinding the tween manually, I suggest that you use
SetLoops(-1, LoopType.Yoyo)
The same amount of control can be achieved by utilizing the OnStepCompleted callback.
public class EnemySpaceship : MonoBehaviour {
private Tweener transitionTweener;
public float transitionTarget = 0f;
private Vector3 Endvalue => new Vector3(0.0f, transitionTarget, 0.0f);
void Start()
{
transitionTweener = transform.DOMove(Endvalue, 3f, false)
.SetOptions(AxisConstraint.Y)
.SetEase(Ease.Linear)
.SetLoops(-1, LoopType.Yoyo)
.SetAutoKill(false);
transitionTweener.OnStepComplete(OnStepCompleted);
}
private void OnStepCompleted() {
if (transitionTweener.CompletedLoops() % 2 == 0) {
Debug.Log("->> Transition played and completed!");
transitionTweener.ChangeEndValue(Endvalue);
} else {
Debug.Log("<<- Transition played backward and completed!");
}
}
}
Solution 2:
If you do need the animation to use the updated target value instantly while the animation is still playing, then we need more clarifications.
Consider the following cases for the time of the change of the transitionTarget value:
during transition from the start location to the transitionTarget.
the new transitionTarget value is yet to be reached
the new transitionTarget value has already been surpassed
during transition back from the transitionTarget to the start location.
I will ignore case 2. entirely, because the spaceship will have to return to the start location anyways, and that location which never changes - so in this solution like in the first, the change will only take effect, as soon as the loop is complete when the start location has been reached.
regarding 1., I suppose we can generally assume, that the movement of the spaceship should be continuous, so we will avoid any sort of "teleportation".
Unfortunately the ChangeEndValue method rewinds the tween.
So if we want to use this method† for case 1.1, we will have to manually "scrub" the tween to the correct position, which can be achieved with the Goto method.
However, it appears that the Goto method does not work properly with infinite Loops (which should be reported as issue). The solution will just execute a single loop, still using LoopType.Yoyo (the number of loops has to be 2 to go forth and back once), and restart the tween afterwards (which is a bit closer to the sample code from the question).
In case of 1.2 we just scrub forward to the position in time, where we move back to the start-position, while we are at the same position, which is 2 * duration - transitionTweener.position
so without further ado, the solution:
public class EnemySpaceship : MonoBehaviour {
private Tweener transitionTweener;
public float transitionTarget = 0f;
private float lastValue;
private Vector3 startValue;
private Vector3 Endvalue => new Vector3(0.0f, transitionTarget, 0.0f);
private const float duration = 3f;
void Start() {
lastValue = transitionTarget;
startValue = transform.position;
transitionTweener = transform.DOMove(Endvalue, duration)
.SetOptions(AxisConstraint.Y)
.SetEase(Ease.Linear)
.SetLoops(2, LoopType.Yoyo)
.SetAutoKill(false);
transitionTweener.OnComplete(RestartTween);
}
private void RestartTween() {
transitionTweener.ChangeEndValue(Endvalue);
transitionTweener.Restart();
}
private void Update() {
if (lastValue != transitionTarget && transitionTweener.CompletedLoops() == 0) {
var t = duration * (transform.position.y - startValue.y) / (transitionTarget - startValue.y);
if (t > duration) {
transitionTweener.Goto(2 * duration - transitionTweener.position, true);
} else {
transitionTweener.ChangeEndValue(Endvalue);
transitionTweener.Goto(t, true);
}
lastValue = transitionTarget;
}
}
}
Note:
To comply with your spec, that the tween should update when the transitionTarget is changed from within the inspector, the solution compares lastValue with transitionTarget every frame in the update method.
To avoid this in the real application, you could probably do the following:
rename the update method to something like UpdateTarget, so you trigger it manually.
use a property to encapsulate transitionTarget and use the properties setter to invoke UpdateTarget
additionally invoke UpdateTarget when the tween is completed: transitionTweener.OnComplete(RestartTween);
†An alternative option not considered in this Answer would be to recreate the tweens in the event of a change of transitionTarget.
A simpler solution than IARI's (but perhaps less efficient) is to kill the current tween when you need to transition and create a new one with the new destination.
Related
I am learning Unity from a Swift SpriteKit background where moving a sprite's x Position is as straight forward as an running an action as below:
let moveLeft = SKAction.moveToX(self.frame.width/5, duration: 1.0)
let delayAction = SKAction.waitForDuration(1.0)
let handSequence = SKAction.sequence([delayAction, moveLeft])
sprite.runAction(handSequence)
I would like to know an equivalent or similar way of moving a sprite to a specific position for a specific duration (say, a second) with a delay that doesn't have to be called in the update function.
gjttt1's answer is close but is missing important functions and the use of WaitForSeconds() for moving GameObject is unacceptable. You should use combination of Lerp, Coroutine and Time.deltaTime. You must understand these stuff to be able to do animation from Script in Unity.
public GameObject objectectA;
public GameObject objectectB;
void Start()
{
StartCoroutine(moveToX(objectectA.transform, objectectB.transform.position, 1.0f));
}
bool isMoving = false;
IEnumerator moveToX(Transform fromPosition, Vector3 toPosition, float duration)
{
//Make sure there is only one instance of this function running
if (isMoving)
{
yield break; ///exit if this is still running
}
isMoving = true;
float counter = 0;
//Get the current position of the object to be moved
Vector3 startPos = fromPosition.position;
while (counter < duration)
{
counter += Time.deltaTime;
fromPosition.position = Vector3.Lerp(startPos, toPosition, counter / duration);
yield return null;
}
isMoving = false;
}
Similar Question: SKAction.scaleXTo
The answer of git1 is good but there is another solution if you do not want to use couritines.
You can use InvokeRepeating to repeatedly trigger a function.
float duration; //duration of movement
float durationTime; //this will be the value used to check if Time.time passed the current duration set
void Start()
{
StartMovement();
}
void StartMovement()
{
InvokeRepeating("MovementFunction", Time.deltaTime, Time.deltaTime); //Time.deltaTime is the time passed between two frames
durationTime = Time.time + duration; //This is how long the invoke will repeat
}
void MovementFunction()
{
if(durationTime > Time.time)
{
//Movement
}
else
{
CancelInvoke("MovementFunction"); //Stop the invoking of this function
return;
}
}
You can use co-routines to do this. To do this, create a function that returns type IEnumerator and include a loop to do what you want:
private IEnumerator foo()
{
while(yourCondition) //for example check if two seconds has passed
{
//move the player on a per frame basis.
yeild return null;
}
}
Then you can call it by using StartCoroutine(foo())
This calls the function every frame but it picks up where it left off last time. So in this example it stops at yield return null on one frame and then starts again on the next: thus it repeats the code in the while loop every frame.
If you want to pause for a certain amount of time then you can use yield return WaitForSeconds(3) to wait for 3 seconds. You can also yield return other co-routines! This means the current routine will pause and run a second coroutine and then pick up again once the second co-routine has finished.
I recommend checking the docs as they do a far superior job of explaining this than I could here
I'm trying to implement cooldowns in my project in Unity, while this code seems to make sense, it doesn't work. The code that's posted is an all-around basic movement script.
I tried doing something with a cooldown -=time.deltatime, but that didn't seem to work. I've been trying several methods, but none seem to work.
The code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MovementScript : MonoBehaviour
{
public float cooldown = 0;
public float actualcooldown = 3f;
public bool isCooldown = false;
// Start is called before the first frame update
void Start()
{
}
// Update is called once per frame
void Update()
{
if (Input.GetKey(KeyCode.R))
{
GetComponent<Renderer>().material.color = Color.red;
}
if (Input.GetKey(KeyCode.G))
{
GetComponent<Renderer>().material.color = Color.green;
}
if (Input.GetKey(KeyCode.B))
{
GetComponent<Renderer>().material.color = Color.blue;
}
if (Input.GetKey(KeyCode.D))
{
transform.Translate(6f * Time.deltaTime, 0, 0);
}
if (Input.GetKey(KeyCode.A))
{
transform.Translate(-6f * Time.deltaTime, 0, 0);
}
if (Input.GetKeyDown(KeyCode.Space) && cooldown <= 0) {
transform.Translate(0f, 20f * Time.deltaTime, 0f);
isCooldown = true;
while (isCooldown == true)
{
coolDownhappening();
}
}
}
public void coolDownhappening()
{
cooldown = actualcooldown;
cooldown -= Time.deltaTime;
if (cooldown <= 0)
{
cooldown = 0;
}
}
}
You do
while (isCooldown == true)
{
coolDownhappening();
}
But you never change isCoolddown anywhere!
Also as was already mentioned in the comments you do not want to use while in the Update method at all, at least not in this usecase! This will freeze the entire mainthread for the given cooldown time - or in your case forever!
There are a lot of other issues in your code so let's go step by step:
Input.GetKey is true every frame while they given key is pressed. However, it makes no sense and only causes unecessary overhead to repeatedly set the materials color to the same value as long as a button stays pressed. What you rather want to do is apply it once.
→ rather use Input.GetKeyDown for these!
GetComponent is a quite expensive call. You should not repeadedly use GetComponent<Renderer>() but rather store the reference once and re-use it later
// most efficient is always to already reference this via the Inspector
[SerializeField] private Renderer _renderer;
// alternatively get it on runtime
private void Awake()
{
if(!_renderer) _rednerer = GetComponent<Renderer>();
}
and then later use
private void Update()
{
if(Input.GetKeyDown(KeyCode.R))
{
_renderer.material.color = Color.red;
}
...
}
Your moving part is actually fine. To make it slightly more readable I would however actually rather do something like
if (Input.GetKey(KeyCode.D))
{
transform.Translate(Vector3.right * 6f * Time.deltaTime);
}
else if (Input.GetKey(KeyCode.D))
{
transform.Translate(Vector3.left * 6f * Time.deltaTime);
}
also note the else here. Depends on your needs of course but usually you want contrary buttons exclusive.
Finally to the real deal: You actually want to have a jump method with a cooldown here.
First here you did it the other way round: Input.GetKeyDown is called only exactly once namely in the frame when the key went down. So your object "jumps" 20 * 1/FPS which for 60 FPS is always about 0.33. You probably rather wanted to move a certain distance upwards over multiple frames. After a certain height is reached, activate a cooldown.
As mentioned in the comments one can do this in Update using a timer but usually this makes the code a bit messy. Rather use a Coroutine:
private bool _canJump;
private void Update()
{
...
// _canJump is cheaper to check so check it first
if (_canJump && Input.GetKeyDown(KeyCode.Space))
{
StartCoroutine(JumpRoutine());
}
}
private IEnumerator JumpRoutine()
{
// avoid concurrent routines
if(!_canJump) yield break;
// disable jumping
_canJump = false;
// Now it really depends on what you actually want to do
// and how your object should move now
// you might e.g. want something like
var jumpedHeight = 0f;
while(jumpedHeight < 20f)
{
var jumpThisFrame = Vector3.up * 6f * Time.deltaTime;
transform.Translate(jumpThisFrame);
// important now! yield tells Unity to "pause" here,
// render this frame, and continue from here int he next frame
// without the yield statements this would again freeze your game until
// the exit condition is fulfilled!
yield return null;
}
// After reaching the target height this waits for 3 seconds but keeps on rendering meanwhile
yield return new WaitForSeconds(actualcooldown);
// after the cooldown allow next jump
_canJump = true;
}
I have a spawner object. Every time a gameobject is spawned, I want that object to move randomly (wandering). The problem in my script is that the gameobject movement is very random (jittery). How can I solve this?
void Start ()
{
InvokeRepeating("SpawnNPC", Spawntime, Spawntime);
}
// Update is called once per frame
void Update () {
population = GameObject.FindGameObjectsWithTag("NPCobject");
for (int i = 0; i < population.Length; i++)
{
getNewPosition();
if (population[i].transform.position != pos)
{
population[i].transform.position = Vector3.MoveTowards(population[i].transform.position, pos, .1f);
}
}
}
void getNewPosition()
{
float x = Random.Range(-22, 22);
float z= Random.Range(-22, 22);
pos = new Vector3(x, 0, z);
}
I made the New randomize vector in different method, because I plan to change it with pathfinder function and make it in different thread/task.
You are choosing a new direction every single frame. That will always be very jittery. You wan't to only change direction after, at least, a small interval. Here is a link to one way to do that from Unity's website. https://docs.unity3d.com/ScriptReference/MonoBehaviour.InvokeRepeating.html
What about using Navigation? As you said wandering, I thought it would give you a nice result and also make your code simple.
The following screenshot is a sample with Navigation. The moving game objects are also changing their direction nicely, although it cannot be seen in the sample because the game object is capsule...
Ground game object in the sample program has NavMesh. See here to build NavMesh.
Agent game object has NavMeshAgent Component. See here to set it up.
Th Behaviour class below is for Agent game object.
using UnityEngine;
using UnityEngine.AI;
public class NavAgentBehaviour : MonoBehaviour {
public Transform[] Destinations { get; set; }
// Use this for initialization
void Start ()
{
InvokeRepeating("changeDestination", 0f, 3f);
}
void changeDestination()
{
var agent = GetComponent<NavMeshAgent>();
agent.destination = Destinations[Random.Range(0, Destinations.Length)].position;
}
}
The next Behaviour class is just for spawning the Agent and setting up the destinations. On Unity, set it to whatever game object in the scene, and allocate game objects to the fields.
using UnityEngine;
public class GameBehaviour : MonoBehaviour {
public GameObject Agent;
public Transform SpawnPoint;
public Transform Destination1;
public Transform Destination2;
public Transform Destination3;
// Use this for initialization
void Start()
{
Agent.SetActive(false);
InvokeRepeating("Spawn", 0f, 2f);
}
void Spawn() {
var newAgent = Instantiate(Agent);
newAgent.GetComponent<NavAgentBehaviour>().Destinations = new[] { Destination1, Destination2, Destination3 };
newAgent.transform.position = SpawnPoint.position;
newAgent.SetActive(true);
}
}
Increase the number of destination, to make the moves look more random. By the way, the destinations do not need to be specified by game objects, which is only for making it easy to see the sample program's behaviour.
The source of the jitteriness comes from the fact that you are updating the position to move every frame so your objects never have a consistent location to move to. I would instead suggest attaching a new script to each of your objects that individually handles their movement. In that script you could do something like the following, which has a delay to keep the target position for more than 1 frame.
float delaytimer;
Vector3 pos;
void Start () {
getNewPosition(); // get initial targetpos
}
void Update () {
delaytimer += Time.deltaTime;
if (delaytimer > 1) // time to wait
{
getNewPosition(); //get new position every 1 second
delaytimer = 0f; // reset timer
}
transform.position = Vector3.MoveTowards(transform.position, pos, .1f);
}
void getNewPosition()
{
float x = Random.Range(-22, 22);
float z= Random.Range(-22, 22);
pos = new Vector3(x, 0, z);
}
You are changing the direction they are moving in every frame, thats what is causing the jitter.
You could wait a few moments before you change the direction, perhaps something like this.
// Outside update
float betweenChanges = 2;
float lastChange = 0;
// Inside update
if(Time.realtimeSinceStartup > lastChange)
{
// Change directions
// ...
lastChange = Time.realTimeSinceStart + betweenChanges;
}
You could also solve this by using InvokeRepeating or a Coroutine.
If you dont want all the NPC's to change direction at the same time and still plan on controlling every NPC from the same class like in your example, you should perhaps add a timer for each NPC instance instead and use that to decide when to change its direction.
A better idea would be to let each NPC have its own Movement-script.
I am learning Unity from a Swift SpriteKit background where moving a sprite's x Position is as straight forward as an running an action as below:
let moveLeft = SKAction.moveToX(self.frame.width/5, duration: 1.0)
let delayAction = SKAction.waitForDuration(1.0)
let handSequence = SKAction.sequence([delayAction, moveLeft])
sprite.runAction(handSequence)
I would like to know an equivalent or similar way of moving a sprite to a specific position for a specific duration (say, a second) with a delay that doesn't have to be called in the update function.
gjttt1's answer is close but is missing important functions and the use of WaitForSeconds() for moving GameObject is unacceptable. You should use combination of Lerp, Coroutine and Time.deltaTime. You must understand these stuff to be able to do animation from Script in Unity.
public GameObject objectectA;
public GameObject objectectB;
void Start()
{
StartCoroutine(moveToX(objectectA.transform, objectectB.transform.position, 1.0f));
}
bool isMoving = false;
IEnumerator moveToX(Transform fromPosition, Vector3 toPosition, float duration)
{
//Make sure there is only one instance of this function running
if (isMoving)
{
yield break; ///exit if this is still running
}
isMoving = true;
float counter = 0;
//Get the current position of the object to be moved
Vector3 startPos = fromPosition.position;
while (counter < duration)
{
counter += Time.deltaTime;
fromPosition.position = Vector3.Lerp(startPos, toPosition, counter / duration);
yield return null;
}
isMoving = false;
}
Similar Question: SKAction.scaleXTo
The answer of git1 is good but there is another solution if you do not want to use couritines.
You can use InvokeRepeating to repeatedly trigger a function.
float duration; //duration of movement
float durationTime; //this will be the value used to check if Time.time passed the current duration set
void Start()
{
StartMovement();
}
void StartMovement()
{
InvokeRepeating("MovementFunction", Time.deltaTime, Time.deltaTime); //Time.deltaTime is the time passed between two frames
durationTime = Time.time + duration; //This is how long the invoke will repeat
}
void MovementFunction()
{
if(durationTime > Time.time)
{
//Movement
}
else
{
CancelInvoke("MovementFunction"); //Stop the invoking of this function
return;
}
}
You can use co-routines to do this. To do this, create a function that returns type IEnumerator and include a loop to do what you want:
private IEnumerator foo()
{
while(yourCondition) //for example check if two seconds has passed
{
//move the player on a per frame basis.
yeild return null;
}
}
Then you can call it by using StartCoroutine(foo())
This calls the function every frame but it picks up where it left off last time. So in this example it stops at yield return null on one frame and then starts again on the next: thus it repeats the code in the while loop every frame.
If you want to pause for a certain amount of time then you can use yield return WaitForSeconds(3) to wait for 3 seconds. You can also yield return other co-routines! This means the current routine will pause and run a second coroutine and then pick up again once the second co-routine has finished.
I recommend checking the docs as they do a far superior job of explaining this than I could here
I am learning Unity from a Swift SpriteKit background where moving a sprite's x Position is as straight forward as an running an action as below:
let moveLeft = SKAction.moveToX(self.frame.width/5, duration: 1.0)
let delayAction = SKAction.waitForDuration(1.0)
let handSequence = SKAction.sequence([delayAction, moveLeft])
sprite.runAction(handSequence)
I would like to know an equivalent or similar way of moving a sprite to a specific position for a specific duration (say, a second) with a delay that doesn't have to be called in the update function.
gjttt1's answer is close but is missing important functions and the use of WaitForSeconds() for moving GameObject is unacceptable. You should use combination of Lerp, Coroutine and Time.deltaTime. You must understand these stuff to be able to do animation from Script in Unity.
public GameObject objectectA;
public GameObject objectectB;
void Start()
{
StartCoroutine(moveToX(objectectA.transform, objectectB.transform.position, 1.0f));
}
bool isMoving = false;
IEnumerator moveToX(Transform fromPosition, Vector3 toPosition, float duration)
{
//Make sure there is only one instance of this function running
if (isMoving)
{
yield break; ///exit if this is still running
}
isMoving = true;
float counter = 0;
//Get the current position of the object to be moved
Vector3 startPos = fromPosition.position;
while (counter < duration)
{
counter += Time.deltaTime;
fromPosition.position = Vector3.Lerp(startPos, toPosition, counter / duration);
yield return null;
}
isMoving = false;
}
Similar Question: SKAction.scaleXTo
The answer of git1 is good but there is another solution if you do not want to use couritines.
You can use InvokeRepeating to repeatedly trigger a function.
float duration; //duration of movement
float durationTime; //this will be the value used to check if Time.time passed the current duration set
void Start()
{
StartMovement();
}
void StartMovement()
{
InvokeRepeating("MovementFunction", Time.deltaTime, Time.deltaTime); //Time.deltaTime is the time passed between two frames
durationTime = Time.time + duration; //This is how long the invoke will repeat
}
void MovementFunction()
{
if(durationTime > Time.time)
{
//Movement
}
else
{
CancelInvoke("MovementFunction"); //Stop the invoking of this function
return;
}
}
You can use co-routines to do this. To do this, create a function that returns type IEnumerator and include a loop to do what you want:
private IEnumerator foo()
{
while(yourCondition) //for example check if two seconds has passed
{
//move the player on a per frame basis.
yeild return null;
}
}
Then you can call it by using StartCoroutine(foo())
This calls the function every frame but it picks up where it left off last time. So in this example it stops at yield return null on one frame and then starts again on the next: thus it repeats the code in the while loop every frame.
If you want to pause for a certain amount of time then you can use yield return WaitForSeconds(3) to wait for 3 seconds. You can also yield return other co-routines! This means the current routine will pause and run a second coroutine and then pick up again once the second co-routine has finished.
I recommend checking the docs as they do a far superior job of explaining this than I could here