ステージデータをコピーにする

音がつくと一気に華やかになるから、たのしいよね。
(先に、ステージデータのコピーとかをやろうと思ってたのに、ちょっと調子に乗ってしまった…)
前にも書いたけど、現在ステージデータはグローバル変数になっているmaps[]をそのまま変更して使っている。
当然、ソースコードが変更されているわけではないのでゲームを再起動すると元のデータをもう一度読み込めるが、ゲーム実行中は元のデータが失われてしまうことになる。
こういう作り方をするとパズルゲームなどでは、1手戻しや面リセットができなくなっちゃうのでつらいよね。
ってわけで、ステージをコピーして使えるように改造して、さらに面リセット機能をつけよう!
今の仕様だと、一回コスコマンが運び方をミスると、右上の×ボタンを押してゲームを再起動するしかないという、鬼仕様になっている。
これを、エンターキーを押すことによって、初期状態にステージを戻してプレイできるようする。
(1手戻しとかあればいいけど、みんな可変配列知らないから説明面倒なので全リセットで)
それともう一つ、だんだんソースコードが大きくなってくると、初めの仕様に引っ張られて変更が大変になってくる。
ぶっちゃけ、勢いで作ったのでMoveObjectとかはかなりひどい感じになってると思う。こうやってだんだん仕様が変わった時に対応できなくなっていくので、
設計は大事だな、っていうのも体感してほしい。

根っこの仕様を変更するのは大変なので、追加で考えていくことにする。

現在

  • stages.h
    • Map maps[] ステージ分のMapデータ配列
  • gameSequence.cpp
    • crrStageNumber ステージ番号(0が1ステージ目、配列のインデックスなのでステージ番号だと思うと1個ずれてるよ)
    • maps[crrStageNumber] 現在のステージのデータ(全体で使えるグローバル変数)

変更後

  • stages.h
    • Map maps[] ステージ分のMapデータ配列
  • gameSequence.cpp
    • crrStageNumber ステージ番号(0が1ステージ目、配列のインデックスなのでステージ番号だと思うと1個ずれてるよ)
    • Map crrMap == maps[crrStageNumber]のデータコピー gameSequence.cppのみで使えるグローバル変数

あとは、maps[]を使っていたところをすべて、crrMapに置き換えてしまえばよい。
(あたりまえだけども、引数でmap渡してるやつは、特に変更しなくていいよ)

まず、冒頭にコピー用のグローバル(ファイルスコープ)変数Map crrMapを宣言する

//現在のステージ current Stage Number
int crrStageNumber = 0;
Map crrMap;

次に、ステージをグローバルのステージからとってきてコピーする関数void InitCrrentMap(Map& _map, int _stageNum)を作る
引数の_mapに、もう一つの引数で指定されたステージ番号(_stageNum)のマップをmaps[]からコピーして_mapのデータを初期化する。

Listing. 1: InitCurrentMap関数の追加
gameSequence.h
    void InitCurrentMap(Map& _map, int _stageNum);
gameSequence.cpp
void InitCurrentMap(Map& _map, int _stageNum)
{
	_map.stage_widthと_map.stage_heightをmapsからコピー
	//その他の領域(7で表されるところ)もコピーしなきゃないからstage_width, stage_heightじゃなくMAX_***の範囲でコピーするんよ
	for (int j = 0; j < MAX_STAGE_HEIGHT; j++) {
		for (int i = 0; i < MAX_STAGE_WIDTH; i++) {
			_map.Datにmaps[_stageNum].Datをコピー
		}
	}
}

あとは適切なタイミングでこの関数を呼んでやれば良い
面リセット機能は、画面にボタンとかをつけてもよいが今回は、エンターキーを押したら面リセットの機能を作ろうと思う

役割を考えると各Update関数で呼び出すのがよさそうだよね。

  • 適切なタイミング一覧
    1. タイトル画面からプレイ画面に切り替わる時(つまり、ゲームの初回の初期化 crrStageNumber = 0の時の初期化)
    2. ゲームクリア時にcrrStageNumber++されて、次のステージを読み込むための初期化
    3. 同じステージを読み込みたいとき(面リセット)
  1. は、UpdateTitleで、SpaceKeyが押されたとき
  2. は、UpdateClearで、crrStageNumberが更新された後
  3. は、UpdatePlayで、エンターキーのプッシュを検出したとき

変更した後の実行例

