タスクチェーン

グラフを作っていると、「ある処理が終わった後、任意の時間を経過して次の処理を呼ぶ」という
ような処理を行いたい場合がよくあります。
例えば、線を書いた後5秒後に四角、さらに5秒後に円を書くというときは、

public function drawAll():Void{
   drawLine();
}
public function drawLine():Void{
  //線描画 
  this.lineTo(x,y); 
  setInterval(this,"drawRect",5000);
}
public function drawRect():Void{
  //四角描画 
  this.lineTo(x,y); 
  setInterval(this,"drawPie",5000);
}
public function drawPie():Void{
  //円描画 
  this.curveTo(x,y); 
}

という風に書いたりしますが、この記述方法の場合

  • 描画順番が変わると、各メソッドの修正が発生する
  • setIntervalのid管理が大変

という問題があります。

直感的にわかりやすいのが、おそらく次のような書き方だと思います。

public function drawAll():Void{
   drawPie();
   wait(5000);
   drawLine();
   wait(5000);
   drawRect();
   wait(5000);
   drawPie();
}
public function drawLine():Void{
  //線描画 
  this.lineTo(x,y); 
}
public function drawRect():Void{
  //四角描画 
  this.lineTo(x,y); 
}
public function drawPie():Void{
  //円描画 
  this.curveTo(x,y); 
}

これならば、描画順が変わったりしても変更はdrawAllのみに絞られますし、
かなり見通しはいいと思います。
がしかし、残念ながらActionScriptにはwait的な役割を持つ関数がありませんorz

ということで、次のような処理ができるクラスを導入してみました。

public function drawAll():Void{
   var tc:TaskChain = new TaskChain();
   tc.addTask("drawPie",this);
   tc.addTask("drawLine",this);
   tc.addTask("drawRect",this);
   tc.addTask("drawPie",this);
   tc.taskToTaskInterval = 5000;//タスクとタスクの間の秒数
   tc.execute();
}
public function drawLine():Void{
  //線描画 
  this.lineTo(x,y); 
}
public function drawRect():Void{
  //四角描画 
  this.lineTo(x,y); 
}
public function drawPie():Void{
  //円描画 
  this.curveTo(x,y); 
}

wait()のパターンよりはわかりにくいですが、初めよりは分かりいいのではないでしょうか?
できることもwait()のパターンと同じです。

以下にTaskChainのソースを上げておきます。
TaskChainと、その内部で使うTaskクラスで構成されています。

class net.souko105.as2util.TaskChain {
	// 最初のメソッドを呼び出す間隔
	public var startInterval:Number = 100;
	//処理と処理の間隔
	public var taskToTaskInterval:Number = 100;

	private var first:Task;
	private var currentObj:Task;	
	private var taskList:Array;
	
	static var chainMap:Object = new Object();
	private static var key:Number = 0;
	
	public function TaskChain(){
		taskList = new Array();		
	}
	//新規タスクを追加します。
	public function addTask(callMethod:String,target:Object):Void{
		
		var task:Task = new Task();
		task.callMethod = callMethod;
		task.target = target;
		if(first==null){
			first = task;
			currentObj = task;
		}else{
			var old:Task = currentObj;
			old.nextExecuteTask = task;
			currentObj = task;
		}
		task.nextInterval = taskToTaskInterval;
		taskList.push(task);
	}
	//タスク実行
	public function execute():Void{
		if(first==null){
			return;
		}
		//GC対象にならないように、staticのマップに格納
		var k = ++key;
		var dt = new Date();
		var mykey =	k+"_"+dt.getTime();
		chainMap[mykey] = this;

		var del:Object = new Object();
		del.key = mykey;
		del.delKey = function(){
			//実行終了後に削除
			TaskChain.chainMap[this.key] = null;
		}
		addTask("delKey",del);
		
		first.id = setInterval(this.first,"execute",startInterval);
	}	
}

class net.souko105.as2util.Task {
	
	public var id:Number;
	public var callMethod:String;
	public var target:Object;
	public var nextInterval:Number;
	public var preExecuteTask:Task;
	public var nextExecuteTask:Task;
	
	public function execute(){
		if(this.id !=null){
			clearInterval(this.id);
		}
		this.target[this.callMethod].call(this.target,this,preExecuteTask);
		
		if(this.nextExecuteTask != null){
			this.preExecuteTask = this;
			this.nextExecuteTask.id = setInterval(this.nextExecuteTask,"execute",this.nextInterval);
		}
	}
}

適当にパッケージ名を付けてパスに通せば動くと思います。
仕組みとしては、

  • addTaskが呼ばれると、新規でタスクを作る
  • 一番最初のタスクは、firstという変数に入れておく
  • 既にタスクが登録されている場合、直前のタスクに次のタスクを呼ぶ処理を追加する

という感じです。
キモはすべてのタスクをstaticのマップに格納しているところで、これを行わないと2番目以降の
タスクが実行されません。staticのマップに引っ掛けないと、1番目のタスクが終了した時点で
残りのタスクがGC対象になるようで、消えてしまいます(ちなみにデバックモードだと動きます)

前の処理の戻り値を次の処理の引数に出来たりすると、もう少し便利かもしれないですね。