1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ASOポップカルチャー専門学校ゲーム系Advent Calendar 2024

Day 2

C++で2つのオブジェクトをユニークポインタのまま相互参照させる方法

Last updated at Posted at 2024-11-18

使用環境
・ISO C++20標準(20でなくても実装可能だと思います。)
・DxLibrary

問題

  1. 二人対戦のゲームで、unique_ptr型のPlayerAとBを用意したい。
  2. PlayerAはBの、BはAの情報が欲しい。
  3. PlayerはActorBaseの子であるCharacterBaseの子として継承されている。
  4. Actorは生成と同時にstd::unique_ptr< ActorBase>を纏める配列actors_に格納され、ActorBase型として処理される。
4.このように、actors_に纏めた状態でのみ処理できるようにする
Actor生成時
actors_.emplace_back(std::move(std::make_unique<Player>()));
actors_.emplace_back(std::move(std::make_unique<Stage>()));
------------------------------
Actor処理時
for(auto& actor : actors_)
{
    actors_->Update();
}

この問題を解決したいときに、出来るなら

GameScene.cpp
std::unique_ptr<Player>playerA = std::make_unique<Player>(playerB);
std::unique_ptr<Player>playerB = std::make_unique<Player>(playerA);
↓
Player.cpp
Player(std::unique_ptr<CharacterBase> otherPlayer){
    otherPlayer_ = otherPlayer;
}
↓
GameScene.cpp
actors_.emplace_back(std::move(playerA));
actors_.emplace_back(std::move(playerB));

ってしたいところなんですけど、当然できないです。(Aを生成するとき、Bはまだ生成されていないのでポインタを渡しようがない。)

とりあえずすぐ思いついた方法が、とりあえず生成し、後から相手プレイヤーをポインタで渡すことでした。

----------
Characterbase.h
もう一方のキャラクター情報を持つ変数を宣言。
CharacterBase* otherPlayer_;
----------
std::unique_ptr<Player>playerA = std::make_unique<Player>();
std::unique_ptr<Player>playerB = std::make_unique<Player>();

playerA->SetEnemy(playerB.get());
playerB->SetEnemy(playerA.get());
            ↓ 親であるCharacterBaseのSetEnemy関数が呼ばれる
void CharacterBase::SetEnemy(CharacterBase* otherPlayer){
    otherPlayer_ = otherPlayer;
}

actors_.emplace_back(std::move(playerA)));
actors_.emplace_back(std::move(playerB)));

これで条件を満たしつつ相手プレイヤーを渡せていますが、生ポインタを許容してしまうと常に解放忘れのリスクが付きまとうことが嫌(この場合はスマートポインタを借用しているだけなので解放忘れは起こり得ないのですが、生ポインタ=deleteしなくていいのか?という疑念をまず持たせたくない)なので、生ポインタを使わなくていい方法を考えていきます。
①actors_やplayerのポインタを、ユニークからシェアードに変えてしまう

----------
Characterbase.h
相手プレイヤーを持つ変数。
std::weak_ptr<CharacterBase> otherPlayer_;
----------
std::shared_ptr<PlayerA>playerA = std::make_shared<Player>();
std::shared_ptr<PlayerB>playerB = std::make_shared<Player>();

playerA->SetEnemy(playerB);
playerB->SetEnemy(playerA);
            ↓ 親であるCharacterBaseのSetEnemy関数が呼ばれる
void CharacterBase::SetEnemy(std::weak_ptr<CharacterBase> otherPlayer){
    otherPlayer_ = otherPlayer;
}

actors_.emplace_back(playerA);
actors_.emplace_back(playerB);

生ポインタ使用よりはだいぶ良くなったと思います。
しかし、「actors_に纏めて処理をする」以上、actors_やそれに格納していたActor達を全てシェアードポインタに変える必要があり、作業の手間もかかる上にPlayerのためだけにStageやItem等他のActorが意味なくシェアードに変わるのが嫌ですね。無駄に参照カウント分のリソースを食うしね。

②参照型を渡そうとする

----------
Characterbase.h
もう一方のキャラクター情報を持つ変数を宣言。
CharacterBase& otherPlayer_;
----------
std::unique_ptr<Player>playerA = std::make_unique<Player>();
std::unique_ptr<Player>playerB = std::make_unique<Player>();

この時点でダメですね。
参照型変数は初期化リストで初期化する必要があるのに、コンストラクタ引数にplayerA,Bを渡すことはできないので、結局playerAの初期化時に生成すらされていないplayerBを渡す必要が生まれてしまいます。
…。
ということで現在の設計方針だとユニークポインタのままで生ポインタを使わずに実装というのは不可能そうなので、設計方針を変える方向で考えます。
考えた結果
元:お互いのプレイヤーが、相手の情報を持っている。
  ↓
