iPad専用アクションゲーム「スライムは投げる」

最近のエントリー

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、をお持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_09.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_09.exe」をダブルクリックし、実行してみてください。
どうでしょう?カーソルキーで自機が移動し、スペースキーを押すと、弾が発射されると思います。

というわけで今回は、弾処理について解説したいと思います。

構造化プログラミング基礎

ここのプログラミング講座を見ている人は、ほとんどが初心者だと思います。
そして初心者にプログラムを組ませてみると、高い確率で、WinMain(またはmain)関数内にひたすら書きまくるようです。
たしかに、それでも動くには動くのですが、プログラムが複雑になってくると、どうしても管理がしにくくなってきますよね?
そこで、ある程度固まった処理、例えば「キー入力でキャラを動かす」だとか「キャラクターを表示する」
というような処理を区切って記述してやります。
そうする事で、プログラムを管理しやすくなったり、バグを発見しやすくなります。

ようするに役割ごとにサブルーチン化するって事です。

弾処理のアルゴリズム基礎

弾の発射から消えて無くなるまでの大まかな流れは以下の通りです。

  1. 弾用の変数(構造体等)を複数、用意する
  2. キー入力があったら、弾データを作成する
  3. 弾を移動させる
  4. 弾が画面外に出たら、弾データを消す
  5. 弾表示
  6. 2へ

重要なポイントは、弾のデータを作ったら、その後、弾のデータを移動させたり削除したりする事でしょうかねぇ。

実際にプログラムを組む

では、判りやすい(たぶん)ように、順番に説明していきたいと思います。

まず、構造化プログラムという事で、必要な変数等をグローバル変数として宣言してやります(ソースの出来るだけ上に書く。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//サイン、コサイン テーブル
float fcos[360],fsin[360];
 
//自機用変数
struct _player{
    float x,y;      //座標
    int renda;      //連打用フラグ
}player={320.0f-32,240.0f-32,0};//初期化
 
#define BMAX 100        //画面内に最大100弾とする(爆
 
//弾用変数
struct _bullet{
    BOOL enable;        //使用:1  未使用:0
    float x,y;      //座標
    short angle;        //進む角度
}bullet[BMAX];

こうする事で、どこでも(どの自作関数内でも)これらの変数を参照する事が出来るようになります。
今まで、プレイヤーの座標はWinMain関数内でcx,cyと宣言し、それらを使用していましたが、今回は構造体(_player)として宣言します。
_bullet構造体は、弾の処理に使用します。
今回使う変数はとりあえず、使用・未使用を表すフラグ(enable)と
座標(x,y)と、進む方向(angle)だけです。

では、一番最初に実行するWinMain関数を見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
 
    //ウィンドウ作成
    if(!InitWindow(hInstance,hPrevInstance,nCmdShow))
        Quit("ウィンドウ作成に失敗しました。");
     
    //DirectDraw開始
    if(!StartDirectDraw(hw))
        Quit("DirectDrawの初期化に失敗しました。");
 
    //ゲーム初期化
    InitGame();
 
    //ゲームループへ
    GameLoop();
     
    return(FALSE);
}

いやぁ、今まではここ(↑)にひたすら書いていたので、今回のプログラムは結構すっきり見えますね。

InitWindow()はウィンドウを作成する処理を書いてあるだけの自作関数なので解説省略。

で、DirectDrawを初期化したらInitGame();を呼び出していますね?
これはゲームに必要な処理を先にしておく自作関数です。中を見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//----------[ ゲーム初期化 ]--------------------------------------------------------------------
void InitGame(void){
    //ビットマップからパレット読み込み
    LoadPalette("ss.bmp");
 
    //オフスクリーンサーフェイス(lpWork)へビットマップ読み込み
    LoadBitmap(lpWork,"ss.bmp",0,0,72,64);
 
    //透明色指定(パレット0番の黒色を透明色とする)
    SetTransColor(lpWork,0);
 
    //弾データ初期化
    for(int i=0;i<BMAX;i++)
        bullet[i].enable=0;     //全て未使用データとする
 
    //サイン、コサインテーブル作成
    for(i=0;i<360;i++){
        fsin[i]=(float)sin(i*3.1415926535/180);
        fcos[i]=(float)cos(i*3.1415926535/180);
    }
}


