Windows ストア アプリでパラパラアニメを動かそう

Windows 8 Store apps Advent Calendar : ATND
http://atnd.org/events/33803

の 15 日目のネタですね。本当は「親馬鹿アプリ」のほうにする予定だったのですが、ずるずる状態になってしまったので、以前ブログに書こうと思っていたネタを掘り出してきました。

最初に言うと「書こうと思っていた」というのは、まだ調査途中で「本当にこれが効率的なのか?」、「定番なのか?」ってのがわからないので、保留にしていたわけで…ええ、忘れていたのもぽちぽちなのですが、まあ、年末だし蔵出しということで。

さて、パラパラアニメといえば「アニメgif」なのですが、いろいろ調べてみると、WPF でうまく表示する方法がありません。いや、探せば「MediaElement」を使う方法がでているので、Windows ストア アプリでも使えるのかもしれません(試しておりません)。ですが、今回はもうちょっと違った方法を取ります。

よくゲームアプリで使われている手法で、俗に「うなぎの画像」と呼ばれる…って呼びませんね、なんと言うのかわからないのですが、横に長い画像を使います。ひとつひとつのセルを横につなげて、表示したい位置をスライドさせるという手法ですね。実は、C++/CX のほうには、これが使える Bitmap オブジェクトがあってそれが「ウリ」だったりすのですが、果たして C# にはあるのか?って、のを夏のセッションを聞いたときに思ったのですが、そのところはちょっと不明です。

ちなみに C# で WritableBitmap を作る方法は、

byte配列からWriteableBitmapオブジェクトを作成する – 酢ろぐ
http://d.hatena.ne.jp/ch3cooh393/20120802/1343892131

を参照ということで。

この「うなぎ画像」を使うと便利なのは、

  • それぞれのセルがひとつに連なっているので、管理が簡単?
  • 複数のセルを読み込まないで済むので、ロード時間が少なくて済む。

ってところです。実際、やってみるとわかるのですが、60 枚近い画像を切り替えるのと、1枚の画像をクリップしながら表示するのでは、CPU への影響が格段に違い、アニメーションもスムースになります。

というわけで、ざっと作り方を紹介。

■うなぎ画像を作る。

適当に Form アプリで「うなぎ画像」を作るツールを作ります。

private void button1_Click(object sender, EventArgs e)
{
    int w = 46*60;
    int h = 46;

    for (int n = 1; n <= 6; n++)
    {
        Bitmap allBmp = new Bitmap(w, h);
        Graphics g = Graphics.FromImage(allBmp);
        for (int i = 1; i <= 60; i++)
        {
            string p = string.Format(@"\temp\balls\{0}\{1:00}.png", n,i);
            Bitmap bmp = new Bitmap(p);
            g.DrawImage(bmp, 46 * (i - 1), 0, 46, 46);
        }
        allBmp.Save(string.Format( @"\temp\ball{0}.png",n));
    }
}

画像自体は、とあるゲームからパクってきたものです。
画像サイズが、46×46 固定なのは、まあ、ツールなので。

■アニメ用のユーザーコントロールを作る

いくつか試したのですが、ユーザーコントロールを作るのが一番楽です。

<UserControl
    x:Class="AniBallControls.AniBall"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:AniBallControls"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    d:DesignHeight="92"
    d:DesignWidth="92">
	<UserControl.Resources>
        <Storyboard x:Name="sbAnime" RepeatBehavior="Forever">
			<DoubleAnimationUsingKeyFrames x:Name="keyFrames1" 
				Storyboard.TargetProperty="(Canvas.Left)" 
				Storyboard.TargetName="img">
                <DiscreteDoubleKeyFrame KeyTime="0:0:0" Value="0"/>
            </DoubleAnimationUsingKeyFrames>
		</Storyboard>
	</UserControl.Resources>
    <Canvas>
        <Image x:Name="img" Source="ms-appx:///balls/ball1.png" 
			Width="5520" Height="92" 
			Canvas.Left="0" Canvas.Top="0"/>
        <Canvas.Clip>
            <RectangleGeometry Rect="0,0,92,92"></RectangleGeometry>
        </Canvas.Clip>
    </Canvas>
</UserControl>

