51 章

皆さん、お久しぶりです。

四聖龍神録Plusでは



こんな風に、絵や文字の弾幕が出現します。この作り方を教えて欲しいというリクエストが何件が寄せられたので、紹介してみようと思います。


では、今回は「漢字弾幕作成ツールを作ってみよう」ということで、龍神録のプログラムとは全く別の「支援ツール作り」をしようと思うので、

この章ではいつものプロジェクトは使いません。

毎回書くプログラムの骨格 https://dixq.net/g/#41 を使って一から作っていきます。もし自分でプロジェクトを作る人は新しいプロジェクトを作って下さい。

どんなツールになるのか、まずは目で見て下さい。なお、このツールは次の章で完成するので、この章での実行結果はアルファ版みたいな感じだと思って下さい。


このように、下に予め用意した自分の描きたい漢字の画像を敷き、その上になぞるように弾を置いていきます。

弾は、ペイントの「罫線」をひく時のように置いていきます。

一度クリックしたら、マウスのポインタに線が引っ張られ、次にクリックすると、そこまで直線的に弾を置いていきます。

2回同じところでクリックすればそこに1つだけ置け、直線的に置きたい時は罫線を引くようにすると置けるということです。

そして、随分こった漢字データを作っているとき、終わり頃になって「あ、間違った」となっても、消したり戻したり出来ないんじゃ泣けてしまいます。

そこで、後戻りできる仕様も実装してみましょう。

さて、随分難しそうに感じてる人もいるかもしれませんが、いたって簡単です。

実装する処理の流れを簡単に説明すると、


〜マウス系〜

  クリックしたかどうかを調べる。

  1回目のクリックだったら、そこの座標を覚える。

  2回目のクリックだったら、1回目クリックした所から直線的に2回目クリックした場所へ向かい、ある程度空白をあけながら弾座標を登録していく

〜描画系〜

  背景を描画する

  現在登録されている弾を描画する

  1回目クリックした状態なら、記憶した場所から現在のマウスポインタ座標まで線を引く



たったこれだけです。

では、実際にプログラム部分を見て行きましょう。

今回はマウスを使います。ですので、この辺の関数を使いましょう。

マウスがどれ位押されているか監視する関数

この関数は内部処理を知らなくても使えるので、特に説明は書きません。

それに、今回のプログラムと一緒に説明するようなものでもないので、ファイルをわけてしまいましょう。


--- Key_Mouse.h ---

typedef struct{
    int x;
    int y;          //座標
    unsigned int Button[8];  //ボタンの押した状態
    int WheelRotVol;//ホイールの回転量
}Mouse_t;

int GetHitKeyStateAll_2(int GetHitKeyStateAll_InputKey[]){
    char GetHitKeyStateAll_Key[256];
    GetHitKeyStateAll( GetHitKeyStateAll_Key );
    for(int i=0;i<256;i++){
        if(GetHitKeyStateAll_Key[i]==1) GetHitKeyStateAll_InputKey[i]++;
        else                            GetHitKeyStateAll_InputKey[i]=0;
    }
    return 0;
}

int GetHitMouseStateAll_2(Mouse_t *Nezumi){
    if(GetMousePoint( &Nezumi->x, &Nezumi->y ) == -1){ //マウスの位置取得
        return -1;
    }
    int MouseInput=GetMouseInput();    //マウスの押した状態取得
    for(int i=0; i<8; i++){            //マウスのキーは最大8個まで確認出来る
        if( (MouseInput & 1<<i ) != 0 ) Nezumi->Button[i]++;   //押されていたらカウントアップ
        else                            Nezumi->Button[i] = 0; //押されてなかったら0
    }
    Nezumi->WheelRotVol = GetMouseWheelRotVol() ;    //ホイール回転量取得
    return 0;
}


はい、このファイル名で保存しておいて下さい。保存する場所はどこでもいいです。

このプログラムを全く変えずに使いたいなら、ダウロードしたプロジェクトを参考にして下さい。「mydat/source」の中にあります。

次に用意する構造体の話をします。


//float型の座標構造体
typedef struct{
    float x,y;
}fPt_t;

