ActionScript 3.0で始めるオブジェクト指向スクリプティング

第38回z座標値に応じて重ね順を変える

前回の第37回「インスタンスの回転と重ね順」は、ふたつの面を前後に置いて、y軸で水平に回した。今回はさらに2面増やし、矩形の画像で四方を囲んで、最後は上下左右に回してみたい図1⁠。前回に続き、面の重ね順をどう決めるかが課題だ。

図1 四方に置いた面を上下左右に回す
図1 四方に置いた面を上下左右に回す

z座標値を調べる仕組みづくり

4面を水平に回すだけなら、前回のスクリプト2と同じように、面が納められた容れ物のSpiteインスタンスの回転角で並べ替えることもできる。しかし、垂直にも回すとなると、ことはそう単純ではない。そこで、4つの面それぞれのz座標で重ね順を決めることにしよう。前回のスクリプト2に手を加えていくことにする。

まず、面のどの位置の座標を調べるかが問題となる。[ライブラリ]のビットマップ(BitmapDataインスタンス)は、Bitmapオブジェクトの基準点が左上角になるように配置された第37回図2再掲の位置座標参照⁠⁠。しかし、左上角のz座標値を比べたのでは、それぞれの面の前後はわからない。

第37回図2 Spriteインスタンスの基準点が中心となるように面の位置を定める(再掲)
第37回図2 Spriteインスタンスの基準点が中心となるように面の位置を定める(再掲)

比べるなら、それぞれの面の中央の座標がよいだろう。容れ物のSpriteインスタンスの基準点は四方を囲む面の中心なので、各面とxまたはz軸との交点で決めるということだ図2⁠。ところが、BitmapDataクラスには位置座標のプロパティがない。つまり、面の左上角がBitmapオブジェクトの基準点であることは動かせない。

図2 各面の中央をxまたはz軸が貫く
図2 各面の中央をxまたはz軸が貫く図2 各面の中央をxまたはz軸が貫く

そのため、4つの面のBitmapオブジェクトは、それぞれSpriteインスタンスで包むことにする。そして、入れ子のBitmapオブジェクトの位置をずらすことによって、親Spriteインスタンスの基準点を面の中心に定める図3⁠。そうすれば、面のSpriteインスタンスのz座標でそれらの前後が決められる。これで面のz座標値を調べる仕組みが整った。

図3 Bitmapオブジェクトの位置はSpriteインスタンスの基準点が中心になるよう左上にずらす
図3 Bitmapオブジェクトの位置はSpriteインスタンスの基準点が中心になるよう左上にずらす

面をつくって返す関数の修正

つぎは、前回のスクリプト2に具体的な手を加えていく。第1に、面のBitmapオブジェクトをつくって返していた関数xCreateFace()だ。これを、Bitmapオブジェクトが入れ子になったSpriteインスタンスで返すよう修正する。引数の宣言はそのままだ。ただし、Spriteインスタンスの基準点が面の中央になるので、関数を呼出すときに渡す座標値がつぎのように変わる。なお、面のSpriteインスタンスを納める変数名も改めた。

// var frontBitmap:Bitmap = xCreateFace(Image0, -nUnit, -nUnit, -nUnit);
// var backBitmap:Bitmap = xCreateFace(Image1, nUnit, -nUnit, nUnit, 180);
var frontSprite:Sprite = xCreateFace(Image0, 0, 0, -nUnit);
var backSprite:Sprite = xCreateFace(Image1, 0, 0, nUnit, 180);

関数xCreateFace()には、以下のように手を加えた。戻り値のデータ型をSpriteで指定し、面のビットマップが納められたBitmapオブジェクトは、新たにつくるSpriteインスタンスで包む。Bitmapオブジェクトの位置は、面の中央にSpriteインスタンスの基準点がくるように、左上にずらす。そのうえで、引数値によりSpriteインスタンスの位置と回転角を定めた。

// function xCreateFace(myBitmapData:Class, nX:Number, nY:Number, nZ:Number, nRotationY:Number = 0):Bitmap {
function xCreateFace(myBitmapData:Class, nX:Number, nY:Number, nZ:Number, nRotationY:Number = 0):Sprite {
  var myBitmap:Bitmap = new Bitmap(new myBitmapData(0, 0));
  var faceSprite:Sprite = new Sprite();
  faceSprite.addChild(myBitmap);
  // myBitmap.x = nX;
  // myBitmap.y = nY;
  // myBitmap.z = nZ;
  // myBitmap.rotationY = nRotationY;
  myBitmap.x = -myBitmap.width / 2;
  myBitmap.y = -myBitmap.height / 2;
  faceSprite.x = nX;
  faceSprite.y = nY;
  faceSprite.z = nZ;
  faceSprite.rotationY = nRotationY;
  // return myBitmap;
  return faceSprite;
}

