DXライブラリで、トランジション(画面切替時演出)を作ってみる
トランジション(画面切り替え時演出)とは
トランジションとは何かというと、シーンの切り替え時に発生する演出の事です。
今回の実装の結果の映像は
https://twitter.com/CTsuchinoko/status/1736821018315174130
を見てください。
シーン遷移と言うと
switch文を使わないゲーム状態遷移inC++
でも書きましたが、
タイトル画面→ゲームプレイ前→ゲーム本編プレイ→ゲームオーバー→最初へもどる
みたいな流れを作った時に、すぐに次のシーンに遷移してはちょっと寂しいですね。
分かりやすい演出で言うと、シーンからシーンへのフェードイン・フェードアウトみたいな感じです。
この辺があるのとないのとで、印象がガラッと変わると思います。
ちなみにパワーポイントの画面切り替え演出だとこんなにあるみたいです。3D的な演出も入っていて楽しいですね。
ちなみにちょうどいいアイデア帳があったので、リンクを貼らせていただきます。
https://gameanimation.info/archives/2615
とはいえ、多彩すぎてDXライブラリでやるのはちょっと大変そうですね。Unityでも大変そう。
と言う事で、今回は基本的なトランジションを作るところまでやってみましょう。
ひとまず
-
フェード:Fade
-
プッシュ:Push
-
ワイプ(フェードと似てるがX座標でグラデーション):Wipe
-
コーム(短冊状に上下に移動):Strip
-
円形(その他図形)(アイリスイン・アウトみたいな感じ):Iris
-
ディゾルヴ(画面タイルがランダムに切り替わる):Tile
-
はがれおちる(画面タイルがランダムに落ちる):FallTile
こんな感じで行きましょうか。僕の時間と体力に余裕があったら3Dまで挑戦したいかなとは思います。
クラス設計
クラス設計はこんな感じで行きます
Transitorは御覧の通り純粋仮想クラスです。
生成時点で遷移演出スタートしてもいいのですが、遅らせたい場合も考えてStartで演出開始するようにしています。
Transitor::UpdateとTransitor::DrawはそれぞれのScene::UpdateやScene::Drawの最後で呼び出すようにします。
(なおScene::Updateの話はswitch文を使わないゲーム状態遷移inC++ で説明してるので、「何それ?」という方はまずそちらをご参照ください)
キーになる関数
キーになるDxLib関数(気になる関数じゃないよ)をいくつか書いておきますね
必ず公式リファレンスを見てから使ってくださいね。
- SetDrawScreen : 描画先を指定する
- MakeScreen : レンダーターゲットを作る(これを使うのはαアリの画面が作れるため)
- GetDrawSceenGraph : これが大事。現在の描画先のグラフィックを指定ハンドルに転送
- SetMaskScreenGraph : 特定のグラフィックハンドルをマスクスクリーンに転送
画面遷移演出なので、レンダ―ターゲットやら画面いじる系がキーになります。
実装
Transitor(基底クラス)
/// <summary>
/// シーン遷移時演出基底クラス
/// </summary>
class Transitor
{
protected:
const int interval_;//切り替えにかかる時間
int frame_=0;//開始から現在までのフレーム数
int oldRT_=0;//切り替え前の画面
int newRT_=0;//切り替え後の画面
public:
Transitor(int interval = 60) : interval_(interval) {}
virtual ~Transitor();
void Start();//演出開始
virtual void Update()=0;//Scene::Updateの最後に呼んでね
virtual void Draw() = 0;//Scene::Drawの最後に呼んでね
virtual bool IsEnd()const;//演出が終了した
};
protectedなのは勿論派生先で使うからです。
ではcpp側も見てみましょう。実装するのはStartとIsEndとデストラクタだけです
void Transitor::Start()
{
const auto& size = Application::GetInstance().GetWindowSize();
oldRT_ = MakeScreen(size.w, size.h);
newRT_ = MakeScreen(size.w, size.h);
int result = GetDrawScreenGraph(0, 0, size.w, size.h, oldRT_);
frame_ = 0;
}
Transitor::~Transitor()
{
DeleteGraph(oldRT_);
DeleteGraph(newRT_);
}
bool
Transitor::IsEnd() const
{
return frame_ >= interval_;
}
大したことはやっていませんが、Start関数ではまず、oldRT_とnewRT_をMakeScreenで生成しています。
これは遷移前シーンのスクショ(と言うより現在のバックバッファ)をoldRT_にコピーして、遷移後シーン描画をバックバッファではなくnewRT_に書かせるために用意しています。
で、遷移前シーンのスクショ(バックバッファ)をoldRT_に転送しているコードがこれです。
int result = GetDrawScreenGraph(0, 0, size.w, size.h, oldRT_);
(左,上)~(右,下)の領域を切り取ってoldRT_に転送しています。とにかく現在の画面の状況がoldRT_に書き込まれる(コピーされる)わけですね。
このときの注意点としてStart呼び出し時点でバックバッファが残っていないといけません。
もし、次のシーンのコンストラクタでStartを呼ぶ場合はUpdate関数内で呼ばれることになりますが、もしUpdate関数前にClearDrawScreenを呼び出してしまうと、コピーすべき画面がもうないので真っ暗なものと遷移先が合成されることになってしまいます。
これでは、いけませんね。
このため、ClearDrawScreenは各シーンのDraw関数内で行う事にしましょう。
FadeTransitor(いわゆるクロスフェード)
はい、これは簡単です。
現在の経過フレームの、最初に設定した「総フレーム数」に対する割合を計算して、それをもとに
SetDrawBlendMode(DX_BLENDMODE_ALPHA,α値);
すればいいだけだからです。
簡単なのでUpdate関数とDraw関数だけここに記します。
void FadeTransitor::Update()
{
if (frame_ < interval_) {
++frame_;
SetDrawScreen(newRT_);
}
else if (frame_ == interval_) {
SetDrawScreen(DX_SCREEN_BACK);
}
}
void FadeTransitor::Draw()
{
if (IsEnd()) {
return;
}
SetDrawScreen(DX_SCREEN_BACK);
auto rate = (float)frame_ / (float)interval_;//割合を求める
DrawGraph(0, 0, oldRT_, true);
//切り替え先画面をアルファブレンディング
SetDrawBlendMode(DX_BLENDMODE_ALPHA, rate * 255);
DrawGraph(0, 0, newRT_, true);
SetDrawBlendMode(DX_BLENDMODE_NOBLEND, 0);
}
Update関数で総フレーム(interval_)に到達するまで経過フレーム(frame_)をインクリメントしています。
この間、切り替え先画面の描画は前画面と合成する必要があるためバックバッファではなくてnewRT_に描画していきます。
終わったら、通常描画通りにDX_SCREEN_BACKに戻します。
次にDrawですが、経過フレームを総フレームで割った割合(rate)を計算しておきます。
フェードの時にミソになる関数は
SetDrawBlendMode(ブレンドモード,適用度合(0~255))
です。
で、rateは取りうる値が0.0~1.0であるため、255を乗算することで、クロスフェードのような見た目になります。
最後にひとつ。Transitorの実装時に
Transitor::UpdateとTransitor::DrawはそれぞれのScene::UpdateやScene::Drawの最後で呼び出すようにします。
と書きましたが、そのルールの理由はTransitor派生クラスのUpdate関数内で書き込み先スクリーンをnewRT_に書き換えてシーン側にnewRT_に描画させておき、Draw関数内で本来のバックバッファDX_SCREEN_BACKに切り替えているのが分かるでしょう?
これによって、前の画面と今回の画面を切り替えているわけです。
次行きましょう。次はプッシュですね?
PushTransitor(押し出し)
押し出しも大した事しませんが、メンバ変数と、コンストラクタの引数に「移動方向」が増えます。
このため、Transitor.hに以下の列挙型を追加しておきます
enum class TransitDirection {
up,
down,
right,
left
};
class PushTransitor :
public Transitor
{
public:
private:
TransitDirection direction_;//プッシュ方向
public:
PushTransitor(PushDirection dir=TransitDirection::up, int interval = 60);
virtual void Update() override;
virtual void Draw() override;
};
Update関数はFadeTransitorの時と同じなので、省略しますね。
Draw関数がこうなります。
void PushTransitor::Draw()
{
if (IsEnd()) {
return;
}
const auto& wsize = Application::GetInstance().GetWindowSize();
SetDrawScreen(DX_SCREEN_BACK);
auto rate = (float)frame_ / (float)interval_;
//最終的に新画面が0に来るようにminusoneを用意する
auto minusone = rate-1.0f;
int endX=0;
int endY=0;
switch (direction_) {
case TransitDirection::left:
endX = -wsize.w;
break;
case TransitDirection::right:
endX = wsize.w;
break;
case TransitDirection::up:
endY = -wsize.h;
break;
case TransitDirection::down:
endY = wsize.h;
break;
}
DrawGraph(endX*rate, endY*rate, oldRT_, true);
DrawGraph(endX* minusone, endY* minusone, newRT_, true);
}
割合の計算のところまでは同じですが、minusoneと言うのを計算しています。これはただ単にrateから1を引いたものです。
rateは0→1になるわけですから、minusoneは-1→0になるわけです。
ここでrateを古い画面、minusoneを新しい画面に割り当てれば、押し出すように位置が移動するわけです。
最終的にminusoneが0になった時に0の位置=本来の位置に来るのですから、通常描画と同じになるわけですね。
あとは方向に合わせてそれぞれの移動量をかけてやればいいわけです。
WipeTransitor(ワイプ)
ワイプっていうのは、フェードな感じなんですが、そのフェード感が右から左に向かって進むとかそういう感じの演出です。
このへんから話がちょ~っとややこしくなってきます。でもこの程度の話でシェーダは使いたくないですね。
どうしましょうか。
ここでキーになる関数は2つです
- DrawBlendGraph :他の画像のブレンドしたうえで描画する(今回はアルファ値を利用する)
- DrawPolygon2D : グラデーション画像を動的に作成する
はい、ではやっていきましょうか。まずヘッダ側から
class WipeTransitor :
public Transitor
{
private:
int gradationH_;//グラデーション用ハンドル
public:
WipeTransitor(TransitDirection dir=TransitDirection::left, int interval = 60);
virtual void Update() override;
virtual void Draw() override;
};
はい、グラデーション用ハンドルがついてますね。また、コンストラクタの引数にワイプ方向を追加しています。
ここからちょっと面倒ですがついてきてくださいね。
DXライブラリにグラデーションを描く関数はないので、自分で作るのですが、まずレンダ―ターゲットとしてこのgradationH_を使ってそこにDrawPolygon2Dでグラデ画像を描いていきます。
それをコンストラクタ内で行います(こういうのコンストラクタ時にやるのはあまり良くない気がするんですが、サンプルコードだし、多少はね)
WipeTransitor::WipeTransitor(TransitDirection dir, int interval)
{
float left, right, top, bottom;
left = right = top = bottom = 1.0f;
switch (dir) {
case TransitDirection::left:
right = 0.0f;
break;
case TransitDirection::right:
left = 0.0f;
break;
case TransitDirection::up:
bottom = 0.0f;
break;
case TransitDirection::down:
top = 0.0f;
break;
default:
break;
}
const auto& wsize = Application::GetInstance().GetWindowSize();
array<COLOR_U8, 4> colors;
colors[0].r = colors[0].g = colors[0].b = colors[0].a = 255 * left * top;//左上
colors[1].r = colors[1].g = colors[1].b = colors[1].a = 255 * right * top;//右上
colors[2].r = colors[2].g = colors[2].b = colors[2].a = 255 * left * bottom;//左下
colors[3].r = colors[3].g = colors[3].b = colors[3].a = 255 * right * bottom;//右下
array<VERTEX2D,6> vertices;
// 左上の頂点の情報をセット( 1ポリゴン目の第1頂点 )
vertices[0].pos.x = 0;
vertices[0].pos.y = 0;
vertices[0].pos.z = 0.0f;
vertices[0].rhw = 1.0f;
vertices[0].dif = colors[0];
vertices[0].u = 0.0f;
vertices[0].v = 0.0f;
// 右上の頂点の情報をセット( 1ポリゴン目の第2頂点 )
vertices[1].pos.x = wsize.w;
vertices[1].pos.y = 0;
vertices[1].pos.z = 0.0f;
vertices[1].rhw = 1.0f;
vertices[1].dif = colors[1];
vertices[1].u = 0.0f;
vertices[1].v = 0.0f;
// 左下の頂点の情報をセット( 1ポリゴン目の第3頂点 )
vertices[2].pos.x = 0;
vertices[2].pos.y = wsize.h;
vertices[2].pos.z = 0.0f;
vertices[2].rhw = 1.0f;
vertices[2].dif = colors[2];
vertices[2].u = 0.0f;
vertices[2].v = 0.0f;
// 右下の頂点の情報をセット( 2ポリゴン目の第1頂点 )
vertices[3].pos.x = wsize.w;
vertices[3].pos.y = wsize.h;
vertices[3].pos.z = 0.0f;
vertices[3].rhw = 1.0f;
vertices[3].dif = colors[3];
vertices[3].u = 0.0f;
vertices[3].v = 0.0f;
// 2ポリゴン目の第2頂点は左下の頂点なのでコピー
vertices[4] = vertices[2];
// 2ポリゴン目の第3頂点は右上の頂点なのでコピー
vertices[5] = vertices[1];
gradationH_ = MakeScreen(wsize.w, wsize.h,true);
auto bkScrH = GetDrawScreen();
//レンダ―ターゲットを変更し、グラデーションの描画
SetDrawScreen(gradationH_);
DrawPolygon2D(vertices.data(), 2, DX_NONE_GRAPH, true);
//グラデーション描いたらまた元に戻す
SetDrawScreen(bkScrH);
}
ちょっと長くなっちゃいましたね。DrawPolygon2DはDrawPrimitive2DでTRIANGLESTRIPにした方がいいかもしれませんが、まぁ、そこらへんは各自で何とかしてください。
ポイントはswitch文の中でleft,right,top,bottomの値を決めているところなのですが、これがDrawPolygon2Dの4頂点の色を決めるもとになります。ここでは「辺」の値を定義してますが、その後に書かれている
colors[0].r = colors[0].g = colors[0].b = colors[0].a = 255 * left * top;//左上
によって点の色情報に変換しているわけです。
Update関数はいつもと変わらないので、Draw関数を見てみましょう。
void WipeTransitor::Draw()
{
if (IsEnd()) {
return;
}
SetDrawScreen(DX_SCREEN_BACK);
DrawGraph(0, 0, newRT_,true);
auto rate = (float)frame_ / (float)interval_;
auto result = DrawBlendGraph(0, 0,oldRT_ , true, gradationH_, 255*rate, 64);
}
最初にも触れましたが、ポイントはDrawBlendGraphです。これは第三引数を第五引数の色でブレンドするものですが、今回は「アルファ値」を使ってブレンドしています。
なお、newRT_とoldRT_がひっくり返っていますが、第六引数の255rateが「境界色(アルファ)」になっていて、その境界色より明るい部分だけ、指定の画像を表示するものです。
255rateは0~255に変化しますが、最終的に255になり255より明るい所はなくなります。つまり今回の場合255になった瞬間に第三引数の画像は見えなくなります。
このため第三引数の画像の方をoldRT_にしているわけです。結果
こうなります。
StripTransitor(短冊状切り替え)
今回は時間ないので縦縞だけ実装します。
まずはヘッダから
//短冊トランジション(縦縞)
class StripTransitor :
public Transitor
{
private:
int width_ = 100;
public:
StripTransitor(int width = 100, int interval = 60);
virtual void Update() override;
virtual void Draw() override;
};
引数のwidthは短冊の幅です。
これもUpdate側はいつも通りなのでDrawを見せます。
void StripTransitor::Draw()
{
if (IsEnd()) {
return;
}
SetDrawScreen(DX_SCREEN_BACK);
const auto& wsize=Application::GetInstance().GetWindowSize();
auto rate = (float)frame_ / (float)interval_;
int lp = (wsize.w / width_) + 1;
DrawRectGraph(0, 0, 0, 0, wsize.w, wsize.h, oldRT_, true);
for (int i = 0; i < lp; ++i) {
if (i % 2 == 0) {
DrawRectGraph(i * width_, wsize.h * (rate-1.0f), i * width_, 0, width_, wsize.h, newRT_, true);
}
else {
DrawRectGraph(i * width_, wsize.h * (1.0f-rate), i * width_, 0, width_, wsize.h, newRT_, true);
}
}
}
簡単ですね。背景にoldRT_を描画しておいて、newRT_を上下方向から本来の位置に戻すという事をしています。敢えてポイントをあげるなら
wsize.h * (1.0f-rate)
と
wsize.h * (rate-1.0f)
の部分です。これは描画するY座標を示しているのですが、rate=0の時は1.0が残るため、Y=-画面高 , Y=画面高になっていてnewRT_は画面に見えません。
ところが時間が進んでrate=1.0になったときにどちらもY=0になるため本来あるべき場所に描画されるというわけです。
はい次いきましょう。
TileTransitor(タイル状切り替え)
これは少しずつタイル状に新しい画面に切り替わるものです。
今回ポイントになる関数はDxLibの関数ではなくC++標準の
- shuffle
- mt19937
です。
ランダムにタイルを剥がれ落ちさせるために使います。
ヘッダ
#include<vector>
#include<random>
/// <summary>
/// タイル状切り替え(ディゾルヴトランジションとも)
/// </summary>
class TileTransitor :
public Transitor
{
private:
int cellSize_=50;
struct XYIdx{
int xidx, yidx;
};
std::mt19937 mt_;
std::vector<XYIdx> tiles_;
public:
TileTransitor(int cellSize = 50, int interval = 60);
virtual void Update() override;
virtual void Draw() override;
};
ランダムに切り替えるため、メルセンヌツイスタを宣言しています。また、タイルごとにインデックス情報を保持しておきたいため、内部的にインデックスをもって置くXYIdx構造体を定義し、それをベクタ配列に持たせています。
通常配列ではなくベクタ配列なのは、セルサイズによって必要なインデックス数が変化するからです。
はい、ではCpp側です。今回はUpdateも工夫してますし、コンストラクタでベクタ配列の初期化をやっているため、コンストラクタ、Update、Draw関数それぞれ見ていきましょう。
コンストラクタ
TileTransitor::TileTransitor(int cellSize, int interval) :cellSize_(cellSize),
Transitor(interval)
{
const auto& wsize=Application::GetInstance().GetWindowSize();
int xnum = (wsize.w / cellSize_) + 1;
int ynum = (wsize.h / cellSize_) + 1;
tiles_.reserve(xnum * ynum);
for (int yidx = 0; yidx < ynum; ++yidx) {
for (int xidx = 0; xidx < xnum; ++xidx) {
tiles_.push_back({xidx,yidx});
}
}
std::shuffle(tiles_.begin(), tiles_.end(),mt_);
}
ここではタイルの数を計算して、中にタイルのXインデックスYインデックスを代入しています。
(wsize.w / cellSize_) + 1;
としているため、割り切れる数値だった場合に1個余計になりますが、面倒なのでここではこのやり方にしています。
これで、画面を覆いつくすタイルを定義しました。それぞれのインデックスで、どこのタイルかが分かるようになっています。
std::shuffle(tiles_.begin(), tiles_.end(),mt_);
shuffle関数は、コンテナの全要素をシャッフルします。最後の引数にメルセンヌツイスタオブジェクトを入れていますが、これはshuffle関数が乱数エンジンを要求するからです。
ともかくこの関数を呼べば並びがランダムになるわけです。
次にUpdate関数を見てみましょう。
void TileTransitor::Update()
{
if (frame_ < interval_) {
++frame_;
SetDrawScreen(newRT_);
}
else if (frame_ == interval_) {
SetDrawScreen(DX_SCREEN_BACK);
}
if (IsEnd()) {
return;
}
const auto& wsize = Application::GetInstance().GetWindowSize();
int xnum = (wsize.w / cellSize_) + 1;
int ynum = (wsize.h / cellSize_) + 1;
int eraseNum= ((xnum * ynum) / interval_);
if (tiles_.size() > eraseNum) {
tiles_.erase(tiles_.end() - eraseNum, tiles_.end());
}
else {
tiles_.clear();
}
}
ちょっと雑な部分があって、改善の余地ありまくりですが、許してね。大雑把に捉えてね。
次に一定数だけ消す処理ですがまず消す数を
int eraseNum= ((xnum * ynum) / interval_);
で計算しておきます。そして
tiles_.erase(tiles_.end() - eraseNum, tiles_.end());
erase関数でお尻から数えてガッツリ削除しています。先頭からでもOKです。
ともかく一定時間でX,Yのインデックスがそれぞれ入った配列の要素がランダムに消されていくのが分かると思います。
Drawに関しては最初にnewRT_を描画しておき、上からoldRT_のタイルが貼られているような描画にします。
void TileTransitor::Draw()
{
if (IsEnd()) {
return;
}
SetDrawScreen(DX_SCREEN_BACK);
const auto& wsize = Application::GetInstance().GetWindowSize();
auto rate = (float)frame_ / (float)interval_;
DrawRectGraph(0, 0, 0, 0, wsize.w, wsize.h, newRT_, true);
for (const auto& cell : tiles_) {
DrawRectGraph(
cell.xidx * cellSize_,
cell.yidx * cellSize_,
cell.xidx * cellSize_,
cell.yidx * cellSize_,
cellSize_, cellSize_,
oldRT_, true);
}
}
xidx,yidxはただのインデックスなので、あらかじめコンストラクタの時に取得しておいたcellSize_を賭けてあげます。
ここまでできれば、タイル状にはがれていってだんだん新しいシーンの画面が描画されるというものになります。
IrisTransitor(いわゆるアイリスイン・アイリスアウト)
アイリスイン・アイリスアウトというのは円形に画面が切り替わるやつです。実際のアニメの場合はキャラの顔に向かって円形に縮まっていく(「トホホ~〇〇はもうコリゴリだよぉ~」)感じですが、今回は画面の真ん中に向かって縮まったり、逆に画面の真ん中から円形に広がっていく感じにします。
こういう事をやるには、DXライブラリでは「マスク」という仕組みを使います。DirectX12とかで言う所の「ステンシル」ですね。
マスクとは、Draw~系の関数で描画する際に、特定の領域にだけ描画し、それ以外の領域には描画しないようにするものです。所謂「抜き」と言う奴ですね。と言う事で、白か黒かの2値の情報です。白い領域は描画し、黒い領域は抜くわけです。
とはいえ、通常だとDXライブラリは
- LoadMask
- DrawMask
くらいでしかマスクを作る事ができません。とはいえたかだか円形のためだけにマスク画像を用意したくもありません。DrawCircleとかで何とかならないんでしょうか・・・?
マスクと通常の画像はそもそも扱いが違う(通常描画はカラーレンダーターゲットバッファへ描画、マスクのほうはステンシルバッファへ描画)ため、マスクにDrawCircle的なものも用意されていません。
もし外部から円形画像を読み込んだところでDrawMaskはDrawRotaGraphみたいに拡大縮小もできませんし、このままではアイリスイン・アウトを実装できません。
ここで有用な関数があります。
- SetMaskScreenGraph : 引数で渡されたグラフィクスハンドルをマスクスクリーンに転送
と言う関数です。ただし、この関数はアルファ値が0か否かでマスクを作ります。このため、転送元の画像はαアリにする必要がありますので注意。
はい、それではポイントになる関数ですが
- MakeScreen : αありのレンダ―ターゲットを作成
- CreateMaskScreen : マスク画面の作成(マスクを利用するには必須…ステンシルバッファやね)
- DeleteMaskScreen : マスク画面の削除
- SetMaskScreenGraph : 引数で渡されたグラフィクスハンドルをマスクスクリーンに転送
- SetMaskReverseEffectFlag : マスクの白黒(抜くか抜かないか)を反転する
- SetUseMaskScreenFlag : マスクを適用するか否かのフラグ設定
です。今回は多いですね。
はいではヘッダ側
class IrisTransitor:
public Transitor
{
private:
int handleForMaskScreen_;//マスク転送用グラフィックハンドル
int maskH_;//マスクハンドル
float diagonalLength_;//対角線の長さ
bool irisOut_=false;//アイリスアウトフラグ(falseならアイリスイン)
public:
IrisTransitor(bool irisOut=false,int interval = 60,bool isTiled=false , int gHandle=-1);
~IrisTransitor();
virtual void Update() override;
virtual void Draw() override;
};
はい、対角線の長さを定義していますが、これは画面中心からの半径最大値ですね。画面幅ですと円が左上などにいきわたりませんから。
ではcpp側です。ちょっと長いですよ。
IrisTransitor::IrisTransitor(bool irisOut,int interval, bool isTiled,int gHandle) :Transitor(interval),
irisOut_(irisOut),
isTiled_(isTiled),
gHandle_(gHandle)
{
const auto& wsize=Application::GetInstance().GetWindowSize();
handleForMaskScreen_ = MakeScreen(wsize.w, wsize.h,true);
maskH_ = CreateMaskScreen();
diagonalLength_ = std::hypotf(wsize.w, wsize.h)/2.0f;
}
IrisTransitor::~IrisTransitor()
{
DeleteMaskScreen();
}
void IrisTransitor::Update()
{
if (frame_ < interval_) {
++frame_;
SetDrawScreen(newRT_);
}
else if (frame_ == interval_) {
SetDrawScreen(DX_SCREEN_BACK);
}
}
void IrisTransitor::Draw()
{
if (IsEnd()) {
return;
}
auto rate = (float)frame_ / (float)interval_;
int backRT = oldRT_;
int maskedRT = newRT_;
if (irisOut_) {
backRT = newRT_;
maskedRT = oldRT_;
rate = 1.0f - rate;
}
//
float radius = (diagonalLength_ ) * rate;
SetDrawScreen(handleForMaskScreen_);
ClearDrawScreen();
DrawCircleAA(320, 240, radius, 32, 0xffffff, true);
//隠し関数(現在のグラフィックハンドルをマスクスクリーンに転送)
SetMaskScreenGraph(handleForMaskScreen_);
//描画領域を反転する
SetMaskReverseEffectFlag(true);
SetDrawScreen(DX_SCREEN_BACK);
SetUseMaskScreenFlag(false);
DrawGraph(0, 0, backRT, true);
SetUseMaskScreenFlag(true);
DrawGraph(0, 0, maskedRT, true);
SetUseMaskScreenFlag(false);
}
はい、ここまで読んできた人にはお分かりのようにまずレンダ―ターゲットをhandleForMaskScreen_にしたうえで円形をDrawCircleAAで描画しています。この半径を時間経過ごとに広げたり縮めたりするわけです。
で、描画が終わったらこのレンダーターゲットの画像をマスクにSetMaskScreenGraphで転送します。
マスク反転しているのは、円を描画した部分が「隠される」からです。反転することで円の部分が描画されるようにしています。
また、oldRT_,newRT_ではなく、わざわざbackRT,maskedRTにしているのは、iriseOut_フラグが立った時に両者の関係を逆転させているからです。
ここまでできれば
だし、アイリスアウトフラグが立ってる場合は
になりますし、ちょっと応用すればこういう事も出来ます。
また、DrawCircleができるという事は、もちろんDrawRotaGraphも使えるわけで、そうするとこういう事もできるわけです。
ハイと言う事で、アイリスインアウトでした。次で最後です
FallTileTransitor(剥がれ落ちるタイル)
これは、TileTransitorのおまけみたいなもんですが、今度は「剥がれ落ちます」。
つまり落下していくわけです。
全体的にはTileTransitorと同じですが、落下スピードや現在位置を記録しておく必要があるためTileTransitorのXYIdx構造体にyoffsetとvyを追加します。あと、別で重力加速度g_を定義しておきます。
struct XYIdx{
int xidx, yidx;
int yoffset=0;
float vy=0.0f;
};
float g_;
ということで、ヘッダ
class FallTileTransitor :
public Transitor
{
private:
int cellSize_=50;
struct XYIdx{
int xidx, yidx;
int yoffset=0;
float vy=0.0f;
};
std::mt19937 mt_;
std::vector<XYIdx> tiles_;
float g_;
public:
FallTileTransitor(int cellSize = 50, float gravity=0.0f,int interval = 60);
virtual void Update() override;
virtual void Draw() override;
virtual bool IsEnd() const override;
};
引数にグラヴィティ(重力)を指定できるようにしています。cpp側を書きますが、TileTransitorと違う部分だけ解説しますね。
FallTileTransitor::FallTileTransitor(int cellSize, float gravity,int interval) :cellSize_(cellSize),
Transitor(interval),
g_(gravity)
{
const auto& wsize=Application::GetInstance().GetWindowSize();
int xnum = (wsize.w / cellSize_)+1;
int ynum = (wsize.h / cellSize_) + 1;
tiles_.reserve(xnum * ynum);
for (int yidx = 0; yidx < ynum; ++yidx) {
for (int xidx = 0; xidx < xnum; ++xidx) {
tiles_.push_back({xidx,yidx});
}
}
std::shuffle(tiles_.begin(), tiles_.end(), mt_);
}
void FallTileTransitor::Update()
{
if (frame_ < interval_+ additional_time) {
++frame_;
SetDrawScreen(newRT_);
}
else if (frame_ == interval_+ additional_time) {
SetDrawScreen(DX_SCREEN_BACK);
}
if (IsEnd()) {
return;
}
for (auto& cell : tiles_) {
if (cell.vy > 0.0f) {
cell.vy += g_;
cell.yoffset += cell.vy;
}
}
auto rit = tiles_.rbegin();
const auto& wsize = Application::GetInstance().GetWindowSize();
int xnum = (wsize.w / cellSize_) + 1;
int ynum = (wsize.h / cellSize_) + 1;
int fallNum = ((xnum * ynum) / interval_);
for (int i=0; rit != tiles_.rend() && i<fallNum; ++rit) {
if (rit->vy > 0.0f || rit->yoffset > 0) {
continue;
}
else {
++i;
rit->vy += g_;
rit->yoffset += rit->vy;
}
}
}
void FallTileTransitor::Draw()
{
if (IsEnd()) {
return;
}
SetDrawScreen(DX_SCREEN_BACK);
float amp=frame_ % 31;
int yoffset = 10*((amp / 30.0f) - 1.0f);
const auto& wsize = Application::GetInstance().GetWindowSize();
auto rate = (float)frame_ / (float)interval_;
DrawRectGraph(0, yoffset, 0, 0, wsize.w, wsize.h, newRT_, true);
for (const auto& cell : tiles_) {
DrawRectGraph(
cell.xidx * cellSize_,
cell.yidx * cellSize_+cell.yoffset + yoffset,
cell.xidx * cellSize_,
cell.yidx * cellSize_,
cellSize_, cellSize_,
oldRT_, true);
}
}
bool
FallTileTransitor::IsEnd() const
{
return frame_ >= interval_+ additional_time;
}
Update関数内で書かれている
for (auto& cell : tiles_) {
if (cell.vy > 0.0f) {
cell.vy += g_;
cell.yoffset += cell.vy;
}
}
これを記述することによって、落下を始めたタイルがどんどん加速しながら落ちていきます。
また、剥がれ落ちる部分は以下のように記述することで
auto rit = tiles_.rbegin();
const auto& wsize = Application::GetInstance().GetWindowSize();
int xnum = (wsize.w / cellSize_) + 1;
int ynum = (wsize.h / cellSize_) + 1;
int fallNum = ((xnum * ynum) / interval_);
for (int i=0; rit != tiles_.rend() && i<fallNum; ++rit) {
if (rit->vy > 0.0f || rit->yoffset > 0) {
continue;
}
else {
++i;
rit->vy += g_;
rit->yoffset += rit->vy;
}
}
ランダムに一定数だけ落下を始めるようになっています。落下タイル数は事前にfallNumで計算しておいて、その枚数だけ落下が始まるようになっています。
Drawは基本的にTileTransitor::Drawと変わらないのですが、せっかくだから俺は画面を縦に揺らすぜ!しています。
float amp=frame_ % 31;
int yoffset = 10*((amp / 30.0f) - 1.0f);
と書いた上で
DrawRectGraph(
cell.xidx * cellSize_,
cell.yidx * cellSize_+cell.yoffset + yoffset,
cell.xidx * cellSize_,
cell.yidx * cellSize_,
cellSize_, cellSize_,
oldRT_, true);
のように、本来の表示Y座標に足してやることで画面が揺れてその衝撃で落ちているように見えるわけです。
TDN遊び心です。
最後に、落下の時間がありますので、IsEnd関数をオーバーライドして、+additional_timeして、猶予を持たせています。
bool
FallTileTransitor::IsEnd() const
{
return frame_ >= interval_+ additional_time;
}
はい、ここまでできれば…
静止画じゃわかりづらいですが、落下しています。
はい、と言う事で、長々と書いていきましたが、画面切り替え演出クラスの作り方でした。
チャチャチャッと書いたので、色々雑なところはありますが、何かしらの参考になれば幸いです。