How to simplify animation transaction - c#

In 2D scene, I have 3 idle states and 3 walk states (each idle/walk state has 3 direction sub state).
I know I can make transactions in animator.
My question is, how to simplify transaction? For example, idle_front state has 5 transactions to other states (idle_back, idle_right, walk_front, walk_back, walk_right), now if I want to add an attack state, also with 3 direction sub state, the transaction lines makes me crazy.
My solution
I have tried using animator.Play() to play animation, but when switch to attack state, there is a problem.
The invocation of idle state play is in FixedUpdate() event function, after response to the attack key press event (to play attack animation), only little frames of attack is played, it is soon replaced by idle animation.
How can I implement similar effect of a trigger, to play the whole attack animation? Or is there any way to simplify transactions, and continue to use a trigger?
Example code is here
private void FixedUpdate()
{
if (moveInput.y == 0)
{
switch (moveInput.x)
{
case < 0:
animator.SetInteger(Direction, (int) MovingDirection.Left);
animator.Play("player_walk_right");
spriteRenderer.flipX = true;
break;
case > 0:
animator.SetInteger(Direction, (int) MovingDirection.Right);
animator.Play("player_walk_right");
spriteRenderer.flipX = false;
break;
}
}
else
{
switch (moveInput.y)
{
case < 0:
animator.SetInteger(Direction, (int) MovingDirection.Down);
animator.Play("player_walk_front");
break;
case > 0:
animator.SetInteger(Direction, (int) MovingDirection.Up);
animator.Play("player_walk_back");
break;
}
}
}
void OnFire()
{
switch (animator.GetInteger(Direction))
{
case (int) MovingDirection.Up:
animator.Play("player_attack_back");
break;
case (int) MovingDirection.Down:
animator.Play("player_attack_front");
break;
case (int) MovingDirection.Left:
animator.Play("player_attack_right");
spriteRenderer.flipX = true;
break;
case (int) MovingDirection.Right:
animator.Play("player_attack_right");
spriteRenderer.flipX = false;
break;
}
}

You just need to stop moving while the attack animation is playing. To know when the animation is end, one way is using events https://docs.unity3d.com/Manual/AnimationEventsOnImportedClips.html
First you need to add an event near the end of the animation, let's name it OnAttackEnd.
NOTE! To ensure the script can receive the event, the script and the animator must be attached on the same GameObject.
In the script add a boolean field to indicate if attack anim is playing and the OnAttackEnd callback.
bool _isAttackAnimPlaying;
void OnAttackEnd()
=> _isAttackAnimPlaying = false;
In OnFire method, set the boolean field to true, and in FixedUpdate method check the value.
void OnFire()
{
switch (animator.GetInteger(Direction))
{
case (int) MovingDirection.Up:
animator.Play("player_attack_back");
_isAttackAnimPlaying = true;
....
}
}
private void FixedUpdate()
{
if(!_isAttackAnimPlaying)
{
.....
}
}

Related

Unity C# Getting info from Switch cases and using it in unity

Im making a game for school and Im trying to get this data from the switch:
public void planets()
{
switch(PlanetType)
{
case planetType.Desert:
Debug.Log("Desert");
maxFuel = 500;
break;
case planetType.Ruined:
Debug.Log("Ruined");
maxFuel = 1500;
break;
case planetType.Lush:
Debug.Log("Lush");
maxFuel = 3000;
break;
}
}
per scene for that specific planet as the maxFuel in the slider in that scene
I have the enum and that stuff, but for some reason in Unity, it only takes the input max fuel in unity and doesnt want to take the max fuel from the switchcase. Any quick fixes?

Question On Sequential Attacks / Finishing Sequence Early

