第三十四章 试着制作激光吧

>>点此回到教程目录

制作难易度:★★★★★★★★ (8/10)


运行结果


如果说要制作像东方那样的弹幕的话,肯定要碰壁的那就是制作激光了。

激光到底是基于什么构造呢。

估计大家都抱着这样子的疑问吧。

在这里我就为大家介绍一下以我自己的方式的激光实现方法吧。

所以,我并不知道这到底是不是游戏业界通常使用的方法,因此先说明一下我自己也不知道这是不是一个合适的方法。

其次,就有必要讨论一下勾股定理、点与直线的距离以及向量的话题了。

虽然这是基础级别的数学讨论,但是如果有“这种东西根本就忘光了嘛!”这类人的话,还请你在谷歌上输入关键词搜一下吧。

首先我来说明一下激光的基本想法。

上图中红色的长方形部分是碰撞判定部分。

如果显示激光的图像的话也就将那个特定的长方形部分作为碰撞判定部分了。

这样一来无论是小的、大的、斜着的还是笔直的激光都能够做到了。

指定图像的4个顶点需要使用使用

DrawModiGraph函数

使用这个函数能够表示出任何朝向的长方形。

那么,碰撞判定需要怎么做呢?

其实现在就变成“长方形和圆的相交问题”了。

用于计算点和直线的距离、圆与圆的距离的方程都是大家都是知道的,但是判定长方形与圆的相交的方程式没有普通的方法的。

我们试着按着以下的步骤进行判定。

1、圆中是否有长方形的顶点 如果有判定为接触 如果没有如果没有的话进行后面的判定式的判定

2、圆是否进入了长方形中 如果进入的话判定为接触 如果没有的话进行后面的判定式的判定

3、圆的中心和长方形边的距离是否在半径以内 如果在半径以内判定为接触 除此之外可以判定为完全没有接触

首先我们来考虑“1、圆中是否有长方形的顶点”吧。

这里的处理只需要我们一个一个地计算“顶点和圆的中心”的距离就好了,非常简单。

-使用勾股定理计算点与点之间的距离。

这样一来

r = sqrt( (xx) + (yy) )

我们判定这个r是否在半径以内,就可以进行接触判定了。

//点是否在圆中 0:没有 1:是
double question_point_and_circle(pt_t p, pt_t rp,double r){
    double dx=p.x-rp.x,dy=p.y-rp.y;
    if(dx*dx + dy*dy < r*r)    return 1;
    else                    return 0;
}


/*中间省略*/

    /*判定圆中是否有长方形4个顶点中的某一个*/
    for(i=0;i<4;i++){
        if(question_point_and_circle(pt[i],rpt,r)==1)
            return 1;
    }

这样子写的话实现好了。

接下来我们来考虑“2、圆是否进入了长方形中”吧。

如果a,b,r所成的角θ和 d,c,r所成的角θ’都在π/2以内的话,那么就可以判定圆的中心在长方形中。

那么我们试着求这个θ。

利用向量A和向量B求解cosθ的方法我们在高中的时候就学过了呢。

在C语言中能够如下进行简单计算。

将两者一起使用的话再C语言中就能够简单地利用下面的函数来求角度。

我们试着实现上述结论吧。

/* 平面向量 */
typedef struct {
  double x, y;
} Vector2_t;

/* diff ← 向量 p - q */
void Vector2Diff(Vector2_t *diff, const Vector2_t *p, const Vector2_t *q)
{
  diff->x = p->x - q->x;
  diff->y = p->y - q->y;
}

/* 向量 p 和 q 的内积(译者注:线性代数中这叫内积,高中好像教的是“数量积”、“点乘”?) */
double Vector2InnerProduct(const Vector2_t *p, const Vector2_t *q)
{
  return p->x * q->x + p->y * q->y;
}

