はじめに
今回はぷよを回転させていきたいと思います。
ぷよが回転できるようになればぐっとPaizaCloudでぷよぷよっぽくなりますね(連鎖は発生しません。)
細かい回転規則にも注意して実装していきましょう。
回転規則
組みぷよの回転規則は次のようになります:
① 回転の軸は、組ぷよが落下体勢に入った際の下側のぷよが担う。
② 回転先のぷよがフィールドの壁・床・他のぷよに入ってしまう場合は、組ぷよを平行移動させて整合性を保つ。
③ ②が解決されない場合は操作は実現させずに、180度回転となる際に回転が実現する。
①は補足する必要はないと思います。
②、③は補足していきます。
②
例えば、組ぷよがフィールドの右下に来たとき、
(画像)
ここで右回転しようとすると、②の処理なしだとこのようになります:
(画像)
これでは壁にめり込んでしまっているので、左に1マス平行移動することによって整合性を保ちます:
(画像)
ちなみに、
(画像)
このような状態で右回転をしようとすると、
(画像)
上に1マス平行移動することによって整合性を保ちます。
このようにして、狭い箇所での回転を実現します。
③
例えば、図のような状況としましょう。
(画像)
ここで右回転しようとしても組ぷよは横にはなれないので、回転は実現しません。
でも、更に右回転しようとすると、
(画像)
180度回転します。
横回転はできなくても180度回転ができる場合があるので、このような実装が必要となります。
回転を定義(①)
では、①を実装していきましょう!
回転を行うために、少々数学的な思考を使います。
今、図は●が回転するぷよの座標(rx, ry)が指す点、✖が軸となるぷよの座標(ax, ay)が指す点を表しているとします。
(画像)
これを右回転するとして、(rx, ry)が回転した後の座標を(X, Y)とします。
(画像)
ここで注意したいことは、ターミナルは半角を1マスとして定義しているので、マスが縦長になっていることです。
目に見える軌跡は円を描いて欲しいので、マス目を正方形に整形してグラフに描くとこのような軌跡になります。
(画像)
これは、中心(ax, ay)、長径4マス、短径2マスの楕円を表しています。
この楕円を中心が原点に来るように平行移動させると、
(画像)
このようになります。
このグラフは、y^2/4 + x^2 = 1 という式で表すことができます。
今からは、点(rx - ax, ry - ay)から原点を中心にθだけ楕円上を移動した点(x', y')を求めます。
(画像)
媒介変数表示して、
rx - ax = 2kcosψ
ry - ay = ksinψ
(k:正の実数, ψ:実数)
加法定理を用いて、
x' = 2kcos(ψ + θ)
= 2k(cosψcosθ - 2ksinψsinθ)
= 2kcosψcosθ - 2ksinψsinθ
= (rx -ax)cosθ - 2(ry - ay)sinθ (∵ rx - ax = 2kcosψ, ry - ay = ksinψ).
∴ x' = (rx -ax)cosθ - 2(ry - ay)sinθ.
y' = ksin(ψ + θ)
= k(sinψcosθ + cosψsinθ)
= ksinψcosθ + kcosψsinθ
= ksinψcosθ + (1/2)*2cosψsinθ (∵ 1 = (1/2)*2)
= (ry - ay)cosθ + (1/2)(rx - ax)sinθ (∵ rx - ax = 2kcosψ, ry - ay = ksinψ).
∴ y' = (ry - ay)cosθ + (1/2)(rx - ax)sinθ.
今、楕円を原点に平行移動させてきていたのでもとの場所に戻すと、
X = x' + ax
= (rx -ax)cosθ - 2(ry - ay)sinθ + ax.
∴ X = (rx -ax)cosθ - 2(ry - ay)sinθ + ax.
Y = y' + ay
= (ry - ay)cosθ + (1/2)(rx - ax)sinθ + ay.
∴ Y = (ry - ay)cosθ + (1/2)(rx - ax)sinθ + ay.
このようにして回転移動した先の座標を得ることができました。
これを用いてぷよを回転させるrollPuyof関数を作成していきます。
数学関数を用いるのでヘッダにmath.hをインクルードします。
※math.hをインクルードした場合、コンパイルする際に語尾に -lm を付け加えてください。
これがないとコンパイルできません!
___game.h____
#include <stdio.h> #include <stdlib.h> #include <termios.h> #include <unistd.h> #include <fcntl.h> #include <time.h> #include <sys/time.h> #include <math.h> #define ... typedef struct { ... } twinPuyoes_t; int kbhit(void); void putColor(int, int, int); int isPassedTime(unsigned long int, struct timeval *); twinPuyoes_t initPuyo(void); void rollPuyof(twinPuyoes_t *, int);
____rollPuyof.c____
#include "game.h" void rollPuyof(twinPuyoes_t *tsumo, int degree) { //回転前の状態を取得 twinPuyoes_t before = *tsumo; //回転座標を代入 tsumo -> rx = (before.rx - before.ax) * cos(degree * M_PI / 180) - 2 * (before.ry - before.ay) * sin(degree * M_PI / 180) + before.ax; tsumo -> ry = (before.ry - before.ay) * cos(degree * M_PI / 180) + (1.0 / 2) * (before.rx - before.ax) * sin(degree * M_PI / 180) + before.ay; //回転状態も更新 //右回転 if(degree == 90){ tsumo -> angle += ANGLE_90; tsumo -> angle %= ANGLE_MAX; //回転状態は360度 = 0度 } //左回転 else if(degree == -90){ tsumo -> angle += ANGLE_270; tsumo -> angle %= ANGLE_MAX; } }
____puyo.c____
#include "game.h" int main(void) { //乱数の初期化 srand((unsigned int)time(NULL)); system("clear"); //画面のクリア //フィールドの描画 for(int y = 1; y <= FIELD_HEIGHT; y++){ for(int x = 1; x <= FIELD_WIDTH; x++){ if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT)) putColor(y, x, CELL_TYPE_WALL); //壁・床の描画 } printf("\n"); } //ぷよの状態をここに格納、壁・床は含めない。 //簡略のため画面の座標通りに情報を格納していく。 int mainField[FIELD_HEIGHT - 1][FIELD_WIDTH - 1]; //初期化 for(int y = 1; y < FIELD_HEIGHT; y++){ for(int x = 1 + 2; x < FIELD_WIDTH - 1; x += 2){ //空白で埋める mainField[y][x] = CELL_TYPE_EMPTY; } } //ゲーム部分 while(1){ ...... while(1){ //自由落下 if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){ ...... } if(kbhit()){ //キーが入力されたかどうかのフラグ int keyFlag = FALSE_BOOL; //位置を変更するとき、もとの位置のぷよはクリアしたい //もとのぷよの情報をここに保持しておく twinPuyoes_t old; switch(getchar()){ case 'a':{ ...... } case 'd':{ ...... } case 's':{ ...... } //右回転 case 'k': old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; rollPuyof(&tsumo, 90); keyFlag = TRUE_BOOL; break; //左回転 case 'j': old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; rollPuyof(&tsumo, -90); keyFlag = TRUE_BOOL; break; default: break; } if(keyFlag == TRUE_BOOL){ //oldの消去 putColor(old.ry, old.rx, old.rollPuyoColor); putColor(old.ay, old.ax, old.axisPuyoColor); //ぷよの描画 putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor); putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor); } } } } return 0; }
実行結果:
(コマンド)
~$ gcc -o run kbhit.o initPuyo.o puyo.o rollPuyof.o isPassedTime.o putColor.o -lm
~$ ./run
実行結果は最後にまとめます。
回転の整合性(②)
②を実装していきます。
先ほど作成したrollPuyof関数を更新していきます。
考え方としては、数値上でとりあえず回転させてみて②の状態を満たしていないなら元に戻して、②の状態なら整合性を取るように平行移動させる、といった感じです。
ここの判断自体は簡単なのですが、問題がひとつだけあります。
フィールドの状態を確認するためには配列mainFieldを用いる必要がありますが、これは2次元配列なので関数の引数に渡す際には色々なトラブルが発生してきます。
そこで、なんとかして関数の引数に渡すべく、新たに構造体info_tを宣言して、その中でmainFieldを扱うこととします。
構造体として宣言しておけば関数の引数に渡した時にトラブルが防げます。
(具体的にどんなトラブルが発生するかは、各自でやってみるとわかります。ポインタ難しい。)
また、mainFieldはフィールドの状態のみを保持していましたが、壁・床という判定も盛り込もうと思います。
____game.h____
#include ...... #define ...... typedef struct { ...... } twinPuyoes_t; typedef struct { int mainField[FIELD_HEIGHT + 1][FIELD_WIDTH + 1]; } info_t; int kbhit(void); void putColor(int, int, int); int isPassedTime(unsigned long int, struct timeval *); twinPuyoes_t initPuyo(void); void rollPuyof(twinPuyoes_t *, int, info_t);
あとはpuyo.c内で使っていたmainField[y][x]みたいなのをinfo.mainField[y][x]のような形に書き直して、rollPuyof関数の引数を正しくしてあげられればOKです。
____puyo.c____
#include "game.h" int main(void) { //乱数の初期化 srand((unsigned int)time(NULL)); system("clear"); //画面のクリア //フィールドの描画 for(int y = 1; y <= FIELD_HEIGHT; y++){ for(int x = 1; x <= FIELD_WIDTH; x++){ if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT)) putColor(y, x, CELL_TYPE_WALL); //壁・床の描画 } printf("\n"); } //ぷよの状態をここに格納、壁・床は含めない。 //簡略のため画面の座標通りに情報を格納していく。 info_t info; //初期化 for(int y = 1; y <= FIELD_HEIGHT; y++){ for(int x = 1; x <= FIELD_WIDTH; x += 2){ //壁・床 if(x == 1 || x == FIELD_WIDTH || y == 1 || y == FIELD_HEIGHT) info.mainField[y][x] = CELL_TYPE_WALL; //空白で埋める else{ info.mainField[y][x] = CELL_TYPE_EMPTY; } } } //ゲーム部分 while(1){ //組みぷよの生成 twinPuyoes_t tsumo = initPuyo(); //初期位置に設置 putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor); putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor); struct timeval startTime; //組みぷよ生成時の時間 gettimeofday(&startTime, NULL); //時間を取得 while(1){ //自由落下 if(isPassedTime(FREE_FALL_TIME_PER_SQUARE, &startTime)){ //移動先にぷよがあるか判定 int puyoFlag = FALSE_BOOL; switch(tsumo.angle){ case ANGLE_0: if(info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_90: case ANGLE_270: if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_180: if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; default: break; } //移動先が適切でなければ終了 if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL){ //ぷよが設置したら、状態を取得 info.mainField[tsumo.ry][tsumo.rx] = tsumo.rollPuyoColor; info.mainField[tsumo.ay][tsumo.ax] = tsumo.axisPuyoColor; break; } twinPuyoes_t old; //再描画用 old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; tsumo.ry++; tsumo.ay++; //再描画 //oldの削除 putColor(old.ry, old.rx, old.rollPuyoColor); putColor(old.ay, old.ax, old.axisPuyoColor); //ぷよの描画 putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor); putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor); //時間をリセット gettimeofday(&startTime, NULL); } if(kbhit()){ //キーが入力されたかどうかのフラグ int keyFlag = FALSE_BOOL; //位置を変更するとき、もとの位置のぷよはクリアしたい //もとのぷよの情報をここに保持しておく twinPuyoes_t old; switch(getchar()){ case 'a':{ //移動先にぷよがあるか判定 int puyoFlag = FALSE_BOOL; switch(tsumo.angle){ case ANGLE_0: case ANGLE_180: if(info.mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_90: if(info.mainField[tsumo.ay][tsumo.ax - 2] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_270: if(info.mainField[tsumo.ry][tsumo.rx - 2] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; default: break; } //移動先が適切でなければ終了 if(tsumo.rx - 2 == FIELD_START_X || tsumo.ax - 2 == FIELD_START_X || puyoFlag == TRUE_BOOL) break; old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; tsumo.rx -= 2; tsumo.ax -= 2; keyFlag = TRUE_BOOL; break; } case 'd':{ //移動先にぷよがあるか判定 int puyoFlag = FALSE_BOOL; switch(tsumo.angle){ case ANGLE_0: case ANGLE_180: if(info.mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_90: if(info.mainField[tsumo.ay][tsumo.ax + 2] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_270: if(info.mainField[tsumo.ry][tsumo.rx + 2] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; default: break; } //移動先が適切でなければ終了 if(tsumo.rx + 2 == FIELD_WIDTH || tsumo.ax + 2 == FIELD_WIDTH || puyoFlag == TRUE_BOOL) break; old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; tsumo.rx += 2; tsumo.ax += 2; keyFlag = TRUE_BOOL; break; } case 's':{ //移動先にぷよがあるか判定 int puyoFlag = FALSE_BOOL; switch(tsumo.angle){ case ANGLE_0: if(info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_90: case ANGLE_270: if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY || info.mainField[tsumo.ay + 1][tsumo.ax] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; case ANGLE_180: if(info.mainField[tsumo.ry + 1][tsumo.rx] != CELL_TYPE_EMPTY) puyoFlag = TRUE_BOOL; break; default: break; } //移動先が適切でなければ終了 if(tsumo.ry + 1 == FIELD_HEIGHT || tsumo.ay + 1 == FIELD_HEIGHT || puyoFlag == TRUE_BOOL) break; old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; tsumo.ry++; tsumo.ay++; keyFlag = TRUE_BOOL; break; } //右回転 case 'k': old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; rollPuyof(&tsumo, 90, info); keyFlag = TRUE_BOOL; break; //左回転 case 'j': old = tsumo; old.rollPuyoColor = CELL_TYPE_EMPTY; old.axisPuyoColor = CELL_TYPE_EMPTY; rollPuyof(&tsumo, -90, info); keyFlag = TRUE_BOOL; break; default: break; } if(keyFlag == TRUE_BOOL){ //oldの消去 putColor(old.ry, old.rx, old.rollPuyoColor); putColor(old.ay, old.ax, old.axisPuyoColor); //ぷよの描画 putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor); putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor); } } } } return 0; }
では、本題のrollPuyof関数を更新していきましょう。
____rollPuyof.c____
#include "game.h" void rollPuyof(twinPuyoes_t *tsumo, int degree, info_t info) { //回転前の状態を取得 twinPuyoes_t before = *tsumo; //回転座標を代入 tsumo -> rx = (before.rx - before.ax) * cos(degree * M_PI / 180) - 2 * (before.ry - before.ay) * sin(degree * M_PI / 180) + before.ax; tsumo -> ry = (before.ry - before.ay) * cos(degree * M_PI / 180) + (1.0 / 2) * (before.rx - before.ax) * sin(degree * M_PI / 180) + before.ay; //回転の整合性 if(info.mainField[tsumo -> ry][tsumo -> rx] != CELL_TYPE_EMPTY){ //上に平行移動 //角に引っかかって持ち上がるケース if(tsumo -> angle == ANGLE_0 && info.mainField[tsumo -> ry - 1][tsumo -> rx] == CELL_TYPE_EMPTY){ tsumo -> ry -= 1; tsumo -> ay -= 1; } //ツモが縦向きになるケース else if(info.mainField[tsumo -> ry - 1][tsumo -> rx] == info.mainField[tsumo -> ay][tsumo -> ax]){ tsumo -> ry -= 1; tsumo -> ay -= 1; } //左右に平行移動 else{ int move = tsumo -> rx - tsumo -> ax; tsumo -> rx -= move; tsumo -> ax -= move; } } //回転状態も更新 //右回転 if(degree == 90){ tsumo -> angle += ANGLE_90; tsumo -> angle %= ANGLE_MAX; //回転状態は360度 = 0度 } //左回転 else if(degree == -90){ tsumo -> angle += ANGLE_270; tsumo -> angle %= ANGLE_MAX; } }
実行結果:
最後にまとめます。
回転の整合性(③)
では最後に180度回転を実装して今回は終了となります。
②を経た結果、軸となるぷよが他の領域に入ってしまった場合にツモの位置を元に戻します。
ただし、すべての情報を元に戻してしまうと180度回転が実装できなくなってしまうので、構造体twinpuyoes_tに180度回転判定用のフラグを追加します。
____game.h____
#include #define ...... typedef struct { int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標 int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標 int rollPuyoColor; //軸でないぷよの色情報 int axisPuyoColor; //軸となるぷよの色情報 int angle; //回転状態, int turn180Flag; //180度回転待ち判定 } twinPuyoes_t; ...... }
では、本日最後のプログラミングとなります。
____rollPuyof.c____
#include "game.h" void rollPuyof(twinPuyoes_t *tsumo, int degree, info_t info) { //回転前の状態を取得 twinPuyoes_t before = *tsumo; //回転座標を代入 tsumo -> rx = (before.rx - before.ax) * cos(degree * M_PI / 180) - 2 * (before.ry - before.ay) * sin(degree * M_PI / 180) + before.ax; tsumo -> ry = (before.ry - before.ay) * cos(degree * M_PI / 180) + (1.0 / 2) * (before.rx - before.ax) * sin(degree * M_PI / 180) + before.ay; //回転の整合性 if(info.mainField[tsumo -> ry][tsumo -> rx] != CELL_TYPE_EMPTY){ //上に平行移動 //角に引っかかって持ち上がるケース if(tsumo -> angle == ANGLE_0 && info.mainField[tsumo -> ry - 1][tsumo -> rx] == CELL_TYPE_EMPTY){ tsumo -> ry -= 1; tsumo -> ay -= 1; } //ツモが縦向きになるケース else if(info.mainField[tsumo -> ry - 1][tsumo -> rx] == info.mainField[tsumo -> ay][tsumo -> ax]){ tsumo -> ry -= 1; tsumo -> ay -= 1; } //左右に平行移動 else{ int move = tsumo -> rx - tsumo -> ax; tsumo -> rx -= move; tsumo -> ax -= move; } } //180度回転 //軸となるぷよが領域に入ってしまったら if(info.mainField[tsumo -> ay][tsumo -> ax] != CELL_TYPE_EMPTY){ //ツモの座標を基に戻す *tsumo = before; tsumo -> turn180Flag++; //180度回転になるなら if(tsumo -> turn180Flag == ANGLE_180){ tsumo -> rx = (before.rx - before.ax) * cos(M_PI) - 2 * (before.ry - before.ay) * sin(M_PI) + before.ax; tsumo -> ry = (before.ry - before.ay) * cos(M_PI) + (1.0 / 2) * (before.rx - before.ax) * sin(M_PI) + before.ay; //フラグのリセット tsumo -> turn180Flag = ANGLE_0; } } //回転状態も更新 //右回転 if(degree == 90){ tsumo -> angle += ANGLE_90; tsumo -> angle %= ANGLE_MAX; //回転状態は360度 = 0度 } //左回転 else if(degree == -90){ tsumo -> angle += ANGLE_270; tsumo -> angle %= ANGLE_MAX; } }
実行結果:
(あとで貼る)
おわりに
ものすごく煩雑としてしまいましたが、なんとか回転も実装することができました。
途中でメインソースで配列mainFieldの仕様変更をしたのでやたら長く感じましたが、ひとまず完成してよかったです。
次回はぷよの設置を完全なものにします。
現状では中途半端に浮くぷよが発生してしまうので、設置が完了したら中途半端なぷよは下まで落下させるようにします。
また、今はカーソルがプレイ中に邪魔になったりしているので、その改善もしていきたいなと思います。
ではでは、お疲れ様でした。
p.s. パソコン壊れました。今は大学のPCから編集等しています。
なので、今は実行結果を貼ることができない環境にあるので、ご容赦くださいな。
また復旧したら実行結果をgif画像でお見せします。