I have a question please in my game when i write "LEFT" in a InputField and click on a UI Button the cube move "LEFT" and eat coins(the same for up, down , right) my problem is when i wrote this code below the player moved but not slowly more like disappear than appear in the position that declare it
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public InputField mainInputField;
//public float speed;
public GameObject Player;
public Button Click_me;
public float smoothing = 1f;
public Transform TargetRight1;
public Transform TargetRight2;
public Transform TargetUP;
// Start is called before the first frame update
void Start()
{
}
public void SubmitName()
{
string[] lines = mainInputField.text.Split('\n');
for (int i = 0; i < lines.Length; i++)
{
if (lines[i] == "UP")
{
// moveUP();
StartCoroutine(MyCoroutineUP(TargetUP));
}
else if (lines[i] == "DOWN")
{
//MoveDown();
}
else if (lines[i] == "LEFT")
{
//MoveLeft();
}
else if (lines[i] == "RIGHT")
{
StartCoroutine(MyCoroutineUP(TargetRight1));
}
}
// Click_me.interactable = false;
}
IEnumerator MyCoroutineUP(Transform target)
{
while (Vector3.Distance(Player.transform.position, target.position) > 0.05f)
{
Player.transform.position = Vector3.Lerp(Player.transform.position, target.position, smoothing * Time.deltaTime);
}
yield return null;
}
}
know if i put the yield return null; inside the while loop like this
while (Vector3.Distance(Player.transform.position, target.position) > 0.05f)
{
Player.transform.position = Vector3.Lerp(Player.transform.position, target.position, smoothing * Time.deltaTime);
yield return null;
}
the player move slowly and get the coins but if i have more than 2 ligne for example i wrote LEFT , UP the while loop won't work properly when i call the function in the first line. sorry for my English
You will get concurrent Coroutines.
It sounds like what you actually are asking is how to stack multiple commands and work them one by one. This gets a bit more complex but sounds like the perfect usecase for a Queue
private readonly Queue<Transform> _commands = new Queue<Transform>();
public void SubmitName()
{
var lines = mainInputField.text.Split('\n');
mainInputField.text = "";
foreach (var line in lines)
{
switch (line)
{
case "UP":
// adds a new item to the end of the Queue
_commands.Enqueue(TargetUp);
break;
case "DOWN":
_commands.Enqueue(TargetDown);
break;
case "LEFT":
_commands.Enqueue(TargetLeft);
break;
case "RIGHT":
_commands.Enqueue(TargetRight);
break;
}
}
StartCoroutine(WorkCommands());
}
private IEnumerator WorkCommands()
{
// block input
Click_me.interactable = false;
// run this routine until all commands are handled
while (_commands.Count > 0)
{
// returns the first element and at the same time removes it from the queue
var target = _commands.Dequeue();
// you can simply yield another IEnumerator
// this makes it execute and at the same time waits until it finishes
yield return MovementCoroutine(target);
}
// when done allow input again
Click_me.interactable = true;
}
To the lerping itself:
I wouldn't lerp like that. That starts the movement very quick and gets slower in the end but never really reaches the target position. If thats what you want leave it but I would rather recommend doing something like
private IEnumerator MovementCoroutine(Transform target)
{
var startPos = transform.position;
var targetPos = target.position;
var timePassed = 0f;
do
{
var lerpFactor = Mathf.SmoothStep(0, 1, timePassed / smoothing);
transform.position = Vector3.Lerp(startPos, targetPos, lerpFactor);
timePassed += Time.deltaTime;
yield return null;
}
while(timePassed < smoothing);
// just to be sure there is no over or undershooting
// in the end set the correct target position
transform.position = targetPos;
}
In smoothing you would then instead set the time in seconds the lerping should take in total. In my opinion this gives you more control. The SmoothStep makes the movement still being eased in and out.
If you want you could additionally also take the current distance into account for always making the object move with more or less the same speed regardless how close or far the target position is by adding/changing
var distance = Vector3.Distance(startPos, targetPos);
var duration = smoothing * distance;
do
{
var lerpFactor = Mathf.SmoothStep(0, 1, timePassed / duration);
...
}
while (timePassed < duration);
now in smoothing you would rather set the time in seconds the object should need to move 1 Unity unit.
I don't know your exact setup for the targets ofcourse but this is how it would look like with targets attached to the player (so they move along with it)
Related
In this script, I call a coroutine using Input; this coroutine allows that if we press F5, change the view of our character that we see in 3rd person mode to 1st person mode and if we press F5 again, it will replace the view. Only, by using input.GetKey and not input.GetKeyDown the coroutine will read several times which gives an ugly result. But when I use Input.GetKeyDown, and I'm in 1st person mode (the basic view is in 3rd person) my character's movements are blocked: I can't move forward, backward, jump etc... error in the script ?
If you want to test on unity, just download the file on this link https://github.com/TUTOUNITYFR/unitypackages-jeu-survie-2022-tufr/blob/main/Episode01/personnage-et-environnement.unitypackage then in the AimBehaviourBasic script, replace all the code with:
`
using UnityEngine;
using System.Collections;
// AimBehaviour inherits from GenericBehaviour. This class corresponds to aim and strafe behaviour.
public class AimBehaviourBasic : GenericBehaviour
{
public Texture2D crosshair; // Crosshair texture.
public float aimTurnSmoothing = 0.15f; // Speed of turn response when aiming to match camera facing.
public Vector3 aimPivotOffset = new Vector3(0.5f, 1.2f, 0f); // Offset to repoint the camera when aiming.
public Vector3 aimCamOffset = new Vector3(0f, 0.4f, -0.7f); // Offset to relocate the camera when aiming.
private int aimBool; // Animator variable related to aiming.
public bool aim; // Boolean to determine whether or not the player is aiming.
// Start is always called after any Awake functions.
void Start ()
{
// Set up the references.
aimBool = Animator.StringToHash("Aim");
}
// Update is used to set features regardless the active behaviour.
void Update ()
{
// Activate/deactivate aim by input.
if (Input.GetKeyDown (KeyCode.F5) && !aim)
{
StartCoroutine(ToggleAimOn());
}
else
if (Input.GetKeyDown (KeyCode.F5) && aim)
{
StartCoroutine(ToggleAimOff());
}
}
// Co-rountine to start aiming mode with delay.
private IEnumerator ToggleAimOn()
{
yield return new WaitForSeconds(0.05f);
// Aiming is not possible.
if (behaviourManager.GetTempLockStatus(this.behaviourCode) || behaviourManager.IsOverriding(this))
yield return false;
// Start aiming.
else
{
aim = true;
int signal = 1;
yield return new WaitForSeconds(0.1f);
aimCamOffset.x = Mathf.Abs(aimCamOffset.x) * signal;
aimPivotOffset.x = Mathf.Abs(aimPivotOffset.x) * signal;
yield return new WaitForSeconds(0.1f);
behaviourManager.GetAnim.SetFloat(speedFloat, 0);
// This state overrides the active one.
behaviourManager.OverrideWithBehaviour(this);
}
}
// Co-rountine to end aiming mode with delay.
private IEnumerator ToggleAimOff()
{
aim = false;
yield return new WaitForSeconds(0.3f);
behaviourManager.GetCamScript.ResetTargetOffsets();
behaviourManager.GetCamScript.ResetMaxVerticalAngle();
yield return new WaitForSeconds(0.05f);
behaviourManager.RevokeOverridingBehaviour(this);
}
// LocalFixedUpdate overrides the virtual function of the base class.
public override void LocalFixedUpdate()
{
// Set camera position and orientation to the aim mode parameters.
if(aim)
{
behaviourManager.GetCamScript.SetTargetOffsets (aimPivotOffset, aimCamOffset);
}
}
// LocalLateUpdate: manager is called here to set player rotation after camera rotates, avoiding flickering.
public override void LocalLateUpdate()
{
AimManagement();
}
// Handle aim parameters when aiming is active.
void AimManagement()
{
// Deal with the player orientation when aiming.
Rotating();
}
// Rotate the player to match correct orientation, according to camera.
void Rotating()
{
// Always rotates the player according to the camera horizontal rotation in aim mode.
Quaternion targetRotation = Quaternion.Euler(0, behaviourManager.GetCamScript.GetH, 0);
float minSpeed = Quaternion.Angle(transform.rotation, targetRotation) * aimTurnSmoothing;
// Rotate entire player to face camera.
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, minSpeed * Time.deltaTime);
}
// Draw the crosshair when aiming.
void OnGUI ()
{
if (crosshair)
{
float mag = behaviourManager.GetCamScript.GetCurrentPivotMagnitude(aimPivotOffset);
GUI.DrawTexture(new Rect(Screen.width / 2 - (crosshair.width * 0.5f),
Screen.height / 2 - (crosshair.height * 0.5f),
crosshair.width, crosshair.height), crosshair);
}
}
}
`
Now you can test, press play and then press F5: the camera will change position but if you try to move (les touches : Z,Q,S,D, escape or arrowUp, arrowDown etc..) absolutely nothing will happen: here is my problem... Sorry for my bad English I am French ; )
Using coroutines like this will mess up your code because both coroutines can run almost at the same time, for example:
If you press F5 the first time ToggleAimOn() will be executed but before it get's finished you can press F5 again and call ToggleAimOff(), and since coroutines are async, that means they can run at the same time so it will create a real messed up behaviour
What you can try is to have another flag beside aim that will check if a coroutine is running like this:
private bool IsCoroutineActive = false;
void Update ()
{
// Activate/deactivate aim by input.
if (Input.GetKeyDown (KeyCode.F5) && !IsCoroutineActive && !aim)
{
StartCoroutine(ToggleAimOn());
}
else
if (Input.GetKeyDown (KeyCode.F5) && !IsCoroutineActive && aim)
{
StartCoroutine(ToggleAimOff());
}
}
// Co-rountine to start aiming mode with delay.
private IEnumerator ToggleAimOn()
{
IsCoroutineActive = true;
yield return new WaitForSeconds(0.05f);
// Aiming is not possible.
if (behaviourManager.GetTempLockStatus(this.behaviourCode) || behaviourManager.IsOverriding(this))
{
IsCoroutineActive = false;
yield return false;
}
// Start aiming.
else
{
aim = true;
int signal = 1;
yield return new WaitForSeconds(0.1f);
aimCamOffset.x = Mathf.Abs(aimCamOffset.x) * signal;
aimPivotOffset.x = Mathf.Abs(aimPivotOffset.x) * signal;
yield return new WaitForSeconds(0.1f);
behaviourManager.GetAnim.SetFloat(speedFloat, 0);
// This state overrides the active one.
behaviourManager.OverrideWithBehaviour(this);
}
IsCoroutineActive = false;
}
// Co-rountine to end aiming mode with delay.
private IEnumerator ToggleAimOff()
{
IsCoroutineActive = true;
aim = false;
yield return new WaitForSeconds(0.3f);
behaviourManager.GetCamScript.ResetTargetOffsets();
behaviourManager.GetCamScript.ResetMaxVerticalAngle();
yield return new WaitForSeconds(0.05f);
behaviourManager.RevokeOverridingBehaviour(this);
IsCoroutineActive = false;
}
I've got a setup right now where I have a 3D object with an empty object parented to it inside the object. When you press one button, an empty object that the camera is following rotates 90 degrees. If you press the other button, it rotates the other direction in 90 degrees. The result is that the camera can spin around the object 4 times before it makes a complete rotation.
Currently it works well, but I'm trying to figure out how I add some easing to the animation so it doesn't look so rough. I know a little about working with curves in animation but I'm not sure how I can apply that to code (or if it's even the best way to do things)
public InputMaster controls;
private bool isSpinning;
private float rotationSpeed = 0.3f;
IEnumerator RotateMe(Vector3 byangles, float intime)
{
var fromangle = transform.rotation;
var toangle = Quaternion.Euler(transform.eulerAngles + byangles);
for (var t = 0f; t < 1; t += Time.deltaTime / intime)
{
transform.rotation = Quaternion.Slerp(fromangle, toangle, t);
yield return null;
transform.rotation = toangle;
}
Debug.Log("finished rotation");
isSpinning = false;
Debug.Log("isSpinning now false");
}
code above is where I create a coroutine that says how it is going to transform. One thing that confuses me here a little is that if I don't have the line that says transform.rotation = toangle; , the rotation comes out at like 89.5 degrees or something, and if you do it multiple times it goes several degrees off. Not sure why that happens.
void Rotation(float amount)
{
Debug.Log("rotation number is " + amount);
float holdingRotate = controls.Player.Camera.ReadValue<float>();
if (holdingRotate == 1 && isSpinning == false)
{
isSpinning = true;
Debug.Log("isSpinning now true");
StartCoroutine(RotateMe(Vector3.up * 90, rotationSpeed));
}
else if (holdingRotate == -1 && isSpinning == false)
{
isSpinning = true;
Debug.Log("isSpinning now true");
StartCoroutine(RotateMe(Vector3.up * -90, rotationSpeed));
}
}
and this part is where the animation gets called up. Any help much appreciated.
Have a look at Animation Curves like #immersive said.
You can easily define your own curves in inspector
simply by adding this to your code:
public AnimationCurve curve;
You can then use AnimationCurve.Evaluate
to sample the curve.
The alternative is to use a premade library from the AssetStore/Package Manager like DOTween or LeanTween to name some.
Comments inline:
public class SmoothRotator : MonoBehaviour
{
// Animation curve holds the 'lerp factor' from starting angle to final angle over time
// (Y axis is blend factor, X axis is normalized 'time' factor)
public AnimationCurve Ease = AnimationCurve.EaseInOut(0, 0, 1, 1);
public float Duration = 1f;
private IEnumerator ActiveCoroutine = null;
public void RotateToward(Quaternion targetAngle) {
// If there's a Coroutine to stop, stop it.
if (ActiveCoroutine != null)
StopCoroutine(ActiveCoroutine);
// Start new Coroutine and cache the IEnumerator in case we need to interrupt it.
StartCoroutine(ActiveCoroutine = Rotator(targetAngle));
}
public IEnumerator Rotator(Quaternion targetAngle) {
// Store starting angle
var fromAngle = transform.rotation;
// Accumulator for Time.deltaTime
var age = 0f;
while (age < 1f) {
// normalize time (scale to "percentage complete") and clamp it
var normalisedTime = Mathf.Clamp01(age / Duration);
// Pull lerp factor from AnimationCurve
var lerpFactor = Ease.Evaluate(normalisedTime);
// Set rotation
transform.rotation = Quaternion.Slerp(fromAngle, targetAngle, lerpFactor);
// Wait for next frame
yield return null;
// Update age with new frame's deltaTime
age += Time.deltaTime;
}
// Animation complete, force true value
transform.rotation = targetAngle;
// Housekeeping, clear ActiveCoroutine
ActiveCoroutine = null;
}
}
for (var t = 0f; t < 1; t += Time.deltaTime / intime)
{
transform.rotation = Quaternion.Slerp(fromangle, toangle, t);
yield return null;
}
transform.rotation = toangle;
Because t won't equal to 1.
You just need to set the rotation to the target after the loop.
(Once it reaches point B, it goes to point A and back to point B in a smooth and orderly fashion). For some reason, the platform refuses to move and stays put. I have tried many things such as using vector3.movetowards and much more but nothing makes it move.
Here is the code. (Point A and Point B are empty game objects that are not parented to the platform)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveTwoTransforms : MonoBehaviour
{
public Transform pointA;
public Transform pointB;
bool HeadingtowardsB;
bool HeadingtowardsA;
public float speed = 10;
// Start is called before the first frame update
void Start()
{
transform.position = pointA.position;
HeadingtowardsB = true;
HeadingtowardsA = false;
GlideAround();
}
// Update is called once per frame
void Update()
{
}
public IEnumerator GlideAround()
{
while (true)
{
while ((Mathf.Abs((pointB.position.x - transform.position.x) + (pointB.position.y - transform.position.y)) > 0.05f) && HeadingtowardsB == true && HeadingtowardsA==false )
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointB.position, speed * Time.deltaTime);
if(Mathf.Abs((pointB.position.x - transform.position.x) + (pointB.position.y - transform.position.y)) > 0.05f)
{
HeadingtowardsB = false;
HeadingtowardsA = true;
}
}
HeadingtowardsB = false;
HeadingtowardsA = true;
while (Mathf.Abs((pointA.position.x - transform.position.x) + (pointA.position.y - transform.position.y)) > 0.05f && HeadingtowardsA==true && HeadingtowardsB==false)
{
yield return new WaitForEndOfFrame();
transform.position=transform.position=Vector3.Lerp(transform.position, pointA.position, speed*Time.deltaTime);
}
}
}
}
There are no error messages, the platform won't move. The platform is still colliding and it seems to behave like a normal platform.
GlideAround() is an IEnumerator and can not be called like a method. You have to start it using StartCoroutine
StartCoroutine(GlideAround());
Also note that speed * Time.deltaTime makes little sense for usage in Lerp. You usually would want a constant value between 0-1 in your case (since you re-use the current position as first parameter).
E.g. a value of 0.5 means: Every frame set the new position to the center between the current and the target position.
Since you catch it using a threashold of 0.05f this should be fine but in general I wouldn't use Lerp like this ... with very small values you might never really reach the target position.
I would therefore prefer to either control the constant speed and use
bool isHeadingA = true;
while(true)
{
// if it was intended you can ofourse also again use
// Vector2.Distance(transform.position, isHeadingA ? pointA.position : pointB.position) <= 0.05f)
while (transform.position != (isHeadingA ? pointA.position : pointB.position))
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.MoveTowards(transform.position, isHeadingA ? pointA.position : pointB.position, speed * Time.deltaTime);
}
// flip the direction
isHeadingA = !isHeadingA;
}
!= has a precision of 0.00001 and is fine here since MoveTowards avoids overshooting so at some point it will surely reach the position if speed != 0.
Or alternatively you can use Lerp if you rather want to control the duration of the movement with a smoothed in and out speed using e.g. Mathf.PingPong as factor and Mathf.SmoothStep for easing in and out like
while(true)
{
yield return new WaitForEndOfFrame();
// linear pingpong between 0 and 1
var factor = Mathf.PingPong(Time.time, 1);
// add easing at the ends
factor = Mathf.SmoothStep(0, 1, factor);
// optionally add even more easing ;)
//factor = Mathf.SmoothStep(0, 1, factor);
transform.position = Vector2.Lerp(pointA.position, pointB.position, factor);
}
Try this:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class MoveTwoTransforms : MonoBehaviour
{
public Transform pointA;
public Transform pointB;
bool HeadingtowardsB;
bool HeadingtowardsA;
public float speed = 10;
// Start is called before the first frame update
void Start()
{
transform.position = pointA.position;
HeadingtowardsB = true;
HeadingtowardsA = false;
StartCoroutine(GlideAround());
}
// Update is called once per frame
void Update()
{
}
public IEnumerator GlideAround()
{
//Because we want a specific speed, the % between the two points
//that we should be at will be equal to (time * speed) / distance
//with an adjustment for going backwards.
float distance = Vector3.Distance(pointA, pointB) * 2;
float lapTime = distance / speed;
float startTime = Time.time;
Debug.Log("The platform speed is: " + speed.ToString());
Debug.Log("The distance for one full lap is: " + distance.ToString());
Debug.Log("One lap will take: " + lapTime.ToString() + " seconds");
while (true)
{
yield return new WaitForEndOfFrame();
float elapsedTime = (Time.time - startTime) % lapTime;
float progress = elapsedTime / (lapTime / 2);
if (progress > 1){
progress = 2 - progress;
}
Debug.Log("The platform speed is currently: " + progress.ToString() + "% between pointA and pointB");
transform.position = Vector2.Lerp(pointA.position, pointB.position, progress);
}
}
As I mentioned in the comments, you Lerp uses a percentage between two points as the return value. You need to give it a percent, not a speed.
Here is an implementation that uses speed, like you wanted, but #derHugo's answer with PingPong is much simpler!
The main issue is that you are not using coroutines properly, as pointed out in derHugo's answer.
However, I'll provide my own answer, seeing that you are making the rookie mistake of way over-engineering this problem.
I think teaching by example might be the most appropriate in this case, so here it is:
If the points dictating the platform's movement are static, you should do this with animation. I won't explain it here. Tutorials like this one can easily be found all over the unity tutorials, unity forums, other StackOverflow Q&As, and youtube.
If your points are dynamic, this is more than enough:
public class MoveTwoTransforms : MonoBehaviour {
public Transform pointA;
public Transform pointB;
public float speed = 10;
void Start() {
transform.position = pointA.position;
StartCoroutine(GlideAround());
}
private IEnumerator MoveTowards(Vector3 targetPosition) {
while (transform.position != targetPosition) {
transform.position = Vector3.MoveTowards(transform.position, targetPosition, speed * Time.deltaTime);
yield return null;
}
}
private IEnumerator GlideAround() {
while(true) {
yield return StartCoroutine(MoveTowards(pointA));
yield return StartCoroutine(MoveTowards(pointB));
}
}
}
Just a final note:
If the platform should have physics or a collider, it is preferable to add a Rigidbody, set it to be kinematic, and to the movement by setting the Rigidbody.position instead of the transform. This is because that is updated on the physics loop (FixedUpdate) rather than the frame loop (Update), and avoids some bugs related to asyncrony between the physics system and moving objects through transform's position.
rigidbody.position = Vector3.MoveTowards(rigidbody.position, //...
Here is the code that worked.
public IEnumerator GlideAround()
{
while (true)
{
while (HasReachedA == false)
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointA.position, 0.01f);
if ((Mathf.Abs(Vector2.Distance(pointA.position, transform.position)) < 0.01f))
{
HasReacedB = false;
HasReachedA = true;
}
}
while (HasReacedB == false)
{
yield return new WaitForEndOfFrame();
transform.position = Vector2.Lerp(transform.position, pointB.position, 0.01f);
if ((Mathf.Abs(Vector2.Distance(pointB.position, transform.position)) < 0.01f))
{
HasReacedB = true;
HasReachedA = false;
}
}
}
}
I previously posted this question on the gameDev SE but with no luck, therefore I am trying to see if I could find some help here.
I am having some troubles with the transitions in my animator. Specifically, I am trying to set up a some code to handle combo sequences, and to do so I am using coroutines that exploit the state machine given by the animations in the animator. Here is my script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityStandardAssets.CrossPlatformInput;
using UnityEngine.SceneManagement;
/*enum PlayerState is a list of states that can be taken by the player character. They will be used to implement a finite state machine-like
behavior with the actions it can take*/
public enum PlayerState {
walk,
attack,
interact,
dash,
stagger
}
public class Player_Base : MonoBehaviour {
//Basic parameters
Rigidbody2D myRigidBody; //These are to call the components of the Player GameObject
public static Transform playerPos;
public PlayerState currentState;
Animator myAnimator;
public HealthManager myStatus;
private bool isAlive = true;
//Sorting layers parameters
public CapsuleCollider2D myFeet;
public static float playerVertPos; // Need this to sort other objects layers in another script.
//Movement parameters
[SerializeField] float moveSpeed = 3f; // use [serfiel] or public in order to have something that you can modify from Unity UI
public Vector2 moveInput;
private bool isMoving;//Implementing the state machine and the *blend trees*, you need only to define one bool for all animations of a kind (eg walking anims)
//Combat parameters
private int comboCounter = 0;
private float comboTimer = 0;
//dash parameters
[SerializeField] float dashTimeMax = 1f;
[SerializeField] float dashTime = 0;
[SerializeField] float dashPush = 0.001f;
[SerializeField] float dashSpeed = 10f;
// Use this for initialization
void Start()
{
currentState = PlayerState.walk;//Initial default state of the player
myRigidBody = GetComponent<Rigidbody2D>(); /*the getcomp looks for the related component in the <> and uses it in the code*/
myFeet = GetComponent<CapsuleCollider2D>();
myAnimator = GetComponent<Animator>();
myAnimator.SetFloat("MoveX", 0);//If i do not set a default values for these, if player attacks without moving first, all colliders will activate and will hit all around him
myAnimator.SetFloat("MoveY", -1);
myStatus = GameObject.FindObjectOfType<HealthManager>();
}
// Update is called once per frame
void Update()
{
playerVertPos = myFeet.bounds.center.y;
moveInput = Vector2.zero;/*getaxis and getaxisraw register the input of the axes and outputs +1 or -1 according to the axis direction*/
moveInput.x = Input.GetAxisRaw("Horizontal");
moveInput.y = Input.GetAxisRaw("Vertical");
if (!isAlive)
{
return;
}
else {
if (currentState == PlayerState.walk)//It will consider walking only when in that state, this means that if it is attacking for instance,
//it needs to change its state. Good for compartimentalization of the actions (otherwise I could have changed the direction of the attacks)
{
if (moveInput != Vector2.zero)//This if statement is such that if there is no new input to update the movement with, the last (idle) animation
//will remain, so if you go right and stop, the player keeps facing right
{
Move();
myAnimator.SetFloat("MoveX", moveInput.x);
myAnimator.SetFloat("MoveY", moveInput.y);
myAnimator.SetBool("isMoving", true);
}
else {
myAnimator.SetBool("isMoving", false);
}
}
//Attack inputs
if (Input.GetKeyDown(KeyCode.Mouse0) && currentState != PlayerState.attack)//second clause because i do not want to indefinitely attack every frame
{
StartCoroutine(FirstAttack());
}
if (Input.GetKeyDown(KeyCode.Space) && currentState != PlayerState.dash)
{
StartCoroutine(Dashing());
}
DeathCheck();//check if player is still alive
}
}
public void Move()
{
moveInput.Normalize();
myRigidBody.MovePosition(myRigidBody.position + moveInput * moveSpeed * Time.deltaTime);
//If i want to work with the velocity vector: i have to use rb.velocity, not just taking the xplatinput times movespeed
}
public void MoveOnAnimation(int xMove, int yMove, float displacement)
{
moveInput.x = xMove;
moveInput.y = yMove;
moveInput.Normalize();
myRigidBody.MovePosition(myRigidBody.position + moveInput * displacement * Time.deltaTime);
}
private IEnumerator FirstAttack() {
//Start Attack
comboCounter = 1;
myAnimator.SetInteger("comboSequence", comboCounter);
currentState = PlayerState.attack;
yield return new WaitForSeconds(AttackTemplate.SetDuration(0.6f) - comboTimer);//Problem: if i reduce the time below the animation time of the second animation, the second animation won't go untile the end
comboTimer = AttackTemplate.SetComboTimer(0.4f);
//if combo not triggered:
while (comboTimer >= 0)
{
Debug.Log(comboTimer);
comboTimer -= Time.deltaTime;
if (Input.GetKeyDown(KeyCode.Mouse0))
{
Debug.Log("Chained");
StopCoroutine(FirstAttack());
StartCoroutine(SecondAttack());
}
yield return null;
}
comboCounter = 0;
myAnimator.SetInteger("comboSequence", comboCounter);
currentState = PlayerState.walk;
}
private IEnumerator SecondAttack()
{
comboCounter = 2;
myAnimator.SetInteger("comboSequence", comboCounter);
currentState = PlayerState.attack;
yield return null;
//if combo not triggered:
yield return new WaitForSeconds(AttackTemplate.SetDuration(0.9f));
comboCounter = 0;
myAnimator.SetInteger("comboSequence", comboCounter);
currentState = PlayerState.walk;
}
private void Dash()
{
if (dashTime >= dashTimeMax)
{
dashTime = 0;
myRigidBody.velocity = Vector2.zero;
currentState = PlayerState.walk;
}
else
{
currentState = PlayerState.dash;
dashTime += Time.deltaTime;
moveInput.Normalize();
Vector2 lastDirection = moveInput;
myRigidBody.velocity = lastDirection * dashSpeed;
}
}
private IEnumerator Dashing()
{
currentState = PlayerState.dash;
for (float timeLapse = 0; timeLapse < dashTime; timeLapse = timeLapse + Time.fixedDeltaTime)
{
moveInput.Normalize();
Vector2 lastDirection = moveInput;
myRigidBody.velocity = lastDirection * dashSpeed;
}
yield return null;
myRigidBody.velocity = Vector2.zero;
currentState = PlayerState.walk;
}
private void DeathCheck() //if the player health reaches 0 it will run
{
if (HealthManager.health == 0) {
isAlive = false; // this is checked in the update, when false disables player inputs
myRigidBody.constraints = RigidbodyConstraints2D.FreezePosition; // if i don't lock its position, the last bounce with the enemy pushes the player towards inifinity
myAnimator.SetTrigger("death");//triggers the death animation
StartCoroutine(LoadNextScene());
}
}
[SerializeField] float LevelLoadDelay = 5f;
[SerializeField] float LevelSlowMo = 1f;
IEnumerator LoadNextScene()
{
Time.timeScale = LevelSlowMo;
yield return new WaitForSecondsRealtime(LevelLoadDelay);
Time.timeScale = 1f;
var CurrentSceneIndex = SceneManager.GetActiveScene().buildIndex;
SceneManager.LoadScene(CurrentSceneIndex + 1);
}
}
What I am doing basically is to use enums to define the player states and on input, if the player is not already in the attacking state, perform an attack. Once the FirstAttack() is called, it will first of all update an integer, comboCounter, which handles the transitions between consecutive attacks, input said integer in the animator and then change my state to attack. After this, I created a while loop that goes on until the end of an established time interval during which the player would be able to press the same attack button to chain the combo. If this does not happen, the state and integer parameter are reset.
The problem I am facing is that while the player can actually perform the combo with the second attack, during all the interval in which the first animation is active it keeps looping. Furthermore, I noticed that the second animation does not reach the end, it seems like it stops once the interval that I previously set will end.
Update: This is the screenshot of my animator window:
The transitions any state -> 1stAttack and 1stAttack -> 2ndAttack is handled by the same integer parameter, comboSequence, which is set to 0 normally, to 1 for 1stAttack and to 2 for the second one. I observed that the transition any state -> 1stAttack is triggered multiple times whenever I press the hit button, in line with the looping problem I am facing.
I have tried a couple of things, for instance using normal functions instead of a coroutine, but in this way, I do not understand why, there are problems with the enums states, also I think that in the long term this approach would be more modular and customisable. I feel like I am missing something trivial but I do not understand what and it has been some time now, so any help would be much appreciated!
Disable Can Transition To Self = false
If I set the rotation speed to 5 for example it will rotate facing the next target waypoint and then will move to it. But the camera rotation will be too fast.
Changing the speed to 0.01 make it rotating in a good slowly smooth speed. But then at 0.01 the camera rotate facing the next waypoint but never move to it. It stay on place.
This is the waypoints script:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Waypoints : MonoBehaviour
{
private GameObject[] waypoints;
private Transform currentWaypoint;
private enum CameraState
{
StartRotating,
Rotating,
Moving,
Waiting
}
private CameraState cameraState;
public GameObject player;
public float speed = 5;
public float WPradius = 1;
public LookAtCamera lookAtCam;
void Start()
{
cameraState = CameraState.StartRotating;
}
void Update()
{
switch (cameraState)
{
// This state is used as a trigger to set the camera target and start rotation
case CameraState.StartRotating:
{
// Sanity check in case the waypoint array was set to length == 0 between states
if (waypoints.Length == 0)
break;
// Tell the camera to start rotating
currentWaypoint = waypoints[UnityEngine.Random.Range(0, waypoints.Length)].transform;
lookAtCam.target = currentWaypoint;
lookAtCam.setTime(0.0f);
cameraState = CameraState.Rotating;
break;
}
// This state only needs to detect when the camera has completed rotation to start movement
case CameraState.Rotating:
{
if (lookAtCam.IsRotationFinished)
cameraState = CameraState.Moving;
break;
}
case CameraState.Moving:
{
// Move
transform.position = Vector3.MoveTowards(transform.position, currentWaypoint.position, Time.deltaTime * speed);
// Check for the Waiting state
if (Vector3.Distance(currentWaypoint.position, transform.position) < WPradius)
{
// Set to waiting state
cameraState = CameraState.Waiting;
// Call the coroutine to wait once and not in CameraState.Waiting
// Coroutine will set the next state
StartCoroutine(WaitForTimer(3));
}
break;
}
case CameraState.Waiting:
// Do nothing. Timer has already started
break;
}
}
IEnumerator WaitForTimer(float timer)
{
yield return new WaitForSeconds(timer);
cameraState = CameraState.StartRotating;
}
public void RefreshWaypoints()
{
waypoints = GameObject.FindGameObjectsWithTag("Target");
}
}
And the look at camera script:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LookAtCamera : MonoBehaviour
{
// Values that will be set in the Inspector
public Transform target;
public float RotationSpeed;
private float timer = 0.0f;
public bool IsRotationFinished
{
get { return timer > 0.99f; }
}
// Update is called once per frame
void Update()
{
if (target != null && timer < 0.99f)
{
// Rotate us over time according to speed until we are in the required rotation
transform.rotation = Quaternion.Slerp(transform.rotation,
Quaternion.LookRotation((target.position - transform.position).normalized),
timer);
timer += Time.deltaTime * RotationSpeed;
}
}
public void setTime(float time)
{
timer = time;
}
}
Problem
Your script basically works! The problem is in
private void Update()
{
if (target != null && timer < 0.99f)
{
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation((target.position - transform.position).normalized), timer);
timer += Time.deltaTime * RotationSpeed;
}
}
there are two issues with that:
You add Time.deltaTime * RotationSpeed so the time it takes to reach the 1 or in your case 0.99 simply takes 1/RotationSpeed = 100 times longer than usual. So your camera will stay in the Rotating state for about 100 seconds - after that it moves just fine!
(This one might be intentional but see below for a Better Solution) Quaternion.Slerp interpolates between the first and second rotation. But you always use the current rotation as startpoint so since the timer never reaches 1 you get a very fast rotation at the beginning but a very slow (in fact never ending) rotation in the end since the distance between the current rotation and the target rotation gets smaller over time.
Quick-Fixes
Those fixes repair your current solution but you should checkout the section Better Solution below ;)
In general for comparing both float values you should rather use Mathf.Approximately and than use the actual target value 1.
if (target != null && !Mathf.Approximately(timer, 1.0f))
{
//...
timer += Time.deltaTime * RotationSpeed;
// clamps the value between 0 and 1
timer = Mathf.Clamp01(timer);
}
and
public bool IsRotationFinished
{
get { return Mathf.Approximately(timer, 1.0f); }
}
You should either use Quaternion.Slerp storing the original rotation and use it as first parameter (than you will see that you need a way bigger RotationSpeed)
private Quaternion lastRotation;
private void Update()
{
if (target != null && !Mathf.Approximately(timer, 1.0f))
{
transform.rotation = Quaternion.Slerp(lastRotation, Quaternion.LookRotation((target.position - transform.position).normalized), timer);
timer += Time.deltaTime * RotationSpeed;
}
else
{
lastRotation = transform.rotation;
}
}
Or instead of Quaternion.Slerp use Quaternion.RotateTowards like
transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.LookRotation((target.position - transform.position).normalized), RotationSpeed * Time.deltaTime);
Better Solution
I would strongly suggest to use the Coroutines for everything instead of handling this kind of stuff in Update. They are way easier to control and makes your code very clean.
Look how your scripts would shrink and you wouldn't need all the properties, fields and comparing floats anymore. You could do most things you are currently getting and setting to wait for a certain thing to happen in only a few single lines.
In case you didn't know: You can actually simply yield return another IEnumerator on order to wait for it to finish:
Waypoints
public class Waypoints : MonoBehaviour
{
private GameObject[] waypoints;
public GameObject player;
public float speed = 5;
public float WPradius = 1;
public LookAtCamera lookAtCam;
private Transform currentWaypoint;
private void Start()
{
// maybe refresh here?
//RefreshWaypoints();
StartCoroutine(RunWaypoints());
}
private IEnumerator RunWaypoints()
{
// Sanity check in case the waypoint array has length == 0
if (waypoints.Length == 0)
{
Debug.Log("No Waypoints!", this);
yield break;
}
// this looks dnagerous but as long as you yield somewhere it's fine ;)
while (true)
{
// maybe refresh here?
//RefreshWaypoints();
// Sanity check in case the waypoint array was set to length == 0 between states
if (waypoints.Length == 0)
{
Debug.Log("No Waypoints!", this);
yield break;
}
// first select the next waypoint
// Note that you might get the exact same waypoint again you currently had
// this will throw two errors in Unity:
// - Look rotation viewing vector is zero
// - and transform.position assign attempt for 'Main Camera' is not valid. Input position is { NaN, NaN, NaN }.
//
// so to avoid that rather use this (not optimal) while loop
// ofcourse while is never good but the odds that you will
// always get the same value over a longer time are quite low
//
// in case of doubt you could still add a yield return null
// than your camera just waits some frames longer until it gets a new waypoint
Transform newWaypoint = waypoints[Random.Range(0, waypoints.Length)].transform;
while(newWaypoint == currentWaypoint)
{
newWaypoint = waypoints[Random.Range(0, waypoints.Length)].transform;
}
currentWaypoint = newWaypoint;
// tell camera to rotate and wait until it is finished in one line!
yield return lookAtCam.RotateToTarget(currentWaypoint);
// move and wait until in correct position in one line!
yield return MoveToTarget(currentWaypoint);
//once waypoint reached wait 3 seconds than start over
yield return new WaitForSeconds(3);
}
}
private IEnumerator MoveToTarget(Transform currentWaypoint)
{
var currentPosition = transform.position;
var duration = Vector3.Distance(currentWaypoint.position, transform.position) / speed;
var passedTime = 0.0f;
do
{
// for easing see last section below
var lerpFactor = passedTime / duration;
transform.position = Vector3.Lerp(currentPosition, currentWaypoint.position, lerpFactor);
passedTime += Time.deltaTime;
yield return null;
} while (passedTime <= duration);
// to be sure to have the exact position in the end set it fixed
transform.position = currentWaypoint.position;
}
public void RefreshWaypoints()
{
waypoints = GameObject.FindGameObjectsWithTag("Target");
}
}
LookAtCamera
public class LookAtCamera : MonoBehaviour
{
// Values that will be set in the Inspector
public float RotationSpeed;
public IEnumerator RotateToTarget(Transform target)
{
var timePassed = 0f;
var targetDirection = (target.position - transform.position).normalized;
var targetRotation = Quaternion.LookRotation(targetDirection);
var currentRotation = transform.rotation;
var duration = Vector3.Angle(targetDirection, transform.forward) / RotationSpeed;
do
{
// for easing see last section below
var lerpFactor = timePassed / duration;
transform.rotation = Quaternion.Slerp(currentRotation, targetRotation, lerpFactor);
timePassed += Time.deltaTime;
yield return null;
} while (timePassed <= duration);
// to be sure you have the corrcet rotation in the end set it fixed
transform.rotation = targetRotation;
}
}
Note
Again instead of Quaternion.Slerp and currentRotation you could also simply use Quaternion.RotateTowards like
transform.rotation = Quaternion.RotateTowards(transform.rotation, targetRotation, RotationSpeed * Time.deltaTime);
And for the movement you can also still use Vector3.MoveTowards if you want
while (Vector3.Distance(currentWaypoint.position, transform.position) < WPradius)
{
transform.position = Vector3.MoveTowards(transform.position, currentWaypoint.position, Time.deltaTime * speed);
yield return null;
}
but I would prefer to use the Lerp solutions. Why I suggest to rather use Lerp?
You can very easy controll now whether you want to move/rotate by a certain speed or rather give it fixed duration in which the move/rotation shall be finished regardless how big the differenc is - or even have some additional checks in order to decide for one of those options!
You can ease-in and -out the movement/rotation! See below ;)
Hint for easing Lerp movements
For still maintaining an eased-in and/or eased-out movement and rotation I found this block How to Lerp like a pro very helpfull! (adopted to my examples)
For example, we could “ease out” with sinerp:
var lerpFactor = Mathf.Sin(passedTime / duration * Mathf.PI * 0.5f);
Or we could “ease in” with coserp:
var lerpFactor = 1f - Mathf.Cos(passedTime / duration * Mathf.PI * 0.5f);
We could even create exponential movement:
var lerpFactor = Mathf.Pow(passedTime / duration, 2);
The multiplication property mentioned above is the core concept behind some interpolation methods which ease in and ease out, such as the famous “smoothstep” formula:
var lerpFactor = Mathf.Pow(passedTime / duration, 2) * (3f - 2f * passedTime / duration);
Or my personal favorite, “smootherstep”:
var lerpFactor = Mathf.Pow(passedTime / duration, 3) * (6f * (passedTime / duration) - 15f) + 10f);