例のキヨシチェックを Sprache で書いてみる

はじめに

http://qiita.com/beinteractive/items/a2264048a601ee998aca

例の(?)が何だか解らないんですが、こういうのが上がってきたので、パーサの練習がてら書いてみました。

使ったもの

https://github.com/sprache/Sprache

C# の Monadic Parser だそうです。Monadic って Haskell で出てきたモナド的なヤツでしょうか。

できたもの

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
44
45
46
47
48
49
50
51
52
53
54
55
using System;
using System.Collections.Generic;
using System.Text;
using Sprache;
using System.IO;

public class ZundokoGenerator {
Random _random = new Random();
public IEnumerator<string> RandomZunDoko() {
for (;;) {
yield return _random.Next(0, 2) == 0 ? "ズン" : "ドコ";
}
}
}

public static class KiyoshiParser {
public static readonly Parser<string> zun =
from txt in Parse.String("ズン").Text()
select txt;
public static readonly Parser<string> doko =
from txt in Parse.String("ドコ").Text()
select txt;
public static readonly Parser<string> zzzd =
from z0 in zun
from z1 in zun
from z2 in zun
from z3 in zun
from dk in doko
select "キヨシ";
}

class Program {
static void Main() {
var zdg = new ZundokoGenerator().RandomZunDoko();
var sb = new StringBuilder();
int count = 0;
for (;;) {
sb.Clear();
for (int i = 0; i < 5; ++i) {
zdg.MoveNext();
sb.Append(zdg.Current);
}
++count;
try {
var kys = KiyoshiParser.zzzd.Parse(sb.ToString());
Console.WriteLine(sb.ToString() + kys);
Console.WriteLine(count + "回ズンドコしました");
break;
}
catch (Sprache.ParseException) {
Console.WriteLine(sb.ToString());
}
}
}
}

解説

ZundokoGenerator

クラス ZundokoGenerator は元になった記事からパクってきましたが、実は UnityEngine.Random が使われていたので、 System.Random を使うように変更しました。

IEnumerator で、ズン or ドコを延々と返すだけですね。

KiyoshiParser

パーサ本体です。

文法についてはなんとなく見てもらえば解ると思いますが、 LINQ のクエリ構文で繋いでいく感じです。

詳しいところは Sprache のドキュメントをあたってもらった方がいいと思いますが、 bisonyacc でいうパターンを from の羅列で記述し、 select でアクションといった感じです。

注意する点は、 Parse.String() は返値が IEnumerator<char> (だっけ?)になっているので、 string に変換してやる必要があるところです。

Program

メインの処理です。

正しくないズンドコ(?)の際は例外で処理しています。

コンパイル&結果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
% mcs main.cs -r:Sprache.dll ; mono main.exe
ドコズンズンズンズン
ズンズンズンズンズン
ドコドコズンドコズン
ドコズンドコズンズン
ドコズンズンドコズン
ドコドコズンドコズン
ズンドコドコズンズン
ドコズンドコドコズン
ズンドコズンズンドコ
ズンドコズンドコドコ
ズンドコドコズンズン
ドコドコズンドコズン
ズンドコドコズンドコ
ドコドコズンズンズン
ドコズンドコズンドコ
ドコズンズンズンズン
ズンドコドコドコズン
ズンズンズンズンドコキヨシ
18回ズンドコしました

