KLab若手エンジニアの これなぁに?

KLab株式会社の若手エンジニアによる技術ブログです。phpやRubyなどの汎用LLやJavaScript/actionScript等のクライアントサイドの内容、MySQL等のデータベース、その他フレームワークまで幅広く面白い情報を発信します。

2010年08月

IRCボットコンテストエントリ: Jsmin ブラウザで動くIRCボット

太平洋高気圧、お前本気出しすぎ。 あとちょっとだけ手加減してもたぶん大丈夫だぞ。 夏いですね。 入社して10日ちょっとのosuga-hです。よろしくお願いします。 締め切りを過ぎてしまいましたが、自分もIRCボットコンテストに向けてブラウザ(Chrome)で動作し(ロジックは)Javascriptで書かれた、Botを作ったので紹介させてください。 普段はBotのような、機能や性格が重要なものは作らないのですが、「そういえばJavascriptで書かれたBotって聞いたことないぞ」と思い立ってしまったので、実装しました。 たまには手段のために目的を選らばなくったっていいじゃない!

■Jsmin

ジャスミンと名づけたこのボットは以下のような機能を持っています。
  1. ハードコーディングされた人口無能 [human]あーなるほどね [Jsmin]本当にわかってるの?
  2. IRC経由で人口無能のルールを追加する機能 [human]add りんご ごりら [human]りんご [Jsmin]ごりら
  3. Javascriptを実行する機能 [human]exp function add( a , b ){ return a + b ; } add( 1 , 2 ) [Jsmin]3
  4. Javascriptをブラウザコンテキストで実行する機能 [human]forceEval alert( "moge" ) 俺の端末に突如alertによるダイアログがポップアップする
4つ目はお遊びですね。 Jsminは以下の2つのポリシーで実装しました。
  • ブラウザで動く
  • ロジックはJSで書く
あとは簡単に実装方法を紹介します。

■IRCのプロトコル部分

ソケット通信を行う必要があり、またIRCのプロトコルも喋らなければならず、ここを1から実装するのはつらかったので その昔所属していた組織でのっぴきならない事情があったためIRCクライアントを自作したときに使った flexircclient というActionScriptのライブラリにJSとのインターフェースを実装しました。 なのでPure Javascriptではありません。 HTML5ならWebSocketあるじゃんという話もあるのですが、Handshakeなどで問題がおきそうだったので今回は完全に選択肢から除外しました。

■人口無能ルールの追加

送られてきたルールはlocalStorageに保存しています。 なので、ブラウザを閉じても学習(?)成果は保存されます。 また、Chromeでは開発者ツールを使うことでlocalStorageの中身をいじくれるので、メンテナンスも簡単です。

■Javascriptコードの実行

危険なAPIの呼び出しなどをしてほしくないので安全なコンテキストで送られてきたJSのコードを評価したいですね。 そのために今回はWebWorkerを使いました。 WebWorkerの中ではdocumentやwindowといったオブジェクトにアクセスできません。 このWebWorkerをサンドボックスとして使うアイデアはこちらで紹介されていました。 ただwhile(1){}とかやられると、困ります。 やるなよ絶対やるなよ。

■ブラウザコンテキストでのコード実行

この機能は、location.href="http://www.google.com"とかやられて、ボットがIRCから出て行ってしまったり、 while(true){alert("\(^o^)/");}とかやられて俺の作業が妨害されたりしたら、面白いんじゃねーの? っていうためだけに存在しています。 実現の方法はただただevalするだけです。 なんのチェックもしてません。

■ソースコード

SWFとかもあるので動作に必要なものはこちらにまとめておきました。 興味のある方はご覧ください。 JSの部分だけですがソースを貼り付けておきます。
//Flash initialization
var flashvars = { };
    params = {
        menu: "false",
        scale: "noScale",
        allowFullscreen: "false",
        allowScriptAccess: "always",
        bgcolor: "#FFFFFF"
    },
    attributes = { id:"FlexIRCClient" };

swfobject.embedSWF("FlexIRCClient.swf", "altContent", "1px", "1px", "9.0.0", "expressInstall.swf", flashvars, params, attributes);