まず、作業用サーフェイス(lpWork)にキャラの画像を読み込んでおきます(左の絵)。
で、_bullet構造体のメンバ変数「enable」を0に設定しておきます(全て)。
enableが0だったら、その構造体は使用していないという事にします。

で、ここの処理が終わると、処理はWinMainの方へ戻って、次はGameLoop();を呼び出していますよね?
この関数はゲームのループ用の自作関数です。コードは次のようになっています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//----------[ メインループ ]--------------------------------------------------------------------
void GameLoop(void){
    while(1){
        WaitSet();              //現在の時間(単位:ミリ秒)取得
         
        MovePlayer();               //自機移動
        Shoot();                    //弾発射判定
 
        MoveBullet();               //弾移動
 
        Show();                 //画像表示
        Wait(1000/60);              //メッセージループへ
    }
}

whileの中の最初と最後のWaitSet()、Wait()については既にやっているので解説省略。

で、まずMovePlayer()関数を呼び出していますよね?こいつはキー入力により自機を動かす処理をする自作関数です。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//----------[ 自機移動 ]------------------------------------------------------------------------
void MovePlayer(void){
     
    int angle=-1;               //とりあえず角度を-1にしておく
    //[38]↑ [40]↓   [37]←   [39]→
    if(keyg[38] && keyg[39])    //右上
        angle=45;
    else
     
     
     
    (略)
 
    //angleの値が変わっていたらキャラクタの座標を変更する
    float mv=4.0f;  //移動量
    if(angle!=-1){
        player.x+=fcos[angle]*mv;
        player.y-=fsin[angle]*mv;
    }
}

この関数内でやっていることは第5回で説明したので、みりゃ判りますね?

ここの処理が終わったら、GameLoopの方へ戻って、次にShoot()関数が呼ばれています。

Shoot()関数では、スペースキーが押されているか判定し、押されていれば弾を作成します。

ここでは、シューティングゲームで使うような、押しっぱなしでも連射できる処理をやります。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//----------[ 弾発射判定 ]----------------------------------------------------------------------
void Shoot(void){
    int i;
    //弾発射判定
    if(keyg[32]){           //キーが押されている場合
         
        if(!player.renda)       //変数rendaが0なら打つ
            for(i=-2;i<=2;i++)   //弾作成
                CreateBullet(player.x+28,player.y-5,90+i*15);
 
        player.renda++;
        player.renda%=10;       //打つ間隔
     
    } else {                //キーが押されていない場合
        if(player.renda){
            player.renda++;
            player.renda%=10;
        }
    }
}

ここでは、ちょっとした工夫が必要ですかね。
スペースキーが押されていた場合に弾を作成するだけでは、
押している間、毎回弾が作成されてしまいますよね?
シューティングゲームのように、一定間隔で弾を撃ちたい場合は、一つ変数を用意し(今回はplayer.renda)、スペースキーが押されている間はこの変数に1を足し、10になったら0に戻してやります。
で、この変数が0の時だけ弾を作成すれば、処理的には10ループに1回弾を発射する事になります。

さて、ここで極端な例を挙げてみます。この弾を出す間隔が10では無く、1000だとしましょう。
普通、ゲームで発射キーを押しっぱなしにする場合、発射キーを押した瞬間に弾が発射され、一定間隔で弾がでますよね?
で、もし変数「player.renda」が仮に500で止まっていたらどうでしょう。
発射キーを押しても、player.rendaは0でないため、しばらく発射されませんね?

これではマズイので、発射キーが押されていない時、player.rendaが0で無い場合には、player.rendaに1を足し、ある数を越えたら0にする、といった処理を付け加えてやります。こうすれば、発射キーを離している間でもplayer.rendaの値が変化するので、発射キーを離し、しばらくして発射キーを押した瞬間に、弾を発射させる事が可能となります。

ついでですが、発射キーが押されていない時にplayer.rendaを0にすると、いつでも発射キーを押した瞬間に弾が出せます(実際にやってみてください)。

