iPhone/iPad用ドット絵エディタ「EDGE touch」

最近のエントリー

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

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

三角関数は怖くない(謎


三角関数というと、なんだか難しく考える人もいるかもしれませんが、今回は別にムツカシー計算をするわけではありません。

右の図を見てください。
これはだいぶ いびつ ではありますが、サインカーブっぽいものです。
ここでまず覚えて欲しいのは、sin関数は「波形」という事と、最大値・最小値が+1・-1という事です。

それから前にも書きましたが、C言語の場合sin関数の引数の単位はラジアンなので、

360度=2π

1度=π/180

となります(今回はあんまり関係ありませんが)。

地球を歪ませる

さて、ここらでサンプルプログラムを見てみましょう。

今回は、画面中に3パターンの歪む地球を表示させています。

また、DirectDrawに関しては、「DirectDraw基礎 第10回」のサンプルコードを使用しています。

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
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
     
    
 
    //パレット設定
    LoadPalette("earth.bmp");
     
    //ビットマップを作業用サーフェイスへ読み込む
    LoadBitmap(lpWork,"earth.bmp",60,15,200,210);
 
    int count=0,i;
    float width=0.f,width_max=50,width_add=0.15f;
 
    DWORD tim;
    while(1){
         
        tim=timeGetTime();      //疑似タイマー処理
        ClearScreen(lpBack);    //バックバッファクリア
        count++;                //カウンタ
 
        //ゆれの大きさを時間によって変化させる(各パターン共通)
        if(width_add>0){
            if((width+=width_add)>width_max)
                width_add=-width_add;
        } else {
            if((width+=width_add)&lt0){
                width=0;
                width_add=-width_add;
            }
        }
 
        //ゆらすパターン1
        SetClipArea(0,0,320,240);
        for(i=0;i<240;i++)
            BltClip(lpBack,int(sin((count+i)/10.f)*width),
                i,320,1,lpWork,0,i,FALSE);
         
        //ゆらすパターン2
        SetClipArea(320,0,320,240);
        for(i=0;i<320;i++)
            BltClip(lpBack,320+i,int(sin((count+i)/10.f)*width),
                1,240,lpWork,i,0,FALSE);
 
        //ゆらすパターン3
        SetClipArea(0,240,320,240);
        for(i=0;i<240;i++)
            BltClip(lpBack,
                ((i%2)?-1:1)*int(sin((count+i)/10.f)*width),
                240+i,320,1,lpWork,0,i,FALSE);
 
        Flip();                 //フリッピング
 
        do{                     //メッセージループ
            while(PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)){
                if(!GetMessage(&msg,NULL,0,0))
                    Quit();
                TranslateMessage(&msg);
                DispatchMessage(&msg);
            }
        }while(timeGetTime()<tim+16);    //1000/60=16.666(秒間60回更新予定)
    }
     
    return(FALSE);
}

順番に説明していきましょうか。

まずパレットを読み込み、地球の画像をlpWorkサーフェイスへ読み込んでおきます。

1
2
3
4
5
//パレット設定
LoadPalette("earth.bmp");
 
//ビットマップを作業用サーフェイスへ読み込む
LoadBitmap(lpWork,"earth.bmp",60,15,200,210);

ここで、画像は320×240内の中央へ来るように、読み込んだ画像の転送先をずらしています(60,15)。
(解りやすくなると思ったので(^^;))

そんでもって今日の本題の部分

1
2
3
4
//ゆらすパターン1
SetClipArea(0,0,320,240);
for(i=0;i<240;i++)
    BltClip(lpBack,int(sin((count+i)/10.f)*width),i,320,1,lpWork,0,i,FALSE);

1つ前の段落でサインカーブについて触れましたが、ここではサインカーブの図を90度右へ回転させたような感じで見ると直ぐに解ると思います(下図参照)。

ここでは画像を、ズバシュァァァ!!と薄く横に切り刻みまくり、それらを横にずらしながら置いていく感じです(謎)。