I'm working on a top down game where when you press the attack button multiple times there's a 3 hit sequence. I have the 3 hit sequence working just fine, but I have two problems I can't seem to find a solution to through my google fu.
When the sequence is finished, I don't have a good way to transition back to the idle state.
When you press the button once or twice, I'm not sure how I can tell whether you haven't pressed the button again in a long enough time to decide the sequence has been cancelled.
(I'm using a state machine I made for the character controller, and the states don't inherit from monobehavior so I don't have access to coroutines in this attack state)
Here is the code, I would really appreciate some feedback and help.
private string currentAttack = "Attack1";
public AttackingState(Character character, StateMachine stateMachine)
: base(character, stateMachine)
{
}
public override void Action()
{
if (Input.GetKeyDown(KeyCode.Space))
{
switch (currentAttack)
{
case "Attack1":
{
this.character.SetTrigger("Attack2");
this.currentAttack = "Attack2";
break;
}
case "Attack2":
{
this.character.SetTrigger("Attack3");
this.currentAttack = "Attack3";
break;
}
case "Attack3":
{
this.character.SetTrigger("Attack1");
this.currentAttack = "Attack1";
break;
}
default:
break;
}
}
if (Input.GetAxis("Horizontal") != 0 || Input.GetAxis("Vertical") != 0)
{
character.SetTrigger("AttackCancelled");
stateMachine.SetState<WalkingState>(new WalkingState(character, stateMachine));
currentAttack = "Attack1";
}
}
public override void OnStateEnter()
{
base.OnStateEnter();
character.SetTrigger("Attack1");
}
you could track a timestamp since the last press and do something like
// stores the last attack time
private float lastAttackTime;
// whereever you want to assign this value from
// The delay before the attack is reset and starts over from 1
private const float MAX_ATTACK_INTERVAL = 1f;
if (Input.GetKeyDown(KeyCode.Space))
{
// Time.time is the time in seconds since the app was started
if(Time.time - lastAttackTime > MAX_ATTACK_INTERVAL)
{
// start over
// mimics the attack3 since there you start over at 1
currentAttack = "Attack3";
}
lastAttackTime = Time.time;
switch (currentAttack)
{
...
}
}
In general instead of string I would recommend either a simple int or an enum instead.

Unity Control object with a script that is controlled with an attached script

I followed a tutorial on making the Tron game for my project and I want to add multiplayer capability. I've set up a message system where a phone sends a direction change to the computer displaying the game to then make their player change direction. Each player is controlled with an attached 'Move' script that uses references to itself and a set of methods based on key inputs. I'm trying to get my game manager to receive these messages from the phone and cause the players to switch direction - I've tried making the move script have static elements but that seems to break it.
Is there a good way of doing this or would it be preferable to write a new move script that controls all players simultaneously that is just attached elsewhere?
The current code is as follows:
switch (direction)
{
case "N":
Instance.player1.GetComponent<Rigidbody2D>().velocity = Vector2.up * Move.speed;
break;
case "E":
Instance.player1.GetComponent<Rigidbody2D>().velocity = Vector2.right * Move.speed;
break;
case "S":
Instance.player1.GetComponent<Rigidbody2D>().velocity = Vector2.down * Move.speed;
break;
case "W":
Instance.player1.GetComponent<Rigidbody2D>().velocity = Vector2.left * Move.speed;
break;
Move is a reference to the script attached to each game, but for the script to function, a call to this method is required after each direction change:
public void spawnWall()
{
lastWallEnd = transform.position;
GameObject objectOfGame = (GameObject)Instantiate(wallPrefab, transform.position, Quaternion.identity);
wall = objectOfGame.GetComponent<Collider2D>();
}
I tried to make it static but it wasn't working so I'm guessing that's not the route to take. SpawnWall is called every time an internal direction change is made.
void createConstantWall(Collider2D collision, Vector2 start, Vector2 finish)
{
collision.transform.position = start + (finish - start) * 0.5f;
float distance = Vector2.Distance(start, finish);
if (start.x != finish.x)
collision.transform.localScale = new Vector2(distance + 1, 1);
else
collision.transform.localScale = new Vector2(1, distance + 1);
}
This function is called in update
Losing method:
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision != wall)
{
//add losing stuff
Destroy(gameObject);
}
}
There are a few ways to handle multiple user inputs from different phones (or other sources). Here is my preferred solution:
All input goes through a generic manager with the player's id whenever movement is sent. This input is converted to an event to simplify dependencies.
Since you have stated you already have a custom message being sent to the game manager with the player number and direction, we will work with that.
Within your GameManager.cs or an InputManager.cs
public static event Action<int, string> OnInputReceived;
void InputReceived(int playerNumber, string direction)
{
OnInputReceived?.Invoke(playerNumber, direction);
}
Within your PlayerMovementController.cs that should be attached to each individual player in the game, you should be tracking your playerId, or be able to check another attached script for the playerId:
public int playerId;
void Awake()
{
GameManager.OnInputReceived += MovePlayer;
}
void MovePlayer(int inputPlayerId, string direction)
{
if(inputPlayerId != playerId)
return;
else
{
//move THIS player in the "direction" using your movement logic
}
}
void OnDestroy()
{
GameManager.OnInputReceived -= MovePlayer;
}
If all of your players have unique ids and have the PlayerMovementController attached, they can now all listen to the inputs but only move when their ID is tied to it. If your GameManager calls InputReceived(1,"E"), all of your PlayerMovementControllers will call MovePlayer(1, "E"), but only the one with playerId == 1 will actually move since all of the others return immediately.
-- Diclaimer: All of this code is freehanded in StackOverflow, so there may be some syntax errors I didn't catch. Please let me know or edit it directly.

