Unity - How to interrupt an ongoing animation by reversing it - c#

Context
I got two transform states on a GameObject, A and B. I also have two animations that transition between the two states: A->B and B->A. The animations have different animation curves. The user can change the state of the GameObject at any moment.
What I want to achieve
If the state is changed while no animations are playing, the animations A->B and B->A are played normally. However, if the state is changed while an animation is playing, I want the current animation to reverse. For example, let's say that the user changes the state to B, and the animation A->B starts playing. Then the user suddenly changes back to state A before the animation A->B has finished. Here I want the A->B animation to start reversing to state A again, not playing the animation B->A.
What I currently have
I have tried to use Unity's Animator but have found no function to reverse an ongoing animation. I am open to using something else than the Animator but prefer not to use any external libraries.
Here is the current code I have with no reverse logic, only changing between state A and B
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestAnimation : MonoBehaviour
{
private Animator _animator;
private bool _isOpen = true;
void Awake()
{
this._animator = GetComponent<Animator>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (this._isOpen)
this._animator.SetTrigger("A");
else
this._animator.SetTrigger("B");
this._isOpen = !this._isOpen;
}
}
}

Found a solution now. Do not know if it is the most simple one but it works. What I did was to activate the animation by playing them via script, not using any triggers or floats in the animator as I tried first.
I also created two new animation states that are the reversed version of A->Band B->A. I looked at the normelizedTime variable in the animator to determine if any animation was currently playing. If so, I played the reversed animation at where it ended, see the code below.
Here is the animator with the different animation states:
And here is the code
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestAnimation : MonoBehaviour
{
private Animator _animator;
private bool _toggle = true;
private bool _isPlayingReverse = true;
void Awake()
{
this._animator = GetComponent<Animator>();
}
void Update()
{
if (Input.GetKeyDown(KeyCode.Space))
{
var animationTime = Mathf.Clamp(this._animator.GetCurrentAnimatorStateInfo(0).normalizedTime, 0.0f, 1.0f);
bool animationOngoing = animationTime < 1;
if (this._toggle)
{
if (animationOngoing && !this._isPlayingReverse)
{
this._animator.Play("A->B - Reversed", 0, 1 - animationTime);
this._isPlayingReverse = true;
}
else
{
this._animator.Play("B->A", 0, 1 - animationTime);
this._isPlayingReverse = false;
}
}
else
{
if (animationOngoing && !this._isPlayingReverse)
{
this._animator.Play("B->A - Reversed", 0, 1 - animationTime);
this._isPlayingReverse = true;
}
else
{
this._animator.Play("A->B", 0, 1 - animationTime);
this._isPlayingReverse = false;
}
}
this._toggle = !this._toggle;
}
}
}

Related

Activating animation when within radius

I am trying to create a script for my enemy turret, but it is not going well. I have a couple animations of the turret being activated and deactivated. What I need is that based on the distance from the player, it plays either animation. So once it moves inside the detection radius it plays the activation animation and once it is outside it plays the deactivation animation. Most of the other ways I try require me to create an Animation Controller, which I have little experience in using. I want a simple way to play one animation once it is inside and play a different one when it is outside. I think there was a way to store the animation clip in the script, and then play it. I have attached my current script, so you know what I mean.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class EnemyTurret : MonoBehaviour
{
public GameObject Player;
public float DistanceToPlayer;
public float DetectionRadius = 75;
// Start is called before the first frame update
void Start()
{
Player = GameObject.FindGameObjectWithTag("PlayerTank");
}
// Update is called once per frame
void Update()
{
DistanceToPlayer = Vector3.Distance(transform.position, Player.transform.position);
if (DistanceToPlayer<=DetectionRadius)
{
Debug.Log("Within Radius");
}
if (DistanceToPlayer >= DetectionRadius)
{
Debug.Log("Outside Radius");
}
}
}
Calculating and checking the distance of the player for every update() is not ideal. It will work, but it will do more work than it needs to when the player isn't even near it. Its not efficient.
What you may want to do if your player is a Rigidbody, is add a SphereCollider to the turret, set isTrigger=true, set the radius to be your detection radius, and handle the OnTriggerEnter() and OnTriggerExit() events to play or stop animations.
You can also add two public Animiation objects to the script, drag and drop your animations in the editor, then you can use animation.Play() and .Stop() etc. to control the animations.
Something similar to this. Not tested fully, but you can get the idea.
public float detectionRadius = 75;
public Animation activateAnimation;
public Animation deactivateAnimation;
void Start()
{
SphereCollider detectionSphere = gameObject.AddComponent<SphereCollider>();
detectionSphere.isTrigger = true;
detectionSphere.radius = detectionRadius;
detectionSphere.center = Vector3.zero;
}
private void OnTriggerEnter(Collider other)
{
if (other.gameObject.tag == "PlayerTank")
{
activateAnimation.Play();
}
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject.tag == "PlayerTank")
{
deactivateAnimation.Play();
}
}
Your animations must not loop otherwise you will have to add more logic to check if animation.isPlaying and do your own animation.Stop() etc.

