【四圣龙神录的编程教室】第13章、给敌人加上射击功能吧

原文地址:

http://dixq.net/rp/13.html


现在我们已经建立好了敌人的信息,这次我们给敌人添加射击子弹的功能吧。

这次要添加的部分很多,我们一边和项目进行比较,一边看吧。

首先,弹幕的出现,是在敌机出现开始计算时间,到了设定好的弹幕发射时间时,进行发射的。

在敌机上设定的,开始发射弹幕的时间,是bltime 变量。从敌人出现开始计数的变量是 cnt。

到了预定的时间,就注册弹幕数据。

————————————————————————————————————————————————————————————————————

//---- enemy.cpp 里添加下面红字的部分  ----

void enter_shot(int i){			//这个函数都是红字
        int j;
        for(j=0;j<SHOT_MAX;j++){//根据flag,寻找空闲的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)      // 这个if 也是红字
                    enter_shot(i);
            }
            else
                printfDx("enemy[i].pattern的%d值不正确。",enemy[i].pattern);
        }
    }
}
————————————————————————————————————————————————————————————————————
在这里,那个弹幕数据是什么样的呢,让我们看看构造体的定义和变量的定义吧。

注:define.h 里有一句

#include "struct.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{
//标志、种类、计数、颜色、状态、至少存在的时间、效果的种类
        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是和子弹相关的构造体。这个也是包含了很多的变量,没必要全部都记住。

只要看到了的话,知道包含这样的变量就行了。

子弹首先包含一个flag标志,另外还包含飞行的速度和角度,坐标和状态等必要的一些参数。

其中 til 表示子弹至少显示多久不消失的 时间技术。

如果只是“飞出画面就消除”的话,有些真正自己想做的弹幕会做不出来。

所以,至少 在 til 里设定的时间,是就算子弹到了画面外也不会消失的,这个变量就是代表这个含义。

eff 是那个子弹会表示什么样的效果, base_angle 是子弹的角度在运动中变化的时候,以此作为一个基准的角度而保存的变量。

shot_t 是管理一种弹幕数据的构造体。

每种弹幕数据里面,每个变量,有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++) //弹幕数据的计算
    {
        //设置了flag标志、并且在设置好的的种类里面(防止溢出)
        if(shot[i].flag!=0 && 0<=shot[i].knd && shot[i].knd<SHOT_KND_MAX)
        {
            shot_bullet[shot[i].knd](i);//指向 knd的弹幕计算函数的指针 s
            hot_calc(i);
            //计算第i个弹幕
            shot[i].cnt++;
        }
    }
}

//--- shotH.cpp 里添加下面的代码 ---
//子弹射击轨迹只有一种,朝自机方向的直线运动
void shot_bullet_H000(int n)
{
    int k;    //--- main.cpp 添加了红色部分 ---
    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;
        }
    }
}
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;//按下ESC的话,退出主循环
        ScreenFlip();//刷新双缓冲
    }
    DxLib_End();//DXLib 的结束处理
    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;
        }
}

——————————————————————————————————————————————————————————————————————————

在弹幕的main 函数里,根据传进来的弹幕编号 n , 计算第n个的弹幕数据。

弹幕数据里面,有SHOT_BULLET_MAX 个子弹,并且对其中注册了的数据进行计算。

假如敌人已经被击倒了还能发射子弹的话,这是不科学的。所以我们在敌人被击破了以后,

就不再进行子弹的注册。于是,敌人被击破了以后,我们要设置两个标志。

如果画面中,一个子弹也没有的话,就结束弹幕。

接下来,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文件里,添加下面的两个函数。

————————————————————————————————————————————————————————————————————————

//--- 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() 里面,在循环的开始的时候,把所有的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 如果设置为开启,就对那种子弹进行表示。

这里对“线性插值绘图”,进行一个简单易懂的介绍,

设置了这种画图方式后,比如像“在坐标为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 红字部分进行更改 ---

//从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;
        }
——————————————————————————————————————————————————————————————————————————

运行结果:

http://dixq.net/rp/swf/13.swf


像运行结果那样,有各种各样的行动方式的敌人的话,就成功了。


(2014-2-18 笔记:这节给敌人加上射击功能,现在回顾一下,理一理逻辑怎么实现的。

在敌机操作的  enemy_act 里面添加一个,到了射击时间就进行弹幕载入的功能。在 shot[SHOT_MAX] 里,找个flag 为空的,设置弹幕标记,子弹种类,敌机编号。

然后主函数里,添加了弹幕的主函数 shot_main()  对所有弹幕进行更新操作。包含 shot_bullet (弹幕模式)和 shot_calc (子弹移动)

shot[i].knd 对应一种弹幕类型,在这里的弹幕模式shot_bullet_H000() ,在shot[n] 里找到一个bullet ,这是子弹,类型设置为 blknd2,设置坐标为敌机,角度向自机。

然后子弹移动里,对这个弹幕下的所有子弹的坐标进行更新,若超出范围则移除。


这下逻辑算是懂了,shot 是弹幕,每个弹幕里有bullet[n] ,这是这个弹幕的子弹。shot 弹幕的类型,是enemy[i].blknd , 而弹幕子弹的类型是enemy[shot[n].num].blknd2

敌机到点,是发射弹幕。 弹幕模式函数里,会自己根据需要加子弹)




本人CSDN博客目录:

http://blog.csdn.net/tidus5



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值