【ゲームプログラミングC++】Chapter2課題を解いてみた
課題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); }
- 処理速度向上と、アニメーションの種類を分かりやすく表現するためにenumを採用しています
具体的なアニメーションの種類については、Actorを継承した子クラス(今回はShip)に任せる形にします。
ループ有無の管理方法
-
Animation
構造体に、ループをするかのbool値isLoop
を追加定義- テクスチャをアニメーションごとに追加する際、同時にbool値を設定する
上記の概要を踏まえたうえで、コードの解説をしていきます。
Actor
hクラスには、今のフレームで描画するアニメーションを格納するmCurrAnimState
変数と、それのGetter/Setterを追加します。
#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::Actor(Game* game)
:mState(EActive)
+ , mCurrAnimState(0)
, mPosition(Vector2::Zero)
, mScale(1.0f)
, mRotation(0.0f)
Ship
hファイルには、アニメーション名を定義したenumを実装。
private:
float mRightSpeed;
float mDownSpeed;
+ enum ShipAnim {
+ ship,
+ walk,
+ jump,
+ punch
+ };
};
Ship.cppへの追加点としては以下の通りです。
-
AnimSpriteComponent
のRegisterAnimation()
で追加するアニメーションとループの有無、アニメーションのキー(enum)を登録 - 数字キーでアニメーションを切り替える処理
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
も追加しています。
+ #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()
に複数アニメーションに対応した処理を追加します。
具体的な処理内容は、各処理にあるコメントアウトを参照してください。
#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の右...となり、右端の次のタイルは次の行の左端を表す)
- csvには-1以上の整数があり、-1は何も描画せず、0以上の数字は、画像左上のタイルを0番とした場合の、描画するタイル番号を表す
- タイルマップの描画
タイルマップはint型の2次元配列で管理します。
タイルセットは専用の構造体を作成し、描画に必要な以下の情報を管理します。 - タイルセット画像(テクスチャ)
- 画像のサイズ(縦横)
- タイルの行数、列数
画像のサイズはSDLの関数を使い取得しますが、タイルの行列数は目視で数えて設定することになります。
上記に加えて画面スクリーンのサイズも設定します。タイル1枚1枚のサイズを行数列数を使い自動で求めるためです。(スクロールがある場合、別の方法を導入する必要があると思われます)
画面サイズのセット関数はBGSpriteComponentから流用します。
#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項目は以下の通りです、コンストラクタは他のコンポーネントのコンストラクタ処理の見よう見まねです。
#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にデータを一元管理した方がわかりやすいと考えたためです。
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()
には現状処理を書いていません。形だけです。
もしスクロールないしカメラ移動に対応させたい場合は、ここに処理を書くことになるのでしょうね。
void TileMapComponent::Update(float deltaTime)
{
//もしスクロールに対応させたいなら
}
次にLoadCSV()
です。大体はコメントアウトで補足してる通りですが、文章で流れを説明すると以下の通りです
- csvファイルを読み込み
- 見つからなかった場合のエラー処理(仮置き)
- csvを1行ずつ読み込み
- 1行からコンマ区切りでタイル番号を1つずつ読み込み(最後の行まで繰り返し)
- 読み込んだタイルを1次元配列に格納(行末まで繰り返し)
- 行末まで読み終わったら、1次元配列をmTileMapに格納
変数宣言がかなり多いですが、具体的な処理は「csvファイル読み込み」「タイルを1枚ずつpush」「タイルを1行ずつpush」と実は3行だけです。
エラー処理はGameでの類似処理をちょっとだけ真似ているだけなので、本格的なエラー処理とは程遠いかもしれません。
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の場合は処理をスキップします。
タイル描画にあたり必要になるのが、srcRect
とdstRect
の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の処理を書き替える必要があるでしょう。
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をインスタンス化して登録しているだけです。
一応、スクリーンサイズがべた書きだったのが気になったので、変数化したものを使いまわす処理に書き換えてます。
#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を複数アニメーションに対応させる
-
既存の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の末尾になる
- 仮にjumpまでの枚数を求めると、値は15になる
-
アニメーションが1つだけのとき
| Anim種類 | state番号
(mAnimBreakpointのindex) | 開始index(mAnimTextureのindex) |
| --- | --- | --- |
| only | 0 | mAnimTexture - 1 | -
mAnimStateで、どのアニメーションを描画させるのかを管理する
-
~~0 <= animState < mAnimBrakPoint.length
の間でなければいけない(外の値をぶち込まれた時にどうするかは考え物。いったんifで無視するだけにする?)~~ 上記を満たせば、iAnimStateを変える- 1F前のowner.animStateと現在のowner.animStateが違っていたらflameをリセットする
- 新しいanimStateの最初のframeに書き換える
-
-
現状の処理
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未満になることはない
- indexがtextureの外側(=テクスチャの総数)より多い数にならないようにする
- ???
- 指定したアニメーションの範囲内になっているようにする
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 -
- ※アニメーションが1つしかない場合、
- ループ条件
-
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未満になることはない
- indexがtextureの外側(=テクスチャの総数)より多い数にならないようにする
- ???
- 指定したアニメーションの範囲内になっているようにする
mCurrFrame >= totalFramesToCurrentAnimState
-
-
mCurrFrame -= mAnimTextures.size();
の変更mCurrFrame -= totalFramesToCurrentAnimState
-
-
-
-
[ ]
//デフォルト引数はhファイルにのみ書く
//こっちにも書くと:コンパイルエラー( https://qiita.com/yut-nagase/items/29d0fc0984e6dbace85e )
//こっちにのみ書くと:構文エラー
- コンストラクタで、
mSumEachAnimFrames
とmAnimState
を管理する- 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を持つ可能性も?)
-
-
読み込んだCSVから、どうやってバラバラのタイルを1つの画像として出力するのか、処理の流れを考える
処理
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の1行にある要素数
-
行数
- csvの行数
=二次元配列vectorのsize
- csvの行数
- (今回の課題ではないだろうが、タイル2枚使う、などの変則に合わせて、個別で持たせるようにした方がよいか?)
-
列数
- src画像(タイルセット)の
- 個別(各TileMapTextureが持つべきもの)
- csvから取ったタイル番号
- タイルの描画位置
-
x
-
タイル番号
%描画するタイルの列数
*描画するタイルの横幅
-
-
y
-
タイル番号
/描画するタイルの列数
*描画するタイルの縦幅
-
-
x
-
描画するタイルの-
列数csvの1行にある要素数
=二次元配列vectorの中のvectorのsize
-
行数csvの行数
=二次元配列vectorのsize
-
- 共通化(TileMapCompoentが持つべきもの)
参考ページ
https://qiita.com/shirosuke_93/items/d5d068bb15c8e8817c34
https://qiita.com/yohm/items/91c5180d9c6d427b22d0
Discussion