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');
}
上記のコードではキャンバスの要素を画面に表示しています。
キャンバスの要素が'|'
だったら画面にはそのまま'|'
と表示します。
それ以外の場合は半角スペース(' '
)を描画します。
おわりに
解説は以上です。
なにか参考になれば幸いです。
🦝 < 雨よ降れ!
🦝 < この街を冷やしたまへ!