46×46 では、デモ用には小さかったので、96×96 に拡大しています。
storyboard を使ってアニメーションさせていますが、中身は後からプログラムで書きます。最初はツールを使って XAML に書いていたのですが、プログラムで書けることが分かったので実行時に書き込みます。
Canvas の上に乗っけて、Canvas.Clip しているところがミソですね。たぶん、WPF とか Silverlight での定番の処理だと思うのですが、似たようなは見つかりませんでした。他にいい方法があるのかも? この clip 位置を、スライドさせる(実際には Image オブジェクトの位置をずらす)ことで、アニメーションができます。

デザイナで見ると、うまくクリップされていることがわかります。本当は横に長い画像なのですが、一番左のセルだけがクリップされています。

で、実際にアニメーションさせるコードがこちら。

public sealed partial class AniBall : UserControl
{
    public AniBall()
    {
        this.InitializeComponent();
    }

    private Size _imageSize = new Size(92, 92);
    private int _curIndex = -1;
    private int _imageCount = 60;

    public ImageSource Source 
    {
        get { return this.img.Source; }
        set { this.img.Source = value; }
    }

    public void Start()
    {
        var kf = this.keyFrames1;
        kf.KeyFrames.Clear();
        for (int i = 0; i < _imageCount; i++)
        {
            var dd = new DiscreteDoubleKeyFrame();
            dd.Value = -_imageSize.Width * i;
            dd.KeyTime = new TimeSpan(10 * 1000 * i * (1000/60)); // 1/60 sec
            kf.KeyFrames.Add(dd);
        }
        this.sbAnime.Begin();
    }
    public void Stop()
    {
        this.sbAnime.Stop();
    }

    private void onTimer(object sender, object e)
    {
        _curIndex++;
        if (_curIndex >= _imageCount)
            _curIndex = 0;
        Canvas.SetLeft(this.img, - _imageSize.Width * _curIndex);
    }
}

普通の storyboard では、開始位置と終了位置を連続につなぎますが(直線とかスプラインとか)、ぱらぱらアニメの場合は、とびとびの値にします。この飛び飛びの値を作るのが DiscreteDoubleKeyFrame クラスですね。これは Blend の Storyboard でも変更できるので、ちまちま 60 フレーム作ることも可能なのですが…面倒なので、プログラムでやります。

この ontimer の負荷がどのくらいかというと、

なところで、3% 以下ですね。ひとつのアニメなので、複数配置したときはどうなるのかは検証していませんが、普通のアニメGIFっぽいことをしたいのであれば、これで OK かと。
ちなに、60 枚の画像切り替えをすると、CPU は 3% 程度で同じなのですが、画面の駒落ちが発生します。どうやら、Image.Source に設定するときが重いらしく、そのあたりの負荷を減らすためにも、一枚のうなぎ画像を使ったほうがよいみたいです。

■メインページを作る

テスト用のメインページには、こんな風に貼り付けます。ユーザーコントロールはツールバーに出てくるので、普通にドラッグ&ドロップすれば ok.

<local:AniBall x:Name="aniBall1" Source="ms-appx:///balls/ball1.png"
    HorizontalAlignment="Left" Height="100" 
                Margin="251,266,0,0" VerticalAlignment="Top" Width="100"/>

タップイベントを設定しておいて、開始と終了を制御します。

AniBall curBall = null;
private void aniBall_Tapped(object sender, TappedRoutedEventArgs e)
{
	if (curBall != null)
	{
		curBall.Stop();
		curBall = null;
	}
	else
	{
		curBall = sender as AniBall;
		curBall.Start();
	}
}

実は、Storyboard が実行中かどうかを取れるので、それをプロパティにすれば良いのですが…まあ、デモなので、これで。

■実行してみる

動かしてみると、結構スムースにくるくると回ります。これだったらゲームに使えるよねっていう感じで、これぐらいだったら、DirectX + C++/CX を使う必要はあるまい(本来はこれが目的)、っていう感じで動きます。

ちなみに、Image コントロールは、透過PNG を扱えるので、背景を付けることもできます。

これができると、パズルゲームぐらいならば、C# で書けそうですよね…って感じですかね。年末年始の休みを利用して、ひとついかがでしょうか? ええ、私は、たぶんお仕事かと orz.

カテゴリー: C#, windows 8 パーマリンク