1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita Tech Festa 2026 とやらで、短期間で大量の記事の投稿を要求されることとなった。
(といっても、アドベントカレンダーよりも期間が長くて記事の要求数が少ないイージーモードである。この程度でキツいと主張するのは甘え)

今回の自分は、「回が回る回」シリーズである程度記事数を稼いできた。
しかし、このシリーズもそろそろ限界かもしれない。

そういえば、記事数を稼げそうな題材として、「Hello World あたたたた」なんていうのもあった。
この題材は、年明けにも既に記事数を稼ぐのに使ったが、まだ扱っていない言語が残っており、稼ぎの余地が残っている。

Hello World あたたたた

「Hello World あたたたた」は、以下の記事で定義されている。
Hello World あたたたた 1日目 概要編 #HelloWorld - Qiita

この記事で、以下の要件が定義されている。

・「あ」か「た」をランダムに出力する
・「あたたたた」が出たら「お前はもう死んでいる」を出力して終了する

そして、以下の処理フローが推奨されている。

変数 hako に空文字をセット
変数 running に true をセット
running が true なら繰り返す
    0か1をランダムに決める
    もし0なら
        変数 char に「あ」をセット
    その他なら
        変数 char に「た」をセット
    画面に char を出力
    char を hako に追加
    もし 最後の 5 文字が「あたたたた」なら
        "お前はもう死んでいる" を出力
        running に false をセット        

東方弾幕風v0.12m

概要

「東方弾幕風v0.12m」は、スクリプトを書くことによって弾幕シューティングゲームの弾幕を作成することを目的とした Windows 用ソフトである。

以下のサイトで、ソフトをダウンロードしたり、ヘルプ (スクリプトのリファレンス) を見たりできる。

東方弾幕風v0.12m

このサイトでは、広告が表示されることがある。
広告の有無・内容・安全性などについて、本記事の筆者は一切関知しない。

スクリプトの書き方

ここでは、東方弾幕風が対応しているスクリプトのうち、今回用いる敵 (弾幕を撃ってくるボス) を表すスクリプトの書き方を紹介する。

スクリプトの骨格

東方弾幕風の (敵を表す) スクリプトは、以下のような構造を取る。

冒頭で、# から始まる行を用いて、このスクリプトのメタデータを記述する。
そして、script_enemy_main 内で、敵の動作を記述する。

スクリプトは、script ディレクトリ内に、文字コード Shift_JIS で保存する。

#東方弾幕風       // これが東方弾幕風の (単発再生用の) スクリプトであることを表す
#Title[選択画面の選択肢として表示するタイトル]
#Player[FREE]     // 用意された全ての自機を使用可能であることを表す
#ScriptVersion[2] // スクリプトのバージョンを表す

script_enemy_main {
	// ここで各処理・各フレームで通して用いる変数を宣言する

	@Initialize {
		// 最初 (敵の出現時) に実行する処理
	}

	@MainLoop {
		// 各フレームで実行する計算処理
	}

	@DrawLoop {
		// 各フレームで実行する描画処理
	}

	@Finalize {
		// 最後 (敵の消滅時) に実行する処理
	}
}

ヘルプの「必須部分」を見ると、ここで紹介した以外のメタデータや動作の記述方法も紹介されている。
なお、同項目では「最低限必要な部分」と主張しているが、これは嘘で、ここで紹介したようにその一部を記述しなくてもエラーにならずに認識された。

ヘルプの「弾幕をつくる人へ」に

スクリプトの「@DrawLoop」部分にrand関数を用いている場合は正常に再生されなくなります。 これは、フレームスキップを行った場合に乱数がずれるからです。

と書かれていることから、@MainLoop は毎フレーム確実に実行され、@DrawLoop はフレームスキップ時実行されない、と推測できる。

変数と式

let foo;

のように、let を用いることで変数を宣言できる。
変数には、それを宣言したブロック、およびその内側のブロックからのみアクセスできる。
宣言していない変数 (未定義の識別子) を使用すると、エラーになる。

let hoge = 72;

のように、宣言と同時に値を代入してもよい。

これはエラーになる
let a, b;

のように、複数の変数をまとめて宣言することはできない。

計算では、C言語に似た以下の演算子が使用できる。

  • 代入 =
  • 四則演算 +-*/、剰余 %
  • 比較 ==!=<<=>>=
  • 論理演算 !&&|| (短絡評価あり)
  • 後置インクリメント ++、後置デクリメント --
  • 複合代入 +=-=*=/=%=

