Domanda Come creare un vettore bidimensionale (primo campo un int, secondo campo puntatore ad oggetti) in C++

giorgik63

Utente Iron
19 Luglio 2024
8
2
3
6
Sto usando C++17 su Windows per un video gioco 2D di tipo RPG retrò. Ho creato un vettore del tipo seguente:
C++:
class Entity
{
public:
    // coordinate mondo: la mappa del mondo
    int worldX = 0, worldY = 0;
 
    std::string name;
   
public:
    Entity( );
    ~Entity( );
   
    virtual void draw( );
};

In un'altra classe (di nome GamePanel) ho dichiarato il seguente attributo:

C++:
std::vector<Entity*> obj;

Inserisco un dei tanti puntatori ad un oggetto (ne esistono varie classi di oggetti derivati da Entity) nel seguente modo:
C++:
class OBJ_Door : public Entity
{
public:
    OBJ_Door( )
    {
        name = "Door";
    }
   
    void draw( ) override;
};

void foo( )
{
    int i = 0;

    Entity* tmp = new OBJ_Door( );
    GamePanel::GetInstance()->obj.push_back(tmp);
    GamePanel::GetInstance()->obj.at(i)->worldX = 10 ;
    GamePanel::GetInstance()->obj.at(i)->worldY = 28;

    i++;
    tmp = new OBJ_Door( );
    GamePanel::GetInstance()->obj.push_back(tmp);
    GamePanel::GetInstance()->obj.at(i)->worldX = 30;
    GamePanel::GetInstance()->obj.at(i)->worldY = 8;
}

Ora questo vettore obj possiede degli elementi appartenenti alla mappa 1 del gioco.
Vorrei ora creare nello stesso vettore un insieme di elementi appartenenti alla mappa 2 del gioco, che quindi conterrà altre istanze di altri oggetti derivati da Entity. In questo modo posso riferirmi ai soli oggetti di una data mappa e disegnarli nel gioco.
Come posso dichiarare questo vettore con le caratteristiche che ho appena descritto ?
 
Perchè al momento mi piace vedere tutti gli aspetti in C++ per creare da zero un gioco 2D. Al momento ho fatto una gran parte del gioco quasi completo. Ora volevo gestire le diverse mappe da usare nel gioco.
Nel frattempo ho pensato chwe poteva essere una soluzione la seguente:

in GamePanel, vario il vettore in
C++:
std::vector<Entity*>* obj;
quindi in (in realtà è un'altra classe, ma per semplicità qui metto una funzione) foo( ):
C++:
int mapNum = 0;
//--------------------------------------mappa 0
int i = 0;
Entity* tmp = new OBJ_Door( );
GamePanel::GetInstance()->obj[mapNum].push_back(tmp);
GamePanel::GetInstance()->obj[mapNum].at(i)->worldX = 10 ;
GamePanel::GetInstance()->obj[mapNum].at(i)->worldY = 28;

 i++;
 tmp = new OBJ_Door( );
 GamePanel::GetInstance()->obj[mapNum].push_back(tmp);
 GamePanel::GetInstance()->obj[mapNum].at(i)->worldX = 30;
 GamePanel::GetInstance()->obj[mapNum].at(i)->worldY = 8;

//-------------------------------------- mappa 1
mapNum++;
i = 0;
Entity* tmp = new OBJ_Coin_Bronze();
GamePanel::GetInstance()->obj[mapNum].push_back(tmp);
GamePanel::GetInstance()->obj[mapNum].at(i)->worldX = 25;
GamePanel::GetInstance()->obj[mapNum].at(i)->worldY = 23;

i++;
tmp = new OBJ_Key();
GamePanel::GetInstance()->obj[mapNum].push_back(tmp);
GamePanel::GetInstance()->obj[mapNum].at(i)->worldX = 26;
GamePanel::GetInstance()->obj[mapNum].at(i)->worldY = 20;

Secondo voi è corretto ?
 
Ultima modifica:
Non uso nessun engine Unity o meglio ancora Unreal, anche se in passato ho usato entrambi. Ho scritto tutto io, da zero. Guardando un bel tutorial in Java. Tutorial su un gioco 2D rpg retro'.
Mi sono accorto di aver commesso un errore, la versione giusta nella dichiarazione del vettore e':
C++:
std::vector<Entity*> obj[10];
In questo modo posso memorizzare i puntatori nel vettore in un array di massimo 10 mappe.
 
Non uso nessun engine Unity o meglio ancora Unreal, anche se in passato ho usato entrambi. Ho scritto tutto io, da zero. Guardando un bel tutorial in Java. Tutorial su un gioco 2D rpg retro'.
Mi sono accorto di aver commesso un errore, la versione giusta nella dichiarazione del vettore e':
C++:
std::vector<Entity*> obj[10];
In questo modo posso memorizzare i puntatori nel vettore in un array di massimo 10 mappe.
Bhe, dipende sempre qual'è il tuo scopo, se lo scopo è puramente di didattica allora continua pure così, ma se intendi fare un gioco per poi farci un business sopra senza usare un engine è un suicidio assicurato, è come reinventare la ruota, impiegheresti il triplo del tempo e comunque non riusciresti ad offrire una qualità che di base qualsiasi engine fornisce (per non parlare di tutte le difficoltà tecniche già consolidate e tutti gli strumenti che un engine può offrire), se non Unity o Unreal ce ne sono di più "lite" come construct/Godot ecc :)

In ogni caos in bocca al lupo :)
 
Se non vuoi usare un engine e vuoi imparare realizzandolo da zero in C++ ti consiglio comunque di farlo per bene: non pre-caricare tutte le mappe in memoria e non creare tutte le entità esplicitamente da codice, il modo corretto è implementare la serializzazione e la deserializzazione dei tuoi oggetti Entity per poterli salvare e leggere su file. A quel punto ti basta una funzione "load_map" dove gli passi il nome o l'id del file e quella penserà a popolare il vettore della mappa attuale. Il risultato è codice più pulito, meno uso di RAM e la possibilità di modificare/aggiungere mappe operando solo sui dati (senza quindi ricompilare il gioco).
 
Grazie dei vostri consigli.
@gabrygin90: hai senza dubbio ragione, sull'uso degli engine già fatti, ma volevo capire meglio come funzionano certe parti del game e quindi la soddisfazione di farlo da me. Quello che faccio non è per fare del business, ma solo per mia curiosità e divertimento.

@JunkCoder: il tuo è un utilissimo consiglio, che terrò presente nella nuova versione del gioco che sto creando. Infatti mi chiedevo anche io se aveva senso caricare tutte le mappe e gli oggetti annessi in memoria in un colpo solo. Devo capire come fare la serializzazione e la deserializzazione degli oggetti derivanti da Entity. Dovrò cercare qualche tutorial che me lo spieghi.
 
Grazie dei vostri consigli.
@gabrygin90: hai senza dubbio ragione, sull'uso degli engine già fatti, ma volevo capire meglio come funzionano certe parti del game e quindi la soddisfazione di farlo da me. Quello che faccio non è per fare del business, ma solo per mia curiosità e divertimento.

@JunkCoder: il tuo è un utilissimo consiglio, che terrò presente nella nuova versione del gioco che sto creando. Infatti mi chiedevo anche io se aveva senso caricare tutte le mappe e gli oggetti annessi in memoria in un colpo solo. Devo capire come fare la serializzazione e la deserializzazione degli oggetti derivanti da Entity. Dovrò cercare qualche tutorial che me lo spieghi.

Dovresti scegliere il formato per serializzare i dati, ad esempio se ti piacerebbe usare un normale editor di testo per modificare le mappe sarebbe meglio usare JSON, INI, CSV... Se non ti importa o preferisci la velocità o mantenere i file mappa di ridotte dimensioni potresti usare un formato binario. Posso postare qui un esempio ma servirebbe qualche altra informazione come se le classi che derivano da Entity hanno altri tipi di valori da salvare o se li distingui solo in base al nome e all'implementazione di draw (es. porta chiusa/aperta sarebbe già un valore fuori da entity base).
 
  • Mi piace
Reazioni: giorgik63
Ultima modifica:
@JunkCoder: ti posto un esempio di una delle classi che derivano da Entity e che va inserito nella mappa del gioco:

C++:
#pragma once
#include "Entity.h"

class OBJ_Key : public Entity
{
public:
    OBJ_Key( );

    void getImage( );
    void setAction( ) override;
    void speak( ) override;
    void setDialogue( ) override;
    void update( ) override;
    void draw( ) override;
};

Quindi di seguito metto la definizione di un metodo di una classe che genera i puntatori

C++:
void AssetSetter::setObject( )
{
    int mapNum = 0;
    int i = 0;

    //------------------------------------ PRIMA MAPPA -------------------------------------------------
    Entity* tmp = new OBJ_Key( );
    GamePanel::GetInstance( )->obj[mapNum].push_back(tmp);
    GamePanel::GetInstance( )->obj[mapNum].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance( )->obj[mapNum].at(i)->worldY = 21 * GamePanel::GetInstance()->tileSize;  // posizione riga 21 + 1 della mappa .txt

    tmp = new OBJ_Axe( );
    i++;
    GamePanel::GetInstance()->obj[mapNum].push_back(tmp);
    GamePanel::GetInstance()->obj[mapNum].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance()->obj[mapNum].at(i)->worldY = 31 * GamePanel::GetInstance()->tileSize;  // posizione riga 31 + 1 della mappa .txt
}

Tutti i vari oggetti da inserire nella mappa, vengono memorizzati in obj così. Stessa cosa per i vari personaggi come gli NPC, i mostri ecc... ma in diverso vettore:

C++:
void AssetSetter::setNPC( )
{
    int mapNum = 0;
    int i = 0;

    //------------------------------------ PRIMA MAPPA -------------------------------------------------
    Entity* tmp = new NPC_OldMan( );
    GamePanel::GetInstance( )->npc[mapNum].push_back(tmp);
    GamePanel::GetInstance( )->npc[mapNum].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance( )->npc[mapNum].at(i)->worldY = 21 * GamePanel::GetInstance()->tileSize;  // posizione riga 21 + 1 della mappa .txt

    tmp = new NPC_OldMan( );
    i++;
    GamePanel::GetInstance( )->npc[mapNum].push_back(tmp);
    GamePanel::GetInstance( )->npc[mapNum].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance( )->npc[mapNum].at(i)->worldY = 31 * GamePanel::GetInstance()->tileSize;  // posizione riga 31 + 1 della mappa .txt
}

void AssetSetter::setMonster( )
{
    int mapNum = 0;
    int i = 0;

    //------------------------------------ PRIMA MAPPA -------------------------------------------------
    Entity* tmp = new MON_GreenSlime( );
    GamePanel::GetInstance( )->monster[mapNum].push_back(tmp);
    GamePanel::GetInstance( )->monster[mapNum].at(i)->worldX = 23 * GamePanel::GetInstance()->tileSize;  // posizione colonna 23 + 1 della mappa .txt
    GamePanel::GetInstance( )->monster[mapNum].at(i)->worldY = 42 * GamePanel::GetInstance()->tileSize;   // posizione riga 37 + 1 della mappa .txt

    i++;
    tmp = new MON_GreenSlime( );
    GamePanel::GetInstance( )->monster[mapNum].push_back(tmp);
    GamePanel::GetInstance( )->monster[mapNum].at(i)->worldX = 34 * GamePanel::GetInstance()->tileSize;  // posizione colonna 34 + 1 della mappa .txt
    GamePanel::GetInstance( )->monster[mapNum].at(i)->worldY = 42 * GamePanel::GetInstance()->tileSize;  // posizione riga 42 + 1 della mappa .txt

    i++;
    tmp = new MON_GreenSlime( );
    GamePanel::GetInstance( )->monster[mapNum].push_back(tmp);
    GamePanel::GetInstance( )->monster[mapNum].at(i)->worldX = 38 * GamePanel::GetInstance()->tileSize;  // posizione colonna 38 + 1 della mappa .txt
    GamePanel::GetInstance( )->monster[mapNum].at(i)->worldY = 42 * GamePanel::GetInstance()->tileSize;  // posizione riga 42 + 1 della mappa .txt
}

dove avremo come classi:

C++:
#pragma once
#include "Entity.h"

class NPC_OldMan : public Entity
{
public:
    NPC_OldMan();

    void getImage();
    void setAction() override;
    void speak() override;
    void setDialogue() override;
    void update() override;
    void draw() override;
};
-----------------------------------------------------------------------------
#pragma once
#include "../entity/Entity.h"
#include "../entity/Projectile.h"

class MON_GreenSlime : public Entity
{
public:
    Projectile* projectile;

public:
    MON_GreenSlime();

    void getImage();
    void setAction() override;
    void damageReaction() override;
    void setDialogue() override;
    void speak() override;
    void checkDrop() override;
    void dropItem(Entity* droppedItem) override;
    void update() override;
    void draw() override;
    void dyingAnimation(int x, int y) override;
};

Poi definisco un altro vettore:
C++:
std::vector<Entity*> entityList;

di conseguenza in GamePanel::draw( ), caricherò gli oggetti, npc, mostri in

C++:
// riempie il vettore lista entita' di tutti gli NPC
for (int i = 0; i < npc[currentMap].size(); i++)
{
    if (!npc[currentMap].at(i)->name.empty())
    {
        entityList.push_back(npc[currentMap].at(i));
    }
}
// riempie il vettore lista entita' di tutti gli Objects
for (int i = 0; i < obj[currentMap].size(); i++)
{
    if (!obj[currentMap].at(i)->name.empty())
    {
        entityList.push_back(obj[currentMap].at(i));
    }
}
// riempie il vettore lista entita' di tutti i mostri
for (int i = 0; i < monster[currentMap].size(); i++)
{
    if (!monster[currentMap].at(i)->name.empty())
    {
        entityList.push_back(monster[currentMap].at(i));
    }
}

// ordiniamo il vettore di entita' in base all'ordine delle coordinate mondo worldY
// di ciascuna entita' inserita
std::sort(entityList.begin(), entityList.end(), [](Entity* a, Entity* b)->bool { return a->worldY < b->worldY; });

// DRAW ENTITIES: Player, NPC, OBJ_*, MONSTER, Particelle
for (auto iter = entityList.begin(); iter != entityList.end(); ++iter)
{
    (*iter)->draw();
}

// EMPTY ENTITY LIST
for (std::vector<Entity*>::iterator it = entityList.begin(); it != entityList.end();)
{
    it = entityList.erase(it);
}

Ora vorrei che questi elementi contenuti in entityList vengano assegnati alla mappa1, così poi da creare altri elementi in entityList per farli appartenere alla mappa2 e così via. Ovviamente questi elementi differiscono in numero e posizione (worldX, worldY) per ogni mappa.
Come devo creare questo nuovo vettore che contiene il tutto, avente come ulteriore campo numMap ?
 
Ti faccio un esempio molto semplice che serializza/deserializza in formato binario su file il tipo entità e le sue coordinate:

C++:
class Entity
{
public:
    // coordinate mondo: la mappa del mondo
    int worldX = 0, worldY = 0;
    int typeID;

    std::string name;

    Entity();
    ~Entity();

    virtual void draw();

    void serialize(FILE* stream);
    static Entity* deserialize(FILE* stream);

protected:
    static bool deserialize_int(int* i, FILE* stream);
};

class OBJ_Door : public Entity
{
public:
    OBJ_Door()
    {
        name = "Door";
        typeID = 1;
    }

    void draw() override;
};

class OBJ_Key : public Entity
{
public:
    OBJ_Key()
    {
        name = "Key";
        typeID = 2;
    }

    void draw() override;
};

class NPC_OldMan : public Entity
{
public:
    NPC_OldMan()
    {
        name = "OldMan";
        typeID = 3;
    }

    void draw() override;
};

const size_t INT_SIZE = sizeof(int);

void Entity::serialize(FILE* stream)
{
    int nameLen = this->name.size();
    fwrite(&typeID, INT_SIZE, 1, stream);
    fwrite(&worldX, INT_SIZE, 1, stream);
    fwrite(&worldY, INT_SIZE, 1, stream);
}

bool Entity::deserialize_int(int* i, FILE* stream)
{
    size_t read = fread(i, INT_SIZE, 1, stream);
    return (read == INT_SIZE);
}

Entity* Entity::deserialize(FILE* stream)
{
    bool ok;
    int X, Y, typeID;

    ok = stream != nullptr
        && deserialize_int(&typeID, stream)
        && deserialize_int(&X, stream)
        && deserialize_int(&Y, stream);

    if (ok)
    {
        Entity* e;
        switch (typeID)
        {
        case 1: e = new OBJ_Door(); break;
        case 2: e = new OBJ_Key(); break;
        case 3: e = new NPC_OldMan(); break;
        default: return nullptr;
        }
        e->worldX = X;
        e->worldY = Y;
        return e;
    }
    return nullptr;
}

bool load_map(std::vector<Entity*>& map, const char* mapFileName)
{
    FILE* stream = fopen(mapFileName, "rb");
    if (stream == nullptr)
        return false;

    Entity* e;
    while ((e = Entity::deserialize(stream)) != nullptr)
        map.push_back(e);
   
    fclose(stream);
    return true;
}

bool save_map(const std::vector<Entity*>& map, const char* mapFileName)
{
    FILE* stream = fopen(mapFileName, "wb");
    if (stream == nullptr)
        return false;

    for (Entity* e : map)
        e->serialize(stream);

    fclose(stream);
    return true;
}

/* eg:
std::vector<Entity*> currentMap;
bool mapFound = load_map(currentMap, "level1.map");
if (mapFound)
{
    // il file esiste e currentMap è stato popolato
}
*/

Se ti servisse aggiungere dati specifici per il tipo di oggetto (es. dialoghi, colore oggetto ecc.) dovresti rendere la serializzazione un metodo virtuale e implementarla in modo diverso per ogni oggetto, mentre per la deserializzazione potresti farla con altri metodi statici oppure aggiungendo un costruttore che legge dal file stream. Questo ovviamente è solo uno dei possibili modi per farlo, è giusto per dare un punto di partenza.
 
  • Mi piace
Reazioni: giorgik63
@JunkCoder: grazie per il tuo codice. Lo studierò bene, per poi usarlo nel gioco che sto scrivendo.

Mi trovo ora in un punto che non riesco a capire. Mi spiego:
ho provato a variare il vettore di puntatori alla classe Entity, creando un array di quel tipo:
C++:
std::vector<Entity*> npc[2][10];
quello che penso faccia: primo campo ([2]) numero di mappe, secondo campo ([10]) numero di puntatori a Entity per ogni elemento del primo campo ([2]).
E' giusto ?
Se sì, perchè quando faccio questa operazione:

C++:
void AssetSetter::setNPC( )
{
    int mapNum = 0;
    int i = 0;

    //------------------------------------ PRIMA MAPPA -------------------------------------------------
    Entity* tmp = new NPC_OldMan();
    
    GamePanel::GetInstance()->npc[mapNum][0].push_back(tmp);
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldY = 21 * GamePanel::GetInstance()->tileSize;  // posizione riga 21 + 1 della mappa .txt
        
    tmp = new NPC_OldMan( );
    i++;
    GamePanel::GetInstance()->npc[mapNum][1].push_back(tmp);
    GamePanel::GetInstance()->npc[mapNum][1].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance()->npc[mapNum][1].at(i)->worldY = 31 * GamePanel::GetInstance()->tileSize;  // posizione riga 31 + 1 della mappa .txt
    
    //------------------------------------ SECONDA MAPPA -------------------------------------------------
    mapNum++;
    i = 0;
    tmp = new NPC_Merchant( );
    
    GamePanel::GetInstance()->npc[mapNum][0].push_back(tmp);
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldX = 12 * GamePanel::GetInstance()->tileSize;  // posizione colonna 12 + 1 della mappa .txt
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldY = 7 * GamePanel::GetInstance()->tileSize;   // posizione riga 7 + 1 della mappa .txt
}

ottengo (a runtime) un errore di "invalid vector subscript" quando esegue la riga:

C++:
GamePanel::GetInstance()->npc[mapNum][1].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt

non riesco a capire perchè ?
 
@JunkCoder: grazie per il tuo codice. Lo studierò bene, per poi usarlo nel gioco che sto scrivendo.

Mi trovo ora in un punto che non riesco a capire. Mi spiego:
ho provato a variare il vettore di puntatori alla classe Entity, creando un array di quel tipo:
C++:
std::vector<Entity*> npc[2][10];
quello che penso faccia: primo campo ([2]) numero di mappe, secondo campo ([10]) numero di puntatori a Entity per ogni elemento del primo campo ([2]).
E' giusto ?
Se sì, perchè quando faccio questa operazione:

C++:
void AssetSetter::setNPC( )
{
    int mapNum = 0;
    int i = 0;

    //------------------------------------ PRIMA MAPPA -------------------------------------------------
    Entity* tmp = new NPC_OldMan();
   
    GamePanel::GetInstance()->npc[mapNum][0].push_back(tmp);
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldY = 21 * GamePanel::GetInstance()->tileSize;  // posizione riga 21 + 1 della mappa .txt
       
    tmp = new NPC_OldMan( );
    i++;
    GamePanel::GetInstance()->npc[mapNum][1].push_back(tmp);
    GamePanel::GetInstance()->npc[mapNum][1].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt
    GamePanel::GetInstance()->npc[mapNum][1].at(i)->worldY = 31 * GamePanel::GetInstance()->tileSize;  // posizione riga 31 + 1 della mappa .txt
   
    //------------------------------------ SECONDA MAPPA -------------------------------------------------
    mapNum++;
    i = 0;
    tmp = new NPC_Merchant( );
   
    GamePanel::GetInstance()->npc[mapNum][0].push_back(tmp);
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldX = 12 * GamePanel::GetInstance()->tileSize;  // posizione colonna 12 + 1 della mappa .txt
    GamePanel::GetInstance()->npc[mapNum][0].at(i)->worldY = 7 * GamePanel::GetInstance()->tileSize;   // posizione riga 7 + 1 della mappa .txt
}

ottengo (a runtime) un errore di "invalid vector subscript" quando esegue la riga:

C++:
GamePanel::GetInstance()->npc[mapNum][1].at(i)->worldX = 21 * GamePanel::GetInstance()->tileSize;  // posizione colonna 21 + 1 della mappa .txt

non riesco a capire perchè ?
Messaggio unito automaticamente:

Ho capito il motivo: GamePanel::GetInstance()->npc[mapNum][1] questo indice [1] indica i puntatori Entity* contenuti nel vettore, poi con .at(i) scelgo quello con indice i.