Progression4:SceneObjectでシーンの構造管理はしない

みなさんはもう手に入れましたか?ProgressionによるFlashコンテンツ開発ガイドブック
なんでも発売から二週間足らずで重版決定だそうで。おめでとうございます!

僕はと言うと、先週行われたProgression本を朗読する会に参加してきました。せっかく買った本なんだから読まなきゃね!
で、この朗読会、スーパー肩パッドさんとシナチクさんがTwitterで音頭をとって開催された企画でして、非常に面白くてためになりました。

まず14時から開始して19時半まで5時間半、ぶっとおしでProgression談義。その後居酒屋に移動して3時間またぶっとおしでFlash談義。
僕自身も大阪てら子でProgressionを勉強したのが2年前。出会って以来この2年間で覚えてるだけで15案件、実際には20案件以上は実践投入してきたほどハマりまくってるProgressionについて、ここまでディープに語り合える場と仲間がいなかったので(個人事業なので家内以外仕事のパートナーが居ないので孤独)ここまで時間を忘れて、ほぼ初対面の方々たちと語らいだのは生まれて初めての経験でした。

本当にいい刺激になりました。

で、いちおう自分がとってたノートもあるんだけど、皆さんの喋った内容がまとめきれていないので、せめて自分のネタだけでもまとめておこうと思ったわけです。

21日の僕の勉強会でもこれやります。
不思議なことに全く同じ記事を「クラススタイル向け」と「コンポーネント向け」で2回書きます。今回はその第一回目です。
第一回目は、「クラススタイル向け」に書いてみようと思います。

目的:SceneObjectでシーンの構造管理をしない

この記事の目的はこれにつきます。ちょっと何を言っているのかわからないと思いますが、大まじめです。
本やネット上のサンプルなどにも、SceneObjectを使ってaddSceneとかで子シーンを管理したりしていますし、クラススタイルに慣れてくると、シーンのloadやinit、gotoやunloadを駆使していろいろと複雑なシーン管理を行えるようになってくるのですが、あえてシーンの管理をほとんどしない方法を探るというやり方です。この方法が何故いいのかは勉強会でお話ししたいと思います。

シーンの構造はXMLで

具体的にどうシーン管理をSceneObjectではさせないかというと、シーンの構造はほぼ全てXMLで管理するようにします。
「な〜んだ。SceneObjectのaddSceneFromXMLだろ。知ってるよ」という方も沢山いらっしゃると思いますが、ここではさらに踏み込んでEasyCastingだけでシーンを管理することを目指します。

普段クラススタイルで作られてる方は「えーEasyCastingってコンポーネントスタイルで扱う簡単シーン管理でしょ。うちの案件はもうちょっと複雑なの。いろいろと出来ないと困る」と思うはずです。大丈夫です。

プロジェクトの種類は「クラス」でOK

クラススタイル派にとってEasyCastingを使う心配事は「こんな簡単機能使ってて、俺がしたいあれとかこれとかちゃんと出来るんだろうか」というところにあると思います。少なくとも僕はそうでした。

基本的にはプロジェクト設定のとき、種類は「クラス」でOKです。FlashDevelopやFlashBuilderの対応など、いつもどおりのプロジェクトで書き出せば先ずは準備はOKです。

サンプルを見てみる

先ずはこちらのサンプル(Prog4MimeticEasyCasting.zip)をダウンロードして解凍してみてください。

解凍して出来るファイル構成は

  • classes
    • casts
      • AbstractCast.as
    • common
      • AbstractSceneManager.as
    • display
      • AbstractSceneContainer.as
    • scenes
      • AbstractScene.as
    • Index.as

こうなっていると思います。
この中で大事なのはcasts.AbstractCasts.asと、scenes.AbstractScene.asと、Index.asの3つだけです。

ちなみに、プロジェクトがこういった構造になっている前提で話を進めます。
project

Index.as

Index.asを見てみましょう。これ以降、ソースがよく出てきますがソースの興味のある方は読んでもいいし、読まなくてもいいです。なぜなら、僕は自分のソースに全く自信が持てないので、嘘を書いてる可能性もあるし何より大事なのは考え方の説明のほうだと思っているからです。

