[AS3]1000個のTweenに挑戦 < 俺俺TweenエンジンOreOreTweenを作ってみよう4

2010/07/15

こんにちは。きんくまです。

今回は、前回の課題となっていたEnterframeをひとつのクラスにまとめて最適化してみよう。です。

Event.ENTER_FRAMEは、各インスタンスごとに登録すると重くなる原因となります。
なので、UpdateManagerというクラスを作って、それだけがENTER_FRAMEを呼び出して、
そこに各Tweenのインスタンスを追加・削除することにしました。

今回作ったもの。
玉の数を1000個にしてみました。

 

追加・削除の具体的な仕組みは、各Tweenごとに一意となるキーをもたせます。
それをキーとしてManagerのObjectに登録します。
削除するときはそれをまたキーとして削除しました。

package kuma_de 
{
	import flash.display.Sprite;
	import flash.events.Event;
	/**
	 * ...
	 * @author KinkumaDesign
	 */
	public class UpdateManager extends Sprite
	{
		private static var _instance:UpdateManager;
		public static function get instance():UpdateManager
		{
			if (_instance == null) {
				_instance = new UpdateManager();
			}
			return _instance;
		}
		
		private var _tweens:Object;
		private var _isUpdating:Boolean;
		private var _tweenCount:int;

		
		public function UpdateManager() 
		{
			if (_instance == null){
				_tweens = {};
				_isUpdating = false;
				_tweenCount = 0;
			}
		}
		
		public function add(tween:OreOreTween, key:String):void
		{
			_tweens[key] = tween
			_tweenCount++;
			if (_isUpdating == false) {
				addEventListener(Event.ENTER_FRAME, update);
				_isUpdating = true;
			}
		}
		
		public function remove(tween:OreOreTween, key:String):void
		{
			for (var i:String in _tweens) {
				if (i == key) {
					_tweens[key] = null;
					delete _tweens[key];
					_tweenCount--;
				}
			}
			if (_tweenCount == 0) {
				removeEventListener(Event.ENTER_FRAME, update);
				_isUpdating = false;
			}
		}
		
		private function update(e:Event):void 
		{
			var tween:OreOreTween;
			for (var i:String in _tweens) {
				tween = _tweens[i];
				tween.update();
				if (tween.isTweenComplete) {
					_tweens[i] = null;
					delete _tweens[i];
					_tweenCount--;
				}
			}
			if (_tweenCount == 0) {
				removeEventListener(Event.ENTER_FRAME, update);
				_isUpdating = false;
			}
		}
	}

}

■OreOreTween

