💨

【ゲームプログラミングC++】Chapter2課題を解いてみた

2024/10/14に公開

課題2.1は飛ばします

課題2.2

  • AnimSpriteComponentクラスを、複数のアニメーションをサポートするように変更しよう
    • 各アニメーションは配列内のテクスチャの範囲として定義する
  • ループしないアニメーションを実装しよう
    • アニメーション終了時、最初のテクスチャに戻さない=最後のテクスチャで止めるようにする
    • テクスチャの範囲を定義する時、ループするかどうかを指定する

実装概要

複数アニメーションの実装場所
今回追加するのは、人間のwalk, jump, punchのアニメーションです。
新しく専用クラスを作るのが面倒だったので、これらはShipクラスに追加実装する方針を取りました。

複数アニメーションのテクスチャ管理方法
課題説明で指定されている「各アニメーションは配列内のテクスチャの範囲として定義する」という点は、以下のように解釈しました。(正直、初めて読んだときは一体どういう意味なのか理解に時間がかかりました)

  • テクスチャ配列mAnimTexturesに、各アニメーションで使用するテクスチャを全て格納(下記のように連想配列などで別個管理はしない)
     mAnimTextures["punch"] = //punch関連のテクスチャ配列を代入
    
  • mAnimTextures配列の「何番目から何番目」が各アニメーションのテクスチャなのかを、別の連想配列(unordered_map<int, Animation> mAnimations)で管理
    • 具体的な実装として、mAnimTexturesにおけるインデックス範囲『first』『last』を管理するAnimation構造体を作成し、mAnimations配列に格納
    • 連想配列のキーintには、Actorの継承クラスが持つenumを設定(後述)

アニメーション状態の管理方法
自分が今どのアニメーションを描画するのかの責任については、Actorを継承したクラスに任せます。

  • Actorに、現在のアニメーションをintで所持するmCurrAnimStateを用意
    • (例:1がwalk, 2がjump)
  • Actorの継承クラスで、各アニメーション名をint型でラベリングするenumを定義
    • 処理速度向上と、アニメーションの種類を分かりやすく表現するためにenumを採用しています
      速度比較
      【ループ回数:10000】
      0,1000,2000,3000,4000,5000,6000,7000,8000,9000,
      
      int time 0.608000[ms]
      
      0,1000,2000,3000,4000,5000,6000,7000,8000,9000,
      
      string time 2.117000[ms]
      
      #include <iostream>
      #include <string>
      #include <time.h>
      #include <unordered_map>
      #include <vector>
      
      int main() {
        const int loop = 10000;
        const int outputPoint = loop / 10;
        std::cout << "【ループ回数:" << loop << "】\n";
      
        clock_t start = clock();
      
        std::unordered_map<int, int> intMap;
      
        for (int i = 0; i < loop; i++) {
          intMap[i] = i;
        }
      
        for (int i = 0; i < loop; i++) {
          int r = intMap[i];
          if (i % outputPoint == 0) {
            std::cout << r << ",";
          }
        }
      
        clock_t end = clock();
      
        const double intTime =
            static_cast<double>(end - start) / CLOCKS_PER_SEC * 1000.0;
        printf("\n\nint time %lf[ms]\n\n", intTime);
      
        // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      
        start = clock();
      
        std::unordered_map<std::string, int> strMap;
      
        for (int i = 0; i < loop; i++) {
          strMap[std::to_string(i)] = i;
        }
      
        for (int i = 0; i < loop; i++) {
          int r = strMap[std::to_string(i)];
          if (i % outputPoint == 0) {
            std::cout << r << ",";
          }
        }
      
        end = clock();
      
        const double strTime =
            static_cast<double>(end - start) / CLOCKS_PER_SEC * 1000.0;
        printf("\n\nstring time %lf[ms]\n\n", strTime);
      }
      

具体的なアニメーションの種類については、Actorを継承した子クラス(今回はShip)に任せる形にします。

ループ有無の管理方法

  • Animation構造体に、ループをするかのbool値isLoopを追加定義
    • テクスチャをアニメーションごとに追加する際、同時にbool値を設定する

上記の概要を踏まえたうえで、コードの解説をしていきます。

Actor

hクラスには、今のフレームで描画するアニメーションを格納するmCurrAnimState変数と、それのGetter/Setterを追加します。

Actor.h
    #pragma once
    #include <vector>
+   #include <unordered_map>
+   #include <string>
    #include "Math.h"
    class Actor
    {
        
        //略

    	State GetState() const { return mState; }
    	void SetState(State state) { mState = state; }
    
+    	int GetCurrAnimState() const { return mCurrAnimState; }
    
    	class Game* GetGame() { return mGame; }
    
    
    	// Add/remove components
    	void AddComponent(class Component* component);
    	void RemoveComponent(class Component* component);
+    protected:
+    	void SetCurrAnimState(int currAnimState) { mCurrAnimState = currAnimState; }
    
    private:
    	// Actor's state
    	State mState;
    
+    	int mCurrAnimState;
    
    	// Transform
    	Vector2 mPosition;
    	float mScale;
    }