新:GameSceneがPlayerの情報を持っておいて、PlayerにはGameSceneの参照を渡す。
  Playerが相手の情報を欲しくなったら、gameScene_.GetPlayerData()と情報を要求する
というように設計方法を変えました。

std::unique_ptr<Player>playerA = std::make_unique<Player>(*this,playerNumber);
std::unique_ptr<Player>playerB = std::make_unique<Player>(*this,playerNumber);

CharacterBase::CharacterBase(GameScene& scene,int plnum),scene_(scene){
    scene.SetPlayer(*this);
    myPlayerNumber_ = plnum;
}

GameScene::SetPlayer(CharacterBase& character){
	if (playerDatas_[0] == nullptr){
		playerDatas_[0] = std::make_unique<GameScene::playerData>(character);
	}
	else if (playerDatas_[1] == nullptr){
		playerDatas_[1] = std::make_unique<GameScene::playerData>(character);
	}
}
// SetPlayerで参照型を引数にしてインスタンスを生成している為GameSceneでキャラクターを持つ参照型の引数が必要だが、
// 当然生成前にプレイヤーの参照を持てるわけがない。
// よって、GameSceneクラス内にインナークラスを持つことで初期化のタイミングを遅延させる。
GameScene.h
class GameScene : public SceneBase,
{
public:
    CharacterBase& GetEnemy(int charNum);
private:
    class playerData{
    private:
    	//	プレイヤー
    	CharacterBase& _player;
    public:
    	//	プレイヤーデータを初期化するだけのコンストラクタ
    	playerData(CharacterBase& chara) : _player(chara){};
    	//	_playerDataを返す
    	CharacterBase& GetPlayer(void);
    };
    
    std::array<std::unique_ptr<GameScene::CharacterBase>, 2> players_;
}

相手プレイヤーの情報が欲しくなったら、scene_.GetEnemy(myPlayerNumber_).GetPos();などで得られる
IPlayerData& GameScene::GetEnemy(int plNum)
{
	if (plNum == player1_)
	{
		return playerDatas_[1]->GetPlayer();
	}
	if (plNum == player2_)
	{
		return playerDatas_[0]->GetPlayer();
	}
}

これで、相手プレイヤーの情報を随時得られ、かつ生もシェアードもウィークも使用していないコードが誕生しました。おめでとう!ありがとう!
でも今はCharacterBaseをそのまま渡しているので、やろうと思えばscene_.GetEnemy(myPlayerNumber_).Destroy()とかInit()とか絶対対戦相手のプレイヤーが呼んではいけないpublic関数まで呼ぶことができてしまい、とても危険です。
はい「お前.ChengeState(STATE::DEAD)」!はい死んだ~俺の勝ち!とか言われるかもしれんからね

そこで活躍するのがインターフェースと呼ばれるもので、これを使うことで呼ぶ必要のないクラスを絶対呼べなくすることができます。

例えば、渡したいのがFLOAT3型のPositionのみだった場合
今までは、

class CharacterBase
{
public:
    void Init();
    void Destroy();
    void 自爆(){自爆して、即敗北します。};
    FLOAT3 GetPosition();
}
-------
CharacterBase& GameScene::GetEnemy(int plNum){実装は上を見て};
FLOAT3 position = scene_.GetEnemy(myPlayerNumber_).GetPosition();
if(負けそう)
{
    scene_.GetEnemy(myPlayerNumber_).自爆();
}

とGetEnemyで返す型をCharacterBaseにしていたので相手を自爆させられるようになっていたのですが、
インターフェースを使うと

class CharacterInfo
{
public:
    virtual FLOAT3 GetPosition() = 0;
}
class CharacterBase,public CharacterInfo
{
public:
    void Init();
    void Update();
    void 自爆(){自爆して、即敗北します。};
    FLOAT3 GetPosition() override;
}
------
CharacterInfo& GameScene::GetEnemy(int plNum){実装は上を見て};
FLOAT3 position = scene_.GetEnemy(myPlayerNumber_).GetPosition();
自爆()はCharacterInfoクラスにないので呼べない

という風に、必要のない関数を絶対に呼べないようにすることができました。
これを先ほどのコードに適用することで、相手の情報を得つつも、相手の操作は絶対に出来ない実装ができると思います。
この技を駆使して、堅牢なカプセルを目指しましょう!

1
0
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?