サイトトップ

Director Flash 書籍 業務内容 プロフィール

Adobe Flash CS3 Professional ActionScript 3.0

□06 キーボードによる操作とArrayおよびObjectクラス

06-01 キーイベントを受取る
インスタンスの移動を矢印キーで行ってみましょう。上下左右の矢印キーでその方向に1ピクセルずつ動き、[shift]キーを同時に押せば移動幅が10ピクセルに変わるようにします。ちょうどFlashの[選択ツール]で選んだエレメントを、矢印キーで移動するときと同じインターフェースです。

キーを押すイベント ー InteractiveObject.keyDown
まずは、キーボードからキーを押したというイベントを受取らなければなりません。キーを押すイベントは、InteractiveObject.keyDown(KeyboardEvent.KEY_DOWN定数)です。MovieClipなどのタイムラインは、もちろんこのイベントを受取れます。

Tips 06-001■InteractiveObjectクラス
InteractiveObjectクラスはDisplayObjectのスーパークラスで、マウスやキーボードの操作などを扱います。MovieClipクラスはDisplayObjectのサブクラスですので、InteractiveObjectクラスを継承します(Word 04-001「継承」参照)。


Tips 06-002■キーを放すイベント
押したキーを放すイベントは、InteractiveObject.keyUp(KeyboardEvent.KEY_UP定数)です。

今回のインターフェースでは、キーを押し続けたときに動き続けるようにしたいので、InteractiveObject.keyDown(KeyboardEvent.KEY_DOWN)イベントが適しています。

では、キー操作で動かそうとするMovieClipインスタンスに、つぎのようなフレームアクションを記述すれば、キーを押したイベントが受取れるでしょうか(図06-001)。リスナー関数xKeyDown()は、テスト用にtrace()関数でイベントオブジェクトを[出力]することにしました。

addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  trace(eventObject);
}
図06-001■KeyboardEvent.KEY_DOWNイベントにリスナー関数を登録

リスナー関数は、trace()関数でイベントオブジェクトを[出力]する。

[ムービープレビュー]で確かめると、いくらキーを押しても[出力]パネルは開かず、何も表示されません。

フォーカスを当てる
インスタンスがキーイベントを受取るには、そのインスタンスにキーボードフォースが当たっていなければならないのです。キーボードフォーカスは、Stage.focusプロパティにターゲットとなるインスタンスを代入して設定します。

Stageオブジェクトには直接アクセスできませんので、DisplayObject.stageプロパティを介する必要があります。上記フレームアクションにつぎのステートメントを加えると、MovieClipインスタンスがキーイベントを受取れるようになります(図6-002)。

stage.focus = this;

[ムービープレビュー]を試してみると、MovieClipインスタンスの周囲にはフォーカスの当たっていることを示す黄色い矩形が表示されます。そして、キーボードのキーを押すと、[出力]パネルにイベントオブジェクトの情報が表示されます(図06-002)。

図06-002■フォーカスを当てるとキーイベントが受取れる

MovieClipインスタンスには、フォーカスを示す黄色い枠が表示される。

ところが、ひとたびステージ上をクリックすると、フォーカスが外れ、黄色い枠も消えます。キーイベントは、受取れなくなってしまいます。

イベントの受取り手
MovieClipインスタンスは、Flash Playerからさまざまなイベントを受取ります。メインタイムラインもまた、MovieClipインスタンスです。

メインタイムラインに置かれたMovieClipインスタンスは、メインタイムラインの子として階層構造を成します(図06-003)。そして、MovieClipシンボルには別のMovieClipインスタンスを入れ子にでき、階層構造がツリー状になることは01-04「ムービークリップのプロパティとターゲットパス」でご説明しました。

図06-003■タイムラインは階層構造を成す

[ムービーエクスプローラ]([ウィンドウ]メニュー)で確認すると、メインタイムラインの子としてMovieClipインスタンスが配置されている。さらに、別のMovieClipを入れ子にすることもできる。[ムービーエクスプローラ]については、[ヘルプ]の[Flashユーザーガイド] > [ワークスペース] > [Flashオーサリングパネルの使用] > [ムービーエクスプローラの使用]参照。

キーイベントは、これらインスタンスのうちフォーカスの当たったものが受取ります。では、MovieClipインスタンスの親であるメインタイムラインに、リスナー関数を登録するとどうでしょう。前述のMovieClipのフレームアクション(図06-002)を、メインタイムラインのフレームに移動します。この場合もStage.focusプロパティを設定して、フォーカスを当てるステートメントは必要です。

addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
stage.focus = this;
function xKeyDown(eventObject:KeyboardEvent):void {
   trace(eventObject);
}

[ムービープレビュー]を確認すると、黄色い枠は表示されず、キー入力に応じてイベントオブジェクトの情報が[出力]されます。しかし、ステージ上をクリックすれば、やはりMovieClipインスタンスの場合と同じくフォーカスは外れ、キーイベントが受取られなくなります。

実は、メインタイムラインには、さらに上層(親)があります。Flash Playerの表示階層のトップにあるのは、Stageインスタンスです。メインタイムラインは、Stageインスタンスの下層(子)になります。

なお、タイムラインに子として配置できるのは、MovieClipインスタンスのほか、[ダイナミックテキスト]や[テキスト入力]などのTextFieldインスタンスや、ボタンシンボルのインスタンスであるSimpleButtonインスタンス、その他DisplayObjectのサブクラスのインスタンスです。


Flash Playerの表示階層は、Stageをトップとしたツリー構造。

Stageインスタンスもまた、キーイベントを受取ることができます。しかも少し特別で、フォーカスを当てる必要がありません。ですから、Stageインスタンスにリスナー関数を登録すれば、フォーカスに関わりなくキーイベントを処理することが可能です。

Tips 06-003■Stageインスタンスとキーイベント
Stageインスタンスは、フォーカスなしにキーイベントを受取ります。したがって、他のインスタンスのキーイベントにもリスナー関数を登録し、そのインスタンスにフォーカスを当てれば、Stageとインスタンスの両方でキーイベントを受取ることになります。


Maniac! 06-001■StageクラスはInteractiveObjectの特別なサブクラス
Stageクラスも、MovieClipと同じく、InteractiveObjectのサブクラスです。したがって、DisplayObjectのサブクラスでもあります(前述Tips 06-001「InteractiveObjectクラス」参照)。

ただし、少し特別なクラスです。まず、前述(Maniac! 04-001-2「StageクラスのインスタンスとSingleton」)のとおり、インスタンスはFlash Playerにひとつだけ自動的に生成され、追加して作成することはできません。

また、InteractiveObjectやDisplayObjectなどのスーパークラスのプロパティやメソッドであっても、使えないものが少なくありません。たとえば、DisplayObject.xDisplayObject.yプロパティを設定することはできず、位置座標は変えられないのです(オンラインヘルプ[ActionScript 3.0のプログラミング] > [表示のプログラミング] > [表示オブジェクトの操作] > [Stageプロパティの設定]参照)。

以下のフレームアクション(スクリプト06-001)を、キー操作で動かすつもりのMovieClipインスタンスに設定すると、キーを押すたびにイベントオブジェクトの情報が[出力]されます。たとえば、左矢印キー[←]を押した場合の[出力]パネルの表示はつぎのようなものです。

[KeyboardEvent type="keyDown" bubbles=true cancelable=false eventPhase=2 charCode=0 keyCode=37 keyLocation=0 ctrlKey=false altKey=false shiftKey=false]
スクリプト06-001■キーを押すとイベントオブジェクトの情報を[出力]する

// MovieClip: キー操作で動かすインスタンス
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  trace(eventObject);
}

イベントオブジェクトの情報には、押したキーを示すコードや、[shift]キーを押していたかどうかを表すプロパティが含まれています。これらのプロパティを使って、MovieClipインスタンスの移動をコントロールしましょう。


06-02 KeyboardEventオブジェクトとキーコード
前節で試したフレームアクションで、イベントオブジェクトの[出力]結果を見ると、keyCodeshiftKeyというプロパティが存在します。前者が押したキーのコードを整数で返し、後者は[shift]キーを押していたかどうかをブール(論理)値で示します。

