3Dで雨を降らす

以前に書いた 「雨を降らす」 の記事では、
2Dで雨を降らせていたが、3Dが扱えるということで今回は3Dで雨を降らせてみよう。

2Dの時は雨粒の大きさで距離感を表現していたが、
3Dでは雨粒を3D空間に配置できるので実際に遠くの位置に配置できる。
雨粒を3D空間に配置できるということは、雨粒を別の角度から見ることもできるということだ!

3D空間の見え方を変えるには、視点と注視点の位置を指定する。
ただしパラメータで指定するのではなく、ちょっと変わった方法で指定する。

具体的には3Dステージデーカーの子として”Camera”という名前のデーカーを作ると
そのデーカーの位置が視点となる。
同様に”Target”という名前のデーカーを作るとそのデーカーの位置が注視点となる。
デーカーの位置を基準とするので、デーカーを動かせば視点を操作できるという仕掛けだ。


class 雨クラス
{
  method 雨クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "雨粒"+String($number++);
      ThreadCreate(call=@雨粒スレッド(name=$name));
      wait 10;
    }
  }

  method 雨粒スレッド(string $name)
  {
    float $x = Float(rand_range(-5000,5000));
    float $y = 10000.0;
    float $z = Float(rand_range(-5000,5000));
    int $time = 1000;

    Create3DImage(name=$name
      , x=$x, y=$y, z=$z, rz=-90.0, ox="Center", oy="Middle", sx=500%, sy=500%
      , image="雨粒.png", sampling="BieLinear", back=true);
    Enter(to=$name);

    Move3D(to=$name, time=$time, y=0.0);
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DModel(name="Stage/空", model="空.lwo", render="Color");
  Create3DObject(name="Stage/雨", class=@雨クラス());
  Create3DNode(name="Stage/Target", y=1000.0);
  Create3DNode(name="Stage/Camera", y=0.0, z=-10.0);
  Enter(to="Stage");
  Enter(to="Stage/*");

 // 視点を移動
  Move3D(to="Stage/Camera", time=10000, y=1000.0, z=-2000.0, step="AccSig");
}
YouTube Preview Image

3Dモデルを表示する

Foooの3D機能は、必要だから作ったというより

3D表示もできたらすごくね!?

くらいの軽いノリで作った。
もはやすごくもなんともないけれど…
昔はちょっとしたことだったのだ。

今でこそ3Dベースのゲームエンジンも珍しくはないが
10年ほど前はまだまだ2Dベース全盛の時代。
3Dといえばトランジッションでソレっぽいことがちょっとできる程度が普通。
自由度の高い3D演出ができれば、頭一つ抜きんでられるのは間違いなかった!

そして実際、3D機能を作った!
が…

3D使う機会がぜんぜんない!

悔しいので、サイコロとか魔方陣とか地球儀とか
こまかーいところで地味~に使っていた(苦笑)

というわけで今回はサイコロを表示してみよう。
サイコロのモデルには3Dソフトで作成したものを使う。
3Dモデルを表示する機能を持つのが、3Dモデルデーカーだ。
Create3DModel命令で作成する。


method Main()
{
  CreateColor(name="Back", w=1280, h=720, color=black);
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DModel(name="Stage/サイコロ", model="サイコロ.lwo", z=400.0);
  Enter(to="Back");
  Enter(to="Stage");
  Enter(to="Stage/サイコロ");
  wait 1000;

  Rotate3D(to="Stage/サイコロ", ry=360.0, time=3000, step="AccSig");
  wait 3000;
  Rotate3D(to="Stage/サイコロ", ry=0.0, time=0);
  Rotate3D(to="Stage/サイコロ", rx=360.0, time=3000, step="AccSig");
  wait 3000;
  Rotate3D(to="Stage/サイコロ", rz=360.0, time=3000, step="AccSig");
  wait 3000;
}
YouTube Preview Image

うん、3Dは楽しい。


3Dで画像を表示する

Foooでは3Dを扱うこともできる。
とはいえFoooは基本的に2Dで処理を行っているので、3Dの扱いは少々特別だ。

3Dを扱うためには、3Dの空間を持つ『3Dステージ』を作る必要がある。
Foooではデーカーが配置される空間のことを『ステージ』と呼ぶ。
普段デーカーを配置しているステージは、2Dの空間である。
2Dの空間上では3Dを扱うことができない。
このためまず、3Dの空間を用意する必要があるのである。

なんともややこしそうな話だが、やることは簡単だ。
3Dステージデーカーを作るだけである。


method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Enter(to="Stage");
}

この3Dステージデーカーの子としてデーカーを作成すると、
そのデーカーは3Dステージデーカーが管理する3D空間内に配置される。

3D空間に配置できるデーカーは3D用の特別なものである。
3Dで画像を表示するには、3Dイメージデーカーを使用する。
3Dイメージデーカーを作成するにはCreate3DImage命令を使用する。


method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DImage(name="Stage/画像", image="画像.png", z=800.0);
  Enter(to="Stage");
  Enter(to="Stage/画像");
}

3D用のデーカーを操作する命令も3D用に特殊なものを使う。
3Dのカードを作成して回転させてみよう。


