プログラミング講座 #8 C++でのCUIゲーム開発

12.2K Views

May 07, 23

スライド概要

部活用に作成した資料です。

かなり長くなってしまいましたが...とりあえずGitに関する話は読んでおいてほしいです

profile-image

ZOIといいます。Web系のプログラミングとかが多少得意かもです。よろしくお願いします。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

プログラミング講座 #8 C++でのCUIゲーム開発 @ZOI_dayo

2.

今回について 今回は、ターミナル上で動くマインスイーパを作ります 利用言語はC++で、結構複雑なこともやります その過程で、チーム開発や大規模なプロジェクトの開発で必須となるアプリ、 「Git」の使い方も解説します という感じで、今回は結構内容が濃いです! ゆっくり読んでいってください なので、#1~#3と、#4のターミナルを半分くらいと、#5あたりを読んでおいた方がいいかも

3.

プロジェクト作成 今回は今までのものに比べれば大きめのプロジェクトなので、フォルダを作ってそこで全部管理します 好きな場所にフォルダを作ってください 名前はなんでもいいです、僕は「minesweeper」にしました(わかりやすければOK)

4.

Git 次に、作業を「バージョン管理」するためのアプリである「Git」を使います Gitを一言で説明するのは難しいのですが... 例えばコードを作りかけの状態「A」があったとして、そこから編集したときに 「Aに〇〇な変更を加えました!」という感じの情報を保存してくれるものです これがなんの役に立つかというと、例えばv1.0.0というバージョンからv1.0.1に更新した時、 コードのどこが変わったのかみれたり、あとは開発中にバグが取れなくなった時は前のバージョンに戻 せる(簡易バックアップ的な)というのもポイントです 複数人の開発ではまた違う使い方をしますがそれは後ほど

5.

Git まずはインストールです Macの方はもうすでに入っているので気にしないでください Windowsの方は、https://git-scm.com/download/win からダウンロード→インストールしてください 大抵の方は「64-bit Git for Windows Setup」です 途中でAdjusting your PATH env~というのが出てきた時は、「Use Git from the Windows Command Prompt」という真ん中のを選んでください (一番下でも大丈夫です)

6.

Git ここからはmac/Windows共通です ターミナル(mac)またはコマンドプロンプト(windows)をひらき、「git」といれてEnterを押してください なんか出てきたらOKです

7.

GitHub 次に、Gitプロジェクトをオンラインで管理するため「GitHub」というサービスを使います Gitは「履歴管理してくれるアプリ」ですが、 GitHubは「Gitで管理されてるプロジェクトをオンラインで共有するサイト」です まあ、まずはアカウント登録です https://github.com/ から好きに登録してください(アイコンとかプロフィールもやりたければ) これはエンジニア向けSNSでもあると思ってくれれば良いです

8.

GitHub (おまけ) 実はGitHubにはグループみたいなの(Organization)を作る機能があり、PC同好会のグループもあり ます なので、GitHubアカウントを作れたら、ユーザーネームをぜひ教えてください、グループに入れます 何もしなくても、見る専門になってもいいのでぜひぜひ

9.

Git (セットアップ) さて、コマンドラインに戻ってきてください 次は、Gitに名前とメルアドを登録しておきます GitHubと同じでなくても良いはずですが、変える理由も無いのでそのままでいいんじゃ無いかと思い ます (ここから、「コンソールに〇〇を入力してEnter」を「$ 〇〇」と書くことにします) ($は入力しなくて大丈夫です、元から書いてあるかも?) $ git config --global user.name "ZOI_dayo" $ git config --global user.email "[email protected]"

10.

GitHubでのレポジトリ作成 GitHubは「Gitプロジェクトを管理」と説明しましたが、このGitプロジェクトのことは 「レポジトリ」と呼ばれます(repoと略されることも) 緑色の「New」から作ってください

11.

GitHubでのレポジトリ作成 上から、 「アプリ名」 「説明」(空白でOK) 「公開/非公開設定」(大体は公開でいいんじゃない?) 「説明書作るか」(今はなくていい) 「.gitignore作るか」(今はなくていい) 「ライセンスの設定」(なくてもいい) ライセンスは、自分の書いたコードはこの条件を満た せば使っていいよ、というガイドラインです 面白いので気になったらぜひ調べてね