package  kuma_de
{
	import flash.events.Event;
	import flash.events.EventDispatcher;
	/**
	 * ...
	 * @author KinkumaDesign
	 */
	public class OreOreTween extends EventDispatcher
	{
		//easing
		public static const EASE_NONE:String = "easeNone";
		public static const EASE_IN_QUAD:String = "easeInQuad";
		public static const EASE_OUT_QUAD:String = "easeOutQuad";
		public static const EASE_IN_OUT_QUAD:String = "easeInOutQuad";
		public static const EASE_OUT_IN_QUAD:String = "easeOutInQuad";
		public static const EASE_IN_CUBIC:String = "easeInCubic";
		public static const EASE_OUT_CUBIC:String = "easeOutCubic";
		public static const EASE_IN_OUT_CUBIC:String = "easeInOutCubic";
		public static const EASE_OUT_IN_CUBIC:String = "easeOutInCubic";
		public static const EASE_IN_QUART:String = "easeInQuart";
		public static const EASE_OUT_QUART:String = "easeOutQuart";
		public static const EASE_IN_OUT_QUART:String = "easeInOutQuart";
		public static const EASE_OUT_IN_QUART:String = "easeOutInQuart";
		public static const EASE_IN_QUINT:String = "easeInQuint";
		public static const EASE_OUT_QUINT:String = "easeOutQuint";
		public static const EASE_IN_OUT_QUINT:String = "easeInOutQuint";
		public static const EASE_OUT_IN_QUINT:String = "easeOutInQuint";
		public static const EASE_IN_SINE:String = "easeInSine";
		public static const EASE_OUT_SINE:String = "easeOutSine";
		public static const EASE_IN_OUT_SINE:String = "easeInOutSine";
		public static const EASE_OUT_IN_SINE:String = "easeOutInSine";
		public static const EASE_IN_EXPO:String = "easeInExpo";
		public static const EASE_OUT_EXPO:String = "easeOutExpo";
		public static const EASE_IN_OUT_EXPO:String = "easeInOutExpo";
		public static const EASE_OUT_IN_EXPO:String = "easeOutInExpo";
		public static const EASE_IN_CIRC:String = "easeInCirc";
		public static const EASE_OUT_CIRC:String = "easeOutCirc";
		public static const EASE_IN_OUT_CIRC:String = "easeInOutCirc";
		public static const EASE_OUT_IN_CIRC:String = "easeOutInCirc";
		public static const EASE_IN_ELASTIC:String = "easeInElastic";
		public static const EASE_OUT_ELASTIC:String = "easeOutElastic";
		public static const EASE_IN_OUT_ELASTIC:String = "easeInOutElastic";
		public static const EASE_OUT_IN_ELASTIC:String = "easeOutInElastic";
		public static const EASE_IN_BACK:String = "easeInBack";
		public static const EASE_OUT_BACK:String = "easeOutBack";
		public static const EASE_IN_OUT_BACK:String = "easeInOutBack";
		public static const EASE_OUT_IN_BACK:String = "easeOutInBack";
		public static const EASE_IN_BOUNCE:String = "easeInBounce";
		public static const EASE_OUT_BOUNCE:String = "easeOutBounce";
		public static const EASE_IN_OUT_BOUNCE:String = "easeInOutBounce";
		public static const EASE_OUT_IN_BOUNCE:String = "easeOutInBounce";
		
		
		private var _target:*;
		private var _tweenObjects:Object;
		private var _updateCount:int;
		private var _maxUpdateCount:int;
		private var _isTweening:Boolean;
		public function get isTweening():Boolean
		{
			return _isTweening;
		}
		private var _isTweenComplete:Boolean;
		public function get isTweenComplete():Boolean
		{
			return _isTweenComplete;
		}
		private static var _serialID:int = 0;
		private static const _PRIOR_ID:String = "tween";
		private var _key:String;
		
		public static function getTweenValues(to:Number, from:Number, duration:Number = 1.0, easing:String = EASE_NONE):Array
		{
			var frameRate:int = 30;//フレームレート
			
			var values:Array = [];
			var segmentNum:int = Math.floor(frameRate * duration); //分割数
			
			var i:int;
			var t:Number;
			var dt:Number = 1 / segmentNum;
			var time:Number;
			var value:Number;
			for (i = 0; i <= segmentNum; i++) {
				t = dt * i; //tは0以上1以下の範囲で動く
				time = t * duration; //開始からの時刻(秒)
				
				value = Equations[easing](time, from, to - from, duration);
				values.push(value); 
			}
			return values;
		}
		
		public function OreOreTween(target:*, to:Object, duration:Number = 1.0, easing:String = EASE_NONE, delay:Number = 0)
		{
			_isTweening = false;
			_isTweenComplete = false;
			_tweenObjects = { };
			_target = target;
			_key = _PRIOR_ID + _serialID;
			_serialID++;
			
			for (var i:String in to) {
				_tweenObjects[i] = getTweenValues(to[i], target[i], duration, easing);
				_maxUpdateCount = _tweenObjects[i].length;
			}
		}
		
		public function start():void
		{
			if (!_isTweening) 
			{
				_isTweening = true;
				_updateCount = 0;
				var manager:UpdateManager = UpdateManager.instance;
				manager.add(this, _key);
			}
		}
		
		public function pause():void
		{
			if (_isTweening) 
			{
				var manager:UpdateManager = UpdateManager.instance;
				manager.remove(this, _key);
				_isTweening = false;
			}
		}
		
		public function reset():void
		{
			_updateCount = 0;
		}
		
		public function resume():void
		{
			if (!_isTweening) 
			{
				var manager:UpdateManager = UpdateManager.instance;
				manager.add(this, _key);
				_isTweening = true;
			}
		}
		
		public function update():void 
		{
			//tween終了
			if (_updateCount == _maxUpdateCount) 
			{
				_isTweenComplete = true;
				dispatchEvent(new Event(Event.COMPLETE));
				
			//tween値を適用
			}else {
				var tweenValues:Array;
				var value:Number;
				for (var i:String in _tweenObjects) {
					_target[i] = _tweenObjects[i][_updateCount];
				}
				_updateCount++;
			}
		}
	}
}

