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 アプリで「うなぎ画像」を作るツールを作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | 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 固定なのは、まあ、ツールなので。
■アニメ用のユーザーコントロールを作る
いくつか試したのですが、ユーザーコントロールを作るのが一番楽です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | < 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 オブジェクトの位置をずらす)ことで、アニメーションができます。
デザイナで見ると、うまくクリップされていることがわかります。本当は横に長い画像なのですが、一番左のセルだけがクリップされています。
で、実際にアニメーションさせるコードがこちら。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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.
1 2 3 | < local:AniBall x:Name = "aniBall1" Source = "ms-appx:///balls/ball1.png" HorizontalAlignment = "Left" Height = "100" Margin = "251,266,0,0" VerticalAlignment = "Top" Width = "100" /> |
タップイベントを設定しておいて、開始と終了を制御します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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.