//弾1つ分の情報
typedef struct{
    int Knd;//弾の種類
    int Col;//弾の色
    float Angle;//弾の角度
    float x,y;
}Bl_t;

//弾全体の情報
typedef struct{
    int Num;//登録した個数
    Bl_t Bl[ PMAX ];    //登録する弾情報
}BlPoint_t;

//操作設定系情報
typedef struct{
    int State;//状態
    int Knd;//種類
    int Col;//色
    int Space;//スペース
    float Angle;//角度
    int flag;//表示・非表示フラグ
    fPt_t fPt1;//1回目クリックの場所
    fPt_t fPt2;//2回目クリックの場所
}Operate_t;


今回このように4種類の構造体を用意しました。一番上はいいですね。全部注釈の通りなんですが、

2番目は弾一つのデータを格納する為の構造体、3番目はその弾を全部セットにした構造体、一番下は操作の設定に関する構造体です。

特に難しくはないですね。弾には種類、色、角度、そして座標があるのでBl_tのようになっています。BlはBulletから取りました。

BlPoint_tでは、漢字を構成する弾の数をPMAXとしたら、PMAX個セットで記録する必要があるので、こんな構造体にしています。

Numには登録した弾の個数が入ります。

設定についても見ての通りです。どんな弾を置くかという設定等についてですね。

では、弾を登録する時の処理を見てみましょう。

2回目クリックされた時、1回目クリックされた座標(以後「1回目の座標」という)のところから直線的にある程度の間隔で弾を置いていきますね。

具体的には、1回目の座標と2回目の座標からatan2で角度を取り、シューティングの移動と同じように、

1回目の座標を初期地点とし、計算した角度で2回目の座標の方向に擬似的に飛んで行けばいいのです。

スピードは設定項目で設定した間隔である「.Space」です。これで計算した地点ごとにおいていけば弾が置けますね。

最初に1回目の座標と2回目の座標との距離を計っておき、先ほどの手順で繰り返し登録し、移動距離がこれを超えると登録をやめればいいのです。

それを計算しているのが以下の3つの関数です。


//弾を登録
void InputBlData(float x, float y, int Knd, int Col, float Angle){
    BlPoint.Bl[BlPoint.Num].x = x;//今の場所を記録
    BlPoint.Bl[BlPoint.Num].y = y;
    BlPoint.Bl[BlPoint.Num].Knd = Knd;//弾の種類
    BlPoint.Bl[BlPoint.Num].Col = Col;//色
    BlPoint.Bl[BlPoint.Num].Angle = Angle;//角度
    BlPoint.Num++;//今何個目かをカウントアップ
}

//弾をおいていくときの計算
void CalcBullet(){
    float x = Operate.fPt1.x, y = Operate.fPt1.y;//最初の場所
    //最初クリックした場所と最後クリックした場所との角度
    float Angle = atan2( Operate.fPt2.y - Operate.fPt1.y, Operate.fPt2.x - Operate.fPt1.x );
    float xlen = Operate.fPt2.x - Operate.fPt1.x;//xの距離
    float ylen = Operate.fPt2.y - Operate.fPt1.y;//yの距離
    float Length = sqrt( xlen * xlen + ylen * ylen );//点と点との距離
    float Proceeded = 0;//今現在進んだ距離

    //今現在進んだ距離が進むべき距離以内の間、かつ登録出来る個数の間ループ
    while( BlPoint.Num < PMAX ){
        InputBlData(x,y,Operate.Knd,Operate.Col,Operate.Angle);
        x += cos( Angle ) * Operate.Space;//進むべき方向へ進む
        y += sin( Angle ) * Operate.Space;
        Proceeded += Operate.Space;//進んだ距離を加算
        if(Length < Proceeded) break;
    }
}

//マウス部の計算
void CalcMouse(){
    if( Mouse.Button[0]==1 ){//左クリックされたら
        switch( Operate.State ){
            case 0://1回目押した時
                //その時の位置を記録
                Operate.fPt1.x = (float)Mouse.x;
                Operate.fPt1.y = (float)Mouse.y;
                Operate.State = 1;
                break;
            case 1://2回目押した時
                //その時の位置を記録
                Operate.fPt2.x = (float)Mouse.x;
                Operate.fPt2.y = (float)Mouse.y;
                Operate.State = 0;
                //1回目押したところから2回目おしたところまでに弾を登録
                CalcBullet();
                break;
        }
    }
}