package classes {
	import flash.net.URLRequest;
	import jp.progression.casts.*;
	import jp.progression.commands.display.*;
	import jp.progression.commands.lists.*;
	import jp.progression.commands.managers.*;
	import jp.progression.commands.media.*;
	import jp.progression.commands.net.*;
	import jp.progression.commands.tweens.*;
	import jp.progression.commands.*;
	import jp.progression.config.*;
	import jp.progression.data.*;
	import jp.progression.debug.*;
	import jp.progression.events.*;
	import jp.progression.loader.PRMLLoader;
	import jp.progression.scenes.*;
	import flash.events.*;
	import classes.common.*;
	import classes.scenes.*;

	/**
	 * ...
	 * @author ...
	 */
	public class Index extends CastDocument {
		AbstractScene;
		/**
		 * 新しい Index インスタンスを作成します。
		 */
		public function Index() {
			// 自動的に作成される Progression インスタンスの初期設定を行います。
			// 生成されたインスタンスにアクセスする場合には manager プロパティを参照してください。
			//super( "index", IndexScene, new WebConfig() );
			//PRMLLoaderにまかせるのでここではnullを入れておく
			super(null, null, new WebConfig);
		}

		/**
		 * SWF ファイルの読み込みが完了し、stage 及び loaderInfo にアクセス可能になった場合に送出されます。
		 */
		override protected function atReady():void {
			var prmlLoader:PRMLLoader = new PRMLLoader(stage);

			prmlLoader.addEventListener(Event.COMPLETE, function(e:Event):void {
				prmlLoader.removeEventListener(Event.COMPLETE, arguments.callee);
				// 開発者用に Progression の動作状況を出力します。
				Debugger.addTarget( manager );
				// 外部同期機能を有効化します。
				manager.sync = true;

				//違うところにナビゲーションしたらトップページに
				manager.addEventListener(ProcessEvent.PROCESS_ERROR, function(e:ProcessEvent):void {
					manager.goto(manager.syncedSceneId);
				});

				// 最初のシーンに移動します。
				manager.goto( manager.syncedSceneId );
			});

			prmlLoader.load(new URLRequest("casting.xml"));
		}
	}
}

基本的に大事なのは、コンストラクタ内では初期化を行わずに、atReadyの中でPRMLLoaderを使ってcasting.xmlを読み込んでいることだけです。

EasyCastingSceneもどきを作る

次にscenes.AvstractScene.asのほうです。こちらは潔いくらいEasyCastingSceneのパクリです。
ヘッダの記載に迷ったのが、ほとんど手を付けてないのでEasyCastingの表記を守りましたが、逆に「へぼくなってる。手を加えたのなら自分の表記にするべき」なのであれば、ヘッダーの部分は僕自身の名前に変えます。興味ある方はオリジナルのEasyCastingと読み比べて見てください。

※2010.05.21 公式の4.0.2bパッチに対応しました

/**
 * Progression 4
 * 
 * @author Copyright (C) 2007-2010 taka:nium.jp, All Rights Reserved.
 * @version 4.0.2b
 * @see http://progression.jp/
 * 
 * Progression Libraries is dual licensed under the "Progression Library License" and "GPL".
 * http://progression.jp/license
 */