cppクラスには、コンストラクタにアニメーション状態の管理変数の初期化処理0を追加します。
Actorへの変更は以上です。

Actor.cpp
 Actor::Actor(Game* game)
 	:mState(EActive)
+	, mCurrAnimState(0)
 	, mPosition(Vector2::Zero)
 	, mScale(1.0f)
 	, mRotation(0.0f)

Ship

hファイルには、アニメーション名を定義したenumを実装。

Ship.h
    private:
    	float mRightSpeed;
    	float mDownSpeed;
    
+    	enum ShipAnim {
+    		ship,
+    		walk,
+    		jump,
+    		punch
+    	};
    };

Ship.cppへの追加点としては以下の通りです。

  • AnimSpriteComponentRegisterAnimation()で追加するアニメーションとループの有無、アニメーションのキー(enum)を登録
  • 数字キーでアニメーションを切り替える処理
Ship.cpp
    Ship::Ship(Game* game)
    	:Actor(game)
    	, mRightSpeed(0.0f)
    	, mDownSpeed(0.0f)
    {
    	// Create an animated sprite component
    	AnimSpriteComponent* asc = new AnimSpriteComponent(this);
+    	std::vector<SDL_Texture*> ship = {
+    		game->GetTexture("Assets/Ship01.png"),
+    		game->GetTexture("Assets/Ship02.png"),
+    		game->GetTexture("Assets/Ship03.png"),
+    		game->GetTexture("Assets/Ship04.png"),
+    	};
+    
+    	asc->RegisterAnimation(ShipAnim::ship, ship);
+    
+    	std::vector<SDL_Texture*> walk = {
+    		game->GetTexture("Assets/Character01.png"),
+    		game->GetTexture("Assets/Character02.png"),
+    		game->GetTexture("Assets/Character03.png"),
+    		game->GetTexture("Assets/Character04.png"),
+    		game->GetTexture("Assets/Character05.png"),
+    		game->GetTexture("Assets/Character06.png"),
+    	};
+    	asc->RegisterAnimation(ShipAnim::walk, walk);
+    
+    	std::vector<SDL_Texture*> jump = {
+    		game->GetTexture("Assets/Character07.png"),
+    		game->GetTexture("Assets/Character08.png"),
+    		game->GetTexture("Assets/Character09.png"),
+    		game->GetTexture("Assets/Character10.png"),
+    		game->GetTexture("Assets/Character11.png"),
+    		game->GetTexture("Assets/Character12.png"),
+    		game->GetTexture("Assets/Character13.png"),
+    		game->GetTexture("Assets/Character14.png"),
+    		game->GetTexture("Assets/Character15.png"),
+    	};
+    	asc->RegisterAnimation(ShipAnim::jump, jump);
+    
+    	std::vector<SDL_Texture*> punch = {
+    		game->GetTexture("Assets/Character16.png"),
+    		game->GetTexture("Assets/Character17.png"),
+    		game->GetTexture("Assets/Character18.png"),
+    	};
+    	asc->RegisterAnimation(ShipAnim::punch, punch, false);
        
    }
    
    void Ship::ProcessKeyboard(const uint8_t* state)
    {
        mRightSpeed = 0.0f;
        mDownSpeed = 0.0f;
        // right/left
        if (state[SDL_SCANCODE_D])
        {
            mRightSpeed += 250.0f;
        }
        if (state[SDL_SCANCODE_A])
        {
            mRightSpeed -= 250.0f;
        }
        // up/down
        if (state[SDL_SCANCODE_S])
        {
            mDownSpeed += 300.0f;
        }
        if (state[SDL_SCANCODE_W])
        {
            mDownSpeed -= 300.0f;
        }
    
+    	if (state[SDL_SCANCODE_0])
+    	{
+    		SetCurrAnimState(ShipState::ship);
+    	}
+    	if (state[SDL_SCANCODE_1])
+    	{
+    		SetCurrAnimState(ShipState::walk);
+    	}
+    	if (state[SDL_SCANCODE_2])
+    	{
+    		SetCurrAnimState(ShipState::jump);
+    	}
+    	if (state[SDL_SCANCODE_3])
+    	{
+    		SetCurrAnimState(ShipState::punch);
+    	}
    }

AnimSpriteComponent

hファイルには前述したAnimation構造体と、登録されたアニメーションを管理するmAnimations配列、配列にアニメーションを登録するRegisterAnimation()を実装。
また、アニメーション切替りを検知するため、mOldAnimStateも追加しています。