KeyboardEventオブジェクトのプロパティ
KeyboardEventオブジェクトがもつ、基本的なプロパティを下表06-001に掲げます。なお、前述(02-07「イベントのことをもう少し知ろう」)のとおり、Event.currentTargetプロパティは、イベントオブジェクトの[出力]結果の中には表示されません。

表06-001■KeyboardEventオブジェクトの基本的なプロパティ
プロパティ:データ型
altKey:Boolean [Alt](Windows)または[option](Macintosh)キーが押されているとtrue、押されていなかったらfalseを返します。
charCode:uint イベントが発生したキー入力のAsccii文字コードを整数で返します。
ctrlKey:Boolean [Ctrl](Windows)または[command](Macintosh)キーが押されているとtrue、押されていなかったらfalseを返します。
currentTarget:Object イベントリスナーが登録され、リスナー関数にイベントを伝えたインスタンスを返します。
keyCode:uint イベントが発生したキーのキーコードを整数で返します。
shiftKey:Boolean [shift]キーが押されているとtrue、押されていなかったらfalseを返します。
type:String 発生したイベント名を文字列で返します。

Tips 06-004■altKeyプロパティ
本稿執筆時のオンラインヘルプ[ActionScript 3.0コンポーネントリファレンスガイド]のKeyboardEvent.altKeyプロパティの項において、このプロパティが「将来の使用のために予約され」たもので(図06-004)、現在はサポートされていないかのように説明されているのは誤りです。

図06-004■ヘルプにおけるKeyboardEvent.altKeyプロパティの説明

正しくは、[Alt](Windows)または[option](Macintosh)キーが押されているかどうかをブール値で返す。

なお、前述LiveDocs(Column 03「LiveDocsオンラインヘルプ」)の英語版では修正されています。

前述のMovieClipに記述したフレームアクション(スクリプト06-001)で、リスナー関数xKeyDown()内のtrace()関数のステートメントをつぎのように書替えれば、押したキーのキーコードとその際[shift]キーが押されていたかどうかのブール(論理)値が[出力]パネルに表示されます。

trace(eventObject.keyCode, eventObject.shiftKey);

たとえば、[shift]キーは押さずに左矢印キーをタイプすると、以下のような[出力]が得られます。数値37は左矢印キーのキーコードです(2番目の値falseは、[shift]キーを押していないことを示します)。

37 false

なお、文字キーのキーイベントを正しく受取るには、半角英数字入力のモード(IMEはオフ)にしておく必要があります。

Tips 06-005■[キーボードショートカットを無効]
[ムービープレビュー]では、たとえば[Enter]または[return]キーや[.](ピリオド)、[,](カンマ)キーなどは、ショートカットとしてキーイベントがメニューに奪われます(図06-005)。そのため、インスタンスはキーイベントを受取ることができません。

図06-005■[制御]メニューの[キーボードショートカットを無効]

[Enter]または[return]キーや[.](ピリオド)、[,](カンマ)キーなどは、ショートカットキーに割当てられている。

[ムービープレビュー]の状態で[制御]メニューから[キーボードショートカットを無効]を選択すると、メニューのショートカットは無効になり、インスタンスがキーイベントを受取れるようになります(図06-005)。

キーコードを調べる
キーコードは、キーボードから入力する「文字」ではなく、「キー」自体に割当てられています。したがって、アルファベットの大文字や小文字を区別しません。キーコードの一覧は、ヘルプの[ActionScript 2.0 の学習] > [キーボードのキーとキーコードの値]に掲載されています。

Tips 06-006■Asccii文字コードはcharCodeプロパティ
タイプされた文字のAscciiコードは、KeyboardEvent.charCodeプロパティで調べることができます。文字コードですので、アルファベットの大文字と小文字は区別されます。ただし、実際に入力された文字そのものを調べている訳ではありません。

キー配列は日本語JISではなく、US配列にもとづきます。したがって、[shift]キーを押しながら数字キーをタイプした場合、[1]キーなら文字「!」と認識されます(JISとUSで同じ文字のため)が、[2]キーでは「"」でなくUS配列の「@」の文字コードが返されます。

Asccii文字コードの一覧も、キーコードと同じく、ヘルプの[キーボードのキーとキーコードの値]の項で確認できます。

キーコードはヘルプで調べてもよいですし、前掲のフレームアクションで実際にキーをタイプして確認することもできます。しかし、文字以外のキーについては、Keyboardクラスに多くの定数が定められています。今回対象となる矢印キーと[shift]キーの定数と値は、下表06-002のとおりです。

表06-002■Keyboardクラスのキー定数
定数 キー
Keyboard.DOWN 下矢印キー 40
Keyboard.LEFT 左矢印キー 37
Keyboard.RIGHT 右矢印キー 39
Keyboard.SHIFT [shift]キー 16
Keyboard.UP 上矢印キー 38

Tips 06-007■keyCodeとshiftKeyプロパティ
KeyboardEvent.keyCodeプロパティは、今イベントが発生したキーのキーコードを返します。[shift]キーについても、そのキーイベントが発生すれば、キーコードの整数16が値となります。

[shift]キーを押さえたまま他のキーをタイプしてイベントが発生すると、今度はそのキーコードがKeyboardEvent.keyCodeプロパティの値として返されます。しかし、KeyboardEvent.shiftKeyプロパティがtrueになりますので、[shift]を押し続けていることが判別できます。

したがって、今回のスクリプトでは、KeyboardEvent.shiftKeyの値を用いますから、[shift]キーのキーコード(定数)は使う必要がありません。

表06-002に示した整数値は、もちろんキーコードです。しかし、キーコードの整数を直接使わずKeyboardクラスの定数を用いた方が、イベントリスナーの登録でイベントに定数を指定するのと同じく(02-07「イベントのことをもう少し知ろう」参照)、スクリプトのシンタックスエラーを防ぐことができ、確実性は増します。

さらに、定数でKeyboard.LEFTと記述した方が、単に整数37と指定するより、左矢印キーを示すことが明らかで、その分誤りも減らすことができるでしょう。


06-03 矢印キーでインスタンスを移動する
キーイベントを捉えて、押されたキーの情報を取得することはできました。また、キーコードから、どのキーが押されたのかを確認する方法もわかりました。すると、矢印キーでインスタンスを動かすスクリプトは、すでに学習した条件判定の練習問題として丁度よさそうです。

矢印キーの方向に1ピクセルずつ移動する ー if/else ifステートメント
[shift]キーで移動ピクセル数を増やすのは一旦脇に置き、矢印キーの方向にインスタンスが1ピクセルずつ動かすスクリプトを考えましょう。関数xMove()を定義して、リスナー関数xKeyDown()から、キーコードを引数に渡して呼出すことにします。関数xMove()は、キーコードが矢印キーであれば、その方向にインスタンスを移動するのです。

stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var bShiftKey:Boolean = eventObject.shiftKey;
  xMove(nKeyCode, bShiftKey);
}

なお、KeyboardEvent.shiftKeyプロパティの値も後で必要になるはずですから、関数xMove()の第2引数として渡しておくことにします。関数xMove()は、渡されたキーコードが矢印キーのいずれかであるかを、if/else ifステートメントで判定して処理すればよいだけです(スクリプト06-002)。

スクリプト06-002■矢印キーの方向に1ピクセルずつ移動する

// MovieClip: キー操作で動かすインスタンス
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var bShiftKey:Boolean = eventObject.shiftKey;
  xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  if (nKeyCode == Keyboard.LEFT) {
    x -= 1;
  } else if (nKeyCode == Keyboard.RIGHT) {
    x += 1;
  } else if (nKeyCode == Keyboard.UP) {
    y -= 1;
  } else if (nKeyCode == Keyboard.DOWN) {
    y += 1;
  }
}

上記スクリプト06-002の関数xMove()内にはelseステートメントはありませんので、矢印キー以外のキーコードが渡されたときには、何もせずに条件判定の処理を抜けます。[ムービープレビュー]で試すと、上下左右の矢印キーを押せばその方向にインスタンスが1ピクセルずつ移動します。

