How to use Tiles in Unity 5?

Asked

Viewed 513 times

0

I’m in big trouble. I’m doing a role-playing game and using tilesets with the 32x32-dimensional Tiles for all of the game’s graphics, but Unity doesn’t support tilemaps. I’ve looked for plugins and "gambiarras" but none worked, only one seems to work and guess... is paid!

I need my player to move ALWAYS 1 in 1 as well as in the RPG maker MV. I would make the entire game on the RMMV but I like freedom to program and not keep clicking buttons to create events. That’s why I went to Unity. The RMMV documentation is horrible and javascript is very confusing at times.

How do I get around the problem? Is there any simple way to solve?

Here is my code:

using UnityEngine;
using System.Collections;

public enum PlayerAnimatioState {
    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 Animator animator;
    private Vector3 directionUp;
    private Vector3 directionDown;
    private Vector3 directionLeft;
    private Vector3 directionRight;

    public PlayerAnimatioState initialState;
    public PlayerAnimatioState animState;
    public float velocity;


    // Use this for initialization
    void Start () {
        animator = GetComponent<Animator> ();
        initialState = PlayerAnimatioState.IDLE_UP;
        animState = initialState;
        animator.SetInteger ("PlayerAnimatorState", (int)animState);
        velocity = 1.5f;
        directionUp = new Vector3 (0, 1);
        directionDown = new Vector3 (0, -1);
    }

    // Update is called once per frame
    void Update () {
        if (Input.GetKeyDown (KeyCode.W)) {
            animState = PlayerAnimatioState.WALK_UP;
        } else if (Input.GetKeyDown (KeyCode.A)) {
            animState = PlayerAnimatioState.WALK_LEFT;
        } else if (Input.GetKeyDown (KeyCode.S)) {
            animState = PlayerAnimatioState.WALK_DOWN;
        } else if (Input.GetKeyDown (KeyCode.D)) {
            animState = PlayerAnimatioState.WALK_RIGHT;
        }
        else if (Input.GetKeyUp (KeyCode.W)) {
            animState = PlayerAnimatioState.IDLE_UP;
        } if (Input.GetKeyUp (KeyCode.A)) {
            animState = PlayerAnimatioState.IDLE_LEFT;
        } if (Input.GetKeyUp (KeyCode.S)) {
            animState = PlayerAnimatioState.IDLE_DOWN;
        } if (Input.GetKeyUp (KeyCode.D)) {
            animState = PlayerAnimatioState.IDLE_RIGHT;
        }
        animator.SetInteger ("PlayerAnimatorState", (int)animState);
        Move ();
    }

    void Move() {
        Vector3 currentPosition = transform.position;
        switch (animState) {
        case PlayerAnimatioState.WALK_UP:
            currentPosition.y += velocity * Time.deltaTime;
            break;
        case PlayerAnimatioState.WALK_DOWN:
            currentPosition.y -= velocity * Time.deltaTime;
            break;
        case PlayerAnimatioState.WALK_LEFT:
            currentPosition.x -= velocity * Time.deltaTime;
            break;
        case PlayerAnimatioState.WALK_RIGHT:
            currentPosition.x += velocity * Time.deltaTime;
            break;
        }
        transform.position = currentPosition;
    }

}

That way the player does not walk a fixed amount, the player who chooses when to stop. I wanted a movement scheme similar to GBA’s Pokémon Ruby.


EDITED

Victor’s response was extremely helpful. I already have 90% of what I need, but I’m still not completely satisfied with it. Suppose the player is standing looking up and I press A (left). If I do this he will move to the left, I wanted him to just turn to the left. The same logic for the other options. I deleted some things from your code because they’re unnecessary for my project, but I saved them for other futures.

Obs: the enum that he created has the same function as options 5, 6, 7 and 8 of mine.