また、演算子 ^ は累乗を表す。複合代入 ^= も使用できる。

前置インクリメント・前置デクリメントは使用できない。
また、代入・複合代入・インクリメント・デクリメントの結果は値として使用できない。

これらはエラーになる
++a;
b = (a = 72);
b = a++;
b = (a += 5);

配列と文字列

[] で囲んで要素を並べることで、配列を作成できる。
配列の要素は、配列[添字] で参照でき、最初の要素は 配列[0] である。

let arr = [1, 2, 3];
let elem = arr[1]; // elem = 2

文字列は、文字を要素とする配列である。

let hoge1 = "hoge";
let hoge2 = ['h', 'o', 'g', 'e'];
// hoge1 == hoge2 が true となる

~ 演算子で、配列を結合できる。

let str = "hello, ";
str = str ~ "world"; // str = "hello, world"

複合代入 ~=使用できない。

これはエラーになる
let str = "hello, ";
str ~= "world";

length(配列) で、配列の要素数を取得できる。

let len = length("hoge"); // len = 4

配列[始点添字..終点添字+1] で、配列の一部を抜き出した新しい配列を作成できる。

let str = "concatenate";
let str2 = str[3..6]; // str2 = "cat"

指定する範囲が配列の前や後ろにはみ出したり、終点が始点より前だったりすると、エラーになる。

これらはエラーになる
let str = "hoge";
let str2 = str[3..5];  // エラー
let str3 = str[-1..2]; // エラー
let str4 = str[2..1];  // エラー
let str5 = str[2..2];  // これはOK (空の配列になる)

「要素」の取り出しと、「1要素の配列」の取り出しの混同に注意しよう。

let str = "hoge";
let v1 = str[1];    // 文字   'o'
let v2 = str[1..2]; // 文字列 "o"
//let str2 = str ~ v1; // エラー:「文字」を結合しようとしている
let str3 = str ~ [v1]; // OK:「文字」を配列に入れて「文字列」にしてから結合
let str4 = str ~ v2;   // OK

マルチバイト文字でも、Shift_JIS の各バイトを「文字」として扱う。
以下の例では、「三」(8E4F) の1バイト目と「色」(9046) の2バイト目を結合して「薩」(8E46) を作っている。

let str = "三色";
let len = length(str); // len = 4
let str2 = str[0..1] ~ str[3..4]; // str2 = "薩"

この件について、公式のヘルプには嘘が書かれている。

変数宣言」には

'a', 'b', 'あ' …char型(内部的にwchar_tで格納するため全角文字もOKです)

と書かれているが、実際は

これはエラーになる
let c = '';

と書くと「文字型の値の長さは1だけです」というエラーが出てしまう。

さらに、「配列」には

let str = "あいうえお"[2];//strは'う'になります。

と書かれているが、実際の str には「い」の1バイト目の '\x82' が格納される。

関数

東方弾幕風では、様々な関数が用意されている。
ここでは、今回使用したもののみを紹介する。

敵の状態を管理する関数

リファレンスでは、「敵を制御する関数」として紹介されている。

SetLife(life)

敵のライフの初期値 (兼、最大値) を数値 life に設定する。

この関数は、@Initialize でちょうど1回呼び出すべきである。
この関数を呼び出さない場合、敵は出現後即消滅してしまう。

また、life変数を使用してはいけない。

AddLife(delta)

敵のライフに delta を足す。
ライフが 0 以下になると、敵は消滅する。
また、ライフは SetLife で設定した初期値を超えない。

情報を取得する関数

リファレンスでは、「特定の値を取得する関数」として紹介されている。

GetClipMinX()

フィールドの左端の x 座標を返す。

GetClipMaxX()

フィールドの右端の x 座標を返す。

GetClipMinY()

フィールドの上端の y 座標を返す。

GetClipMaxY()

フィールドの下端の y 座標を返す。

GetCenterX()

フィールドの中心の x 座標を返す。

GetCenterY()

フィールドの中心の y 座標を返す。

rand_int(min, max)

min 以上 max 以下の整数の乱数を返す。

リファレンスでは、rand_int の説明の中に

min>maxならrand(max,min)が返ります。

と書かれており、rand は (整数とは限らない) 実数を返す別の関数である。
しかし、実際に試してみると、数回試して整数しか返ってこなかった。
従って、これは嘘である疑いが強い。