/* 向量 p 和 q 的外积
 (译者注:注意,2维空间中的叉乘是:
    V1(x1, y1) X V2(x2, y2) = x1y2 – y1x2
看起来像个标量,事实上叉乘的结果是个向量,方向在z轴上。上述结果是它的模。在二维空间里,让我们暂时忽略它的方向,将结果看成一个向量,我们有:
    A x B = |A||B|Sin(θ)
然而角度 θ和点乘的角度有一点点不同,他是有正负的,是指从A到B的角度。)*/
double Vector2OuterProduct(const Vector2_t *p, const Vector2_t *q)
{
  return p->x * q->y - p->y * q->x;
}

/*中略*/
/*中间省略*/

        /* 求向量 C→P 和 C→Q 所成的角θ以及旋转方向。*/
        Vector2_t c, p, q; /* 输入数据 */
        Vector2_t cp;      /* 向量 C→P */
        Vector2_t cq;      /* 向量 C→Q */
        double s;          /* 外积:(C→P) × (C→Q) */
        double t;          /* 内积:(C→P) ・ (C→Q) */
        double theta,theta2;/* θ (弧度) */

        /*将 c,p,q 设定为想要的值。*/
        c.x = pt[0].x;  c.y = pt[0].y;
        p.x = pt[1].x;  p.y = pt[1].y;
        q.x = x;                q.y = y;

        /* 计算旋转方向以及角度θ。*/
        Vector2Diff(&cp, &p, &c);          /* cp ← p - c   */
        Vector2Diff(&cq, &q, &c);          /* cq ← q - c   */
        s = Vector2OuterProduct(&cp, &cq); /* s ← cp × cq */
        t = Vector2InnerProduct(&cp, &cq); /* t ← cp ・ cq */
        theta = atan2(s, t);

接下来考虑“3、圆的中心和长方形边的距离是否在半径以内”吧。

我还记得在高中的时候学习过求点和直线的距离的方法,现在我们来求点和“线段”的距离。

将长方形的顶点设定为α、β的时候,首先判定圆的中心r是否在长方形中。

如果像上面那样r在长方形中的话,向量ab和向量ac所形成的θ应该有cosθ>0。

如果在长方形中的话,则从r开始向αβ引的垂线的长度就是距离了。

现在利用垂线向量和线段αβ的内积为0的结论进行以下实现。

//求点和线段的距离
double get_distance(double x, double y, double x1, double y1, 
                                        double x2, double y2){
    double dx,dy,a,b,t,tx,ty;
    double distance;
    dx = (x2 - x1); dy = (y2 - y1);
    a = dx*dx + dy*dy;
    b = dx * (x1 - x) + dy * (y1 - y);
    t = -b / a;
    if (t < 0) t = 0;
    if (t > 1) t = 1;
    tx = x1 + dx * t;
    ty = y1 + dy * t;
    distance = sqrt((x - tx)*(x - tx) + (y - ty)*(y - ty));
    return distance;
}


/*中间省略*/

    /*求线段和点的距离*/
    for(i=0;i<4;i++){
        if(get_distance(rpt.x,rpt.y,pt[i].x,pt[i].y,pt[(i+1)%4].x,pt[(i+1)%4].y)<r)
            return 1;
    }

现在试着将上述内容作为一束激光的判定函数来实现吧。

—-在 out_lazer.cpp中进行的新规追加如下—-

#include "../include/GV.h"
#include <math.h>
#include <stdio.h>

/* 平面向量 */
typedef struct {
    double x, y;
} Vector2_t;

/* diff ← 向量 p - q */
void Vector2Diff(Vector2_t *diff, const Vector2_t *p, const Vector2_t *q){
    diff->x = p->x - q->x;
    diff->y = p->y - q->y;
}

/* 向量 p 和 q 的内积 */
double Vector2InnerProduct(const Vector2_t *p, const Vector2_t *q){
    return p->x * q->x + p->y * q->y;
}

/* 向量 p 和 q 的外积 */
double Vector2OuterProduct(const Vector2_t *p, const Vector2_t *q){
    return p->x * q->y - p->y * q->x;
}