リセットしたときに何も表示されないから、急に画面がリセットされた感じで気持ち悪いけど、一応できました。
リセットしたときは、リセット用の画面移行シーンを挟むとかにすると、いいけど、シーン増やすのも面倒なのでいったんこれでいいことにする
興味ある人はやってみると良い。
(実は、ゲームシーケンスの状態にRESETを追加してresetUpdateとresetDrawを挟んで、元のシーンに戻るようにするだけなのでそこまで面倒ではないよ。中身もほとんどクリアシーンのコピーでできるはず)

Fig. 1: 面データコピーと面リセット機能の追加

あとは、オールクリア(全面クリア)の時の処理や、1手戻し、グラフィック表示の強化などが課題として残っている気がする。
現在、プレイヤーキャラは、コマ送りで一瞬にして次の移動場所にタイルの座標ベースで移動する。(すごろくの駒移動を自動で見てる気分)
これを、次のマスにぬるぬると移動するように変更すると少し気持ちよくなる
さてどうしよう?現在、通常のアクションゲームを作る時のように各キャラクターの座標などをデータで持っているわけではない。
ステージに各キャラの配置が書いてあってその場面を変更することにより、毎ターンの盤面を更新しながら表示している。
この状態で、キャラをぬるぬる動かすにはどうしたらいいだろうか(しかも大きな変更なしで…)
すでに、ここまで来てしまっているので、仕様がぬるぬる動く仕様にしていなかったのは明らかなので逆に頭をひねらなければならない

CostcoManは、タイルの大きさごとにキャラを動かして、目的の形にするスライドパズルと同じような仕組みのパズルである。(よく考えると色々似てるよね)
なので、タイルごとにペキペキとコマ送りで動いてもそんなに変な感じはしない。
そこで、方向キーを押したときに、次のタイルまでを塗るっとスムーズに動くように改造しよう。
方針は以下の通り。

Mapデータを拡張し、移動があった時、更新後のオブジェクトがどの方向から来たかを記録しておく、便宜上From配列と呼ぶ

  1. まずいつも通り、配列の中のマップデータで移動を行う(Mapの配列データはこの時点で更新される)
  2. プレイヤーと荷物以外は普通に描画する。
  3. プレイヤーと荷物の移動があったら、moveフラグをONにする
  4. moveフラグがONの間にグラフィック画面では以下の処理を行う。
    1. From配列から移動があったオブジェクトの、移動前の座標を計算
    2. 移動前の座標を0,移動後の座標を1として、0~1の間でどの位置にいるかを更新していく(当然小数点で)、この数値を移動レートと呼ぶ
    3. 計算した少数座標にCHR_SIZEをかけて、表示座標を計算する
    4. 表示座標に動くオブジェクトを表示する
    5. 移動レートが1になるまで、繰り返す。1になったらmoveフラグをOFF、FromデータをすべてNONEにリセット

つまり、データだけ移動させておいて、ある一定時間内に、表示だけ滑らかに動かす。を繰り返す。
いろいろおかしなことにならないように、moveフラグがONの間は、方向入力を受け付けないようにする。
こんな感じで、大幅な変更なしで行けるんじゃないかなと思っている(楽観視)

それではさっそく改造していこう

ぬるぬる移動のために、マップ構造体にFromデータと移動レートを追加する。

Listing. 2: マップ構造体の変更
globals.h
struct Map
{
	int stage_width;
	int stage_height;
	int Dat[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
        //ここから追加
        //移動レート
	double moveRatio;
        //Fromデータ(そのオブジェクトはどこからやってきたか)
	direction From[MAX_STAGE_HEIGHT][MAX_STAGE_WIDTH];
};

これを変更したということは、初期化関数も変更しなきゃないよね。

Listing. 3: void InitCurrentMapの更新
gameSequence.h
void InitCurrentMap(Map& _map, int _stageNum);
gameSequence.cpp
void InitCurrentMap(Map& _map, int _stageNum)
{
        _map.stage_widthと_map.stage_heightをmapsからコピー
	//その他の領域もコピーしなきゃないからMAX_***の範囲でコピーするんよ
	for (int j = 0; j < MAX_STAGE_HEIGHT; j++) {
		for (int i = 0; i < MAX_STAGE_WIDTH; i++) {
			_map.Datにmaps[_stageNum].Datをコピー
			_map.Fromデータをdirection::NONEで初期化(リセット)
		}
	}
	_map.moveRatioを0リセット
}

次に、ぬるぬるプレイヤーを動かすためにDrawStageを改良する。
さすがに同じようにはいかないので、新しく2つの関数を作ってそれをDrawStageの代わりに呼ぶことにする。(DrawStageはそのまま放っておく:別に呼ばなきゃいいだけ) 今までは、ゲーム登場するオブジェクトは、タイルとタイルの中間にあったりしなかったので、スライドパズルのようにタイルを入れ替えれば表示は終わりだった。
次はこんな場面がある。。。

Fig. 2: 小数点を持った座標で表示

この中途半端な座標を移動レートで表す。