Text is supposed to be displayed when childCount equals 0, but it doesn't

this should be a very simple answer. I'm following a Unity with C# tutorial for making a simple Space Invaders game, and at one point it is shown that when our enemyHolder object has no child objects left (when all enemies are destroyed) the attached text under the winText function should be displayed.
So we have
if (enemyHolder.childCount == 0)
{
winText.enabled = true;
}
When I run the code the text isn't displayed after the enemies are destroyed and no child object is left. It's like the code stops getting read at that point, although the character is still movable and you can generate new shots.
If I create two "Enemy" child objects and tell it to display the winText rather when the childCount reaches 1, it does work.
So why is it not working when the function calls for == 0?
EDIT: Complete class code:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class EnemyController : MonoBehaviour
{
private Transform enemyHolder;
public float speed;
public GameObject shot;
public Text winText;
public float fireRate = 0.997f;
// Start is called before the first frame update
void Start()
{
winText.enabled = false;
InvokeRepeating("MoveEnemy", 0.1f, 0.3f);
enemyHolder = GetComponent<Transform>();
}
void MoveEnemy()
{
enemyHolder.position += Vector3.right * speed;
foreach (Transform enemy in enemyHolder)
{
if (enemy.position.x < -10.5 || enemy.position.x > 10.5)
{
speed = -speed;
enemyHolder.position += Vector3.down * 0.5f;
return;
}
if (enemy.position.y <= -4)
{
GameOver.isPlayerDead = true;
Time.timeScale = 0;
}
if (enemyHolder.childCount == 1)
{
CancelInvoke();
InvokeRepeating("MoveEnemy", 0.1f, 0.25f);
}
if (enemyHolder.childCount == 0)
{
winText.enabled = true;
}
}
}
}
Your code is inside the void MoveEnemy() function.
I'm assuming your script is attached to in-game enemies. Your code doesn't run because the MoveEnemy function no longer runs if there are no enemies.
So, you need to handle enemy movement and scene handling in different scripts.
The code that checks the enemy holder's number of children should be placed inside a void Update() function. This Update() function should be placed on an object that never gets deleted. Its advantage is that it runs every frame.
As a convention, devs generally use separate empty objects or even the camera to attach scripts which contain Update functions that handle the scene. Good luck!
Read more on Update

