ユーニックス総合研究所

  • home
  • archives
  • c-rain

C言語で雨を降らせるプログラムを作る

  • 作成日: 2023-07-29
  • 更新日: 2023-12-24
  • カテゴリ: C言語

C言語で雨を降らせるプログラムを作る

毎日暑いですね。
こう暑いと祈禱でもして雨なんか降らせたくなりますよね。
冷たい雨で街のアスファルトと建物を冷やして、涼めると嬉しいです。
しかし現実はカンカン晴れ。なんで!?

悲しいので今回はC言語で雨を降らせるプログラムを作りました。
そのプログラムの解説をしていきたいと思います。

関連記事
C言語でcharをintに変換する方法
C言語でenumをtypedefして使う【列挙型】
C言語でforeachマクロを実装する方法
C言語でnull判定する方法【NULL, 比較】
C言語でできることを解説!C言語歴16年の開発者が語る
C言語でオブジェクト指向する【単一継承の方法】
C言語でグローバルに関数を使う方法
C言語でシャローコピーとディープコピーを実装する

プログラム実行風景

今回作ったプログラムを実行すると以下のような結果になります。

ソースコード全文

ソースコード全文は以下になります。

#include <stdio.h>  
#include <stdlib.h>  
#include <string.h>  
#include <unistd.h>  
#include <time.h>  

typedef struct {  
    int x, y;  
    int len;  
    int amount_y;  
} Rain;  

enum {  
    NRAINS = 100,  
    CANVAS_WIDTH = 80,  
    CANVAS_HEIGHT = 40,  
    CANVAS_SIZE = CANVAS_WIDTH * CANVAS_HEIGHT,  
};  

Rain rains[NRAINS];  
int canvas[CANVAS_SIZE];  

void init(void) {  
    srand(time(NULL));  

    for (size_t i = 0; i < NRAINS; i++) {  
        Rain *rain = &rains[i];  
        rain->x = rand() % CANVAS_WIDTH;  
        rain->y = rand() % CANVAS_HEIGHT;  
        rain->len = 1 + rand() % 5;  
        rain->amount_y = 1 + rand() % 3;  
    }  
}  

void update(void) {  
    memset(canvas, 0, sizeof canvas);  

    for (size_t i = 0; i < NRAINS; i++) {  
        Rain *rain = &rains[i];  

        if (rain->y >= CANVAS_HEIGHT) {  
            rain->x = rand() % CANVAS_WIDTH;  
            rain->y = 0;  
        }  

        for (size_t j = 0; j < rain->len; j++) {  
            size_t n = (rain->y + j) * CANVAS_WIDTH + rain->x;  
            if (n >= CANVAS_SIZE) {  
                continue;  
            }  
            canvas[n] = '|';  
        }  
        rain->y += rain->amount_y;  
    }  
}  

void draw(void) {  
    system("clear");  // Windowsはsystem("cls")  

    for (size_t y = 0; y < CANVAS_HEIGHT; y++) {  
        for (size_t x = 0; x < CANVAS_WIDTH; x++) {  
            size_t i = y * CANVAS_WIDTH + x;  
            int c = canvas[i];  
            switch (c) {  
            case '|': putchar('|'); break;  
            default: putchar(' '); break;  
            }  
        }  
        putchar('\n');  
    }  
}  

void run(void) {  
    for (;;) {  
        update();  
        draw();  
        usleep(1000 * 33);  
    }  
}  

int main(void) {  
    init();  
    run();  
    return 0;  
}  

プログラム全体の構成

プログラムとしては2次元行列のキャンバスを作り、そこに雨をセットして描画する、という感じです。
雨粒を表現する構造体を作り、その構造体の配列を作って要素をランダムに初期化します。
そしてゲームループで雨粒の状態を更新して雨粒を上から下に移動して雨のように見せます。
基本的にはこの考え方で実装します。

定数

定数は以下になります。

enum {  
    NRAINS = 100,  
    CANVAS_WIDTH = 80,  
    CANVAS_HEIGHT = 40,  
    CANVAS_SIZE = CANVAS_WIDTH * CANVAS_HEIGHT,  
};  

定数については以下のような役割があります。

  • NRAINS ... 雨粒の個数
  • CANVAS_WIDTH ... キャンバスの横幅
  • CANVAS_HEIGHT ... キャンバスの高さ
  • CANVAS_SIZE ... キャンバスの要素数

構造体

構造体は以下になります。

typedef struct {  
    int x, y;  
    int len;  
    int amount_y;  
} Rain;  

Rainは雨粒を表す構造体えす。
x, yは雨粒のキャンバス上の座標です。
lenは雨粒の縦の長さです。
amount_yは雨粒の縦方向の移動量です。

グローバル変数

グローバル変数は以下になります。

Rain rains[NRAINS];  
int canvas[CANVAS_SIZE];  

rainsは雨粒です。
canvasはキャンバスです。

main関数

まずmain関数から見ていきます。
main関数では2つの関数を呼び出しています。
init関数とrun関数です。

int main(void) {  
    init();  
    run();  
    return 0;  
}  