簡単ですね('ω`)

SpriteKit でゲームを作ってみる

動機

iOS7 Tech Talks に当選したので、いそいそとお出かけしたら、ゴイスーなホテルでランチまでごちそうになってしまった。その割にアウトプットも無い自分なので、これは申し訳ないなと思い、 Apple 社激推しの SpriteKit でも使ってゲームでも作ってみるかと思ったのだが、例によって締切がないと動かない体質が邪魔をしたので、 Advent Calendar に登録して退路を断ってみた次第('ω`)

偶然にも、本日 iOS7 Tech Talks のビデオ が公開されたらしいので、是非そちらも(要 Developer アカウント)。

最初に書いておきますが、最後まで出来ませんでした……('A`)

SpriteKit とは?

  • iOS7 より利用できる Apple 社謹製の 2D 描画& 2D 物理シミュライブラリ
  • OS(SDK) に最初から含まれるので、利用が簡単
  • UIKit に似た実装で、利用が簡単
  • 描画部分と物理シミュ部分が最初から統合されているので、扱いが簡単
  • パーティクルエフェクトが利用可能。パラメータを Xcode 上でプレビューしながら編集できる
  • 性能について書くと炎上するので、そこは割愛
  • OSX でも使えるので、マルチプラットフォーム(と言っていいのだろうか)

この記事では、SpriteKit を使って、ゲームを作ろうと思います。

どんなゲームを作ったか

  • SpriteKit は 2D ゲームのためのライブラリである -> 2D のゲーム……? -> 2D シューティングだ!

Screen Shot 2013-12-24 at 20.29.27.png

というわけで、懐かしいキャラバンシューティングを作ることにしました。

縛り

  • 描画周りは SpriteKit 以外利用しない
    • 大前提
  • なるべく Objective-C のみで作成する
    • Objective-C 歴自体は iPhone3G の頃からだけど、記述時間的にはさっぱりなので、 Objective-C 的にヌルい部分もあるけど、そこは目を瞑る('A`)
  • ちゃんと遊べるものにする
    • すみませんすみません('A`)

作っていく

色々書こうと思ったんですが、そもそも完成しておらず、予定していたソースの公開もままならぬ故、実際に SpriteKit でゲームを作る上で気になった点などを記していこうと思います。

SKView

SpriteKit で表示をするために必要な専用の View です。

ほぼ通常の UIView と同じですが、表示内容については、SKScene という別のクラスを渡します。この SKScene を継承してゲーム独自の処理を加えていきます。

SKScene

その名の通り、SpriteKit において、ゲームのシーン(舞台)を表します。

これに後述する SKNode を子供として追加していくことで画面を作ります。この辺は UIView の仕組みと似ていると思います。

またこのクラスは、ゲームフレーム単位で update: というメソッドが OS から呼ばれます。通常のアプリと違い、ゲームの場合、入力があって初めて処理が呼ばれるのではなく、常に処理を行っておきたいため、このような仕様になっていると思われます。

SKNode

SpriteKit における画面作りの基本になるクラスです。

UIKit では、UIView を基本として画面を構成しましたが、SpriteKit では同じように SKNode をツリー状に構成していくことで画面を作ります。実は SKSceneSKNode を継承しています。

1
2
3
4
5
SKScene* scene = [[SKScene alloc] initWithSize:CGSizeMake(320, 480)];
SKNode* node0 = [SKNode node];
[scene addChild:node0];
SKNode* node1 = [SKNode node];
[node0 addChild:node1];

このように、SKNodeSKNode の子供になることができます。UIView 等と同じく、座標系は親の影響を受けます。

派生クラス

SKNode は、それ単体では凝った何かを表示することが出来ません。

何かを表示したりする際は、SKNode の派生クラスを使います。色々ありますが、今回は SKSpriteNode のみ簡単に説明します。

SKSpriteNode は、SKNode に一枚の絵を表示する機能を追加したものです。一枚の絵は、単色の矩形か、SKTexture で指定するイメージになります。

SKTexture

SKTexture は SpriteKit におけるイメージ全般を取り扱うクラスです。UIImageView 等のように簡単にイメージを取り扱うことができます。

Xcode でいわゆるテクスチャアトラスを簡単に作ることが出来るのですが、実はこれが少し罠で、しばらくハマってた時期がありました。

SKTexture は画像一枚だけでなく、画像一枚の一部分(サブテクスチャといいます)を扱うことが出来ます。サブテクスチャは、既にある SKTexture から切り出す部分を指定することで作ることが出来ます。これは Xcode によって作られたテクスチャアトラス中のテクスチャを使う場合もそうなります。

しかし、サブテクスチャからサブテクスチャを作ることは出来ません。これが地味に困る場合があります。

具体例を挙げます。

人が歩くアニメーションパターンを用意したとしましょう。同じキャラクタなので、一枚のイメージファイル(a)に複数のパターンを追加します。そして、それをテクスチャアトラス(b)にします。

実際に使用する際、a を呼び出すことは可能です。

1
SKTexture* tex = [SKTexture textureWithImageNamed:@"a"];

で呼び出せるはずです。

ですが、ここから a 中の歩きパターンを個々に取り出そうとすると、上手くいきません。

これは、tex が既にサブテクスチャになってるためです。テクスチャアトラスからテクスチャを取り出した際には、自動的にサブテクスチャになってしまうようです。

色々試したのですが、 SKTexture がサブテクスチャなのかどうかを判定も出来ませんし、ましてや UV を書き換えることも出来ないようなので、これはプログラムというよりデータを作る段階で意識しておく必要があることになります。

簡便なのはいいのですが、UV くらい書き換えられてもいいのではないかと思います。iOS8 に期待です。

最後に

本当は他にも色々あるのですが、時間がなくて、こんな中途半端で終わって申し訳ないです。せめて、動く状態のプロジェクトを年内にはアップしたいと思いますので、興味があれば見てやってください。

ちなみに、動画はこの辺にあります

そういや、今日はクリスマスイヴですね('ω`;)

追記

すっかり忘れてましたが、ソースをアップしました

UnityEditor と実機間で通信がしたい

UnityEditorとビルド後の実機との間で通信がしたい

表題の件を、色々と今まで試行錯誤しつつやっていたんですが、公式で PlayerConnection が実装され、晴れて自前実装がお役御免になりそうです。
なんか勿体ないので記録として残しておこうかと思って書きました。決してContribute稼ぎではな(ry

昔のお話

昔々、CD-ROMというメディアでゲームが提供されていた時代のことですが、当然開発中にテストをする際もCD-ROMにゲームを焼く(書き込む)必要がありました。

しかし、CD-ROMに焼くのも時間がかかる。下手したら現在のビルド〜転送よりも時間がかかる。更に言えば、当時のライターは焼きに失敗することもままあり、ブランクのCD-ROMもタダじゃない。

では、昔の人は実機で確認する際にどうしたのかというと、開発に使っているパソコン(プログラムを書いたりコンパイルしたりする)のHDDを実機からCD-ROMドライブに見えるようにエミュレートしてました。そうすると、実行されているプログラムからCD-ROMのファイルを読もうとした際に、自動的にHDDから読み込むようになっていました。

現代でよくある話

  • パターン1

    1. UnityEditor上で開発、良い感じになったのでビルド、実機(iOS/Android等)へ転送
    2. 実機で動いたけど、動いたけど……ちょっと操作感覚が気にくわないから調整するか
    3. UnityEditor上で調整、再度ビルド、実機へ転送
    4. 実機で確認。うーんなにか違うな……
    5. 再度ビルド……
  • パターン2

    1. UnityEditor上で開発、今度はマスターデータを外部から読み込むようにしたのでいちいちビルドしなくて平気だぜ
    2. マスターデータが一カ所にしかない、あっても本番と検証の二カ所程度なので、複数人でいじると死ぬ

どちらも、ビルド〜転送が秒で完了すれば解決する話なんですが、それは現実的ではない。

もしくは、UnityRemoteでなんとかするというのもありますが、公式的にもあまり活用されていないので、微妙感。

調整したいものが、数値のような単純なものであれば実機用のデバッグメニューを用意して調整することも可能ですが、限界もあります。

UnityEditorはよくできてますし、たいていの場合UnityEditor上で動くものは実機でも同じように動きます。また、外部のゲームコントローラを使うようなゲーム機の場合はコントローラが同じなら、UnityEditorでもそこそこ操作感覚は変わりません。

現代で解決するには

実機とホストで通信が簡単に行えて、ファイルの転送ができれば良いんで、構造的に採用したのは、

  • ホスト側でHTTPサーバを立てる
  • 実機側からホストにHTTPリクエストを投げる

という方法を採用しました。

初期はホスト側のHTTPサーバをRubyで実行してたりしたのですが、Editorと別に起動しなくてはいけない点が面倒くさくて、
最終的にはEditorWindow内で HttpListener を使ってHTTPサーバを実行しました。

具体的な実装方法

ここから、明日から役に立たないUnity情報です。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
public class HostIOWindow : EditorWindow {
[MenuItem("Window/HostIO")]
static void Open() {
var win = (HostIOWindow)EditorWindow.GetWindow(typeof(HostIOWindow));
win.Show();
}
static HostIOWindow _instance = null;
HttpListener _listener;
Task<HttpListenerContext> _task;
IPAddress _local;
byte[] _ip;
//
void Awake() {
if (!HttpListener.IsSupported) {
throw new NotSupportedException("not supported");
}
}
void Update() {
if (_task == null) {
_task = _listener.GetContextAsync();
} else {
if (_task.IsCompleted) {
var ctxt = _task.Result;
try {
RequestHandler(ctxt);
}
catch (WebException e) {
ErrorHandler(ctxt, e);
}
finally {
_task.Dispose();
_task = null;
}
} else
if (_task.IsCanceled) {
_task.Dispose();
_task = null;
} else
if (_task.IsFaulted) {
_task.Dispose();
_task = null;
}
}
Repaint();
}
// サーバ起動
void StartWebServer() {
StopWebServer();
_task = null;
_ip = HostIOUtil.DetectLocalAddress();
_listener = new HttpListener();
_listener.Prefixes.Add("http://localhost:4649/");
var url = string.Format("http://{0}.{1}.{2}.{3}:4649/", _ip[0], _ip[1], _ip[2], _ip[3]);
_listener.Prefixes.Add(url);
_listener.Start();
Debug.Log("START WEB SERVER => " + url);
}
// リクエストハンドラ
void RequestHandler(HttpListenerContext ctxt) {
var req = ctxt.Request;
var res = ctxt.Response;
var pathelem = req.RawUrl.Split('/').ToList();
pathelem.RemoveAll((e) => string.IsNullOrEmpty(e));
if (pathelem.Count > 0) {
switch (pathelem[0]) {
case "assets":
if (pathelem.Count == 1) {
RequestHandler_file(ctxt, "/");
} else {
var pathname = "/" + Path.Combine(pathelem.GetRange(1, pathelem.Count - 1).ToArray());
pathname = pathname.Replace('\\', '/');
RequestHandler_file(ctxt, pathname);
}
break;
case "ping":
RequestHandler_ping(ctxt);
break;
default:
throw new WebException("unknown API");
}
} else {
throw new WebException("unknown API");
}
}
}

色々端折ってますが、だいたいこんな感じでUnityEditor側のサーバを起動します。

次は実機側ですが、以下のような感じになります。

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public class HostIO : MonoBehaviour {
static HostIO _instance = null;
static byte _pinCode = 0;
static StringBuilder _addressBase = new StringBuilder(1024);
static int _addressBaseRewindLength;
static bool _connect;
static byte[] _ip;
static readonly string _saveKey = "HostIO";
static readonly string _savePINCodeKey = _saveKey + "_PINCode";
static float _wait = 1.0f;


public static HostIO Instance {
get { return _instance; }
}
public static byte PINCode {
get { return _pinCode; }
set {
_pinCode = value;
_addressBase.Clear();
_addressBase.Append(string.Format("http://{0}.{1}.{2}.{3}", _ip[0], _ip[1], _ip[2], value));
_addressBase.Append(":3751");
_addressBaseRewindLength = _addressBase.Length;
Debug.Log("SERVER: " + _addressBase.ToString());
_wait = 0.0f;
}
}
public static bool IsConnected {
get { return _connect; }
}
void Awake() {
#if UNITY_EDITOR
// UnityEditor上で起動した場合は不要なので死ぬ
_ip = null;
Destroy(this);
#else
// みんな大好きシングルトン
if (_instance != null) {
Destroy(this);
} else {
_connect = false;
_instance = this;
DontDestroyOnLoad(gameObject);
_ip = HostIOUtil.DetectLocalAddress();
PINCode = (byte)PlayerPrefs.GetInt(_savePINCodeKey, 0);
}
#endif
}
void OnDestroy() {
if (_instance == this) {
PlayerPrefs.SetInt(_savePINCodeKey, (int)PINCode);
PlayerPrefs.Save();
_connect = false;
_instance = null;
}
}
void Start() {
StartCoroutine(ConnectServer());
}
// 定期的にHTTPサーバにPingしてサーバが生きているかどうかチェックする
IEnumerator ConnectServer() {
while (true) {
if (PINCode != 0) {
using (var webreq = UnityWebRequest.Get(MakeAPIUrl("/ping"))) {
yield return webreq.SendWebRequest();
if (webreq.isNetworkError || webreq.isHttpError) {
_connect = false;
} else {
_connect = true;
}
}
} else {
_connect = false;
}
yield return new WaitForSecondsRealtime(1.0f);
}
}
// 同期的にファイルを読む
public static byte[] LoadFile(string path) {
using (var webreq = UnityWebRequest.Get(MakeAPIUrl("/assets" + path))) {
var async = webreq.SendWebRequest();
while (!async.isDone) {
Thread.Sleep(1);
}
var req = async.webRequest;
if (req.isNetworkError || req.isHttpError) {
return null;
}
return req.downloadHandler.data;
}
}
// 非同期的にファイルを読む
public static UnityWebRequestAsyncOperation LoadFileAsync(string path) {
using (var webreq = UnityWebRequest.Get(MakeAPIUrl("/assets" + path))) {
return webreq.SendWebRequest();
}
}
}

これまた色々端折ってますが、だいたいこんな感じです。

注意点としては、 Resources などのように UnityObject として返ってくることがない点です。あくまで byte[] として読めるだけです。その後の処置に関しては、そこそこ苦労しています。

いきなりソースをぶちまけてみましたが、簡単に流れを説明すると、

  • UnityEditor側
    1. EditorWindowを作る
    2. その中でHttpListenerを作り、待ち受けを開始する
    3. ホストのIPアドレスを取得しておき表示する
    4. HTTPリクエストがあったらそれに応じて処理する
  • 実機側
    1. 指定されたIPアドレスと通信を開始する
    2. 定期的にPingを発して通信状況を確認する
    3. 何かしらファイルが読みたくなったらHTTPサーバに問い合わせる

実機側でのIPアドレス指定ですが、SRDebuggerを利用し、デバッグメニューから指定できるようにしてあります。また、一度指定したアドレスは PlayerPrefs に保存しておき、次回以降は使い回します(アドレス振り直しは頻繁に起こらないだろうという予測)。

具体的な用例

  • xLuaのようなスクリプトで処理を記述するゲームの場合、スクリプトを修正する度にビルドしてたら日が暮れるので、ホストPC上のスクリプトを更新したら実機側でそのままリロードできるようにしました。
  • StreamingAssets に埋め込むようなAssetBundleを更新する度にビルドしてたら……なので、リロードできるようにしました。

最後に

長々と書いてきましたが、今まで書いた内容はほぼ無効です。 PlayerConnection を使いましょう。もっと簡単に安定して通信が出来るはずです。