毎回countをインクリメントする事で、使用する波形の「開始位置」をずらす事が出来ます。
これにより、「動く」うねりを実現出来ます。また、・・・)/10.fと適当に割っていますが、大きな数字で割ると、波の間隔が広くなります。
まぁ、いろいろ変数をいじってみて、「どこを変えるとどうなる」というのを試してみてください(^^;説明するより、そうした方が早いです。

あと、パターン2・パターン3がありますが、これはもう説明するまでもありませんね。

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

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

やっぱり苦手なGDI

何のフォントも設定しないまま文字列を出力する方法は「DirectDraw基礎 第4回」、「DirectDraw基礎 第6回」で既にやりました。
今回は、True Typeを使用するという事で、フォントを作成しなければなりません。

ではサンプルコードを順番に説明していきます。

TextOutFont関数が今回作成した自作関数です。

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
//----------[ True Typeフォントを使用したテキストの出力 ]---------------------------------------
void TextOutFont(LPDIRECTDRAWSURFACE surface, int x, int y, int height, char *text_str, char *font_name, unsigned char col){
 
    HFONT new_font,old_font;
    HDC hdc;
 
    //フォント作成
    new_font=CreateFont(
        height, //高さ
        0,  //横幅
        0,  //角度
        0,  //よくわからなんだ
        FW_NORMAL,  //太さ
        0,  //斜体
        0,  //下線
        0,  //打ち消し
        DEFAULT_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,
        DEFAULT_QUALITY,DEFAULT_PITCH,font_name);
     
    surface->GetDC(&hdc);            //デバイスコンテキスト取得
    SetBkMode(hdc,TRANSPARENT);         //背景モード設定
    old_font=(HFONT)SelectObject(hdc,new_font); //フォント選択
                        //フォントカラー設定
    SetTextColor(hdc,RGB(peEntry[col].peRed,peEntry[col].peGreen,peEntry[col].peBlue));
    TextOut(hdc,x,y,text_str,strlen(text_str)); //出力
    SelectObject(hdc,old_font);         //フォント復元
    surface->ReleaseDC(hdc);         //デバイスコンテキスト解放
    DeleteObject(new_font);         //フォント消去
}
 
 
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
 
    
 
    //メッセージループ
    while(1){
        //テキスト出力
        ClearScreen(lpBack);
         
        //ノーマル
        TextOutFont(lpBack,0,0,60,"Takabo Soft 2000","Comic Sans MS",1);
 
        
 
        Flip();

まず、CreateFont関数を呼び出して、フォントを作成します。書式は以下の通りです。

書式 HFONT CreateFont( int nHeight, int nWidth, int nEscapement, int nOrientation, int fnWeight, DWORD fdwItalic, DWORD fdwUnderline, DWORD fdwStrikeOut, DWORD fdwCharSet, DWORD fdwOutputPrecision, DWORD fdwClipPrecision, DWORD fdwQuality, DWORD fdwPitchAndFamily, LPCTSTR lpszFace ); ながっ!
nHeight フォントの高さを論理単位で指定。ドット数では無いらしい。0を指定すると、デフォルトの高さになる。
nWidth フォントの横幅を指定。0を指定すると、高さにあった横幅になる。
nEscapement 文字列全体の角度を1/10度単位で指定します。
nOrientation 各文字の角度を1/10度単位で指定します。とか言ってるけど、うちじゃうごかなんだ・・・。
fnWeight フォントの太さを指定します。指定する値は0~1000ですが、400で通常、700でボールド体です。
fdwItalic 1を指定すると斜体になります。
fdwUnderline 1を指定すると下線が付きます。
fdwStrikeOut 1を指定すると打ち消し線が付きます。
fdwCharSet フォントの文字セットを指定するらしい。よく解らないのでDEFAULT_CHARSETを指定(爆
fdwOutputPrecision 出力精度を指定するらしい。よく解らないのでOUT_DEFAULT_PRECISを指定(爆2
fdwClipPrecision クリッピング精度を指定するらしい。やっぱりよく解らないのでCLIP_DEFAULT_PRECISを指定(爆3
fdwQuality 出力品質を指定するらしい。・・・よく解らないのでDEFAULT_QUALITYを指定(爆4
fdwPitchAndFamily フォントのピッチとファミリを指定するらしい。なんじゃそりゃ!というワケでDEFAULT_PITCHを指定(爆5
lpszFace フォント名を指定します。
戻り値 成功すると、論理フォントのハンドルが返るらしい。

というように、私にも一部ワケが解りませんが、フォントが作成し終わったら、デバイスコンテキスト取得し、「SelectObject」を呼び出しています。
この関数は、指定したデバイスコンテキストにGDIのオブジェクト(フォント・ペン・ブラシ等)を選択する(関連づけさせるというべきか・・・)ものです。
この関数の戻り値は、今まで選択されていたGDIのオブジェクトです(新しくフォントを指定したら、今まで選択されていたフォントが戻り値となる)。

こうしてTextOut関数を呼び出してやれば、設定したフォントで文字列を描画してくれるわけです。
描画し終えたら、後始末としてSelectObjectで元のフォントを選択してやります。
昔この作業をサボったら動作がおかしくなったような気がしましたが今やってみると、そんなこともありませんねぇ・・・。
いらなくなったフォントとかを消す前にこうして、元のフォントをSelectObjectで選択させるというのが正しい方法なので、一応やっておいた方が良いと思います。

最後に、DeleteObject関数で、最初に作成したフォントを削除してやります。この作業をサボルと間違いなくウィンドウズはおかしくなります(爆。
ウィンドウズにはGDIリソースというのがあり、CreateFont関数のようにGDIの何かを作成する度に
GDIリソースが減っていきます。このリソースは普段のウィンドウズでも使用しているため、どっかのアプリケーションでこれを使い尽くしてしまうと、他のアプリケーションまで動かなくなるという事態が起きるので、要らなくなったら、ちゃんと消してやりましょう。

ちょっとした飾り付け

True Typeフォントで描画した場合、だいぶ陳腐なものになりがちですが、影や縁を付ける事で多少ながらマシになるかもしれません。

1
2
3
4
5
6
7
8
9
10
//影付き
TextOutFont(lpBack,0+2,100+2,60,"影付き","MS ゴシック",0);
TextOutFont(lpBack,0,100,60,"影付き","MS ゴシック",1);
 
//縁付き
TextOutFont(lpBack,0,200-1,60,"縁付き","MS 明朝",0);
TextOutFont(lpBack,0,200+1,60,"縁付き","MS 明朝",0);
TextOutFont(lpBack,0-1,200,60,"縁付き","MS 明朝",0);
TextOutFont(lpBack,0+1,200,60,"縁付き","MS 明朝",0);
TextOutFont(lpBack,0,200,60,"縁付き","MS 明朝",1);

今回は、TextOutFontを並べて影を付けたりしていますが、この関数を呼ぶ毎にフォント作成したり
しているので、アルゴリズム上よくないですよね?影を付ける機能なんかは、TextOutFont関数内に組み込んでしまった方が良いですね。
この辺は各自改造していってください。

フォントの注意点

True Typeフォントを使用する場合、実行するパソコンに使用するフォントがインストールされていなければなりません。
また、制作者のパソコンにフォントがあるからと言ってそれを使用した場合でも、そのプログラムを配布した先のパソコンにそのフォントがインストールされているかも解りません。
ですから、ウィンドウズに標準でインストールされているフォント以外の使用はお薦めできません(自作のフォントなら配布出来ると思いますが)。

「MS P明朝」だとか「MS Pゴシック」ならまず入ってると思います。

また、このような問題の解決策として、絵(BMP)で描いた文字を使用するという手もあります。
これもまたいつか紹介しましょうか。

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

では、まずサンプルコードをダウンロードし、解凍してください。
私はVisual C++6.0でコンパイルしているので、お持ちの方はVisual C++でプロジェクトファイルを開いてください(「game_11.dsw」をダブルクリックすれば開けます)。
圧縮ファイルに含まれる「game_11.exe」をダブルクリックし、実行してみてください。
どうでしょう?画面が切り替わり、フルスクリーン化すると思います。
そして、どこからか弾が沸き、自機(スライム)の方向へ飛んできますよね?

というわけで、今回は角度の算出とアニメーションについて説明していきたいと思います。

三角関数のお勉強 その1


第4回では、三角関数としてsin、cos、tanを説明しました。
しかし、これだけで、敵から、自機への角度を算出する事は出来ません。

そこで、今回はatan(アークタンジェント)というものを使用します。

普通のtanは

tanΘ=b/a

でしたが、アークタンジェントは

Θ=atan(b/a)

となります。要するに、タンジェントの逆算みたいな感じですね。

三角関数のお勉強 その2


というわけで、基本が解れば、あとは当てはめるだけですね。
左の図のように敵、自機が存在し、角度Θを求めたい場合、

Θ=atan((sy-my)/(sx-mx))

とすればいいですよね?

ただし、y/xが例えば1/1、-1/-1の場合、値は一緒ですよね?
という事で、atanの返値Θも半分の180度周期になってしまいます。(ちなみにsin,cosは360度周期,sin(0)=sin(360)=sin(360*n))

そこで、x,yの符号を利用して、ある条件の時はΘに180を足すと言った処理が必要になってきます。

また、xが0の場合、y/0=∞となってしまい、エラーとなってしまいます。
これも回避しなければなりません。

アークタンジェントをC言語で使用する

C言語でアークタンジェントを使用したい場合は、「math.h」をインクルードした後、atan( double v ) を呼び出せば出来ます。

ただ、上でも書いたように、返ってくる角度Θは180度周期だったり、xが0だった場合にエラーとなったりします。
で、いろいろ判定して正しい値を出そうとしがちですが、C言語にはatan2( double y, double x) という関数が用意されています。
この、yとxに値を引き渡してやると、360度周期で角度を返してくれます。しかも、xが0の時の処理もしっかりやってくれるので便利です。

ただ、atanもそうですが、atan2の返値の単位はラジアンです(0~360度=0~2π)。
atan2の場合は-π~+πまでが返ってくるので、atan2の返値をπで割れば、-1~+1までになりますよね?
そしてπは180°ですから、その値に180を掛けてやると、値は-180°~+180°になりますね?
これに、360°を足し、360で割った余りを出すと、0~359°の値が出てきます。

1
2
3
4
5
6
7
double tmp;
short angle;
tmp=atan2(-double(sy-my),double(sx-mx));
tmp/=3.1415926535;
tmp*=180;
tmp+=360;
angle=(short)tmp%360;

こんな感じかな。

これで敵から自機への角度Θが出せますね?

サンプルコードの解説

今回のコードは、「第9回 キー入力によって弾を発射」を少し改良し、ランダムに弾を作成し、自機の方向へ飛ばすようにします。
弾の移動等、詳しい事は第9回を参照してください。

今回は、ついでに弾をアニメーションさせる事にするので、_bullet構造体にanimationという変数を
追加しておきます。

1
2
3
4
5
6
7
//弾用変数
struct _bullet{
    BOOL enable;    //使用:1  未使用:0
    float x,y;      //座標
    char animation; //アニメーション
    short angle;    //進む角度
}bullet[BMAX];

では、メインループを見てみます。

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

第9回と見た目が同じですが、Shoot()とMoveBullet()、Show()の中身を変更しています。(他は全部同じ)

1
2
3
4
5
6
7
8
9
10
11
//----------[ 弾発射判定 ]----------------------------------------------------------------------
void Shoot(void){
    //1/100の確率で弾作成
    int x,y,angle;
    if(rand()%100==0){
        x=rand()%640;   //x,y座標はランダム
        y=rand()%480;
        angle=GetAngle(x+8,y+8,(int)player.x+16,(int)player.y+16);  //自機への角度取得
        CreateBullet((float)x,(float)y,angle);
    }
}

ここでは、1/100の確立で適当な位置を出し、自機への角度を取得して、弾を作成しています。
rand()という関数は0~RAND_MAXまでの乱数を返してくれるので、適当な値pで割った余りを出せば、0からpのランダム値が出せます(度数分布が崩れるので本当はあまりよろしくない)。

で、GetAngle(int mx,int my,int sx,int sy);関数は自作関数で、mx,myからsx,syまでの角度を算出します。
GetAngle(x+8,y+8,(int)player.x+16,(int)player.y+16);のx+8,y+8というように、ある値を足していますが、自機などの座標は、画像の左端を表わしているため、画像の中心を表わすためには、画像サイズの半分の値を足せばいいですよね?

1
2
3
4
//----------[ 角度取得 ]------------------------------------------------------------------------
short GetAngle(int mx,int my,int sx,int sy){
    return short(atan2(-double(sy-my),double(sx-mx))/3.141592*180+360)%360;
}

この自作関数は上の方で説明したので解りますね(^^;

で、弾をアニメーションさせるためには、弾移動中にanimationの値をどんどん増やしていきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//----------[ 弾移動 ]--------------------------------------------------------------------------
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;
         
        //アニメーション
        bullet[i].animation++;
        if(bullet[i].animation>=12)
            bullet[i].animation=0;
        //++bullet[i].animation%=12;  と書いても可
 
        //画面外に出たら未使用データにする
        if(bullet[i].x<-16 || bullet[i].x>640 || bullet[i].y<-16 || bullet[i].y>480)
            bullet[i].enable=0;
    }
}

今回使用する画像はです。弾のアニメーションは12パターンありますよね?
ですから、animationの値は0~11の間を繰り返し変化するようにプログラムしてやります。

で、後は、このanimationの値をうまく画像に照らし合わせて画面に表示してやればいいわけです。
1つの弾の画像サイズが16×16で左にスライムが32ドットあるので、弾画像のx座標は32+16×アニメーションという事になりますよね?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//----------[ 画像表示 ]------------------------------------------------------------------------
void Show(void){
    ClearScreen(lpBack);        //バックバッファ初期化
    DdTextOut(lpBack,0,0,"カーソルキーで移動 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,
                16,16,lpWork,32+16*bullet[i].animation,0,1);
 
    //自機転送
    BltClip(lpBack,(int)player.x,(int)player.y,32,32,lpWork,0,0,1);
    //フリップ
    Flip();
}

このアニメーションは、繰り返し行われる弾やら敵に限りますね。
自機のようなキー入力によって画像を変えたいときには使えません。

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

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

というわけで今回は、ジャンプについて解説したいと思います。

ジャンプ

さて、ジャンプと言うと重力が関係してくるので、なんとなく物理法則(奇数の法則だったかな・・・忘れた)っぽいものを思い浮かべそうですが、そんなムツカシー法則は一切
使いません(爆。第6回 慣性の時と処理が似ている(というかほとんど一緒)のですが、違うのは、Y座標に進む値(今回はsy)に重力として毎回一定の値を足すだけです。

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

今回は画面下が地面という設定にします。

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
//----------[ メイン関数 ]----------------------------------------------------------------------
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpszCmdParam, int nCmdShow){
     
    <略>
 
    int cx=320-16;
    float cy=448.0f;
    float sy=0.0f;
 
    //メインループ
    while(1){
        WaitSet();          //現在の時間(単位:ミリ秒)取得
        ClearScreen(lpBack);        //バックバッファ初期化
        DdTextOut(lpBack,0,0,"スペースキーでジャンプ  pauseキーで終了",255);
 
        //ジャンプ開始
        if(keyg[32] && cy==448.0f)
            sy=-12.0f;  //ジャンプ力
         
        cy+=sy;         //慣性(?
        sy+=0.3f;           //重力を加える
         
        if(cy>=448)      //画面より下に行かないようにする
            cy=448.0f;
         
        //キャラクタ表示
        BltClip(lpBack,cx,(int)cy,32,32,lpWork,0,0,1);
        Flip();                 //フリップ
        Wait(1000/60);              //メッセージループへ
    }  

自然落下は、空から地面に向かって重力によって加速しながら進んでいきますよね?
車で言えば、どんどんアクセル踏んでスピード出して加速していくようなものです。
で、今回のプログラムの場合、syがスピードのようなものになり、重力によってそのスピードが
どんどん増加していく形になります。

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

では、まずサンプルコードをダウンロードし、解凍してください。
私は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は双方向リスト構造でやってました。

記事検索

コミュニティ

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