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.