package classes.scenes {
	import flash.display.Sprite;
	import flash.utils.Dictionary;
	import flash.utils.getDefinitionByName;
	import jp.nium.core.debug.Logger;
	import jp.nium.core.L10N.L10NNiumMsg;
	import jp.nium.utils.ObjectUtil;
	import jp.nium.utils.StringUtil;
	import jp.progression.commands.display.AddChild;
	import jp.progression.commands.display.AddChildAt;
	import jp.progression.commands.display.RemoveChild;
	import jp.progression.commands.lists.ParallelList;
	import jp.progression.core.L10N.L10NProgressionMsg;
	import jp.progression.core.PackageInfo;
	import jp.progression.events.DataProvideEvent;
	import jp.progression.events.SceneEvent;
	import jp.progression.executors.CommandExecutor;
	import jp.progression.Progression;
	import jp.progression.scenes.*;
	import classes.display.AbstractSceneContainer;
	import classes.common.AbstractSceneManager;
	import classes.casts.*;
	
	/**
	 * EasyCastingScene クラスは、拡張された PRML 形式の XML データを使用して ActionScript を使用しないコンポーネントベースの開発スタイルを提供するクラスです。
	 * 
	 * 
	 * @example 	 * // EasyCastingScene インスタンスを作成する
	 * var scene:EasyCastingScene = new EasyCastingScene();
	 * 
	 */
	public class AbstractScene extends SceneObject {
		AbstractCast;
		/**
		 * すべてのインスタンスを保持した Dictionary インスタンスを取得します。
		 */
		private static var _instances:Dictionary = AbstractSceneManager.getInstance().contentInstance;
		
		/**
		 * 現在表示しているインスタンスを保持した Dictionary インスタンスを取得します。
		 */
		private static var _displayingList:Dictionary = AbstractSceneManager.getInstance().displayingList;
		
		/**
		 * コンテナインスタンスを保持した Dictionary インスタンスを取得します。
		 */
		private static var _containers:Dictionary = AbstractSceneManager.getInstance().containers;
		
		
		
		
		
		/**
		 * 自身に移動した際に表示させる表示オブジェクトを保持した配列を取得します。
		 * 
		 */
		public function get casts():Array { return _casts.slice(); }
		private var _casts:Array;
		
		/**
		 * キャストオブジェクトのクラス名をキーとして、パラメータを保持した Dictionary インスタンスを取得します。
		 */
		private var _castParameters:Dictionary;
		
		
		
		
		
		/**
		 * 新しい EasyCastingScene インスタンスを作成します。
		 * Creates a new EasyCastingScene object.
		 * 
		 * @param name
		 * シーンの名前です。
		 * 
		 * @param initObject
		 * 設定したいプロパティを含んだオブジェクトです。
		 * 
		 */
		public function AbstractScene( name:String = null, initObject:Object = null ) {
			// 初期化する
			_casts = [];
			_castParameters = new Dictionary();
			
			// 親クラスを初期化する
			super( name, initObject );
			
			// Progression が CommandExecutor を実装していなければ例外をスローする
			if ( Progression.config.executor != CommandExecutor ) { throw new Error( Logger.getLog( L10NNiumMsg.getInstance().ERROR_017 ).toString( className, "CommandExecutor" ) ); }
			
			// イベントリスナーを登録する
			super.addEventListener( SceneEvent.SCENE_UNLOAD, _sceneUnload, false, 0, true );
			super.addEventListener( SceneEvent.SCENE_INIT, _sceneInit, false, 0, true );
			super.addEventListener( SceneEvent.SCENE_ADDED_TO_ROOT, _sceneAddedToRoot, false, 0, true );
			super.addEventListener( SceneEvent.SCENE_REMOVED_FROM_ROOT, _sceneRemovedFromRoot, false, 0, true );
		}
		
		
		
		
		
		/**
		 * XML データが EasyCasting 拡張 PRML フォーマットに準拠しているかどうかを返します。
		 * 
		 * 
		 * @param prml
		 * フォーマットを検査したい XML データです。
		 * 
		 * @return
		 * フォーマットが合致すれば true を、合致しなければ false となります。
		 * 
		 * 
		 * @example 		 * 
		 */
		public static function validate( prml:XML ):Boolean {
			prml = new XML( prml.toXMLString() );
			
			// コンテンツタイプを確認する
			switch ( String( prml.attribute( "type" ) ) ) {
				case "text/easycasting"			:
				case "text/prml.easycasting"	: { break; }
				case "text/prml.plain"			: { break; }
				default							: { return false; }
			}
			
			// 必須プロパティを精査する
			for each ( var cast:XML in prml..cast ) {
				if ( !String( cast.attribute( "cls" ) ) ) { return false; }
			}
			
			// PRML として評価する
			prml.@type = "text/prml.plain";
			
			return SceneObject.validate( prml );
		}
		
		
		
		
		
		/**
		 * この SceneObject インスタンスの子を PRML 形式の XML データから追加します。
		 * 
		 * 
		 * @param prml
		 * PRML 形式の XML データです。
		 * 
		 */
		override public function addSceneFromXML( prml:XML ):void {
			//  の cls を上書きする
			/*
			for each ( var scene:XML in prml..scene ) {
				scene.@cls = "jp.progression.scenes.EasyCastingScene";
			}
			*/
			
			// PRML のフォーマットが正しくなければ例外をスローする
			if ( !validate( prml ) ) { throw new Error( Logger.getLog( L10NNiumMsg.getInstance().ERROR_005 ).toString( "PRML" ) ); }
			
			// 親のメソッドを実行する
			super.addSceneFromXML( prml );
		}
		
		/**
		 * 保持しているデータを解放します。
		 * 
		 */
		override public function dispose():void {
			_casts = [];
			_castParameters = new Dictionary();
			
			// 親のメソッドを実行する
			super.dispose();
		}
		
		
		
		
		
		/**
		 * シーン移動時に目的地がシーンオブジェクト自身もしくは親階層だった場合に、階層が変更される直前に送出されます。
		 */
		private function _sceneUnload( e:SceneEvent ):void {
			// 対象がルートでなければ終了する
			if ( this != super.root ) { return; }
			
			// コンテナを取得する
			_containers[super.container] ||= new AbstractSceneContainer( this );
			var container:AbstractSceneContainer = _containers[super.container];
			
			// コマンドリストを作成する
			var removeChildList:ParallelList = new ParallelList();
			
			// すでに表示されている対象を検索する
			for ( var cls:String in _displayingList ) {
				// コマンドを追加する
				removeChildList.addCommand( new RemoveChild( container, _displayingList[cls] ) );
				
				// 登録から削除する
				delete _displayingList[cls];
			}
			
			// コマンドを追加する
			super.addCommand( removeChildList );
		}
		
		/**
		 * シーンオブジェクト自身が目的地だった場合に、到達した瞬間に送出されます。
		 */
		private function _sceneInit( e:SceneEvent ):void {
			// コンテナを取得する
			_containers[super.container] ||= new AbstractSceneContainer( this );
			var container:AbstractSceneContainer = _containers[super.container];
			
			// コンテナが登録されていなければ登録する
			if ( !super.container.contains( container ) ) {
				super.container.addChild( container );
			}
			
			// コマンドリストを作成する
			var addChildList:ParallelList = new ParallelList();
			var removeChildList:ParallelList = new ParallelList();
			
			// すでに表示されている対象を検索する
			for ( var cls:String in _displayingList ) {
				// 登録されていれば次へ
				if ( _castParameters[cls] ) { continue; }
				
				// コマンドを追加する
				removeChildList.addCommand( new RemoveChild( container, _displayingList[cls] ) );
				
				// 登録から削除する
				delete _displayingList[cls];
			}
			
			// 現在のシーンで必要な対象を追加する
			for ( var cast:String in _castParameters ) {
				// インスタンスを取得する
				var instance:Sprite = _displayingList[cast];
				
				// すでに表示されていれば次へ
				if ( instance ) { continue; }
				
				// インスタンスを取得する
				instance = _instances[cast];
				
				// プロパティを設定する
				ObjectUtil.setProperties( instance, _castParameters[cast] );
				
				// インデックスを取得する
				var index:String = _castParameters[cast].index;
				
				// コマンドを追加する
				if ( index ) {
					addChildList.addCommand( new AddChildAt( container, instance, parseInt( index ) ) );
				}
				else {
					addChildList.addCommand( new AddChild( container, instance ) );
				}
				
				// 表示中リストに登録する
				_displayingList[cast] = instance;
			}
			
			// コマンドを追加する
			super.addCommand( removeChildList, addChildList );
		}
		
		/**
		 * シーンオブジェクトが直接、またはシーンオブジェクトを含むサブツリーの追加により、ルートシーン上のシーンリストに追加されたときに送出されます。
		 */
		private function _sceneAddedToRoot( e:SceneEvent ):void {
			if ( super.dataHolder ) {
				super.dataHolder.addEventListener( DataProvideEvent.DATA_UPDATE, _update, false, 0, true );
			}
		}
		
		/**
		 * シーンオブジェクトが直接、またはシーンオブジェクトを含むサブツリーの削除により、ルートシーン上のシーンリストから削除されようとしているときに送出されます。
		 */
		private function _sceneRemovedFromRoot( e:SceneEvent ):void {
			if ( super.dataHolder ) {
				super.dataHolder.removeEventListener( DataProvideEvent.DATA_UPDATE, _update );
			}
		}
		
		/**
		 * 管理するデータが更新された場合に送出されます。
		 */
		private function _update( e:DataProvideEvent ):void {
			// データを取得する
			var xml:XML = new XML( super.toXMLString() );
			
			// cast を取得する
			_castParameters = new Dictionary();
			for each ( var cast:XML in xml.cast ) {
				var o:Object = {};
				
				// アトリビュートを取得する
				for each ( var attribute:XML in cast.attributes() ) {
					o[String( attribute.name() )] = StringUtil.toProperType( attribute );
				}
				
				_castParameters[String( cast.@cls )] = o;
			}
			
			// インスタンスを作成する
			_casts = [];
			for ( var clsPath:String in _castParameters ) {
				try {
					// クラスの参照を取得する
					var cls:Class = getDefinitionByName( clsPath ) as Class;
					
					// インスタンスを生成する
					_instances[clsPath] ||= new cls();
					
					// リストに追加する
					_casts.push( clsPath );
				}
				catch ( e:Error ) {
					if ( PackageInfo.hasDebugger ) {
						// 警告を表示する
						Logger.error( Logger.getLog( L10NProgressionMsg.getInstance().ERROR_018 ).toString( clsPath ) );
					}
					
					// 登録を破棄する
					delete _castParameters[clsPath];
				}
			}
		}
	}
}