12.

GitHubでのレポジトリ作成 で、なんかよくわからないコマンドが書かれた画面が出ると思うんですが、とりあえずほっといてコマン ドラインに戻ってきてください この「[email protected]:~~~」をコピーしておきましょう 今のレポジトリ作成で、「Git管理のフォルダ」が GitHub上にできました、これをダウンロードします パソコンに作ったminesweeperフォルダへcdで移動します

13.

GitHubでのレポジトリ作成 ここで、このシリーズの#4の18ページでの、「鍵の生成」というのをやってください これは、GitHubとの通信はSSHを使うので、その認証に必要です(詳しくは#4の周辺を見て) できたら、公開鍵(~~.pub)の中身をコピーして、 GitHubの設定ページのSSH keysから登録しましょう 名前は好きなものを、TypeはAuthで、Keyにペーストです

14.

レポジトリのクローン さて、これでGitHubと繋がったか見てみましょう $ ssh -T [email protected]を実行して、自分のユーザー名が含まれていればOKです 繋がったら、あとはダウンロードしてくるだけです minesweeperフォルダにいることを確認して、($ pwd でOK) $ git clone [email protected]:〇〇/〇〇.git です

15.

Gitの細かい仕組み Gitはバージョンで保存する、と言いましたが、実際にはちょっと複雑です RPG等でのチェスト(設置型ストレージ)とインベントリ(持ち運べるポケット、ナップサック)を想像してくだ さい コードを変更した時、その変更はインベントリ的なところに保存されます それをチェストに入れて蓋を閉めれば一つのバージョンとして保存される感じです

16.

Git用語 これを踏まえて、Git世界でよく使われる用語を見てみましょう remote/local : それぞれ「オンライン側(GitHub)」or「手元のパソコン側」のGit管理フォルダ clone : remoteをダウンロードしてくる(そしてlocalを作る)こと commit : さっきの例えだと「チェストを閉める」「履歴の一部になる」こと add/stage : この2つは同じ意味で、「チェストに入れる」「commit予定にする」こと pull : remoteを使ってlocalの履歴を更新すること(履歴はcommitで変化することに注意) push : pullの逆、localを使ってremoteを更新すること とりあえずこのくらいわかったら十分です(困ったら調べたらいいので大体でいいです) チーム開発の時はあと何個か増えますが...まあ後回しです

17.

実際にやってみる さっきのgit cloneしたminesweeperフォルダをVSCodeか何かでひらき、ファイルを作りましょう 僕はC++で今から作っていくのでmain.cppを作りました (違う言語ならうまく読み替えてください) その後、以下を実行します $ git status これで、「Untracked filesなんたら」みたいなやつが出るはずです、これは「過去の履歴にないけど今 あるやつ」つまり「前回のコミット以降で増えたファイル」です

18.

実際にやってみる 次にこうです $ git add main.cpp これで、もう一度statusを打てば、「Changes to be committed」に移動してるはずです これで、「main.cppファイルを作成」と言う操作が「stage」された、つまりコミット予定になったということ です つまり、ここで、こんなふうにすれば... $ git commit -m “main.cppファイルを作成” 「main.cppファイルを作成」という名前のコミットができます

19.

実際にやってみる しかし状況が分かりにくいですね... 実は、gitコマンドはよく使う割にややこしく、直接コマンドを触らないことも多いです (ただし、いつかトラブった時に使うことになるので、今はコマンドでやってもらいました ) 早速アプリを入れましょう、と言っても簡単です いつものVSCodeでフォルダを開くだけです 右側のこのアイコンを押してください→ 「コミット」「ステージ」など見覚えがある単語がありますね!

20.

実際にやってみる なので、これからは基本「Gitメニューのファイルの+ボタンを押す」→「メッセージを入れてコミットをす る」という流れになると思います しかし、履歴の変化がわかりずらいので、 拡張機能を一つ入れます 「git graph」と検索して入れてください

21.

