第十三章 让敌人射出子弹吧
接下来,既然我们已经把敌人的信息确立了,那么我们就试着让它们射出子弹吧。
由于本节追加的地方有点多,所以请一边比较一起发布的工程的内容,一边往下阅读。
首先,我们在敌人出现之后的计数器和设定好了的弹幕发射的计数器的值相同的时候进行弹幕的登录。
设定在敌人信息中的弹幕的开始时间是bltime。而敌人出现后的计数器变量是cnt。
因此在以下时刻进行登录。
—- 在enemy.cpp中追加了红字部分 —-
/*** 修改注意 ***/
void enter_shot(int i){
int j;
for(j=0;j<SHOT_MAX;j++){//搜索flag为无效的敌人
if(shot[j].flag==0){//如果有没有使用过的弹幕数据
memset(&shot[j],0,sizeof(shot_t));//初始化并登录之
shot[j].flag=1;//设置flag为有效
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){//如果敌人的flag为有效的话
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);
}
}
}
现在让我们看看结构体和变脸的定义,它们描述了弹幕数据是什么样的。
请保证
#include "struct.h"
写在define.h的最后。以后的章节中也是这样。
— 在define.h 中进行以下追加 —
//一个敌人所拥有的最大子弹的数量
#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{
//flag、种类、计数器、颜色、状态、保证不消失的最短时间、效果的种类
int flag,knd,cnt,col,state,till,eff;
//坐标、角度、速度、基本角度、瞬间记忆速度
double x,y,angle,spd,base_angle[1],rem_spd[1];
}bullet_t;
//和射击有关的结构体
typedef struct{
//flag、种类、计数器、发射的敌人的编号
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]; //SE的flag
GLOBAL shot_t shot[SHOT_MAX];//射击信息
— 在ini.cpp 的 ini() 中进行以下追加 —
memset(shot,0,sizeof(shot_t)*SHOT_MAX);
bullet_t是和子弹相关的结构体。它内部同样也包含了很多的变量,不过没有必要记住。
只需要一眼看上去,能够知道这个结构体中有这样的变量就行了。
对于子弹来说,flag、飞行的速度、角度、坐标、状态之类的信息自然是必要的。
其中,til表示至少此区间内不会消失的子弹的计数器数。
如果“只要跑出画面就销毁之”,那么就可能出现做不出来自己想做的弹幕。
因此,为了在till设定的时间内子弹就算跑到画面外面也不销毁之,也就设定了这个变量。
eff表示子弹附加了什么样子的效果,base_angle表示子弹的角度动态变化的时候保存以作为基准的角度。
shot_t这个构造体表示将弹幕数据作为一个实体来管理的构造体。
每一个弹幕数据都作为拥有各种变量和拥有SHOT_BULLET_MAX(在这里是1000)个子弹的结构体。
由于shot_t是敌人射出的弹幕的信息,我们只需要定义敌人某个时刻所能表现的最大的子弹的数量就行了。
换言之,弹幕数据的数据库中如果事先准备了30个弹幕数据,那么总的子弹的数量就是30XSHOT_BULLET_MAX个蛋了。由于现在保存了相当大的数,因此最好不要让bullet_t中有太多不相关的变量。
其次,由于弹幕数据、子弹数据都使用了登录的方法,搜索登录的信息然后计算之。
因此,弹幕信息main部分在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++){//弹幕数据计算
//如果flag为有效且设定的种类没有错误的情况(防止溢出)
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 中进行以下追加 —
//只发射一发,向自机直线移动
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();//敌人处理main
/*** 修改请注意 ***/
shot_main();//射击main
/*** 修改请注意 ***/
graph_main();//绘制main
stage_count++;
break;
default:
printfDx("错误的func_state\n");
break;
}
/*** 修改请注意 ***/
music_play();
/*** 修改请注意 ***/
if(CheckStateKey(KEY_INPUT_ESCAPE)==1)break;//按下ESC键则跳出循环
ScreenFlip();//里外画面翻转
}
DxLib_End();//DX Library终止处理
return 0;
}
直至弹幕数据的最大数量循环搜索已经登录的弹幕,如果有已经登录的数据,那么交给计算部分处理。 从现在开始我们将会制作种类繁多的弹幕,因此这里我们按照敌人行动模式那样子使用函数指针。
实际的计算部分如下所示。(在之前的函数的基础上进行追加)
—在 shot.cpp 中进行以下追加 —
void shot_calc(int n){
int i,max=0;
if(enemy[shot[n].num].flag!=1)//如果敌人被打倒的话
shot[n].flag=2;//将之前登录的射击的flag设置为无效
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;
}
}
由于从弹幕main函数中将弹幕编号作为n传递出去,因此我们要进行第n号弹幕数据的计算。
由于弹幕数据中有SHOT_BULLET_MAX个子弹,我们进一步进行在其中登录的数据的计算。
并且,敌人被打倒了还在发射子弹的话看起来会很奇怪,因此我们必须在敌人被打倒之后取消子弹的登录。因此,敌人一旦被打倒,就flag设定而为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;//flag
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;//置子弹的发射声音flag为有效
}
}
}
由于shot[n].num是登录了的敌人数据的识别编号,因此对于enemy可以像上面那样使用。
其次,最后se_flag[0]=1是为了展示子弹的发射声效。
由于音乐文件的管理在某个固定的地方进行会比较好,因此这里只将其flag设置为有效。
在music.cpp文件中追加下面两个函数。
—在 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,使得循环开始之前,每个se的flag都为0。
接着,在循环的最后使用music_play播放所有flag为有效的编号的效果音。
这么一来,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)发射的时候,其flag应该是有效的。
此时检查整个数组的flag,如果某个种类子弹的flag为有效,那么就显示之。
这里有一个叫做“线性插值绘制”的不好理解的词汇。
当我们使用这个技术的时候,像进行“在坐标为1.5像素的地方绘制”这种,想要显示图像的位置有小数点的场合,便可以非常好地在中间进行插值,然后描绘出来。
现在,请想象一下我们在中学的时候学习的一次函数“y=10x”的图像。它是笔直地倾泻向右上方延伸呢。
当这种射击进行的时候,可以想象,如果只在没有小数点的像素上前进,那么子弹会前进地会变得摇摇晃晃的。
因此如果我们使用被称为双线性(Bilinear)法的方法来描绘的话,就可以在其中进行很好地插值并描绘出来。(译者注:插值,这里也可以译为补间,如果知道补间动画这个概念的人应该会很容易明白这个道理。)
—在 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 中进行红字部分变更 —
//从Excel中读入敌人出现信息并保存的函数
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;
}
运行结果