  • moveRateが1.0の時は、完全に移動後の座標
  • 0 < moveRate < 1.0 の時は、移動前と移動後の間のどこかの座標
  • moveRateが0.0の時は、完全に移動前の座標

つまり、今度は床面とプレイヤーや荷物の座標がずれているときがあるので、今までのように、タイルの座標にそのまま画像を当てはめると変なことになる
(床ごと、荷物やプレイヤーが動いちゃうよね。)
なので、動かないオブジェクトと動くオブジェクトを別々に書かなければならない。

  1. void DrawStaticObject(Map& _map);
    1. 動かないオブジェクト(スタティックなオブジェクト)を描画する関数
  2. void DrawStageSmoothMove(Map& _map);
    1. 動かないオブジェクトの上に、動くオブジェクトを重ねて書く関数

この2本立てで描画することにする。

void DrawStaticObject(Map& _map)の作成

まず、gameSeauence.hにプロトタイプ宣言を書こう。(もうかけるよね) んで、荷物とプレイヤーを除いた純粋なマップのみを描く関数をつくことになるので

gameSequence.cpp
void DrawStaticObject(Map& _map)
{
    //これは今まで通り
	Texture ObjImg[7]{
	TextureAsset(U"FLOOR"),
	TextureAsset(U"WALL"),
	TextureAsset(U"LUGG"),
	TextureAsset(U"GOAL"),
	TextureAsset(U"PLAYER"),
	TextureAsset(U"PLAYER"),
	TextureAsset(U"LUGG"),
	};
 
	for (int j = 0; j < _map.stage_height; j++) {
		for (int i = 0; i < _map.stage_width; i++) {
			objNumにi,jの位置のオブジェクト番号を取得
			switch (objNum)
			{
			case FLOOR:
			case HUMAN:
			case LUGG:
				床だけ描画
				break;
			case WALL:
				床の上に壁を描画
				break;
             case GOAL:
			case HUMAN_ON_GOAL:
			case LUGG_ON_GOAL:
				床の上にゴールを描画
				break;
			default:
				その他はskip(どう書けば効率的?)
				break;
			}
		}
	}
}

もう一個のほう、void DrawStageSmoothMove(Map& _map);では、冒頭でDrawStaticObjectを呼び出して動かないオブジェクトを書いてあげてから、
その下で(ゆっくりと)動くオブジェクト(プレイヤーと荷物)を描いてやります。
いったん、マップだけ表示して確認しよう!
こっちもプロトタイプ宣言をgameSequence.hに追加するのをお忘れなく。

gameSequence.cpp
void DrawStageSmoothMove(Map& _map)
{
	Scene::SetBackground(Palette::Darkgrey);
	DrawStaticObject(_map);
}

最後に今まで、DrawPlayの中で、DrawStageを呼んでいたところをDrawStageSmoothMoveで置き換えてしまおう

gameSequence.cpp
void DrawPlay()
{
	Scene::SetBackground(Palette::Black);
	FontAsset(U"font")(U"PLAY_SCENE").drawAt(Scene::Center());
	//DrawStage(crrMap);
	DrawStageSmoothMove(crrMap);
}

結果

Fig. 3: ステージのみの描画

あとは入力の受付や、キャラの入れ替えなどはできているので、前のタイル位置から、新しいタイル位置にプレイヤーと荷物を動かすアニメーションを演出としてつけてやる。

