Welcome, Guest. Please login or register. Did you miss your activation email?

Author Topic: Tilemap cell ID  (Read 246 times)

0 Members and 1 Guest are viewing this topic.

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Tilemap cell ID
« on: April 05, 2021, 02:29:46 pm »
Hi guys!
Apologies for the noob question :)
I'm trying to figure out the way I could map my Tilemap's vertex arrays elements with IDs of the tiles, so I could access and address any individual tile from outside - for example, get tile coordinates after clicking on it.
I came up with the idea of creating a separate vector of rectangles generated from tilemap for loop but it seems very inefficient and unnecessary. There must be an easier way right? :)

Stauricus

  • Sr. Member
  • ****
  • Posts: 260
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #1 on: April 05, 2021, 04:45:51 pm »
no need for a separate vector. also you don't need adresses if, for example, all sand tiles are going to be the same and do the same thing.
if you have a tilemap like:

tile_map = [[0, 0, 0, 1, 0,],
[0, 0, 0, 1, 0,],
[2, 2, 0, 1, 0,],
[2, 2, 1, 1, 0,],
[0, 0, 0, 1, 0,]];

and each tile is 32x32 pixels, all you have to do to get a particular tile ID is divide mouse position by tile size. after mouse clicking, it would be something like that:
//Inside your events loop
sf::Vector2i tile_size(32, 32);
if (event.type == sf::Event::MouseButtonReleased && event.mouseButton.button == sf::Mouse::Left){
    int tile_id = tile_map[sf::Mouse::getPosition().x/tile_size.x][sf::Mouse::getPosition().y/tile_size.y];
}


EDIT: i was tempted to make a full working example :D
#include <iostream>
#include <SFML/Graphics.hpp>

int main(){
    std::vector<int> tile_map = {0, 2, 3, 2, 1,
                                 0, 2, 2, 2, 0,
                                 0, 2, 0, 2, 0,
                                 0, 0, 1, 1, 1,
                                 0, 2, 1, 1, 0};
    sf::Vector2i map_size(5, 5); //Number of rows and columns of the map. in this example must match the vector above, since I didn't add any checker methods to ensure everything fits well.
    sf::Vector2i tile_size(32, 32); //Tile size in pixels
    sf::Vector2i tex_tile_size(32, 32); //Original tile size in the texture file, in pixels
    sf::VertexArray vertices(sf::Quads, map_size.x*map_size.y*4); //4 vertices for each tile
    sf::Texture tileset;
    tileset.loadFromFile("tileset.png");

    sf::RenderWindow window(sf::VideoMode(map_size.x*tile_size.x, map_size.y*tile_size.y), "SFML Tilemap");

    for (int y=0; y<map_size.y; y++){
        for (int x=0; x<map_size.x; x++){
            const int n = y*map_size.x+x;
            sf::Vertex* quad = &vertices[n*4];
            quad[0].position = sf::Vector2f(x*tile_size.x, y*tile_size.y);
            quad[1].position = sf::Vector2f((x+1)*tile_size.x, y*tile_size.y);
            quad[2].position = sf::Vector2f((x+1)*tile_size.x, (y+1)*tile_size.y);
            quad[3].position = sf::Vector2f(x*tile_size.x, (y+1)*tile_size.y);

            const int tile_id = tile_map[n];
            const int tx = tile_id % (tileset.getSize().x / tex_tile_size.x);
            const int ty = tile_id / (tileset.getSize().x / tex_tile_size.x);

            quad[0].texCoords = sf::Vector2f(tx*tex_tile_size.x, ty*tex_tile_size.y);
            quad[1].texCoords = sf::Vector2f((tx+1)*tex_tile_size.x, ty*tex_tile_size.y);
            quad[2].texCoords = sf::Vector2f((tx+1)*tex_tile_size.x, (ty+1)*tex_tile_size.y);
            quad[3].texCoords = sf::Vector2f(tx*tex_tile_size.x, (ty+1)*tex_tile_size.y);
        }
    }

    while (window.isOpen()){
        sf::Event event;
        while (window.pollEvent(event)){
            if (event.type == sf::Event::Closed){
                window.close();
            }
            if (event.type == sf::Event::MouseButtonReleased && event.mouseButton.button == sf::Mouse::Left){
                sf::Vector2i current_tile(sf::Mouse::getPosition(window).x/tile_size.x, sf::Mouse::getPosition(window).y/tile_size.y);
                int tile_id = tile_map[current_tile.y*map_size.x+current_tile.x];
                std::cout << "You clicked tile [" << current_tile.x << "][" << current_tile.y << "]. ID of this tile is " << tile_id << std::endl;
            }
        }

        window.clear(sf::Color::White);
        window.draw(vertices, &tileset);
        window.display();
        sf::sleep(sf::milliseconds(10));
    }
    return 0;
}
 



