別荘1
更新日:2010/6/28
14歳からはじめる C言語オンラインゲーム プログラミング教室(著者:大槻有一郎) |
このページのほとんどの、用語・ソース・解説等々、は 14歳からはじめる C言語オンラインゲーム プログラミング教室(著者:大槻有一郎) の書籍内から引用させて頂いています。 ムンバが自分勝手にコメントや解釈などしてる部分に関しましては 信用できませんので、どうぞご了承下さいませ。 ちなみに、この本も信用できないと、ご意見頂きました。^^; タイトルに「C言語」と、うたっているのにも関わらず・・・ (ってか、DXライブラリ自体がC++なのに早く気づけよ。俺っ!^^;) C++を多様してるので今後、購入を予定してる方は、ご検証の程。 |
・ | P.36〜 | C言語をおさらいしよう | |||
・ | P.40〜 | ヘッダファイルのおさらい | |||
・ | P.46〜 | 文字列処理のおさらい | |||
・ | P.51〜 | 構造体のおさらい | |||
・ | P.54〜 | ポインタとメモリ管理のおさらい | |||
・ | P.64〜 | DXライブラリを使ってみよう | |||
・ | P.72〜 | テストプログラムを書いてみよう | |||
・ | P.78〜 | 画像を動かしてみよう | |||
・ | P.90〜 | アクションゲームのタイトル画面を作ろう | |||
・ | P.96〜 | ポーズ機能を組み込む |
C別荘に戻る プログラミングの館へ 解らないC〜っ!へ |
ループと条件分岐・ヘッダファイル・文字列処理・構造体・ポインタ
(14歳からはじめる C言語オンラインゲーム プログラミング教室(著者:大槻有一郎)内のソース引用)
#include const int 配列 main void srand unsigned time NULL
for rand bool while if printf char gets_s
TOPへ
P.40〜 ヘッダファイルのおさらい
他のソースコードの呼び出し
osarai3.h
(14歳からはじめる C言語オンラインゲーム プログラミング教室(著者:大槻有一郎)内のソース引用)
#ifndef #define #endif
この例では、osarai3.h が最初にインクルードされた時は __OSARAI_3_H__ は定義されていないため
その内容は有効になる。
しかし、2度目にインクルードされた時は、最初のインクルード時に、#define __OSARAI_3_H__ で定義してるため
#endif までの間は全て無視される。
これなら重複していても、再定義エラーは起きない。
__OSARAI_3_H__ の部分(命名)は、他の要素との重複を避けるために、重なりにくいものにする。
自作関数例
osarai2.cpp
(14歳からはじめる C言語オンラインゲーム プログラミング教室(著者:大槻有一郎)内のソース引用)
osarai3.cpp
(14歳からはじめる C言語オンラインゲーム プログラミング教室(著者:大槻有一郎)内のソース引用)
TOPへ
P.46〜 文字列処理のおさらい
文字列を記憶させた char配列が二つある。
この2つを、連結したり途中に数値を挟んだりするやり方。
C言語では、文字列をchar型の配列やポインタに記憶する。
つまりC言語での文字列の連結とは、配列から配列へ要素をコピーする事。
//strncatを使った連結
もっとも一般的なのが、文字列連結命令の strncat(ストリングキャット)関数を使う方法。
cat は、concat(コンキャット、連結と言う意味)のもじり。
Visual C++ には、安全なstrncat_s 関数(C++)が用意されている。
第2引数に連結先の配列サイズを、第4引数に連結する文字列を指定する事で
バッファオーバーフローが起きるのを防ぐ。
配列サイズは、sizeof 演算子で求められる。
errno_t strcpt_s( char *strDest, size_t, number0fElements, const char *strSource,
size_t count);
strncpy 関数と同じような動作にするためには、第4引数を _TRUNCATE とする必要がある。
sizeof strcpy strcpy_s strlen memcpy memcpy_s break sprintf sprintf_s
TOPへ
P.51〜 構造体のおさらい
C言語では、構造体は3通りの宣言・定義方法がある。
struct 型名{ メンバ変数の宣言 }; struct 型名 構造体変数名; |
struct 型名{ メンバ変数の宣言 } 構造体変数名; |
typedef struct { メンバ変数の宣言 } 型名; 型名 構造体変数名; |
C++タイプの宣言方法
一番シンプルで、他のオブジェクト言語の文法に似ていて移行しやすいので、この本で使う。との事・・・
この本のタイトルは「C言語・・・」なのにね〜。
立ち読みで良かったかな?この本・・・。(^_^; アハハ…
struct 型名{ メンバ変数の宣言 }; 型名 構造体変数名; |
(14歳からはじめる C言語オンラインゲーム プログラミング教室(著者:大槻有一郎)内のソース引用)
実行結果とかのサンプルは、御座いません。
struct 型名{
メンバ変数の宣言
};
型名 構造体変数名;
のサンプルもありません。
以上で、構造体の復習は終了みたいです。
struct
TOPへ
P.54〜 ポインタとメモリ管理のおさらい
下のソースコードのmessage関数は、名前の文字列を渡すと・・・
「Welcome」を付けて返してくれる。
しかし、このプログラムには誤りがあって正しい結果を返しません。
それは何故か?また、どう修正すれば正しく動くのか?
このプログラムでは ポインタ がトラブルの原因になっている。
ポインタとは、変数が記録されているメモリ上の場所を表す特殊な変数で
変数を宣言する時に、型名の後に * (アスタリスク)を付けるとポインタとなる。
また、通常の変数の前に & (アンド・アンパサンド)を付けると
その変数が記憶されてるメモリアドレスを表すので、ポインタに代入できる様になる。
int val = 10;
int *p_val; //int型ポインタの宣言
p_val = &val // 変数valのアドレスをポインタに代入
ポインタは、C言語最大の難関と言われてる。
ポインタを乗り越えるコツは「変数のメモリ上で(の、が抜けてる?)扱われ方を
明確にイメージできるようにする事。
またポインタのトリッキー(ひねった)使い方は、なるべく避けた方がいい。
(インクルード演算子などの使用等)
変数が記録される場所や記録されてる時間は、変数の種類によって異なる。
グローバル変数 は、プログラムを起動した時に 静的領域 に確保され、終了時に開放される。
ローカル変数は、関数を呼び出した時に スタック領域 に確保され
関数から出て呼び出し元に戻る時に解放される。
動的確保 はプログラマが好きなタイミングで確保・解放できる。確保される場所は ヒープ領域 。
3つの中で、特に注意が必要なのが、ローカル変数。
ローカル変数はプログラマが、確保と解放を意識しなくても使える手軽な変数。
しかし、ポインタと組み合わせて使う時は、それがネックになる。
ポインタが参照してる変数が消滅してしまう事がある。
↑のソースでは、message関数内で確保したchar型配列bufのアドレスを
呼び出し元に渡そうとしている。
しかし、bufはmain関数に戻った時には消滅してるため、実行時エラーが起きる。
ちなみに、簡単に↑のソースを実行できる様にした↓ソース
(static使用・勝手にソース改造)
(1)グローバル変数を使う
これだと、一時的に使う文字列を記憶するために
グローバル変数使うのは、あまり美しいやり方ではない。
(2)ローカル変数を使う
ローカル変数でも、使う前に消滅しなければいいので
呼び出し元の関数で宣言しておけば問題ないはずです。
このソースでは、message関数の外で配列を確保しておき、引数に配列の先頭アドレスとサイズを渡す。
「ローカル変数を使う」となってるが、引数に渡すメモリアドレスは
グローバル変数や動的確保したメモリアドレスでも構わない。
(3)動的確保を使う
ローカル変数を使ったソースは、関数の引数が多すぎるので
ヒープ領域からの動的確保 を使った形に直した。
このソースではmessage関数内部で書き込み用配列を用意し
そこに文字列を書き込んで、メモリアドレスを返す。
メモリを動的確保するには、C言語では malloc 関数(解放は free 関数)を使うが
ここではC++の new 演算子を使う。
1個分のデータを確保したい時: new型名()
配列を確保したい時: new型名[ ]
ヒープ領域に確保した領域は,勝手に消える事は無いので
呼び出される側から呼び出し元に、安心してアドレスを渡す事ができる。
その代わり、確保した領域は呼び出し元が delete 演算子を使って解放しなければいけない。
解放し忘れた場合、ヒープ領域の中にゴミ(使用できない領域)が残る。
これを、メモリーリークと言う。
グローバル変数のソース:書き込み先の配列が固定されてるのが問題。
複数の書き込み配列を使い分ける方法が無い。
ローカル変数のソース:引数が多いのが難点だが、書き込み先配列を自由に指定できる。がメリット。
動的変数のソース:一見、使い勝手は良さそうだが関数を呼び出すたびに新しい配列が作られる。
また、使う側に配列をdeleteする必要がある事を伝えなくてはいけない。
比べてみると、ローカル変数がもっとも使いやすい。
引数があまりにも多くて面倒な場合は、構造体にまとめる事を検討する。
TOPへ
P.64〜 DXライブラリを使ってみよう
VisualC++ 2008 Express Edition での設定 DXライブラリ本家内のページを引用(2010/5/27現在)
TOPへ
P.72 テストプログラムを書いてみよう
// 14歳・・・ #include "DxLib.h" // DXライブラリのヘッダファイルを取り込む。 int WINAPI WinMain( HINSTANCE hI, HINSTANCE hP, LPSTR lpC, int nC){ //アプリケーションの開始 ChangeWindowMode(TRUE); if( DxLib_Init() == -1 ) return -1; DrawBox( 0, 0, 120, 120, 0x00FFFF, TRUE ); WaitKey(); DxLib_End(); return 0; } /* // DXライブラリ本家のお試しサンプルソース #include "DxLib.h" // プログラムは WinMain から始まります int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow ) { if( DxLib_Init() == -1 ) // DXライブラリ初期化処理 { return -1 ; // エラーが起きたら直ちに終了 } DrawPixel( 320 , 240 , 0xffff ) ; // 点を打つ WaitKey() ; // キー入力待ち DxLib_End() ; // DXライブラリ使用の終了処理 return 0 ; // ソフトの終了 } */ /* // C言語何でも質問サイト「ゲームプログラミングの館」内のお試しサンプルソース #include "DxLib.h" int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow ){ ChangeWindowMode( TRUE ) ; // ウインドウモードに変更 if( DxLib_Init() == -1 ) return -1; // DXライブラリ初期化処理 エラーが起きたら終了 LoadGraphScreen( 0 , 0 , "char.png" , TRUE ) ; // 画像は「C言語何でも質問サイト」内の画像 WaitKey() ; // 結果を見るためにキー待ち(『WaitKey』を使用) DxLib_End() ; // DXライブラリ使用の終了処理 return 0 ; // ソフトの終了 } */
int WINAPI WinMain( ) の部分が違うんだけど
この本は
int WINAPI WinMain( HINSTANCE hI, HINSTANCE hP, LPSTR lpC, int nC)
int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow )
↑他2つのサンプルソース
この本の様に、省略可して短くできるって事みたいだ。w
最終的には、↓こんな感じになりました。
// 14歳・・・ mymain.cpp #include "DxLib.h" //DXライブラリのヘッダファイルの取り込み int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int) //アプリケーションの開始 { ChangeWindowMode(TRUE); //画面モード変更関数・TRUE:ウインドモード FALSE:フルスクリーンモード if( DxLib_Init() == -1 ) return -1; //DXライブラリの初期化関数 DrawBox( 0, 0, 120, 120, 0x00FFFF, TRUE ); //四角形を描画する関数 WaitKey(); //何かキーが押されるまでプログラムを一時停止する関数 DxLib_End(); //DXライブラリを終了する関数 return 0; //プログラムを終了 }
TOPへ
P.78〜 画像を動かしてみよう
・画像の読み込みと表示
・ループを使って移動させる方法
・キー入力に応じて動かす方法
など
DXライブラリの機能だけでは、四角・円・線などだけ。
画像ファイルを読み込んで、画面に表示する方法。
// 14歳・・・ mymain.cpp #include "DxLib.h" //DXライブラリのヘッダファイルの取り込み int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int) //アプリケーションの開始 { ChangeWindowMode(TRUE); //画面モード変更関数・TRUE:ウインドモード FALSE:フルスクリーンモード if( DxLib_Init() == -1 ) return -1; //DXライブラリの初期化関数 int ghandle = LoadGraph("media\\f-player.bmp"); //画像を読み込む関数 DrawGraph(296, 224, ghandle, TRUE); //画像を表示する関数 WaitKey(); //何かキーが押されるまでプログラムを一時停止する関数 DxLib_End(); //DXライブラリを終了する関数 return 0; //プログラムを終了 } |
・動画を動かす
#include "DxLib.h" //DXライブラリのヘッダファイルの取り込み int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int) { //ChangeWindowMode(TRUE); if( DxLib_Init() == -1 ) return -1; int ghandle = LoadGraph("media\\f-player.bmp"); //画像を読み込む関数 for(int x = 0; x < 320; x++){ DrawGraph(x, 224, ghandle, TRUE); } //DrawGraph(296, 224, gazou001, TRUE); //画像を表示する関数 WaitKey(); DxLib_End(); return 0; } |
このソースを実行すると、画像の残像が残ってしまう。
プログラムで、前に描いた(表示した)画像の上に重ねて書いてるせい。
画像が動いてる様に表示するには、前の画像を消しながら
次の画像を表示する様にしなければいけない。
#include "DxLib.h" int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int) { if(DxLib_Init() == -1) return -1; int gazou001 = LoadGraph("abc.bmp"); SetDrawScreen(DX_SCREEN_BACK); //裏画面を描画対称にする for(int x = 0; x < 320; x++) { //ClsDrawScreen(); //画面消去 ClearDrawScreen(); //画面消去 DrawGraph(x, 224, gazou001, TRUE); ScreenFlip(); //画面入れ替え(画像表示) } WaitKey(); DxLib_End(); return 0; } |
本家更新履歴(2006/3/11)DXライブラリバージョン2.21aより < 「つうこうにん」さんから情報頂きました。
ClsDrawScreen(); → ClearDrawScreen();
に変更。
表画面と裏画面の2つの画面を用意し
裏画面を消去して画像を書き込み終わってから、表画面と入れ替える様にする。
その仕事をするのが SetDrawScreen関数と ScreenFlip関数。
・無限ループとProcessMessage
ゲームは、クリアーかゲームオーバーになるまで続くので
for文よりもWhile文を使ったループの方が適してる。
// 14歳・・・ mymain.cpp #include "DxLib.h" int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int) { ChangeWindowMode(TRUE); if( DxLib_Init() == -1 ) return -1; int gazou001 = LoadGraph("abc.bmp"); int x = 0; SetDrawScreen(DX_SCREEN_BACK); //裏画面を描画対称にする while(ProcessMessage() == 0 && CheckHitKey(KEY_INPUT_ESCAPE) == 0) //Escキーが押されたら終了 { ClearDrawScreen(); //画面消去 if(x <= 320) x = x + 2; DrawGraph(x, 224, gazou001, TRUE); ScreenFlip(); //画面入れ替え } DxLib_End(); return 0; } |
Windowsアプリケーションでは、一定期間ごとにOSから送られてくるメッセージを、処理しないといけない。
という決まりがあるので
メッセージを処理しない無限ループを作成してしまうと
同時に動いてる他のアプリケーションとの連携がうまくいかなくなり
様々なトラブルが起きる。
DXライブラリでは ProcessMessage関数をループ中にはさむ事で、メッセージを処理する事ができる。
この関数を全ての関数に入れる必要は無いが
ScreenFlip関数 1回につき ProcessMessage関数を1回呼び出す様にする。
このソースでは、whileループの終了条件として
CheckHitKey関数によって Escキー が押されたら 1 を、押されていなければ 0 を返す。
(Escキーが押されたらループを抜け、それ以外のキーが押されたらループを抜けない。)
監視するキーの種類は、定数によって指定する。
↓
DXライブラリ内のリファレンスの CheckHitKey 参照
定数というのは、変更できない変数の事で
#define か const を使って宣言する。
・キー入力に応じてキャラクターを移動させる
キー入力に応じてキャラクター画像を動かす。
キー入力に応じて画像を動かすには
←キーが押されたら x座標の値を減らし、→キーが押されたら x座標を増やす。
と言った具合にキー入力で座標を変化させれば良い。
キー入力を調べる関数には CheckHitKey関数もあるが
アクションゲームなら GetJoypadInputState関数がお勧め。
この関数は、チェックできるキーの数が少ない代わりに、ジョイパッドのボタンも同時にチェックできる。
// 14歳・・・ mymain.cpp #include "DxLib.h" int WINAPI WinMain( HINSTANCE, HINSTANCE, LPSTR, int) { ChangeWindowMode(TRUE); if( DxLib_Init() == -1 ) return -1; int ghandle = LoadGraph("media\\f-player.bmp"); int x = 0, y = 0; SetDrawScreen(DX_SCREEN_BACK); while(ProcessMessage() == 0 && CheckHitKey(KEY_INPUT_ESCAPE) == 0) //Escキーが押されたら終了 { ClearDrawScreen(); //自キャラ移動 int key = GetJoypadInputState(DX_INPUT_KEY_PAD1); //キーとPAD1のチェック if(key & PAD_INPUT_UP) y -= 4; if(key & PAD_INPUT_DOWN) y += 4; if(key & PAD_INPUT_LEFT) x -= 4; if(key & PAD_INPUT_RIGHT) x += 4; DrawGraph(x, y, ghandle, TRUE); ScreenFlip(); } DxLib_End(); return 0; } |
TOPへ
P.90〜 アクションゲームのタイトル画面を作ろう
・背景画面をスクロールさせる。
・ロゴを、フェードインさせたり点滅させたりする。
・ここからがゲーム作りの本番
・今回は、障害物を避けながら進んでく伝統的なミニゲームを作る。
まずは、リアルタイムゲームを作りやすい形に WinMain関数を修正する。
「リアルタイム」とは、人間の体感時間に合わせてプログラムを進行させる事で
一定のタイミングを保つ仕組みが必要。
方法
毎回の移動量と1ループにかかる時間の両方を固定する。か
1ループにかかる時間に合わせて毎回、移動量を調整する。
方法がある。
DXライブラリの場合、1ループにかかる時間はリフレッシュレートの設定によって決まるので
1ループの時間が固定できない以上、その変動に合わせて毎回移動量を調整するしかない。
この方法でタイミングを調整する処理を加えたものが次のソース。
// 14歳〜・・・オンライン・・・ //mymain.cpp #include "DxLib.h" #include <limits.h> #include <string.h> //速度調整・グローバル変数の宣言 int g_lastcalltime = 0; //経過時間計測用変数 float g_frametime = 0; //1フレームにかかった秒数 //プロトタイプ宣言 void MyMain(); //ローカル関数 int LoadFiles(); //ローカル関数 int WINAPI WinMain(HINSTANCE hI, HINSTANCE hP, LPSTR lpC, int nC) { if(strcmp(lpC, "-f") != 0) ChangeWindowMode(TRUE); if( DxLib_Init() == -1 ) return -1; //ファイル読み込み if(LoadFiles() == -1) return -1; //メインループ g_lastcalltime = GetNowCount() & INT_MAX; SetDrawScreen(DX_SCREEN_BACK); while(ProcessMessage() == 0) { //現在時刻を計測 int curtime = GetNowCount() & INT_MAX; int curinterval = curtime - g_lastcalltime; //最終呼び出しからの経過時間 ClearDrawScreen(); //1ループにかかった時間(秒数)を求める g_frametime = (float)(curinterval) / 1000.0f; //メイン処理の呼び出し MyMain(); //現在時刻を最終呼び出し時刻にする g_lastcalltime = curtime; ScreenFlip(); } DxLib_End(); return 0; } //ゲーム固有の処理 float g_x = 640; int g_jikibmp; int temptime = 0; //計測開始時間 //ファイルの読み込み処理を書くローカル関数 int LoadFiles() { g_jikibmp = LoadGraph("media/f-player.bmp"); if(g_jikibmp == -1) return -1; return 1; } //メイン処理を書くローカル関数 void MyMain() { //画像が右端にきたら、画像を左側に戻し、temptimeに現在時刻を代入 if(g_x > 640) { g_x = -65; temptime = GetNowCount() & INT_MAX; } //出現時間(ミリ秒)と1ループあたりの経過時間を表示 int curtime = GetNowCount() & INT_MAX; char buf[80]; sprintf_s(buf, 80, "%f : %f", (float)(curtime - temptime) / 1000.0f, g_frametime); DrawString(0, 100, buf, 0xFFFFFF); //キャラクタを移動 DrawGraph((int)g_x, 200, g_jikibmp, TRUE); g_x += 100.0f * g_frametime; } |
if(strcmp(lpC, "-f") != 0) ChangeWindowMode(TRUE);
ゲームをフルスクリーンで実行したい場合は
実行ファイルのショートカットを作成して、次の様に(P.141の様に、ショートカットを作成した場合?)設定する。
-f を付けて起動した場合は、フルスクリーンモードで
付けない場合は、ウィンドウモードで表示する様にしてある。
int LoadFiles( ) void MyMain( ) は、ローカル変数。
両関数が長くなってきたので、ローカル変数にした。
LoadFiles関数
int LoadFiles() { g_jikibmp = LoadGraph("media/f-player.bmp"); if(g_jikibmp == -1) return -1; return 1; }
は、画像の読み込みに失敗すると -1 を返すので
画像の読み込みに失敗した場合は、プログラムを終了させる。
g_lastcalltime = GetNowCount() & INT_MAX;
このサンプルソースでは、DXライブラリの GetNowCount関数の結果を、正の数にするために
GetNowCount() & INT_MAX;
と言う計算を行っている。
Windows内部では、起動後の経過時間を、正負の符号無しの unsigned int で管理してる。
ところが、GetNowCount関数は、これを符号付の int に変換して返すため
符号付き整数では最上位ビットが、「0の時に正の数、1の時に負の数」とするため
符号付きから符号無し整数に変換すると、正の数が負になる事がある。
例えば、符号無し整数の
2,147,483,648 を符号付きの int に変換すると、-2,147,483,648 となる。
そこで、ビット演算子の & を使って、最上位ビットを 0 に変え、つねに正になる様にしている。
なお、正の最大値を表す定数 INT_MAX は
C標準ライブラリの limits.h で定義されている。
・経過時間の計測
//1ループにかかった時間(秒数)を求める
g_frametime = (float)(curinterval) / 1000.0f;
メインループ内で現在時刻を調べ、変数 curtime に代入。(int curtime = GetNowCount() & INT_MAX;)
curtime から g_lastcalltime を引いた値が、1ループあたりの経過時間。(int curinterval = curtime
- g_lastcalltime;)
これを float型実数に変換し、1000で割って秒数を求め g_frametime に代入しておく。
最後に g_lastcalltime に curtime を代入し、次回の計測に備える。(g_lastcalltime = curtime;)
・ゲーム固有のメイン処理
ゲーム固有の処理は MyMain関数(ローカル関数)に書く。(void MyMain())
ここでは、ちゃんとタイミング調整できているかを確認するために
キャラクターを動かしながら「出現してからの経過時間」と「1ループあたりの経過時間(g_frametime)」を表示させてる。
注目して欲しいのは、キャラクターの横座標を記録するグローバル変数 g_x を float型にしてる事と
g_x に対して「100.0f * g_frametime」を加えてる事。
100.0f は、移動速度単位は ピクセル/秒)。
リフレッシュレートが 60Hzと70Hzの環境でプログラムを実行してみると
1ループあたりの時間は 0.016〜0.017秒、0.014〜0.015秒と違ってる。
しかし、「移動速度×g_frametime」の計算で、移動量を調節してるため
どちらも移動速度は「100ピクセル/秒」に固定される。
ここで行ってる「移動量=移動速度×1ループあたりの経過時間」という計算は
等速度運動の公式(本では当速度が誤字)、つまり「距離=速度×時間」と全く同じ。
DrawString
TOPへ
P.96〜 ポーズ機能を組み込む
(工事中)
TOPへ