描画部も全く難しくありません。

背景を描画して、弾を登録した個数分描画して、クリックした状態なら、記録した座標から現在のマウスポインタの位置まで線を描けばいいのです。

その内容がこちらです。


//データを見せる
void Show(){
    int i;
    //背景を描画
    DrawGraph(0,0,ImgBack,FALSE);
    //弾を描画
    for(i=0; i<BlPoint.Num; i++){
        DrawCircle( (int)BlPoint.Bl[ i ].x, (int)BlPoint.Bl[ i ].y, 3, Red, TRUE );
    }
    //ピーと引っ張る線を描画
    if( Operate.State==1 ){
        DrawLine( (int)Operate.fPt1.x, (int)Operate.fPt1.y, Mouse.x, Mouse.y, Red );
    }
    //マウスポインタ部に弾を表示する
    DrawCircle( Mouse.x, Mouse.y, 3, Red, TRUE );
    //弾の上に、今設定されているスペースがどれ位か表示する
    DrawLine(Mouse.x,Mouse.y,Mouse.x+Operate.Space,Mouse.y,Blue);
}


後はメイン関数です。


int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow ){
    ChangeWindowMode(TRUE);//ウィンドウモード
    SetGraphMode(640,640,32);//画面サイズ変更
    if(DxLib_Init() == -1 || SetDrawScreen( DX_SCREEN_BACK )!=0) return -1;//初期化と裏画面化
    SetMouseDispFlag( TRUE ) ;//マウス表示オン
    ini();//初期化
    load();//ロード
    while(ProcessMessage()==0 && ClearDrawScreen()==0 && GetHitKeyStateAll_2(Key)==0 && Key[KEY_INPUT_ESCAPE]==0){
          //↑メッセージ処理          ↑画面をクリア           ↑入力状態を保存       ↑ESCが押されていない
        GetHitMouseStateAll_2(&Mouse);
        CalcMouse();//マウス部の計算
        CalcOperate();//操作系の計算
        Show();//見せる
        ScreenFlip();
    }
    DxLib_End();
    return 0;
}



データのロード部などは省きましたが、これで全部です。では最後に全体をキレイに見渡してみましょう。




--- main.cpp ---

#include "../../../include/DxLib.h"
#include "math.h"
#include "Key_Mouse.h"

#define PI2 (3.141562f*2)    //円周率*2

#define PMAX 1000    //登録する弾の最大数

//float型の座標構造体
typedef struct{
    float x,y;
}fPt_t;

//弾1つ分の情報
typedef struct{
    int Knd;//弾の種類
    int Col;//弾の色
    float Angle;//弾の角度
    float x,y;
}Bl_t;

//弾全体の情報
typedef struct{
    int Num;//登録した個数
    Bl_t Bl[ PMAX ];    //登録する弾情報
}BlPoint_t;
BlPoint_t BlPoint;

//操作設定系情報
typedef struct{
    int State;
    int Knd;
    int Col;
    int Space;
    float Angle;
    int flag;
    fPt_t fPt1;
    fPt_t fPt2;
}Operate_t;

int Key[256];//キー
int Red,White,Blue;//色
int ImgBullet[14][10],ImgBack;//弾の画像と背景画像
Mouse_t Mouse;//マウス
Operate_t Operate;//操作設定

//初期化
void ini(){
    Operate.Knd=7;//弾の初期種類を7に
    Operate.Space=20;//スペースは最初20
    Operate.flag=1;
}

//ロード
void load(){
    White=GetColor(255,255,255);
    Red = GetColor(255,0,0);
    Blue = GetColor(0,255,255);
    ImgBack = LoadGraph("mydat/img/ryu.png");
}