method Main()
{
  Create3DStage(name="Stage", w=1280, h=720);
  Create3DNode(name="Stage/カード", z=800.0);
  Create3DImage(name="Stage/カード/表", image="表.png");
  Create3DImage(name="Stage/カード/裏", image="裏.png", ry=180.0);
  Enter(to="Stage");
  Enter(to="Stage/カード");
  Enter(to="Stage/カード/*");
  wait 1000;
  Rotate3D(to="Stage/カード", ry=360.0, time=3000);
}
YouTube Preview Image

3D用の命令を使ってはいるが、このように2Dの場合とほとんど同じ感覚で
3Dを制御するスクリプトを書くことができる。


路地裏さつき[ヒロイン十二宮編]

Posted 2013.04.01 in 雑記

TYPE-MOONの2013年エイプリルフール企画!
路地裏さつき[ヒロイン十二宮編]
http://www.typemoon.com/

プレイするとわかりますが…

戦闘パートの演出&スクリプトを担当しています。

っていうか…現在TYPE-MOONで働いてますハイ。

エンジンにはノベルスフィアさんのO2 Engineを使用。
吉里吉里のKAG3互換のゲームエンジンです。
http://novelsphere.jp/

iPhoneでもプレイできます。
3G回線だとロードきついですががが。
是非プレイしてやってください!
(追記:公開終了しました)


ボタンを作る

さてさてここまでFoooの多様な機能を紹介してきたが
ここまで読んだ方々は皆、薄々…疑問に…思っているのではないかと思う。

「いったいどこがアドベンチャーゲーム用の
ゲームエンジンなんだ!」

と。

どんな演出でも実現できるようにと、自由度を追求し続けた結果がコレである。
もはやアドベンチャーゲーム用のエンジンというよりは
より汎用的なマルチメディアエンジンのようなかんじである。
しかしどんなに複雑なことができるとは言っても
Foooはあくまで『動く漫画』を実現するために開発されたゲームエンジンだ。

けれどもゲームを構成する要素は『動く漫画』だけではない。
ゲームがゲームである以上、ユーザーが操作できる部分…
ユーザーインターフェース(UI)がある。

開発が進むにつれ、このUI部分についても『動く漫画』部分と同様に
高度な演出を行いたいという要望が出始めた。

高い汎用性を持ち始めたFooo…
高度な演出が必要なUI…
………………

「UIもFoooスクリプトで作っちゃえばよくね?」

となるのは必然的な流れだった。

Decor_Button ボタンデーカー

そんなこんなでFoooにはUIを実現するデーカーがいろいろあるが、
そのもっとも代表的なものが『ボタンデーカー』だ。
ボタンデーカーは名前の通り、ボタン機能を実現するデーカーである。
ボタンデーカーを作るにはCreateButton命令を使用する。


  CreateButton(name="ボタン", class=@ボタンクラス());

一見するとCreateObject命令となんら違いがない。
ボタンデーカーはオブジェクトデーカーのようにクラスを指定して使う。
ボタンデーカーがオブジェクトデーカーと異なるのは、
ユーザーの操作に応じて予め決められたメソッドを自動的に呼び出す点だ。
次のようなメソッドが呼び出される。

OnButton_Focus ボタンがフォーカスされた時
OnButton_UnFocus ボタンからフォーカスがはずれた時
OnButton_Down ボタンが押し下げられた時
OnButton_Up ボタンが押し上げられた時
OnButton_Enable ボタンが有効になった時
OnButton_Disable ボタンが無効になった時

このような特定のタイミングで自動的に呼び出されるメソッドのことを『イベントハンドラ』と呼ぶ。
ボタンデーカーがやってくれることはこのイベントハンドラの呼び出しだけである。
ボタンに必要な画像の作成や、ボタンの画像の制御などは
コンストラクタやイベントハンドラに動作を書き込むことで実現する。

これは実際の使い方を見たほうがわかりやすいだろう。

Button_Normal Button_Focus Button_Down
ボタン_通常.png ボタン_フォーカス.png ボタン_ダウン.png

style ボタン書式
{
  face="MS ゴシック", size=40, color=white, interval=0
}

class ボタンクラス
{
  method ボタンクラス(string $text)
  {
    CreateImage(name="通常"
      , ox="Center", oy="Middle"
      , image="ボタン_通常.png", sampling="BieLinear");
    CreateImage(name="フォーカス"
      , ox="Center", oy="Middle"
      , image="ボタン_フォーカス.png", sampling="BieLinear"
      , alpha=0%);
    CreateImage(name="ダウン"
      , ox="Center", oy="Middle"
      , image="ボタン_ダウン.png", sampling="BieLinear"
      , alpha=0%);

    CreateText(name="テキスト"
      , ox="Center", oy="Middle"
      , style="ボタン書式", text=$text, sampling="BieLinear");
  }
  method OnEnter()
  {
    Enter(to="*");
  }

