gotutiyan’s blog

プログラミング関連の話題を中心に

Processingで「時間」を扱う一般的なテクニック

はじめに

本記事では時間を扱うテクニック全般を紹介します.

時間を扱いたくなる場面としては,ゲーム制作であれば,ステージをクリアするのに時間制限を設けるとき,必殺技的な何かを打った時のクールダウンを設定するときなどが考えられます.アート作品のポートフォリオでは,定数時間で他の作品に移るような処理が考えられます.もしくは学校の課題では,アナログ時計などを作るかもしれません.

このような時間全般の話題を網羅するのが本記事の目指すところです.それぞれの内容は独立しているので,必要そうなところだけ読んでいただければと思います.

目次

時間を扱うためのメソッド・システム変数

まずは,時間を扱うためのメソッドやシステム変数を紹介します.

hour(), minute(), second()

まさに時間を返すメソッドです.順番に時,分,秒を表していて,リアルタイムと連動しています.

void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  int s = second(); 
  int m = minute();  
  int h = hour();    
  text(h+":"+m+":"+s,50,100);
}

millis()

実行時からのミリ秒を表します.1秒=1000ミリ秒です.second()との違いはミリ秒であることと,何よりも実行時からの時間を表すことです.

プログラムが実行された瞬間を0秒として,時間が増えていきます.1000で割り算をすれば秒だけ抽出,1000.0で割り算をすれば秒に加えて小数点以下が表示されて,時間経ってる感が出ます.

void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  text(millis(),50,100);
  text(millis()/1000.0,50,200);
  text(millis()/1000,50,300);
}

frameCount

実行時からのフレーム数を表すシステム変数です.これは時間そのものではないですが,時間と共に増加しますし,値の増え方がsecond()millis()の中間くらいなので場面によっては使いやすいです.

void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  text(frameCount,50,100);
}

時間を0から計測しなおすテクニック

(本記事なんですが,これを書きたかったので書いているまであります.)

ある時間制限を設けているようなゲームで,ゲームオーバーになったとします.ゲームはリスタートできて,プレイヤーは何度も挑戦することになります.この時,時間制限は毎回0から数えるべきです.

時間計測系の処理は,基本的にmillis()を用いるのが簡単です.しかし,millis()は実行時から増える一方なので,「途中でまた0から始めたいとき」は,millis()だけでは対応できません.ここではその解決方法について紹介します.

解決方法といっても,複雑なものではありません.以下の構造で実現可能です.

int base_time = 0;
void setup(){
  size(500,500);
}

void draw() {
  int time = millis() - base_time;
  
  if(/*0から数え直したい条件*/){
    base_time = millis();
  }
}

ある変数を用いて,millis()-変数の形にするのがミソです.上記では変数名をbase_timeとしています.

実行した瞬間は,
millis()-変数 = millis()-0 = millis()
であるため,millis()そのものを表します.そして,0から数え直したい条件を満たせば変数にその時点でのmillis()を代入することで,その瞬間の式の値は
millis()-変数 = millis()-millis() = 0
となります.その後,また第1項目のmillis()は増加し続けるので,millis()-変数はまた増えていきます.

以下のサンプルは,画面をクリックすれば時間が0から数え直されるコードです.先ほどのコードでは,if(/*0から始めたいきっかけ*/)の中でbase_timeの更新処理をしていましたが,必ずしもif文の中で行うとは限らず,mouseClicked()のような関数の中でやる場合もあります.

int base_time = 0;
void setup(){
  size(500,500);
  textSize(50);
  fill(0);
}

void draw() {
  background(255);
  int time = millis() - base_time;
  text(time/1000.0, 50, 100);
  
}

void mouseClicked(){
  base_time = millis();
}

周期性:時間から角度へ

ここでは周期性と時間について扱います.様々な場面で使用する頻出のテクニックです.「周期性」というのは,ずっと見ているといつか同じ状態に戻ってくるようなものです.時計は一日中眺めていると同じ時間に戻ります.

周期的なものを表すには,「時間を角度とみなして三角関数の中に入れる」ということをします.例えばアナログ時計では,時間を長針の角度,分を短針の角度に変換していると考えられます.このような実装について具体例とともに見ていきます.

アナログ時計の短針の実装を例に 〜map()メソッド〜