Tips 06-008■ブラウザのコンテンツにおけるキー操作
スクリプト06-002のようなキーで操作するコンテンツは、[パブリッシュプレビュー]などブラウザで閲覧した場合、すぐにキー入力してもキーイベントを受取りません。それは、ブラウザウィンドウ内のSWFが、フォーカスをもっていないからです。

ブラウザウィンドウのSWFにフォーカスを与えるもっとも簡単で確実な方法は、ユーザーにSWF上をクリックしてもらうことです。コンテンツを開始する際に、[スタート]ボタンを置いてそれをクリックさせるといった工夫が考えられます。

[shift]キーで移動ピクセル数を増やす
それではつぎに、[shit]キーを押していたら移動ピクセル数を増やすという処理の追加です。そこで、新たにxGetPixels()という関数を定義して、引数にKeyboardEvent.shiftKeyの値を渡したうえで、移動すべきピクセル数を返してもらうことにします。

そして、関数xMove()におけるif/else ifステートメントで、座標のプロパティに対する代入式の右辺に関数xGetPixels()の呼出しを指定します。たとえば、ifステートメント(左矢印キーを押したとき)のDisplayObject.xプロパティへの代入式は、つぎのように記述すればよいのです。

x -= xGetPixels(bShiftKey);

関数xGetPixels()の行うべき処理は、きわめて簡単です。引数として渡されたKeyboardEvent.shiftKeyの値がtrueなら(つまり[shift]キーを押していたら)10を、そうでなければ1を返すというだけです。簡単なif/elseステートメントで記述することが可能でしょう。

けれども、ここでは値を返す条件判定の処理ということで、条件演算子?:を使ってみることにします(Tips 05-008「条件演算子?:を使う」参照)。記述がシンプルになることと、条件演算子?:を使う練習という以外には、if/elseステートメントを用いなかった理由はとくにありません。

条件演算子?:は、以下のシンタックスで記述します。そして、条件の評価がtrueとされると式1、falseであれば式2の値を返します。

条件 ? 式1 : 式2

返された値は、変数などに代入することができます。たとえば、Boolean型の変数bShiftKeyが与えられた場合、変数値がtrueなら10、falseであれば1を、int型で宣言した変数nPixelsに代入するステートメントはつぎのとおりです。

var nPixels:int = bShiftKey ? nShiftMove : nMove;

それでは、前記スクリプト06-002に、関数xGetPixels()の定義を加えます。矢印キーを単独で押した場合の移動ピクセル数1と、[shift]キーと組合わせた場合の値10は、それぞれ変数として宣言しておくことにします(スクリプト06-003)。

スクリプト06-003■矢印キーの方向に1ピクセル/+[shift]で10ピクセルずつ移動する

// MovieClip: キー操作で動かすインスタンス
var nMove:int = 1;   // 矢印キー単独の移動ピクセル数
var nShiftMove:int = 10;   // 矢印+[shift]キーの移動ピクセル数
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var bShiftKey:Boolean = eventObject.shiftKey;
  xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  if (nKeyCode == Keyboard.LEFT) {
    x -= xGetPixels(bShiftKey);
  } else if (nKeyCode == Keyboard.RIGHT) {
    x += xGetPixels(bShiftKey);
  } else if (nKeyCode == Keyboard.UP) {
    y -= xGetPixels(bShiftKey);
  } else if (nKeyCode == Keyboard.DOWN) {
    y += xGetPixels(bShiftKey);
  }
}
function xGetPixels(bShiftKey:Boolean):int {
  var nPixels:int = bShiftKey?nShiftMove:nMove;
  return nPixels;
}

[ムービープレビュー]で試すと、矢印キーでその方向に1ピクセルずつ動き、[shift]キーを加えれば10ピクセル単位で移動します(図06-006)。

図06-006■矢印キーの方向に1ピクセル/+[shift]で10ピクセルずつインスタンスが移動する

[shift]キーを押すと、移動ピクセル数が増える。

Maniac! 06-002■関数として切り分ける利点
処理を関数として小分けすることは一般に、(1)処理の流れと組立てが明らかになり、(2)バグの発見や修正をしやすく、(3)機能の追加・拡張にも対応は容易になるという利点があります。

前記スクリプト06-003のxGetPixels()は、関数にすることの意味がわかりやすい例です。関数xGetPixels()を定義せず、処理を直接xMove()に記述するとどうなるでしょう。[shift]キーを押したかどうかによる移動ピクセル数の計算は、関数xMove()内の最初に行っておくと整理はしやすいです。

function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  var nPixels:int = bShiftKey?nShiftMove:nMove;
  if (nKeyCode == Keyboard.LEFT) {
    x -= nPixels;
  // [後略]...

しかしこの場合、押されたのが矢印キーでなくても、変数bShiftKeyの値を条件判定して、変数nPixelsに値を代入する処理は行われます。矢印キーが押されないかぎり、(矢印キーの判定以外)実質的に何もしなくてよいはずなので、この処理は無駄です。

処理の効率を考えれば、移動ピクセル数の計算は、if/else ifステートメント内に書く方がよいでしょう。すると、つぎのような記述になります。

function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  if (nKeyCode == Keyboard.LEFT) {
    x -= bShiftKey?nShiftMove:nMove;
  } else if (nKeyCode == Keyboard.RIGHT) {
    x += bShiftKey?nShiftMove:nMove;
  // [後略]...

処理効率の問題はなくなりました。しかし、もし移動の動作に問題が生じたときは、if/else ifステートメントの条件演算子?:の式をひとつひとつ確認しなければならなくなります。移動ピクセル数の計算方法が変わったり、追加する処理が発生したときも、対応しにくい構成となります。

したがって、わずか2行のステートメントであっても、関数xGetPixels()は定義した方がよいのです。


06-04 switchステートメントを使う
この06章で実現するコンテンツの動作は、スクリプト06-003で完成です。しかし、同じ動作をするスクリプトは、ひとつではありません。

もちろん、特定のプロジェクトの今の仕様だけを考えるのでしたら、問題なく動きさえすればそれで差支えありません。けれども、仕様には修正や追加が入るかもしれません。あるいは、そのスクリプトを他のプロジェクトにも流用できれば、生産性が上がります。

そのような先を見据えたスクリプティングを行うには、複数のやり方を考え、それぞれの得失を知っておくことが役に立つでしょう。少し手間がかかっても、汎用性のあるスクリプトが書ければ、全体としての効率を向上させることができます。


今少しだけ手間をかければ、後で楽になる。キリギリスでなく、アリになろう!

switchステートメントのシンタックス
前記スクリプト06-003の関数xMove()において、if/else if条件の左辺はいずれも関数の引数nKeyCodeで固定です。ひとつの式の値を、複数の式と等価比較する処理には、switchステートメントを使うことができます。switchは、caseステートメントと組合わせて使います。

以下のようなシンタックスで、switchステートメントに指定した式が、caseステートメントの式の値と等しいかどうかを比較します。switchステートメントの式0が、caseステートメントの最初の式1に等しければステートメント1、2番目の式2と等しいときはステートメント2が、N番目の式Nと等価であればステートメントNが実行されます。いずれのcaseステートメントの式とも一致しなかったときは、defaultのステートメントが処理されます。

switch (式0) {
  case 式1 :
    ステートメント1;
  case 式2 :
    ステートメント2;
  ...
  case 式N :
    ステートメントN;
  default :
    ステートメントDefault;
}

ただし、ひとつ大きな注意点があります。それは、if/else if/elseステートメントと異なり、caseステートメントは処理を抜けないということです。上記のswitchに指定した式0が最初のcaseステートメントの式1と等しければ、ステートメント1だけでなくステートメント2以降のすべてのcaseステートメントが実行されてしまいます。その際は、2番目のcaseステートメント以降に指定した式との等価比較は行われません。

たとえば、スクリプト06-003の関数xMove()の処理をswitchステートメントで書替えて、左右矢印キーの処理をつぎのように書いたとしましょう。試してみると、右矢印キーは動作するものの、左矢印キーを押してもインスタンスが動きません。

function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  switch (nKeyCode) {
    case Keyboard.LEFT :
      x -= xGetPixels(bShiftKey);
    case Keyboard.RIGHT :
      x += xGetPixels(bShiftKey);
  }
}

それは、左矢印キーが押され、switchステートメントに指定した変数nKeyCodeの値が最初のcaseステートメントの式の値Keyboard.LEFTと一致したとき、インスタンスの座標を左に動かす処理に加えて、つぎのcaseステートメントの右に移動する処理が行われてしまうからです。

caseステートメントを実行した後switchステートメントの処理を抜けるには、そのためのステートメントbreakを明示的に記述する必要があります。

Tips 06-009■caseステートメントが処理を抜けない利点
caseステートメントが、breakステートメントを書かないかぎり処理を続けてしまうという仕様には、何か利点はあるでしょうか。たとえば、左右の矢印キーに加えて、アルファベットの[A](キーコード65)と[D](キーコード68)のキーでも左右の移動ができるようにしたいときには、スクリプトをつぎのように記述することができるのです。

function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  switch (nKeyCode) {
    case Keyboard.LEFT :
    case 65 :   // [A]キー
      x -= xGetPixels(bShiftKey);
      break;   // 処理を抜ける
    case Keyboard.RIGHT :
    case 68 :   // [D]キー
      x += xGetPixels(bShiftKey);
      break;   // 処理を抜ける
  }
}

