飴屋

WPF/日記8

アニメーションの設定

前回、アバタのアニメーション設定でつまづいてから、長いこと試行錯誤を繰り返した結果、ようやく出口がみえてきた感じです。問題点を整理すると、Identityで書き出した3Dモデルデータとアニメーションデータをリソース辞書に登録した際に以下のような問題が発生していたのでした。

  1. リソースの中ではx:Name属性が使えない。(x:Keyで参照されるだけの存在)
  2. リソースの中のアニメーション情報()のStoryboard.TargetName要素では、アニメーション対象の名前(x:Name)属性を指定しなければならない。

アニメーション対象の指定にどうしてx:Name要素が必要であるのかは未だにわかっていませんが、コード上で動的にアバタを増やすには避けては通れない道のようですのでリソース辞書内のx:Name属性のない右足と左足を動かすべく、以下のように対処してみました。

NameScope

myAvatar = (ModelVisual3D)ChatWorld.FindResource("myMan");
ChatWorld.Children.Add(myAvatar);

まず、上記のコードで動的に追加されたアバタに名前をつけてあげます。NameScopeという仕組みを使うとのことでしたので以下のようにコードを変更して"myavatar"と名前をつけてみました。

myAvatar = (ModelVisual3D)ChatWorld.FindResource("myMan");
NameScope namescope = new NameScope();
namescope.RegisterName("myavatar", myAvatar);
NameScope.SetNameScope(this, namescope);
ChatWorld.Children.Add(myAvatar);

これで、アニメーション情報の対象に"myavatar"と指定してやれば、"myavatar"に含まれる任意のプロパティを動かしてやることができるはずです。以下、右足の回転アニメ情報を掲載します。Storyboard.TargetNameに"myavatar"を指定しています。

<Rotation3DAnimationUsingKeyFrames RepeatBehavior="Forever" BeginTime="0:0:0" Duration="0:0:2.000000"
Storyboard.TargetName="myavatar"
Storyboard.TargetProperty="(ModelVisual3D.Content).Children[0].Children[2].
(Model3DGroup.Transform).(Transform3DGroup.Children)[2].(RotateTransform3D.Rotation)">
...
</Rotation3DAnimationUsingKeyFrames>

Identityでは右足と左足を含むに名前をつけてアニメーション対象にしていましたが、アニメーション対象が"myavatar"に変わってしまったので、Storyboard.TargetProperty属性も変更する必要があります。"myavatar"をルートにして、子供の子供のTransformの子供のRotateTransform3DのRotation属性がアニメーションの対象となっていますが、すごいわかりにくいですね。このPropertyPathの書式がさっぱりわからず、ずっと画面の前で固まっていたことが思い出されます。多分、もう少しシンプルに書きなおせるはずなのですが、もうしばらくいじりたくないので次にいきます。

自分の中でもやもやしていたことがあったのでついでに書いておきますと、ContentとChildrenって何がどう違うのかわかりませんでした。一つのタグの中の抱合物という点では一緒ですが、ちゃんと意味があって書き分けられているようです。HTMLやXMLに通じている人なら掴みやすいかもしれません。

モデルオブジェクト要素
単数ContentChild
複数ItemsChildren

Storyboard.SetTargetNameメソッド

さらに話を進めて、「アニメーション情報に"myavatar"と書きたくない」という欲求が湧いてきたので解決してみました。"myavatar"という名前はコード上で動的につけた名前ですので、XAML上では使いたくありません。そこでまずXAML上ののStoryboard.TargetNameを全部消してしまいました。そしてこれらのタグを囲っていたStoryboardオブジェクトに対してコード上で名前をつけてやることにしました。

Storyboard walking = (Storyboard)ChatWorld.FindResource("Animation_man_Animation_0");
Storyboard.SetTargetName(walking, "myavatar");

