Nós já fizemos um vídeo mostrando como renderizar mapas para jogos feitos com C++, no entanto, nesse exemplo usando a TinyXML2, mas há uma alternativa ainda melhor que é a tmxlite.
tmxlite é uma biblioteca C++ leve usada para carregar e ler arquivos de mapas no formato TMX (formato usado pelo editor de mapas Tiled).
.tmx
(como tiles, camadas, objetos etc.).Se você quer carregar mapas .tmx
no seu jogo em C++ (ex: com SFML, SDL, OpenGL), o tmxlite fornece a estrutura para ler os dados, e você cuida da renderização e lógica.
A seguir vamos ver como: Instalar, Compilar e Rodar o tmxlite
no Windows e no Ubuntu(ou qualquer distro GNU/Linux).
Antes de mais nada, você precisará das seguintes dependências instaladas no seu Windows:
Caso não tenha instala, clique no link que você irá direto para um tutorial que lhe mostrará como fazer!
Dependências para Windows:
Fechar e abrir o terminal(PowerShell) após instalações é fundamental para as alterações surgirem efeito!
Agora vamos instalar o tmxlite, você pode rodar os comandos abaixo um de cada vez, ou criar um script PowerShell e rodar:
InstallTmxlite.ps1
git clone https://github.com/fallahn/tmxlite
cd .\tmxlite
meson setup build
meson compile -C build
meson install -C build --destdir "C:\tmxlite"
Copy-Item -Path '.\tmxlite\tmxlite\include' -Destination 'C:\tmxlite' -Recurse
Se você fez o script, basta rodar com o Windows PowerShell:
powershell InstallTmxlite.ps1
ou com o PowerShell:pwsh InstallTmxlite.ps1
.
Após instalado o tmxlite você já pode remover esse repositório que foi clonado!
Como vamos testar com SFML e compilar com o GNU GCC MinGW é importante você possuir a versão do SFML para o GCC:
SFML-2.6.2-windows-gcc-13.1.0-mingw-64-bit.zip)
:C:\SFML-2.5.1
) para C:\SFML-2.5.1-GCC
(coloque um -GCC ao final da pasta para diferenciar da versão VS)Preparando seu projeto:
New-Item -ItemType Directory -Path 'MyProject'
assets/
(que usaremos como exemplo) clicando nesse link: https://terminalroot.com/downloads/assets.zip e descompacte(Extrair aqui).Invoke-WebRequest -Uri "https://terminalroot.com/downloads/assets.zip" -OutFile "assets.zip"
Se seu descompactador criou subpasta, você precisa deixar a pasta assets/
assim(sem nenhuma subpasta dentro):
./assets/
├── box.jpg
├── floor.jpg
└── map.tmx
0 directories, 3 files
Está tudo pronto, quando mostrarmos o exemplo, direi como você compilar os arquivos aí dentro, agora vamos ver o procedimento para o Ubuntu.
O processo é o quase o mesmo, só que basta usar o APT, para instalar as dependências:
sudo apt meson ninja-build build-essential git clang libsfml-dev curl
Lembrando que o SFML precisa ser inferior a versão 3.
Para descobrir a versão do SFML que está no seu sistema, use um dos comando abaixo:
apt list --installed | grep libsfml
grep -R "SFML_VERSION" /usr/include/SFML/
Agora é só clonar, compilar e instalar o tmxlite:
git clone https://github.com/fallahn/tmxlite
cd tmxlite/
meson setup build
meson compile -C build
sudo meson install -C build
Dropping privileges to "$USER" before running ninja...
ninja: Entering directory `./tmxlite/build'
ninja: no work to do.
Installing tmxlite/src/libtmxlite.so to /usr/local/lib/x86_64-linux-gnu
Depois é só copiar os includes(onde você ainda está, pois há outra pasta de nome tmxlite dentro de tmxlite):
sudo cp -r tmxlite/include/tmxlite /usr/local/include/
sudo ldconfig # Opcional
Se quiser ver a versão instalada do seu tmxlite, rode o comando abaixo:
curl \
https://raw.githubusercontent.com/fallahn/tmxlite/refs/heads/master/meson.build \
2>/dev/null | \
grep ' version:'
Também crie seu projeto: mkdir MyProject
, faça download desse assets/
em: https://terminalroot.com/downloads/assets.zip e descompacte(Extrair aqui).. Se seu descompactador criou subpasta, você precisa deixar a pasta assets/
assim(sem nenhuma subpasta dentro):
./assets/
├── box.jpg
├── floor.jpg
└── map.tmx
0 directories, 3 files
E vamos ao tutorial do tmxlite!
Dentro do seu MyProject
(tanto no Windows quando no GNU/Linux), crie um arquivo main.cpp
para preenchermos com o código básico SFML e cole isso dentro:
É um projeto básico SFML, para mais detalhes, faça nosso curso: https://terminalroot.com.br/sfml
#include <SFML/Graphics.hpp>
#include <iostream>
int main(){
sf::RenderWindow window(sf::VideoMode(1280,720), "SFML::Tmxlite");
while(window.isOpen()){
sf::Event event;
while(window.pollEvent(event)){
if(event.type == sf::Event::Closed){
window.close();
}
}
window.clear();
window.display();
}
return EXIT_SUCCESS;
}
Agora vamos incrementá-lo com código para o tmxlite:
#include <tmxlite/Map.hpp>
#include <tmxlite/Layer.hpp>
#include <tmxlite/TileLayer.hpp>
#include <tmxlite/ObjectGroup.hpp>
./assets/map.tmx
Logo depois(abaixo) de
RenderWindow
.
Esse map.tmx
foi feito com Tiled Map
tmx::Map map;
if(!map.load("./assets/map.tmx")){
std::cerr << "Falha ao carregar o mapa TMX.\n";
return -1;
}
Imagens que mostra o processo de criação com o Tiled Map Editor:
Escolhendo as dimensões do mapa.
Criando um mapa simples.
const float map_width = static_cast<float>(map.getTileCount().x * map.getTileSize().x);
const float map_height = static_cast<float>(map.getTileCount().y * map.getTileSize().y);
Layer::Type::Tile
,Layer::Type::Object
,Layer::Type::Image
ouLayer::Type::Group
const auto& layers = map.getLayers();
Sprite
:sf::Texture floor_tex, box_tex;
if(!floor_tex.loadFromFile("./assets/floor.jpg") || !box_tex.loadFromFile("./assets/box.jpg")){
std::cerr << "Falha ao carregar imagens dos tiles.\n";
return -1;
}
sf::Sprite tile_sprite;
Substitua o window.clear()
pelo código abaixo:
window.clear(sf::Color(138, 138, 138));
for (const auto& layer : layers) {
if (layer->getType() == tmx::Layer::Type::Tile) {
auto* tile_layer = dynamic_cast<const tmx::TileLayer*>(layer.get());
const auto& tiles = tile_layer->getTiles();
const auto layer_size = tile_layer->getSize();
const auto tile_size = map.getTileSize();
for (std::size_t y = 0; y < layer_size.y; ++y) {
for (std::size_t x = 0; x < layer_size.x; ++x) {
std::size_t index = x + y * layer_size.x;
std::uint32_t tile_id = tiles[index].ID;
// Tile vazio
if (tile_id == 0){
continue;
}
// Decide qual imagem usar com base no: tile_id
if(tile_id == 1){
tile_sprite.setTexture(box_tex);
}else if(tile_id == 2){
tile_sprite.setTexture(floor_tex);
}else{
continue;
}
tile_sprite.setPosition(static_cast<float>(x * tile_size.x), static_cast<float>(y * tile_size.y));
window.draw(tile_sprite);
}
}
}
}
Ok, o código mais básico tmxlite é esse, versão completa:
main.cpp
#include <SFML/Graphics.hpp>
#include <iostream>
#include <tmxlite/Map.hpp>
#include <tmxlite/Layer.hpp>
#include <tmxlite/TileLayer.hpp>
#include <tmxlite/ObjectGroup.hpp>
int main(){
sf::RenderWindow window(sf::VideoMode(1280,720), "SFML::Tmxlite");
tmx::Map map;
if(!map.load("./assets/map.tmx")){
std::cerr << "Falha ao carregar o mapa TMX.\n";
return -1;
}
const float map_width = static_cast<float>(map.getTileCount().x * map.getTileSize().x);
const float map_height = static_cast<float>(map.getTileCount().y * map.getTileSize().y);
const auto& layers = map.getLayers();
sf::Texture floor_tex, box_tex;
if(!floor_tex.loadFromFile("./assets/floor.jpg") || !box_tex.loadFromFile("./assets/box.jpg")){
std::cerr << "Falha ao carregar imagens dos tiles.\n";
return -1;
}
sf::Sprite tile_sprite;
while(window.isOpen()){
sf::Event event;
while(window.pollEvent(event)){
if(event.type == sf::Event::Closed){
window.close();
}
}
window.clear(sf::Color(138, 138, 138));
for (const auto& layer : layers) {
if (layer->getType() == tmx::Layer::Type::Tile) {
auto* tile_layer = dynamic_cast<const tmx::TileLayer*>(layer.get());
const auto& tiles = tile_layer->getTiles();
const auto layer_size = tile_layer->getSize();
const auto tile_size = map.getTileSize();
for (std::size_t y = 0; y < layer_size.y; ++y) {
for (std::size_t x = 0; x < layer_size.x; ++x) {
std::size_t index = x + y * layer_size.x;
std::uint32_t tile_id = tiles[index].ID;
if (tile_id == 0){
continue;
}
if(tile_id == 1){
tile_sprite.setTexture(box_tex);
}else if(tile_id == 2){
tile_sprite.setTexture(floor_tex);
}else{
continue;
}
tile_sprite.setPosition(static_cast<float>(x * tile_size.x), static_cast<float>(y * tile_size.y));
window.draw(tile_sprite);
}
}
}
}
window.display();
}
return EXIT_SUCCESS;
}
Agora vamos ver como construir e rodar no Windows e no Linux.
Crie um script de nome: build.ps1
(PowerShell) e insira esse conteúdo abaixo:
Esse script copia as
.dll
, linka alib
e inclui a pastainclude
das duas bibliotecas(SFML e tmxlite).
if ($args[0] -eq "--prepare") {
Write-Output "Executando preparação..."
Copy-Item -Path "C:\SFML-2.5.1-GCC\bin\*.dll" -Destination .
Copy-Item -Path "C:\tmxlite\bin\*.dll" -Destination .
g++ .\main.cpp -I C:\SFML-2.5.1-GCC\include\ -L C:\SFML-2.5.1-GCC\lib\ -I C:\tmxlite\include -L C:\tmxlite\lib -lsfml-main -lsfml-graphics -lsfml-system -lsfml-window -ltmxlite
.\a.exe
} else {
if (Test-Path ".\libtmxlite.dll") {
Write-Output "Compilando..."
g++ .\main.cpp -I C:\SFML-2.5.1-GCC\include\ -L C:\SFML-2.5.1-GCC\lib\ -I C:\tmxlite\include -L C:\tmxlite\lib -lsfml-main -lsfml-graphics -lsfml-system -lsfml-window -ltmxlite
.\a.exe
} else {
Write-Output ""
Write-Output "Use: pwsh build.ps1 --prepare"
Write-Output ""
}
}
Note que primeiro você precisa usar o parâmetro --prepare
para copiar os arquivos e depois da primeira cópia não precisa mais usá-lo:
Os comandos com
.exe
no final no Windows são indiferentes, ou seja, ter ou não ter, não faz diferença. O mesmo serve para o.\
(ponto barra invertida antes do nome do arquivo).
Exemplo com PowerShell:
pwsh build.ps1 --prepare
Exemplo com Windows PowerShell:
powershell build.ps1 --prepare
Se o Windows Defender emitir problema ao executar o comando, use assim para permitir a execução:
# Windows PowerShell:
powershell -ExecutionPolicy Bypass -File build.ps1 --prepare
# PowerShell
pwsh -ExecutionPolicy Bypass -File build.ps1 --prepare
Depois de preparar e compilar, ele vai rodar o binário automaticamente, se não rodar, rode:
.\a.exe
Vai aparecer essa janela:
Basta compilar com as flags necessárias: -lsfml-graphics -lsfml-window -lsfml-system
(para o SFML) e -ltmxlite
(para o tmxlite):
g++ main.cpp -lsfml-graphics -lsfml-window -lsfml-system -ltmxlite
Depois é só rodar o binário criado:
./a.out
Vai aparecer a mesma imagem do Windows:
Como o mapa que eu criei possui uma largura de 3200
pixel e altura de 736
, a janela do nosso jogo possui largura de 1280
para ver todo o cenário eu criei uma câmera caso você queira editar o arquivo com Tiled e notar que após alterar nem precisa recompilar que todo cenário será modificado.
Para ver essa câmera eu criei um patch
, se você quiser aplicá-lo para testar a câmera, faça o seguinte:
Crie um arquivo de nome: camera.patch
e copie e cole o conteúdo abaixo:
29a30,40
> sf::RectangleShape player(sf::Vector2f(64.f, 64.f));
> player.setFillColor(sf::Color::Red);
>
> float player_x = 10.f;
> float player_y = 512.f;
> player.setPosition(player_x, player_y);
>
> float player_speed = 300.f;
> sf::Clock clock;
>
>
37a49,67
> float dt = clock.restart().asSeconds();
>
> if(sf::Keyboard::isKeyPressed(sf::Keyboard::Right)){
> player_x += player_speed * dt;
> }else if(sf::Keyboard::isKeyPressed(sf::Keyboard::Left)){
> player_x -= player_speed * dt;
> }
>
> if(player_x < 0.f) player_x = 0.f;
> if(player_x > map_width - player.getSize().x) player_x = map_width - player.getSize().x;
>
> float view_offset_x = player_x + player.getSize().x / 2.f - window.getSize().x / 2.f;
>
> if(view_offset_x < 0.f) view_offset_x = 0.f;
> if(view_offset_x > map_width - window.getSize().x)
> view_offset_x = map_width - window.getSize().x;
>
> player.setPosition(player_x - view_offset_x, player_y);
>
64c94,99
< tile_sprite.setPosition(static_cast<float>(x * tile_size.x), static_cast<float>(y * tile_size.y));
---
> float draw_x = static_cast<float>(x * tile_size.x) - view_offset_x;
> float draw_y = static_cast<float>(y * tile_size.y);
>
> tile_sprite.setPosition(draw_x, draw_y);
>
> window.draw(player);
Salve o arquivo e aplique o patch com o comando:
patch main.cpp camera.patch
No Windows você pode instalar o comando patch
de instalando o Git Bash, veja o tutorial abaixo:
Tutorial desse próprio blog: Terminal Root.
Após recompilar o resultado será similar ao git abaixo:
O bloco vermelho simula o Player.
O ideal para desenvolvimento de jogos com certeza é Programação Orientada a Objetos e ECS, dependendo o tamnho do seu projeto ECS talvez não seja tão necessário assim, mas POO com certeza, pois torna o desenvolvimento menos trabalhoso e com ECS deixa mais organizado e de fácil manutenção para adicionar ou remover recursos.
Você pode fazer da maneira que mais lhe convém, eu mesmo costumo organizar meus projetos como nesse vídeo, mas, caso seu projeto seja grande o ideal é ECS mesmo.
Então, criei uma versão ECS para você notar que com tmxlite seus projetos ficam organizados e roda normalmente.
A estrutura do ECS está assim:
O
System.hpp
não está sendo usado, mas deixei aí caso você queira implementá-lo na sua estrutura.
Para testar, basta fazer o download do arquivo .zip
em: https://terminalroot.com/downloads/ecs.zip, descompactar e construir com CMake e rodar.
Exemplo:
wget -q https://terminalroot.com/downloads/ecs.zip
unzip ecs.zip -d ecs
cd ecs
cmake . -B build
cmake --build build
./build/ECS_Tilemap
Essa versão já está com a câmera.
No Windows você pode instalar o Cmake de acordo com esse tutorial, use o Clang, defina o clang
como ambiente, veja aqui um exemplo e use cmake -g "Unix Makefiles"
para compilar no terminal, ou sem para compilar com MSVC no Visual Studio.
Esse artigo ficou muito grande, pois faz parte da documentação do jogo que que estou desenvolvendo. Tive que refazer sprites , mapas,… ainda é só um esboço, mas abaixo tem uma ideia inicial de como está ficando. Feito d absoluto do zero: