Home > OS >  Declaring an object as an array, and then declaring the type of one of the values in the same array
Declaring an object as an array, and then declaring the type of one of the values in the same array

Time:11-17

(Sorry for potentially confusing title) I'm making an old styled ascii based rogue-like. Here's some code:

    class Entity {
    public:
        class Player {
        public:
            int x, y, target;
            char collide;
            int power = 1, speed = 1, defense = 1, health = 10;
            int hp = health;
            
            void move(int dir);
        };
        class Kobold {
        public:
            int x, y;
            char collide;
            const int power = 2, speed = 1, defense = 1, health = 3;
            int hp = health;

            void move(int dir);
        };

My goal is to have an array named enemy and have each array value (or whatever it's called) have a different class as its type. For Example: enemy[0] can be an object of Kobold which is a member of Entity, and have enemy[5] be an object of Spider which is a member of Entity and etc. This way it's easier to have enemies that that are easier to declare and reference in for loops. What I'm asking for is how to declare the array enemy in such a way. It would be nice to have and answer to what I'm asking, but I'm not sure it's possible in a simple way, so if anyone has a simpler or easier way to accomplish my goal I would also like to see it. Thanks in advance.

Edit: Example code (ignore that it's not real):

//Declaring an array with 8 values with no type
void enemy[8];

//Assigning the Kobold class to the first enemy as it's type
enemy[0] = Entity::Kobold;

//Assigning the Spider class to the 3rd enemy as it's type
enemy[2] = Entity::Spider;

//Player attacks 3rd enemy (spider) and inflicts damage
enemy[2].hp -= player.power;

//Checks if any enemy is dead, and if so then delete it
for (int i = 0; i < 8; i  ) {
    if (enemy[i].hp <= 0) {
        //Removing enemy from it's class, therefore deleting it
        void enemy[i];
    }
}

//First enemy (Kobold) attacks player, inflicting damage
player.hp -= enemy[0].power;

//Player isn't dead, and attacks first enemy (Kobold) and kills it
enemy[0].hp -= player.power;

//Checks if any enemy is dead, and if so then delete it (again)
for (int i = 0; i < 8; i  ) {
    if (enemy[i].hp <= 0) {
        //Removing enemy from it's class, therefore deleting it
        void enemy[i];
    }
}

printw("You Win!");

I know it's not effective and probably has some errors, but it's just there to help visualize what I'm trying to do.

Edit:

This is similar to what I'm asking: Is it possible to have an array of different objects that come from the same base class?

I am trying to make an array as an object of Entity but I want to have any array value be an object of any sub-class from Entity. So something like this:

Entity enemy[5];
enemy[0] = new Kobold;
enemy[4] = new Spider;

And then I can reference to the easier like this:

for (int i = 0; i < 5; i  ) {
    if (array[i].x == player.x and array[i].y == player.y) {
        printw("player and an enemy are in the same spot");
    }
}

So the point is to easily access variables from every enemy (virtually) at once.

Another example (checking if any enemy has 0 hp):

for (int i = 0; i < amount_of_enemies; i  ) {
    if (enemy[i].hp <= 0) {
        //some code to delete the enemy and give player XP etc.
    }
}

CodePudding user response:

Part 1

There are two popular ways that can be used here, one way is to use C 17's std::any. Another way is to use virtual methods and interfaces.

std::any approach allows to store any unrelated classes within same array std::vector<std::any>. std::any allows to store different types in single container supporting only single type.

Later when accessing entities from this array you have to check and cast their types to specific types and process each type inside if clause. Every if will have specific for this type processing code.

With any you can have totally different methods and fields for every processed class, for example methods .player_shout() (for Player) and .kobold_defense() (for Kobold) in code below.

Array may also have some types that you can't process or don't know, then you can skip them or handle in other way, see code below:

Try it online!

#include <iostream>
#include <vector>
#include <any>

class Entity {
public:        
    class Player {
    public:
        int x, y, target;
        char collide;
        int power = 1, speed = 1, defense = 1, health = 10;
        int hp = health;
        
        void move(int dir) { std::cout << "Moved Player to dir " << dir << std::endl; }
        void player_shout() { std::cout << "Player shouted!" << std::endl; }
    };
    class Kobold {
    public:
        int x, y;
        char collide;
        const int power = 2, speed = 1, defense = 1, health = 3;
        int hp = health;

        void move(int dir) { std::cout << "Moved Kobold to dir " << dir << std::endl; }
        void kobold_defense() { std::cout << "Kobold defensed!" << std::endl; }
    };
};

void ProcessEntities(std::vector<std::any> & entities) {
    for (std::size_t i = 0; i < entities.size();   i) {
        auto & e = entities[i];
        if (e.type() == typeid(Entity::Player)) {
            Entity::Player & player = *std::any_cast<Entity::Player>(&e);
            player.move(2);
            player.player_shout();
        } else if (e.type() == typeid(Entity::Kobold)) {
            Entity::Kobold & kobold = *std::any_cast<Entity::Kobold>(&e);
            kobold.move(1);
            kobold.kobold_defense();
        } else
            std::cout << "Unknown entity " << i << " of type "
                << e.type().name() << std::endl;
    }
}

struct BadType {};

int main() {
    std::vector<std::any> entities = {
        Entity::Player(), Entity::Kobold(), Entity::Player(),
        Entity::Player(), BadType(), Entity::Kobold(), BadType()};
    ProcessEntities(entities);
}

Output:

oved Player to dir 2
Player shouted!
Moved Kobold to dir 1
Kobold defensed!
Moved Player to dir 2
Player shouted!
Moved Player to dir 2
Player shouted!
Unknown entity 4 of type 7BadType
Moved Kobold to dir 1
Kobold defensed!
Unknown entity 6 of type 7BadType

Part 2

You may also use virtual methods to represent common class with some attributes (fields) and methods common to all entities. This is called a virtual interface approach.

All entities should inherit from this common interface and implement all inherited methods. Common class doesn't implement interface methods itself, although it can if needed.

You can have arbitrary deep hierarchy of virtual classes, anything is possible. Following example shows implementation of simple Common class and inheritance of it by all entities.

You can also mix virtual classes inheritance with type-casting approach (that was done for std::any in Part-1 of my answer). Following example calls common virtual method move() and also shows common attribute speed. And rest of logic is done through type-checking with type() method and type-casting with dynamic_cast, after casting you can use specific (non-inherited) attributes of each entity.

Hence you can mix in arbitrary way virtual inheritance with dynamic type casting.

Try it online!

#include <iostream>
#include <vector>
#include <memory>

class Entity {
public:
    class Common {
    public:
        int x, y;
        char collide;
        int power = 1, speed = 1, defense = 1, health = 10;
        int hp = health;

        virtual void move(int dir) = 0;
        virtual std::type_info const & type() const = 0;
        virtual ~Common() {}
    };

    class Player : public Common {
    public:
        int player_attr = 123;

        std::type_info const & type() const { return typeid(*this); }
        void move(int dir) { std::cout << "Moved Player to dir "
            << dir << " with health " << health << std::endl; }
        void player_shout() { std::cout << "Player shouted with attr "
            << player_attr << "!" << std::endl; }
    };
    class Kobold : public Common {
    public:
        int kobold_attr = 456;

        std::type_info const & type() const { return typeid(*this); }
        void move(int dir) { std::cout << "Moved Kobold to dir "
            << dir << " with health " << health << std::endl; }
        void kobold_defense() { std::cout << "Kobold defensed with attr "
            << kobold_attr << " !" << std::endl; }
    };
};

void ProcessEntities(std::vector<std::shared_ptr<Entity::Common>> & entities) {
    for (std::size_t i = 0; i < entities.size();   i) {
        auto & e = entities[i];
        std::cout << "Entity " << i << ": Speed " << e->speed << ". ";
        e->move(i % 3); // Common method
        if (e->type() == typeid(Entity::Player)) {
            Entity::Player & player = dynamic_cast<Entity::Player &>(*e);
            player.player_shout();
            std::cout << "Player attr " << player.player_attr << std::endl;
        } else if (e->type() == typeid(Entity::Kobold)) {
            Entity::Kobold & kobold = dynamic_cast<Entity::Kobold &>(*e);
            kobold.kobold_defense();
            std::cout << "Kobold attr " << kobold.kobold_attr << std::endl;
        } else
            std::cout << "Unknown entity " << i << std::endl;
    }
}

int main() {
    std::vector<std::shared_ptr<Entity::Common>> entities = {
        std::make_shared<Entity::Player>(), std::make_shared<Entity::Kobold>(),
        std::make_shared<Entity::Player>(), std::make_shared<Entity::Player>(),
        std::make_shared<Entity::Kobold>()};
    ProcessEntities(entities);
}

Output:

Entity 0: Speed 1. Moved Player to dir 0 with health 10
Player shouted with attr 123!
Player attr 123
Entity 1: Speed 1. Moved Kobold to dir 1 with health 10
Kobold defensed with attr 456 !
Kobold attr 456
Entity 2: Speed 1. Moved Player to dir 2 with health 10
Player shouted with attr 123!
Player attr 123
Entity 3: Speed 1. Moved Player to dir 0 with health 10
Player shouted with attr 123!
Player attr 123
Entity 4: Speed 1. Moved Kobold to dir 1 with health 10
Kobold defensed with attr 456 !
Kobold attr 456

If you need more simple version of virtual inheritance, to be more understandable for you, I've created this simpler code:

Try it online!

class Entity {
public:
    class Common {
    public:
        int x, y;
        char collide;
        int power = 0, speed = 0, defense = 0, health = 0;
        int hp = health;
        
        virtual void move(int dir) = 0;
        virtual ~Common() {}
    };
    class Player : public Common {
    public:
        Player() { power = 1; speed = 1; defense = 1; health = 10; hp = health; }
        void move(int dir) {}
    };
    class Kobold : public Common {
    public:
        Kobold() { power = 2; speed = 1; defense = 1; health = 3; hp = health; }
        void move(int dir) {}
    };
    class Spider : public Common {
    public:
        Spider() { power = 3; speed = 2; defense = 2; health = 5; hp = health; }
        void move(int dir) {}
    };
};

int main() {
    Entity::Common * enemies[3] = {
        new Entity::Kobold(), new Entity::Spider()};
    enemies[0]->move(3);
    enemies[0]->hp -= enemies[1]->power;
}

Part 3

If two first solutions (from Part 1 and 2 of my answer) don't fit for you, then I can suggest third solution based on std::variant and std::visit.

I took from your question examples of how you process enemies and player. And modified it a bit to use my third idea of using std::variant. See code below.

There are some tiny modification to your enemies classes like adding copy constructor (see CONSTR(Kobold);) and implementing Empty kind of entity to store empty values.

You can see in following code that I created two macros EN and EN2. Basically if you store your enemies as std::variant then you have to use one of these two macros in order to access and/or modify an enemy or interact with player. Because you can't use std::variant type directly. These macros use std::visit to access underlying specific type.

In order to start using std::variant solution like below you just have to define once special type Enemy, just by enumerating all possible enemy classes using Enemy = std::variant<Entity::Kobold, Entity::Spider, Entity::Dragon, Entity::Empty>;, see code.

I created Empty class to store empty values in array. You can check if element is empty by special macro EMPTY(enemy).

Try it online!

#include <variant>
#include <vector>
#include <iostream>

class Entity {
public:
    #define COPY(self, o) { self.x = o.x; self.y = o.y; self.collide = o.collide; self.power = o.power; self.speed = o.speed; self.defense = o.defense; self.health = o.health; self.hp = o.hp; }
    #define CONSTR(T) \
        T() {} \
        T(T const & o) { COPY((*this), o); } \
        T & operator =(T const & o) { COPY((*this), o); return *this; }

    class Player {
    public:
        int x, y, target;
        char collide;
        int power = 1, speed = 1, defense = 1, health = 10;
        int hp = health;
        
        void show_hp() const { std::cout << "Player health " << hp << std::endl; }
        void move(int dir) { std::cout << "Moved Player to dir " << dir << std::endl; }
    };
    class Kobold {
    public:
        int x, y;
        char collide;
        int power = 2, speed = 1, defense = 1, health = 1;
        int hp = health;

        CONSTR(Kobold);

        void show_hp() const { std::cout << "Kobold health " << hp << (hp <= 0 ? ", Dead!!!" : "") << std::endl; }
        void move(int dir) { std::cout << "Moved Kobold to dir " << dir << std::endl; }
    };
    class Spider {
    public:
        int x, y;
        char collide;
        int power = 3, speed = 1, defense = 1, health = 5;
        int hp = health;

        CONSTR(Spider);

        void show_hp() const { std::cout << "Spider health " << hp << (hp <= 0 ? ", Dead!!!" : "") << std::endl; }
        void move(int dir) { std::cout << "Moved Spider to dir " << dir << std::endl; }
    };
    class Dragon {
    public:
        int x, y;
        char collide;
        int power = 7, speed = 1, defense = 1, health = 20;
        int hp = health;

        CONSTR(Dragon);

        void show_hp() const { std::cout << "Dragon health " << hp << (hp <= 0 ? ", Dead!!!" : "") << std::endl; }
        void move(int dir) { std::cout << "Moved Dragon to dir " << dir << std::endl; }
    };
    class Empty {
    public:
        int x, y;
        char collide;
        int power = 0, speed = 0, defense = 0, health = 1;
        int hp = health;

        CONSTR(Empty);

        void show_hp() const { std::cout << "Empty!!!" << std::endl; }
        void move(int dir) { std::cout << "Empty!!!" << std::endl; }
    };
    #define EMPTY(x) (typeid(x) == typeid(Entity::Empty))
};

using Enemy = std::variant<
    Entity::Kobold, Entity::Spider, Entity::Dragon, Entity::Empty>;

#define EN(_pos, code) \
    std::visit([&](auto & enemy) code, enemies[_pos]);

#define EN2(_pos0, _pos1, code) \
    std::visit([&](auto & enemyA) { \
        std::visit([&](auto & enemyB) code, enemies[_pos1]); \
    }, enemies[_pos0]);

void Process(std::vector<Enemy> & enemies) {
    Entity::Player player;

    EN(2, {
        // Player attacks 3rd enemy (spider) and inflicts damage
        enemy.show_hp();
        enemy.hp -= player.power;
        enemy.show_hp();
    });

    //Checks if any enemy is dead, and if so then delete it
    for (int i = 0; i < enemies.size();   i) {
        EN(i, {
            if (!EMPTY(enemy) && enemy.hp <= 0) {
                //Removing enemy from it's class, therefore deleting it
                enemies[i] = Entity::Empty();
                std::cout << "Enemy " << i << " is dead and removed." << std::endl;
            }
        });
    }

    EN(0, {
        player.show_hp();
        //First enemy (Kobold) attacks player, inflicting damage
        player.hp -= enemy.power;
        player.show_hp();

        enemy.show_hp();
        //Player isn't dead, and attacks first enemy (Kobold) and kills it
        enemy.hp -= player.power;
        enemy.show_hp();
    });

    //Checks if any enemy is dead, and if so then delete it
    for (int i = 0; i < enemies.size();   i) {
        EN(i, {
            if (!EMPTY(enemy) && enemy.hp <= 0) {
                //Removing enemy from it's class, therefore deleting it
                enemies[i] = Entity::Empty();
                std::cout << "Enemy " << i << " is dead and removed." << std::endl;
            }
        });
    }

    EN2(3, 4, {
        enemyA.show_hp();
        enemyA.hp -= enemyB.power;
        enemyA.show_hp();

        enemyB.show_hp();
        enemyB.hp -= enemyA.power;
        enemyB.show_hp();
    });

    //Checks if any enemy is dead, and if so then delete it
    for (int i = 0; i < enemies.size();   i) {
        EN(i, {
            if (!EMPTY(enemy) && enemy.hp <= 0) {
                //Removing enemy from it's class, therefore deleting it
                enemies[i] = Entity::Empty();
                std::cout << "Enemy " << i << " is dead and removed." << std::endl;
            }
        });
    }

    std::cout << "You win!" << std::endl;
}

int main() {
    std::vector<Enemy> enemies = {
        Entity::Kobold(), Entity::Dragon(), Entity::Spider(),
        Entity::Spider(), Entity::Dragon(), Entity::Kobold()};
    Process(enemies);
}

Output:

Spider health 5
Spider health 4
Player health 10
Player health 8
Kobold health 1
Kobold health 0, Dead!!!
Enemy 0 is dead and removed.
Spider health 5
Spider health -2, Dead!!!
Dragon health 20
Dragon health 17
Enemy 3 is dead and removed.
You win!
  • Related