//弾を登録
void InputBlData(float x, float y, int Knd, int Col, float Angle){
    BlPoint.Bl[BlPoint.Num].x = x;//今の場所を記録
    BlPoint.Bl[BlPoint.Num].y = y;
    BlPoint.Bl[BlPoint.Num].Knd = Knd;//弾の種類
    BlPoint.Bl[BlPoint.Num].Col = Col;//色
    BlPoint.Bl[BlPoint.Num].Angle = Angle;//角度
    BlPoint.Num++;//今何個目かをカウントアップ
}

//弾をおいていくときの計算
void CalcBullet(){
    float x = Operate.fPt1.x, y = Operate.fPt1.y;//最初の場所
    //最初クリックした場所と最後クリックした場所との角度
    float Angle = atan2( Operate.fPt2.y - Operate.fPt1.y, Operate.fPt2.x - Operate.fPt1.x );
    float xlen = Operate.fPt2.x - Operate.fPt1.x;//xの距離
    float ylen = Operate.fPt2.y - Operate.fPt1.y;//yの距離
    float Length = sqrt( xlen * xlen + ylen * ylen );//点と点との距離
    float Proceeded = 0;//今現在進んだ距離

    //今現在進んだ距離が進むべき距離以内の間、かつ登録出来る個数の間ループ
    while( BlPoint.Num < PMAX ){
        InputBlData(x,y,Operate.Knd,Operate.Col,Operate.Angle);
        x += cos( Angle ) * Operate.Space;//進むべき方向へ進む
        y += sin( Angle ) * Operate.Space;
        Proceeded += Operate.Space;//進んだ距離を加算
        if(Length < Proceeded) break;
    }
}

//マウス部の計算
void CalcMouse(){
    if( Mouse.Button[0]==1 ){//左クリックされたら
        switch( Operate.State ){
            case 0://1回目押した時
                //その時の位置を記録
                Operate.fPt1.x = (float)Mouse.x;
                Operate.fPt1.y = (float)Mouse.y;
                Operate.State = 1;
                break;
            case 1://2回目押した時
                //その時の位置を記録
                Operate.fPt2.x = (float)Mouse.x;
                Operate.fPt2.y = (float)Mouse.y;
                Operate.State = 0;
                //1回目押したところから2回目おしたところまでに弾を登録
                CalcBullet();
                break;
        }
    }
}

void CalcOperate(){
}

//データを見せる
void Show(){
    int i;
    //背景を描画
    DrawGraph(0,0,ImgBack,FALSE);
    //弾を描画
    for(i=0; i<BlPoint.Num; i++){
        DrawCircle( (int)BlPoint.Bl[ i ].x, (int)BlPoint.Bl[ i ].y, 3, Red, TRUE );
    }
    //ピーと引っ張る線を描画
    if( Operate.State==1 ){
        DrawLine( (int)Operate.fPt1.x, (int)Operate.fPt1.y, Mouse.x, Mouse.y, Red );
    }
    //マウスポインタ部に弾を表示する
    DrawCircle( Mouse.x, Mouse.y, 3, Red, TRUE );
    //弾の上に、今設定されているスペースがどれ位か表示する
    DrawLine(Mouse.x,Mouse.y,Mouse.x+Operate.Space,Mouse.y,Blue);
}

int WINAPI WinMain( HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nCmdShow ){
    ChangeWindowMode(TRUE);//ウィンドウモード
    SetGraphMode(640,640,32);//画面サイズ変更
    if(DxLib_Init() == -1 || SetDrawScreen( DX_SCREEN_BACK )!=0) return -1;//初期化と裏画面化
    SetMouseDispFlag( TRUE ) ;//マウス表示オン
    ini();//初期化
    load();//ロード
    while(ProcessMessage()==0 && ClearDrawScreen()==0 && GetHitKeyStateAll_2(Key)==0 && Key[KEY_INPUT_ESCAPE]==0){
          //↑メッセージ処理          ↑画面をクリア           ↑入力状態を保存       ↑ESCが押されていない
        GetHitMouseStateAll_2(&Mouse);
        CalcMouse();//マウス部の計算
        CalcOperate();//操作系の計算
        Show();//見せる
        ScreenFlip();
    }
    DxLib_End();
    return 0;
}



- Remical Soft -