時間から角度というように,ある単位の値を他の単位に変換したいときは良くあります.アナログ時計であれば,0~60という「分」である単位を,0~360度,もしくは0~2πラジアンという「角度」に変換したいということです.このような時に役立つのがmap()メソッドです.

map(変数, 元の単位の下限,元の単位の上限,新しい単位の下限,新しい単位の上限)
// 0~59を取る秒という単位を,0~2πという角度の単位に変換
rad = map(second(), 0, 59, 0, 2*PI)

このメソッドは,下限の上限の範囲の同じ割合のところに値を変換します.例えばsecond()の値が30なら,0~59の範囲では真ん中なので,変換後は0~2πの範囲での真ん中,つまりπに変換されます.

map()は変換後の数値を返すため,戻り値をそのままsin()やcos()の引数に入れることができます.以下の例では,1秒ごとに線が動いていきます.これはアナログ時計における短針の実装そのものです.

void setup(){
  size(500,500);
}

void draw() {
  background(255);
  // 秒を角度に変換
  float rad = map(second(), 0, 59, 0, 2*PI);
  // それを三角関数の引数に
  float x = width/2 + 100*cos(rad);
  float y = height/2 + 100*sin(rad);
  line(width/2, height/2, x, y);
}

f:id:gotutiyan:20200805113702p:plain
実行例

millis()やframeCountを角度に変換するには?

先ほど短針の実装をしましたが,ここでfloat rad = map(second(), 0, 59, 0, 2*PI);という変換をしていました.ただこれは,second()が0~59しか取らないことが分かっているから書けることです.millis()やframeCountは無限に増え続けるため,上限がありません.つまりmap()の第3引数の値が書けないんですね.

でも,あまり問題はありません.というのは,結局三角関数に入れてしまうからです.sinxのグラフを書けばわかるように,三角関数というのは,引数の値がどれだけ大きくなっても,1から-1の間をウロウロする関数です.なので,そもそもmap()は使わずに,millis()やframeCountを直接sin()の中に入れてしまえば良いです.これでも,周期性は実現できます.

f:id:gotutiyan:20200807191050p:plain
sin(x)のグラフ

ただし,「ちょうど1200ミリ秒で周期的にさせたい!」とか「ちょうど45フレームで周期的にさせたい!」というように,周期をすごく厳密に定める場合には,余りをとって,0~2πに変換する方法が考えられます.ある整数modで余りをとった時,その結果は0~mod-1になるので,map()を用いてこの範囲を0~2πの範囲に変換すれば良いです.先ほどの短針の実装も,second()に対してmod=60で余りを取っていると考えると同じパターンです.

void setup(){
  size(500,500);
}

void draw() {
  background(255);
  // ちょうど50フレーム周期にしたい!
  int mod = 50;
  // modの余りをみて,0~mod-1の範囲を0~2*PIの範囲へ変換
  float rad = map(frameCount%mod, 0, mod-1, 0, 2*PI);
  // それを三角関数の引数に
  float x = width/2 + 100*cos(rad);
  float y = height/2 + 100*sin(rad);
  line(width/2, height/2, x, y);
}

時間変数を自分好みの速さに調整する

時間変数の「増える速さ」に注目すると,hour()<minute()<second()<frameCount<millis()というような優劣がついています.時間をどんな処理に使うかにもよりますが,大体は思い通りの早さでは無いため,そのまま使える場面は少ないです.そんな時は,適当に掛け算や割り算をすることで調整します.

以下はframeCountを使う場合のサンプルです.

void setup(){
  size(500,100);
}

void draw() {
  background(255);
  ellipse(frameCount,50,30,30);
  ellipse(frameCount/2,50,30,30);
  ellipse(frameCount/4,50,30,30);
}

割る数が2倍になれば,当然速さは1/2になります.この一つ前で書いた周期性と関連づけるのであれば,周期が1周するのにかかる時間が2倍になるとも言えます.このように適当に割ったり掛けたりすることで,「目的の値の増え方」を得るというのは必須テクニックです.

おわりに

ここでは時間を扱う一般的なテクニックを紹介しました.他にもこんな処理の解説が欲しいということがあれば,コメントに書いていただけると追記するかもしれません.

またこの他にも,シーン遷移に関するテクニックを紹介した記事もあるのでよろしければご覧ください.

gotutiyan.hatenablog.com