これで、前回のスクリプト2とムービーの見た目は変わらないものの、ふたつの面のSpriteオブジェクトがつくられ、容れ物の親Spriteインスタンス内に 配置される。この段階で[ムービープレビュー]を確かめる場合は、スクリプト中の面のSpriteオブジェクトの変数名を直す必要がある。また、面の重ね順を整える関数xSetOrder()は、前項で述べたとおり処理を大幅に書替えるので、一旦本体をコメントアウトしておこう。

// mySprite.addChild(frontBitmap);
// mySprite.addChild(backBitmap);
mySprite.addChild(frontSprite);
mySprite.addChild(backSprite);

function xSetOrder():void {
  /*
  var nTop:uint = mySprite.numChildren - 1;
  if (mySprite.rotationX > 90) {
    mySprite.setChildIndex(backBitmap, nTop);
  } else {
    mySprite.setChildIndex(frontBitmap, nTop);
  }
  */
}

面のz座標値で重ね順を入替える関数

第2に手を加える関数xSetOrder()は、面のSpriteインスタンスのz座標値により重ね順を入替えるよう定義し直す。ところが、ここで問題が生じる。面のインスタンスは親Spriteインスタンスに入れ子になっており、回すのはこの容れ物の親インスタンスだった。ということは、面のインスタンスの座標そのものは、1ピクセルたりとも動かないのだ。

このようなとき、インスタンスの基準とする座標空間を親インスタンス以外に変換することができる。それが、Transform.getRelativeMatrix3D()メソッドだ。インスタンスのDisplayObject.transformプロパティに対してこのメソッドを呼出すと、インスタンスのもつTransform.matrix3Dプロパティの値を、メソッドの引数に渡したDisplayObjectインスタンスから見た値に計算し直してくれる。

Transformオブジェクト.getRelativeMatrix3D(基準とするインスタンス)

Transform.getRelativeMatrix3D()メソッドは、Matrix3Dオブジェクトを返す。その座標値が欲しいときは、Matrix3D.positionプロパティを調べればよい。xyzの各プロパティをもったVector3Dオブジェクトが得られる。z座標値はVector3D.zプロパティだ。

面のインスタンスをz座標値順に並べ替えるには、第24回「インスタンスの管理と配列の並べ替え」インスタンスの重ね順を管理するで用いたArray.sort()メソッドが使える。そのためには、容れ物の親Spriteインスタンスから、面の子インスタンスを取出して配列に納めなければならない。親インスタンスの表示リストから、指定したインデックス番号の子インスタンスを取出すのはDisplayObjectContainer.getChildAt()メソッドだ。子インスタンスの数は、DisplayObjectContainer.numChildrenプロパティで調べられた。

親インスタンス.getChildAt(子インスタンスのインデックス)

定義し直した関数xSetOrder()は、以下のとおりだ。まず、容れ物である親インスタンスの表示リストから面の子インスタンスをすべて取出して、それらを新たな配列(faces_array)に納める[1]⁠。つぎに、その配列エレメントとなった子インスタンスをArray.sort()メソッドで並べ替える。引数には比較関数(compare())を渡して、インスタンスのz座標値の順にする。

比較関数compare()は、渡される子インスタンスのメインタイムライン(this)から見たMatrix3DオブジェクトをTransform.getRelativeMatrix3D()メソッドにより求めて、Matrix3D.positionプロパティでz座標値を調べる。

ただし、z座標値は奥向きが正で、表示リスト内のインデックスは手前ほど値が大きい。比較関数からの戻り値を決めるとき、注意する必要がある。

function xSetOrder():void {
  var faces_array:Array = new Array();
  var nChildren:uint = mySprite.numChildren;
  for (var i:uint = 0; i < nChildren; i++) {
    faces_array.push(mySprite.getChildAt(i));
  }
  faces_array.sort(compare);
  for (var j:uint = 0; j < nChildren; j++) {
    var faceSprite:Sprite = faces_array[j];
    mySprite.setChildIndex(faceSprite, j);
  }
}
function compare(a:Sprite, b:Sprite):Number {
  var nA:Number = a.transform.getRelativeMatrix3D(this).position.z;
  var nB:Number = b.transform.getRelativeMatrix3D(this).position.z;
  if (nA < nB) {
    return 1;
  } else if (nA > nB) {
    return -1;
  } else {
    return 0;
  }
}