if/else ifステートメントをswitchステートメントで書替える
前記スクリプト06-003を、switchステートメントにより書替えると、つぎのようになります(スクリプト06-004)。defaultは、とくに処理が必要なければ、省略して差支えありません。

スクリプト06-004■矢印キーの方向にインスタンスを移動する ー switchステートメント

// MovieClip: キー操作で動かすインスタンス
var nMove:int = 1;
var nShiftMove:int = 10;
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var bShiftKey:Boolean = eventObject.shiftKey;
  xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  switch (nKeyCode) {
    case Keyboard.LEFT :
      x -= xGetPixels(bShiftKey);
      break;
    case Keyboard.RIGHT :
      x += xGetPixels(bShiftKey);
      break;
    case Keyboard.UP :
      y -= xGetPixels(bShiftKey);
      break;
    case Keyboard.DOWN :
      y += xGetPixels(bShiftKey);
      break;
  }
}
function xGetPixels(bShiftKey:Boolean):int {
  var nPixels:int = bShiftKey?nShiftMove:nMove;
  return nPixels;
}

いちいちbreakステートメントが入るのは気になるものの、キーコードの条件判定はif/else ifステートメントより見やすいでしょう。キーの種類が追加されたり、変更されたりした場合にも、スクリプト全体の構成が把握しやすいと思われます。[ムービープレビュー]で試せば、前のスクリプト06-003と同じように矢印キーと[shift]キーで、インスタンスを移動することができます。

ところで、下矢印キー(Keyboard.DOWN)を押した場合の最後のcaseステートメントに、breakステートメントは必要でしょうか。このスクリプト06-004についてのみいえば不要です。しかし、後で他のキーが加わるかもしれませんし、caseステートメントの順番を入替える可能性もあります。そうした場合に備えて、予めbreakステートメントを加えておくことは意味があります。

Tips 06-010■defaultステートメントは任意
ヘルプの[switchステートメント]の項を見ると、「switchステートメントには、どのcaseステートメントも式に一致しない場合に実行するデフォルトケースを指定する必要があります」と説明されています。しかし、本文に述べたとおり、defaultステートメントの記述は任意です。

実際、ヘルプの[defaultステートメント]の項には、「switchステートメントにdefaultケースステートメントは必須ではありません」と明記されています。

なお、defaultステートメントは、switchステートメントの最後に置く必要はありません。とはいえ、特別な理由がないかぎりは、最後に記述した方がわかりやすいでしょう。


Tips 06-011■switchステートメントの等価比較
switchステートメントにおける等価比較は、厳密な等価演算子===(Tips 05-002「等価と厳密な等価」参照)と同じく、データ型も含めて等しいかどうかを評価します。


Maniac! 06-003■switchステートメントに指定できる式
Javaでは、switchステートメントに指定できるのは整数型の式にかぎられているようです。しかし、ActionScriptには、そのようなデータ型の制限はありません。


06-05 Arrayインスタンス(配列)を使う
キーコードを扱う手法を、もうひとつご紹介します。いわゆる条件判定はしません。Arrayクラスのインスタンスである配列を使って、キーコードを処理します。

Arrayインスタンスの操作
配列とは、Arrayクラスのインスタンスです。配列には、複数の値に通し番号をつけて格納することができます。いわば値の容れ物となるのが、Arrayインスタンスなのです。ひとつのArrayインスタンスはひとつの変数に格納されながらも、そのインスタンスに複数の値が収められます。


配列は容れ物。その中に、整理番号のついた値を収める。

Arrayインスタンスは、new演算子によりコンストラクタ(Word 03-001「コンストラクタ」参照)を呼出して生成することができます。Arrayインスタンスに用いる接尾辞は、"_array"です。

var my_array:Array = new Array();

配列(Arrayインスタンス)に値を追加するには、Array.push()メソッドを用います。引数に指定した値が、配列にその要素(エレメント)として追加されます。

my_array.push("日曜日");
my_array.push("月曜日");
my_array.push("火曜日");
my_array.push("水曜日");
my_array.push("木曜日");
my_array.push("金曜日");
my_array.push("土曜日");

Array.push()メソッドは、行列に並ぶように、引数に指定した値を配列の最後尾に加えていきます。エレメント(要素)には、0から始まる整数の通し番号であるインデックスが与えられます。値を参照するには、配列アクセス演算子[](角括弧)を添えて、エレメントのインデックスを指定します。

trace(my_array[0]);   // 出力: 日曜日

Date.dayプロパティは、曜日を0から始まる整数で返します(03-04「Dateクラスで日付を調べる」参照)。したがって、つぎのフレームアクションで、今日の曜日を[出力]することができます(図06-007)。

var today_str:String = my_array[new Date().day];
trace(today_str);

trace()関数の引数に配列インスタンスそのものを指定すると、配列エレメントが順番にカンマ区切りで[出力]パネルに表示されます(図06-007)。

trace(my_array);   // 出力: 日曜日,月曜日,火曜日,水曜日,木曜日,金曜日,土曜日
図06-007■配列エレメントとインスタンスの[出力]

配列エレメントは、配列アクセス演算子[]にインデックスを指定して取得する。配列インスタンスを[出力]すると、エレメントがカンマ区切りで表示される。

Arrayインスタンスは、new演算子でコンストラクタを呼出すことなく、直接記述してインスタンスを作成することができます。その場合、配列は角括弧[]で表します。つぎのステートメントは、新規の空のArrayインスタンス(配列)を作成して、変数my_arrayに格納します。

var my_array:Array = [];

さらに、値を角括弧[]内にカンマ区切りで記述すれば、値の入った配列をつくることもできます。このように直接記述された値は、「リテラル」(Word 03-003「リテラル」参照)と呼びました。Arrayインスタンスは、リテラルで作成することができるのです。なお、配列のエレメント(要素)数つまり長さは、Array.lengthプロパティで調べることができます。

var my_array:Array =
["日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"];
trace(my_array[0]);   // 出力: 日曜日
trace(my_array.length);   // 出力: 7

キーコードで配列に値を設定
配列アクセス演算子[](角括弧)は、値を調べるだけでなく、値を設定することもできます。また、エレメントのインデックスは、連番である必要はありません。飛ばしたインデックスの値を調べると、未定義であることを示すundefinedが返されます。

var my_array:Array = new Array();
my_array[2] = "two";
trace(my_array[0]);   // 出力: undefined
trace(my_array[1]);   // 出力: undefined
trace(my_array[2]);   // 出力: two
trace(my_array);   // 出力: ,,two
trace(my_array.length);   // 出力: 3