  method OnButton_Focus()
  {
    Opaque(to="フォーカス", alpha=100%, time=200);
  }
  method OnButton_UnFocus()
  {
    Opaque(to="フォーカス", alpha=0%, time=200);
  }
  method OnButton_Down()
  {
    Opaque(to="ダウン", alpha=100%, time=0);
    Zoom(to=".", sx=90%, sy=90%, time=50);
  }
  method OnButton_Up()
  {
    Opaque(to="ダウン", alpha=0%, time=200);
    Zoom(to=".", sx=100%, sy=100%, time=200);
  }
}

method Main()
{
  CreateButton(name="ボタン1"
    , x=320, y="Middle"
    , class=@ボタンクラス(text="ボタン1"));
  CreateButton(name="ボタン2"
    , x=640, y="Middle"
    , class=@ボタンクラス(text="ボタン2"));
  CreateButton(name="ボタン3"
    , x=960, y="Middle"
    , class=@ボタンクラス(text="ボタン3"));
  Enter(to="*");
}
YouTube Preview Image

OnButton~メソッドが自動的に呼び出されている様子がわかるだろう。
ボタンデーカーの実装はちょっと手間だが、その分ボタンの動作を細かくカスタマイズできる。


onPaintが二度呼ばれる実験

Posted 2013.03.07 in 吉里吉里

前回の「onPaintが二度呼ばれる問題」の記事において、
現象を確認するスクリプトを掲載し忘れていたのでこちらに掲載しておく。
とても簡単なスクリプトで現象を確認できる。


class TestLayer extends Layer
{
  var timer;
  var paintCount = 0;

  function TestLayer(window, parent)
  {
    super.Layer(...);
    timer = new Timer(onTimer, "");
    timer.interval = 1000;
    timer.enabled = true;
  }

  function finalize()
  {
    invalidate timer;
    super.finalize();
  }

  function onTimer()
  {
    Debug.message("onTimer");
    paintCount = 0;
    update();
  }

  function onPaint()
  {
    Debug.message("onPaint "+(++paintCount));
    update();
//  callOnPaint = false;
  }
}

class TestWindow extends Window
{
  function TestWindow()
  {
    super.Window(); 
    add(new TestLayer(this, null));
    visible = true;
  }
}

var window = new TestWindow();
Debug.console.visible = true;

タイマーで1秒ごとにonTimerを呼び出し、そこからupdate関数を呼び出している。
これを実行するとコンソールに…


onTimer
onPaint 1
onPaint 2
onTimer
onPaint 1
onPaint 2

と表示され続けるはずだ。
1度のonTimerの呼び出しに対してonPaintが二度呼ばれているのがわかる。
そしてcallOnPaint=false;のコメントアウトをはずして再度実行すると…


onTimer
onPaint 1
onTimer
onPaint 1

となる。
二度目のonPaintの呼び出しが抑制されている。

この動作は決して誤った動作ではない。
update関数の作用を正しく理解すると、むしろ至極当たり前の動作だと思えるだろう。
本当の問題はupdate関数を誤って使ってしまいがちなところにあるのである。


onPaintが二度呼ばれる問題

Posted 2013.03.06 in 吉里吉里

画面の再描画を効率化しようと思う時、
誰しもがまず思いつくであろうことがLayerクラスのonPaintイベントの利用だ。
データが変更された時にすぐに再描画を行わずupdate関数を呼び出しておいて
画面の描画前に呼ばれるonPaintでまとめて遅延描画する仕組みである。

シンプルな仕組みのように思えるが…
実はそこには大きな落とし穴があった。

onPaintの不可思議な動作

onPaintを使って再描画を効率化した際、逆に再描画が重くなるという現象に遭遇した。
なぜなのか詳しく調べてみると、意外な事実が判明した。

onPaintが連続して二度呼ばれている!

さらに詳しく調べると、最初に実行されたonPaintを抜ける時に
onPaintを呼び出すことを意味するcallOnPaintフラグがtrueになっていることがわかった。
これが原因っぽい気がするが…吉里吉里のリファレンスには

吉里吉里リファレンス Layerクラス callOnPaint
「onPaint イベント が処理し終わるとこのプロパティは自動的に偽に戻されます」

とあるので、onPaintを抜ける時にtrueになっていても問題ではなさそうだ。
しかし念のためにソースコードを見てみると…
………onPaintを呼び出す前にcallOnPaintフラグを落としている!
リファレンスの誤りだろうか。
途中で仕様変更されたのだろうか。

ともかく、やはりonPaintを抜ける時にcallOnPaintフラグがtrueになっているのが問題なかんじがする。
むしろonPaintを呼び出す前にcallOnPaintフラグが落とされるという事実と照らし合わせるとこれは
onPaint内の処理でcallOnPaintフラグを立てるような処理がなされていることを意味する。
しかしonPaint内でupdateを呼び出してはいないし、
callOnPaintフラグを直接いじるようなこともしていない…
ん、まてよ、updateといえば…

描画プラグインで無効領域を報告するためにupdateを呼び出してる!

そう、プラグインで拡張された描画関数を呼び出していたのである。
その中のupdate呼び出しがcallOnPaintフラグをtrueにしている真犯人だったのだ。