have fun!
« Last Edit: April 05, 2021, 08:46:42 pm by Stauricus »

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #2 on: April 05, 2021, 07:16:01 pm »
That's cool! But I don't have flat 32x32 tiles - I have a height attribute (with varying y points). So I need to take those quad transformations into account.

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #3 on: April 05, 2021, 08:45:32 pm »
so I made std::vector<sf::ConvexShape> tileShape;
in the tilemap for loop where setting quad positions I'm copying those quads and saving them into ConvexShape.
So I basically copying the entire map into the convex shapes... which in my view defeating the whole purpose of VertexArray tilemap, isn't it?
Or is it OK? What do you guys think?
Is there another lightweight way? :)

Stauricus

  • Sr. Member
  • ****
  • Posts: 260
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #4 on: April 05, 2021, 09:01:38 pm »
if I understand correctly, you don't need convexShapes. just adjust the sf::VertexArray accordingly to each height. maybe something like:

quad[0].position = sf::Vector2f(x*tile_size.x, y*tile_size.y+height_increase);

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #5 on: April 05, 2021, 10:02:49 pm »
Yes, I have that at quad positions.

But I don't see this working because tile_size.y might not be 32...
sf::Vector2i current_tile(sf::Mouse::getPosition(window).x/tile_size.x, sf::Mouse::getPosition(window).y/tile_size.y);

Stauricus

  • Sr. Member
  • ****
  • Posts: 260
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #6 on: April 05, 2021, 10:29:43 pm »
ops, you're right. well, if each tile is going to have a different shape, you'll need to calculate every tile shape based on their heights.
what you can do is check if the nearby tiles are overlapping the tile your mouse is currently in, based on their heights.

but if you are planning the case of 2 tiles with the same height having different shapes, then its probably better to identify they by using shaders, as we did in this topic.

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #7 on: April 06, 2021, 01:51:40 pm »
Well, that's what I mean - how would I check the tile height if I don't have anything to identify this tile with?
When I create convex shapes - I can use getGlobalBounds... what can I use with vertices?

Stauricus

  • Sr. Member
  • ****
  • Posts: 260
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #8 on: April 06, 2021, 03:25:59 pm »
getGlobalBounds will return a minimal bouding box rectangle that contains the shape. It won't give you precise bounds for checking collision.
https://www.sfml-dev.org/documentation/2.5.1/classsf_1_1ConvexShape.php#ac0e29425d908d5442060cc44790fe4da

you could create an array of a class Tile with height atribute

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #9 on: April 08, 2021, 01:59:14 am »
Yeah well, I'm probably pretty thick but I just can't figure out the way of making this statement work if the tile isn't rectangle...

sf::Vector2i current_tile(sf::Mouse::getPosition(window).x/tile_size.x, sf::Mouse::getPosition(window).y/tile_size.y);

yes I got a std::vector<std::vector<int>> m_heightmap;
which holds height variables for every quad on the map and is shared with all 4 tiles surrounding it.
but still can't see a way of calculating the shape of a tile from that...

Paul

  • Jr. Member
  • **
  • Posts: 70
    • View Profile
Re: Tilemap cell ID
« Reply #10 on: April 09, 2021, 11:57:10 pm »
Use point in polygon calculation in all cases (you can find various implementations). Actually you must have all 4 coordinates already since you are able to draw them. Based on your previous code, you know x, y, tile_width and tile_height of each tile. Let's say tile in base is rectangle with size 64 x 64 pix. Plus you have y offset from m_heightmap.

In the for loop will be:
int current_tile = -1; // Not valid
int id = 0;

top_left.x = tile.x;
top_left.y = tile.y + m_heightmap[some_index];

top_right.x = tile.x + tile_width;
top_right.y = tile.y + m_heightmap[some_index];