Maniac! 06-004■配列のインデックス
配列に値を設定すると、その値にインデックスを与えてインスタンスにメモリします。ですから、インデックスの連番を飛ばして値を設定しても、飛ばされたインデックスにはとくにメモリが割かれる訳ではありません。つまり、空の配列の100万番目のインデックスに値を設定しても、999,999のインデックスが無駄にメモリを消費することはないのです。

なお、Array.lengthプロパティは、値の設定された最後のエレメントのインデックスに1加算した値(インデックスは0から始まるので)を返します。したがって、飛ばされた連番も数に含まれますので、このプロパティを「エレメント数」とするのは、厳密な表現とはいえないでしょう。そのため、本書では配列の「長さ」と呼ぶことにします。

キーコードは整数で、値はすべてユニークです。そのため、配列で管理しやすい値だといえます。MovieClipインスタンスを左右の矢印キーで1ピクセルずつ動かす場合には、つぎのようなフレームアクション(スクリプト06-005)で実現できます。

スクリプト06-005■矢印キーで左右に1ピクセルずつインスタンスを移動する ー 配列を利用

// MovieClip: キー操作で動かすインスタンス
var keys_array:Array = new Array();
keys_array[Keyboard.LEFT] = -1;
keys_array[Keyboard.RIGHT] = 1;
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var bShiftKey:Boolean = eventObject.shiftKey;
  xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  var nPixels:Number = keys_array[nKeyCode];
  if (nPixels) {
    x += nPixels;
  }
}

配列keys_arrayには、左右矢印キーのキーコード(Keyboard.LEFTKeyboard.RIGHT)をインデックスに指定して、それぞれ水平方向の移動ピクセル数(-1と1)を格納しました。すると、関数xMove()の処理は、きわめてシンプルになります。

押されたキーのキーコードとして渡された引数nKeyCodeをインデックスに指定して、配列keys_arrayから値を取出します。引数nKeyCodeが左右いずれかの矢印キーのキーコードでしたら、水平方向の移動ピクセル数が取得できます。したがって、その値をそのままDisplayObject.xプロパティに設定すればよいでしょう。

左右矢印キー以外のキーコードであれば、配列にはそのインデックスの値がなく、undefinedが返されます。undefinedは、Number型の変数に設定すると、NaNに変換されます。if条件では、0とNaN以外の数値はtrueと評価されます(Tips 05-003「if条件の評価」)。ですから、配列から得られた値をifステートメントで判定したうえで、インスタンスの水平座標を移動します。

ドット演算子と配列アクセス演算子
前記スクリプト06-005に垂直方向の動きを加えようとすると、プロパティを変数で指定したくなります。プロパティ名のxyを変数に入れて、その変数を何らかのかたちで指定することにより、操作するプロパティが切替えられれば、水平方向と垂直方向のふたつの動きがひとつのステートメントで実現できるからです。

しかし、もちろんつぎのようなフレームアクションでは、スクリプトを記述したMovieClipインスタンスのDisplayObject.xプロパティの値を変えることはできません。文法的な誤りはないものの、まったく違った処理になります。

var myProperty = x;
myProperty += 1;

つまり、第1ステートメントでは、変数myProperty(データ型はあえて指定しませんでした)に、インスタンスのDisplayObject.xプロパティの「数値」を代入しています。「プロパティ」そのものが格納された訳ではないのです。

第2ステートメントは、変数myPropertyの値に数値1を加算しているだけです。変数myPropertyは、DisplayObject.xプロパティとは何の関わりももっていません。

実は、配列アクセス演算子[]を使うと、プロパティ名を文字列で指定して、そのプロパティにアクセスできるのです。このとき注意したいのは、ここで配列アクセス演算子[]を用いるのは、MovieClipその他のインスタンスに対する操作であって、配列つまりArrayインスタンスの操作の話ではないということです。

これから解説しようとしているのは、ドット演算子(.)によるプロパティやメソッドへのアクセスを、配列アクセス演算子[]により書替える方法です。たとえば、MovieClipインスタンスmy_mcのDisplayObject.xプロパティに1加算して右に移動させる処理は、ドット演算子(.)を使ってつぎのように記述しました。

my_mc.x += 1;

このステートメントの.xの部分を配列アクセス演算子[]で書替えるには、ドット(.)を配列アクセス演算子[]に替え、プロパティ名は文字列で角括弧[]の中に指定します。

my_mc["x"] += 1;

ドット演算子(.)を使った記述は、プロパティの指定にかぎらず、すべて配列アクセス演算子[]で書替えることが可能です。たとえば、my_mcの子のMovieClipインスタンスchild_mcの再生を開始する処理(MovieClip.play()メソッドの呼出し)は、ドット演算子(.)を使ってつぎのように記述します。

my_mc.child_mc.play();

配列アクセス演算子[]を使った書替え方は、前のプロパティの例と同じです。ドット(.)を配列アクセス演算子[]に替え、インスタンス名やメソッド名を文字列で角括弧[]の中に指定します。

my_mc["child_mc"]["play"]();

注意すべき点をふたつ挙げます。第1に、ドット演算子(.)と配列アクセス演算子は、ひとつのステートメントに両方使うこともできます。そのとき、配列アクセス演算子[]に書替えるのは、文字列で指定したい項目の左側のドット(.)です。

my_mc["child_mc"].play();

MovieClip.play()メソッドを文字列で指定しないのなら、その左のドット(.)は残しておかなければなりません。以下のようにこのドットも削除してしまうと、シンタックスエラーになります。

my_mc["child_mc"]play();   // シンタックスエラー!play()の左にドット(.)がない


左側のドットを[]に替えて、中に文字列を指定する。

注意すべき第2の点は、スクリプトを記述しているインスタンスがコントロール対象のときです。たとえば、水平座標を1ピクセル右に動かす場合、つぎのようなステートメントを書きます。

x += 1;

しかし、配列アクセス演算子で以下のように記述すると、シンタックスエラーになってしまいます。

["x"] += 1;   // シンタックスエラー

配列アクセス演算子[]は、ドット(.)演算子の代替です。ドット(.)を書くためには、必ず左に参照(ターゲット)が置かれていなければならないのです。スクリプトの記述されたインスタンスを参照するのはthisです(前記の例であればthis.x)。したがって、配列アクセス演算子[]の左には、thisを添える必要があります。

this["x"] += 1;

Tips 06-012■参照なしの配列アクセス演算子は配列を作成する
参照を指定せずにいきなり配列アクセス演算子[]が書かれると、配列のリテラル記述と解釈され、Arrayインスタンスが作成されてしまいます。

つまり、以下のステートメントは、文字列"x"をひとつだけエレメント(インデックス0)にもった配列を作成し、そこに加算後代入演算子+=で値を設定しようとしていることになります。

["x"] += 1;   // シンタックスエラー

配列に対するそのような操作はできないので、シンタックスエラーになる訳です。

プロパティを文字列で指定できるということは、その名前を変数に入れて、操作対象のプロパティを変更できることになります。たとえば、以下のフレームアクションを記述すると、そのMovieClipインスタンスを水平方向右に1ピクセル動かします。変数property_strに格納する文字列を"y"に変更すれば、垂直方向の下向きにインスタンスを移動できます。

var property_str:String = "x";
this[property_str] += 1;

Maniac! 06-005■ドットアクセスと配列アクセスの処理速度
型指定されたデータのプロパティやメソッドにアクセスするスピードは、ドット演算子(.)の方が配列アクセス演算子[]による処理よりも高速です。また、配列アクセス演算子を使うと、[Strictモード](Tips 05-002「等価と厳密な等価」参照)によるエラーチェックも行われなくなります。

ですから、ドット演算子で処理が記述できる場合には、そちらを使った方がよいでしょう。

配列の入れ子
プロパティ名を変数に入れて、インスタンスをコントロールする処理についてはめどがつきました。つぎは、前記スクリプト06-005に修正を加えて、インスタンスを上下左右に動かせるようにしましょう。すると、プロパティ名も配列に格納した方がよいでしょう。

その場合、プロパティ名を格納した配列は、もうひとつ別に定義することも考えられます。しかし、今回はひとつの同じ配列に、プロパティ名の情報も含めることにします。