(Unity C#) NPC Movement Script + Animations Not Running Smoothly

I have created a NPC that follows the main player. When the player is in a certain range of the NPC, the NPC is supposed to walk, run, and attack based on the distance between the player and the NPC.The NPC has a C# MutantMovement Script attached, which also contains Animations. The NPC is also a NavMesh Agent. My problem is that the Animations do not run smoothly with the logic. I hope someone can help. Below is the MutantMovement Script and the Animation Controller.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;
[RequireComponent (typeof (NavMeshAgent))]
[RequireComponent (typeof (Animator))]
public class MutantMovement : MonoBehaviour {
[SerializeField] private float _range = 10f;
public Transform _goal; // destination
public Transform _player;
private Animator _anim;
private bool _alive;
private float _distance;
private NavMeshAgent _agent;
void Start ()
{
_goal = GameObject.Find("Player").transform;
_player = GameObject.Find("Player").transform;
_alive = true;
// get a reference to the NavMesh Agent component
_agent = GetComponent<NavMeshAgent>();
_anim = GetComponent<Animator>();
}
void Update()
{
Vector3 direction = _goal.position - this.transform.position;
float angle = Vector3.Angle(direction, this.transform.forward);
if(Vector3.Distance(_goal.position, this.transform.position) < 10 &&
angle < 50){
_anim.SetBool("isIdle", false);
if ((_alive && direction.magnitude > 5))
{
_agent.destination = _goal.position;
_anim.SetBool("isWalking", true);
_anim.SetBool("isAttacking", false);
_anim.SetBool("isRunning", false);
if((_alive && direction.magnitude > 6.5)){
_agent.destination = _goal.position;
_anim.SetBool("isRunning", true);
_anim.SetBool("isWalking", false);
_anim.SetBool("isAttacking", false);
}
}
else{
_agent.isStopped = false;
_anim.SetBool("isAttacking", true);
_anim.SetBool("isWalking", false);
_anim.SetBool("isRunning", false);
}
}else{
_anim.SetBool("isIdle", true);
_anim.SetBool("isWalking", false);
_anim.SetBool("isAttacking", false);
}
}
public void SetAlive(bool alive)
{
_alive = alive;
}
}
Animation Controller
I would use Animator triggers rather than booleans in your particular case, so the transitions from one animation to another are triggered only once.
In your state machine, you can then set the transitions so :
walking or attacking ---[trigger run]----> running animation
running or walking -----[trigger attack]---> attack animation
running or attacking ----[trigger walk] ---> walking animation
It makes it overall easier as you don't have to turn the other bools to false when triggering a new animation state.
Another thing you can check to make sure the transitions are fluid, is to have a look at the exit time of your animations - if you turn "Has Exit Time" off, your transitions will be instant. You can find this option if you open your animator controller and click on your state machine transition arrow.

How can I make a game object jump when I press a key (preferably space)?

I have this C# script attached to my main camera game object which also has a capsule collider attribute. However, it doesn't seem to do anything. How should I modify/add to this to make the camera "jump" and fall down to the ground again?
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class Jump : MonoBehaviour {
[HideInInspector] public bool jump = false;
public float jumpForce = 1000f;
public Transform groundCheck;
private bool grounded = false;
private Rigidbody rb;
// Use this for initialization
void Awake ()
{
rb = GetComponent<Rigidbody>();
}
// Update is called once per frame
void Update ()
{
grounded = Physics2D.Linecast(transform.position, groundCheck.position, 1 << LayerMask.NameToLayer("Ground"));
if (Input.GetButtonDown("Jump") && grounded)
{
jump = true;
}
}
void FixedUpdate()
{
if (jump)
{
rb.AddForce(new Vector2(0f, jumpForce));
jump = false;
}
}
}
Also, I would like to have the key for this be the spacebar if possible, but whatever key works or is there already is fine. I am still learning C#, so please forgive me if the solution is obvious.
This line is most likely causing the problem:
grounded = Physics2D.Linecast(transform.position, groundCheck.position, 1 << LayerMask.NameToLayer("Ground"));`
There are 2 reason that it wont produce proper results:
You haven't setup your ground tiles or the place where you character moves to the "Ground" layer. You wont have this by default but you can add it from the Project Settings->Tags and Layers menu.
Your colliders are not close enough to the ground thus not causing collision.
Besides that it should work fine.

Creating a start menu in Unity 3D

I was trying to create a start menu to my unity game when I found this script that enables a hidden sprite as soon as the game starts. The script then disables it when the player presses the left mouse button or space. When I try to make multiple sprites show up and the disappear, using the same script, only one sprite appears. I'm also trying to find a way to change the scipt so that the payer have to click on the actual sprite to disable it instead of just pressing the space key.
This is the script:
using UnityEngine;
using System.Collections;
public class StartScreen : MonoBehaviour {
static bool sawOnce = false;
// Use this for initialization
void Start () {
if(!sawOnce) {
GetComponent<SpriteRenderer>().enabled = true;
Time.timeScale = 0;
}
sawOnce = true;
}
// Update is called once per frame
void Update () {
if(Time.timeScale==0 && (Input.GetKeyDown(KeyCode.Space) || Input.GetMouseButtonDown(0)) ) {
Time.timeScale = 1;
GetComponent<SpriteRenderer>().enabled = false;
}
}
}
It sounds as if you are looking to do something like this: http://docs.unity3d.com/ScriptReference/MonoBehaviour.OnMouseDown.html
Also, I always create a separate scene for my main menu systems, as they have done in that link.

Categories