bottom_right.x = tile.x + tile_width;
bottom_right.y = tile.y + tile_height + m_heightmap[some_index];

bottom_left.x = tile.x;
bottom_left.y = tile.y + tile_height + m_heightmap[some_index];

if (PointInPolygon(mouse, top_left, top_right, bottom_right, bottom_left)
{
    current_tile = id;
};
id++;
 

There are probably two concepts in the basics
1) Calculate tile coordinates in for loop
2) Pre-generate coordinates when creating a map and store it in tile classes or structs

Second approach is less messy (at least from my experience) and you can use data wherever it is needed.

There is no simple way how to get tile id in 3D or pseudo 3D maps, it leads into various problems. Thats why games have often flat maps or height is rather an illusion like in Transport Tycoon :) For example if height offset will be extend to other tile above, point in polygon can be valid in more than 1 case (>1 tile is selected). There is more ways, you can fill tiles with color and check ID based on that, use some raycast etc.
« Last Edit: April 12, 2021, 07:03:54 pm by Paul »

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #11 on: April 10, 2021, 12:38:59 am »
Thanks, Paul!
I don't quite understand this statement: if (PointInPolygon(mouse, top_left, top_right, bottom_right, bottom_left)

Can you explain please what is PointInPolygon and how do I check if the mouse is within those 4 coordinates?

Paul

  • Jr. Member
  • **
  • Posts: 70
    • View Profile
Re: Tilemap cell ID
« Reply #12 on: April 10, 2021, 08:56:14 am »
It's calculation whether point (mouse) is inside polygon (4 verticles of map tile).

Checking all map tiles vs mouse cursor can be slow with such algorithm, you can get approximate region of nearest tiles first (like sf::Vector2i current_tile(sf::Mouse::getPosition(window).x/tile_size.x, sf::Mouse::getPosition(window).y/tile_size.y); for 2x2 tiles etc.) and check those only.

#include <SFML/Graphics.hpp>
#include <SFML/Window.hpp>
#include <vector>

int PointInPolygon(const sf::Vector2f& p, const std::vector<sf::Vector2f>& v)
{
        int i, j, c = 0;
        for (i = 0, j = v.size() - 1; i < v.size(); j = i++)
        {
                if (((v[i].y > p.y) != (v[j].y > p.y)) &&
                        (p.x < (v[j].x - v[i].x) * (p.y - v[i].y) / (v[j].y - v[i].y) + v[i].x))
                        c = !c;
        }
        return c;
}

int main()
{
        sf::RenderWindow window(sf::VideoMode(800, 600), "SFML works!");
        window.setVerticalSyncEnabled(true);

        std::vector<sf::Vector2f> v{ {25 , 50}, {100 , 50}, {100 , 150}, {45 , 160} };

        sf::ConvexShape polygon;
        polygon.setPointCount(4);
        polygon.setPoint(0, v[0]);
        polygon.setPoint(1, v[1]);
        polygon.setPoint(2, v[2]);
        polygon.setPoint(3, v[3]);
        polygon.setOutlineColor(sf::Color::Red);
        polygon.setOutlineThickness(2);

        while (window.isOpen())
        {
                sf::Event event;
                while (window.pollEvent(event))
                {
                        switch (event.type)
                        {
                        case sf::Event::Closed:
                                window.close();
                                break;

                        default: break;
                        }
                }

                if (PointInPolygon(static_cast<sf::Vector2f>(sf::Mouse::getPosition(window)), v))
                {
                        polygon.setFillColor(sf::Color::Green);
                }
                else
                        polygon.setFillColor(sf::Color::Red);
               
                window.clear();
                window.draw(polygon);
                window.display();
        }

        return 0;
}

frienzy

  • Newbie
  • *
  • Posts: 13
    • View Profile
    • Email
Re: Tilemap cell ID
« Reply #13 on: April 12, 2021, 12:57:13 pm »
Yes Paul!
It works! :) So I'm still copying all tiles from vertices into std::vector<sf::ConvexShape> tileShapes;
Meanwhile I made a function void checkTiles which checks mouse position and calculates region of 3 tiles based on 32x32 tile size. It selects closest tile to mouse and 1 up from it and 1 down from it. I didn't check to left and right because tile sizes on X doesn't change and is always 32. This function saves int variable representing tile ID into std::vector<unsigned int> vec; and it matches the tile sequence of std::vector<sf::ConvexShape> tileShapes;
Then based on what ID's I'm having currently I'm checking for tileShapes[vec] and saving all 4 corner positions into std::vector<sf::Vector2f> v1, v2 and v3. Then I'm running your algorithm on these 3 vectors and checking which v is true in a separate function - unsigned int currentTile(mousePosView). And then in gamestate rendering it like that: this->window->draw(this->map.tileShapes[map.currentTile(mousePosView)]);

