====== 細かいところほげほげ、略して!こまほげー(小堺さん最近見ないなぁ) ======
==== プレイヤーを表示 ====
次に、今の状態でプレイヤーを表示してみよう\\
DrawStaticObjectは完成しているので、スムーズにプレイヤーと荷物を移動させながら表示する関数\\
* **void DrawMovableObject(Map& _map);**
を作成する。\\
実は、今までの表示関数とそんなに変えなくてもよい。(まだ中間地点の表示できなくてもよい)\\
動かないオブジェクト以外の時の処理(荷物とプレイヤーの表示)を、表示のswitch-case文のところで処理してやるだけである。\\
あとは、今後、中間地点の処理が出てくるのでPoint型(整数対応)ではなくVec2型(実数対応)で処理していくことにする。\\
DrawMovableObjectの作成
void DrawMovableObject(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"),
};
ColorF col{ 1.0, 1.0 };
if (isShiftOn)
{
col = ColorF{ 1.0, 0.5 };
}
//方向のベクトルを実数対応にする
Vec2 dirVector[5] =
{
{ 0, -1 },{ -1, 0 },{ 0, +1 },{ +1, 0 },{ 0, 0 }
};
for (int j = 0; j < _map.stage_height; j++) {
for (int i = 0; i < _map.stage_width; i++) {
OBJNAME objNum = GetObjectNum({ i,j }, _map);
//オブジェクトの位置を実数対応にする
Vec2 pos = CHR_SIZE * Vec2{ i,j };
switch (objNum)
{
case HUMAN_ON_GOAL:
case LUGG_ON_GOAL:
//プレイヤー、荷物 on the オブジェクトの時
objNum番に対応した、Textureを(pos, col)でdraw
//(Shift押してたらcolの切り替えで半透明になるよ)
break;
case HUMAN:
case LUGG:
//プレイヤー、荷物 on the 床の時
ObjImg[objNum].draw(pos);
break;
default:
continue;
break;
}
}
}
}
次に、さっき作った新しい描画関数\\
**void DrawStageSmoothMove(Map& _map)**を書き換える\\
void DrawStageSmoothMove(Map& _map)
{
Scene::SetBackground(Palette::Darkgrey);
//動かないオブジェクトの描画
DrawStaticObject(_map);
ここでプレイヤーや荷物などの動くオブジェクトの描画関数を呼ぶ
}
これで、isMoving状態の0~1タイマー表示+プレイヤー、荷物表示ができる。
=== 実行結果 ===
{{game-engineer:classes:2023:something-else:summertime-special-cource:20230821-120858-883.png?300}}
isMoving状態タイマーとPlayer、LUGGの表示
ついでに、\\
- 入力があったタイミングでisMovingがtrueになり
- UpdateCharPosSmoothMoveによって、isMoving状態のタイマーが1.0を超えたときにisMovingがfalseに切り替わっている
かどうかを確認しよう。どうしたらいい?\\
簡単なのはPlayUpdateの入力取得の処理(GetDirection)の前に、isMovingをPrintで表示してみるって作戦\\
isMovingの逐次表示
省略
else
{
Print << isMoving;
nextDir = GetDirection();
MoveObject(nextDir, crrMap);
UpdateCharPosSmoothMove(crrMap);
}
=== 実行結果 ===
入力前はisMovingはfalseで、入力があると同時にtrueになりmoveRateのカウントアップが始まり、moveRateが1.0を超えるとfalseに戻るのが確認できる。\\
{{game-engineer:classes:2023:something-else:summertime-special-cource:20230821-121543-394.png?300}}
{{game-engineer:classes:2023:something-else:summertime-special-cource:20230821-121555-821.png?300}}
{{game-engineer:classes:2023:something-else:summertime-special-cource:20230821-121558-294.png?300}}
入力前、入力あり、入力1秒後のisMoving表示
==== isMovingがtrueの間は入力を受け付けなくする ====
今のままだと、isMovingがtrueの間、すなわち「移動のアニメーションが再生されている間」も入力を受け付けてしまい、アニメーションがしっちゃかめっちゃかになる。\\
アニメーションが再生されているmoveRateが0~1の間の時間は、入力を受け付けないようにしたい。\\
どうしたら実現できるだろう。。。\\
現在、移動のための入力を受け付けているのは、GetDirectionを呼んでいる箇所のみである。\\
isMovinがfalseの時だけ入力を受け付けるようにすればいいよね?\\
ちなみに、UpdateCharPosSmoothMove(crrMap);はisMovingがtrue,falseにかかわらず更新しなければならないのはわかるよね。\\
総合すると。。。どうしたらいいか考えてやってみよう!\\
isMovingの間に入力があってもプレイヤーが移動しなくなれば、ソースコードは正しいことになるので確認してみよう\\
==== Fromデータの記録 ====
次に、移動前座標から、移動先座標までのアニメーションを作るために、移動先座標に新しいデータFromデータを作成する。\\
というか、追加してあるので使っていく\\
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];
};
とりあえずInitCrrStage関数の中では、FromデータはすべてNONE(方向なし)で初期化してある。\\
多分、移動が終了(isMovingがオン ⇒ moveRatioが0 ⇒ moveRatioが1 ⇒ isMovingがオフの一連の流れの後)した後Fromデータは次の移動のために初期化しないといけなさそうなので、\\
初期化用の関数を作っておこう。これは、引数で指定したマップのすべてのFormデータをNONEにしてやるだけなので簡単。\\
プロトタイプ宣言をgameSequence.hに追加しとくのを忘れないように。\\
ResetFromData関数の追加
void ResetFromDat(Map& _map)
{
//移動率を0にしておく
_map.moveRatio = 0.0;
//今度は、マップのプレイヤーが移動する可能性のあるデータのみなので_map.stage_height,_map.stage_widthを使っても大丈夫
for (int j = 0; j < _map.stage_height; j++) {
for (int i = 0; i < _map.stage_width; i++) {
_map.From[j][i]にNONEを代入することで初期化
}
}
}
==== Fromデータの意味と実際の記録法 ====
ステージを表すタイル配列の、ある位置{i, j}には、 int Dat[j][i]と、direction From[j][i]のデータがある。と思うとよい\\
Fromデータは、文字通りその座標にあるオブジェクトがどこからやってきたか前の位置の座標からの方向が入っている。\\
例えば、(x+は右方向、y+方向は下方向)プレイヤーが {1, 2}の座標から、次の位置{1, 3}に移動したとすると、タイル座標では、1タイル分下にプレイヤータイルが動いていることになる。\\
この場合は、Dat[3][1]=HUMAN、でFrom[3][1]=UP、を代入しておく。\\
このデータを見ることによって、**現在のフレームの{1,3}のプレイヤーは上からやってきて現在に至る**。ということが把握できるようになる。\\
(なんか、某分厚いゲーム系の有名本を参考にデータ形式考えたけど、移動した方向と逆の方向を記録しなきゃないから、頭こんがらかるよね。That confused me.)\\
{{game-engineer:classes:2023:something-else:summertime-special-cource:fromdatta.png?600}}
Fromデータの記録
後は、isMovingがtrueになるタイミングで、Fromデータも記録してやればよいと思う。\\
方向入力があった時のdirection(入力方向)と移動前位置から、移動後の位置のFromデータをセットする関数として、\\
**void SetMoveObject(direction _dir, Point _pos, Map& _map);**\\
を、作成する。これは、指定位置に方向をセットするだけなのでそんなに難しくない。\\
SetMoveObjectの作成
void SetMoveObject(direction _dir, Point _pos, Map& _map)
{
if (入力方向が方向なしだったら)
return;//そのままおかえり願う
//こいつらも、何回も出てくるからお外に出しちゃってもよかったけど、面倒だからそのままにしたよ。
Point dirVector[5] =
{
{ 0, -1 },{ -1, 0 },{ 0, +1 },{ +1, 0 },{ 0, 0 }
};
direction fromDir[5] =
{
UP, LEFT, DOWN, RIGHT, NONE
};
if (移動アニメーション中でなければ) {
_map.moveRatio を0で初期化;
指定位置{_pos.x, _pos.y}のFromデータに、どっちから来たかという方向fromDir[_dir]をセット。
}
}
MoveObjectの中で、移動後の座標と、もし荷物を押す場合は、荷物が動いた後の座標として、
* Point nextPos
* プレイヤーの移動後の座標
* Point nextNextPos
* 荷物を押す場合の、荷物の移動後の座標
が得られるので、その位置にFromデータ記録を仕込む。(SetMoveObjectを呼ぶ)
MoveObject関数の更新
void MoveObject(direction _dir, Map& _map)
{
省略
OBJNAME crr = GetObjectNum(pp, _map);
OBJNAME next = GetObjectNum(nextPos, _map);
OBJNAME nextNext = GetObjectNum(nextNextPos, _map);
switch (next)
{
case FLOOR:
プレイヤーの移動処理(nextへ)
◎Fromデータのセット(next)
プレイヤーが動くならisMovingをtrueに
break;
case WALL:
break;
case GOAL:
プレイヤーの移動処理(nextへ)
◎Fromデータのセット(next)
プレイヤーが動くならisMovingをtrueに
break;
case LUGG:
switch (nextNext)
{
case FLOOR:
荷物の移動処理(nextNextへ)
◎Fromデータのセット(荷物分 nextNext)
プレイヤーの移動処理(nextへ)
◎Fromデータのセット(プレイヤー分 next)
プレイヤーが動くならisMovingをtrueに
break;
case GOAL:
以下同様なので省略するけど考えてね
default:
break;
}
}
==== Fromデータから移動前の座標を計算 ====
今ある座標$p(x, y)$から右に動いて$p'(x',y')$に移動したのを右向きのベクトル$dir(1,0)$を使て$p' = p + dir$とあらわす。\\
とすると、逆に、移動前の座標は$p = p' - dir$で表される。\\
つまり、void DrawMovableObject(Map& _map)の中でFromデータがあると、移動前の座標を以下のように求められる。
実際に関数内に記述して計算し、その位置にプレイヤーを表示してみる。\\
これを実行したときに、どうなるかソースコードを呼んで想像できた人は、結構プログラミングに慣れてきた人だね。\\
移動前座標の計算
//Vec2 pos = Vec2{ i,j } - dirVector[_map.From[j][i]];で、移動前の位置を逆算できるよ
void DrawMovableObject(Map& _map)
{
省略
Vec2 dirVector[5] =
{
{ 0, -1 },{ -1, 0 },{ 0, +1 },{ +1, 0 },{ 0, 0 }
};
for (int j = 0; j < _map.stage_height; j++) {
for (int i = 0; i < _map.stage_width; i++) {
OBJNAME objNum = GetObjectNum({ i,j }, _map);
//移動前の配列上の座標を求めて
Vec2 pos = Vec2{ i,j } - dirVector[_map.From[j][i]];
//キャラクターサイズをかけて、表示座標に変換
pos = pos * CHR_SIZE;
//ここから下は変更なし。
switch (objNum)
{
case HUMAN_ON_GOAL:
case LUGG_ON_GOAL:
ObjImg[objNum].draw(pos, col);
break;
case HUMAN:
case LUGG:
ObjImg[objNum].draw(pos);
break;
default:
continue;
break;
}
}
}
}
=== 考察してみる ===
現在のプログラムの流れは以下のようになる。\\
- キー入力
- 新しい配列座標P'にプレイヤーを移動
- Fromデータをセット
- isMovingがON
- 移動前の座標Pを計算
- moveRatioカウントアップスタート(初期値0)
- moveRatioが1.0になるまで
- 移動前の座標Pにプレイヤーの画像を表示
- moveRatioが1.0になったら
- isMovingをOFFに
- moveRatioを0にリセット
- Fromデータをリセット
- 移動後の座標P’にプレイヤーの画像を表示
のようになる。つまり、入力してから、配列側のデータは即座に座標が変更されるが、表示座標はmoveRatioが0⇒1.0になってから(moveRatioはDeltaTime足しているのでおおよそ1秒後)移動される。\\
つまり入力から1秒たってから、新しい座標にプレイヤー画像が移動するはず!\\
試してみよう。\\
=== 実行結果 ===
{{game-engineer:classes:2023:something-else:summertime-special-cource:siv3d_app_debug_build_d3d11_145_fps_f_800x600_v_800x600_s_800x600_2023-08-21_16-01-11.mp4?300}}
入力から1秒後に移動
移動するまでの時間は、moveRatioすなわち、\\
void UpdateCharPosSmoothMove(Map& _map)
{
if (isMoving) {
Print << _map.moveRatio;
_map.moveRatio = Clamp(_map.moveRatio + Scene::DeltaTime(), 0.0, 1.0);
この部分で決まっている。moveRatioが0~1になるのに現在は1秒かかっているので、2秒にしたければScene::DeltaTime()/2.0を足していけば良い\\
逆に0.5秒で0⇒1にしたければ2*Scene::DeltaTime()/0.5;にすればよい。\\
グローバル変数でdouble speed;を宣言し\\
dobule speed = ○○; //○○秒で移動完了
省略
_map.moveRatio = Clamp(_map.moveRatio + Scene::DeltaTime()/speed, 0.0, 1.0);
とすれば、speed秒で移動完了させることができる。ただし、speedは絶対0にはできないので注意\\
==== moveRatioを使って、中間位置を算出 ====
ここまで来たらもう少しである。\\
道具はそろっているので、後はvoid DrawMovableObject(Map& _map)の中で、移動前の座標から、FROM方向にCHR_SIZE*moveRatio移動したところにプレイヤーや荷物を表示すればいい。荷物を押しているときは、プレイヤーと荷物は同時に同じ速さで動くはずなので、同じmoveRatioが使える。\\
{{game-engineer:classes:2023:something-else:summertime-special-cource:interpolation.png?400}}
Fromデータと、moveRatioを使った表示法
=== 表示位置の算出 ===
表示位置の算出は以下のような方針でやってみる\\
FromデータとmoveRatioは、UpdateCharPosSmoothMoveで自動的に切り替わるようになっているので、表示関数DrawMovableObjectでは、それを使うだけでよい\\
Vec2 dirVector[5] =
{
{ 0, -1 },{ -1, 0 },{ 0, +1 },{ +1, 0 },{ 0, 0 }
};
が、宣言されているものとして。\\
- dirVector[From[j][i]]から、移動前座標からの移動方向ベクトルを求める(PointじゃなくVec2なことに注意)
- 現在座標P’(移動後の座標)Vec2{ i,j }から、dirVector[_map.From[j][i]]を引いて移動前の座標Pを求める(これは前にやってあるよね)
- 移動前の座標Pからの移動率0~1(つまりmoveRatio)をdirVectorにかけて、前の座標からの方向付きの移動率interpolationVectorを求める
- 移動前の座標Pに、上で求めたinterpolationVectorを足して、中間座標を求める
- 最後にCHR_SIZEをかけて表示座標に変換して、表示
滑らか表示関数の修正
void DrawMovableObject(Map& _map)
{
省略
Vec2 dirVector[5] =
{
{ 0, -1 },{ -1, 0 },{ 0, +1 },{ +1, 0 },{ 0, 0 }
};
for (int j = 0; j < _map.stage_height; j++) {
for (int i = 0; i < _map.stage_width; i++) {
OBJNAME objNum = GetObjectNum({ i,j }, _map);
Vec2 interpolationVectorを計算(dirVector[_map.From[j][i]]に _map.moveRatioをかける)
Vec2 posに移動前の座標を計算
switch (objNum)
{
case HUMAN_ON_GOAL:
case LUGG_ON_GOAL:
pos = CHR_SIZEとposとinterpolationVectorをつかって表示座標を計算
ObjImg[objNum].draw(pos, col);
break;
case HUMAN:
case LUGG:
posに表示座標を計算
ObjImg[objNum].draw(pos);
break;
default:
continue;
break;
}
}
}
}
そして表示してみる!\\
speed=2.0(2秒で1コマ移動)で設定した例\\
大体狙った通りに動いていると思う。\\
{{game-engineer:classes:2023:something-else:summertime-special-cource:siv3d_app_debug_build_d3d11_145_fps_f_800x600_v_800x600_s_800x600_2023-08-21_17-10-10.mp4}}
滑らか移動の例
=== クリア時のチェック ===
大体、speed=0.3~0.5ぐらいが小気味よく動くと思うので試してほしい。\\
全部うまくいったかな?と思うと、全部荷物を片付けたときに、最後まで荷物が動く前にクリア画面に移動してしまう。\\
これはどうしてだろうか?\\
(こういうバグの原因が、すぐに想像できるようになるとプログラミングスキルが上がってくるよ)\\
これは、クリアチェックのタイミングのせいである。UpdatePlay内では以下の順番で処理をしている。\\
if (CheckClear(crrMap))
{
オーディオ再生やタイマーなどクリア処理へ
}
else
{
移動の更新
}
そんで、移動はデータ上は先に移動してクリア状態になったものを、後からmoveRatioで表示だけ移動している。のである。\\
なので、移動の表示が全部終わる前にクリアチェックに引っかかっちまうのであった。。。\\
さてどうしようか?\\
これは、isMovingを使うことで簡単に解決できる。isMovingがONの時はクリア処理に行かなければいいのである。\\
if (CheckClear(crrMap))
{
//配列上はクリア状態
if (だけど、isMovingがONだったら) {
UpdateCharPosSmoothMoveを呼んで位置を更新してreturnしちゃう
}
オーディオ再生やタイマーなどクリア処理へ
}
else
{
移動の更新
}
これで大体ゲームとしてはいい感じになると思う!
==== 最後の最後 ====
後は、
* 1手戻し
* ステージの追加
* 手数の表示
* エンディングの追加
* スコアの追加
* リプレイ・再生機能
などの追加が考えらえるが、後は自分でいろいろやってみよう。