  1. まずいつも通り、配列の中のマップデータで移動を行う(Mapの配列データはこの時点で更新される)
  2. プレイヤーと荷物以外は普通に描画する。
  3. プレイヤーと荷物の移動があったら、moveフラグをONにする
  4. moveフラグがONの間にグラフィック画面では以下の処理を行う。
    1. From配列から移動があったオブジェクトの、移動前の座標を計算
    2. 移動前の座標を0,移動後の座標を1として、0~1の間でどの位置にいるかを更新していく(当然小数点で)、この数値を移動レートと呼ぶ
    3. 計算した少数座標にCHR_SIZEをかけて、表示座標を計算する
    4. 表示座標に動くオブジェクトを表示する
    5. 移動レートが1になるまで、繰り返す。1になったらmoveフラグをOFF、FromデータをすべてNONEにリセット

次に、入力があったらmoveフラグをONにするを作ってみよう。
class作れるようになったらもっとまとめられるけど、今の知識で楽に作れるように、これもファイルスコープの変数にしよう。
gameSequence.cppの冒頭にbool型の変数として追加する

gameSequence.cpp
#include "globals.h"
#include "gameSequence.h"
#include "stages.h"
 
//現在のステージ current Stage Number
int crrStageNumber = 0;
Map crrMap;
 
//カウントダウンタイマー
double CDTimer = 6.0;
bool isTimerOn = false;
//Moveフラグ
bool isMoving = false;

フラグをオンにするタイミングは、「プレイヤーが動いたとき」である。(荷物はプレイヤーが動いたとき以外には動かない)
moveObject関数の、switch-case文のプレイヤーが動くときすべてでisMovingをtrueにしてやる。
まったくもってスマートではないけど、今更である。

gameSequence.cpp
void MoveObject(direction _dir, Map& _map)
{
省略
	switch (next)
	{
	case FLOOR:
           プレイヤーが動くならisMovingをtruebreak;
	case WALL:
                プレイヤーが動くならisMovingをtruebreak;
	case GOAL:
                プレイヤーが動くならisMovingをtruebreak;
	case LUGG:
		switch (nextNext)
		{
		case FLOOR:
                        プレイヤーが動くならisMovingをtruebreak;
		case GOAL:
		以下同様なので省略
	default:
		break;
	}
 
}

これでプレイヤーが動いたときに動いたよフラグを立てることができる。
フラグを立てたら、一定時間でmoveRatioを0~1に更新する
これはタイマーを応用すればよい。moveRatioを一定時間で更新する関数を作る

void UpdateCharPosSmoothMove(Map& _map)の作成

isMoveがONの時だけ、moveRatioにScene::DeltaTime()を足す関数。
(ってことは、DeltaTimeは秒数を小数点で返すから、1.0秒でmoveRatioは1.0になるね! 動いている速さというか時間を調整するにはどうしたらいいかな?)
gameSequence.hへのプロトタイプ宣言の追加はもはや当たり前なので省略。

gameSequence.cpp
void UpdateCharPosSmoothMove(Map& _map)
{
	if (isMovingがONなら) {
                //画面に_map.moveRatioを表示、ちゃんと0~1に収まっているか確認しよう
		Print << _map.moveRatio;
                //Clamp(値、0.0, 1.0)は値を0~1の範囲に収めて返してくれるありがたい関数、ちょい1.0からはみ出したら自動で1にしてくれる
		_map.moveRatioを、「Clampで_map.moveRatioにScene::DeltaTime()を足したものが0.01.0に収まるように調整したもの」で更新
		//小数は == 1.0 とか == 0.0 とかできません。なぜかは鈴木先生に習ったよね。
                //仕方ないので1.0との誤差(差分)を計算して、それが10のマイナス10乗よりも小さかったら(誤差がすんげー小さいとき)もはや等しいでしょ、ということにする
		if (abs(_map.moveRatio - 1.0) < 1e-10) {
			_map.moveRatio1.0と等しいってことは、動きの演出が終わったってことなので
             _map.moveRatioとisMovingを初期化してやります
			//今はいいけど後々FROMデータも初期化しないとね
	}
}

この関数により、

  1. 入力でisMovingがONになる
  2. 毎フレームUpdateCharPosSmoothMoveによりmoveRatioが更新
  3. moveRatioが1.0になったら、moveRatioを0にリセット、isMovingをOFFにする

という流れができる。
UpdatePlay内の入力をとった次あたりにこれを呼んであげよう!

gameSequence.cpp
//UpdatePlay内
	else
	{
		nextDir = GetDirection();
		MoveObject(nextDir, crrMap);
		UpdateCharPosSmoothMove(crrMap);
	}

実行結果

初期状態から動けそうな方向(笑)を押すと、0~1のタイマーの値が表示されれば希望が見えるはず。

Fig. 4: タイル間移動率タイマー

こまかいとこほげほげ パート4 へ

  • game-engineer/classes/2023/something-else/summertime-special-cource/costcoman-siv3d-7.txt
  • 最終更新: 2年前
  • by root