//Util
function log( text ){ document.getElementById("cmd").innerHTML = text + "
" + document.getElementById("cmd").innerHTML  ; }
function $ (){ return document.getElementById.apply( document , arguments ); }

//Interface for swf
//  initialized after onReady event
var client ;

//------------------------------------------------------
// Event handlers of SWF

//SWF is on ready to connect IRC
function onReady(){
    client = document.FlexIRCClient ;
    $("message").innerHTML = "";
    $("form").style.display = "block";

}
function connect(){
    $("message").innerHTML = "connecting..." ;
    $("form").style.display = "none" ;
    client.connect( $("name").value , $("host").value , $("channel").value ) ;
}

//entered to channel
function onEnterChannel( ){
    $("message").innerHTML = $("name").value + "entered to channel '" + $("channel").value + "'" ;
    mergeLocalStorage();
}

//on recieve message
function onMessage( message ){
    setTimeout( function(){
        var i , n , func ;
        for( i = 0 , n = reg.length ; i < n ; i++ ){
            if( message.match( reg[i][0] ) ){
                func = reg[i][1] ;
                var a = reg[i].slice( 2 );
                a.push( message );

                func.apply( this , a );
                break;
            }else{

            }
        }
    },10 );
}

//------------------------------------------------------
// Bot settings
var reg = [
    [ /なるほどね?。?$/ , muno          , "本当にわかってるの?" ] ,
    [ /ってことか。?$/  , muno          , "マジで!?"           ] ,
    [ /^実は/           , muno          , "マジで!?"           ] ,
    [ /^add/            , addRule       ] ,
    [ /^exp/            , exp           ] ,
    [ /^forceEval/      , forceEval     ]
] ,
sandbox = new Worker( "sandbox.worker.js" ); //webworker for sandbox
sandbox.onmessage = function ( event ){      //on receive result from webworker
    data = event.data ;
    client.send( data ); //send result to irc
}

function muno( text ){
    client.send( text );
}

function exp( text ){
    var code = text.replace( "exp" , " "); //parse irc message

    sandbox.postMessage( code ); //pass code to webworker
}

function forceEval( text ){
    var code = text.replace( "forceEval " , " "); //parse irc message

    try{
        var result = eval( code ); //run code
        client.send( result );
    }catch( e ){
        client.send( e.toString() );
    }
}

function addRule( message ){
    var param = message.replace( "add " , "" ).split( " " ); //parse irc message
    if( param.length != 2 ){ return; }

    localStorage[ param[0] ] = param[1] ; // save to localStorage
    reg.push( [ param[0] , muno , param[1] ] );
}

/**
 * load rules from localStorage
 */
function mergeLocalStorage(){
    var key , val ;
    for( key in localStorage ){
        val = localStorage[key];
        reg.push( [ key , muno , val ] );
    }
}

IRCボットコンテストエントリ: Monty the Python bot

こんばんは、最近なんだかバテ気味の若手ブログ初登場、nakazawa-kです。 よろしくお願いします。 どうやらIRCボットのコンテストをやるらしいと聞いて息巻き、大急ぎでざくっと実装してから実に2ヶ月ほど寝かせてしまったボットを投下してみます。 最初のお題が出た瞬間に思いついたのが「Pythonボット」でした。『Pythonで書かれた』という意味ではなく『Pythonを実行してくれる』ということです。
(nakazawa-k) >>>print "hello!"
(bot) hello!
こういう風にIRCがPythonコードであふれたら素敵だと思いませんか!? 私は思います。作りましょう。これで機能が決まりました。 次は超重要、名前です。 みなさんパイソンといえば何を思い浮かべるでしょう。 ニシキヘビ? いえいえパイソンといえばモンティ・パイソンです。テリー・ギリアムです。 Pythonを実行してくれるボットの名前にモンティ以上のものはないでしょう。 ということでPythonで書かれたPythonコードを実行してくれるモンティボットを作ってみました。 実装を始めるにあたり、まず目標をKLabの社内にある「どぶろく制度」を使って『1時間以内にサクッと動くようにする』と設定しました。 ちなみに「どぶろく制度」とは標準業務時間の10%を好きに使い、上司に断ることなく自分の興味が赴くまま研究や開発を行えるというものです。Googleの20%ルールや3Mの15%ルール(こちらは不文律ですね)と似たものです。 標準業務時間の10%とは、だらだらとやっていてはすぐに過ぎ去ってしまう位です。KLab入社間もなかったnakazawa-kにとって、IRCボットは打って付けのネタでした。 閑話休題。目標が決まったのでとにかくシンプルな実装を目指していきました。 IRCのプロトコルにも多少興味はあったのでRFC1459を流し読みし、その上でIRC接続ライブラリとしてPython IRC libraryを利用しました。 ※ソースは記事の最後に貼り付けてあります。 使い方
$ ./monty.py server[:port] #channel nickname
いじり方
(nakazawa-k) >>>self.v = range(1,101) (nakazawa-k) >>>print self.v (monty) [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, 99, 100]
こういう感じで、>>>に続けてPythonの式を書くと評価してくれます。printは結果を取得してIRCへ流してくれます。 「あの実装どうやるんだっけ?誰か教えてー」 という質問にサンプルコードを返信するだけではあまりテンションが上がりませんが、コードの先頭に">>>"を書くだけで実行結果を即確認出来超ハイテンションになれます。これで皆もっとPythonが好きになってくれるはずです!!
超簡単な仕組み SingleServerIRCBotクラスを継承すると、チャンネルでの発言時にon_pubmsg()が呼ばれます。この中に">>>"で始まるものを見つけると、右から左でPythonコードとして実行しています。 これだけでもPythonの実行権限で出来ることは、実質相当色々出来てしまいます。それこそファイルの作成や削除など、結構思いのままなので専用VMを作成して走らせています。
応用例 若手ブログ未登場のkさんによる「チャンネル内のユーザからなると(op権限)を奪いまくる」ボットの機能を模倣してみる
>>>self.excp="logbot" >>>[(nick not in [self.excp,self.nick]) and (c.notice(self.channel, nick + ' is dead'), c.mode(self.channel, "-o "+ nick)) or 1 for nick in [u for u in self.channels.items()[0][1].users()]]
発言が行われたチャンネルで、指定nickと自分以外の全ユーザからop権限を剥奪してくれます。 このように、MontyはPythonの式として書けるものなら、非常に多くの処理を自由に実行することが出来ます。
残念なところ ・execでのコード実行は、あまりにもフリーダムすぎる →PyPyベースのsandbox環境へ持ち込みたい。ただ、既存ライブラリから完全に切り離された環境ではあまり面白いことが出来ないのでほどほどに・・・。 例えば
(nakazawa-k) >>>weather (monty) =六本木付近の天気予報= 8/19晴れ 8/20曇りのち雨のち晴れのち小雨
こういうことが出来ると段々夢が広がってくるじゃないですか!
夢破れて・・・ 1) 当初はインタラクティブシェルをそのままIRC上へ持ち込み、謎のIRCペアプログラミング(複数人が1つのインタラクティブシェルを使ってコードを開発するというすさまじい共同作業)などをやりたいなーと夢想しました。まあこれは洒落なので実際にやりたければscreenを使うのが近道でしょう。 2) 当初はpopenして適宜入出力をパイプ取得すれば良いと考えましたが、そうそう素直にstdoutへ各行出力をしてくれるわけではありませんでした。ターミナルの機能を内部でそこそこ使っていたりと結構複雑化するポイントが垣間見えました。それでは手軽に書いてサクッと動かすという主旨に反してしまうので泣く泣く断念。
最後に マルチチャンネル非対応のため、社内の技術雑談チャンネルで放し飼いにしています。 ちょっと残念なコードを食べさせるとinternal exceptionといって実行を放り出してしまうドジッ子(いえ、コードを書いた人のほうがドジッ子なんです)ですがKLab IRCサーバへお立ち寄りの際(!?)は可愛がってあげてください。
#! /usr/bin/env python

from ircbot import SingleServerIRCBot
from irclib import nm_to_n, nm_to_h, irc_lower, ip_numstr_to_quad, ip_quad_to_numstr
import random
import re
import os
import sys
import StringIO
import time

class Monty(SingleServerIRCBot):
    def __init__(self, channel, nickname, server, port=6667):
        SingleServerIRCBot.__init__(self, [(server, port)], nickname, nickname)
        self.nick = nickname
        self.channel = channel

    def on_nicknameinuse(self, c, e):
        c.nick(c.get_nickname() + "_")
        self.nick += "_"

    def on_welcome(self, c, e):
        c.join(self.channel)

    def on_pubmsg(self, c, e):
        nick = nm_to_n(e.source())
        matched = re.match(r">>>(.*)", e.arguments()[0])
        if matched != None:
            outputBuffer = StringIO.StringIO()
            sys.stdout = outputBuffer
            exceptionBuf = ''
            try:
                exec matched.group(1)
            except:
                exceptionBuf = sys.exc_info()[0]
                print "internal exception"
            c.notice(self.channel, outputBuffer.getvalue())
            sys.stdout = sys.__stdout__
            print exceptionBuf
        return

def main():
    import sys
    if len(sys.argv) != 4:
        print "Usage: testbot   "
        sys.exit(1)

    s = sys.argv[1].split(":", 1)
    server = s[0]
    if len(s) == 2:
        try:
            port = int(s[1])
        except ValueError:
            print "Error: Erroneous port."
            sys.exit(1)
    else:
        port = 6667
    channel = sys.argv[2]
    nickname = sys.argv[3]

    bot = Monty(channel, nickname, server, port)
    bot.start()

if __name__ == "__main__":
    main()

IRCボットコンテストエンリ 最終兵器Haskellによるゲームボット

こんにちは夏です。takada-atです。 夏こそ純粋関数型言語ですね(謎)。 みなさまどうぶつしょうぎというゲームをごぞんじでしょうか? http://ja.wikipedia.org/wiki/どうぶつしょうぎ 子どもへの将棋普及のために考えられた簡易版の将棋風ゲームです。 以前社内でこのゲームが流行していた時期に、HaskellによるAIを開発しました。 今回はそのAIとIRC上で対戦できるようにしました。 なんとIRC上でどうぶつしょうぎの対戦ができてしまうゲームAIボットです。アスキーアートによるインターフェースがあまりに素朴なので、思わず昔をなつかしんで故郷の両親に電話をかけてしまう、などの効果もあるのではないかと自負しています。 盤面はこういう感じで表示されます。
21:03 (takada-at) !ds-start
21:03 (dshogi) __A__B__C_
21:03 (dshogi) 1-KR-LI-ZO
21:03 (dshogi) 2 * -HY * 
21:03 (dshogi) 3 * +HY * 
21:03 (dshogi) 4+ZO+LI+KR
21:03 (dshogi) []
21:03 (dshogi) []
!ds-move XXX という発言で、自分の手を操作します。 たとえば、C4にある駒をC3に動かしてみましょう。
21:04 (takada-at) !ds-move C4C3
21:04 (dshogi) __A__B__C_
21:04 (dshogi) 1-KR-LI-ZO
21:04 (dshogi) 2 * -HY * 
21:04 (dshogi) 3 * +HY+KR
21:04 (dshogi) 4+ZO+LI * 
21:04 (dshogi) []
21:04 (dshogi) []
21:04 (dshogi) __A__B__C_
21:04 (dshogi) 1 * -LI-ZO
21:04 (dshogi) 2-KR-HY * 
21:04 (dshogi) 3 * +HY+KR
21:04 (dshogi) 4+ZO+LI * 
21:04 (dshogi) []
21:04 (dshogi) []
すぐにAIが対抗する手を打ってきます。 すべてのコマンドは、!ds- というプレフィックスではじまります。 !ds-help と発言すれば、ヘルプを表示します。 実装の詳細には踏み込みませんが、AIは深さ優先探索です。 Haskellをつかったことないと意味不明だと思いますが、盤面の状態、乱数の種などなどをStateモナドで持ち回す感じの実装になってます。 HaskellによるIRCボットの実装については以下を参考にしました。 http://www.haskell.org/haskellwiki/Roll_your_own_IRC_bot あとIRCメッセージのパーサーライブラリを利用しています。 http://hackage.haskell.org/package/irc

■使い方

dshogi.conf を適当に編集して、サーバーとかチャンネルを指定します。 以下のコマンドで実行します。 念のためWindows環境でビルドしたexeファイルとLinux環境でビルドしたバイナリの両方をつけました。 ./DshogiIRC dshogi.conf

■ビルド方法

万が一ビルドしてみたい方がいれば以下の方法でお願いします。 ちなみに手元の環境では、ghcのバージョンは6.12.1。ircライブラリだけインストールすればビルドできました。
runhaskell Setup.hs configure
runhaskell Setup.hs build
cp dist/build/DshogiIRC/DshogiIRC .
ソースコードは以下にあります。 http://lab.klab.org/young/wp-content/uploads/data/code/dshogi.tar.gz

ボットコンテスト twistedを使って常駐型IRC発言プロキシサーバ作ってみました

こんにちは! もう一人の sasaki-k です。 以前からpythonの twisted でなにか作ってみようと思っていたので、技術の無駄遣いネタとして 「常駐型IRCプロキシサーバ」を作ってみました。 バッチプログラムからIRCへ何度も発言させようとすると下のように都度joinとquitしてしまい、 「邪魔だなあ」と言われることが多かったのが背景です。
23:02 *hoge-bot join #channel (~hoge-bot@xxxx.klab.jp) (発言) 23:02 *hoge-bot quit (Remote host closed the connection) 23:02 *hoge-bot join #channel (~hoge-bot@xxxx.klab.jp) (次の発言) 23:02 *hoge-bot quit (Remote host closed the connection)
そこで、twistedに付属しているIRCのクライアントを改造して、サーバソケットで 外部プログラムから発言したいメッセージを受け付けられるようにしてみました。ソケットサーバ兼IRCクライアントです。 このpythonスクリプトをバッチの最初で起動しておいて、所定のListenポートに発言したい内容を Socketクライアントから流しこめば発言してくれる、という流れです。 あるいはdaemontools などでこのスクリプトを常駐させておくのもよさそうです。 使った環境は Twisted 10.1.0 python 2.5.2 です。

# vim: fileencoding=utf-8

"""
Announce irc bot.
"""

# twisted imports
from twisted.words.protocols import irc
from twisted.internet import reactor, protocol
from twisted.python import log

# system imports
import sys

class IrcBotClient(irc.IRCClient):
    """
    IRCに接続する側のprotocolです。接続するとfactoryのprotocolを差し替えます。
    """    
    nickname = "announce-bot"

    def connectionMade(self):
        irc.IRCClient.connectionMade(self)
        self.factory.protocol = self

    def signedOn(self):
        """Called when bot has succesfully signed on to server."""
        self.join(self.factory.channel)

class IrcBotFactory(protocol.ClientFactory):
    """
    IRCに接続する側のprotocolのfactoryです。
    """
    protocol = IrcBotClient

    def __init__(self, channel):
        self.channel = channel

    def clientConnectionLost(self, connector, reason):
        """If we get disconnected, reconnect to server."""
        connector.connect()

    def clientConnectionFailed(self, connector, reason):
        print "connection failed:", reason
        reactor.stop()

class IrcBotCommander(protocol.Protocol):
    """
    メッセージ送信プログラムからの文字列を受け取ります(utf-8固定)
    """
    def dataReceived(self, data):
        channel = "#" + self.factory.botFactory.channel
        data = unicode(data, 'utf-8')
        data = data.rstrip()
        data = data.encode('iso-2022-jp')
        self.factory.botFactory.protocol.msg(channel, data)
        self.transport.loseConnection()

class IrcBotCommanderFactory(protocol.ServerFactory):
    """
    メッセージ送信プログラムのfactoryです。IrcBotへのポインタを持ちます
    """
    protocol = IrcBotCommander

    def __init__(self, botFactory):
        self.botFactory = botFactory

# main
if __name__ == '__main__':
    # initialize logging
    log.startLogging(sys.stdout)
    
    ircBotFactory = IrcBotFactory(sys.argv[1])
    ircBotCommanderFactory = IrcBotCommanderFactory(ircBotFactory)

    # xxx.xxx.xxxはircサーバ
    reactor.connectTCP("xxx.xxx.xxx", 6667, ircBotFactory) 
    reactor.listenTCP(12345, ircBotCommanderFactory)

    # run bot
    reactor.run()


おかげで絶え間なく(?) 発言できるようになりましたー
23:02 *announce-bot join #channel (~hoge-bot@xxxx.klab.jp) 23:02 (announce-bot) ああああ 23:03 (announce-bot) <-------------- テストテスト --------------> 23:10 (announce-bot) <-------------- これからよろしくね -------------->
今後複数チャンネル対応とか、他の人の発言に対応した動きをするとか、できたらいいなと思ってます。