Unity Animations

I am trying to change character animations using a script, based on key inputs, but Unity seems to only play the default "standing idle" animation and occasionally switching to the "crouched idle", is there a different way to handle animations or am I just doing the script wrong? Here is my script as it currently stands
using UnityEngine;
using System.Collections;
public class CharacterControl : MonoBehaviour {
private Animator animator;
public bool crouched;
private string sc;
// Use this for initialization
void Start () {
animator = GetComponent<Animator> ();
}
// Update is called once per frame
void Update () {
if (crouched == true) {
sc = "crouch";
} else {
sc = "standing";
}
animator.Play (sc + "_idle");
if (Input.GetButton ("Fire3")) {
if (crouched == false) {
crouched = true;
} else {
crouched = false;
}
}
}
}
Try replacing
if (Input.GetButton ("Fire3")) {
if (crouched == false) {
crouched = true;
} else {
crouched = false;
}
}
with
if (Input.GetButton ("Fire3")) {
crouched = true;
} else {
crouched = false;
}
Now, when you hold down the "Fire3" button, your character should crouch, and when you release it he/she should stand again
Also a suggestion: Put the other code in the function (if (crouched == true) ... animator.Play (sc + "idle"); after this code (the Input.GetButton check). That way, your character should instantly start crouching the same frame that the button is pressed; otherwise, he/she will the frame after
Explanation
Input.GetButton will return true while you're pressing down (in the middle of clicking or touching) on the button each frame. Each time Update is called (around 1/60th of a second) your code will check if you're pressing down, and toggle crouched. When you click/tap the button, you'll likely be pressing down for a few frames, so crouched will switch from true to false, back and forth, a few times. In some cases (when you press down for an odd number of frames) crouched will be switched, but in other cases (when you press down for an even number of frames) crouched will stay as it was before you clicked on the button, preventing your character from crouching, or standing if he was crouching before.
Source: From the official API: Input.GetButton
I would strongly suggest using animation states and transit from one to another when you get an input. Checkout my answer at: Unity 2D animation running partially
Yes you can do the toggle action like this
void Update()
{
if(Input.MouseButtonDown(2))
{
crouched = true;
}
if(Input.MouseButtonUp(2))
{
crouched = false;
}
}
You can try this code:
crouched = Input.GetButtonDown("Fire3") ? true : false;

How to make cursor dissapear after inactivity in Unity

My goal is simple, like a lot of applications, I want to make my cursor invisible after a certain time of inactivity of my user.
My solution doesn't seem really optimized, here's the algorithm :
void Start {
Timer t = new Timer(5000);//Create a timer of 5 second
t.start();
}
void Update {
if(MouseMoved){
t.reset();
}
timeLeft = t.deltaTime;
if ( timeLeft == 5000 )
{
Cursor.visible = false;
}
}
I really don't like to check at every frame if the mouse is moved, I'd prefer that my mouse moved trigger something, but I'm lost here, is anyone have a better solution ?
This should be the good way to achieve the task, using coroutine, without any memory issue:
private Coroutine co_HideCursor;
void Update()
{
if (Input.GetAxis("Mouse X") == 0 && (Input.GetAxis("Mouse Y") == 0))
{
if (co_HideCursor == null)
{
co_HideCursor = StartCoroutine(HideCursor());
}
}
else
{
if (co_HideCursor != null)
{
StopCoroutine(co_HideCursor);
co_HideCursor = null;
Cursor.visible = true;
}
}
}
private IEnumerator HideCursor()
{
yield return new WaitForSeconds(3);
Cursor.visible = false;
}
There are two ways to check for things like this outside of the update loop, that I know of:
1 - Custom Events.
Writing your own custom event would allow you to call something like "OnMouseMove()" outside of the update function and it would only execute when the mouse cursor's position changes.
2 - Coroutines
Creating a separate coroutine would allow you to perform this check less frequently. To do this, you make an IEnumerator and put your mouse movement logic in there. Something like:
IEnumerator MouseMovement()
{
while(true)
{
if(MouseMoved)
{
//Do stuff
}
yield return new WaitForSeconds(1f);
}
}
That coroutine would perform the check once every second while it is running. You would start the coroutine by saying:
StartCoroutine(MouseMovement());
And to stop it, you call
StopCoroutine(MouseMovement());
If you Start it when the timer reaches 0 and stop it when the cursor is moved, you can also prevent the coroutine from running all the time, only using it while the cursor is inactive.

Categories

Resources