randomize

2017/09/06

Killzone KidのArmaスクリプト講座:ループ

ループはコードの中の要素で、一つの文を何度も何度も繰り返すものだ。ループが必要になる時はいつか来るし、いくつかの命令も存在する。例を挙げよう。3つ同じアイテムをインベントリに追加するとき、単にアイテムを追加、追加、追加で3つ追加できる。でも20回繰り返す時、20回同じ文を書くより、もっと簡単で素早いやり方がある。

forループ。このループ命令は{}コードの中を決められた回数繰り返すことができる。この命令は2つの構文で動かすことが出来る。2つの構文はよく似ているが、処理速度に違いがある。100000回繰り返してみたが、片方のループは2.5倍早く完了した。
for "_i" from 5 to 93 step 7 do {hint str _i}; //96を表示
これが速い方の繰り返しだ。この構文は次のような命令だ:プライベート変数_iを作り、5を代入、それが93以上になったらループを終了、そうでなければhint str _iを実行し7を追加、93以上ならループを終了、そうでなければhint str _iを実行し7を追加…と条件が満たされるまで繰り返す。もしstepを省略した場合、デフォルトに則り_iは1づつ増える。93以上で処理を終了するのは、toで設定したからだ。この構文における終了条件はif _i >= 93でこれが成り立つと終了する。また、toの値はループが始まるときにのみ処理されるため、ceil (random 10)のように設定した時、ランダムなループ回数を設定し、その処理はループ開始時にのみ一度だけ行われる。他にも独自な要素として、_iforの中でのみ動くプライベート変数で、つまりループの外に_iがあったとしても、ループ内の_iには影響を及ぼさない。stepを0にすることで無限回のループが可能。
for "_i" from 0 to 1 step 0 do {}; //無限にループする
もし他の言語でのコーディングに慣れているとしたら、別の構文も存在する。こっちが遅い方のループだ。
for [{_i=5},{_i<=93},{_i=_i+7}] do {hint str _i}; //89を表示
この構文は次のような命令だ:_iに5を代入、それが93以下でなければループを終了、条件が満たされている場合hint str _iを実行した後7を足す、それが93以下でなければループを終了、条件が満たされている場合hint str _iを実行した後7を足す…こうして条件が満たされなくなるまでループを実行する。最初の例とは違い、_iは設定した上限を超えることはない(訳注:早い方の例は条件が満たされた後でも一度だけ括弧の中を実行するが、遅い方の例はそうではない)。これは条件の判断基準がwhile {_i <=} 93 doループによって行われているからだ。_iは設定しないかぎりプライベートではない。
_i = 101;
for [{private "_i"; _i=5},{_i<=93},{_i=_i+7}] do {};
hint str _i; //101を表示
最初のforループとは違い、このループは毎回のループで条件をチェックする。これが遅い理由なんじゃないかと思う。条件の数字はループ中でも変更することが出来る。
k = 3; a = {k = k + 0.5; k};
for [{_i=0},{_i<(call a)},{_i=_i+1}] do {
     diag_log format ["_i:%1/k:%2", _i, k];
};
//.rpt出力結果
//"_i:0/k:3.5"
//"_i:1/k:4"
//"_i:2/k:4.5"
//"_i:3/k:5"
//"_i:4/k:5.5"
//"_i:5/k:6"
//"_i:6/k:6.5"
このループは意図的にあるいはミスで無限にループさせることが出来る。条件が常にtrueなら(訳注:trueとハードコードする以外にも符号を間違えるなどの要因で)無限に出来る。
for [{_i=0},{true},{_i=_i+1}] do {}; //無限回のループ
あなたはこう言うかもしれない。(訳注:無限ループについて?)何でこんなに面倒なことしなくちゃいけないんだ?while {true}ループなら簡単に事を済ませられる。本当は完全に無限ってわけじゃないけれども。whileについて見ていこう。
_i=5; while {_i<=93} do {hint str _i; _i=_i+7}; //89を表示
_i=5; while {_i<=93} do {_i=_i+7}; hint str _i; //96を表示
ご覧のように、先のforループと同じ結果をwhileで得ることが出来た。一方でミスで暴走させるのも非常に簡単だ。エンジンによりwhileループの回数に限りがあるのはこれが理由だ。whileループは最大10000回までしか実行されず、超えると中断される。次の文をinit(訳注:init.sqfではなくUnitのInit欄)に入れて結果を見てみよう。
i=0; while {true} do {i=i+1}; hint str i; //10000を表示
この制限はwhileがエンジンによって呼び出され、かつcallの返り値を待たねば次に進めない時のためにある。この制限はイベントハンドラ、FSM、initのいずれかで無限whileループをしようとした時に発生する。スクリプトがspawnexecVMで実行された時は無限回ループされる。また先の例のforループにはこの制限は適用されない。init.sqfは呼び出されていないので、while {true}ループは無限に実行される。(下の捕捉にて、少しだけ詳しく解説します)まとめると…
spawn {while {true} do {}} //常に無限
call {while {true} do {}} //FSM・イベントハンドラ・initから実行の時10000
call {while {true} do {}} //init.sqf・execVMで実行された時無限
spawn {call {while {true} do {}}} //常に無限
多くの人がwhile {true}ループを物事のチェックに使い、またそれは既存のコードに継ぎ足しする際非常に簡単なため、共有するとき特によく使われる方法だ。これは悪い習慣であり、あなたは何百万というループによってCPUの速度を落とすことは望んでいない。代わりにイベントハンドラを学ぼう。Armaには様々なイベントハンドラがあり、条件が満たされた瞬間にコードが実行される。例としてプレイヤーが乗り物に乗り降りしたことを検知してみよう。初心者らしくwhile {true}でやることもできるし、GetInGetOutイベントハンドラを使ってプロっぽく事を済ませることも出来る。
//イイネ
_veh addEventHandler ["GetIn",{_this call isIn}];
_veh addEventHandler ["GetOut",{_this call isOut}];