iPhoneをアナログゲームコントローラにしてみる(3)

暑い日が続いていますが、いかがお過ごしですか。最近複数の人から、「老けた」と言われて若干凹んだponpoko1968です。 「iPhoneをアナログゲームコントローラにしてみる」の最終回、サーバの実装です。 まずはデモ動画からどうぞ。


今回は、前回作成したリモートコントローラの状態を受信して、リアルタイムで表示するサンプルアプリを作成します。

アプリケーションの作成

先ずはプロジェクトの作成です。サーバについても、簡素な画面構成なので、「View-Based Application」を選択します。 主に手を加えるのは、メインの画面を制御するViewControllerクラスです。

Game Kitサーバのセットアップ

ViewControllerがGame Kitからの通知を受信できるよう、GKSessionDelegateプロトコルをサポートさせます。

@interface AnalogRemoteServerViewController : UIViewController {
  // Game Kit
  GKSession* gkSession;
  NSMutableDictionary* peers;
(中略)
「UIViewController」のように継承元のクラスの指定の後ろに、"<",">"で括ってサポートするプロトコルを指定します。複数指定する場合はカンマで区切って書きます。 ViewControllerクラスに、接続しているコントローラの状態を保持するメンバ変数を追加します。 リモートコントローラ同様、サーバもGKSessionインスタンスへのポインタを保持します。 さらに、サーバは複数のリモートコントローラからの接続を考慮し、各リモートコントローラの状態を保持する辞書を追加します。 サーバはリモートコントローラからの接続時にPeerIDと呼ばれる文字列を渡されるので、この文字列をキー値として、後述するControllerStateクラスのインスタンスを格納することにします。

クライアントからの接続を受け付ける

リモートコントローラがサーバに接続しに来ると、didReceiveConnectionRequestFromPeerが呼ばれます。

- (void)session:(GKSession *)session didReceiveConnectionRequestFromPeer:(NSString *)peerID {
  NSError* error;
  NSLog(@"session:%p didReceiveConnectionRequestFromPeer: from=\"%@\"", session, peerID );
  if(  [peers count] < 1 ){
    [session acceptConnectionFromPeer:Peerid error:&error];
    [session setDataReceiveHandler:self  withContext:peerID];
  }else {
    [session denyConnectionFromPeer:peerID ];
  }
}
今回は、サンプルプログラムの簡単のため、リモートコントローラからの接続を1台のみに制限しています。 具体的にはpeersメンバ変数の要素数が1未満の場合には、セッションにacceptConnectionFromPeerを呼んで接続を許可します。 また、1以上の場合、すなわちリモートコントローラがすでに接続している場合はdenyConnectionFromPeerを呼んでそれ以上の接続を拒否します。 今回のコードも設計上は複数のリモートコントローラからの接続をサポートしているので、ゲームのデザインによって同時に接続できるリモートコントローラのの最大数を変更すると良いでしょう。 このとき、sessionに対し、

    [session setDataReceiveHandler:self  withContext:peerID];
を呼び出すことで、データが到着したときのハンドラ(コールバック)関数をViewControllerに設定します。ハンドラ関数は名前と引数の型が決められており、セッションからデータを受け取るクラスは

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context 
の形式の関数を定義しなければいけません。 このあと、通常であれば、session:peer:didChangeStateが呼ばれます。 アプリケーションがリモートコントローラからの接続が確立したと認識させるのは、このsession:peer:didChangeState:が呼ばれたタイミングで、peerすなわちリモートコントローラの状態を見て行いましょう。以下のコードを参照して下さい。

- (void)session:(GKSession *)session peer:(NSString *)peerID didChangeState:(GKPeerConnectionState)state {
  NSLog(@"session:%p peer:%p\"%@\" didChangeState:%d",session,peerID,peerID,state );
  if( state == GKPeerStateConnected ){
    ControllerState* controller = [[ControllerState alloc] init];
    controller.delegate = self;
    [peers setObject:controller forKey:peerID];
    [controller release];

    NSLog(@"state = GKPeerStateConnected");


  }else if ( state == GKPeerStateDisconnected ){
    NSLog(@"state = GKPeerStateDisconnected");
    [peers removeObjectForKey:peerID];
  }

}
上記メソッドが呼ばれる際、GameKit側からGKPeerConnectionState定数が渡されます。stateの値がGKPeerStateConnectedであった場合には、peerIDをキーに、後述するControllerStateインスタンスを生成してpeersメンバ変数に登録しておきます。 その後、