Here is my code edited after incorporating the ideas of his reply:

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 PlayerMovementBehaviour : MonoBehaviour {
    public Vector3 destino;
    private Animator animator;
    public PlayerAnimationState animState;
    public float velocity;
    public int tileWidth;
    public int tileHeight;

    void Start() {
        animState = PlayerAnimationState.IDLE_UP;
        destino = transform.position;
        velocity = 3f;
        tileWidth = tileHeight = 1;
        animator = GetComponent&lt;Animator&gt; ();
    }

    void FixedUpdate() {
        animator.SetInteger (&quot;PlayerAnimatorState&quot;, (int)animState);
        EscolherMovimento();
        Move();
    }

    void EscolherMovimento() {
        if (Movendo()) return;

        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;
        }

        if (Input.GetKey(KeyCode.W)) {
            destino.y += tileHeight;
        } else if (Input.GetKey(KeyCode.A)) {
            destino.x -= tileWidth;
        } else if (Input.GetKey(KeyCode.S)) {
            destino.y -= tileHeight;
        } else if (Input.GetKey(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 &lt;= passo.magnitude) {
            transform.position = destino;
        } else {
            transform.position += passo;
        }
        if (passo.x &lt; 0 &amp;&amp; Mathf.Abs(passo.x) &gt; Mathf.Abs(passo.y)) animState = PlayerAnimationState.WALK_LEFT;
        if (passo.x &gt; 0 &amp;&amp; Mathf.Abs(passo.x) &gt; Mathf.Abs(passo.y)) animState = PlayerAnimationState.WALK_RIGHT;
        if (passo.y &lt; 0 &amp;&amp; Mathf.Abs(passo.x) &lt;= Mathf.Abs(passo.y)) animState = PlayerAnimationState.WALK_DOWN;
        if (passo.y &gt; 0 &amp;&amp; Mathf.Abs(passo.x) &lt;= Mathf.Abs(passo.y)) animState = PlayerAnimationState.WALK_UP;
    }

    bool Movendo() {
        return transform.position != destino;
    }

    bool PodeAndar() {
        return true;
    }

}
  • Without looking at your code, it is difficult to help you. Anyway, change the transform.position of your player would not solve?

  • 1

    Edited question

  • 1

    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.

  • I wonder the same thing.

  • 1

    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].

  • 3

    @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.

  • @moustache Well observed. I’ll be more careful next time.

Show 2 more comments

1 answer

3


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 (MonoBehaviours) 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 ifs 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.

  • I answered my own question because I needed to put more code and if I edited my question it would get too big. Please check it out. Thank you so much for your help!

  • @Michaelpacheco Ok, I’m already thinking about the solution.

  • Not yours if you saw but I switched the GetKeyDown for GetKey to accept a continuous movement. I think this influences logic.

  • @Michaelpacheco Revised solution.

  • In your new logic I would need to release the key to continue the movement. If the player is standing looking up and I press down (S) and hold it, it should walk down, and not just look down and stop waiting for me to drop the S and then squeeze it again for it to walk. But thanks for trying to help.

  • @Michaelpacheco Holding the S for more than a second didn’t work?

  • No, logic is making a mistake. If you start face up and hold the A (left) it turns left, wait a second and walk left. If you stop and hold it right, it turns right and after a second walks left kkkk.

  • @Michaelpacheco At some point you pressed two keys simultaneously?

  • No, not at any time.

  • @Michaelpacheco I think it’s because he wasn’t checking to see if the key had been released while he was walking, just when he had stopped. I edited the answer once more.

  • The error remains. I think this response time is not a good solution. The player will definitely get angry.

  • @Michaelpacheco If the player has to press the key twice to start walking, you think he would be angry?

  • 1

    @Michaelpacheco I found, it was typo. I had wrong a sign. I put a - on the right instead of +.

  • I worked out an elegant solution to the problem. To not mess up the post I put the code in Pastebin. I thought you would want to see the solution. Again thank you so much for your help. http://pastebin.com/LCkJ9r01

Show 9 more comments

Browser other questions tagged

You are not signed in. Login or sign up in order to post.