のこりのクラス

package 
{
	import com.bit101.components.HSlider;
	import com.bit101.components.Label;
	import com.bit101.components.List;
	import com.bit101.components.PushButton;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import kuma_de.OreOreTween;
	import net.hires.debug.Stats;
	
	/**
	 * ...
	 * @author KinkumaDesign
	 */
	[SWF(width="450",height="450",backgroundColor="0x000000",frameRate="30")]
	public class Main extends Sprite 
	{
		public var balls:Array;
		public var ballNum:int = 1000;
		public var tweens:Array;
		public var startPos:Circle;
		public var endPos:Circle;
		public var startBtn:PushButton;
		public var easingList:List;
		public var durationSlider:HSlider;
		public var opacitySlider:HSlider;
		
		public var ore:OreOreTween;

		public function Main():void 
		{
			if (stage) init();
			else addEventListener(Event.ADDED_TO_STAGE, init);
		}
		
		private function init(e:Event = null):void 
		{
			removeEventListener(Event.ADDED_TO_STAGE, init);
			addChild( new Stats() );
			
			startPos = new Circle(10, 0xE91283, 1, true);
			addChild(startPos);
			startPos.setPosition(50, 100);
			
			endPos = new Circle(10, 0xE91283, 1, true);
			addChild(endPos);
			endPos.setPosition(400, 200);
			
			
			startBtn = new PushButton(this, 250, 380, "start", startBtnClickHD);
			
			var label:Label = new Label(this, 100, 280, "EASING");
			label.textField.textColor = 0xffffff;
			var easingListItem:Array = getEasingListItem();
			easingList = new List(this, 100, 300, easingListItem);
			easingList.selectedIndex = easingListItem.length - 6;
			
			label = new Label(this, 250, 280, "DULATION");
			label.textField.textColor = 0xffffff;
			
			durationSlider = new HSlider(this, 250, 300, null);
			durationSlider.maximum = 2.0;
			durationSlider.minimum = 0.1;
			durationSlider.tick = 0.1;
			durationSlider.value = 1.1;
			
			label = new Label(this, 250, 320, "OPACITY");
			label.textField.textColor = 0xffffff;
			opacitySlider = new HSlider(this, 250, 340, null);
			opacitySlider.maximum = 1.0;
			opacitySlider.minimum = 0;
			opacitySlider.tick = 0.01;
			opacitySlider.value = 1;
		}
		
		private function startBtnClickHD(e:MouseEvent):void
		{
			var i:int;
			var ball:Circle;
			var tween:OreOreTween;
			if (ore == null) {
				var duration:Number = durationSlider.value;
				var easing:String = String(easingList.selectedItem);
				
				balls = [];
				tweens = [];

				for (i = 0; i < ballNum; i++) {
					ball = new Circle(3, 0xffffff * Math.random());
					addChild(ball);
					//初期位置へ
					var offset:Array = getRandomOffSet();
					ball.setPosition(startPos.x + offset[0], startPos.y + offset[1]);
					ball.alpha = 1;
					balls.push(ball);
					
					//使い方
					ore = new OreOreTween(ball, { 
						x:endPos.x,
						y:endPos.y,
						alpha:opacitySlider.value
						}, duration, easing);
					if (i == ballNum - 1) {
						ore.addEventListener(Event.COMPLETE, oreCompleteHD);
					}
					ore.start();
					tweens.push(ore);
				}
				
				startBtn.label = "PAUSE";
			}else if (ore.isTweening) {
				for (i = 0; i < ballNum; i++) {	
					tween = tweens[i];
					tween.pause();
				}
//				ore.pause();
				startBtn.label = "RESUME";
			}else if (!ore.isTweening) {
				for (i = 0; i < ballNum; i++) {	
					tween = tweens[i];
					tween.resume();
				}
//				ore.resume();
				startBtn.label = "PAUSE";
			}
		}
		
		private function getRandomOffSet():Array
		{
			var rad:Number = Math.PI * 2 * Math.random();
			var radius:Number = 100 * Math.random();
			return [Math.cos(rad) * radius, Math.sin(rad) * radius];
		}
		
		private function oreCompleteHD(e:Event):void 
		{
			ore.removeEventListener(Event.COMPLETE, oreCompleteHD);
			ore = null;
			var i:int;
			var ball:Circle;
			for (i = 0; i < ballNum; i++) {
				ball = balls[i];
				removeChild(ball);
				ball = null;
			}
			startBtn.label = "START";
		}
		
		private function getEasingListItem():Array
		{
			var ar:Array = [
			"easeNone",
			"easeInQuad",
			"easeOutQuad",
			"easeInOutQuad",
			"easeOutInQuad",
			"easeInCubic",
			"easeOutCubic",
			"easeInOutCubic",
			"easeOutInCubic",
			"easeInQuart",
			"easeOutQuart",
			"easeInOutQuart",
			"easeOutInQuart",
			"easeInQuint",
			"easeOutQuint",
			"easeInOutQuint",
			"easeOutInQuint",
			"easeInSine",
			"easeOutSine",
			"easeInOutSine",
			"easeOutInSine",
			"easeInExpo",
			"easeOutExpo",
			"easeInOutExpo",
			"easeOutInExpo",
			"easeInCirc",
			"easeOutCirc",
			"easeInOutCirc",
			"easeOutInCirc",
			"easeInElastic",
			"easeOutElastic",
			"easeInOutElastic",
			"easeOutInElastic",
			"easeInBack",
			"easeOutBack",
			"easeInOutBack",
			"easeOutInBack",
			"easeInBounce",
			"easeOutBounce",
			"easeInOutBounce",
			"easeOutInBounce"
			];
			return ar;
		}
	}
	
}

他のクラスは前回と同じなんで割愛します。
ソースコード一式をアップしましたので、興味があったら見てください。

作ってみての感想とか

とりあえず、今回で俺俺Tweenは完了とします。
あとは、フィルター処理やそれぞれの個性となるような機能を追加していってみたら面白いかもしれないです。

今回のOreOreTweenだけであれば、

・OreOreTween(Tweenのメインクラス)
・Equations(イージング)
・UpdateManager(最適化用のEnterframeをひきうけるユーティリティ)

の3つのクラスだけでできることがわかりました。
意外と少ない要素でできるんだなーと。
なんで、Tweenライブラリのクラス数が多いものは、基本部分ではなくそれぞれの独自機能に
多くの容量を割いているのではないかと思いました。
また、今回作ったものは1000個のSpriteでフレームレートがさがってきているので、
このあたりの最適化もいろいろと工夫されているんじゃないかと思いました。


自作iPhoneアプリ 好評発売中!
フォルメモ - シンプルなフォルダつきメモ帳
ジッピー電卓 - 消費税や割引もサクサク計算!

ページトップへ戻る