AnimSpriteComponent.h
+   #include <unordered_map>

    class AnimSpriteComponent : public SpriteComponent
    {
    public:
    	AnimSpriteComponent(class Actor* owner, int drawOrder = 100);
    	// Update animation every frame (overridden from component)
    	void Update(float deltaTime) override;
    	// Set the textures used for animation
    	void SetAnimTextures(const std::vector<SDL_Texture*>& textures);
    
+    	// 課題2
+    	void RegisterAnimation(const int animStateKey, const std::vector<SDL_Texture*>& textures, const bool isLoop = true);
    
    	// Set/get the animation FPS
    	float GetAnimFPS() const { return mAnimFPS; }
    	void SetAnimFPS(float fps) { mAnimFPS = fps; }
    private:
+    	//課題2 アニメーション情報
+    	struct Animation {
+    		Animation() : first(0), last(0), isLoop(true) {};
+    		Animation(const int head, const int tail, const bool isLoop = true)
+    			:first(head)
+    			, last(tail)
+    			, isLoop(isLoop) {}
+    
+    		int first;
+    		int last;
+    		bool isLoop;
+    
+    		int CalcTotalFrames() const { return last - first + 1; }
+    	};
    
    	// All textures in the animation
    	std::vector<SDL_Texture*> mAnimTextures;
    
+    	//課題2
+    	int mOldAnimState;					// 1F前のownerのanimStateを管理
+    	std::unordered_map<int, Animation> mAnimations; // 各Animの最初と最後のframeのindexを管理する
    
    	// Current frame displayed
    	float mCurrFrame;
    
        //以下略

cppにはmOldAnimStateの初期化とRegisterAnimation()の実装、そしてUpdate()に複数アニメーションに対応した処理を追加します。
具体的な処理内容は、各処理にあるコメントアウトを参照してください。

AnimSpriteComponent.cpp
    #include "AnimSpriteComponent.h"
    #include "Math.h"
+   #include "Actor.h"
+   #include <iterator>
    
    AnimSpriteComponent::AnimSpriteComponent(Actor* owner, int drawOrder)
    	:SpriteComponent(owner, drawOrder)
    	, mCurrFrame(0.0f)
    	, mAnimFPS(24.0f)
+    	, mOldAnimState(0)
    {
    }

    void AnimSpriteComponent::Update(float deltaTime)
    {
    	SpriteComponent::Update(deltaTime);
    	if (mAnimTextures.size() > 0)
    	{
    		// Update the current frame based on frame rate
    		// and delta time
    		mCurrFrame += mAnimFPS * deltaTime;
    
+    		//課題2
+    		int currAnimState = mOwner->GetCurrAnimState();
+    		Animation currentAnim = mAnimations[currAnimState];
+    
+    		//animが切り替わった場合、描画するtextureをリセット
+    		if (mOldAnimState != currAnimState)
+    		{
+    			mCurrFrame = static_cast<float>(currentAnim.first);
+    		}
+
+    		//ループしないアニメーションの場合、ラップ調整されるされる前に最後のframeを描画するようにする
+    		if (!currentAnim.isLoop && mCurrFrame > currentAnim.last)
+    		{
+    			mCurrFrame = static_cast<float>(currentAnim.last);
+    		}
+    
    		// Wrap current frame if needed
+    		//課題2
+    		// 「mCurrFrame > currentAnim.last」だと、キャスト時に小数点切り捨てとなり、1つ手前のアニメーションがチラチラ描画される
+    		while (mCurrFrame >= currentAnim.last + 1)
    		{
+    			//課題2 描画対象のアニメーションの枚数分減算するようにする
+    			mCurrFrame -= currentAnim.CalcTotalFrames();
    		}
    
    		// Set the current texture
    		SetTexture(mAnimTextures[static_cast<int>(mCurrFrame)]);
+    
+    		mOldAnimState = currAnimState;
    	}
    }

+    //デフォルト引数はhファイルにのみ書く
+    //cppにも書くと:コンパイルエラー
+    //cppにのみ書くと:構文エラー
+    void AnimSpriteComponent::RegisterAnimation(const int animStateKey, const std::vector<SDL_Texture*>& textures, const bool isLoop)
+    {
+    	mAnimations[animStateKey] = Animation(
+    		mAnimTextures.size(),
+    		mAnimTextures.size() + textures.size() - 1,
+    		isLoop
+    	);
+    
+    	std::vector<SDL_Texture*> result = mAnimTextures;
+    	result.reserve(mAnimTextures.size() + textures.size()); // 事前に追加分のメモリを確保することで効率化
+    	std::copy(textures.begin(), textures.end(), std::back_inserter(result));
+    	SetAnimTextures(result);
+    }

課題2.3

タイルセットを描画するTileMapComponentを実装しよう

<!-- :::message alert
2.3の実装コードリンク(後で消す後で消して後で消すように!!!)
::: -->
2.3での変更点は、TileMapComponentを実装したこと以外では、GameでTileMapComponentを呼び出す処理を追記しただけのため、TileMapComponentについて詳細に説明したいと思います。

TileMapComponent

初めに簡単な用語の説明から

  • タイルセット
    • 同じ寸法のテクスチャがタイルのように敷き詰められた画像

    【画像引用元】
    https://rpgmakerofficial.com/product/trinity/guide/002_004.html

  • タイルマップ
    • 本記事では、タイルマップとは「タイルセットを基に描画された背景」と「使用するタイルとタイルの使用位置が記されたcsvファイル」のことを指して使用します

TileMapComponent特有となる処理は以下の通りです。

  • タイルセットの設定
  • タイルマップ(csv)の読込
    • csvには-1以上の整数があり、-1は何も描画せず、0以上の数字は、画像左上のタイルを0番とした場合の、描画するタイル番号を表す
      • (1は0の右、2は1の右...となり、右端の次のタイルは次の行の左端を表す)
  • タイルマップの描画
    タイルマップはint型の2次元配列で管理します。
    タイルセットは専用の構造体を作成し、描画に必要な以下の情報を管理します。
  • タイルセット画像(テクスチャ)
  • 画像のサイズ(縦横)
  • タイルの行数、列数
    画像のサイズはSDLの関数を使い取得しますが、タイルの行列数は目視で数えて設定することになります。
    上記に加えて画面スクリーンのサイズも設定します。タイル1枚1枚のサイズを行数列数を使い自動で求めるためです。(スクロールがある場合、別の方法を導入する必要があると思われます)
    画面サイズのセット関数はBGSpriteComponentから流用します。
TileMapComponent.h
    #pragma once
    #include "SpriteComponent.h"
    #include "SDL/SDL.h"
    #include <vector>
    #include "Math.h"
    #include <string>
    
    class TileMapComponent :
    	public SpriteComponent
    {
    public:
    	TileMapComponent(class Actor* owner, int drawOrder = 20);
    	void Update(float deltaTime) override;
    	void Draw(SDL_Renderer* renderer) override;
    	void SetScreenSize(const Vector2& size) { mScreenSize = size; }
    	void SetTileSet(
    		SDL_Texture* texture,
    		const int rows,
    		const int columns
    	);
    	void LoadCSV(const std::string& fileName);
    private:
    	//タイルセットの情報
    	struct TileSet
    	{
    		SDL_Texture* mTexture;
    		int width, height;
    		int rows = 1, columns = 1;//0での除算を回避
    	};
    	TileSet mTileSet;
    
    	Vector2 mScreenSize;
    	std::vector<std::vector<int>> mTileMap;
    };


次に、一番大事となるcppファイルの説明に移ります。関数1つごとに説明します。
include項目は以下の通りです、コンストラクタは他のコンポーネントのコンストラクタ処理の見よう見まねです。

TileMapComponent.cpp
    #include "TileMapComponent.h"
    #include <vector>
    #include <fstream>
    #include <sstream>
    #include <iostream>
    
    TileMapComponent::TileMapComponent(Actor* owner, int drawOrder)
    	:SpriteComponent(owner, drawOrder)
    	, mTileSet()
    {
    }

次にSetTileSet()です。やってることはほぼ名前の通りです。
タイルセット画像とタイルの列数と行数を受け取り、mTileSetに代入後、SDL_QueryTextureを使用し、画像の縦横を代入します。
なお、元々SpriteComponentが持っていたmTextureに画像は代入しません。AnimSpliteComponentがmTextureとは別の変数でテスクチャを管理していた前例があるのと、mTileSetにデータを一元管理した方がわかりやすいと考えたためです。

SetTileSet()_TileMapComponent.cpp
    void TileMapComponent::SetTileSet(SDL_Texture* tileSetTexture, const int rows, const int columns)
    {
    	mTileSet.mTexture = tileSetTexture;
    	mTileSet.columns = columns;
    	mTileSet.rows = rows;
    	SDL_QueryTexture(tileSetTexture, NULL, NULL, &mTileSet.width, &mTileSet.height);
    }

Update()には現状処理を書いていません。形だけです。
もしスクロールないしカメラ移動に対応させたい場合は、ここに処理を書くことになるのでしょうね。

Update()_TileMapComponent.cpp
    void TileMapComponent::Update(float deltaTime)
    {
    	//もしスクロールに対応させたいなら
    }

次にLoadCSV()です。大体はコメントアウトで補足してる通りですが、文章で流れを説明すると以下の通りです

  1. csvファイルを読み込み
  2. 見つからなかった場合のエラー処理(仮置き)
  3. csvを1行ずつ読み込み
  4. 1行からコンマ区切りでタイル番号を1つずつ読み込み(最後の行まで繰り返し)
  5. 読み込んだタイルを1次元配列に格納(行末まで繰り返し)
  6. 行末まで読み終わったら、1次元配列をmTileMapに格納

変数宣言がかなり多いですが、具体的な処理は「csvファイル読み込み」「タイルを1枚ずつpush」「タイルを1行ずつpush」と実は3行だけです。
エラー処理はGameでの類似処理をちょっとだけ真似ているだけなので、本格的なエラー処理とは程遠いかもしれません。

LoadCSV()_TileMapComponent.cpp
    void TileMapComponent::LoadCSV(const std::string& fileName)
    {
    	//csvファイル読み込み
    	std::ifstream tileMap(fileName);//「tileMap」は変数名
    
    	//ファイルが見つからなかった場合のエラー処理
    	if (!tileMap)
    	{
    		SDL_Log("TileMapComponent:csvファイルが見つかりませんでした %s", fileName.c_str());
    		return;//要検討。ゲームを落とす処理がベスト?
    	}
    
    	//csvファイル1行をtilesRowStrに入れる。それを最後の行まで繰り返す
    	std::string tilesRowStr;
    	while (std::getline(tileMap, tilesRowStr))
    	{
    		std::vector<int> addTilesRow;
    		std::string tileStr;
    		std::istringstream iss(tilesRowStr);//入力専用の文字列ストリーム
    
    		while (std::getline(iss, tileStr, ','))
    		{
    			addTilesRow.push_back(std::stoi(tileStr));//String TO Int );
    		}
    
    		mTileMap.push_back(addTilesRow);
    	}
    }

最後に、一番大事なDraw()の説明です。
mTileMapから要素を1つずつ取り出し、タイルを描画していきます。タイル番号が-1の場合は処理をスキップします。
タイル描画にあたり必要になるのが、srcRectdstRectの2つです(gemini曰く、destination:目的地の略)。

srcRect
src(ソース、元)のRect(矩形)
描画する画像(タイルセット)の一部分(タイル)だけを、位置と縦横幅をもとにくり抜いて描画するために使用

dstRect
dst(目的地)のRect(矩形)
目的地(プレイ画面)のどの位置にどれくらいの縦横幅で描画するのかを指定するために使用

2つのrectの位置とサイズの求め方は以下の通り

srcRect dstRect
サイズ w 画像の横幅 / タイルの列数 スクリーン横幅 / タイルマップの列数
h 画像の縦幅 / タイルの行数 スクリーン横幅 / タイルマップの行数
位置 x タイル番号 % タイルの行数(余り) 列番号 * dstRect.w
y タイル番号 / タイルの行数 行番号 * dstRect.w

列番号と行番号は、要は「何行目(列目)にあるか」(0始まり)です。コードではrouIdx, columnIdxで求めてます。

Draw()では上記2つの矩形を、二次元配列のfor文で計算すると同時に描画する、という処理を1つずつ行うのが仕事です。

なお、この実装はカメラの移動とズームは考慮していません。
仮に移動とズームを実装するとして、srcRectの処理は変わらないため、dstRectの処理を書き替える必要があるでしょう。

Draw()_TileMapComponent.cpp
    void TileMapComponent::Draw(SDL_Renderer* renderer)
    {
    	SDL_Rect srcRect;
    	//タイルは全て同じ大きさなので予め設定
    	srcRect.w = static_cast<int>(mTileSet.width / mTileSet.columns);
    	srcRect.h = static_cast<int>(mTileSet.height / mTileSet.rows);
    
    	int rowIdx = 0;
    	for (auto& tileMapRow : mTileMap)
    	{
    		int columnIdx = 0;
    		for (auto& tile : tileMapRow)
    		{
    			if (tile != -1)
    			{
    				// 描画したいタイルマップ上のタイル位置
    				srcRect.x = static_cast<int>((tile % mTileSet.columns) * srcRect.w);
    				srcRect.y = static_cast<int>((tile / mTileSet.columns) * srcRect.h);
    
    				SDL_Rect dstRect;
    				// タイルの描画サイズ
    				dstRect.w = static_cast<int>(mScreenSize.x / tileMapRow.size());
    				dstRect.h = static_cast<int>(mScreenSize.y / mTileMap.size());
    				// タイルの描画位置
    				dstRect.x = static_cast<int>(columnIdx * dstRect.w);
    				dstRect.y = static_cast<int>(rowIdx * dstRect.h);
    
    				SDL_RenderCopy(renderer,
    					mTexture,
    					&srcRect,
    					&dstRect
    				);
    			}
    			columnIdx++;
    		}
    		rowIdx++;
    	}
    }

以上でTileMapComponentの実装は終わりです。
カメラ度外視とはいえ、かなり苦戦しました。

Game

最後にGame.cppですが、ここではTileMapComponentをインスタンス化して登録しているだけです。
一応、スクリーンサイズがべた書きだったのが気になったので、変数化したものを使いまわす処理に書き換えてます。

Game.cpp
    #include "SpriteComponent.h"
    #include "Ship.h"
    #include "BGSpriteComponent.h"
    #include "TileMapComponent.h"

    void Game::LoadData()
    {
    	// Create player's ship
    	mShip = new Ship(this);
    	mShip->SetPosition(Vector2(100.0f, 384.0f));
    	mShip->SetScale(1.5f);
    
+    	Vector2 screen = Vector2(1024.0f, 768.0f);
    
    	// Create actor for the background (this doesn't need a subclass)
    	Actor* temp = new Actor(this);
    	temp->SetPosition(Vector2(512.0f, 384.0f));
    	// Create the "far back" background
    	BGSpriteComponent* bg = new BGSpriteComponent(temp);
+    	bg->SetScreenSize(screen);
    	std::vector<SDL_Texture*> bgtexs = {
    		GetTexture("Assets/Farback01.png"),
    		GetTexture("Assets/Farback02.png")
    	};
    	bg->SetBGTextures(bgtexs);
    	bg->SetScrollSpeed(-100.0f);
    	// Create the closer background
    	bg = new BGSpriteComponent(temp, 50);
+    	bg->SetScreenSize(screen);
    	bgtexs = {
    		GetTexture("Assets/Stars.png"),
    		GetTexture("Assets/Stars.png")
    	};
    	bg->SetBGTextures(bgtexs);
    	bg->SetScrollSpeed(-200.0f);
    
+    	TileMapComponent* tm = new TileMapComponent(temp);
+    	tm->SetScreenSize(screen);
+    	SDL_Texture* tstex = GetTexture("Assets/Tiles.png");
+    	tm->SetTileSet(tstex, 24, 8);
+    
+    	tm->LoadCSV("Assets/MapLayer1.csv");
+
+       //他のタイルマップ
+    	//tm->LoadCSV("Assets/MapLayer2.csv");
+    	//tm->LoadCSV("Assets/MapLayer3.csv");
    }

これで課題2.3は完了です!

まとめ

実装して動かしたのが以下の動画です。

他のCSVファイルでどんな背景が描画されるかは、ぜひご自身の目で確かめてみてください。

次はChapter3の解説記事でお会いしましょう。

おまけ

完成に至るまでにnotionで書き殴りでしていた内容をここに残しておきます。
完成版とはほど多い内容が書いてあったり、そもそもメモをし損ねた内容もあるため、実装の参考にはせず、「へ~途中こんなことも考えてたんだ」程度に眺めてください。
notionからそのままコピペしてきただけなので、読みずらいのはご容赦ください。

書き殴り思考メモ

2.2

  • AnimSpriteComponentを複数アニメーションに対応させる

    AnimSpriteComponent

    メンバ変数・仕様

    • 既存のmAnimTexturesに複数アニメーションを全て入れる

    • mAnimBreakPointで、mAnimTexturesにある複数アニメーションの切り替わりポイント(配列のいくつからそのアニメーションが終わる)を管理

      | Anim種類 | state番号
      (mAnimBreakpointのindex) | 開始index(mAnimTextureのindex) |
      | --- | --- | --- |
      | walk | 0 | 5 |
      | jump | 1 | 14 |
      | punch | 2 | 17 |

      • 各stateにAnimStateの枚数 を管理する場合?

        https://qiita.com/haruyama480/items/961f5f14f4cf4787cce9

        特定要素までの合計はこれを使えばよさそう?

        | Anim種類 | state番号
        mSumEachAnimFramesのindex) | 各Animの枚数 |
        | --- | --- | --- |
        | walk | 0 | 6 |
        | jump | 1 | 9 |
        | punch | 2 | 3 |

        • 仮にjumpまでの枚数を求めると、値は15になる
          • -1すればそのanimStateの末尾になる
      • アニメーションが1つだけのとき

      | Anim種類 | state番号
      (mAnimBreakpointのindex) | 開始index(mAnimTextureのindex) |
      | --- | --- | --- |
      | only | 0 | mAnimTexture - 1 |

    • mAnimStateで、どのアニメーションを描画させるのかを管理する

    ChangeAnimState(int animState)

    • ~~0 <= animState < mAnimBrakPoint.lengthの間でなければいけない(外の値をぶち込まれた時にどうするかは考え物。いったんifで無視するだけにする?)~~
    • 上記を満たせば、iAnimStateを変える
    • 1F前のowner.animStateと現在のowner.animStateが違っていたらflameをリセットする
      • 新しいanimStateの最初のframeに書き換える
      • mCurrFrame = totalFramesToCurrentAnimState - mSumEachAnimFrames[mOwner→GetAnimState]

    Update

    • 現状の処理

      if (mAnimTextures.size() > 0)
      {
      	// Update the current frame based on frame rate
      	// and delta time
      	mCurrFrame += mAnimFPS * deltaTime;
      
      	// Wrap current frame if needed
      	while (mCurrFrame >= mAnimTextures.size())
      	{
      		mCurrFrame -= mAnimTextures.size();
      	}
      
      	// Set the current texture
      	SetTexture(mAnimTextures[static_cast<int>(mCurrFrame)]);
      }
      
    • ループ処理の動作を考える

      • mCurrFrameの補正

      • breakpointの場合

        • ループ条件
          • mCurrFrame >= mAnimTextures.size()
            • indexがtextureの外側(=テクスチャの総数)より多い数にならないようにする
              • この計算の場合、while文を実行する最小条件がtextureの枚数となる=(最小条件で減算すると0になる)mCurrFrameが0未満になることはない
          • ???
            • 指定したアニメーションの範囲内になっているようにする
            • mCurrFrame >= mAnimBreakpoint[mAnimState] + 1
        • mCurrFrame -= mAnimTextures.size();の変更
          • mCurrFrame -= 現在のAnimStateのbreakpoint - (mAnimState-1)のAnimStateのbrakpoint(mAnimStateが0なら0)
          • mCurrFrame -= mAnimBreakPoint[mAnimState] + 1 - mAnimBrakPoint[mAminState-1]
            • +1は補正、frameが4枚(Shipも4枚)なら、

              while(mCurrFrame >= mAnimTextures.size()) でmCurrentFrameが4になる=mTexturesの外側をさすことがなくなる

            • mAnimState==0なら、0

            • 三項演算子で分ける
              int animHeadFrame = mAnimState == 0 ? 0 : mAnimBreakpoint[mAnimState - 1];

          • mCurrFrame -= mAnimBreakpoint[mAnimState] - animHeadFrame;
            • ※アニメーションが1つしかない場合、
              mCurrframe -= mAnimBreakpoint[mAnimState] - 0;
            • mAnimBreakpoint[mAnimState] = mAnimTexture -
      • eachAnimの場合

        • int totalFramesToCurrentAnimState
          = std::reduce(std::begin(mSumEachAnimFrames), std::next(std::begin(mSumEachAnimFrames), mAnimState));
        • コンパイラを最新版C++20に変更しないといけない
        • ループ条件
          • mCurrFrame >= mAnimTextures.size()
            • indexがtextureの外側(=テクスチャの総数)より多い数にならないようにする
              • この計算の場合、while文を実行する最小条件がtextureの枚数となる=(最小条件で減算すると0になる)mCurrFrameが0未満になることはない
          • ???
            • 指定したアニメーションの範囲内になっているようにする
            • mCurrFrame >= totalFramesToCurrentAnimState
        • mCurrFrame -= mAnimTextures.size();の変更
          • mCurrFrame -= totalFramesToCurrentAnimState
          • ※アニメーションが1つしかない場合、 totalFramesToCurrentAnimState = mAnimTexture.size()になる

    • [ ]

    //デフォルト引数はhファイルにのみ書く

    //こっちにも書くと:コンパイルエラー( https://qiita.com/yut-nagase/items/29d0fc0984e6dbace85e

    //こっちにのみ書くと:構文エラー

    Actor

    • コンストラクタで、mSumEachAnimFramesmAnimStateを管理する
      • mAnimStateは各Actorクラスでenumで管理でもすればいいと思う
      • mAnimStateの初期値は0のため、初期化は不要?
      • mSumEachAnimFrames
      • mAnimStateを持たせる
        • そもそもActorからcomponentにアクセスできなかった
        • mAnimStateをAnimSpriteComponentから参照してもらう
  • ループしないアニメーションをサポートする

