読者です 読者をやめる 読者になる 読者になる

WonderPlanet DEVELOPER BLOG

ワンダープラネットの開発者ブログです。モバイルゲーム開発情報を発信。

Particle 用の plist ファイルを読み書きする

こんにちは。今回ブログを担当します 藤澤です。

前回の記事ではパーティクル編集用のツールを自分で作ってみよう ということについてご紹介しました。今回は作成したパーティクルを実際にアプリで使用する方法について もう少し詳しく触れてみたいと思います。

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 通りあります。

  1. 画像ファイルをリソースに持たせて textureFileName にファイル名を指定する。
  2. 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);  

いかがでしょうか。これくらいのツールなら意外と簡単に実装できますので試していただければと思います。