//控えめに言ってクソ
lastVehicle = objNull;
lastSeat = "";
null = [] spawn {
    private ["_veh","_roles","_seat"];
    while {true} do {
        _veh = vehicle player;
        if (_veh != player) then {
            _roles = assignedVehicleRole player;
            _seat = if (count _roles > 0) then [{_roles select 0},{""}];
            if (_veh != lastVehicle || _seat != lastSeat) then {
                lastVehicle = _veh;
                lastSeat = _seat;
                [_veh, _seat, player] call isIn;
            };
        } else {
            if (!isNull lastVehicle) then {
                [lastVehicle, lastSeat, player] call isOut;
            };
        };
        sleep 0.1;
    };
};
whileの反応速度を上げるためには、ループの間のsleepの値を小さくする必要がある。ループ速度が上がり、CPU処理速度は下がる。イベントハンドラなら毎回条件が満たされた時即座に実行される。

forEachループ。これは以前軽く紹介したので、今回は短めに紹介しよう。このループは渡された配列一つひとつに従いループし、値が無くなったら終了する。ループ中に配列の内容を足したり引いたりしてもループ回数には何の影響も与えない。
_array = [1,2,3];
diag_log format ["_array:%1", _array];
{
    _array = _array + [0];
    diag_log format ["_x:%1/_forEachIndex:%2/_array:%3", _x, _forEachIndex, _array];
} forEach _array; 
diag_log format ["_array:%1", _array];
//.rpt出力結果
//"_array:[1,2,3]"
//"_x:1/_forEachIndex:0/_array:[1,2,3,0]"
//"_x:2/_forEachIndex:1/_array:[1,2,3,0,0]"
//"_x:3/_forEachIndex:2/_array:[1,2,3,0,0,0]"
//"_array:[1,2,3,0,0,0]"
ただし、set命令を使用するときには注意が必要だ。ループ中にsetを使って配列を改変すると無限ループに陥る危険がある。(次の例ではminを使って制限して無限ループを回避している)
_array = [1,2,3];
diag_log format ["_array:%1", _array];
{
    _array set [(count _array) min 5, 0];
    diag_log format ["_x:%1/_forEachIndex:%2/_array:%3", _x, _forEachIndex, _array];
} forEach _array; 
diag_log format ["_array:%1", _array];
//.rpt出力結果
//"_array:[1,2,3]"
//"_x:1/_forEachIndex:0/_array:[1,2,3,0]"
//"_x:2/_forEachIndex:1/_array:[1,2,3,0,0]"
//"_x:3/_forEachIndex:2/_array:[1,2,3,0,0,0]"
//"_x:0/_forEachIndex:3/_array:[1,2,3,0,0,0]"
//"_x:0/_forEachIndex:4/_array:[1,2,3,0,0,0]"
//"_x:0/_forEachIndex:5/_array:[1,2,3,0,0,0]"
//"_array:[1,2,3,0,0,0]"
特定の目的のためのforEachもあり、チームに対して使えるforEachMemberforEachTeamMemberforEachMemberAgentがある。いずれにせよ_xに現在の値が入り、_forEachIndexに添字が入る。