あとはガンガンXMLを書いていく

ほかのクラスはどうでもいいです。
とりあえずバグが出なくなるように調整したクラスだったりしますから。

あとはProgressionシーンエディタでもいいですし、XMLを直接でもいいですし、PRMLをがっつり書いていきます。
がっつり書き終えたら、次の二つのことをしておきます。

  1. PRMLの属性を type=”text/prml.plain” にしておく
  2. sceneのcls属性を全部”classes.scenes.AbstractScene”にしておく

あとは動かすだけ!

たったこれだけでクラススタイルの自由な環境を維持したままシーンの構造をXMLにまるなげ出来る環境が整いました。
PRMLのtypeは”text/prml.plain”ですので、XMLでも個別に動かしたいシーンはclasses.scenes.AbstractSceneをオーバーライドしたカスタムSceneObjectでいつもどおりの開発も行えます。
もう少しコンポーネントとハイブリッドな開発を行いたければ、僕の一番のおすすめはPRMLをEasyCastingと同じようにcastタグを追加するスタイルです。

castタグのcls属性もクラスの指定なので、ここで独自のCastObjectを指定しておけば、クラススタイルの開発と変わることなくキャストによる開発を行えます。むしろSceneObjectをXML以外で触ることがほとんど無くなるので従来のクラススタイルより一歩進んでスマートに開発が行えるようになりました。

castタグ以外にも、ほかにも自由にXMLにタグを追加してdataHolderから値をとるなど、かなり柔軟で強力な開発を行えるスタイルなので、ばりばりクラスを使いこなす人ほど「コンポーネントスタイルとの連携なんて」と思わず是非とも実践して欲しい技の一つです。

ご意見ご質問は随時受け付けております。みんなでオレオレProgression開発スタイルを語り合おう!

Progression4:SceneObjectでシーンの構造管理はしない」への1件のフィードバック

コメントを残す

メールアドレスが公開されることはありません。