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.