そのとき使うのが、配列の入れ子です。配列のエレメントとして、配列を格納するのです。スクリプト06-005では、配列keys_arrayのエレメントは-1や1といった単なる数値でした。これを変更し、[プロパティ名, 数値]という2エレメントのArrayインスタンスを作成したうえで、そのインスタンスをさらに配列keys_arrayに収めるのです。

var keys_array:Array = new Array();
keys_array[Keyboard.LEFT] = ["x", -1];
keys_array[Keyboard.RIGHT] = ["x", 1];
keys_array[Keyboard.UP] = ["y", -1];
keys_array[Keyboard.DOWN] = ["y", 1];

この場合もエレメントの取出し方は、数値だけの配列の場合と同じく、配列アクセス演算子[]でインデックスを指定すればよいです。ただし、取出したエレメントがまた配列ですので、その配列中の値を使うにはもう1度配列アクセス演算子[]で値を取得しなければなりません(図06-008)。

var key_array:Array = keys_array[Keyboard.UP]; trace(key_array);   // 出力: y,-1
trace(key_array[0]);   // 出力: y
trace(key_array[1]);   // 出力: -1

入れ子の配列中の値を調べるには、つねにエレメントの配列を取出す必要はありません。配列アクセス演算子を2回使って、直接値を取得することも可能です(図06-008)。なお、2重の入れ子の配列を「2次元配列」と呼ぶことがあります。

trace(keys_array[Keyboard.UP][0]);   // 出力: y
trace(keys_array[Keyboard.UP][1]);   // 出力: -1
図06-008■入れ子の配列の値へのアクセス

エレメントの配列を一旦取出してもよく、入れ子の配列内の値に直接アクセスすることも可能。2重の入れ子の配列の値は、配列アクセス演算子[]を2回指定する。

Tips 06-013■入れ子の配列インスタンスをつくり忘れない
2重の入れ子の配列をつくろうとしたとき、間違えてつぎのようにスクリプトを書く人がいます。

var my_array:Array = new Array();
my_array[0][0] = 1;   // エラー

このスクリプトは、エラーになります(図06-009)。なぜなら、入れ子の配列インスタンスが存在しないからです。配列アクセス演算子によりアクセスしただけでは、自動的に配列はつくられません。

図06-009■入れ子の配列をつくらないときのエラー

「プロパティがありません」というエラーは、Arrayインスタンスが存在しないため。

入れ子の配列も、必ずインスタンスの作成を行わなければならないのです。入れ子のArrayインスタンス作成のステートメントを挿入すれば、エラーはなくなります。

var my_array:Array = new Array();
my_array[0] = new Array();   // 入れ子の配列を作成
my_array[0][0] = 1;
trace(my_array[0][0]);   // 出力: 1

配列のリテラル記述を使うと、上記スクリプトの2行ステートメントを1行にまとめることもできます。

var my_array:Array = new Array();
my_array[0] = [1];   // 入れ子の配列をリテラル記述で作成
trace(my_array[0][0]);   // 出力: 1

配列を使った矢印キーによるインスタンスの移動
スクリプト06-005に垂直方向のインスタンスの移動の処理を加えて、フレームアクションを完成させましょう(図06-006)。

スクリプト06-006■矢印キーの方向にインスタンスを移動する ー 入れ子の配列を利用

// MovieClip: キー操作で動かすインスタンス
var nMove:int = 1;
var nShiftMove:int = 10;
var keys_array:Array = new Array();
keys_array[Keyboard.LEFT] = ["x", -1];
keys_array[Keyboard.RIGHT] = ["x", 1];
keys_array[Keyboard.UP] = ["y", -1];
keys_array[Keyboard.DOWN] = ["y", 1];
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var bShiftKey:Boolean = eventObject.shiftKey;
  xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  var key_array:Array = keys_array[nKeyCode];
  if (key_array) {
    this[key_array[0]] += key_array[1]*xGetPixels(bShiftKey);
  }
}
function xGetPixels(bShiftKey:Boolean):int {
  var nPixels:int = bShiftKey?nShiftMove:nMove;
  return nPixels;
}

関数xMove()の最初のステートメントは、配列keys_arrayからキーコードの値nKeyCodeに格納されたエレメントを取出しています。押されたのが矢印キーであれば、入れ子の配列が取得できます。それ以外のキーコードの場合は、エレメントがありませんのでundefinedが返されます。undefinedをArray型の変数に設定すると、nullに変換されます。

if条件の評価は、クラスのインスタンスがtrueで、nullfalseです(Tips 05-003「if条件の評価」)。ですから、矢印キーが押されたときのみ、ifステートメントの処理が行われます。

ifステートメントでは、取出された入れ子の配列key_arrayからプロパティ名と数値を取得し、プロパティの操作を行っています。ただし、代入式の右辺で、数値に関数xGetPixels()の戻り値を乗じています。

関数xGetPixels()の定義は、前のスクリプト06-003や06-004と同じで、移動ピクセル数を返します。したがって、入れ子の配列key_arrayのインデックス1(2番目)の数値は、移動ピクセル数ではなく、プラスマイナスの移動方向を指定する数値として扱われることになります(ですから、値は±1のいずれかです)。

配列を使った手法は、今回のキーコードのような各インデックスごとに、多くの情報を収めることができます。そして、配列アクセス演算子[]を使ったプロパティの操作のように、配列エレメントの値を用いてシンプルなステートメントにまとめられれば、スマートな処理になります。


06-06 Objectインスタンスの配列を使う
前節のスクリプト06-006で、キーコードをインデックスにした配列を利用するのは、わかりやすいデザインです。もっとも、入れ子の配列key_arrayについては、インデックス0と1がそれぞれ何の値なのか、ステートメントだけからは理解できません。半年後に自分で見ても、わからなくなる恐れがあります。

this[key_array[0]] += key_array[1]*xGetPixels(bShiftKey);

このような場合、整数インデックスではなく、変数のように任意の名前が設定できると便利でしょう。そのときに利用するのが、Objectクラスです。Objectクラスのインスタンス(たとえばoKeyとします)を使えば、つぎのようなステートメントに書替えることができます。

this[oKey.property_str] += oKey.nDirection*xGetPixels(bShiftKey);

ObjectインスタンスoKeyからプロパティ名の文字列を取出してそのプロパティにアクセスし、さらにoKeyから方向についての数値を取得して設定していることが明らかになります。

Objectインスタンスの操作
Objectクラスには、ふたつの顔があります。そのひとつは、配列と同じく容れ物だということです。ただし、値にはインデックスでなく、変数と同様の名前をつけて管理します。まず、Objectインスタンスは、他のクラスと同じく、new演算子でコンストラクタを呼出して作成します。本書ではObjectインスタンスを入れる変数名には、接頭辞"o"をつけることにします。

var oMyObject:Object = new Object();

Objectインスタンスに格納する値は、通常「プロパティ」と呼ばれます。プロバティの設定も取得も、Objectインスタンスに対してドット演算子(.)を使って行います(図06-010)。

oMyObject.myProperty = "test";
trace(oMyObject.myProperty);   // 出力: test
図06-010■Objectインスタンスへのプロパティの設定と取得

Objectインスタンスへのプロパティの設定と取得には、ドット演算子(.)を用いる。

もちろん、前述のとおり「ドット演算子(.)を使った記述は、プロパティの指定にかぎらず、すべて配列アクセス演算子[]で書替えることが可能です」。したがって、前記のステートメントは、つぎのように書替えることもできます。

oMyObject["myProperty"] = "test";
trace(oMyObject["myProperty"]);   // 出力: test

Tips 06-014■Objectインスタンスの[出力]結果
trace()関数でObjectインスタンスそのものを引数にしても、[出力]パネルには[object Object]と表示されるのみです(図06-011)。格納された値が、Arrayインスタンスのように[出力]されることはありません。

図06-011■Objectインスタンスのtrace()関数による[出力]

[object Object]としか[出力]されず、プロパティ値は表示されない。

また、Objectインスタンスも、Arrayインスタンスと同じように、リテラルで記述して作成することができます。空の新しいObjectインスタンスを作成するには、空の中括弧{}を記述します。これは、new Object()の呼出しと同じです。