Only thing I'm struggling with now is that when mouse position on X axis is right on the edge of 2 tiles it's calling tileShape[0] for some reason :/
Any ideas?

Here is my code:
#pragma once

#include <SFML/Graphics.hpp>

class TileMap : public sf::Drawable, public sf::Transformable
{
public:

    std::vector<sf::ConvexShape> tileShapes;
    std::vector<unsigned int> vec;
    std::vector<sf::Vector2f> v;
    std::vector<sf::Vector2f> v1;
    std::vector<sf::Vector2f> v2;
    std::vector<sf::Vector2f> v3;

    void checkTiles(sf::Vector2f mousePosView, sf::Vector2i tileSize, unsigned int width, unsigned int height)
    {
       
        vec[0] = unsigned int(((int)mousePosView.x / tileSize.x + ((int)mousePosView.y / tileSize.y) * width));
        if((int)mousePosView.y < (height * 32) - 32)
        vec[1] = unsigned int(((int)mousePosView.x / tileSize.x + (((int)mousePosView.y / tileSize.y) + 1) * width));  
        else
            vec[1] = unsigned int(((int)mousePosView.x / tileSize.x + ((int)mousePosView.y / tileSize.y) * width));
        if ((int)mousePosView.y > 32)
            vec[2] = unsigned int(((int)mousePosView.x / tileSize.x + (((int)mousePosView.y / tileSize.y) - 1) * width));
        else
            vec[2] = unsigned int(((int)mousePosView.x / tileSize.x + ((int)mousePosView.y / tileSize.y) * width));

        if ((int)mousePosView.y < height * 32 && (int)mousePosView.x < width * 32)
        {
            v1 = { {tileShapes[vec[0]].getPoint(0)}, {tileShapes[vec[0]].getPoint(1)}, {tileShapes[vec[0]].getPoint(2)}, {tileShapes[vec[0]].getPoint(3)} };
            v3 = { {tileShapes[vec[2]].getPoint(0)}, {tileShapes[vec[2]].getPoint(1)}, {tileShapes[vec[2]].getPoint(2)}, {tileShapes[vec[2]].getPoint(3)} };
            v2 = { {tileShapes[vec[1]].getPoint(0)}, {tileShapes[vec[1]].getPoint(1)}, {tileShapes[vec[1]].getPoint(2)}, {tileShapes[vec[1]].getPoint(3)} };
        }      
    }

    int PointInPolygon(const sf::Vector2f& p, const std::vector<sf::Vector2f>& v)
    {
        int i, j, c = 0;
        for (i = 0, j = v.size() - 1; i < v.size(); j = i++)
        {
            if (((v[i].y > p.y) != (v[j].y > p.y)) &&
                (p.x < (v[j].x - v[i].x) * (p.y - v[i].y) / (v[j].y - v[i].y) + v[i].x))
                c = !c;
        }
        return c;
    }

    unsigned int currentTile(sf::Vector2f mousePosView)
    {

        if (PointInPolygon(static_cast<sf::Vector2f>(mousePosView), v1))
        {
            return vec[0];
        }
        else if (PointInPolygon(static_cast<sf::Vector2f>(mousePosView), v2))
        {
            return vec[1];
        }
        else if (PointInPolygon(static_cast<sf::Vector2f>(mousePosView), v3))
        {
            return vec[2];
        }
    }
   