さてさてonPaintを抜ける時にcallOnPaintがtrueになる原因は判明したが、
それでなぜonPaintが二度呼ばれることになるのか。
それを理解するにはまず吉里吉里の再描画の仕組みと、
update関数の作用について知る必要がある。

画面の再描画の流れ

吉里吉里はイベントシステムである。
吉里吉里 イベントシステム

画面の再描画もイベントによって管理されている。
画面の再描画は描画イベント(tTVPWinUpdateEvent)が実行された時に行われる。

描画イベントは原則的になんらかの描画関数を呼び出した時に発行される。
また描画関数は描画した領域を無効領域として報告する。
発行された描画イベントはイベントキューに蓄えられる。
イベントキューに蓄えられる描画イベントは原則として各画面につき1個で、
重複するイベントは無視される。
そして描画イベントはイベント配信処理において低優先度で実行される。

描画イベントは実行されるとまず
callOnPaintフラグがtrueな全てのレイヤーに対してonPaintの呼び出しを行う。
次に全てのレイヤーの無効領域の範囲のバッファの更新を行う。
そしてバッファの最終的な合成結果を画面に転送する。

update関数の作用

update関数には3つの作用がある。

1.callOnPaintフラグをtrueにする
2.無効領域を報告する
3.描画イベント(tTVPWinUpdateEvent)を発行する

まず1によって次回の画面の再描画時にonPaintを呼ぶことを予約する。
ここで重要な事実がある。
callOnPaintフラグはあくまでonPaintを呼ぶ予約にすぎないということだ。
callOnPaintフラグを立てただけでは、画面の再描画は行われない。
画面の再描画が行われるのは、描画イベントが発行され実行された時である。

つまりレイヤーの再描画を行わせる目的で
layer.callOnPaint = true;
などとやっても期待するような動作にはならない。
update関数はcallOnPaintフラグをtrueにするのと同時に描画イベントを発行するので、
update関数を呼び出せば期待通り画面の再描画が行われる。

onPaintが二度呼ばれる理由

ここまでわかればonPaintが二度呼ばれる理由はすぐわかる。
onPaint関数内で、update関数を呼び出したらどうなるか。
callOnPaintフラグがtrueになり、描画イベントが発行される。
描画イベントが発行されるということは…画面の再描画が行われる!
そしてcallOnPaintフラグがtrueなので、再びonPaintが呼び出される!

しかしここでまた不思議なことがある。
そのような流れで再びonPaintが呼ばれるのであれば、
onPaintが無限に呼び出され続けることになりそうなものである。
けれどもそうはならない。

これにはイベントキューの処理上の仕様が関係している。
描画イベントは原則としてイベントキューに各画面につき1個しか蓄えられないのだが、
描画イベント実行中は例外的に2個までイベントキューに蓄えられるのである。
つまり現在実行中の描画イベントに加えて、もう1個イベントキューに蓄えられるということである。
これは言い換えれば、画面の再描画が行われるのは
一度のイベント配信処理において最大2回までということである。
よって無限に呼び出されるようなことにはならない。

update関数を呼ばなくても二度目の再描画は行われる

描画イベントは標準の描画関数を呼び出した時にも発行されるので
実はonPaintで標準の描画関数を使った際にも二度目の画面の再描画処理は行われる。

けれどもこれはほとんどの場合問題にならない。
なぜならonPaintで発生した無効領域はその後のバッファの更新で処理されるからだ。
バッファの更新を終えるとそこまでに蓄えられていた無効領域はクリアされる。
二度目の画面の再描画時には無効領域は空になっているので
実際には具体的な再描画はなんら行われずすぐに処理を終える。

しかし1度目のonPaintでupdateを呼び出して2度目のonPaintで描画を行った場合は
再び無効領域が発生し画面の再描画が再度実施されてしまう。
これは非常に無駄な処理になりかねない。

プラグインでupdateを使うのは誤り?

プラグインで独自の描画関数を追加する際には、吉里吉里の標準の描画関数と同じように
描画した領域を無効領域として報告するようにするのが正しい実装である。
もしそうしなければ、レイヤーのバッファの更新が正しく行われない可能性がある。
たまたま吉里吉里の標準の描画関数を一緒に呼び出していて
うまく動作する場合もあるが…たまたまにすぎない。

プラグインからの無効領域の報告にはupdate関数が一般的に使用されているようだ。
しかし無効領域の発生を報告するためだけを目的として
update関数を使用するのはおそらく本来誤りだ。

なぜなら吉里吉里の標準の描画関数は、update関数の2と3の処理しか行わないからである。
つまり1の動作…callOnPaintフラグを立てる処理が余計なのである。
そもそもupdate関数は本来onPaintとセットで使うべきものなのだろう。

しかし無効領域の報告だけを目的としてupdate関数を使ったプラグインが既に多くある。
その現状を踏まえた上で、この問題を回避する方法について考えてみよう。

対策1: onPaintで明示的にcallOnPaintフラグを落とすようにする

ズバリ言って、updateによってcallOnPaintがtrueにされてしまうのであれば
明示的にcallOnPaintフラグを落としてしまえばいい。
ここまでごちゃごちゃと理屈を述べてきたが、実際これだけでおおかた問題を回避できる。