var oMyObject:Object = {};

プロパティの入った新規のObjectインスタンスを作成したいときには、各プロパティ名とその値はコロン(:)で結び、複数のプロパティ間をカンマ(,)で区切って中括弧{}内に記述します。リテラル記述では、プロパティ名にはダブルクォーテーション(")をつける必要はありません。ただし、値については、文字列はダブルクォーテーション(")で括ります。

var oMyObject:Object = {myProperty0:"test", myProperty1:1};
trace(oMyObject.myProperty1);   // 出力: 1

なお、プロパティ名は、識別子で指定します。

Tips 06-015■連想配列
Objectクラスのように、値に名前をつけて格納し、管理するインスタンスの役割は「連想配列」(associative array)と呼ばれることがあります。連想配列というのは、あくまで「機能」に対する呼び名です。「配列」が、Arrayクラスとそのインスタンスを指すのとは異なります。つまり、連想配列は、配列(Array)のことではないのです。

実際、後述するように、プロパティに名前をつけて値を設定・取得できるという機能は、ActionScriptのすべてのクラスがもっています。したがって、連想配列という言葉は、Objectクラスとそのインスタンスを意味する訳ではなく、またArrayクラスとそのインスタンスである配列と混同されやすいので、本書ではこの用語は使いません。

[*筆者用参考] JavaScript講座「5章 配列