init関数はプログラムを初期化する関数です。
run関数はゲームループを回します。

次にinit関数を見ていきます。

init関数

init関数は以下になります。

void init(void) {  
    srand(time(NULL));  

    for (size_t i = 0; i < NRAINS; i++) {  
        Rain *rain = &rains[i];  
        rain->x = rand() % CANVAS_WIDTH;  
        rain->y = rand() % CANVAS_HEIGHT;  
        rain->len = 1 + rand() % 5;  
        rain->amount_y = 1 + rand() % 3;  
    }  
}  

この関数ではまずsrand()で乱数のシードを初期化します。
time(NULL)の返り値をシード生成の種としています。
こうするとrand()関数で毎回違う乱数が生成されます。

for文で雨粒を初期化します。
rain->x = rand() % CANVAS_WIDTH;では雨粒のx座標を初期化しています。
rand()の値をCANVAS_WIDTHで剰余算すると0からCANVAS_WIDTHより下の値が手に入ります。
yについても同様です。

rain->len = 1 + rand() % 5;では雨粒の縦の長さを設定しています。
1以上、5以下の値が設定されます。
amount_yについても同様です。

run関数

run関数は以下になります。

void run(void) {  
    for (;;) {  
        update();  
        draw();  
        usleep(1000 * 33);  
    }  
}  

run関数では無限ループでupdate関数とdraw関数を呼び出しています。
usleep()で1フレームごとに33ミリ秒の休止を入れています。
これがあるとアニメーションしているようになります。

update関数

void update(void) {  
    memset(canvas, 0, sizeof canvas);  

    for (size_t i = 0; i < NRAINS; i++) {  
        Rain *rain = &rains[i];  

        if (rain->y >= CANVAS_HEIGHT) {  
            rain->x = rand() % CANVAS_WIDTH;  
            rain->y = 0;  
        }  

        for (size_t j = 0; j < rain->len; j++) {  
            size_t n = (rain->y + j) * CANVAS_WIDTH + rain->x;  
            canvas[n] = '|';  
        }  
        rain->y += rain->amount_y;  
    }  
}  

update関数ではおもにcanvasの値を更新しています。
まずmemset()canvasの値をぜんぶ0に初期化します。

それから雨粒の数だけループを回します。
雨粒のy座標がCANVAS_HEIGHT以上になったら座標を初期化します。

        if (rain->y >= CANVAS_HEIGHT) {  
            rain->x = rand() % CANVAS_WIDTH;  
            rain->y = 0;  
        }  

そしてrain->lenの数だけループを回してcanvasに雨粒である|文字を入れます。

        for (size_t j = 0; j < rain->len; j++) {  
            size_t n = (rain->y + j) * CANVAS_WIDTH + rain->x;  
            if (n >= CANVAS_SIZE) {  
                continue;  
            }  
            canvas[n] = '|';  
        }  

キャンバスのインデックス(添え字)は以下の式で生成します。

    size_t n = (rain->y + j) * CANVAS_WIDTH + rain->x;  

nがインデックスです。
雨粒のY座標 * キャンバスの横幅 + 雨粒のX座標という式でインデックスを生成できます。
canvasは1次元配列で、座標はx, yの2次元ですが、この座標の変換をこの式でやってます。

            if (n >= CANVAS_SIZE) {  
                continue;  
            }  

上記のコードではインデックスがCANVAS_SIZE以上になったらループをスキップしています。
こうすることでcanvasの範囲外の添え字アクセスを防止しています。

    rain->y += rain->amount_y;  

上記のコードでは雨粒のy座標を更新しています。
y座標にamount_yを加算するだけです。

draw関数

draw関数は以下になります。

void draw(void) {  
    system("clear");  // Windowsはsystem("cls")  

    for (size_t y = 0; y < CANVAS_HEIGHT; y++) {  
        for (size_t x = 0; x < CANVAS_WIDTH; x++) {  
            size_t i = y * CANVAS_WIDTH + x;  
            int c = canvas[i];  
            switch (c) {  
            case '|': putchar('|'); break;  
            default: putchar(' '); break;  
            }  
        }  
        putchar('\n');  
    }  
}  

この関数ではまずsystem("clear")で端末の画面をクリアしています。
system関数は引数の文字列のコマンドを呼び出す関数です。
clearコマンドはLinux系では端末画面のクリアに使われます。
Windowsの場合はclsコマンドを使います。

    for (size_t y = 0; y < CANVAS_HEIGHT; y++) {  
        for (size_t x = 0; x < CANVAS_WIDTH; x++) {  
            size_t i = y * CANVAS_WIDTH + x;  
            int c = canvas[i];  
            switch (c) {  
            case '|': putchar('|'); break;  
            default: putchar(' '); break;  
            }  
        }  
        putchar('\n');  
    }  

上記のコードではキャンバスの要素を画面に表示しています。
キャンバスの要素が'|'だったら画面にはそのまま'|'と表示します。
それ以外の場合は半角スペース(' ')を描画します。

おわりに

解説は以上です。
なにか参考になれば幸いです。

🦝 < 雨よ降れ!

🦝 < この街を冷やしたまへ!