実際にやってみる Gitメニューの右上になんか増えてるので押すと、驚くほど見やすい画面で Gitの履歴が表示されます 僕のは右みたいな感じになってますが、 初めは点は一つくらいしかないと思います こういうふうに、点(=commit)を繋げていくイメージです

22.

実装 さて、Gitは一旦これくらいにしておきます また複数人開発をする時になればいろいろありますが、とりあえずはコミットとプッシュできれば大丈夫 なはずです 次はコードを書いていきます! C++を使うので違う言語でやりたい人は適宜読み替えてください C++については、競プロ編(#5、#6)の知識を使うかもしれません

23.

実装 まず初めにテンプレですね #include <bits/stdc++.h> using namespace std; int main() { // 今から書く } bits/stdc++.hを、stringやvectorなど必要なものだけにすれば多少コンパイルという作業が早くなるの ですが、まあいいでしょう あとusing namespace stdも行儀が悪いのですがまあいいとします

24.

実装 まずは画面を作りましょう サイズはNとしておいて、こんな感じですかね? int N = 10; for(int x=0; x<N; x++) { for(int y=0; y<N; y++) { cout << “- ”; } cout << endl; }

25.

実装 一旦ここまでで実行してみます 実行方法ですが... 今まで競プロをやってきた方なら、AtCoderのコードテストでやってたと 思います ただそれだとできることが限られてくるため、今回は「コンパイラ」を入れます Windowsの人は、http://www.cygwin.com/からCygwinを入れるか、 #4を読んでLinux環境を用意しておいてください macOSの人は何もしなくていいです

26.

コンパイラのインストール Cygwinの場合何もしなくていいです(というかCygwinがコンパイラです) Linuxの場合、$ g++ --versionを入れてNotFound的なことを言われたら $ sudo apt install build-essentialを実行してください(時間かかります) macOSの場合、$ g++ --versionを入れると反応あると思いますが、これはg++という名前ですが Clangという別のコンパイラです(つまり詐欺です) なぜデフォルトでこんな意味のわからないことをしているのか意味がわかりませんが、 このままだと<bits/stdc++.h>が使えないので、どうにかします(次ページ)

27.

コンパイラのインストール (macのみ) どうにかすると言ってもClangじゃないg++ (小泉構文?) を入れるだけです #4を読んでhomebrewを入れておいてください あとは $ brew install gcc、それが終わったら下の2行を実行してください $ ln -s /opt/homebrew/bin/gcc-13 /usr/local/bin/gcc $ ln -s /opt/homebrew/bin/g++-13 /usr/local/bin/g++ その後、$ g++ --versionと打って「g++ (Homebrew GCC … ) …」と出てきたらOKです

28.

コンパイル ではmain.cppのあるディレクトリへ移動して、 $ g++ ./main.cpp してください すると、a.outというファイルができるはずです コンパイルとは、ソースコードをアプリ(実行可能ファイル)(機械語)に変換するアプリです (ここら辺知りたい時は#7とか役立つかも)

29.

実行 つまりa.outはアプリなので、実行できます 実行方法は、$ ./a.out です (ちなみに競プロとかの時も同じようにg++→./a.outでできます) いい感じに出力されましたか? これからも、コードを少し変えるごとにコンパイル→実行してください エラーログが出た場合、よく読めば原因がわかりますGoogle翻訳でいいので読みましょう

30.

マスの状態を管理する このように表示するだけでは、爆弾がどこにあるとかどこが空いているとかの管理ができません なので、それを入れておく変数を作ります forの前、Nの後に以下を書きましょう vector<vector<int>> stage(N, vector<int>(N, -1)) 初期値-1でN*Nの2重配列ですね この数字で状態を保存することにします

31.
[beta]
enum

int型で保存すると、-1なら初期状態、0なら開けられている、1なら爆弾、みたいになりますが、
あとで仕様変更したくなった時、数字が一つづつずれたとしたら地獄が起こります
なので、enumというものを使いましょう!
これををmain()の前に書いて、
enum class State {
DEFAULT,
OPENED,
BOMB,
};
stageは次のように修正します
vector<vector<State>> stage(N, vector<State>(N, State::DEFAULT));

32.

enum enumはなんというか、選択肢みたいな感じです 今回は、「State」というenumにDEFAULT、OPENED、BOMBの3つを設定しましたが、 State型の変数はこれ以外の値は絶対に取りません つまり、数字管理の時、いきなり9999が出てきた時は困りますが、enumを使うことで それが絶対発生しないようになったわけです あと、好きな名前をつけられるのでコードが読みやすいです まあ使ってたらわかるでしょう(int管理で一回書いてみるともっとわかります)

33.

盤面表示 次は、このデータを盤面に反映しなければなりません 盤面表示、は何回も行う操作だと思うので、関数にしてしまいます 引数にさっきのstageを受け取って、表示するという関数です(Nも必要なのでもらっときます) 返り値は特に必要ないのでvoid型にしましょう (コードは長いので次ページ)

34.
[beta]
盤面表示 (コード)

function show(int N, vector<vector<State>> stage) {
for(int x=0; x<N; x++) {
for(int y=0; y<N; y++) {
if(stage[x][y] == State::DEFAULT) cout << “- ”;
else if(stage[x][y] == State::OPENED) cout << “
else cout << “x “;
}
cout << endl;
}
}
で、mainのfor部分をshow(N, stage)に書き換えてください

”;

35.

盤面表示 (おまけ) 今回のコードでは、引数にvec<vec>を渡しているわけですが、関数の呼び出しごとにこれがコピーさ れて渡されてしまいます 今回のアプリの場合10*10程度のサイズなので問題ありませんが、競プロなどの場合巨大なサイズで ある可能性があり、コピーに時間がかかります そのため、値自体をコピーするのではなく、メモリアドレス(数字一つ)を渡すことで早くすることができま す void show(int N, vector<vector<State>> *stage) { … や show(N, &stage) に変更し、 show関数内ではstageではなく(*stage)を使えば良いのです ただし今回影響は薄く、これではコードが読みづらいため、省略します

36.

爆弾配置 爆弾をおくようにしましょう 個数は変数bomb_countで設定するとして、被らずに選び出す必要があります 今回は(あまりよくないのですが)ランダムで選んでみて未選択なら使うことにします (一般的なマインスイーパなら、全体マス数に対して爆弾の個数はかなり少ないので OKです) random_device型の関数でランダムな整数が得られるので、% N によって 縦横それぞれの座標を決めることにします

37.
[beta]
爆弾配置 (コード)

ステージ作成よりあと、showより前にこのコードを書きます
int bomb_count = 5;
random_device rnd;
for (int i=0; i < bomb_count; i++) {
int x = rnd() % N;
int y = rnd() % N;
if (stage[x][y] != State::BOMB) stage[x][y] = State::BOMB;
else i--;
}

38.

現状把握 現状これです バグってたらいい感じに直してください

39.

一旦コミット さて皆さんそろそろ忘れてるかもですが、Gitというものを使っていたんでした このコードも、ようやく爆弾を配置して表示するところまでできるようになったので、 一旦コミットをしておきましょう コミットはセーブポイントに当たるので、もう少し頻繁でもいいのかもしれませんが、 少なくともこのくらいではしておくべきなのでは...? と思います VSCodeでなんとかするなら全部の「+」を押してメッセージ入れてコミット、 コマンドでやるなら $ git add . → $ git commit -m “爆弾配置とステージ表示まで” とか ですね Commit後には忘れずPushしておきましょう

40.

マス開け処理 次にマスを開けれるようにします でもクリックを作るのは大変なので、「a 1」というふうに入力→Enter→盤面表示となるようにします そのために、まずは目盛り(?)をつけましょう show関数を少しいじって、最初に1,2,3…とかa,b,c…とか表示しとけばいいです

41.
[beta]
メモリ表示(コード)

void show(int N, vector<vector<State>> stage) {
cout << " ";
for(int x=0; x<N; x++) cout << x+1 << " ";
cout << endl;
for(int x=0; x<N; x++) {
cout << (char)('a' + x) << " ";
for(int y=0; y<N; y++) {
if(stage[x][y] == State::DEFAULT) cout << "- ";
else if(stage[x][y] == State::OPENED) cout << "
else cout << "x ";
}
cout << endl;
}
}

";

42.

マス開け処理 いい感じに表示されますね Nが10を超えるとずれるのですが、 まあ気にしないことにします (対策したい人はやってみてね)

43.
[beta]
開く処理

open(N, stage, x, y) を作っていきましょう
こんな感じかな?
void open(int N, vector<vector<State>> stage, int x, int y) {
if(stage[x][y] == State::DEFAULT) stage[x][y] = State::OPENED;
else if(stage[x][y] == State::OPENED) cout << “opened” << endl;
else cout << “game over” << endl;
}
しかし、実はこれではうまく動きません

44.

開く処理 main()の中に show(N, stage); open(N, stage, 2, 2); show(N, stage); と書いて実行してみてください 同じステージが2回表示されるはずです これは、35ページあたりの「盤面表示(おまけ)」の話で理解できます open()にstageを渡した時、stageはコピーされてopenの中で使われます つまり、open()中でstageを変更しても、元のmain()のstageは変更されないのです

45.

ポインタでの参照渡し なので、そのまま値を渡すのではなく、そのメモリアドレスを渡します こうすることで、「メモリアドレス」がコピーされるので、 「第84371番地」(適当)をコピーして「第84371番地」を関数にわたし、 それにアクセスすれば、mainとopenで同じデータを共有できるわけです やり方は「盤面表示 (おまけ)」と全く同じです (コードは次ページ)

46.
[beta]
開く処理(コード)

void open(int N, vector<vector<State>> *stage, int x, int y) {
if((*stage)[x][y]==State::DEFAULT) (*stage)[x][y] = State::OPENED;
else if((*stage)[x][y] == State::OPENED) cout << "opened" << endl;
else cout << "game over" << endl;
}
実行は
open(N, &stage, 2, 2);
です
実行したとき、open前後でデータが変わりましたか?

47.

休憩 ここら辺で一旦コミットしてもいいかも? 現状のコードはこれです→ (結構書いたね〜)

48.
[beta]
入力の実装

開く部分を操作できるようにしましょう
普通にcinでもらおうとすればOKです
main()中の、今showやopenを実行してるところを以下のように書き換えます
while(true) {
show(N, stage);
cout << "x y : ";
char x_char;
string y_char;
cin >> x_char >> y_char;
int x = x_char - 'a', y = stoi(y_char) - 1;
open(N, &stage, x, y);
}

49.

入力の実装 実行してみましょう 入力待ちの状態になったら、「a 1」などと入力してEnterです ゲームオーバー処理はないので、Ctrl+Cを押して強制終了してください ようやくゲームらしくなってきた...? かも

50.

連鎖で消えるやつを実装 (語彙力皆無) マインスイーパっぽさを出すやつです まずはルールの確認です - 周り8ますにある爆弾の個数を考える 0ならば周囲8ますも自動オープン 自動で開けられたマスでも同じ操作を考える (以下無限ループ) のはずです

51.

連鎖で消えるやつを実装 今回は、周り8マスの爆弾の個数を調べる関数count()を作ります そしてopen()関数の中で、条件を満たせば(=count()が0ならば)周囲8マスに対してもopen()を 呼び出すことにします open()のなかでopen()を実行してもいいのか?と思うかもしれませんが、実は大丈夫です (こういうふうなものを「再帰呼び出し」と呼びます)

52.
[beta]
連鎖で消えるやつを実装 (コード)

C++では、実行するところより上に関数定義を書け、というルールがあるので、
count()はshowの上くらいに置いておきましょう
int count(int N, vector<vector<State>> stage, int x, int y) {
int ans = 0;
// 変則的なforなので注意! i=-1,0,1と3回実行です
for(int i = -1; i <= 1; i++) {
for(int j = -1; j <= 1; j++) {
if(i == 0 && j == 0) continue;
if(stage[x+i][y+j] == State::BOMB) ans++;
}
}
return ans;
}

53.

連鎖で消えるやつを実装 (コード) しかしこれではバグります、というのもx==0の時、[-1]を参照してしまうからです 参照先が範囲内(0~N-1)かどうかを確認しましょう ifの前にifを何個か増やしておきます if(x+i < 0 || N <= x+i) continue; if(y+j < 0 || N <= y+j) continue; if(stage[x+i][y+j] == State::BOMB) ans++; (もちろんstageをポインタで渡して高速化してもいいですが省略)

54.
[beta]
連鎖で消えるやつを実装 (コード)

次にopen()の再起呼び出しを実装します
void open(int N, vector<vector<State>> *stage, int x, int y) {
if((*stage)[x][y] == State::DEFAULT) {
(*stage)[x][y] = State::OPENED;
if(count(N, *stage, x, y) == 0) {
for(int i = -1; i <= 1; i++) {
for(int j = -1; j <= 1; j++) {
if(i == 0 && j == 0) continue;
if(x+i < 0 || N <= x+i) continue;
if(y+j < 0 || N <= y+j) continue;
if((*stage)[x+i][y+j] != State::OPENED) open(N, stage, x+i, y+j);
}
}
}
} else if((*stage)[x][y] == State::OPENED) cout << "opened" << endl;
else cout << "game over" << endl;
}

55.

連鎖で消えるやつを実装 (コード) これで実行してみましょう なんかいっぱい消えましたね〜やったー

56.

休憩 (現時点でのコードです コミットも忘れずに)

57.

仕上げ あと残ってることをさっさとやってしまいましょう まずは、マスに数字(count()の結果)を表示します show()内の以下の文を、 else if(stage[x][y] == State::OPENED) cout << " "; こう変更します else if(stage[x][y]==State::OPENED)cout << count(N,stage,x,y) << " ";

58.
[beta]
仕上げ

ただ周りに爆弾のないところに「0」と表示されるのも鬱陶しいので、こうしましょう
else if(stage[x][y] == State::OPENED) {
int c = count(N, stage, x, y);
if(c == 0) cout << " ";
else cout << c << " ";
}

これはもう完全にマインスイーパでしょう →

59.

仕上げ 次にゲームオーバー処理を作ります やり方はいろいろありますが、今回は「終了フラグ」を使ってやってみます つまり、bool型変数「game_finished」をfalseにしておき、 trueになったらゲームを終わる、つまりwhileを抜けます コードは次ページ

60.

仕上げ open()の定義より前のどこかにこれを書いておき、 bool game_finished = false; open()の以下の行を書き換えます else cout << "game over" << endl; ↓ else { game_finished = true; cout << "game over" << endl; }

61.

仕上げ あと、main()の中のwhile(true)をこのように書き換えます while(!game_finished) { これで完了です

62.

仕上げ ゲームオーバーといえば、そういえばゲームクリア処理を作っていませんでした int型変数openedを作っておいて、マスが開けられるたびに1ずつ増やして、 N*N - bomb_countになればクリア、というのでやってみましょう game_finishedの近くでいいのでint opened = 0;を書き、open()の以下の行を (*stage)[x][y] = State::OPENED; このように変更しましょう (*stage)[x][y] = State::OPENED; opened++;

63.

仕上げ あとはmain()のwhileの最終行あたりに、次の処理を追加すれば if(opened == N * N - bomb_count) { cout << “clear!” << endl; game_finished = true; } できてるはずです

64.

仕上げ あとはまあ、爆弾が見えてる状態だと面白くないので、show()の次の行を、 if(stage[x][y] == State::DEFAULT) cout << "- "; このようにして、 if(stage[x][y] == State::DEFAULT || stage[x][y] == State::BOMB) cout << "- "; あとは下の方のこの行を削除します(まあしなくても動作は変わりませんが) else cout << "x ";

65.

仕上げ これでゲームとしてはある程度遊べるようになったと思います! あとは爆弾数を増やすとか、フラグ機能をつけるとか、 「a 1」じゃなく「1 a」でも動くようにするとか、まあ好きなようにいじってみてください

66.

まとめ 最終的なコードはこんな感じです〜

67.

まとめ GitHubのここでもコードを見れるようにしているので、ここからもどうぞ〜 https://github.com/ZOI-dayo/Cpp-Minesweeper/blob/main/slide.cpp スライドもコードもめちゃくちゃ長くなってしまいました...すみません ただ、こういうふうな簡単なゲームとかアプリみたいなものをサッと作れるようになると めちゃくちゃ面白いので、ぜひできるようになってください〜

68.

終わり