    bool load(const std::string& tileset, sf::Vector2i tileSize, const int* tiles, unsigned int width, unsigned int height, std::vector<std::vector<int>> m_heightmap, sf::Color gridColor)
    {
        // load the tileset texture
        if (!m_tileset.loadFromFile(tileset))
            return false;
       
        // resize the vertex array to fit the level size
        m_vertices.setPrimitiveType(sf::Quads);
        m_vertices.resize(width * height * 4);
        m_gridX.setPrimitiveType(sf::Lines);
        m_gridX.resize(width * height * 4);
        m_gridY.setPrimitiveType(sf::Lines);
        m_gridY.resize(width * height * 4);      
        tileShapes.resize(width * height);
        v.resize(4);
        vec.resize(3);

        for (auto i = 0; i < tileShapes.size(); i++) {
            tileShapes[i].setPointCount(4);
            tileShapes[i].setOrigin(0, 0);
        }
                   
        // populate the vertex array, with one quad per tile
        for (unsigned int x = 0; x < width; ++x)
            for (unsigned int y = 0; y < height; ++y)
            {
                // get the current tile number
                int tileNumber = tiles[x + y * width];
                                       
                // find its position in the tileset texture
                int tu = tileNumber % (m_tileset.getSize().x / tileSize.x);
                int tv = tileNumber / (m_tileset.getSize().x / tileSize.x);

                // get a pointer to the current tile's quad
                sf::Vertex* quad = &m_vertices[(x + y * width) * 4];
                sf::Vertex* gridX = &m_gridX[(x + y * width) * 4];
                sf::Vertex* gridY = &m_gridY[(x + y * width) * 4];      
                           
                // define its 4 corners
                quad[0].position = sf::Vector2f(x * tileSize.x, y * tileSize.y + m_heightmap[x][y]);
                quad[1].position = sf::Vector2f((x + 1) * tileSize.x, y * tileSize.y + m_heightmap[x + 1][y]);
                quad[2].position = sf::Vector2f((x + 1) * tileSize.x, (y + 1) * tileSize.y + m_heightmap[x + 1][y + 1]);
                quad[3].position = sf::Vector2f(x * tileSize.x, (y + 1) * tileSize.y + m_heightmap[x][y + 1]);

                tileShapes[x + y * width].setPoint(0, quad[0].position);
                tileShapes[x + y * width].setPoint(1, quad[1].position);
                tileShapes[x + y * width].setPoint(2, quad[2].position);
                tileShapes[x + y * width].setPoint(3, quad[3].position);
                       
                gridX[0].position = quad[0].position;
                gridX[1].position = quad[1].position;
                gridX[2].position = quad[3].position;
                gridX[3].position = quad[2].position;

                gridY[0].position = quad[0].position;
                gridY[1].position = quad[3].position;
                gridY[2].position = quad[1].position;
                gridY[3].position = quad[2].position;
                             
                gridX[0].color = gridColor;
                gridX[1].color = gridColor;
                gridX[2].color = gridColor;
                gridX[3].color = gridColor;

                gridY[0].color = gridColor;
                gridY[1].color = gridColor;
                gridY[2].color = gridColor;
                gridY[3].color = gridColor;
               
                // define its 4 texture coordinates
                quad[0].texCoords = sf::Vector2f(tu * tileSize.x, tv * tileSize.y);
                quad[1].texCoords = sf::Vector2f((tu + 1) * tileSize.x, tv * tileSize.y);
                quad[2].texCoords = sf::Vector2f((tu + 1) * tileSize.x, (tv + 1) * tileSize.y);
                quad[3].texCoords = sf::Vector2f(tu * tileSize.x, (tv + 1) * tileSize.y);                                  
            }
       
        return true;      
    }

private:

    virtual void draw(sf::RenderTarget& target, sf::RenderStates states) const
    {
        // apply the transform
        states.transform *= getTransform();

        // apply the tileset texture
        states.texture = &m_tileset;

        // draw the vertex array
        target.draw(m_vertices, states);
        target.draw(m_gridX);
        target.draw(m_gridY);
    }

    sf::VertexArray m_vertices;
    sf::VertexArray m_gridX;
    sf::VertexArray m_gridY;
    sf::Texture m_tileset;
   
};

Paul

  • Jr. Member
  • **
  • Posts: 70
    • View Profile
Re: Tilemap cell ID
« Reply #14 on: April 13, 2021, 12:37:00 pm »
Good :) Map and terrain implementation in 2D is interesting topic but it's rather outside of SFML related problems to deal with all issues in code. In addition, specific advice can be misleading because no one knows the project.

It would be more interesting to address various techniques that can be used in general (SFML, graphical API). Like terrain texture blending, another options how deal with height, 2D sprites vs 3D terrain, layer rendering, overlapping (depth), use of shaders..