2.3

流れ

<aside>
⚠️

懸念リスト

  • タイルの列数をどこで管理する?
    • getline()でできたvectorのsizeで管理?
    • 「タイルの列数は常にN個である」という前提のもと、別の場所で管理する?
  • 1タイル当たりの縦横幅さはどこで管理する?
    • タイル画像の縦横÷列数(行数)で求められはするが…
      </aside>

Game

  • LoadDataでTileMapのインスタンスを生成

  • 使用するタイルマップ画像を渡す(SetTileMap )

  • 使用するCSVファイルを渡す?(LoadCSV)

    • CSVファイルを読み込む方法を調査する
    //採取的に管理する描画対象のタイルデータ
    vector<vector<int>> tileMapData;
    //csvファイル読み込み
    std::ifstream mapLayer("MapLayerN.csv");//「mapLayer」は変数名(つまりなんでもいい9
    
    //ファイルが見つからなかった場合のエラー処理
    if(!mapLayer)
    {
    	//ファイル読み込みが失敗したときの例外処理
    	std::cerr << "TileMapComponent:csvファイルが見つかりませんでした" << std::endl;
    	return 1;//要検討。ゲームを落とす処理にする
    }
    
    //csvファイル1行1行を管理
    string str;
    
    //csvファイル1行をstrに入れる。それを最後の行まで繰り返す
    while(getline(mapLayer**, str))
    {**
    	std::istringstream iss(line);//入力専用の文字列ストリーム
    	std::vector<int> values;
    	
    **}**
    

    採用版

    void LoadCSV(const std::string& fileName)
    {
    	//採取的に管理する描画対象のタイルデータ
    	std::vector<std::vector<int>> tileMapData;
    
    	//csvファイル読み込み
    	std::ifstream tileMap(fileName);//「map」は変数名(つまりなんでもいい9
    	//std::ifstream mapLayer("MapLayerN.csv");//「map」は変数名(つまりなんでもいい9
    
    	//ファイルが見つからなかった場合のエラー処理
    	if (!tileMap)
    	{
    		SDL_Log("TileMapComponent:csvファイルが見つかりませんでした %s", fileName.c_str());
    		//return nullptr;
    		//return 1;//要検討。ゲームを落とす処理にする
    	}
    
    	//csvファイル1行1行を管理
    	std::string rowStr;
    
    	//csvファイル1行をstrに入れる。それを最後の行まで繰り返す
    	while (std::getline(tileMap, rowStr))
    	{
    		std::vector<int> rowTiles;
    		std::string tile;
    		std::istringstream iss(rowStr);//入力専用の文字列ストリーム
    
    		while (std::getline(iss, tile, ','))
    		{
    			int v = std::stoi(tile);
    			rowTiles.push_back(v);
    		}
    
    		tileMapData.push_back(rowTiles);
    	}
    }
    