//求点和线段的距离
double get_distance(double x, double y, double x1, double y1, 
                    double x2, double y2){
    double dx,dy,a,b,t,tx,ty;
    double distance;
    dx = (x2 - x1); dy = (y2 - y1);
    a = dx*dx + dy*dy;
    b = dx * (x1 - x) + dy * (y1 - y);
    t = -b / a;
    if (t < 0) t = 0;
    if (t > 1) t = 1;
    tx = x1 + dx * t;
    ty = y1 + dy * t;
    distance = sqrt((x - tx)*(x - tx) + (y - ty)*(y - ty));
    return distance;
}

//返回点与点的距离
double get_pt_and_pt(pt_t p1, pt_t p2){
    return sqrt((p1.x-p2.x)*(p1.x-p2.x)+(p1.y-p2.y)*(p1.y-p2.y));
}

//点是否在圆中。0:没有 1:在
double question_point_and_circle(pt_t p, pt_t rp,double r){
    double dx=p.x-rp.x,dy=p.y-rp.y;
    if(dx*dx + dy*dy < r*r)    return 1;
    else                    return 0;
}

//交换
void swap_double(double *n, double *m){
    double t=*m;
    *m=*n;*n=t;
}

//通过3个点返回所成夹角
double get_sita(pt_t pt0,pt_t pt1,pt_t rpt){
    /* 计算向量 C→P 和 C→Q 所形成的角θ以及旋转方向*/
    Vector2_t c, p, q; /* 输入数据 */
    Vector2_t cp;      /* 向量 C→P */
    Vector2_t cq;      /* 向量 C→Q */
    double s;          /* 外积:(C→P) × (C→Q) */
    double t;          /* 内积:(C→P) ・ (C→Q) */
    double theta;      /* θ (弧度) */

    /*将 c,p,q 设定为所想的值。*/
    c.x = pt0.x;    c.y = pt0.y;
    p.x = pt1.x;    p.y = pt1.y;
    q.x = rpt.x;    q.y = rpt.y;

    /* 计算旋转方向以及角度θ。*/
    Vector2Diff(&cp, &p, &c);          /* cp ← p - c   */
    Vector2Diff(&cq, &q, &c);          /* cq ← q - c   */
    s = Vector2OuterProduct(&cp, &cq); /* s ← cp × cq */
    t = Vector2InnerProduct(&cp, &cq); /* t ← cp ・ cq */
    theta = atan2(s, t);
    return theta;
}

//判定长方形和圆是否相碰
int hitjudge_square_and_circle(pt_t pt[4], pt_t rpt, double r){
    int i;
    double a[4],b[4];//a:傾き b:y切片
    double x=rpt.x,y=rpt.y;

    /*判断圆中是否有长方形4个顶点中的某一个*/
    for(i=0;i<4;i++){
        if(question_point_and_circle(pt[i],rpt,r)==1)
            return 1;
    }
    /*到此为止*/

    /*判定长方形中是否有物体进入*/

    theta =get_sita(pt[0],pt[1],rpt);//3点所成的角1
    theta2=get_sita(pt[2],pt[3],rpt);//3点所成的角2

    if(0<=theta && theta<=PI/2 && 0<=theta2 && theta2<=PI/2)
        return 1;

    /*到此为止*/

    /*求线段和点的距离*/
    for(i=0;i<4;i++){
        if(get_distance(rpt.x,rpt.y,pt[i].x,pt[i].y,pt[(i+1)%4].x,pt[(i+1)%4].y)<r)
            return 1;
    }
    /*到此为止*/

    return 0;//如果哪里都没有碰撞的话就没有碰撞判定
}


int out_lazer(){
    int i,j;
    pt_t sqrp[4],rpt={ch.x,ch.y};//长方形四个顶点和圆的中心
    //激光数次循环
    for(i=0;i<LAZER_MAX;i++){
        //如果激光已经登录且设定了碰撞判定
        if(lazer[i].flag>0 && lazer[i].hantei!=0){
            for(j=0;j<4;j++){//设定激光的四个顶点
                sqrp[j].x=lazer[i].outpt[j].x;
                sqrp[j].y=lazer[i].outpt[j].y;
            }
            //长方形和圆的接触判定
            if(hitjudge_square_and_circle(sqrp,rpt,CRANGE))
                return 1;
        }
    }
    return 0;
}

