さて敵情報が確立したので、今度は敵にショットを撃たせてみましょう。
今回は追加する部分が多いので、同梱されているプロジェクトの内容と比較しながらご覧下さい。
まず、弾幕の登録は、敵が出現してからのカウント数と、設定した弾幕発射カウントが一致した時に行います。
敵に設定された、弾幕開始時間はbltimeです。敵が出現してからのカウント数はcntです。
よって以下の時に登録します。
---- enemy.cpp に赤字部を追加 ---- void enter_shot(int i){ int j; for(j=0;j<SHOT_MAX;j++){//フラグのたって無いenemyを探す if(shot[j].flag==0){//未使用の弾幕データがあれば memset(&shot[j],0,sizeof(shot_t));//初期化して登録 shot[j].flag=1;//フラグをたてる shot[j].knd=enemy[i].blknd;//弾の種類 shot[j].num=i;//num=どの敵から発射されたものか。 shot[j].cnt=0; return ; } } } //敵の行動制御 void enemy_act(){ int i; for(i=0;i<ENEMY_MAX;i++){ if(enemy[i].flag==1){//その敵のフラグがオンになってたら if(0<=enemy[i].pattern && enemy[i].pattern<ENEMY_PATTERN_MAX){ enemy_pattern[enemy[i].pattern](i); enemy[i].x+=cos(enemy[i].ang)*enemy[i].sp; enemy[i].y+=sin(enemy[i].ang)*enemy[i].sp; enemy[i].x+=enemy[i].vx; enemy[i].y+=enemy[i].vy; enemy[i].cnt++; enemy[i].img=enemy[i].muki*3+(enemy[i].cnt%18)/6; //敵が画面外に外れたら消す if(enemy[i].x<-20 || FIELD_MAX_X+20<enemy[i].x || enemy[i].y<-20 || FIELD_MAX_Y+20<enemy[i].y) enemy[i].flag=0; if(enemy[i].bltime==enemy[i].cnt) enter_shot(i); } else printfDx("enemy[i].patternの%d値が不正です。",enemy[i].pattern); } } }
ここでその弾幕データはどうなっているか、構造体の定義と変数の定義を見てみましょう。
注: define.h にある
#include "struct.h"
はファイルの一番最後になるようにして下さい。以後の章も同様です。
--- define.h に以下を追加 --- //敵1匹が持つ弾の最大数 #define SHOT_BULLET_MAX 1000 //一度に画面に表示できる敵の弾幕の最大数 #define SHOT_MAX 30 //ショットの種類の最大数 #define SHOT_KND_MAX 1 //効果音の種類の最大数 #define SE_MAX 100 //敵の行動パターンの最大数 #define ENEMY_PATTERN_MAX 11 --- struct.h に以下を追加 --- //弾に関する構造体 typedef struct{ //フラグ、種類、カウンタ、色、状態、少なくとも消さない時間、エフェクトの種類 int flag,knd,cnt,col,state,till,eff; //座標、角度、速度、ベースの角度、一時記憶スピード double x,y,angle,spd,base_angle[1],rem_spd[1]; }bullet_t; //ショットに関する構造体 typedef struct{ //フラグ、種類、カウンタ、どの敵から発射されたかの番号 int flag,knd,cnt,num; //ベース角度、ベーススピード double base_angle[1],base_spd[1]; bullet_t bullet[SHOT_BULLET_MAX]; }shot_t; --- GV.h に以下を追加 --- GLOBAL int img_bullet[10][10]; //弾の画像 //音楽ファイル用変数部 GLOBAL int sound_se[SE_MAX]; GLOBAL int se_flag[SE_MAX]; //サウンドフラグ GLOBAL shot_t shot[SHOT_MAX];//ショット情報 --- ini.cpp の ini() に以下を追加 --- memset(shot,0,sizeof(shot_t)*SHOT_MAX);
bullet_tが弾についての構造体です。 これもまた、沢山変数がありますが、覚える必要はありません。
ざっと見ておいて、こんな変数が中にあるんだなとだけ知っておいて下さい。
弾にはフラグをはじめ、飛んでいるスピードや角度、座標や状態などが必要なのは当然ですね。
中でもtilは最低でも消えない弾のカウント数です。
単純に「画面から消えたら消す」では、自分が本当に作りたい弾幕が作れない場合があります。
そこで、最低でもtillで設定した時間は弾が画面外に出ても消えないようにする為にこの変数を設定します。
effはその弾にどんなエフェクトを付けるか、base_angleは弾の角度が動的に変化する時、その基準となる角度を保存するものです。
shot_tという構造体は弾幕データを1つ分管理する構造体です。
弾幕データ1つ分に、各変数と、SHOT_BULLET_MAX(ここでは1000)個弾がある構造になっています。
shot_tは敵が撃つ弾幕情報なので、敵が一度に表示出来る最大数と同じ数だけ用意しておけばいいでしょう。
つまり、弾幕データの格納庫を30個用意しておけば、結局弾は30xSHOT_BULLET_MAX個用意することになり、
かなりの数用意される事になりますので、bullet_tにはあまり無駄な変数は持たせないようにしましょう。
また、弾幕データ、弾データ共に登録制なので、登録している情報を探して計算します。
そこで、弾幕情報メイン部はshot.cppファイル内でこのようになっています。
--- shot.cpp を以下のように記述 --- #include "../include/GV.h" extern void shot_bullet_H000(int); void (*shot_bullet[SHOT_KND_MAX])(int) ={ shot_bullet_H000, }; //n番目のショットを登録した敵と自機との角度を返す double shotatan2(int n){ return atan2(ch.y-enemy[shot[n].num].y,ch.x-enemy[shot[n].num].x); } //空いている弾を探す int shot_search(int n){ int i; for(i=0;i<SHOT_BULLET_MAX;i++){ if(shot[n].bullet[i].flag==0){ return i; } } return -1; } void shot_main(){ int i; for(i=0;i<SHOT_MAX;i++){//弾幕データ計算 //フラグが立っていて、設定した種類が間違っていなければ(オーバーフロー対策) if(shot[i].flag!=0 && 0<=shot[i].knd && shot[i].knd<SHOT_KND_MAX){ shot_bullet[shot[i].knd](i);//.kndの弾幕計算関数を呼ぶ関数ポインタ shot_calc(i);//i番目の弾幕を計算 shot[i].cnt++; } } } --- shotH.cpp に以下を追加 --- //1発だけ、自機に向かって直線移動 void shot_bullet_H000(int n){ int k; if(shot[n].cnt==0){ if(shot[n].flag!=2 && (k=shot_search(n))!=-1){ shot[n].bullet[k].knd =enemy[shot[n].num].blknd2; shot[n].bullet[k].angle =shotatan2(n); shot[n].bullet[k].flag =1; shot[n].bullet[k].x =enemy[shot[n].num].x; shot[n].bullet[k].y =enemy[shot[n].num].y; shot[n].bullet[k].col =enemy[shot[n].num].col; shot[n].bullet[k].cnt =0; shot[n].bullet[k].spd =3; se_flag[0]=1; } } } --- main.cpp の 赤字部分を追加 --- int WINAPI WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR lpCmdLine,int nCmdShow){ ChangeWindowMode(TRUE);//ウィンドウモード if(DxLib_Init() == -1 || SetDrawScreen( DX_SCREEN_BACK )!=0) return -1;//初期化と裏画面化 while(ProcessLoop()==0){//メインループ music_ini(); switch(func_state){ case 0://初回のみ入る処理 load(); //データロード first_ini();//初回の初期化 func_state=99; break; case 99://STGを始める前に行う初期化 ini(); load_story(); func_state=100; break; case 100://通常処理 calc_ch(); //キャラクタ計算 ch_move(); //キャラクタの移動制御 enemy_main();//敵処理メイン shot_main();//ショットメイン graph_main();//描画メイン stage_count++; break; default: printfDx("不明なfunc_state\n"); break; } music_play(); if(CheckStateKey(KEY_INPUT_ESCAPE)==1)break;//エスケープが入力されたらブレイク ScreenFlip();//裏画面反映 } DxLib_End();//DXライブラリ終了処理 return 0; }
弾幕データの最大数までループして登録されているものを探し、登録されているデータがあれば、計算部に処理を渡します。
弾幕の種類はこれからかなりの数作ることになるので、敵の行動パターンの時のように関数ポインタを使っています。
実際の計算部は以下のようになっています。(先ほどの関数の上に追加します)
--- shot.cpp に以下を追加 --- void shot_calc(int n){ int i,max=0; if(enemy[shot[n].num].flag!=1)//敵が倒されたら shot[n].flag=2;//それ以上ショットを登録しないフラグに変える for(i=0;i<SHOT_BULLET_MAX;i++){//n番目の弾幕データの弾を計算 if(shot[n].bullet[i].flag>0){//その弾が登録されていたら shot[n].bullet[i].x+=cos(shot[n].bullet[i].angle)*shot[n].bullet[i].spd; shot[n].bullet[i].y+=sin(shot[n].bullet[i].angle)*shot[n].bullet[i].spd; shot[n].bullet[i].cnt++; if(shot[n].bullet[i].x<-50 || shot[n].bullet[i].x>FIELD_MAX_X+50 || shot[n].bullet[i].y<-50 || shot[n].bullet[i].y>FIELD_MAX_Y+50){//画面から外れたら if(shot[n].bullet[i].till<shot[n].bullet[i].cnt)//最低消えない時間より長ければ shot[n].bullet[i].flag=0;//消す } } } //現在表示中の弾が一つでもあるかどうか調べる for(i=0;i<SHOT_BULLET_MAX;i++) if(shot[n].bullet[i].flag>0) return; if(enemy[shot[n].num].flag!=1){ shot[n].flag=0;//終了 enemy[shot[n].num].flag=0; } }
弾幕メイン関数から弾幕番号がnとして渡されますので、n番目の弾幕データを計算します。
弾幕データの中にSHOT_BULLET_MAX個の弾があるので、更にその中で登録されているデータの計算を行います。
そして、敵は倒されたらそれ以上弾を発射し続けるとおかしいので、敵は倒されたらそこで
弾の登録をやめなければなりません。そこで、最初に敵が倒されたらフラグを2にしているのです。
そして、画面中に弾が一つもなければ弾幕を終了します。
そして、Hardの0番の弾幕計算関数はshotH.cppでshot_bullet_H000として以下のように計算します。
--- shotH.cpp に以下を追加 --- void shot_bullet_H000(int n){ int k; if(shot[n].cnt==0){//弾幕が始まって0カウント目 //敵が倒されていなくて、探した登録可能な弾番号が有効なら if(shot[n].flag!=2 && (k=shot_search(n))!=-1){ shot[n].bullet[k].knd =enemy[shot[n].num].blknd2;//弾の種類 shot[n].bullet[k].angle =shotatan2(n);//角度 shot[n].bullet[k].flag =1;//フラグ shot[n].bullet[k].x =enemy[shot[n].num].x;//座標 shot[n].bullet[k].y =enemy[shot[n].num].y; shot[n].bullet[k].col =enemy[shot[n].num].col;//色 shot[n].bullet[k].cnt =0;//カウンタ shot[n].bullet[k].spd =3;//スピード se_flag[0]=1;//弾の発射音フラグを立てる } } }
shot[n].numは登録した敵のデータ識別番号なので、enemyに対して上記のように利用できます。
また、最後にse_flag[0]=1としているのは、弾の発射音を鳴らすためです。
音楽ファイルの管理は一箇所で行った方がいい為、ここではフラグのみを立てます。
music.cppファイルに以下の2つの関数を追加します。
--- music.cpp に以下を追加 --- void music_ini(){ memset(se_flag,0,sizeof(int)*SE_MAX); } void music_play(){ int i; for(i=0;i<SE_MAX;i++){ if(se_flag[i]==1) PlaySoundMem(sound_se[i],DX_PLAYTYPE_BACK); } }
music_iniで、ループの最初に毎回フラグを全て0にします。
そしてループの最後にmusic_playでフラグが立っている番号の効果音を鳴らします。
するとPlaySoundMemは一箇所にのみかけばよく、管理もバグチェックも楽になります。
後はshot.cppに追加した関数を他から呼べるようにしておきます。
--- function.h に以下を追加 --- //shot.cpp GLOBAL double shotatan2(int n); GLOBAL int shot_search(int n); GLOBAL void shot_main(); //music.cpp GLOBAL void music_ini(); GLOBAL void music_play(); --- load.cpp に以下を追加 --- //弾画像のファイルを読み込む LoadDivGraph( "../dat/img/bullet/b0.png" , 5 , 5 , 1 , 76 , 76 , img_bullet[0] ) ; LoadDivGraph( "../dat/img/bullet/b1.png" , 6 , 6 , 1 , 22 , 22 , img_bullet[1] ) ; LoadDivGraph( "../dat/img/bullet/b2.png" , 10 , 10 , 1 , 5 , 120 , img_bullet[2] ) ; LoadDivGraph( "../dat/img/bullet/b3.png" , 5 , 5 , 1 , 19 , 34 , img_bullet[3] ) ; LoadDivGraph( "../dat/img/bullet/b4.png" , 10 , 10 , 1 , 38 , 38 , img_bullet[4] ) ; LoadDivGraph( "../dat/img/bullet/b5.png" , 3 , 3 , 1 , 14 , 16 , img_bullet[5] ) ; LoadDivGraph( "../dat/img/bullet/b6.png" , 3 , 3 , 1 , 14 , 18 , img_bullet[6] ) ; LoadDivGraph( "../dat/img/bullet/b7.png" , 9 , 9 , 1 , 16 , 16 , img_bullet[7] ) ; LoadDivGraph( "../dat/img/bullet/b8.png" , 10 , 10 , 1 , 12 , 18 ,img_bullet[8] ) ; LoadDivGraph( "../dat/img/bullet/b9.png" , 3 , 3 , 1 , 13 , 19 , img_bullet[9] ) ; //敵のショット音を読み込む sound_se[0]=LoadSoundMem("../dat/se/enemy_shot.wav");
最後に弾を描画します。
ある弾幕情報(shot)の中の弾(bullet)が発射されていたらそのフラグがオンのはずです。
全ての配列のフラグを調べ、フラグがオンになっていたらその種類の弾を表示します。
ここで「線形補完描画」というわかりにくい言葉が出てきますが、
これをセットして描画することで、「座標1.5ピクセル目に描画」のように、表示したい位置に小数点が含まれるような場合、
キレイに中間を補間して描画してくれるのです。
ここで、中学の時にならった一次関数「y=10x」のグラフを思い出してください。キツイ傾斜の右上がりですね。
こんなショットを撃つ時、小数点を考慮しないピクセル上を進むと、ガタガタになってしまうことが予想できると思います。
ですのでこのバイリニア法と呼ばれる方法で描画することで中間をキレイに補間して描画させます。
--- graph.cpp に以下を追加 --- //弾丸の描画 void graph_bullet(){ int i,j; SetDrawMode( DX_DRAWMODE_BILINEAR ) ;//線形補完描画 for(i=0;i<SHOT_MAX;i++){//敵の弾幕数分ループ if(shot[i].flag>0){//弾幕データがオンなら for(j=0;j<SHOT_BULLET_MAX;j++){//その弾幕が持つ弾の最大数分ループ if(shot[i].bullet[j].flag!=0){//弾データがオンなら if(shot[i].bullet[j].eff==1) SetDrawBlendMode( DX_BLENDMODE_ADD, 255) ; DrawRotaGraphF( shot[i].bullet[j].x+FIELD_X, shot[i].bullet[j].y+FIELD_Y, 1.0, shot[i].bullet[j].angle+PI/2, img_bullet[shot[i].bullet[j].knd][shot[i].bullet[j].col],TRUE); if(shot[i].bullet[j].eff==1) SetDrawBlendMode( DX_BLENDMODE_NOBLEND, 0) ; } } } } SetDrawMode(DX_DRAWMODE_NEAREST);//描画形式を戻す } --- graph.cpp に以下赤字部を追加 --- void graph_main(){ graph_enemy(); graph_ch(); graph_bullet(); graph_board(); } --- load.cpp の赤字部を変更 --- //敵の出現情報をエクセルから読み込んで格納する関数 void load_story(){ int n,num,i,fp; char fname[32]={"../dat/csv/13章/storyH0.csv"}; int input[64]; char inputc[64]; fp = FileRead_open(fname);//ファイル読み込み if(fp == NULL){ printfDx("read error\n"); return; }
実行結果
- Remical Soft -