◆PROCESSING 逆引きリファレンス
カテゴリー:制御系
一定時間後に処理を行うには
【解説】
PROCESSINGで、一定時間後に処理を行いたい事は無いでしょうか?
例えば
- ある図形を表示し、5秒後に別の図形を表示する(A)
- ある処理を実行し、3秒後に再度実行する(B)
といった具合です。
以下では、このように「ある時点から何秒経過したか」を測定し、その時点でなんらかの処理を行いたい場合の処理について紹介したいと思います。
「ある時点から何秒経過したか」をどのように測定するかについては、いくつかの方法があります。
描画に関係する処理(Aなどの場合)なら、draw()ループの処理時間や、プログラムの動作時間をもとに計算するのが便利です。
特に描画処理に関係しない汎用的な時間測定(Bのように、一定時間待つ処理など)なら、マルチスレッドを用いてサブスレッド側で時間を計測するのが良いと思います。
また一定間隔で何かの処理を実行させるには、JavaのTimerクラスを利用するという手段もあります。こちらについては「一定時間後に処理を行うには(Timer編)」記事にまとめましたので、よろしければ参考としてください。
【詳細】
まず、(A)の処理で使えそうなタイマーとしては
- draw()ループが回る時間(A-1)
- プログラムが開始してからの経過時間(A-2)
の2つがあります。
A-1:draw()ループが回る時間を利用する
この方法はPROCESSINGの draw()ループの処理回数を使って、ある時点からの時間を測定するものです。
draw()ループは、デフォルトでは 1/60 秒間隔で自動的に繰り返し実行されています。よって、draw()ループが1回まわるたびに変数を加算していき、変数が60になった時点(=60回まわった時点)で 1 秒が経過したと判断します。
(画像URL:illust-AC 様:K-factoryさん)
具体的には、以下のようなコーディングになります。
下記を実行すると、最初は画面中央に円が描かれます。そして3秒経過すると自動的に図形が四角形に変化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/** * PROCESSING 3 時間計測例-1 * @auther MSLABO * @version 2019/04 V1.0 */ //Loop回数計測変数 int count = 0; void setup(){ size(200,200); //draw()処理を 1/60秒で回す frameRate(60); } void draw(){ //Loop回数加算 //countは1/60秒ごとに1増える count++; if( count < 60*3 ){ //開始から3秒以内(180回回る前)なら円を描く ellipse(100,100,width/2,height/2); } else { //3秒以上経過したら四角を描く rect( 50,50,100,100); //ここで描画Loop終了 noLoop(); } } |
count 変数は 1/60秒ごとに1加算されていきます。この変数が60になったら1秒経過したと判断できます。
今回は3秒後に図形を変更したいので、60 * 3 = 180 経過したかどうかを判断基準としています。
この方法の欠点は
- 測定精度が、draw()処理のループ間隔に依存する事
- draw()ループ内で重たい処理を行った時、計測が乱れる事
でしょうか。
draw()処理は 1/60秒間隔で繰り返し実行されると書きましたが、正確に 1/60秒=約16ミリセカンド単位で回っているわけではないのです。
PCの負荷やプログラムの処理内容に従って、若干の遅延やブレが生じます。つまり厳密ではないのです。
それでも良い(つまり多少の誤差は許される)のなら、この処理が一番シンプルでわかりやすいと思います。
A-2:プログラムが開始してからの経過時間を利用する
PROCESSINGには、プログラムが開始してからのミリセカンドを計測する命令(millis)があります。
draw()処理の中でこれを利用して、「ある処理が行われた」時点の秒数(millisの値)と、「現在の秒数(その時点のmillisの値)」を比較し、その差分で時間を計測する事が可能です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
/** * PROCESSING 3 時間計測例-2 * @auther MSLABO * @version 2019/04 V1.0 */ //処理時間計測変数 int starttm = 0; void setup(){ size(200,200); //開始時の時間を取得 starttm = millis(); } void draw(){ //開始時からの差分時刻を計算する int sabun = millis() - starttm; if( sabun < 1000 * 3 ){ //開始から3秒以内なら円を描く ellipse(100,100,width/2,height/2); } else { //3秒以上経過したら四角を描く rect( 50,50,100,100); //ここで描画Loop終了 noLoop(); } } |
上記ではプログラム開始時点でのミリセカンドを取得(starttm)しておき、それを draw()処理を回るごとに取得したミリセカンドと比較する事で、3秒(3000ミリセカンド)経過したかどうかを判断しています。
ただし、この処理の場合もA-1の方法と同じ欠点を抱えています。
この欠点を緩和するためには、draw()処理の中で時間を計測するのはやめて、計測処理そのものを別処理(別スレッド)にする必要があります。
B:別スレッドを利用する
Aの方法の欠点は、draw()処理の中で時間を計測している事に起因しています。
よって時間の計測処理を draw()ループとは別の処理で行い、Aの欠点を回避するのがBの方法となります。
Bの方法では、マルチスレッドを利用します。マルチスレッドに詳しくない方は「マルチスレッドを実現するには」記事を参照してみてください。
この例ではサブスレッド側で時間を計測し、その結果(つまり一定時間経過した事を)何らかの方法でメインスレッド側に通知します。
(画像URL:illust-AC 様:K-factoryさん)
サブスレッド側からメインスレッド側へ非同期に情報を伝達する理想的な方法は、Javaのリフレクション (reflection) 機能を利用する事です。
長所はAに比べて正確な時間が測定できる事になります。ただし、必然的に処理は面倒くさくなります(汗)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
/** * PROCESSING 3 時間計測例-3 * @auther MSLABO * @version 2019/04 V1.0 */ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; //スレッド制御変数 boolean timerFlg = false; Method method = null; PApplet mainApplet; // サブスレッドから結果を受け取るメソッド // マウスクリックから3秒後に呼ばれる public void GetSubStatus(){ println("3秒経過しました:" + millis()); timerFlg = false; } void setup(){ size(200,200); //ステータス受信メソッドを取得 try{ mainApplet = this; method = mainApplet.getClass().getMethod("GetSubStatus"); } catch( NoSuchMethodException ex){ ex.printStackTrace(); } } void draw(){ } void mouseClicked(){ //マウスクリックから3秒計測する if(timerFlg == false){ //サブスレッド実行 println("計測開始:" + millis() ); thread("SubThread"); } timerFlg = true; } void SubThread(){ //ここがサブスレッドで実行される try{ //3秒寝る Thread.sleep(3000); //メイン処理のメソッドを呼び出す if( method != null ){ method.invoke(mainApplet); } } catch(IllegalAccessException ex ){ ex.printStackTrace(); } catch(InvocationTargetException ex){ ex.printStackTrace(); } catch(InterruptedException ex){ ex.printStackTrace(); } } |
処理がずいぶんと増えましたね。はい、めんどくさいです(汗)。
メイン処理(メインスレッド側)で通知を受け取るメソッドを定義しておき、サブスレッド側から 3秒後にinvoke 命令で呼び出しています。
<出力例>
数ミリセカンド程度の誤差はあるでしょうが、おおむね正確に計測できている事がわかります。
その他の方法
マルチスレッドはめんどくさいけど、ある処理からの経過時間はちゃんと測定したいというワガママな人には、単純に Sleep 処理を行うという手段もあります。
ただしメインスレッド(draw処理など)の中でSleepをおこなうと、描画が期待した動作にはなりません。
なぜなら draw()関数は、(なぜか初回を除いて)処理が終了した時に描画結果を表示するからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
/** * PROCESSING 3 時間計測例-4 * @auther MSLABO * @version 2019/04 V1.0 */ void setup(){ size(200,200); } void draw(){ //初回だけ円が表示される ellipse(100,100,width/2,height/2); try{ //3秒寝る Thread.sleep(3000); } catch(InterruptedException ex){ ex.printStackTrace(); } //円の上に四角を描く rect( 50,50,100,100); try{ //3秒寝る Thread.sleep(3000); } catch(InterruptedException ex){ ex.printStackTrace(); } } |
例えば上記サンプルのように
- 円を描く
- 3秒寝る
- 円の上に四角を描く
- 3秒寝る
という順番で処理を行うと、初回だけ円->四角という順番で表示が行われますが、以降は必ず(円の上に四角を描いた状態で表示されるため)四角しか表示されなくなります。
(画像URL:illust-AC 様:K-factoryさん、食べられる前頭葉さん)
描画処理に使うのではなく、あくまでタイミング制御に使いたいという事であれば、この方法はAの欠点を補う簡単な方法となります。
結論
draw()ループが十分に短い間隔(1/60秒など)で回る事が期待できるなら、通常はAの方法で事足りるでしょう。
一方、通信処理やタイミングをあわせた処理など、経過時間が重要な意味を持つ場合はBの方法を応用するのが良いと思います。
draw()処理の中でSleepを行う処理は、意図した描画結果にはならない点を十分に理解した上で利用するならOKだと思います。
【関連記事】
サンプルプログラム
3秒ごとに描画を変更する例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
/** * PROCESSING 3 時間計測 Sample * @auther MSLABO * @version 2019/04 V1.0 */ int nextStep = 0; //次に行う処理変数 boolean apStart = false; //処理開始FLG boolean waitingFlg = false; //待ち合わせ中FLG PFont pFont; //文字 void setup(){ size(300,300); //文字指定 pFont = loadFont("IPAGothic-48.vlw"); textFont(pFont); textSize( 24 ); textAlign(CENTER,TOP); fill(0); } void draw(){ background(230); //マウスがクリックされていない場合 if(apStart == false){ text("待機中…",width/2,height/2); return; } if(nextStep == 0){ text("1番めの処理実行中",width/2,height/2); waitSecond(3000); //3秒まつ } else if(nextStep == 1 ){ text("2番めの処理実行中",width/2,height/2); waitSecond(3000); //3秒まつ } else if(nextStep == 2 ){ text("3番めの処理実行中",width/2,height/2); waitSecond(3000); //3秒まつ } else if(nextStep == 3 ){ //最初に戻る nextStep = 0; apStart = false; } } //時間待ち合わせ処理 void waitSecond(int ms){ //すでに待ち合わせ中なら、何もしない if(waitingFlg) return; //指定時間を計測する //次に行う処理は、現在の処理+1 番とする TimerThread timerThread = new TimerThread(ms,nextStep+1); //Threadクラスを生成 Thread subThread = new Thread(timerThread); //サブスレッド開始 subThread.start(); } void mouseClicked(MouseEvent me){ //マウスクリックで開始 apStart = true; } //時間計測用サブスレッド class TimerThread implements Runnable { int _time; //待ち時間 int _next; //次に行う処理 //コンストラクタ public TimerThread(int time, int next ){ //引数記憶 _time = time; _next = next; } @Override public void run(){ // ここがサブスレッドとして動作する try{ //待ち合わせ中ON waitingFlg = true; //指定時間寝る Thread.sleep(_time); //待ち合わせ中OFF waitingFlg = false; //次の処理へ移行させる nextStep = _next; } catch(InterruptedException ex){ ex.printStackTrace(); } } } |
プログラムを実行すると、画面に「待機中…」と表示されます。
マウスをクリックすると「1番めの処理実行中」と表示した後、3秒後に「2番めの処理実行中」と表示します。その3秒後には「3番めの処理実行中」と表示され、その3秒後に「待機中…」へと戻ります。
時間の計測にサブスレッド処理を利用しています。ただし「表示処理」を目的にしているのでリフレクション (reflection) 機能は利用せず、グローバル変数を用いた制御を行っています。
<出力サンプル>
(画像URL:illust-AC 様:K-factoryさん、よっとさん)
本ページで利用しているアイコン画像は、下記サイト様より拝借しております。各画像の著作権は、それぞれのサイト様および作者にあります。