こんにちは。今回ブログを担当します 藤澤です。
前回の記事ではパーティクル編集用のツールを自分で作ってみよう ということについてご紹介しました。今回は作成したパーティクルを実際にアプリで使用する方法について もう少し詳しく触れてみたいと思います。
plist の読み書き
アプリでパーティクルを使用するにはプロパティの値を plist に書き出すと便利です。plist ファイルとは、要は XML ファイルなのですが、一般的な XML が
<キー1>値1</キー1> <キー2>値2</キー2>
という形なのに対して
<key>キー1</key> <string>値1</string> <key>キー2</key> <real>値2</real>
という形になっており、書き出しはともかく読み込みにはちょっと不便です。
C# なら LINQ を使って
int i = 0; int j = 0; return XElement.Load(file).Elements("dict").Elements() .GroupBy(_ => i = i + ++j % 2) // 1, 1, 2, 2, …… という数列を作って先頭から 2 件ずつグルーピングする .ToDictionary(x => x.ElementAt(0).Value, x => x.ElementAt(1).Value);
こんな感じに Dictionary に変換してあげると扱いやすくなると思います。
また、このままだと いちいちキャストするのが面倒なので、dynamic を使ってアクセスできるようにすると もう少しきれいになりそうです。
dynamic を適用して、plist の書き出しまで実装したサンプルは こんな感じになります。
public class PropertyList : DynamicObject { public static dynamic From(string file) { if (!File.Exists(file)) return null; return new PropertyList(file); } public static void Save(PropertyList plist, string file) { plist.Save(file); } public void Save(string file) { var toXElement = new Func<object, XElement>(o => { switch (Convert.GetTypeCode(o)) { case TypeCode.Int16: case TypeCode.Int32: case TypeCode.Int64: return new XElement("integer", o); case TypeCode.Single: case TypeCode.Double: return new XElement("real", o); default: return new XElement("string", o); } }); var values = this.plist.Keys .Select(k => new { Key = k, Value = this.plist[k] }) .SelectMany(kv => new[] { new XElement("key", kv.Key), toXElement(kv.Value) }); var root = new XDocument( new XDocumentType("plist", "-//Apple//DTD PLIST 1.0//EN", "http://www.apple.com/DTDs/PropertyList-1.0.dtd", null), new XElement("plist", new XAttribute("version", "1.0"), new XElement("dict", values) ) ); root.Save(file); } private IDictionary<string, object> plist; public PropertyList(string file) { if (!File.Exists(file)) return; var toObject = new Func<XElement, object>(x => { switch (x.Name.LocalName) { case "integer": return int.Parse(x.Value); case "real": return double.Parse(x.Value); default: return x.Value; } }); var i = 0; var j = 0; this.plist = XElement.Load(file).Elements("dict").Elements() .GroupBy(_ => i = i + ++j % 2) .ToDictionary(x => x.ElementAt(0).Value, x => toObject(x.ElementAt(1))); } public override bool TryGetMember(GetMemberBinder binder, out object result) { result = this.plist[binder.Name]; return true; } public override bool TrySetMember(SetMemberBinder binder, object value) { this.plist[binder.Name] = value; return true; } }
dynamic の欠点として IntelliSense が効かなくなってしまうので、かわりに static で Save を呼べるようにしています。
使用する際はこんな感じになります。
// plist を読み込んで var plist = PropertyList.From("foo.plist"); // 値を取得 double d = plist.duration; // 値を変更して plist.angle = 90; // plist に書き出し PropertyList.Save(plist, "bar.plist");
plist の key 名
CCParticleSystem の initWithDictionary を見るとわかりますが、plist の key と CCParticleSystem のプロパティとの対応は次のようになっています。
プロパティ | key |
---|---|
angle | angle |
angleVar | angleVariance |
blendFunc | blendFuncSource, blendFuncDestination |
duration | duration |
emitterMode | emitterType |
endColor | finishColorAlpha, finishColorRed, finishColorGreen, finishColorBlue |
endColorVar | finishColorVarianceAlpha, finishColorVarianceRed, finishColorVarianceGreen, finishColorVarianceBlue |
endRadius | minRadius |
endRadiusVar | なし |
endSize | finishParticleSize |
endSizeVar | finishParticleSizeVariance |
endSpin | rotationEnd |
endSpinVar | rotationEndVariance |
gravity | gravityx, gravityy |
life | particleLifespan |
lifeVar | particleLifespanVariance |
posVar | sourcePositionVariancex, sourcePositionVariancey |
radialAccel | radialAcceleration |
radialAccelVar | radialAccelVariance |
rotatePerSecond | rotatePerSecond |
rotatePerSecondVar | rotatePerSecondVariance |
sourcePosition | sourcePositionx, sourcePositiony |
speed | speed |
speedVar | speedVariance |
startColor | startColorAlpha, startColorRed, startColorGreen, startColorBlue |
startColorVar | startColorVarianceAlpha, startColorVarianceRed, startColorVarianceGreen, startColorVarianceBlue |
startRadius | maxRadius |
startRadiusVar | maxRadiusVariance |
startSize | startParticleSize |
startSizeVar | startParticleSizeVariance |
startSpin | rotationStart |
startSpinVar | rotationStartVariance |
tangentialAccel | tangentialAcceleration |
tangentialAccelVar | tangentialAccelVariance |
totalParticles | maxParticles |
texture | textureImageData |
なし | textureFileName |
それぞれの key 名に対応するプロパティの値を書き出します。値を書き出す際のタグは、整数は integer、実数は real、文字列は string となります。
position や color のような構造体はメンバごとに key が分かれていますので、それぞれの key に値を書き出せば OK です。BlendFunc の値は以下のように定義されていますので integer で書き出してやれば OK です。
#define GL_ZERO 0 #define GL_ONE 1 #define GL_SRC_COLOR 0x0300 #define GL_ONE_MINUS_SRC_COLOR 0x0301 #define GL_SRC_ALPHA 0x0302 #define GL_ONE_MINUS_SRC_ALPHA 0x0303 #define GL_DST_ALPHA 0x0304 #define GL_ONE_MINUS_DST_ALPHA 0x0305 #define GL_DST_COLOR 0x0306 #define GL_ONE_MINUS_DST_COLOR 0x0307 #define GL_SRC_ALPHA_SATURATE 0x0308
注意する必要があるのは texture くらいかと思います。
texture の値
パーティクルに使用するテクスチャの画像を指定する方法は 2 通りあります。
- 画像ファイルをリソースに持たせて textureFileName にファイル名を指定する。
- textureImageData に画像のデータを書き出す。
1.はとくに問題ないと思います。
2.の場合 画像ファイルのバイナリデータを zlib もしくは gzip で圧縮した後、Base64 エンコードした文字列を書き出します。
zlib や gzip 圧縮というと C# では DeflateStream や GZipStream クラスがありますが、残念ながらこれらで圧縮したデータは CCParticleSystem では読み込めません(※)。かわりに、zlib.net などのライブラリを使う必要があります。
※ zlib は RFC1950、DeflateStream は RFC1951 となっており、実装する RFC が異なるようです。確認はしていませんが、MSDN の記述を見ると .NET Framework 4.5 以降なら大丈夫かもしれません。
逆に、他のツールで作成された plist を C# 側で読み込む場合、zlib 形式は DeflateStream では読めませんが gzip 形式は GZipStream で読むことができました。
これらを踏まえて、テクスチャの読み書きをするサンプルはこちらです。
public class TextureHelper { public static string EncodeTexture(byte[] data) { using (var output = new MemoryStream()) using (var zip = new ZOutputStream(output, zlibConst.Z_DEFAULT_COMPRESSION)) { zip.Write(data, 0, data.Length); zip.Flush(); zip.finish(); return Convert.ToBase64String(output.ToArray()); } } public static byte[] DecodeTexture(string data) { var gzipped = Convert.FromBase64String(data); return DecodeTextureAsGzip(gzipped) ?? DecodeTextureAsZlib(gzipped); } private static byte[] DecodeTextureAsGzip(byte[] data) { try { using (var input = new MemoryStream(data)) using (var gzip = new GZipStream(input, CompressionMode.Decompress)) using (var output = new MemoryStream()) { CopyStream(gzip, output); return output.ToArray(); } } catch { return null; } } private static byte[] DecodeTextureAsZlib(byte[] data) { try { using (var input = new MemoryStream(data)) using (var output = new MemoryStream()) using (var zip = new ZOutputStream(output)) { CopyStream(input, zip); return output.ToArray(); } } catch { return null; } } private static void CopyStream(Stream input, Stream output) { int len; var buf = new byte[1024]; while ((len = input.Read(buf, 0, buf.Length)) > 0) output.Write(buf, 0, len); output.Flush(); } public static BitmapSource ImageFrom(byte[] data) { using (var input = new MemoryStream(data)) { var decoder = new PngBitmapDecoder(input, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); return decoder.Frames[0]; } } }
使用する際はこうなります。
// テクスチャの書き出し var data = File.ReadAllBytes("foo.png"); var texture = TextureHelper.EncodeTexture(data); // テクスチャを読み込んで画像に変換 data = TextureHelper.DecodeTexture(texture); var img = TextureHelper.ImageFrom(data);
いかがでしょうか。これくらいのツールなら意外と簡単に実装できますので試していただければと思います。