ここの関数内では弾を作成する時にCreateBullet( x座標 , y座標 , 角度(0~359) )自作関数を呼び出しています。
ではどういった処理をしているのか見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//----------[ 弾作成 ]--------------------------------------------------------------------------
void CreateBullet(float x,float y,short angle){
    int i;
    int no=-1;  //作成する弾のデータ番号
     
    //構造体の中で、空いている(未使用)データを探す
    for(i=0;i<BMAX;i++)
        if(bullet[i].enable==0){
            no=i;
            break;
        }
 
    //空いているデータが見つからなかったら作成せずに戻る
    if(no==-1)
        return;
 
    //データ詰める
    bullet[no].x=x;
    bullet[no].y=y;
    bullet[no].angle=angle;
    bullet[no].enable=1;    //使用中にする
}

さて、初めの方で呼び出したInitGame()内では_bullet構造体の全てのenableを0にし、未使用データとしましたね?
ですから、ここ(CreateBullet)では、構造体の配列の0番からBMAX-1(今回は99)までを順に参照し、enableが0(つまり未使用となっているデータ)を探します。

そんでもって、空いているデータが見つかったら、その番号の構造体に情報を詰め込んでやります。
最後にenableを1とし、no番の構造体を使用データという事にしていますよね?
ですから、次にCreateBullet関数内で空いているデータを順に探した時も、no番のenableは0では無いので
上から新しくデータが詰められる事はありません。

でShoot()関数へ戻り、GameLoop()の方へ戻り、次にMoveBullet()を呼び出しています。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//----------[ 弾移動 ]--------------------------------------------------------------------------
void MoveBullet(void){
    int i;
    float mv=6.0f;
 
    for(i=0;i<BMAX;i++){
        if(bullet[i].enable==0) //未使用データだったら次の処理へ
            continue;
 
        //移動
        bullet[i].x+=fcos[bullet[i].angle]*mv;
        bullet[i].y-=fsin[bullet[i].angle]*mv;
         
        //画面外(今回は画面上)に出たら未使用データにする
        if(bullet[i].y<-32)
            bullet[i].enable=0;
    }
}

ここでは、構造体_bulletの配列を0から順番に参照し、enableが0でないデータ(つまり使用しているデータ)だけを処理します。
角度による移動自体は第4回で説明したので省略。

そして、画面外に出たらenableを0にし、未使用データとしてやります。

GameLoop()に戻り、次にShow()が呼び出されます。これは、ディスプレイに一連の表示をする
自作関数です。
コードを見てみましょう。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//----------[ 画像表示 ]------------------------------------------------------------------------
void Show(void){
    ClearScreen(lpBack);        //バックバッファ初期化
    DdTextOut(lpBack,0,0,"カーソルキーで移動 spaceキーで弾発射 pauseキーで終了",255);
 
    int i;
    //弾表示
    for(i=0;i<BMAX;i++)
        if(bullet[i].enable==1)
            BltClip(lpBack,(int)bullet[i].x,(int)bullet[i].y,8,32,lpWork,64,0,1);
 
    //自機転送
    BltClip(lpBack,(int)player.x,(int)player.y,64,64,lpWork,0,0,1);
    //フリップ
    Flip();
}