AS1&2 Note 06-001■リテラル記述したObjectインスタンスのプロパティ名
ActionScript 3.0では、リテラル記述したObjectインスタンスのプロパティ名は、ダブルクォーテーション(")をつけることも可能です。

var oMyObject:Object = {"myProperty":"test"};
trace(oMyObject.myProperty);   // 出力: test

ActionScript 2.0/1.0では、プロパティ名をダブルクォーテーション(")で括ればエラーになります。ですから、プロパティ名にダブルクォーテーション(")をつけない記述に統一した方が、ActionScriptのバージョンに煩わされません。

[*筆者用参考]「ObjectリテラルやisSubClassOfについて


Maniac! 06-006■構造体
Objectインスタンスを「構造体」と呼ぶ人がいます。しかし、これは他言語の用語です。Cでは、いくつもの変数をひとつにまとめたデータ型が構造体とされるようです。確かに、その説明だけ聞くと、Objectクラスと似ています。

しかし、もちろん言語が違えば、具体的な内容は当然異なるはずです。Cのプログラミング経験がある人に説明するのであれば別として、わざわざ他の言語の異なるものに対する用語をActionScriptの解説に採入れる意義は感じられません。

[*筆者用参考] C#によるプログラミング入門「構造体」、「クラス -構造体-」、JavaA2Z「構造体とは

Objectはすべてのクラスのスーパークラス
Objectクラスのもうひとつの顔は、ActionScriptのすべてのクラスのスーパークラスだということです。前述04-01「マウス座標を調べる」で確認したとき、MovieClipクラスの継承はObjectクラスを先頭にしていました(図06-012)。ということは、DisplayObjectやInteractiveObjectクラス(Tips 06-001「InteractiveObjectクラス」参照)も、当然Objectクラスを継承します。

図06-012■[ヘルプ]に示されたMovieClipクラスの継承

Objectクラスは先頭なので、DisplayObjectやInteractiveObjectクラスも当然これを継承している。

さらに、「DisplayObjectクラスを継承するサブクラスは、ステージ上に表示するインスタンスを作成するクラス」でした(前述Tips 04-001「DisplayObjectクラスのサブクラス」)。ですから、ボタンのSimpleButtonやTextFieldクラスも、Objectのサブクラスです。さらに、ArrayやDateクラスは、Objectクラスを直接継承しています。

すべてのクラスがObjectを継承しているということは、すべてのクラスが基本的にObjectクラスの機能をもつことになります。たとえば、タイムラインにMovieClipインスタンスmy_mcを配置したとすると、フレームアクションでインスタンスに対して、つぎのようにプロパティを設定・取得することができます(図06-013)。

my_mc.myProperty = "test";
trace(my_mc.myProperty);   // 出力: test
図06-013■MovieClipインスタンスにプロパティを設定

Objectインスタンスと同じように、ドット演算子(.)で設定・取得できる。

Tips 06-016■MovieClipインスタンスに設定したプロパティは変数
MovieClipインスタンスに設定したプロパティというのは、そのインスタンスの変数になります。ActionScriptの内部的な処理では、プロパティと変数とは質的に同じものです。

ただし、ドット演算子(.)による設定では、MovieClipのフレームアクションでvar宣言した場合とは異なり、データ型の指定ができません。

けれども、データの容れ物としてインスタンスを使いたい場合、クラスに予め定義されたプロパティやメソッドをとくに利用する必要がありません。また、継承は親に遡るほど、定義されているプロパティやメソッドが少ない分データも軽くなります。そのため、容れ物としては、もっぱらObjectインスタンスが用いられることになるのです。

また、実際には多くのクラスが、ActionScriptに予め定義されているプロパティ以外、アクセスできない設定になっています。たとえば、TextFieldインスタンスに任意のプロパティを設定しようとすれば、エラーになります(図06-014)。

図06-014■TextFieldインスタンスには任意のプロパティは設定できない

[ムービープレビュー]すると、[コンパイルエラー]が表示される。

Maniac! 06-007■任意のプロパティを設定できるクラス ー dynamic
ObjectやMovieClipなどスクリプトで任意のプロパティを設定できるクラスは、[ヘルプ]で調べると「クラス」の項目にdynamicという指定があります(図06-015)。クラスのdynamic属性については、クラス定義の解説をする際に改めて触れます。

図06-015■[ヘルプ]のMovieClipクラスの説明冒頭

「クラス」の項目に、dynamicという指定がある。

●Objectインスタンスの配列を使った矢印キーによるインスタンスの移動
前記スクリプト06-006に修正を加え、キー操作の情報を収める配列keys_arrayには、Objectインスタンスをエレメントとして設定することにします。Objectインスタンスにはproperty_strとnDirectionのふたつのプロパティを追加し、前者には座標を操作するためのプロパティ名("x"または"y")、後者には移動方向を決める整数(-1または1)を値として設定します(スクリプト06-007)。

スクリプト06-007■矢印キーの方向にインスタンスを移動する ー Objectインスタンスの配列を利用

// MovieClip: キー操作で動かすインスタンス
var nMove:int = 1;
var nShiftMove:int = 10;
var keys_array:Array = new Array();
keys_array[Keyboard.LEFT] = {property_str:"x", nDirection:-1};
keys_array[Keyboard.RIGHT] = {property_str:"x", nDirection:1};
keys_array[Keyboard.UP] = {property_str:"y", nDirection:-1};
keys_array[Keyboard.DOWN] = {property_str:"y", nDirection:1};
stage.addEventListener(KeyboardEvent.KEY_DOWN, xKeyDown);
function xKeyDown(eventObject:KeyboardEvent):void {
  var nKeyCode:int = eventObject.keyCode;
  var bShiftKey:Boolean = eventObject.shiftKey;
  xMove(nKeyCode, bShiftKey);
}
function xMove(nKeyCode:int, bShiftKey:Boolean):void {
  var oKey:Object = keys_array[nKeyCode];
  if (oKey) {
    this[oKey.property_str] += oKey.nDirection*xGetPixels(bShiftKey);
  }
}
function xGetPixels(bShiftKey:Boolean):int {
  var nPixels:int = bShiftKey?nShiftMove:nMove;
  return nPixels;
}

関数xMove()の実質的な処理内容は、入れ子の配列を使ったスクリプト06-006と基本的に同じです。ただ、配列keys_arrayから取出されるエレメントはObjectですので、ローカル変数oKeyはObjectで型指定しています。if条件の評価結果は、指定したのが配列でもObjectインスタンスでも同じです。

ifステートメントで行っている以下の代入式の処理が、配列でなくObjectインスタンスからプロパティ名の指定により値を取出しているため、配列を使ったスクリプト06-006よりもわかりやすくなっています。

this[oKey.property_str] += oKey.nDirection*xGetPixels(bShiftKey);

Column 06 値と参照
ActionScriptのさまざまなデータ型は、ふたつの種類に分けられます。プリミティブ型(基本型)とリファレンス型(参照型または複合型)です。違いは、値を他の変数や、関数の引数として渡したときに生じます。

[*筆者用参考] Wikipedia「データ型」、「値型と参照型」、IT戦記「JavaScriptのthisについて

プリミティブ型の変数値を他の変数に渡す
プリミティブ型に属するデータ型は、数値(int、Number、uint)と文字列(String)、およびブール(論理)値(Boolean)です。たとえば、Number型の変数に設定した数値を、他の変数に代入してみます。

var nOriginal:Number = 0;
var nCopy:Number = nOriginal;

その後、一方の変数の値を変更して、それぞれの変数値をtrace()関数で[出力]してみましょう。結果は、簡単に予想できますね。

nCopy += 1;
trace(nOriginal);
trace(nCopy);

変数nOriginalの値はもとのまま変わらず0で、変数nCopyの値は1加算されて1が[出力]されます(図06-016)。これが、プリミティブ型の値を、他の変数に渡した場合の結果です。これは当たり前と思う人も、少なくないでしょう。しかし、リファレンス型のデータでは、この結果が異なるのです。

図06-016■変数の数値を他の変数に代入して加算する

ふたつの変数には、独立した値が設定される。

リファレンス型の変数値を他の変数に渡す
数値と文字列、それにブール(論理)値以外のオブジェクトは、すべてリファレンス型です。本章で学習した配列やObjectインスタンスも、当然含まれます。それでは、配列を使って、前述と同様の処理を行ってみましょう。

まず、数値0をひとつだけエレメントにもつ配列を、変数original_arrayに設定します。つぎに、その変数値を別の変数copy_arrayに代入してみます。そして、変数copy_arrayの配列には、もうひとつエレメントとして数値1を加えることにします。ふたつの変数値を[出力]した結果は、どうなるでしょうか。

var original_array:Array = [0];
var copy_array:Array = original_array;
copy_array.push(1);
trace(original_array);
trace(copy_array);

ふたつの変数は、ともに"0,1"を[出力]します(図06-017)。つまり、両方の変数に、配列エレメントとして数値1が加わったということです。この結果は、プログラミングの経験が少ない方には、不思議に見えるかもしれません。

図06-017■変数の配列を他の変数に代入してエレメントを加える

ふたつの変数の配列が、同じエレメントをもっている。

日常生活で、同じようなことがないか考えてみました。Flash Player銀行のOriginal支店に口座を開いて、1万円入金したとしましょう。近くにCopy支店ができたので、ATMにキャッシュカードを入れて、残高を確認してみました。すると、Original支店で預けた1万円が記録されています。

そこで、Copy支店のATMで1万円引出してから、急いでOriginal支店に行って残高を照会しました。すると、残高0でした。どうでしょう。これは当然ですね。

Original支店に預けた1万円というのは、別にお札に名前を書いて、支店の金庫に保管してもらった訳ではありません。口座情報を、おそらく本店のコンピュータに新規作成して、そこに1万円の入金という記録をしただけのことです。

Copy支店のATMでも、口座番号さえ与えれば、その記録を見ることができます。お金を引出せば、本店コンピュータの口座情報が変更されます。その情報は、Original支店から見ても当然同じです。

前記の配列の処理も、これと似ています。配列を新規に作成すると、Flash Playerが新たなメモリを確保してその値を収めます。そして、その配列を変数original_arrayに代入するとき、配列そのものでなく、銀行の口座番号に当たるメモリの場所を与えるのです。変数original_arrayは、いわばキャッシュカードをもらったようなものです。

配列の設定された変数original_arrayの値を別の変数copy_arrayに代入しても、キャッシュカードがもうひとつ与えられるだけです。現金が増える訳ではありません。そのカードを使ってエレメントを追加すれば、Flash Playerのメモリにあるその口座の配列が変更されます。したがって、最初の変数original_arrayのキャッシュカードで調べても、口座は同じですから、配列の情報が変わっていることになるのです。


リファレンス型データはキャッシュカードを渡す。口座情報は本店のコンピュータの中。

配列でなく、Objectインスタンスで試しても、同じ結果になります。前述のプリミティブ型データである数値と文字列、およびブール(論理)値以外のすべてのデータは、リファレンス型です。リファレンス型データはみな、変数に代入するとデータのありか(メモリ場所)を渡すのです。

値渡しと参照渡し
リファレンス型データが変数や、あるいは関数の引数に渡すデータのありかを「参照」と呼びます。それに対して、プリミティブ型データは「値」自体を渡します。プリミティブ型データは値渡しで、リファレンス型データは参照渡しなのです。この違いを理解しておくことは大切です。

たとえば、プリミティブ型データの文字列を引数に渡すと、別の文字列(名前)を後ろに連結して返す関数xAppendName()を定義してみます。

function xAppendName(_str:String):String {
   _str += "Fumio Nonaka";
   return _str;
}

プリミティブ型データである文字列は値渡しですから、関数xAppendName()から返された文字列を新たな変数値とするためには、改めて変数に代入しなければなりません。

var my_str:String = "written by ";
my_str = xAppendName(my_str);   // 改めて変数に代入
trace(my_str);   // 出力: written by Fumio Nonaka

リファレンス型データであれば、引数として渡されるのは参照ですから、関数から処理後のデータを返す必要はありません。たとえば、配列を引数に受取って、エレメントを最後にひとつ追加するように関数xAppendName()を書替えてみます。値を返すreturnステートメントがないことにご注目ください。

function xAppendName(_array:Array):void {
   _array.push("Fumio Nonaka");
}

リファレンス型データである配列は、関数xAppendName()の引数に参照を渡すだけで、戻り値は受取りません。しかも、変数に代入するステートメントなしに、配列の値は変更されます。

var my_date:Date = new Date();
var my_array:Array = [my_date.fullYear, my_date.month+1, my_date.date];
xAppendName(my_array);   // 関数の戻り値はなく、変数への代入も不要
trace(my_array);   // 出力: 2007,12,1,Fumio Nonaka

Maniac! 06-008■プリミティブ値はイミュータブルオブジェクト
厳密には、「ActionScriptではプリミティブ値はイミュータブルオブジェクトとして内部的に格納されます」([ActionScript 3.0のプログラミング] > [ActionScript言語とシンタックス] > [データ型])。「イミュータブルオブジェクト」というのは、値が変わらない(immutable)オブジェクトです。

オブジェクトですから、データの受渡しをする際には、実は値そのものでなく、参照を渡します。したがって、たとえば膨大な文字列が設定された変数を別の変数に代入しても、同じ値が複製されて新たなメモリに収められるのではなく、もとの値の参照が別の変数に渡されるだけです。

つまり、つぎのような100行の代入のステートメントがあっても、数値1はメモリにひとつだけ格納され、100個の変数にはその参照が渡されます。そのため、メモリが浪費されません。

var a0:int = 1;
var a1:int = a0;
var a2:int = a0;
// ...[中略]...
var a99:int = a0;

しかし、リファレンス型データと異なるのは、値が変えられないことです。上記の100行のステートメントの後に、最初の変数値を加算したとします。

a0 += 2;

すでにメモリに保持されている値1は変えられません。そこで、加算後の値3は新たなイミュータブルオブジェクトとして別のメモリに作成され、その新しい参照が変数に代入されるのです。けれど、他の99の変数は、相変わらずもとの値(イミュータブルオブジェクト)への参照を保持しています。したがって、ひとつの変数値を変更しても、他の変数には影響は及びません。

[*筆者用参考] 福井プログラマー生活向上委員会「文字列変数コピーは参照コピーである」。

[Prev/Next]


作成者: 野中文雄
更新日: 2008年4月5日 一部の文に補足。
更新日: 2008年3月18日 図06-003の説明に[ムービーエクスプローラ]について補足。
更新日: 2008年1月27日 Maniac! 06-008を追加。
作成日: 2007年12月19日


Copyright © 2001-2008 Fumio Nonaka.  All rights reserved.