I think the solution is for you to maintain a state that says whether it is aligned with the Tiles grid or not. Make the character move until he’s aligned, and don’t let anything stop him until it happens. As soon as he lines up, stop the movement.
I think that would be it:
using UnityEngine;
using System.Collections;
public enum PlayerAnimationState {
WALK_UP = 1,
WALK_RIGHT = 2,
WALK_DOWN = 3,
WALK_LEFT = 4,
IDLE_UP = 5,
IDLE_RIGHT = 6,
IDLE_DOWN = 7,
IDLE_LEFT = 8,
}
public enum PlayerDirection {
UP = 1,
RIGHT = 2,
DOWN = 3,
LEFT = 4,
}
public class PlayerBehaviour : MonoBehaviour {
private Animator animator;
private Vector3 destino;
public PlayerDirection direction;
public float velocity;
public int tileWidth;
public int tileHeight;
// Use this for initialization
void Start() {
animator = GetComponent<Animator>();
direction = PlayerDirection.UP;
destino = transform.position;
animator.SetInteger("PlayerAnimatorState", ((int) direction) + 4);
velocity = 1.5f;
}
// Update is called once per frame
void FixedUpdate() {
EscolherMovimento();
Move();
animator.SetInteger("PlayerAnimatorState", ((int) direction) + (Movendo() ? 0 : 4));
}
void EscolherMovimento() {
if (Movendo()) return;
//destino = transform.position; // Caso você tenha certeza de que sempre já estará alinhado.
destino = Alinhar(transform.position); // Caso possa estar desalinhado.
if (Input.GetKeyDown(KeyCode.W)) {
destino.y += tileHeight;
} else if (Input.GetKeyDown(KeyCode.A)) {
destino.x -= tileWidth;
} else if (Input.GetKeyDown(KeyCode.S)) {
destino.y -= tileHeight;
} else if (Input.GetKeyDown(KeyCode.D)) {
destino.x += tileWidth;
}
}
void Move() {
if (!PodeAndar()) return;
Vector3 paraPercorrer = destino - transform.position;
Vector3 passo = paraPercorrer.normalized * velocity * Time.deltaTime;
if (paraPercorrer.magnitude <= passo.magnitude) {
transform.position = destino;
} else {
transform.position += passo;
}
if (passo.x < 0 && Mathf.abs(passo.x) > Mathf.abs(passo.y)) direction = PlayerDirection.LEFT;
if (passo.x > 0 && Mathf.abs(passo.x) > Mathf.abs(passo.y)) direction = PlayerDirection.RIGHT;
if (passo.y < 0 && Mathf.abs(passo.x) <= Mathf.abs(passo.y)) direction = PlayerDirection.UP;
if (passo.y > 0 && Mathf.abs(passo.x) <= Mathf.abs(passo.y)) direction = PlayerDirection.DOWN;
}
bool Movendo() {
return transform.position == destino;
}
bool PodeAndar() {
return true;
}
private int TileNumber(float pos, int tileSize) {
return (int) Mathf.Floor(pos / tileSize);
}
private float TileOffset(float pos, int tileSize) {
// Forma otimizada de pos >= 0 ? pos % tileSize : tileSize - (-pos % tileSize)
return ((pos % tileSize) + tileSize) % tileSize;
}
private Vector3 Alinhar(Vector3 alinhando) {
return new Vector3(TileNumber(alinhando.x, tileWidth) * tileWidth, TileNumber(alinhando.y, tileHeight) * tileHeight);
}
}
First, I introduced the variables tileWidth
and tileHeight
. The names are self-explanatory.
I also changed the method Update
for FixedUpdate
, which is most suitable for updating the physics and logic of the game, by being called at regular time intervals.
I also introduced the method Movendo()
to know if it is moving or not and separated the reading logic from the keyboard in the method EscolherMovimento()
.
The character has a destination point to which he will walk whenever he is not there (this variable is called destino
). Note that this destination point will only be changed in the method EscolherMovimento()
when a KeyDown
is received, and that events KeyDown
will only be accepted if it is stopped (i.e., under the destination point). This way, as long as it is moving, it will keep moving until it is finished, ignoring any other KeyDown
(it will only read the state of the keys when it is stopped). This new version does not care about the KeyUp
. Note that this means that the destination will only be set when it starts to move, not when it is already moving and new keyboard events can only be processed when this destination point is reached and the character stops.
The method Move()
(that I changed a lot) is responsible for making the character walk to the destination, making him take a step towards the destination. The first if
serves to ensure that it does not exceed the destination position at any stage and also serves to stabilize its position by cancelling the error margin of the calculations with float
that could make him wobble within micrometers of his destination position without ever reaching and stopping. If the character is already stopped over its destination point, the method also works (but will not make any movement).
The method Move()
also works if the character for some reason has to move diagonally or move in three dimensions, or is chasing a moving target (in case the target position is being changed frequently). If the destination position is changed by means other than the method EscolherMovimento()
, the method Move()
should continue working without any kind of problem. Also, the speed at which it moves is the same in any direction.
The method EscolherMovimento()
assumes that the initial position may be misaligned from the Tiles grid when deciding which is the destination position. If the starting position is always aligned, you can change the way the variable destino
is calculated as shown in the commented code (but if left as is, it will continue to work the same). The target position will always be defined by this method as an aligned position. Note that the concept of grid/Tile is only important here in the method EscolherMovimento()
, which means the method Move()
works independently of the existence of Tiles/grids, and therefore the method Move()
can be used both for objects that have to be aligned to the grid of Tiles and for those that do not need to be.
The method Move()
now also takes care to decide which side the character is looking at. Or to be more exact, which side he has just taken a step to. That direction is the direction
, for which I also believed the enum
PlayerDirection
. Note that I am no longer using the PlayerAnimationState
(that you had incorrectly typed as PlayerAnimatioState
), but I still left it in the code because I believe you must be using it somewhere else. The value of the PlayerAnimationState
can be reconstructed by obtaining the value of PlayerDirection
and add up to 4 if the character is stationary. This process is what I used to put the same values in the PlayerAnimatorState
that you had been putting.
The method TileNumber
is the number of the Tile in which the character is, where the first parameter is the position of the character in some dimension and the second is the size of the Tile in this dimension. The method TileOffset
(which I am not using, but which you may need) calculates how much the character is dislocated or misaligned from the Ile in which it is located.
The calculation of position and displacement/misalignment is based on the lower left corner of a Tile. If you need to use the Tile center as a base, then you would need to make a change to the methods TileNumber
and TileOffset
:
private int TileNumber(float pos, int tileSize) {
return (int) Mathf.Floor((pos + tileSize / 2) / tileSize);
}
private float TileOffset(float pos, int tileSize) {
return (((pos + tileSize / 2) % tileSize) + tileSize) % tileSize - tileSize / 2;
}
There is an important side effect to be mentioned: If you change the value of transform.position
of a character without also changing the destino
, it will immediately begin to walk to the destination. For this, there is the method PodeAndar()
. The logic of this method always returns true
, which means the character will always try to walk towards his destiny. Replace this logic with something else and you can control when the character can walk even if not in your destination or not.
Note that much of this code is reusable. To implement several Npcs, simply change the operation of the methods EscolherMovimento()
and PodeAndar()
. The other methods are the same. With a little (just a little) work, you can separate them into components (MonoBehaviour
s) different.
Finally, one last warning: I didn’t test this, so I don’t know if I messed up some silly little detail. But if that’s not what’s up here, then that’s almost it.
EDITED
After the initial solution was posted, considering the feedback of the author of the question, now it is necessary to look first and then walk, in addition to eliminating what ended up not being necessary, here is my new version:
using UnityEngine;
using System.Collections;
public enum PlayerAnimationState {
WALK_UP = 1,
WALK_RIGHT = 2,
WALK_DOWN = 3,
WALK_LEFT = 4,
IDLE_UP = 5,
IDLE_RIGHT = 6,
IDLE_DOWN = 7,
IDLE_LEFT = 8,
}
public class PlayerBehaviour : MonoBehaviour {
private Vector3 destino;
private Animator animator;
private KeyCode precisaSoltar;
private float horaTeclaPressionada;
public PlayerAnimationState animState;
public float velocity;
public int tileWidth;
public int tileHeight;
void Start() {
precisaSoltar = KeyCode.Space;
horaTeclaPressionada = Time.fixedTime;
animState = PlayerAnimationState.IDLE_UP;
destino = transform.position;
velocity = 3f;
tileWidth = tileHeight = 1;
animator = GetComponent<Animator>();
}
void FixedUpdate() {
EscolherMovimento();
Move();
animator.SetInteger("PlayerAnimatorState", (int) animState);
}
private void Parar() {
switch (animState) {
case PlayerAnimationState.WALK_UP:
animState = PlayerAnimationState.IDLE_UP;
break;
case PlayerAnimationState.WALK_DOWN:
animState = PlayerAnimationState.IDLE_DOWN;
break;
case PlayerAnimationState.WALK_LEFT:
animState = PlayerAnimationState.IDLE_LEFT;
break;
case PlayerAnimationState.WALK_RIGHT:
animState = PlayerAnimationState.IDLE_RIGHT;
break;
}
}
void EscolherMovimento() {
bool m = Movendo();
if (!m) Parar();
if (precisaSoltar != KeyCode.Space && Input.GetKey(precisaSoltar) && Time.fixedtime - horaTeclaPressionada < 1.0f) return;
precisaSoltar = KeyCode.Space;
if (!m) LerTeclado();
}
void LerTeclado() {
if (Input.GetKeyDown(KeyCode.W) && animState != PlayerAnimationState.IDLE_UP) {
animState = PlayerAnimationState.IDLE_UP;
precisaSoltar = KeyCode.W;
} else if (Input.GetKey(KeyCode.W) && animState == PlayerAnimationState.IDLE_UP) {
destino.y += tileHeight;
} else if (Input.GetKeyDown(KeyCode.A) && animState != PlayerAnimationState.IDLE_LEFT) {
animState = PlayerAnimationState.IDLE_LEFT;
precisaSoltar = KeyCode.A;
} else if (Input.GetKey(KeyCode.A) && animState == PlayerAnimationState.IDLE_LEFT) {
destino.x -= tileWidth;
} else if (Input.GetKeyDown(KeyCode.S) && animState != PlayerAnimationState.IDLE_DOWN) {
animState = PlayerAnimationState.IDLE_DOWN;
precisaSoltar = KeyCode.S;
} else if (Input.GetKey(KeyCode.S) && animState == PlayerAnimationState.IDLE_DOWN) {
destino.y -= tileHeight;
} else if (Input.GetKeyDown(KeyCode.D) && animState != PlayerAnimationState.IDLE_RIGHT) {
animState = PlayerAnimationState.IDLE_RIGHT;
precisaSoltar = KeyCode.D;
} else if (Input.GetKey(KeyCode.D) && animState == PlayerAnimationState.IDLE_RIGHT) {
destino.x += tileWidth;
}
if (precisaSoltar != KeyCode.Space) horaTeclaPressionada = Time.fixedTime;
}
void Move() {
if (!PodeAndar()) return;
Vector3 paraPercorrer = destino - transform.position;
Vector3 passo = paraPercorrer.normalized * velocity * Time.deltaTime;
if (paraPercorrer.magnitude <= passo.magnitude) {
transform.position = destino;
} else {
transform.position += passo;
}
if (passo.x < 0 && Mathf.abs(passo.x) > Mathf.abs(passo.y)) direction = PlayerAnimationState.WALK_LEFT;
if (passo.x > 0 && Mathf.abs(passo.x) > Mathf.abs(passo.y)) direction = PlayerAnimationState.WALK_RIGHT;
if (passo.y < 0 && Mathf.abs(passo.x) <= Mathf.abs(passo.y)) direction = PlayerAnimationState.WALK_UP;
if (passo.y > 0 && Mathf.abs(passo.x) <= Mathf.abs(passo.y)) direction = PlayerAnimationState.WALK_DOWN;
}
bool Movendo() {
return transform.position == destino;
}
bool PodeAndar() {
return true;
}
}
The big difference is in if
s that read the input. They have the following form:
if (Input.GetKeyDown(KeyCode.TECLA) && animState != PlayerAnimationState.IDLE_DIRECAO) {
animState = PlayerAnimationState.IDLE_DIRECAO;
precisaSoltar = KeyCode.TECLA;
} else if (Input.GetKey(KeyCode.TECLA) && animState == PlayerAnimationState.IDLE_DIRECAO) {
destino.eixo -= tileMedida;
}
Notice that he now uses the GetKeyDown
and now the GetKey
. The first if
from this block means that if the key has just been pressed and the character is not looking in the corresponding direction, then he will look and mark that for something to happen later, the key pressed will need to be first released. The second if
means that if the key is pressed (no matter how long, possibly continuously) and the character was already looking in the right direction, then he continues.
That alone would not work when turning, because when pressing a key it would turn and start walking immediately. That’s where enter the variables precisaSoltar
and horaTeclaPressionada
. They serve to track which key should be released before you can start walking (it’s the same key used to turn). If the user does not release it within a second, the character should start Nadr anyway. If the user looses and presses again, he will start walking.
That stretch:
if (precisaSoltar != KeyCode.Space && Input.GetKey(precisaSoltar) && Time.fixedtime - horaTeclaPressionada < 1.0f) return;
precisaSoltar = KeyCode.Space;
This checks if the key that should be released has in fact been released or if it has been held down for more than a second. The space key is used to denote the case where no key needs to be released. Already this snippet:
if (precisaSoltar != KeyCode.Space) horaTeclaPressionada = Time.fixedTime;
Serves to record when the key was pressed so that it is possible to count a second from that moment on.
I notice you removed the destino = Alinhar(transform.position);
. This is a valid thing to do and it works, but now you will have to be more careful when controlling the character’s destiny, because you no longer have something that forces the alignment of the destination in the grid of Iles if for some reason it ends up standing in a misaligned position. On the other hand, depending on what you are doing, this may even be inevitable and/or desirable and/or intentional.
There’s one last detail. I think animator.SetInteger("PlayerAnimatorState", (int) animState);
should be at the end of the method FixedUpdate()
, and not at the beginning. Otherwise, it will set a value that will be outdated when the execution of this method is finished.
Without looking at your code, it is difficult to help you. Anyway, change the
transform.position
of your player would not solve?– Victor Stafusa
Edited question
– Michael Pacheco
I am surprised at the three votes against on this question. I personally believe that this is a very good question and did not deserve these negative votes.
– Victor Stafusa
I wonder the same thing.
– Michael Pacheco
Hello Michael. Welcome to SOPT. This site is not a forum. You should not keep editing the question to ask another question so you have the answer to one of your problems. For that, open another question. If you haven’t done it yet, be sure to do the [tour] and read [Ask].
– Luiz Vieira
@Victorstafusa maybe the problem was what Luiz Vieira said, the question ended up being a mess, IE, her idea is good, but the way put was messed up and only a person with a lot of will to answer could do it. There are those who think that this type of question should not be encouraged, not by the content, but by the way it goes against the basic philosophy of the site. So the negative may have been given subjectively and collaterally to its contents.
– Maniero
@moustache Well observed. I’ll be more careful next time.
– Michael Pacheco