length(array)

配列 array の要素数を返す。

東方弾幕風では文字列も配列の一種なので、文字列の文字数もこれで取得できる。

描画を行う関数

リファレンスでは、「描画系関数」として紹介されている。

DrawText(text, x, y, size, alpha)

座標 (x, y) に、大きさ size、不透明度 alpha で文字列 text を描画する。

CreateRenderTarget(name, width, height)

名前 name、最低横幅 width、最低縦幅 height のテクスチャを作成する。
実際の横幅と縦幅は、2の累乗になる。
このテクスチャは、描画先として使用できる。

DeleteGraphic(name)

名前 name のテクスチャを破棄する。
処理の流れにもよるかもしれないが、たとえば終了時、すなわち @Finalize 内で用いる。

SetRenderTarget(name)

描画先を名前 name のものに設定する。
以前設定されていた描画先の名前が返るので、テクスチャへの描画が完了したら、これを用いてもとの描画先に戻しておくのがよい。

SetTexture(name)

DrawGraphic で描画する (すなわち、描画元の) テクスチャを名前 name のテクスチャに設定する。

SetGraphicRect(minx, miny, maxx, maxy)

DrawGraphic で描画するのを、描画元の (minx, miny) を左上、(maxx, maxy) を右下とする矩形に設定する。

この設定を行わないと、描画結果はでたらめで毎回異なるものになった。
描画元の全体を描画する場合でも、設定は必要なようである。

↓でたらめな描画結果の例
SetGraphicRect を呼び出していないときの描画結果の例

DrawGraphic(x, y)

SetTextureSetGraphicRect での設定に基づいて、描画元テクスチャの指定した部分を (x, y) を中心として描画する。

東方弾幕風v0.12m で Hello World あたたたた

以下のスクリプトで、「Hello World あたたたた」を行うことができた。

hwa.txt
#東方弾幕風
#Title[Hello World あたたたた]
#Player[FREE]
#ScriptVersion[2]

script_enemy_main {
	let charSize;
	let target;
	let charX;
	let charY;
	let hako;
	let running;

	@Initialize {
		charSize = 16;
		target = "あたたたた";
		SetLife(5000);
		CreateRenderTarget("画面", GetClipMaxX(), GetClipMaxY());
		SetTexture("画面");
		SetGraphicRect(GetClipMinX(), GetClipMinY(), GetClipMaxX(), GetClipMaxY());
		charX = GetClipMinX();
		charY = GetClipMinY() + 32;
		hako = "";
		running = true;
	}

	@MainLoop {
		if (running) {
			// 0 か 1 をランダムに決める
			let r = rand_int(0, 1);
			// それに応じて変数 char の値をセット
			let char;
			if (r == 0) {
				char = "";
			} else {
				char = "";
			}
			// 画面に char を出力
			if (charX + charSize > GetClipMaxX()) {
				charX = GetClipMinX();
				charY += charSize;
			}
			let oldRenderTarget = SetRenderTarget("画面");
			DrawText(char, charX, charY, charSize, 255);
			charX += charSize;
			// char を hako に追加
			hako = hako ~ char;
			// 最後の 5 文字が「あたたたた」か判定
			if (
				length(hako) >= length(target) &&
				hako[length(hako)-length(target)..length(hako)] == target
			) {
				// "お前はもう死んでいる" を出力
				charX = GetClipMinX();
				charY += charSize;
				DrawText("お前はもう死んでいる", charX, charY, charSize, 255);
				// running に false をセット
				running = false;
			}
			SetRenderTarget(oldRenderTarget);
		} else {
			// running が false なら終了する
			AddLife(-5000);
		}
	}

	@DrawLoop {
		DrawGraphic(GetCenterX(), GetCenterY());
	}

	@Finalize {
		DeleteGraphic("画面");
	}
}

実行結果例

「Hello World あたたたた」の結果を表示できている。
なお、東方弾幕風のリプレイ機能を用い、一度得られた結果を再現することも可能である。

実行結果例 1
実行結果例 2

解説

共通変数の宣言

	let charSize;
	let target;
	let charX;
	let charY;
	let hako;
	let running;

各フレームを通して状態を保持する変数として、以下を宣言する。