function onPaint()
{
  // なんらかの描画処理

  callOnPaint = false;
}

対策2: onPaintで無駄な再描画をしないようにしておく

再描画フラグなどを独自に設けて、無駄な再描画をしないようにしておくのも良い方法だ。
何かが何かして二度目のonPaintが実行されたとしても、これならば無駄な再描画を確実に回避できる。


function onPaint()
{
  if(!isNeedRedraw) return;
  isNeedRedraw = false;

  // なんらかの描画処理
}

対策3: updateを呼ぶ際にcallOnPaintを維持するようにする

プラグイン側で無効領域の発生を報告するためだけにupdate関数を使うのは誤り!
とは言ってもそのようなことを直接できる関数はupdate関数しかないので
現実問題としてupdate関数を使わざるを得ない。

そこで、意図しない不必要なonPaintの呼び出しを避けるために
callOnPaintフラグを一時保存してupdateを呼び出した後に書き戻すようにする。
たとえば次のような関数を作って使うようにする。


static tjs_uint32 g_Hint_callOnPaint = 0;
static tjs_uint32 g_Hint_update = 0;

bool Update(iTJSDispatch2* pLayer, const RECT* pRect)
{
  // レイヤークラス取得
  tTJSVariant Value;
  TVPExecuteExpression(TJS_W("Layer"), &Value);
  iTJSDispatch2 *pLayerClass = Value.AsObjectNoAddRef();

  // パラメータを用意
  const int PARAM_NUM = 4;
  tTJSVariant Params[PARAM_NUM];
  Params[0] = pRect->left;
  Params[1] = pRect->top;
  Params[2] = pRect->right - pRect->left;
  Params[3] = pRect->bottom - pRect->top;

  // パラメータポインタの配列を用意
  tTJSVariant* ParamPtrs[PARAM_NUM];
  for(int i=0; i < PARAM_NUM; i++) ParamPtrs[i] = &Params[i];

  // callOnPaintを退避
  tTJSVariant callOnPaint;
  if(TJS_FAILED(pLayerClass->PropGet(0, TJS_W("callOnPaint"), &g_Hint_callOnPaint, &callOnPaint, pLayer))) return false;

  // 領域更新
  tTJSVariant result;
  if(TJS_FAILED(pLayerClass->FuncCall(0, TJS_W("update"), &g_Hint_update, &result, PARAM_NUM, ParamPtrs, pLayer))) return false;

  // callOnPaintを復元
  if(TJS_FAILED(pLayerClass->PropSet(0, TJS_W("callOnPaint"), &g_Hint_callOnPaint, &callOnPaint, pLayer))) return false;

  return true;
}

まとめ

onPaint関数はさらにpiledCopy関数の呼び出し時や
トランジッション時にも呼ばれる可能性があるため問題はいっそう複雑だ。
update関数の使い方には細心の注意を心がけたい。


Dictionaryの具体的修正点

Posted 2013.03.05 in 吉里吉里

吉里吉里(TJS2)のDictionary(辞書配列)の作成に関する高速化策について
試しにいじってみた内容を具体的にメモがてら書いておく。
コードの黄色文字が追加修正部分。

吉里吉里公式のソースコードはこちら。
https://sv.kikyou.info/trac/kirikiri/

吉里吉里のソースコードのビルドには古いBorland C++ Builderが必要。
kirikiri2\src\core\environ\win32 に
5, 6, 2006, 2007のプロジェクトファイルがあるのでそれを使う。
6を使ったが若干修正が必要だった。いじろうとする人はまずここで躓くかも。

ハッシュテーブルに再構築用の関数を追加

ハッシュテーブルの実装を担っているのはtTJSCustomObjectクラスである。
現在の要素数から適切なハッシュテーブルサイズを計算して
ハッシュテーブルの再構築を行うRebuildHash関数が既にあるので
これを分解してハッシュテーブルを外から細かく操作する関数を追加する。