另外,前面我们使用物理的运动来控制敌人,而激光也要漂亮地旋转,表现出在既定的坐标附近接连地缓慢旋转、随后突然停止的效果,因此我们也对此进行物理上的计算。

激光的旋转速度在这里我们称为角速度。

我们现在从力学的三大公式开始,逐步推导出激光的旋转速度吧。

这样就导出A了。由于能够写出在ty次计数类进行θmax度角旋转的方程,那么我们根据这个方程来计算角度的话就能够完成漂亮地加速了。 因此,我们进行以下追加以实现激光。

—- struct.h 追加 —-

//为激光的物理计算处理准备的结构体
typedef struct{
        int conv_flag;//是否旋转的flag
        double time,base_ang,angle;//旋转时间、基本角度、角度
        double conv_x,conv_y,conv_base_x,conv_base_y;//旋转前的坐标、旋转的基本坐标
}lphy_t;

//激光的结构体
typedef struct{
        int flag,cnt,knd,col,state;// flag、计数器、种类、颜色
        double haba,angle,length,hantei;// 幅度、角度、长度、判定范围(相对显示的幅度,指定0~1中的值)
        pt_t startpt,disppt[4],outpt[4];//发射激光的初始点、显示坐标、碰撞判定范围
        lphy_t lphy;
}lazer_t;

—-在 boss_shot.cpp 中追加红字部分 —-

void enter_boss(int num){
        if(num==0){//中路Boss开始的时候
                memset(enemy,0,sizeof(enemy_t)*ENEMY_MAX);//销毁杂兵敌人
                memset(shot,0,sizeof(shot_t)*SHOT_MAX);//销毁弹幕
                boss.x=FMX/2;//Boss的初始坐标
                boss.y=-30;
                boss.knd=-1;//弹幕的种类
        }
        boss.flag=1;
        boss.hagoromo=0;//是否扇状扩展的flag
        boss.endtime=99*60;//剩余时间
        boss.state=1;//变为待机中状态
        boss.cnt=0;
        boss.graph_flag=0;//还原绘制flag
        boss.knd++;
        boss.wtime=0;//初始化待机时间

        /***修改请注意***/
        memset(lazer,0,sizeof(lazer_t)*LAZER_MAX);//初始化激光信息
        /***修改请注意***/

        memset(&boss_shot,0,sizeof(boss_shot_t));//初始化Boss的弹幕信息
        input_phy(60);// 60次计数后在物理计算下回到固定位置
}

激光旋转了。不过是在离开旋转基准点的位置进行旋转的。

因此,我们设定

控制在哪里旋转的conv_x,conv_y 控制以哪个地方为基准 选装的conv_base_x,conv_base_y 设定激光从哪里发射的startpt.x,startpt.y

然后使用坐标的旋转以变换坐标。

将p旋转β角变为p’,我们可以用以下方法计算p’。

因此我们将这个进行坐标转换的函数

//坐标旋转
//将点(x0,y0)以(mx,my)为基准进行ang角旋转后的坐标传入(x,y)中。
void conv_pos0(double *x, double *y, double x0, double y0, double mx, double my,double ang){
        double ox=x0-mx,oy=y0-my;
        *x=ox*cos(ang) +oy*sin(ang);
        *y=-ox*sin(ang)+oy*cos(ang);
        *x+=mx;
        *y+=my;
}

像这样子进行实现。

激光的幅度用“haba”设定。(译者注:看了前面的代码的读者可能会注意到作者对有些变量的命名并不是用英文,而是用日语的罗马音标注的。) 激光的长度用“lenght”设定。 激光的碰撞判定范围用hantei设定。带入0~1的值,就可以确定是设定为显示出的总幅度的多大比例的范围了。

由于要把这些结果全部以运行结果的方式来展示很困难,因此我们将会在下一章中展示的反魂蝶的激光判定的绘制结果。

弹幕的话请在下一章中阅读。

谢辞: 这些理论参考了C言語何でも質問サイト(译者注:Dixq先生的网站)里大家的意见。非常感谢。

>>点此回到教程目录

results matching ""

    No results matching ""