lundi 30 septembre 2019

callback to clean pointers (MCVE : auto eject traitor Soldier from Vehicle)

In my game, Soldier that becomes a traitor will be automatically ejected from Turret, Tank, and a lot of holder. (MCVE)

A Soldier can reside in 1 Turret and 1 Tank at the same time.
A Soldier can reside in at most 1 Turret and at most 1 Tank.

Here is the Soldier, Turret and Tank class (In real cases, they are in 3 files) :-

#include <iostream>
#include <string>
#include <vector>
struct Soldier{
    private: int team=0;
    public: void setTeam(int teamP){
        team=teamP;
        //should insert callback
    }
};
struct Turret{
    Soldier* gunner=nullptr;
    //currently it is so easy to access Turret's gunner
};
struct Tank {
    std::vector<Soldier*> passenger;
    //currently it is so easy to access Tank's passenger by index
};
std::vector<Soldier*> global_soldier;
std::vector<Turret*> global_turret;
std::vector<Tank*> global_tank;

Here is the main().
Currently whenever programmer want to set team of any instance of Soldier, he has to do the ejection manually :-

int main(){
    Soldier soldier1; global_soldier.push_back(&soldier1);
    Turret turret1;   global_turret.push_back(&turret1);
    turret1.gunner=&soldier1;
    //v game loop
    soldier1.setTeam(2);
    //v manual ejection (should be callback?)
    for(auto& ele: global_turret){  
        if(ele->gunner==&soldier1){
            ele->gunner=nullptr;
        }
    }
    for(auto& ele: global_tank){
        for(auto& elePass: ele->passenger){
            if(elePass==&soldier1){
                elePass=nullptr;
            }
        }
    }
    //^ manual ejection
    std::cout<<"should print 1="<<(turret1.gunner==nullptr)<<std::endl;
}

This leads to a lot of boilerplate code after every call of Soldier::setTeam(int) (maintenance problem) and performance problem.

How to fix it?

Current advantage that I don't want to lose :-
- It is so easy to access the Turret's gunner, and one of a Tank's passenger by index.

My workaround (MCVE)

In Soldier, I created a callback hub (callback_changeTeams).
In Turret and Tank, I create a callback definition to Soldier (Turret::ChangeTeam and Tank::ChangeTeam).

Here is the working code.
Soldier :-

class Soldier;
struct Callback{
    public: virtual void changeTeam_virtual(Soldier* soldier)=0;
};
std::vector<Callback*> callback_changeTeams;
struct Soldier{
    private: int team=0;
    public: void setTeam(int teamP){
        if(team==teamP){

        }else{
            team=teamP;
            for(auto callback :callback_changeTeams) 
                callback->changeTeam_virtual(this);
        }
        //should insert callback
    }
};

Turret :-

struct Turret;
std::vector<Turret*> global_turret;
struct Turret{
    Soldier* gunner=nullptr;
    struct ChangeTeam : public Callback{
        public: virtual void changeTeam_virtual(Soldier* soldier){
            for(auto& ele: global_turret){  
                if(ele->gunner==soldier){
                    ele->gunner=nullptr;
                }
            }
        }
    };
};

Tank (similar as Turret's ) :-

struct Tank;
std::vector<Tank*> global_tank;
struct Tank {
    std::vector<Soldier*> passenger;
    struct ChangeTeam : public Callback{
        public: virtual void changeTeam_virtual(Soldier* soldier){
            for(auto& ele: global_tank){
                for(auto& elePass: ele->passenger){
                    if(elePass==soldier){
                        elePass=nullptr;
                    }
                }
            }
        }
    };
};

Main :-

std::vector<Soldier*> global_soldier;
int main(){
    Turret::ChangeTeam turretCallback;
    Tank::ChangeTeam tankCallback;
    callback_changeTeams.push_back(&turretCallback);
    callback_changeTeams.push_back(&tankCallback);
    Soldier soldier1; global_soldier.push_back(&soldier1);
    Turret turret1;   global_turret.push_back(&turret1);
    turret1.gunner=&soldier1;
    //v game loop
    soldier1.setTeam(2);
    //v should be callback

    std::cout<<"should print 1="<<(turret1.gunner==nullptr)<<std::endl;
}

Disadvantages:-

1. Duplication code between Turret & Tank.
Not so bad, but in real case, I have a lot of Turret-like (that store a pointer to Soldier) and Tank-like (that store array of Soldier). It would be a lot of code duplication.

2. Still Bad performance. I have to iterate every Turret and Tank whenever I just change a team setting of a Soldier.
This can be solved by cache parent Turret and Tank, so iteration is not required.
However, in real case, I have a lot of parent type and it would be dirty e.g. :-

struct Soldier{
    //... some field / functions ...
    Turret* parentTurret;
    Tank* parentTank;
    SomeParentClass1* parent1;
    SomeParentClass2* parent2;   // bra bra bra.. ... dirty messy
};

My random ideas (not much useful) : Smart pointer (std::shared_ptr); std::unordered_map ; change design-pattern; make the callbacks commit as batch; I am using Entity Component System.

Aucun commentaire:

Enregistrer un commentaire