リソース辞書からアニメーション情報を引っ張り出した後で、Storyboard.SetTargetNameメソッドでTargetNameを"myavatar"に設定しています。これでStoryboard内の全てのアニメーション(DoubleAnimationUsingKeyFramesやRotation3DAnimationUsingKeyFrames)のStoryboard.TargetNameがうまく設定されたようです。

キー操作再び

アニメーションの設定が終わったところでビルドを通してみると、キー入力に対してアニメーションが再生と停止を行ってくれたのはいいのですが、どうもキーを押した瞬間(KeyDownイベント発生時)にちょっと動いてすぐに止まってしまうという現象が発生しました。おそらくキーを押しっぱなしにしている状態は、キーが連打されている状態と等しいようで、キーが連打された分だけアニメーションを頭から再生しようとしているのが止まっているように見えたということのようでした。そこで先日買ってきたWPF 3Dプログラミングを眺めてみて、実践的なサンプルコードを参考にキーイベントの処理方法を変えてみることにしました。前にViewPort3DタグにKeyUpプロパティとKeyDownプロパティを追加しましたが、これを削除し、新たにタイマーをつかってイベントを監視する方法を選択してみました。

タイマーの追加

ここでいうタイマーは一定時間ごとに決まった処理を平行して行ってくれることを指しています。すなわち別のスレッドをたてる必要があるので・・・

using System.Windows.Threading;

を追加しておきます。次にタイマーを生成します。

private DispatcherTimer timer = new DispatcherTimer();

そして、Page1のコンストラクタに30ミリ秒毎にupdateAvatarメソッドを呼ぶように設定します。30ミリ秒毎に呼び出されるということは秒間33フレームぐらいでしょうか。

timer.Interval = TimeSpan.FromMilliseconds(30);
timer.Tick += new EventHandler(this.updateAvatar);
timer.Start();

キーイベントを取得

updateAvatarメソッドには引数にイベント情報がくっついてくるので、これを確認すれば、キーボードのボタンごとに処理があれこれ書けるようになります。上下左右のカーソルキーでアバタを操作する場合はそれぞれのボタンについて以下のように動作を書いてやります。(詳細は省略)

void updateAvatar(object sender, EventArgs e)
{
if (Keyboard.IsKeyDown(Key.Up))
{
....
}
else if (Keyboard.IsKeyDown(Key.Down))
{
....
}
else if (Keyboard.IsKeyDown(Key.Right))
{
....
}
else if (Keyboard.IsKeyDown(Key.Left))
{
....
}
}

歩くアニメーションは以下の処理で開始します。何度も再生しないようにアニメーション状態のフラグを作ってやると正しく動きました。。

//辞書から読み込み
Storyboard walking = (Storyboard)ChatWorld.FindResource("Animation_man_Animation_0");
//ターゲット設定
Storyboard.SetTargetName(walking, "myavatar");
//開始
walking.Begin(this, true);

止めるのは以下の通り。

walking.Stop(this);

移動と回転

アニメーションができても足踏みするばかりなので、体の向きを変えたり、実際に歩みを進めてみることにします。

float angle = (アバタの向き(度数法));
float radian; //アバタの向き(弧度法)
Vector3D pos = new Vector3D(0, 0, 0); //アバタの現在位置
radian = (float)(angle / 360 * Math.PI * 2); //ラジアン角に変換
pos.X += (float)(Math.Sin(radian)); //進行方向に向かってX前進
pos.Z += (float)(Math.Cos(radian)); //進行方向に向かってZ前進
Matrix3D m = new Matrix3D(); //座標変換用行列
Quaternion q = new Quaternion(new Vector3D(0, 1, 0), angle);
m.Rotate(q);
m.OffsetX = pos.X;
m.OffsetY = pos.Y;
m.OffsetZ = pos.Z;
MatrixTransform3D trans = new MatrixTransform3D(m); //行列からTransform作成
myAvatar.Content.Transform = trans;

こんな感じでangleで示した方向に歩いていってくれるようになりました。アバタを複数表示する段になったら、アバタをクラス化してメンバ変数にposやangleを用意してやらないといかんなと思いました。

WPF