まぁ、ここは見りゃ判りますね(爆

弾は使用しているものだけ表示してやります。

とりあえず、これで説明は終わりです(^^;

今回のプログラムの流れを図で表すと、こんな感じになるのかも

矢印は関数の呼び出しです。

おまけ

今回は弾のデータ構造に構造体の配列を使用しましたが、この場合、配列中に使用しているデータと使用していないデータが両方含まれていたり、弾数に制限があったりして少し不便ですよね?
そんなときはリスト構造を使用すると結構スッキリ記述できます。
Space Soldierは双方向リスト構造でやってました。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_08.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_08.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、何かキーを押すと、そのキーコードが画面に表示されると思います。
というわけで今回は、キー入力について解説したいと思います。

二次元座標同士の当たり判定


右の図のように、座標上に2つの長方形が存在したとします。
ここで、この2つの物体(長方形)が重なっているか(当たっているか)を調べるときに、次のような式をよく使います。

1
2
3
4
if(ax2<bx1 || ax1>bx2 || ay1<by2 || ay2>by1)
    当たっていない
else
    当たっている

ただ、例によって(?)数学上の座標とパソコンのディスプレイの座標ではY軸の向きが逆なので、

1
2
3
4
if(ax2<bx1 || ax1>bx2 || ay1>by2 || ay2<by1)
    当たっていない
else
    当たっている

これが正しい式となります。まぁ、当たった時の判定だけしたいのならば、「!」を使えば直ぐに出来ますね。

1
2
if(!(ax2<bx1 || ax1>bx2 || ay1>by2 || ay2<by1))
    当たっている

実際にゲームに使用する場合は、キャラクターの全体もしくは、その一部を当たり判定とし、その部分を長方形として扱い、上の式へ代入します。
ただ、この方法はシューティングゲーム等のような、「あまり精度にはこだわらないから速度の速い方法がいい」という場合に使用してください。

レッツプログラミング

ではサンプルコードを見ていきます。

今回は、「第3回 キー入力によって複数のキャラクタを動かす」の最後の方へちょこっと加え、2匹のスライムが重なった(当たった)時にそのメッセージを表示するようにします。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
     
    <略>
 
    //座標
    int cx[2]={160-16,480-16},cy[2]={240-16,240-16};
     
    //キーテーブル
    unsigned char key_up[2]={38,104},key_down[2]={40,98},
        key_left[2]={37,100},key_right[2]={39,102};
     
    //その他の変数
    int i,mv=3;
 
    //メインループ
    while(1){
        WaitSet();          //現在の時間(単位:ミリ秒)取得
        ClearScreen(lpBack);        //バックバッファ初期化
        DdTextOut(lpBack,0,0,"カーソルキー、テンキー(4,6,8,2)で移動 pauseキーで終了",255);
 
        for(i=0;i<2;i++){
            //キャラクター移動
            if(keyg[key_up[i]]) //↑
                cy[i]-=mv;
            else
            if(keyg[key_down[i]])   //↓
                cy[i]+=mv;
            else
            if(keyg[key_left[i]])   //←
                cx[i]-=mv;
            else
            if(keyg[key_right[i]])  //→
                cx[i]+=mv;
             
            //キャラクタ表示
            BltClip(lpBack,cx[i],cy[i],32,32,lpWork,32*i,0,1);
        }
 
        //当たり判定
        if(!(cx[0]>cx[1]+31 || cx[0]+31<cx[1] || cy[0]+31<cy[1] || cy[0]>cy[1]+31))
            DdTextOut(lpBack,0,16,"当たっている",255);
         
         
        Flip();                 //フィリップ
        Wait(1000/60);              //メッセージループへ
    }

解説するまでも無いですね。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、をお持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_07.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_07.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、何かキーを押すと、そのキーコードが画面に表示されると思います。
というわけで今回は、キー入力について解説したいと思います。

よくわからない解説

ではサンプルコードを見ていきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
     
    <略>
 
#define CMAX 81 //記憶しておく座標の数
#define OSTEP 20    //オプションを配置する間隔
 
    //    cx:座標 cy:座標  mv:移動量  
    float cx[CMAX],cy[CMAX],mv=3.0f;
         
    //座標初期化
    int i;
    for(i=0;i<CMAX;i++){
        cx[i]=float(320-16);
        cy[i]=float(240-16);
    }
     
    int angle=0;
 
    //サイン、コサインテーブル作成
    float fsin[360],fcos[360];
 
    for(i=0;i<360;i++){
        fsin[i]=(float)sin(i*3.1415926535/180);
        fcos[i]=(float)cos(i*3.1415926535/180);
    }
 
    //メインループ
    while(1){
        WaitSet();                  //現在の時間(単位:ミリ秒)取得
        ClearScreen(lpBack);        //バックバッファ初期化
        DdTextOut(lpBack,0,0,"↑、↓、→、←で移動 pauseキーで終了",255);
         
        angle=-1;   //とりあえず角度を-1にしておく
 
        //[38]↑ [40]↓   [37]←   [39]→
        if(keyg[38] && keyg[39])    //右上
            angle=45;
        else
        
        
        
        <キー入力省略>
 
        //angleの値が変わっていたらキャラクタの座標を変更する
        if(angle!=-1){
 
            //座標を記憶しておく(バックアップ
            for(i=CMAX-2;i>=0;i--){
                cx[i+1]=cx[i];
                cy[i+1]=cy[i];
            }
 
            cx[0]+=fcos[angle]*mv;
            cy[0]-=fsin[angle]*mv;
        }
 
        //キャラクタ表示
        //オプション
        //後ろ(古い座標)から表示
        for(i=CMAX-1;i>0;i-=OSTEP)
            BltClip(lpBack,(int)cx[i],(int)cy[i],32,32,lpWork,32,0,1);
         
        //本体
        BltClip(lpBack,(int)cx[0],(int)cy[0],32,32,lpWork,0,0,1);
         
         
        Flip();                 //フィリップ
        Wait(1000/60);              //メッセージループへ
    }  

グラディウスのオプションは、本体(自機)の通った跡を遅れて通りますよね?
ですから、こういった処理は、自機の座標をいくつか記憶しておき、古い座標の所へオプションを配置してやればいいのです。

まず、キャラクタ(自機)の座標を配列でいくつか確保してやります。

1
2
#define CMAX 81 //記憶しておく座標の数
float cx[CMAX],cy[CMAX];

このとき、一番新しい座標をcx[0],cy[0]とし、一番古い座標をcx[CMAX-1],cy[CMAX-1]とします。
ゲーム中、キー入力があり、自機の座標が変更されようとしたら、cx[0],cy[0]~cx[CMAX-2],cy[CMAX-2]の中身を
cx[1],cy[1]~cx[CMAX-1],cy[CMAX-1]へコピーします(つまり1つ後ろへずらす)。
それで、cx[0],cy[0]の値を変更してやればいいのです。

1
2
3
4
5
6
7
8
9
if(angle!=-1){
    //座標を記憶しておく(バックアップ
    for(i=CMAX-2;i>=0;i--){
        cx[i+1]=cx[i];
        cy[i+1]=cy[i];
    }
    cx[0]+=fcos[angle]*mv;
    cy[0]-=fsin[angle]*mv;
}

座標を一つ後ろへずらす時、

1
2
3
4
for(i=0;i<CMAX-1;i++){
    cx[i+1]=cx[i];
    cy[i+1]=cy[i];
}

こんなふうに前からコピーしてしまうと、cx[0]の値がcx[1]へ、cx[1]の値がcx[2]へ・・・というように
全て同じ値になってしまうので、やめましょう(爆。ちゃんと後ろからコピーしていきましょう。

で、自機の座標跡を作っていったら、次は表示ですね。

1
2
3
4
5
6
7
8
9
10
#define OSTEP 20    //オプションを配置する間隔
 
    <略>
 
    //後ろ(古い座標)から表示
    for(i=CMAX-1;i>0;i-=OSTEP)
        BltClip(lpBack,(int)cx[i],(int)cy[i],32,32,lpWork,32,0,1);
         
    //本体
    BltClip(lpBack,(int)cx[0],(int)cy[0],32,32,lpWork,0,0,1);

オプション用の画像は自機の座標跡の古い所から表示してやります。
優先度の問題なのですが、先に描いた方が画面の後ろに表示されますよね?
なので、新しい座標の所程、後で描いてやります(自機は一番最後)

上のようなプログラムだと、
80,60,40,20番目の座標へ順番にオプションを配置し0番の座標へ自機を配置しています。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、をお持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_06.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_06.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、カーソルキーを押すと、キャラクターが動くと思います。
というわけで今回は、慣性の法則を使用したキャラ移動について解説したいと思います。

慣性の法則とは?

慣性の法則とは、私も詳しくは知りませんが、「物体は同じ運動を続けようとする」とかそんな感じの法則だった気がします。
宇宙空間のような摩擦の少ない場所で、一度動き出すと何かにぶつかったりするまで動き続けるというやつです。
スケートで一度動き出したらなかなか止まらない現象も同じですね。
詳しく知りたいなら物理の先生とかに聞いてください。

過去のゲームでも慣性を使用したゲームなんて腐るほどありますよね?
月面着陸ゲームだとか。
Bio100%さんの蟹味噌もそうですね。
使い道はいろいろあるので覚えておいても損は無いでしょう。

実際にプログラムを組む

ではサンプルコードを見ていきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
     
    <略>
 
    //    cx:座標     cy:座標 mv:加速度  
    float cx=320.0f-16,cy=240.0f-16,mv=0.1f;
    float cmx=0.0f,cmy=0.0f;//移動量
     
    int angle=0;
 
    //サイン、コサインテーブル作成
    float fsin[360],fcos[360];
 
    int i;
    for(i=0;i<360;i++){
        fsin[i]=(float)sin(i*3.1415926535/180);
        fcos[i]=(float)cos(i*3.1415926535/180);
    }
 
    //メインループ
    while(1){
        WaitSet();                  //現在の時間(単位:ミリ秒)取得
        ClearScreen(lpBack);        //バックバッファ初期化
        DdTextOut(lpBack,0,0,"↑、↓、→、←で移動 pauseキーで終了",255);
         
        angle=-1;   //とりあえず角度を-1にしておく
 
        //[38]↑ [40]↓   [37]←   [39]→
        if(keyg[38] && keyg[39])    //右上
            angle=45;
        else
        if(keyg[37] && keyg[38])    //左上
            angle=135;
        else
        if(keyg[37] && keyg[40])    //左下
            angle=225;
        else
        if(keyg[40] && keyg[39])    //右下
            angle=315;
        else
        if(keyg[38])    //↑
            angle=90;
        else
        if(keyg[40])    //↓
            angle=270;
        else
        if(keyg[37])    //←
            angle=180;
        else
        if(keyg[39])    //→
            angle=0;
 
        //angleの値が変わっていたら移動量を変更する
        if(angle!=-1){
            cmx+=fcos[angle]*mv;
            cmy-=fsin[angle]*mv;
        }
 
        cx+=cmx;
        cy+=cmy;
 
         
        //跳ね返る
        if(cx<0){        //左
            cx=0.0f;
            cmx=-cmx;
        }
        if(cx>640-32){   //右
            cx=float(640-32);
            cmx=-cmx;
        }
        if(cy<0){        //上
            cy=0.0f;
            cmy=-cmy;
        }
        if(cy>480-32){   //下
            cy=float(480-32);
            cmy=-cmy;
        }
 
        //キャラクタ表示
        BltClip(lpBack,(int)cx,(int)cy,32,32,lpWork,0,0,1);
        Flip();                     //フィリップ
        Wait(1000/60);              //メッセージループへ
    }

では、順番に解説して行きたいと思います。

1
2
3
4
float cmx=0.0f,cmy=0.0f;        //移動量(慣性
<略>
cx+=cmx;
cy+=cmy;

移動量(cmx,cmy)は毎回キャラクタの座標を移動させる値です。
例えば、cmxを5.0に設定しておけばキャラは右へ移動し続けます。
で、キー入力により、この移動量を変えることによって、慣性の法則のようなものが実現できるというわけです。

今まではキー入力でそのままキャラの座標を変更していましたが、慣性を使用する場合はキー入力で移動量を変化させ、移動量によって毎回キャラクターを移動させます。

それから、ついでなのでキャラクターを画面の端で跳ね返らせる事にします(そうしないと直ぐ画面外に行ってしまう)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//跳ね返る
if(cx<0){        //左
    cx=0.0f;
    cmx=-cmx;
}
if(cx>640-32){   //右
    cx=float(640-32);
    cmx=-cmx;
}
if(cy<0){        //上
    cy=0.0f;
    cmy=-cmy;
}
if(cy>480-32){   //下
    cy=float(480-32);
    cmy=-cmy;
}

例えば、左端の壁を越えてしまった(cxが0より小さくなった)場合は、まずcxを0にします。
これをやらないとキャラクターが埋まってしまったり、帰ってこなくなる場合があるからです(^^;
で、移動量(cmx)の符号を反転させてやります。

サンプルコードのダウンロード

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_05.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_05.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、カーソルキーでキャラクタが8方向へ移動すると思います。

というわけで、今回は4つのキー入力で8方向へキャラクタを移動させる方法を説明したいと思います。

やってはいけないのかも?

「ゲーム制作基礎 第2回」では4つのキー入力でキャラクタを4方向に動作させましたが、8方向というとよくこんな感じでやってしまいます(第2回のキー入力部分のelseを外しただけ)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int cx=320-16,cy=240-16,mv=3;
 
    
 
    //キャラクター移動
    if(keyg[38])    //↑
        cy-=mv;
    if(keyg[40])    //↓
        cy+=mv;
    if(keyg[37])    //←
        cx-=mv;
    if(keyg[39])    //→
        cx+=mv;
         
    //キャラクタ表示
    BltClip(lpBack,cx,cy,32,32,lpWork,0,0,1);

たしかに、これでも8方向の移動は可能ですが、例えば右上に移動する場合はどうでしょう?
mvが3として、右に3、上に3移動しますよね?直線距離で考えると4.24264068・・・というように3よりも移動距離が大きい事になります。
ですから、このプログラムでは斜めに移動する場合、移動量がおかしいという事になります。
(あえてこれにするという考えも有りだと思いますが。)

キャラクターの8方向移動

というわけで、8方向、同じ距離で進ませるには、三角関数を使用すると、比較的簡単に出来ます。
三角関数については前回みっちりやったので大丈夫ですね?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
 
    <略>
 
    //    cx:座標 cy:座標 mv:移動量  
    float cx=320.0f-16,cy=240.0f-16,mv=3.0f;
     
    int angle=0;
 
    //サイン、コサインテーブル作成
    float fsin[360],fcos[360];
 
    int i;
    for(i=0;i<360;i++){
        fsin[i]=(float)sin(i*3.1415926535/180);
        fcos[i]=(float)cos(i*3.1415926535/180);
    }
 
    //メインループ
    while(1){
        WaitSet();                  //現在の時間(単位:ミリ秒)取得
        ClearScreen(lpBack);        //バックバッファ初期化
        DdTextOut(lpBack,0,0,"↑、↓、→、←で移動 pauseキーで終了",255);
         
        angle=-1;   //とりあえず角度を-1にしておく
 
        //[38]↑ [40]↓   [37]←   [39]→
        if(keyg[38] && keyg[39])    //右上
            angle=45;
        else
        if(keyg[37] && keyg[38])    //左上
            angle=135;
        else
        if(keyg[37] && keyg[40])    //左下
            angle=225;
        else
        if(keyg[40] && keyg[39])    //右下
            angle=315;
        else
        if(keyg[38])    //↑
            angle=90;
        else
        if(keyg[40])    //↓
            angle=270;
        else
        if(keyg[37])    //←
            angle=180;
        else
        if(keyg[39])    //→
            angle=0;
 
        //angleの値が変わっていたらキャラクタの座標を変更する
        if(angle!=-1){
            cx+=fcos[angle]*mv;
            cy-=fsin[angle]*mv;
        }
 
        //キャラクタ表示
        BltClip(lpBack,(int)cx,(int)cy,32,32,lpWork,0,0,1);
        Flip();                     //フィリップ
        Wait(1000/60);              //メッセージループへ
    }

まず1つ角度を表す変数を用意しておき(angle)、それをまず-1に設定しておきます。
そしてキー判定によって、そのキーが示す角度へangleを変更します。
もしangleが最初に設定した-1とは違う値だったら、キーが押されていた事になり、そのangleが示す方向へ移動させてやれば良いというわけです。
ただ、気を付けないといけないのが、キー判定の順番です。
斜め移動のキー判定を先にやってからでないと、いけません。
実際にいろいろいじってみれば何故かが解ると思います。

記事検索

コミュニティ

Banner designed by gebo様
ドット絵掲示板
ドット絵掲示板
自作の「ドット絵」を投稿することができます。ドッターさん同士の交流の場としてご利用ください。
TakaboSoft Wiki
TakaboSoft Wiki
ソフトウェアに関する質問・不具合報告・要望などを書き込むことができます。