tjsObject.h
class tTJSCustomObject : public tTJSDispatch { // 略 public: // ハッシュテーブル操作 tjs_int CalcHashSize(tjs_int count); void RebuildHashByCount(tjs_int count); void RebuildHashBySize(tjs_int size); }

tjsObject.cpp
// 要素数から適切なハッシュテーブルサイズを計算する tjs_int tTJSCustomObject::CalcHashSize(tjs_int count) { // decide new hash table size tjs_int r, v = count; // 略(ReubildHash関数からコピペ) return newhashsize; } // ハッシュテーブルを要素数指定で再構築する void tTJSCustomObject::RebuildHashByCount(tjs_int count) { RebuildHashBySize(CalcHashSize(count)); } // ハッシュテーブルをサイズ指定で再構築する void tTJSCustomObject::RebuildHashBySize(tjs_int size) { // rebuild hash table RebuildHashMagic = TJSGlobalRebuildHashMagic; tjs_int newhashsize = size; if(newhashsize == HashSize) return; // 略(ReubildHash関数からコピペ) }

辞書に再構築用の関数を追加

Dictionaryの実装はtTJSDictionaryNIクラスにあるのでそこにもリビルド用の関数を追加する。
tTJSCustomObjectクラスのオブジェクトを内包しているので単に処理を受け渡すだけである。
またリビルドが適切かどうか判断するために要素数とハッシュサイズを取得する関数も追加しておく。


tjsDictionary.h
class tTJSDictionaryNI : public tTJSNativeInstance, public tTJSSaveStructuredDataCallback { // 略 public: // ハッシュテーブル操作 void RebuildHashByCount(tjs_int count); void RebuildHashBySize(tjs_int size); tjs_int GetHashSize() const; tjs_int GetCount() const; }

tjsDictionary.cpp
// ハッシュテーブルを要素数指定で再構築する void tTJSDictionaryNI::RebuildHashByCount(tjs_int count) { Owner->RebuildHashByCount(count); } // ハッシュテーブルをサイズ指定で再構築する void tTJSDictionaryNI::RebuildHashBySize(tjs_int size) { Owner->RebuildHashBySize(size); } // ハッシュテーブルサイズを取得 tjs_int tTJSDictionaryNI::GetHashSize() const { return Owner->HashSize; } // 要素数を取得 tjs_int tTJSDictionaryNI::GetCount() const { return Owner->Count; }

rebuild関数を追加する

さてこれで準備が整った。
まず最も簡単な作業から。
吉里吉里のDictionaryクラスにrebuild関数を追加する。


tjsDictionary.cpp
TJS_BEGIN_NATIVE_METHOD_DECL(/*func.name*/rebuild) { TJS_GET_NATIVE_INSTANCE(/* var. name */ni, /* var. type */tTJSDictionaryNI); if(!ni->IsValid()) return TJS_E_INVALIDOBJECT; tjs_uint32 count = 0; if(numparams >= 1 && param[0]->Type() != tvtVoid) count = (tjs_int)*param[0]; ni->RebuildHashByCount(count); return TJS_S_OK; } TJS_END_NATIVE_STATIC_METHOD_DECL(/*func.name*/rebuild)

アサイン時に予め再構築する

アサインの修正も簡単だ。
Dictionaryのassign関数と、assignStruct関数の具体的な処理は
Assign, AssignStructure関数にあるのでそこをちょこっとだけいじる。


tjsDictionary.cpp
void tTJSDictionaryNI::Assign(iTJSDispatch2 * dsp, bool clear) { // copy members from "dsp" to "Owner" // determin dsp's object type tTJSArrayNI *arrayni = NULL; tTJSDictionaryNI *dicni = NULL; if(dsp && TJS_SUCCEEDED(dsp->NativeInstanceSupport(TJS_NIS_GETINSTANCE, TJSGetArrayClassID(), (iTJSNativeInstance**)&arrayni)) ) { // convert from array if(clear) Owner->Clear(); // 予め要素数指定で再構築する tjs_int count = Owner->Count + arrayni->Items.size()/2; if(64 <= count) // 足きり Owner->RebuildHashByCount(count); tTJSArrayNI::tArrayItemIterator i; for(i = arrayni->Items.begin(); i != arrayni->Items.end(); i++) { tTJSVariantString *name = i->AsStringNoAddRef(); i++; if(arrayni->Items.end() == i) break; Owner->PropSetByVS(TJS_MEMBERENSURE|TJS_IGNOREPROP, name, &(*i), Owner); } } else if(dsp && TJS_SUCCEEDED(dsp->NativeInstanceSupport(TJS_NIS_GETINSTANCE, TJSGetDictionaryClassID(), (iTJSNativeInstance**)&dicni)) ) { // dictionary if(clear) Owner->Clear(); tAssignCallback callback; callback.Owner = Owner; // 予め要素数指定で再構築する tjs_int count = Owner->Count + dicni->GetCount(); if(64 <= count) // 足きり Owner->RebuildHashByCount(count); dsp->EnumMembers(TJS_IGNOREPROP, &tTJSVariantClosure(&callback, NULL), dsp); } else { // otherwise if(clear) Owner->Clear(); tAssignCallback callback; callback.Owner = Owner; dsp->EnumMembers(TJS_IGNOREPROP, &tTJSVariantClosure(&callback, NULL), dsp); } } void tTJSDictionaryNI::AssignStructure(iTJSDispatch2 * dsp, std::vector &stack) { // assign structured data from dsp tTJSDictionaryNI *dicni = NULL; // tTJSArrayNI *dicni = NULL; // 元々これだったけどミス? if(TJS_SUCCEEDED(dsp->NativeInstanceSupport(TJS_NIS_GETINSTANCE, ClassID_Dictionary, (iTJSNativeInstance**)&dicni)) ) { // copy from dictionary stack.push_back(dsp); try { Owner->Clear(); // 予め要素数指定で再構築する tjs_int count = dicni->GetCount(); if(64 <= count) // 足きり Owner->RebuildHashByCount(count); tAssignStructCallback callback; callback.Dest = Owner; callback.Stack = &stack; dsp->EnumMembers(TJS_IGNOREPROP, &tTJSVariantClosure(&callback, NULL), dsp); } catch(...) { stack.pop_back(); throw; } stack.pop_back(); } else { TJS_eTJSError(TJSSpecifyDicOrArray); } } void tTJSDictionaryNI::AssignStructure

ロード時に予め再構築する

ロード時の処理は(const)がつく場合とつかない場合とで処理が異なるが
とりあえず(const)がつく場合だけ修正。

この修正は少々手間だ。
データ文字列の解析時に構文解析と意味解析を同時に行っているため、
要素を実際に追加する前に要素の総数がわからないからである。
そこで要素をすぐに辞書に追加せずにいったんリストに蓄え、
要素の総数が判明したら一気に要素を追加するようにする。

まず、式ノードに追加要素を一時保管する処理を追加する。


tjsInterCodeGen.h
class tTJSExprNode { // 略 protected: // 追加要素一時保管処理 struct SElementData { tTJSString name; tTJSVariant value; }; list< SElementData* > *pElementTempList; public: void AddDictionaryElementExBegin(); void AddDictionaryElementEx(const tTJSString & name, const tTJSVariant & val); void AddDictionaryElementExEnd(); }

tjsInterCodeGen.cpp
#include "tjsObject.h" #include "tjsDictionary.h" tTJSExprNode::tTJSExprNode() { // 略 pElementTempList = NULL; } // 辞書に要素追加開始 void tTJSExprNode::AddDictionaryElementExBegin() { pElementTempList = new list< SElementData* >; } // 辞書に要素追加 void tTJSExprNode::AddDictionaryElementEx(const tTJSString & name, const tTJSVariant & val) { SElementData* pData = new SElementData; pData->name = name; pData->value = val; pElementTempList->push_back(pData); } // 辞書に要素追加終了 void tTJSExprNode::AddDictionaryElementExEnd() { tjs_uint32 Size = pElementTempList->size(); if(Size == 0) { delete pElementTempList; pElementTempList = NULL; return; } // 予め要素数指定で再構築する if(64 <= Size) // 足きり { tTJSDictionaryNI *dicni = NULL; tTJSVariantClosure clo = Val->AsObjectClosureNoAddRef(); if(TJS_SUCCEEDED(clo.Object->NativeInstanceSupport(TJS_NIS_GETINSTANCE, TJSGetDictionaryClassID(), (iTJSNativeInstance**)&dicni)) ) { dicni->RebuildHashByCount(Size); } } // 辞書に蓄えておいた要素を流し込む list< SElementData* >::iterator it = pElementTempList->begin(); list< SElementData* >::iterator end = pElementTempList->end(); while(it != end) { SElementData* pElement = *it; AddDictionaryElement(pElement->name, pElement->value); delete pElement; it++; } pElementTempList->clear(); delete pElementTempList; pElementTempList = NULL; }

次に構文解析処理で一時保管用関数を使用するように修正。
AddDictionaryElementをAddDictionaryElementExに置き換える。

※追記
以下のtjs.tab.cppはtjs.yから生成されるファイルで、
正式にはそちらのファイルを修正してbisonを通して生成すべきのようです。
じんさんご指摘ありがとうございます。


tjs.tab.cpp
/* Line 1455 of yacc.c */ #line 839 "syntax/tjs.y" { tTJSExprNode *node = cc->MakeNP0(T_CONSTVAL); iTJSDispatch2 * dsp = TJSCreateDictionaryObject(); node->SetValue(tTJSVariant(dsp, dsp)); dsp->Release(); cc->PushCurrentNode(node); cn->AddDictionaryElementExBegin(); ;} break; case 255: /* Line 1455 of yacc.c */ #line 846 "syntax/tjs.y" { (yyval.np) = cn; cn->AddDictionaryElementExEnd(); cc->PopCurrentNode(); ;} break; case 259: /* Line 1455 of yacc.c */ #line 859 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (4)].num)), - lx->GetValue((yyvsp[(4) - (4)].num))); ;} break; case 260: /* Line 1455 of yacc.c */ #line 860 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (4)].num)), + lx->GetValue((yyvsp[(4) - (4)].num))); ;} break; case 261: /* Line 1455 of yacc.c */ #line 861 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), lx->GetValue((yyvsp[(3) - (3)].num))); ;} break; case 262: /* Line 1455 of yacc.c */ #line 862 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), tTJSVariant()); ;} break; case 263: /* Line 1455 of yacc.c */ #line 863 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), (yyvsp[(3) - (3)].np)->GetValue()); ;} break; case 264: /* Line 1455 of yacc.c */ #line 864 "syntax/tjs.y" { cn->AddDictionaryElementEx(lx->GetValue((yyvsp[(1) - (3)].num)), (yyvsp[(3) - (3)].np)->GetValue()); ;} break;

まとめ

足きりの定数は適当に決めたものである。
pElementTempListは例外が飛んだ場合などに備えてクリア処理やデストラクタでも破棄すべきだろう。
辞書生成直後に再構築するようなケースではコンストラクタを通した方がより無駄がないと思うが
今回はできるだけ元の構造をいじらないアプローチにしてみた。


マスクを動かす

マスクデーカーはマスク画像を使って表示制限を行うが、
「画像」とは「画像ファイル」じゃなくてもいいんじゃないか?
と、勘のいい人なら気づくかもしれない。

そう、マスクデーカーにはマスクとして「画像ファイル」ではなく、
「画像として扱えるデーカー」を指定することもできる。
やり方はGetDecor関数でデーカーへの参照を取得しmaskパラメータに渡すだけだ。


CreateMask(name="マスク", mask=GetDecor("画像を持つデーカー"));

これでいったい何ができるのかというと、
マスクにバッファデーカーを指定することでマスク画像を動かすことが可能になる。
試してみよう。


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="マスク", effect=$effect, time=1000);
  wait 1000;
  Transform(to="マスク", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateBuffer(name="バッファ", w=1280, h=720, x=10000);
  CreateImage(name="バッファ/星", image="マスク.png"
    , x="Center", y="Middle", ox="Center", oy="Middle");
  Enter(to="バッファ");
  Enter(to="バッファ/星");
  Rotate(to="バッファ/星", time=30000, angle=360);
  Zoom(to="バッファ/星", time=30000, sx=400%, sy=400%);

  CreateImage(name="画像2", image="画像2.png");
  CreateMask(name="マスク", mask=GetDecor("バッファ"), mask_mode="Blue");
  CreateImage(name="マスク/画像1", image="画像1.png");
  CreateObject(name="マスク/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="マスク/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

mask_modeというパラメータは画像の何の情報を不透明度として扱うかという指定である。
デフォルトでは自動で判断されるのだが、今回は都合が悪いので直接指定している。
“Blue”は青色要素を不透明度とする指定だ。

ちなみに似たようなことがUniversalTransit命令などでも可能である。


マスクを使う

バッファデーカーは子デーカーの表示をバッファの領域内に制限するので
ウィンドウデーカーのような用途でも使用できる。

実はウィンドウデーカーには「回転できない」という欠点がある。
ただしその分非常に高速に動作する。
バッファデーカーはウィンドウデーカーよりもかなり低速だが、
画像として扱えるのでもちろん回転も可能だ。

しかしウィンドウデーカーもバッファデーカーも表示を制限できるのは四角の形だけである。
丸形や星形に表示を制限するといったようなことはできない。

Decor_Mask マスクデーカー

バッファデーカーを拡張し、自由な形に表示制限できる窓を実現するのがマスクデーカーである。
マスクデーカーはマスク画像を使ってバッファに写しこまれた画像に透明部分を設定する。
バッファは画像として扱えるのだから、バッファの表示を制限したい部分を透明にしてやれば、
自由な形に表示を制限することができるという寸法だ。

マスクデーカーを作成するにはCreateMask命令を使用する。


CreateMask(name="マスク", mask="マスク.png");

組み込んでみよう。

マスク.png
Mask


class 煙クラス
{
  method 煙クラス()
  {
  }

  method OnEnter()
  {
    int $number = 0;
    while(true)
    {
      string $name = "煙粒"+String($number++);
      ThreadCreate(call=@煙粒スレッド(name=$name));
      wait 100;
    }
  }

  method 煙粒スレッド(string $name)
  {
    CreateImage(name=$name
      , x=0, y=0, sx=20%, sy=20%, ox="Center", oy="Middle"
      , angle=rand_range(0,359), image="煙粒.png");
    Enter(to=$name);

	int $time = 2000;
    int $angle = 90+rand_range(-10,10);
    float $rad = radian(Float($angle));
    float $move = 300.0;
    float $x = $move*cos($rad);
    float $y = -$move*sin($rad);

    Move(to=$name, time=$time, x=$x, y=$y, step="DecSin");
    Zoom(to=$name, time=$time, sx=200%, sy=200%);
    Opaque(to=$name, time=$time, alpha=0%, step="AccSin");
    WaitDecor(to=$name);
    Delete(to=$name);
  }
}

method Test(string $effect)
{
  Transform(to="マスク", effect=$effect, time=1000);
  wait 1000;
  Transform(to="マスク", effect=$effect, time=1000, begin=100%, end=0%);
  wait 1000;
}

method Main()
{
  CreateImage(name="画像2", image="画像2.png");
  CreateMask(name="マスク", mask="マスク.png");
  CreateImage(name="マスク/画像1", image="画像1.png");
  CreateObject(name="マスク/煙", x=1280/2, y=720/2+150, class=@煙クラス());
  Enter(to="*");
  Enter(to="マスク/*");
  wait 3000;

  call @Test(effect="LinearLeft");
  call @Test(effect="BlindLeft");
  call @Test(effect="CurtainLeft");
  call @Test(effect="ShaveLeft");
  call @Test(effect="SlashH");
  call @Test(effect="BoxCenter");
  call @Test(effect="HoleCenter");
  call @Test(effect="FanCenter");
  call @Test(effect="TensileLeft");
  call @Test(effect="Mosaic");
}
YouTube Preview Image

今回も変更点はごく僅かだ。
このようにFoooではデーカーの「組み合わせ方」を利用した機能がとても多く
Foooの大きな特徴の一つである。