RailSimⅡ の大きな特徴のひとつに「スイッチ機能」があります。
これは塗装や形態、車番の選択、ギミックの制御など、さまざまな状態を細かく設定できる仕組みです。
ここまで柔軟に状態を制御できるソフトウェアは多くありません。
一方で、設定を細かく作り込もうとすると定義ファイルが煩雑になりやすく、
構文ミスを招く原因にもなります。
このページでは、スイッチ機能の仕組みを整理し、簡潔で可読性の高い定義ファイルの作成を目指します。
RailSim における「スイッチ」は、一般的なプログラミングにおける変数に近い概念です。
値を格納したり、必要なときに取り出したりできる「箱」のようなものと考えられます。
ただし、RailSim のスイッチは通常の変数に比べ、機能が限定されています。主な特徴は次の通りです。
DefineSwitch
)int
型)また、以下の特例も存在します。
補足
システムスイッチについては、マニュアルのmodel-switch
のページに各スイッチの解説があります。
今後、これら各システムスイッチの解説ページも作成予定です。
通常のスイッチはDefineSwitch
を使い、スイッチ名と選択肢を定義します。
プログラムにおける変数の定義と初期化をまとめたようなものですが、型の宣言は省略されています。
実際に定義ファイルに記述するコード例は次の通りです。
DefineSwitch "スイッチサンプル" {
Entry = "A"; // 0番目の選択肢
Entry = "B"; // 1番目の選択肢
Entry = "C"; // 2番目の選択肢
Entry = "D"; // 3番目の選択肢
}
たとえば 2 番目の選択肢(C)を選ぶと、
スイッチ「スイッチサンプル」には2
という数値が格納されます。
この値を後で取り出し、さまざまな状態制御に利用します。
スイッチの選択肢は0 から順番に番号が振られる点に注意が必要です。
たとえば、車番に関するスイッチを定義する場合、
DefineSwitch "車番" {
Entry = "1"; // 0番目の選択肢
Entry = "2"; // 1番目
Entry = "3"; // 2番目
Entry = "4"; // 3番目
Entry = "5"; // 4番目
}
選択肢の名称が「1」であっても、実際に格納される値は0
となります。
この表示と格納値のズレが混乱のもとになることがあります。
もし車番とスイッチ値を一致させたい場合は、ダミーの選択肢を先頭に追加する方法があります。
DefineSwitch "車番" {
Entry = "車番選択"; // ダミー(0番目)
Entry = "1"; // 1番目
Entry = "2"; // 2番目
Entry = "3"; // 3番目
Entry = "4"; // 4番目
Entry = "5"; // 5番目
}
この方法により、スイッチ値と車番表示を直感的に一致させることが可能です。
ここからは、先に定義したスイッチの値を用いて、
状態を制御するコードについて見ていきます。
スイッチの値を取り出す方法には、主に次の 2 つがあります。
ApplySwitch
構文If~Else
構文まずは、よりシンプルなApplySwitch
構文から説明します。
ApplySwitch
構文は、スイッチの値を最もシンプルに利用する方法です。
例えば、次のようにスイッチが定義されているとします。
DefineSwitch "スイッチサンプル" {
Entry = "A"; // 0番目の選択肢
Entry = "B"; // 1番目の選択肢
Entry = "C"; // 2番目の選択肢
Entry = "D"; // 3番目の選択肢
}
ここでApplySwitch
構文を次のように記述することで、
選択されたスイッチ値に応じて状態を変化させることができます。
ApplySwitch "スイッチサンプル" {
Case 0: ChangeTexture = 0, "A.png";
Case 1: ChangeTexture = 0, "B.png";
Case 2: ChangeTexture = 0, "C.png";
Case 3: ChangeTexture = 0, "D.png";
}
ここでCase
の後の数字は、選択肢の順番(スイッチに格納される数値)に対応しています。
たとえば、0 番目と 2 番目の選択肢(A と C)が同じ状態(どちらもA.png
)で良い場合は、次のように記述できます。
ApplySwitch "スイッチサンプル" {
Case 0,2: ChangeTexture = 0, "A.png";
Case 1: ChangeTexture = 0, "B.png";
Case 3: ChangeTexture = 0, "D.png";
}
複数のケースを,
(カンマ)で区切ってまとめることで、記述を簡潔にできます。
さらに、特定のケース(例:3 番目)だけが異なり、
それ以外はすべて同じ処理をしたい場合は、Default
を使うことができます。
ApplySwitch "スイッチサンプル" {
Case 3: ChangeTexture = 0, "D.png";
Default: ChangeTexture = 0, "A.png";
}
注意
Default
は必ず一番後ろに記述する必要があります。
ApplySwitch
は、ユーザー定義スイッチだけでなく、システムスイッチにも適用できます。
例:進行方向を示す_FRONT
スイッチを使い、ヘッドライトとテールライトを制御する場合。
ApplySwitch "_FRONT" {
Case 0: ChangeTexture = 0, "HeadLight.png"; // 前進時
Case 1: ChangeTexture = 0, "TailLight.png"; // 後退時
}
_FRONT
では、前進時に 0、後退時に 1が自動的に格納されています。
ApplySwitch
はシンプルな状態分岐には便利ですが、
複数のスイッチが絡む複雑な制御では記述が煩雑になります。
たとえば、以下のように 3 つのスイッチを組み合わせる場合を考えます。
ApplySwitch "スイッチ1" {
Case 0:
ApplySwitch "スイッチ2" {
Case 2,3:
ApplySwitch "スイッチ3" {
Case 0: ChangeTexture = 0, "A.png";
}
Case 4:
ApplySwitch "スイッチ3" {
Case 1: ChangeTexture = 0, "B.png";
}
}
Case 1:
ApplySwitch "スイッチ2" {
Case 0,1:
ApplySwitch "スイッチ3" {
Case 0: ChangeTexture = 0, "C.png";
}
Case 4:
ApplySwitch "スイッチ3" {
Case 1: ChangeTexture = 0, "D.png";
}
}
}
このようにスイッチの入れ子構造になり、
定義ファイルが非常に煩雑になり、管理が難しくなります。
そこで、より簡潔に記述できる方法として、
次にIf~Else
構文を紹介します。
If~Else
構文は、一般的なプログラミングにおいて基本的かつ多用される記述方法です。
設定した条件を満たすか満たさないかを判定し、それぞれの場合に応じた処理を定義します。
RailSim においても、次のような構文で使用されます。
If 変数または式 {
条件を満たす(結果が0以外)場合の処理
}
Else {
条件を満たさない(結果が0)場合の処理
(記述する場合はIf文とセット。必要なければ省略可能)
}
一見すると難しく感じるかもしれませんが、
ここでは順を追って解説していきます。
まず、_FRONT
スイッチを使った基本的な例を見てみます。
_FRONT
には前進時に 0、後退時に 1が格納されています。
これを If 文で制御する基本パターンは次の通りです。
If "_FRONT" {
ChangeTexture = 0, "TailLight.png"; // "_FRONT"が1(後退時)
}
Else {
ChangeTexture = 0, "HeadLight.png"; // "_FRONT"が0(前進時)
}
この場合、"_FRONT"
から値をそのまま取り出し、
その値が 0 以外(後退時)なら If の中が、0(前進時)なら Else の中が実行されます。
次に、否定演算子!
を使って条件を反転させる例を見てみます。
!
は、後に続く値が 0 なら 1 を、0 以外なら 0 を返します。
If !" _FRONT" {
ChangeTexture = 0, "HeadLight.png"; // "_FRONT"が0 → !" _FRONT"が1 → 前進時
}
Else {
ChangeTexture = 0, "TailLight.png"; // "_FRONT"が1 → !" _FRONT"が0 → 後退時
}
この方法により、条件の視点を反転させることができます。
なお、今回のシンプルなケースでは否定演算子を使う必然性は薄いですが、
より複雑な条件分岐では非常に有効な手段になります。
システムスイッチのように 0 と 1 の二値しか取らない場合は、
この単純な If 構文で問題ありません。
しかし、次のような複数の選択肢を持つスイッチでは注意が必要です。
DefineSwitch "スイッチサンプル" {
Entry = "A"; // 0番目の選択肢
Entry = "B"; // 1番目の選択肢
Entry = "C"; // 2番目の選択肢
Entry = "D"; // 3番目の選択肢
}
この状態で、例えば単純な If 構文を使うと次のようになります。
If "スイッチサンプル" {
ChangeTexture = 0, "B.png"; // "スイッチサンプル"が0以外(1, 2, 3)のいずれか
}
Else {
ChangeTexture = 0, "A.png"; // "スイッチサンプル"が0
}
ここでは、0 以外の全ての値に対して同じ処理しかできず、
個別の値ごとに異なる処理を行うことができません。
そこで複数の値に対して個別に処理を行いたい場合は、
条件式(演算子付き)を使います。
ここまでの例では、If文のカッコ内には変数そのものや、
変数の値を反転(例:!"_FRONT")させたものを直接記述してきました。
この場合、変数の値自体(0か、0以外か)を判定し、処理を分岐していました。
ここからはさらに一歩進め、
変数同士を比較した結果である条件式を使って分岐する方法を紹介します。
たとえば、次のように記述します。
If "_FRONT" == 0 {
ChangeTexture = 0, "HeadLight.png"; // "_FRONT"が0(前進時)
}
Else {
ChangeTexture = 0, "TailLight.png"; // "_FRONT"が0以外(後退時)
}
ここでは、"_FRONT"という変数そのものではなく、
「"_FRONT"の値が0と等しいかどうか」という比較結果をIf文に渡しています。
ここで、混乱しやすいポイントがあります。
一見すると、
ように見えるため、
「先ほどIf 変数または式{}
内の処理は値が0以外の値の時と説明していたのに、なぜIfの中に入るのか?」
と疑問に思うかもしれません。
ですが、ここで大事なのは、
If文に渡しているのは条件式の結果であるという点です。
というふうに、
比較演算の結果(0または1) をIf文が判定しているのです。
つまり、"_FRONT"の値が0であっても、
"_FRONT" == 0という条件式は成立(= 1) となり、If文の中に入ります。
これを押さえておくと、
複雑な条件式を組み立てるときにもスムーズに理解できるでしょう!
プログラムでは、数学と異なり比較には==
を使う点に注意してください。
(=
は代入用の記号であり、比較では使えません)
RailSim の定義ファイルでは、値の代入は選択肢の選択や自動処理によって行われるため、
自分で=
による代入を書く必要は基本的にありません。
条件式を少し変えることで、次のようにも記述できます。
If "_FRONT" == 1 {
ChangeTexture = 0, "TailLight.png"; // "_FRONT"が1(後退時)
}
Else {
ChangeTexture = 0, "HeadLight.png"; // "_FRONT"が1以外(前進時)
}
ここで、先ほどは個別判定ができなかったスイッチサンプルに対して、
条件式を用いる方法を見てみましょう。
DefineSwitch "スイッチサンプル" {
Entry = "A"; // 0番目の選択肢
Entry = "B"; // 1番目の選択肢
Entry = "C"; // 2番目の選択肢
Entry = "D"; // 3番目の選択肢
}
これに対して条件式を使うと、次のような記述が可能です。
If "スイッチサンプル" == 0 {
ChangeTexture = 0, "A.png";
}
If "スイッチサンプル" == 1 {
ChangeTexture = 0, "B.png";
}
If "スイッチサンプル" == 2 {
ChangeTexture = 0, "C.png";
}
If "スイッチサンプル" == 3 {
ChangeTexture = 0, "D.png";
}
このように、個別の値ごとに処理を分けることができるようになります。
ここまでは非常にシンプルな条件分岐の例でした。
これまでに取り上げた例、特に最後のような場合では、
ApplySwitch
構文を使った方がより簡潔に記述できるでしょう。
次は、If~Else
構文が真価を発揮する、
より複雑な条件分岐の例を見ていきます。
例えば、これまでたびたび取り上げてきた_FRONT
スイッチによるヘッドライト・テールライトの制御ですが、
実際に組み込む際には、「車両の電源が入っているか」「ライトが点灯状態か」といった
他の条件と組み合わせることが多くなります。
ここでは、次のようなスイッチが定義されているものとします。
DefineSwitch "電源" {
Entry = "ON";
Entry = "OFF";
}
DefineSwitch "ライト設定" {
Entry = "点灯";
Entry = "消灯";
}
ここで、電源が ON かつライト設定が点灯の場合のみ、
ライト制御を行うようにしてみます。
ApplySwitch だけで記述しようとすると、
まだ比較的シンプルな条件とはいえ、3 重の入れ子構造になってしまいます。
ApplySwitch "電源" {
Case 0:
ApplySwitch "ライト設定" {
Case 0:
ApplySwitch "_FRONT" {
Case 0: ChangeTexture = 0, "HeadLight.png"; // 電源ONかつライト点灯かつ前進時
Case 1: ChangeTexture = 0, "TailLight.png"; // 電源ONかつライト点灯かつ後退時
}
Case 1:
ChangeTexture = 0, "LightOFF.png"; // 電源ONだがライト消灯設定
}
Case 1:
ChangeTexture = 0, "LightOFF.png"; // 電源OFF
}
そのため、ここではIf
文を使って表現してみます。
次のように、If
文を組み合わせることで表現できます。
If ("電源" == 0) && ("ライト設定" == 0) && ("_FRONT" == 0) {
ChangeTexture = 0, "HeadLight.png"; // 電源ONかつライト点灯かつ前進時
}
If ("電源" == 0) && ("ライト設定" == 0) && ("_FRONT" == 1) {
ChangeTexture = 0, "TailLight.png"; // 電源ONかつライト点灯かつ後退時
}
If ("電源" == 1) || ("ライト設定" == 1) {
ChangeTexture = 0, "LightOFF.png"; // 電源OFFまたはライト消灯設定
}
&&
と||
演算子について ここで使われている&&
は論理積 ANDを意味します。
日本語で表現すると「かつ」に当たるものです。
条件1 && 条件2
→ 条件 1 と条件 2 が両方とも成立するときに真となる一方で||
は論理和 ORを意味します。
日本語で表現すると「または」に当たるものです。
条件1 || 条件2
→ どちらか一方でも成立すれば真となる1 つのIf
文に条件をすべて詰め込まず、
条件を段階的に分けて記述することで、より可読性を高める方法もあります。
If ("電源" == 0) && ("ライト設定" == 0) {
// 電源ONかつライト点灯設定
If "_FRONT" == 0 {
ChangeTexture = 0, "HeadLight.png"; // 前進時
}
Else {
ChangeTexture = 0, "TailLight.png"; // 後退時
}
}
Else {
ChangeTexture = 0, "LightOFF.png"; // 電源OFFまたはライト消灯設定
}
また、If
文とApplySwitch
構文を組み合わせることも可能です。
If ("電源" == 0) && ("ライト設定" == 0) {
// 電源ONかつライト点灯設定
ApplySwitch "_FRONT" {
Case 0: ChangeTexture = 0, "HeadLight.png"; // 前進時
Case 1: ChangeTexture = 0, "TailLight.png"; // 後退時
}
}
Else {
ChangeTexture = 0, "LightOFF.png"; // 電源OFFまたはライト消灯設定
}
このように、適度に条件分岐を分割することで、
コードの見通しが良くなり、管理しやすくなります。
可読性や管理のしやすさは個人の感覚に依存する部分も大きく、
必ずしも「これが正解」という書き方はありません。
ただし、基本的には
次は、さまざまなスイッチを組み合わせたより実践的な応用例を見ていきます。
例えば、車番に関する次のようなスイッチが定義されているとします。
DefineSwitch "車番" {
Entry = "車番選択"; // ダミー(0番目)←選択肢名(車番)とスイッチ値を一致させるための調整
Entry = "1"; // 1番目
Entry = "2"; // 2番目
Entry = "3"; // 3番目
~中略~
Entry = "98"; // 98番目
Entry = "99"; // 99番目
Entry = "100"; // 100番目
}
ここで、車番が奇数のときと偶数のときで仕様を変えたい場合、
どのように記述すればよいでしょうか?
最もシンプルな方法は、ApplySwitch 構文を使うことです。
実際に ApplySwitch で記述すると、次のようになります。
ApplySwitch "車番" {
Case 1,3,5,7,9,11,~中略~,95,97,99: ChangeTexture = 0, "奇数.png";
Default: ChangeTexture = 0, "偶数.png";
}
この場合、行数自体は少ないものの、
といった手間が発生し、
保守性に乏しいという問題があります。
これに対して、If 文を使うとよりシンプルに記述できます。
奇数と偶数の判定は、「2 で割った余り」を使って行います。
具体的には次のような条件式になります。
"車番" % 2 == 1
ここで
です。
実際に If 文で条件分岐を表現すると、次のようになります。
If "車番" % 2 == 1 { // 車番が奇数のとき
ChangeTexture = 0, "奇数.png";
}
Else { // 車番が偶数のとき
ChangeTexture = 0, "偶数.png";
}
特に車番が多い場合や、
後から車番が増減する可能性がある場合は、If 文を使った方が可読性・保守性に優れています。
駅舎アドオンでは、通常の駅設備に加え、踏切や信号といった線路付帯設備も対象になります。
特に踏切は典型例であり、列車が踏切区間に進入したことを検知したら、警報器を鳴らし遮断機を下ろす必要があります。
RailSimでは、駅舎アドオンで定義された線路に列車が進入したかを検知するために、
_APPROACH1
および _APPROACH2
というシステムスイッチが用意されています。
この2つが存在する理由は、
列車の進行方向ごとに別々に検知できるようにするためです。
駅舎アドオンでの線路定義は、次のように記述します。
Platform {
Coord = (0.0, 0.0, 0.0); // 線路の始点座標
Coord = (0.0, 0.0, 100.0); // 線路の終点座標
}
ここで、
となります。
列車がこの区間に存在しない場合、
_APPROACH1または_APPROACH2には0が代入されます。
列車がこの区間に進入した場合は、
どちらかに1が代入されます。
両方が同時に1になることはありません。
列車が進行方向に関係なく在線しているかを検知するには、
次のように記述します。
If ("_APPROACH1" == 1) || ("_APPROACH2" == 1){
(列車が進入した時の処理)
}
ここでは論理和(||
、OR)演算子を使っています。
さらに、ビットOR演算子|
を使えば、
より短く記述することも可能です。
If "_APPROACH1"|"_APPROACH2"{
(列車が進入した時の処理)
}
補足
ビット演算子については、この後の章で詳しく解説します。
これまでの説明では、概要を簡単にするために一部正確でない部分がありました。
ここでは、より厳密な仕組みについて解説します。
駅舎アドオンでの線路定義では、次のような記述を行うことができます。
Platform { // 1番線
Coord = (0.0, 0.0, 0.0);
Coord = (0.0, 0.0, 100.0);
}
Platform { // 2番線
Coord = (4.0, 0.0, 0.0);
Coord = (4.0, 0.0, 100.0);
}
Platform { // 3番線
Coord = (8.0, 0.0, 0.0);
Coord = (8.0, 0.0, 100.0);
}
Platform { // 4番線
Coord = (12.0, 0.0, 0.0);
Coord = (12.0, 0.0, 100.0);
}
このように、複数の番線を持つ構成も可能です。
_APPROACH1
・_APPROACH2
の正確な仕組み これらのスイッチは、ビット単位で在線情報を格納しています。
言葉ではわかりづらいので具体例で見てみましょう。
4番線まである場合:
このように、各ビット(桁)が各番線の在線状態を表しています。
ここで注意すべきは、このビット列は2進数だということです。
例えば、ビット列0110
は10進数に直すと6
です。
実際にRailSimの定義ファイル上では、スイッチ値は10進数として扱われます。
そのため、例えば2番線と3番線に列車がいる場合の検知条件は次のように記述します。
If ("_APPROACH1" == 6) || ("_APPROACH2" == 6) {
(2番線と3番線に列車が進入したときの処理)
}
スイッチに格納できるのは32bit整数です。
つまり、最大で32番線まで在線検知が可能ということになります。
最初に説明した「1本だけの線路」の場合は、
2進数と10進数が見た目上同じ(0001=1)だったため、この説明を省略していました。
ここで問題が発生します。
たとえば「3番線だけに列車がいるか検知したい」としましょう。
単純に、
If ("_APPROACH1" == 4) || ("_APPROACH2" == 4)
と記述すると、3番線のみ列車がいるときには検知できますが、
3番線+他の番線にも列車がいるときには検知できません。
例:
0100
(4)0110
(6)1100
(12)このように、3番線に列車がいればビット4が1になりますが、
他のビットも同時に立っていると単純比較では対応できません。
3番線が含まれるパターン(10進数表記)は次のとおりです。
0100
)0101
)0110
)0111
)1100
)1101
)1110
)1111
)これらすべてに対応しようとすると、
膨大な条件式を列挙しなければならず、非常に煩雑になります。
こうした問題を解決するために、
次章ではビット演算子を使ったシンプルな検知方法を紹介します。
ビット演算子とは、2進数の各ビットを操作するための演算子です。
計算というよりも、「フィルタリング」や「判定しやすい形に変換する」ような用途で使われます。
実際には2進数の内部構造に対して操作が行われますが、
RailSimの定義ファイル上では10進数表記で記述する必要があるため、
そこが少し難解に感じる原因となります。
先ほどの3番線だけを検知する条件式について、
より効率的な2つのアプローチを紹介します。
3番線が在線しているパターンでは、
必ず「3桁目のビット」が1になっているという共通点がありました。
これを判定するには、**ビットAND演算子&
**を使う方法があります。
例:
If ("_APPROACH1" & 4) || ("_APPROACH2" & 4) {
(3番線に列車が進入したときの処理)
}
この処理はマスクと呼ばれます。
ざっくり言えば「指定したビットだけを抜き出してチェックする」仕組みです。
例:
0101
(1番線と3番線に列車)に0100
でマスク → 0100
(4)1010
(2番線と4番線に列車)に0100
でマスク → 0000
(0)つまり、マスクしたビットが立っていれば0以外の値になり、
立っていなければ0になるわけです。
結果として、3番線に列車がいる場合だけ条件が成立する、
シンプルなIf文で判定できるようになります。
この方法はとてもスッキリしていますが、
裏で「2進数」と「10進数」を行き来して考える必要があり、
慣れるまで若干混乱することもあります。
もう少し直感的な方法もあります。
それがビットシフト>>
とマスク&
の組み合わせです。
ビットシフトは、各ビットを左右にズラす操作です。
例:
1110
(2番線・3番線・4番線に在線)に対して
1110 >> 2
(右に2ビットシフト)をすると、
0011
になります。
つまり、
という挙動です。
ビットシフトを使って、3番線の情報を最下位ビット(1桁目)に持ってきます。
そのうえで、マスク&1
を使って判定します。
例:
If (("_APPROACH1" >> 2) || ("_APPROACH2" >> 2)) & 1 {
(3番線に列車が進入したときの処理)
}
ここでは、
"_APPROACH1" >> 2
で、3番線の情報を1桁目に寄せる& 1
で、最下位ビットだけをチェックするという流れです。
駅舎アドオンを作らない限り、
ビット演算の知識は必須ではありませんが、
理解しておくと非常に強力な道具になります。
これまで、特定の番線に列車が在線しているかを検知する方法を解説してきました。
では、このセクションの冒頭で例示した踏切の場合はどうでしょうか?
ここでは、4本の線路(複々線) をまたぐ踏切を考えます。
踏切の役割を考えると、
逆に言えば、
ということです。
ここでポイントとなるのが、システムスイッチ_APPROACH1
と_APPROACH2
です。
つまり、次の条件式で踏切動作を制御できます。
例:
If "_APPROACH1" | "_APPROACH2" {
(警報器を鳴らし遮断機を下ろす処理)
}
|
の意味 ここで使われている|
(ビットOR演算子)は、
という動きをします。
例:
0011
と 0110
をビットORすると → 0111
つまり、
どちらかに列車がいれば、必ず最終結果は「0ではない」値になるわけです。
複々線踏切の制御においては、
"_APPROACH1" | "_APPROACH2"
が0かどうかで判定という非常にシンプルな条件式で、安全に踏切制御が可能になります。