コスコマン動く
次に、コスコマンを動かしてみよう。
え、文字って動かせるの?と思ったかもしれないが、今まで授業でやってきたアニメーションの画面表示と同様に、
ちょっとずつ文字を動かしていくと動いているように見える。
あとは、通常のグラフィック表示の時のように、毎フレーム画面をクリアできれば、簡易的なコンソール画面でのアニメーションが実現できそう
意味は分からなくていいので、空のプロジェクトを一戸作って以下のプログラムを実行してみてほしい。
#include <iostream>
#include <thread>
#include <string>
using namespace std;
int main() {
string str = "___________________O";
string mleft;
while (true) {
mleft = str.back();
str.pop_back();
str = mleft + str;
system("cls");
cout << str << endl;
std::this_thread::sleep_for(std::chrono::milliseconds(30));
}
}
ボールが、左から右に動いているよね!
こんな感じで、毎フレーム画面を無理くりクリアして、coutで位置を合わせて表示をすることで画面表示と簡易的なフレーム表現を行う。
今作っているゲーム、コスコマンでは、キャラクターは、ユーザーの入力がないときには全く動かず、ユーザーがキー入力をした時だけキャラクターが動く
なので、フレーム更新のタイミングは、「ユーザーがキー入力をした時」が、よさそうである。
ここで一つ問題点、
悲報:今まで習ってきたキー入力はエンターキー押すまで確定しない
え、別にいいじゃん。って、思った人もいるかもしれないが、例えばwasdで上下左右に動くゲームがあったとすると、
########## //右に3コマ動きたいときに、本来であれば # # //入力:ddd # # //で済むはずが、cinやgetlineはエンターキー入れるまで入力が終わらないので、 # @→→→@ # //入力:d⏎d⏎d⏎ # # //⏎はエンターキーの入力を表す。 ########## //になってしまう。
これは、とてもじゃないけど遊べん!さてどうしよう。
今回はとても特殊な入力関数を使う(多分今後二度と使わない人は使わないと思うので、覚えなくてもいいよ)
せめてゲームらしく
後々の形を考えて、タイトルとかクリア画面を追加したいので、いつものようにPlay画面の更新をしてる風に
- void UpdatePlay();
- void DrawPlay();
を作ってしまって、main側を
int main() { while (true) { DrawPlay(); UpdatePlay(); } return 0; }
こんな感じに表示するようにしてみよう。
(グラフィックのゲームループでいう、フレームのループになってる部分だね。)
ただ、このままだと無限に全力ループかますのでこっちに入力する隙を与えてくれない。。。
そこで以下のように、2つの関数を作っていく。
Update内の_getch()は、前に授業でやった「文字を1文字入力してエンター押すとASCIIコードで取得する int getchar();」の仲間で、
「文字を1文字ASCIIコードで取得するけど、押した瞬間とっちゃうんだからね。でも入力するまでは待ってあげる」という関数である。
つまり、画面に入力文字が残らないけど、キーボード押すと変数に文字は入力されちゃいます。(ゲームに使えそうだよね)
system(“cls”);はコンソール画面で使える命令をC++の上から呼び出しちゃう反則すれすれの関数で、今回は画面クリア命令を呼び出している。
これで、入力待ちとフレームごとの画面クリアが、コンソール画面で実現できる(完璧な処理ではないけど)
#include <string> #include <conio.h> #include <iostream> void UpdatePlay(); void DrawPlay(); void DrawPlay() { system("cls"); DrawStage(sampleSatge); cout << "Input wasd to Move Costco M@n" << endl; } void UpdatePlay() { int c = _getch(); }
実行結果
結果は以下の画面のようになり、入力待ちになるのでなんか押してみてほしい。
_getch()でとった入力は何もせず捨てているので何も起こらないが、
入力待ち ⇒ 入力 ⇒ 画面クリア ⇒ 表示
のループができているのがわかる。
初めの一歩(右)
次に入力を反映させてみる。
通常のアニメーションと同じで、内部でプレイヤー位置を動かしておいて、画面クリアして描画すれば動く。はず
右にプレイヤーを動かしてみよう。
- プレイヤーの現在位置の座標を取得、保存
- プレイヤーの位置を右に移動
- 元のプレイヤーの位置に空白を代入
- 配列を画面に表示
各オブジェクトの位置は、ステージの配列のインデックスを座標代わりに配置されている。
costco manは4番なので探してみると、現在は sampleStage.Dat[2][2]([行][列])の位置にある。
これを、x,yの座標になおすと座標は(列、行)の順になるから注意が必要。
{1,1,1,1,1,1,1,1,1,1},
{1,0,0,2,3,3,0,0,0,1},
{1,0,4,2,0,0,0,0,0,1},
{1,1,1,1,0,1,1,0,0,1},
{1,0,0,0,0,2,0,0,0,1},
{1,0,0,3,0,0,0,0,0,1},
{1,0,0,1,1,0,1,0,0,1},
{1,0,0,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,0,0,1},
{1,1,1,1,1,1,1,1,1,1}
(なんかプレイヤーの位置2,2に置いたのはしっぱいだよねぇ。。。絶対わかりづらい(やる気が出たら後で直そう))
プレイヤーの位置を、座標で取得する関数をつくる。座標はxyの座標を表すPoint型を作っちゃおう
プレイヤーを探すときはenum OBJNAMEを使っていこう
//これは前に書いてあるから書かなくていいよ、これ使うよってこと ------
enum OBJNAME {
FLOOR, WALL, LUGG, GOAL, HUMAN, HUMAN_ON_GOAL, LUGG_ON_GOAL
};
//ここまで ---------------------------------------------------------
struct Point
{
int x, y;
};
Point GetPlayerPos(Map& _map)
{
for (int j = 0; j < _map.stage_height; j++)
{
for (int i = 0; i < _map.stage_width; i++)
{
if (配列が、OBJNAME::HUMANだったら)
return(座標を返す);
}
}
return{ -1,-1 };//オブジェクトが見つからないとかエラー
}
次に、プレイヤーを右に動かせばいいのだが、オブジェクトをマップにセットする関数が欲しい。
void SetObjtoMap(OBJNAME _obj, Point _pos, Map& _map)
{
_map.Datの_pos.x, _pos.yに対応する要素(_obj)をセットする
//領域チェックとかをしないとダメだよ
//(完成するとエラーが起こらない配置に自然と変わるけどね)
}
あとは、実際に動かしてみる。
UpdatePlayerで、座標を更新していく
void UpdatePlay()
{
//入力を取得
int c = _getch();
if(入力のASCIIコードが'd'または'D'なら(好きじゃないけどwasdでいどうにする。。。))
{
Point crrP = 現在位置を取得
Point nextP = {x座標を更新, y座標は動かない};
SetObjtoMapでグローバル変数sampleSatgeのcrrPに対応する位置にOBJNAME::FLOORを設定(元居た場所からプレイヤーを削除して床をセット)
SetObjtoMapでグローバル変数sampleSatgeのnextPに対応する位置にOBJNAME::HUMANを設定(計算したプレイヤー位置にプレイヤーをセット)
}
}
あとは表示してみよう。ちょっとおかしなことが起こるけど、こいつ動くぞ
ほかのオブジェクトをすべて床にしながら、@が右に移動すると思う。
とりあえず気にしないで、上下左右移動に対応させてしまおう
void UpdatePlay()
{
//入力を取得
int c = _getch();
//ここで入力待ちされるため、これ以降でcに何も入ってないことはない
Point crrP = 現在位置を取得
Point nextP;
if(入力のASCIIコードが'd'または'D'なら(好きじゃないけどwasdでいどうにする。。。))
{
nextP = {x座標を更新, y座標は動かない};
}
else if(入力のASCIIコードが'a'または'A'なら(好きじゃないけどwasdでいどうにする。。。))
{
nextP = {x座標を更新, y座標は動かない};
}
else if(入力のASCIIコードが'w'または'W'なら(好きじゃないけどwasdでいどうにする。。。))
{
nextP = {x座標は動かない, y座標を更新};
}
else if(入力のASCIIコードが'd'または'D'なら(好きじゃないけどwasdでいどうにする。。。))
{
nextP = {x座標は動かない, y座標を更新};
}else
{ //変な入力あったら(wasd以外)何も変わらず
nextP = crrP;
}
SetObjtoMapでグローバル変数sampleSatgeのcrrPに対応する位置にOBJNAME::FLOORを設定(元居た場所からプレイヤーを削除して床をセット)
SetObjtoMapでグローバル変数sampleSatgeのnextPに対応する位置にOBJNAME::HUMANを設定(計算したプレイヤー位置にプレイヤーをセット)
}
実行結果
なんか、画像で表現できないので画像は省略
ほかのオブジェクト食べちゃうけど、上下左右に移動できることを確認!