TileMap

    • 読み込んだCSVから、どうやってバラバラのタイルを1つの画像として出力するのか、処理の流れを考える
      • BackGroundのように、描画対象のタイルの番号と位置を管理するvectorで管理すればいいか?

        //元のmTextureは、大本のタイル画像を管理するために使う?
        
        struct TileMapTexture
        {
        	SDL_Texture* mTexture;
        	Vector2 mOffset;
        	SDL_RECT srcrect// タイルのどこを描画するかを管理する?tileNuimberと役割被る
        	int tileNumber;//描画対象のタイルの番号を管理。-1を設定した場合は描画処理をスキップ
        };
        //mOffsetはCSVを読み込むときに設定する?
        //1枚目は0,0。以降、xは+OO, yは〇枚目ごとに+OO、とする?
        std::vector<TileMapTexture> mTileMapTextures;
        
      • (もしかしたらタイル一枚ごとにComponentを持つ可能性も?)

処理

Game

???

TileMap

  • Draw

    // BGSpriteからコピペしているため、部分部分注意
    for (auto& bg : mBGTextures/*タイル1枚1枚*/)
    {
    	if(csvから取ったタイル番号 != -1){//or > -1
    		SDL_Rect srcRect;
    		// Assume screen size dimensions
    		dstRect.w = static_cast<int>(src画像の横幅 / src画像のタイルの横幅);
    		dstRect.h = static_cast<int>(src画像の縦幅 / src画像のタイルの縦幅);
    		// Center the rectangle around the position of the owner
    		dstRect.x = static_cast<int>
    		(
    			csvから取ったタイル番号 % src画像のタイルの列数 * src画像のタイルの横幅
    		);
    		dstRect.y = static_cast<int>
    		(
    			csvから取ったタイル番号 / src画像のタイルの列数 * src画像のタイルの縦幅
    		);
    		
    		SDL_Rect dstRect;//dst=destination(目的地, 宛先)
    		// Assume screen size dimensions
    		dstRect.w = static_cast<int>(描画するタイルの横幅);
    		dstRect.h = static_cast<int>(描画するタイルの縦幅);
    		// Center the rectangle around the position of the owner
    		dstRect.x = static_cast<int>
    		(
    			描画するタイルの列数(0 to 最大列数-1) * 描画するタイルの横幅
    		);
    		dstRect.y = static_cast<int>
    		(
    			描画するタイルの行数(0 to 最大行数-1) * 描画するタイルの縦幅
    		);
    	
    		// Draw this background
    		SDL_RenderCopy(renderer,
    			bg.mTexture,
    			/*
    			・bg.mTextureのように、各tilemaptextureが描画するtextureを持つパターン
    			・component自身がsrcとなる画像を持ち、各tileMapTextureが引用するパターン
    			二つ考えられる
    			*/
    			nullptr,
    			&dstRect
    		);
    	}
    }
    
    • 共通化(TileMapCompoentが持つべきもの)
      • src画像(タイルセット)の
        • 横幅
        • 縦幅
      • src画像のタイルの
        • 横幅
          • ~~src画像の横幅 ÷二次元配列vector内のvectorのsize
            でも求められそうではある~~
          • src画像の横幅 ÷列数
            でも求められそうではある
        • 縦幅
          • ~~src画像の縦幅 ÷二次元配列vectorのsize
            でも求められそうではある~~
          • src画像の縦幅 ÷行数
            でも求められそうではある
      • src画像のタイルの列数
        • 二次元配列vectorの中のvectorのsize⇒これはタイルマップ(実際に背景として並べるタイルの枚数)なので違う
      • src画像のタイルの行数
      • 描画するタイルの
        • 列数
          • csvの1行にある要素数
            二次元配列vectorの中のvectorのsize
        • 行数
          • csvの行数
            二次元配列vectorのsize
        • (今回の課題ではないだろうが、タイル2枚使う、などの変則に合わせて、個別で持たせるようにした方がよいか?)
    • 個別(各TileMapTextureが持つべきもの)
      • csvから取ったタイル番号
      • タイルの描画位置
        • x
          • タイル番号 % 描画するタイルの列数 * 描画するタイルの横幅
        • y
          • タイル番号 / 描画するタイルの列数 * 描画するタイルの縦幅
      • 描画するタイルの
        • 列数
          • csvの1行にある要素数
            二次元配列vectorの中のvectorのsize
        • 行数
          • csvの行数
            二次元配列vectorのsize

参考ページ

https://qiita.com/shirosuke_93/items/d5d068bb15c8e8817c34

https://qiita.com/yohm/items/91c5180d9c6d427b22d0

https://cvtech.cc/readcsv/

https://qiita.com/shirosuke_93/items/d5d068bb15c8e8817c34

https://qiita.com/aobeee/items/e61870e6b6ac496399c0

Discussion