変数 役割
charSize 描画する文字の大きさ
target できたら終了する文字列
charX 次の文字を描画する x 座標
charY 次の文字を描画する y 座標
hako これまで出た文字の履歴 (「Hello World あたたたた」で指定)
running 文字を追加中か (「Hello World あたたたた」で指定)

初期化 (@Initialize)

		charSize = 16;
		target = "あたたたた";

文字の大きさと終了条件を設定する。(定数の役割)

        SetLife(5000);

ライフの初期値を設定し、即消滅するのを防ぐ。

		CreateRenderTarget("画面", GetClipMaxX(), GetClipMaxY());
		SetTexture("画面");
		SetGraphicRect(GetClipMinX(), GetClipMinY(), GetClipMaxX(), GetClipMaxY());

ゲーム画面全体を覆う大きさのテクスチャを作成し、「画面」という名前をつける。
このテクスチャのゲーム画面に対応する部分全体をゲーム画面に描画するための設定を行う。

このテクスチャに「画面」という名前をつけることで、Hello World あたたたた の「画面に char を出力」という条件を満たしやすくした。

		charX = GetClipMinX();
		charY = GetClipMinY() + 32;
		hako = "";
		running = true;

状態を初期化する。
文字の出力先の x 座標は画面の左端に、y 座標は画面の上端の HP バー・残り時間の表示を避けた位置に設定する。
hakorunning は Hello World あたたたた での指定に従って設定する。

更新 (@MainLoop)

		if (running) {
			/* 後述 */
		} else {
			// running が false なら終了する
			AddLife(-5000);
		}

running が真であれば、更新処理を行う。
そうでなければ、ライフを 0 にし、終了する。

			// 0 か 1 をランダムに決める
			let r = rand_int(0, 1);
			// それに応じて変数 char の値をセット
			let char;
			if (r == 0) {
				char = "";
			} else {
				char = "";
			}

rand_int 関数で 0 か 1 の値をとる乱数を取得し、その値に応じて変数 char の値を Hello World あたたたた で指定された文字列にセットする。

			// 画面に char を出力
			if (charX + charSize > GetClipMaxX()) {
				charX = GetClipMinX();
				charY += charSize;
			}

次に文字を描画しようとする位置が画面からはみ出しそうなら、改行する。

			let oldRenderTarget = SetRenderTarget("画面");
			DrawText(char, charX, charY, charSize, 255);
			charX += charSize;

描画先をテクスチャ「画面」に設定し、変数 char に格納された文字列を描画する。
次の描画に備え、文字を描画する位置を更新する。

			// char を hako に追加
			hako = hako ~ char;

変数 char に格納された文字列を、変数 hako に格納された文字列の末尾に追加する。

			// 最後の 5 文字が「あたたたた」か判定
			if (
				length(hako) >= length(target) &&
				hako[length(hako)-length(target)..length(hako)] == target
			) {

変数 hako に格納された文字列の末尾に終了条件の文字列が現れたかを判定する。

まず、hako に格納された文字列の長さが終了条件の文字列の長さ以上かを判定する。
(文字数が足りなければ、終了条件の文字列は現れていないことが確定する。範囲外を取り出そうとしてエラーになるのを防ぐ)
そうであれば、hako に格納された文字列の最後から終了条件の文字列の長さと同じ長さの文字列を取り出し、それが終了条件の文字列と等しいかをチェックする。

				// "お前はもう死んでいる" を出力
				charX = GetClipMinX();
				charY += charSize;
				DrawText("お前はもう死んでいる", charX, charY, charSize, 255);
				// running に false をセット
				running = false;
			}

終了条件を満たしたので、改行して「お前はもう死んでいる」を出力し、runningfalse をセットする。

			SetRenderTarget(oldRenderTarget);

テクスチャ「画面」に設定していた描画先を、デフォルトに戻す。

描画 (@DrawLoop)

		DrawGraphic(GetCenterX(), GetCenterY());

出た文字を描画したテクスチャ「画面」の内容を、ゲーム画面に描画する。

片付け (@Finalize)

		DeleteGraphic("画面");

出た文字を描画する用のテクスチャ「画面」を破棄する。

おわりに

東方弾幕風v0.12m で「Hello World あたたたた」を実現できた。
標準でリプレイが使えるというのが面白い。

なお、今回の「画面」に文字を描画する仕組みは簡易的なものであり、

  • 幅が異なる文字に対応できない
  • 「お前はもう死んでいる」が画面からはみ出す場合でも、改行しない

といった問題がある。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?