おっと、onEachFrameループを忘れるところだった。これは与えられたスクリプトを毎フレーム繰り返す無限ループだ。例えばフレームレートなどを表示するときなどに便利で、例えばwhile {true}sleep 0.01ループでGUIのスクリプトを回すとき、1秒に100回HUDを更新することになり、60FPSの時に少し無駄(訳注:1秒につき40回分の処理が無駄)が発生する。こういう時はonEachFrameが適切だ。
onEachFrame {hint str diag_fps};
onEachFrame {hint str diag_tickTime};
この命令は独立したスクリプト{}をループさせるため、onEachFrameの後ろにあるスクリプトは(訳注:whileなどとは違い)続けて実行される。また、エンジンは一度に一個のonEachFrame命令しか実行できないので、次にonEachFrameを実行するとき以前のものは上書きされる。つまりonEachFrameをキャンセルするときは空のスクリプトを渡せばよい。他にも、sleepwaitUntilのような遅延は使えない。引数を渡すときはグローバル変数である必要がある。
_i = "123";
onEachFrame {
    hint str (isNil "_i"); //"true"を表示し続ける
};
cutText [str _i, "PLAN DOWN"]; //(onEachFrameで)処理がストップすることなく"123"を表示する
onEachFrame {hint "1"; sleep 1; hint "2"}; //.rptはエラーを吐き続ける
i = 0;
onEachFrame {
    i = i + 1;
    hint str i; //0からカウントアップし続ける
};
i = 0; //0に再設定
onEachFrame {}; //ループを停止
まだあるぞ。

Enjoy,
KK


KK's blog – ArmA Scripting Tutorials: Loops by Killzone Kid
Translated by POLPOX

補足

while {true}ループ周りの話なんですけど、ごちゃごちゃしてるし私自身もあまり理解していないので、こういう概念が存在することを理解していただければ幸い。
あのあたりを深く見ていくと、callだのspawnだのscheduledだのnon-scheduledだのという言葉が出てきてチンプンカンプンなので実体験をもとに書くわけですけど、とりあえず次の図を見てください。
このスクリプトの流れを表した図において、左から右にスクリプトの処理が進んでいます。
黒の流れにおいて、遅延は一切許されず、スクリプトの処理を待つ列でギチギチに詰まっています。これをnon-scheduledと呼びます。
赤の回り道を回るcallは、この流れの中にあります。
青の道はspawnで分岐した処理を表しています。これは流れの中にはなく、自由に実行されます。これをscheduledと呼び、黒の流れとspawnは同時に処理され(?)、右に進んでいきます。
あ、scheduledだのは覚えなくていいです。

さて、この中でwhileに10000回の制限がかけられているものはどれでしょう。答えは黒と赤の流れです。
理由は、無限ループに入るとその後の処理が全く行えなくなるため(=クラッシュ)です。
そのための制限なんですね。

以上を踏まえて、以下のスクリプトを順にDebug Consoleで実行してみて、その処理の違いをよく観察してみてください。
[] call {i = 0 ; while {true} do {i = i + 1 ; hint str i ;}}
[] spawn {i = 0 ; while {true} do {i = i + 1 ; hint str i ;}}
分からんでもないぐらいに思ってください。俺も分からん。

ちなみに、sleepが使えるのは青の道だけです。

0 件のコメント :

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。