前回のスクリプト2に上述の修正を加えたのが、以下のスクリプト1だ。この段階でのムービーの動きは、前回のスクリプト2と変わらない第37回図6再掲⁠⁠。しかし、中身は面のインスタンスの入れ子を使って、その重ね順をz座標により定めている。面を増やしたり、垂直回転を加える準備が整った。

スクリプト1 Sprite内にふたつのビットマップを置いてマウスポインタの位置に応じて水平に回す
// タイムライン: メイン
// [ライブラリ]のビットマップに[クラス]としてImage0とImage1を設定
var mySprite:Sprite = new Sprite();
var nX:Number = stage.stageWidth / 2;
var nY:Number = stage.stageHeight / 2;
var nUnit:Number = 100 / 2;
var frontSprite:Sprite = xCreateFace(Image0, 0, 0, -nUnit);
var backSprite:Sprite = xCreateFace(Image1, 0, 0, nUnit, 180);
var nDeceleration:Number = 0.3;
mySprite.z = 0;
var myMatrix3D:Matrix3D = mySprite.transform.matrix3D;
mySprite.x = nX;
mySprite.y = nY;
addChild(mySprite);
mySprite.addChild(frontSprite);
mySprite.addChild(backSprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xCreateFace(myBitmapData:Class, nX:Number, nY:Number, nZ:Number, nRotationY:Number = 0):Sprite {
  var myBitmap:Bitmap = new Bitmap(new myBitmapData(0, 0));
  var faceSprite:Sprite = new Sprite();
  faceSprite.addChild(myBitmap);
  myBitmap.x = -myBitmap.width / 2;
  myBitmap.y = -myBitmap.height / 2;
  faceSprite.x = nX;
  faceSprite.y = nY;
  faceSprite.z = nZ;
  faceSprite.rotationY = nRotationY;
  return faceSprite;
}
function xRotate(eventObject:Event):void {
  var nRotationY:Number = (mouseX - nX) * nDeceleration;
  myMatrix3D.appendTranslation(-nX, -nY, 0);
  myMatrix3D.appendRotation(nRotationY, Vector3D.Y_AXIS);
  myMatrix3D.appendTranslation(nX, nY, 0);
  xSetOrder();
}
function xSetOrder():void {
  var faces_array:Array = new Array();
  var nChildren:uint = mySprite.numChildren;
  for (var i:uint = 0; i < nChildren; i++) {
    faces_array.push(mySprite.getChildAt(i));
  }
  faces_array.sort(compare);
  for (var j:uint = 0; j < nChildren; j++) {
    var faceSprite:Sprite = faces_array[j];
    mySprite.setChildIndex(faceSprite, j);
  }
}
function compare(a:Sprite, b:Sprite):Number {
  var nA:Number = a.transform.getRelativeMatrix3D(this).position.z;
  var nB:Number = b.transform.getRelativeMatrix3D(this).position.z;
  if (nA < nB) {
    return 1;
  } else if (nA > nB) {
    return -1;
  } else {
    return 0;
  }
}
第37回図6 インスタンスの重ね順が正しく変わる(再掲)
第37回図6 インスタンスの重ね順が正しく変わる(再掲) 第37回図6 インスタンスの重ね順が正しく変わる(再掲)

4つの面を上下左右に回す

それでは、面はあとふたつ増やして四方を囲む4面とし、垂直の回転も加えて上下左右に回してみよう。実は、もう新たに学ぶべきことは何もない。ただ、処理を追加するだけだ。まず、面のSpriteインスタンスをふたつ増やし、それらを容れ物の親Spriteインスタンスに加える。

var frontSprite:Sprite = xCreateFace(Image0, 0, 0, -nUnit);
var backSprite:Sprite = xCreateFace(Image1, 0, 0, nUnit, 180);
var leftSprite:Sprite = xCreateFace(Image2, -nUnit, 0, 0, 90);   // 追加
var rightSprite:Sprite = xCreateFace(Image3, nUnit, 0, 0, -90);   // 追加
mySprite.addChild(frontSprite);
mySprite.addChild(backSprite);
mySprite.addChild(leftSprite);   // 追加
mySprite.addChild(rightSprite);   // 追加

つぎに、回転のアニメーションをさせるリスナー関数xRotate()には、垂直に回す処理が加わる。これは、第36回「Matrix3Dクラスの後から加える変換」スクリプト1とまったく同じだ。

function xRotate(eventObject:Event):void {
  var nRotationY:Number = (mouseX - nX) * nDeceleration;
  var nRotationX:Number = (mouseY - nY) * nDeceleration;   // 追加
  myMatrix3D.appendTranslation(-nX, -nY, 0);
  myMatrix3D.appendRotation(nRotationY, Vector3D.Y_AXIS);
  myMatrix3D.appendRotation(nRotationX, Vector3D.X_AXIS);   // 追加
  myMatrix3D.appendTranslation(nX, nY, 0);
  xSetOrder();
}

前掲スクリプト1にこれらの修正を加えたのが、つぎのスクリプト2だ。[ムービープレビュー]を確かめると、四方を囲む4つの面がマウスポインタの位置に応じて上下左右に回る(前掲図1⁠。

スクリプト2 四方に配置した面がマウスポインタの位置に応じて上下左右に回る
// タイムライン: メイン
// [ライブラリ]のビットマップに[クラス]としてImage0~Image3を設定
var mySprite:Sprite = new Sprite();
var nX:Number = stage.stageWidth / 2;
var nY:Number = stage.stageHeight / 2;
var nUnit:Number = 100 / 2;
var frontSprite:Sprite = xCreateFace(Image0, 0, 0, -nUnit);
var backSprite:Sprite = xCreateFace(Image1, 0, 0, nUnit, 180);
var leftSprite:Sprite = xCreateFace(Image2, -nUnit, 0, 0, 90);
var rightSprite:Sprite = xCreateFace(Image3, nUnit, 0, 0, -90);
var nDeceleration:Number = 0.3;
mySprite.z = 0;
var myMatrix3D:Matrix3D = mySprite.transform.matrix3D;
mySprite.x = nX;
mySprite.y = nY;
addChild(mySprite);
mySprite.addChild(frontSprite);
mySprite.addChild(backSprite);
mySprite.addChild(leftSprite);
mySprite.addChild(rightSprite);
addEventListener(Event.ENTER_FRAME, xRotate);
function xCreateFace(myBitmapData:Class, nX:Number, nY:Number, nZ:Number, nRotationY:Number = 0):Sprite {
  var myBitmap:Bitmap = new Bitmap(new myBitmapData(0, 0));
  var faceSprite:Sprite = new Sprite();
  faceSprite.addChild(myBitmap);
  myBitmap.x = -myBitmap.width / 2;
  myBitmap.y = -myBitmap.height / 2;
  faceSprite.x = nX;
  faceSprite.y = nY;
  faceSprite.z = nZ;
  faceSprite.rotationY = nRotationY;
  return faceSprite;
}
function xRotate(eventObject:Event):void {
  var nRotationY:Number = (mouseX - nX) * nDeceleration;
  var nRotationX:Number = (mouseY - nY) * nDeceleration;
  myMatrix3D.appendTranslation(-nX, -nY, 0);
  myMatrix3D.appendRotation(nRotationY, Vector3D.Y_AXIS);
  myMatrix3D.appendRotation(nRotationX, Vector3D.X_AXIS);
  myMatrix3D.appendTranslation(nX, nY, 0);
  xSetOrder();
}
function xSetOrder():void {
  var faces_array:Array = new Array();
  var nChildren:uint = mySprite.numChildren;
  for (var i:uint = 0; i < nChildren; i++) {
    faces_array.push(mySprite.getChildAt(i));
  }
  faces_array.sort(compare);
  for (var j:uint = 0; j < nChildren; j++) {
    var faceSprite:Sprite = faces_array[j];
    mySprite.setChildIndex(faceSprite, j);
  }
}
function compare(a:Sprite, b:Sprite):Number {
  var nA:Number = a.transform.getRelativeMatrix3D(this).position.z;
  var nB:Number = b.transform.getRelativeMatrix3D(this).position.z;
  if (nA < nB) {
    return 1;
  } else if (nA > nB) {
    return -1;
  } else {
    return 0;
  }
}

2面から4面に増やし、垂直回転を加える作業はあっけなかった。この実習からわかるように、まず仕組みや構成を初めによく考えておくことが大切だ。そしてつぎに、1面から2面、そして4面というように、簡単なつくりから、順を追って複雑にすることである。

フォーラムなどで、いきなり6面体を上下左右に回そうとして、どうしたらいいかわかりません、というような質問がある。しかも往々にして、スクリプトはどこかのサンプルのコピー&ペーストで、中身がわかっていなかったりする。遠回りなようでも、理解と試行をひとつひとつ着実に積重ねていくことが王道だろう。

3次元空間の扱いはこの後インスタンスというモノでなく、座標を使ったメモリ上の処理に移る。しかしその前に旬のお題として、Flash Professional CS5から加わったText Layout Frameworkによる新しいテキストの扱いを解説しよう。

今回解説した次のサンプルファイルがダウンロードできます。

おすすめ記事

記事・ニュース一覧