    controller.delegate = self;
として、 ControllerStateクラスがコントローラの状態変化を検知した場合に、ViewControllerクラスが通知を受け取るようにします。

データの受信

前節で説明したように、Game Kitが受信したデータをビューコントローラクラスに定義したハンドラ関数で処理します。今回はControllerStateという独自のクラスを定義し、受け取ったデータの解釈は、そのクラスにまかせることにします。

- (void) receiveData:(NSData *)data fromPeer:(NSString *)peer inSession: (GKSession *)session context:(void *)context {
  ControllerState* controller = [peers objectForKey:peer];
  if( controller ) {
    [controller updateWithDecodingRawData:data];
  }else {
    NSLog(@"data from unknown peer %@",peer);
  }
}
Game Kitから渡されたpeer文字列をキーに ControllerStateインスタンスの参照を取り出し、引数で受けとったNSDataのインスタンスを渡します。

データのデコード

今回のサンプルでは、ちょっと大げさですが、リモートコントローラから送信されたコントローラの状態を解釈するクラスControllerStateと、その状態を他のクラスに通知するためのデリゲート通知プロトコルControllerStateDelegateを定義しました。 こういったクラスとプロトコルを定義した意図として、通信セッションの接続や切断などに関するあれこれはシステム内で単一であることが保証されているクラス(=シングルトン)に担当させるのが適切ですが、ゲーム内の複数のキャラクタを各リモートコントローラを使って動かしたい場合などには、設計上各リモートコントローラからの情報を各キャラクタを表現するクラスが直接受け取ることで、プログラムの見通しが良くなると考えたからです。 また、デリゲートプロトコルを介することで、たとえば、ユーザからの入力をリモートコントローラから外付けキーボードなど別の周辺機器に切り替えたい場合にゲーム側のロジックに関するコードを変更する必要がなくなります。
今回の場合GKSessionのデリゲートを受け取るのはViewControllerなので、CoCoaの設計上は厳密に言うとシングルトンではないのですが、アプリケーション構成上Viewが1つしかないためViewControllerに色々やらせてます^^;
一連の処理をまとめると、下記の図のようになります。 データの流れ

画面表示

最後に、コントローラの状態表示を行います。今回の場合はControllerStateクラスが検知したリモートコントローラの状態変化もViewControllerが受け取ります。 ControllerStateクラスは下記の3つの通知を送ります。
  • buttonPressed - ボタンが押された
  • buttonReleased - ボタンが離された
  • controllerTilted: - コントローラが傾いた
buttonPressed、 buttonReleasedについては比較的自明なのでサンプルコードを見て頂くとして、 controllerTiltedについては、

- (void) controllerTilted:(float)tilt {
  //NSLog(@"controllerTilted val = %f",tilt);
  [arrowView setTransform:CGAffineTransformMakeRotation(tilt)];
}
としています。tiltという値はコントローラの傾き角度がラジアンで渡ってきているので、矢印の画像を持つサブビューをInterfaceBuilderで作り、そのビューに対してCGAffineTransformMakeRotation()で作成した回転行列を作用させることで、矢印の画像を回転させています。

最後に

最終回となりましたこの連載ですが、こうして見直すといくつか課題が残っています。
  • UDPなので、パケットの順番が逆転する可能性があるが対応されていない
  • →UDPベースの既存のプロトコルを参考にしてみる
  • ControllerStateを介することでパフォーマンスに影響はないか?あるとしたらどのように改良すべきか
  • →Objective-Cのメソッド呼び出しを高速化する方法を調べてみる
  • ボタンを押したときの衝撃でiPhoneが揺れてしまう
  • →n時点前からのiPhoneの傾きの移動平均値を計算して傾きを緩やかにする(ゲームによるけど)
といったところが挙げられます。筆者なりにそれぞれの対応案もあるのですが、もし本稿を読まれて自分のアプリに取り入れてやろうという方がいらっしゃったら、是非このような課題にもチャレンジしていただいて、さらにそのような改良をブログ等で発表して頂ければうれしいです。 サンプルコードはこちらからどうぞ。
 KLab若手エンジニアブログのフッター