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

Raspberry PiとGobotで娘とぬいぐるみ救出ゲームをした話

組織開発 【執筆者】長谷川

f:id:g-editor:20160620194656j:plain:w800

こんにちは。ぐるなびで広告関連サービスの開発を担当している長谷川@2児の父です。

Raspberry PiGobotを使って、子供と遊べる宝探しIoTゲームを作りました。

はじめに

我が家には二人の娘がいます。

次女はまだ生後半年なのでようやく寝返りができるようになったところですが、3歳の長女はいろいろなことがわかるようになって、毎日泣いたり怒ったり笑ったり激しい喜怒哀楽を見せてくれます。

父親として感情豊かになってきた娘の喜ぶ顔を見たいという思いから、これまで何度もサプライズをやってきたのですが、子供は意外とシビアであまり食いついてくれません。そんな中彼女が唯一夢中になったのがお正月の宝探しゲームでした。

f:id:hasegawa-ma:20160526124718j:plain:w320

※ハート型なのは家にあったふせんがこの形しかなかったためで、特に意味はありません

3歳の娘に普通にお年玉をあげてもあまり盛り上がらないだろうと思い、ヒントをもとに宝探し風にして探させるという昭和の漫画とかでよくあるやつを試してみました。ふせんに指示された場所に行くと次の指示が書かれたふせんが貼ってあって、順番に進んでいくと最後にポチ袋が待っているアレです。

娘はこのゲームをとても気に入ってくれて、今年の正月は三ヶ日毎朝同じ場所にふせんを貼って、毎朝同じようにお年玉探しをやらされました。

ゲームを準備する

このような宝探しゲームをIoTでやったらもっと喜んでくれるんじゃないかと思い、Raspberry Piを使って試してみました。

用意したもの

  • スマホ
  • ぬいぐるみ(宝物)
  • 南京錠
  • Raspberry Pi2
  • LED x 3
  • ケーブル類

ゲームのルール

手元に南京錠があったので、宝物を隠してこれで施錠して無事解錠できたらクリアというゲームにします。

f:id:hasegawa-ma:20160526233709j:plain:w320

施錠するための南京錠

鍵の施錠番号が3桁なので問題を3問用意し、1、2問目はWEBブラウザで回答するクイズ、最終問題はRaspberry Piの電子部品を使って答えるタイプにしました。

使用マシン&言語

ハードウェアにRaspberry Pi 2を使い、フロントエンドはReact.jsのシングルページアプリケーション、サーバサイドはGo言語 + Gobotで実装。 フロントエンドにReact.jsを選んだのは、Websocketを使う上でSPAのほうが画面制御がしやすそうだったから。 サーバサイドのGo言語+Gobotは以下の理由から選定しました。

  • WEBサーバ、GPIO操作、WebSocket操作が1つの言語で完結する
  • ラズパイだけでなくいろんなプラットフォーム(ArduinoのほかAR.DroneやSpheroなど)に対応しているので、覚えておけば今後応用がききそう
  • 並列処理ができるので複雑なGPIO操作が容易に実装できそう

ストーリー設定・娘の大好きなぬいぐるみを救出する

今回協力いただく、娘が一番気に入っているぬいぐるみの「ワン」。

f:id:hasegawa-ma:20160526125418j:plain:w320

年中持ち歩くものだからなかなか洗濯させてくれない

元々は奥さんの実家の車に放置されていた出処不明のぬいぐるみなのですが、娘はそれを大変気に入り「ワン」と名前を付けて一緒に寝たり、お出かけに連れて行ったりします。 今回は悪いやつがワンをクローゼットの中に隠したという設定にしました。

ゲームを作る

さっそく作っていきましょう。

悪役をHTML&CSSで作る

まずはワンを連れ去った悪いやつをデザインします。

ゲームはiPhoneのWEBブラウザをベースに進行するのでHTMLとCSSで描画。デザインは単純な図形の組み合わせでできるどくろタイプとしました。ちなみに娘は少し怖がりなので、どくろは極力怖くない見た目にしています。

f:id:hasegawa-ma:20160526125801p:plain:w400

   <div class="dokuro">
      <div class="dokuro1">
            <div class="eye1">
          <div class="eye-black1">
          </div>
            </div>
            <div class="eye2">
          <div class="eye-black2">
          </div>
            </div>
      </div>
          <div class="dokuro2">
            <div class="dokuro2-1">
            </div>
            <div class="dokuro2-2">
            </div>
            <div class="dokuro2-3">
            </div>
          </div>
    </div>

CSS

.dokuro{
    margin-left: 45px;
    position:absolute;
    top:115px;    
}

.dokuro1 {
    background: #240BB7;
    height: 180px;
    width: 200px;
    border-radius: 40% 40% 0 0;
    border: solid 5px #240BB7;
}

.dokuro2 {
    margin-top: -5px;
    top:110px;
}

.dokuro2-1 {
    position:relative;
    border: solid 5px #240BB7;    
    background: #240BB7;
    margin-left: 30px;
    height: 40px;
    width: 45px;
}

.dokuro2-2 {
    position:relative;
    border: solid 5px #240BB7;    
    background: #240BB7;
    margin-left: 77px;
    height: 40px;
    width: 45px;
    top: -40px;
}

.dokuro2-3 {
    position:relative;
    border: solid 5px #240BB7;    
    background: #240BB7;
    margin-left: 124px;
    height: 40px;
    width: 45px;
    top: -80px;
}

.eye1{
    position: relative;
    top: 80px;
    left: 30px;
    height: 70px;
    width: 55px;
    background: white;
    border-radius: 10% 50% 10% 10%;
}

.eye2{
    top: 10px;
    position: relative;
    left: 110px;
    height: 70px;
    width: 55px;
    background: white;
    border-radius: 50% 10% 10% 10%;
}

.eye-black1{
    position: relative;
    top: 40px;
    left: 35px;
    height: 30px;
    width: 20px;
    background: black;
    border-radius: 35%;
}

.eye-black2{
    position: relative;
    top: 40px;
    height: 30px;
    width: 20px;
    background: black;
    border-radius: 35%;
}

やられたときの顔も用意。

f:id:hasegawa-ma:20160530124302p:plain:w400

    <div class="dokuro" id="dokuro-damage">
      <div class="dokuro1">
    <div class="eye1">
      <div class="eye-batsu1">
      </div>
      <div class="eye-batsu2">
      </div>
    </div>
    <div class="eye2">
      <div class="eye-batsu3">
      </div>
      <div class="eye-batsu4">
      </div>
    </div>
      </div>
      <div class="dokuro2">
    <div class="dokuro2-1">
    </div>
    <div class="dokuro2-2">
    </div>
    <div class="dokuro2-3">
    </div>
      </div>
    </div>

CSS

.eye-batsu1 {
    position: absolute;
    top: 35px;
    left: 25px;
    width: 30px;
    border: solid 1px black;
    -webkit-transform: rotate(30deg);
}

.eye-batsu2 {
    position: absolute;
    top: 50px;
    left: 25px;
    width: 30px;
    border: solid 1px black;
    -webkit-transform: rotate(-30deg);
}

.eye-batsu3 {
    position: absolute;
    top: 35px;
    width: 30px;
    border: solid 1px black;
    -webkit-transform: rotate(-30deg);
}

.eye-batsu4 {
    position: absolute;
    top: 50px;
    width: 30px;
    border: solid 1px black;
    -webkit-transform: rotate(30deg);
}

表情が変わるだけだとわかりにくいのでアニメーションCSSで効果をつけます。問題に正解するとドクロが点滅し、ダメージを受けた(っぽい)演出をさせます。

#dokuro-damage{
    -webkit-animation: dokuro-damage 0.2s ease 0s 2 forwards;
    opacity: 1;
}

@-webkit-keyframes dokuro-damage{
    0% {
    opacity: 0;
    }
    100% {
    opacity: 1;
    }
}

解答を間違えてどくろがドヤ顔するときのHTMLも作ります。

f:id:hasegawa-ma:20160530124554p:plain:w400

ドヤ顔のHTML

    <div class="dokuro" id="dokuro-kachi">
      <div class="dokuro1">
    <div class="eye1">
      <div class="eye-batsu1">
      </div>
    </div>
    <div class="eye2">
      <div class="eye-batsu3">
      </div>
    </div>
      </div>
      <div class="dokuro2">
    <div class="dokuro2-1">
    </div>
    <div class="dokuro2-2">
    </div>
    <div class="dokuro2-3">
    </div>
      </div>
    </div>

問題を作る

悪いやつの基本パターンができたので、次に問題作成にとりかかります。問題は南京錠の解錠番号にあわせて3問。

第1問:React.jsとCSS3のアニメーション

1問目は簡単な引き算です。

f:id:g-editor:20160704182013p:plain:w320

React.jsで実装したソースは以下。

var Q1 = React.createClass({
    getInitialState: function(){
    return {
        answer1: "",
    }
    },
    answerChange: function(e){
    this.setState({
        answer1: e.target.value
    })
    },

    gotoQ2: function(){
    var answer1 = this.state.answer1;
    if(answer1 == "2"){
        React.render(<Seikai1 />,document.body);
    } else {
        nextElm = Q1;
        React.render(<Batsu />,document.body);
    }
    },
    render:function(){
    return (
        <div>
            <div className="container quiestion">
            <b>だい1もん</b><br />
            あかいボールが 5つあるぞ<br />
            3つひくと のこりはいくつか わかるか?
                </div>
            <div className="container q-field">
                <span className="ball" id="ball1"></span>
                <span className="ball" id="ball2"></span>
                <span className="ball" id="ball3"></span>
                <span className="ball" id="ball4"></span>
                <span className="ball" id="ball5"></span>
            </div>
            <div className="container answer-box">
                <div className="input-group">
                <input type="text" className="form-control" value={this.state.answer1} onChange={this.answerChange} />
            </div>
            <br />
            <div className="input-group">
                <input type="submit" value="こたえる" className="btn-primary"  onClick={this.gotoQ2} />
            </div>
            </div>
        </div>
    )
    }
});

css

.ball{
    background: red;
    height: 30px;
    width: 30px;
    border-radius: 50%;
    top:115px;
    position:absolute;
}

#ball1{
    left:20px;
}

#ball2{
    left:55px;
}

#ball3{
    left:90px;
}

#ball4{
    left: 125px;
}

#ball5{
    left:160px;
}

.answer-box {
    margin-top: 250px;
}

onChangeで入力された答えをanswer1に格納しておき、こたえるボタンが押されたらgotoQ2で回答をチェック。正解ならQ2へ、不正解であればハズレの画面を表示させます。

娘はひらがなとカタカナは普通に読め、数字も12までなら(時計の文字盤を見ながら)数えられるのですが、引き算は教えたことがないので多分答えられません。

ですのでヒントを仕込みます。CSS3のアニメーション機能を使って30秒経過したらボールが落下して答えがわかるようにしました。 先ほどのCSSに、30秒経過するとボールが落ちるアニメーションを追加。

#ball3{
    left:90px;
    -webkit-animation: falldown 3s forwards;
    -webkit-animation-delay: 20s;
}

#ball4{
    left: 125px;
    -webkit-animation: falldown 3s forwards;
    -webkit-animation-delay: 20.1s;
}

#ball5{
    left:160px;
    -webkit-animation: falldown 3s forwards;
    -webkit-animation-delay: 20.2s;
}

@-webkit-keyframes falldown {
    0%{
    top:115px;
    }
    100%{
    top:200%;
    }
}    

画面を30秒放置するとこんな感じでボールが落下します。残ったボールの数を答えれば正解。

f:id:g-editor:20160704182109p:plain:w320

第2問:React.jsとCSS3のアニメーション

2問目は身近なところから出題。信号のライトの並び順を問題にしました。

f:id:g-editor:20160704182132p:plain:W320

こちらも1問目同様、20秒経過したら色がつくようにアニメーションCSSを入れます。

f:id:g-editor:20160704182203p:plain:W320

ソースはこちら。

    var Q2 = React.createClass({
    getInitialState: function () {
        return {
        checked1: false,
        checked2: false,
        checked3: false
        }
    },
    handleCheckBox1:function(e){
        this.setState({
        checked1: e.target.checked,
        checked2: false,
        checked3: false
        });
    },
    handleCheckBox2:function(e){
        this.setState({
        checked1: false,
        checked2: e.target.checked,
        checked3: false
        });
    },
    handleCheckBox3:function(e){
        this.setState({
        checked1: false,
        checked2: false,
        checked3: e.target.checked
        });
    },
    gotoQ3: function(e){
        var answer2 = this.state.checked3;
        if(answer2 == true){
        React.render(<Seikai2 />,document.body);
        } else {
        nextElm = Q2;
        React.render(<Batsu />,document.body);
        }
    },
    render:function(){
        return(
            <div>
            <div className="container quiestion">
            <b>だい2もん</b><br />
            しんごうきの いちばんみぎは <br />
            なにいろか わかるか?
        </div>
            <div className="container q-field">
            <span className="signal" id="signal1"></span>
            <span className="signal" id="signal2"></span>
            <span className="signal" id="signal3"></span>
            </div>
            <div className="container answer-box">
            <div className="input-group">
            <label>
            <input type="radio" name="signal" value="blue" checked={this.state.checked1} onChange={this.handleCheckBox1} />あお<br/>
            </label><br />
            <label>
            <input type="radio" name="signal" value="yellow" checked={this.state.checked2} onChange={this.handleCheckBox2} />きいろ<br/>
            </label><br />
            <label>
            <input type="radio" name="signal" value="red" checked={this.state.checked3} onChange={this.handleCheckBox3} />あか<br/>
            </label><br />
            </div>
            <br />
            <div className="input-group">
            <input type="submit" value="こたえる" className="btn-primary" onClick={this.gotoQ3} />
            </div>
            </div>
            </div>
        )
    }
    });

CSS

.signal{
    height: 30px;
    width: 30px;
    border-radius: 50%;
    top:115px;
    position:absolute;
    background: black;
}

#signal1{
    left:20px;
    -webkit-animation: signal1 0.2s 3 forwards;
    -webkit-animation-delay: 10s;
}

#signal2{
    left:55px;
    -webkit-animation: signal2 0.2s 3 forwards;
    -webkit-animation-delay: 10s;
}

#signal3{
    left:90px;
}

@-webkit-keyframes signal1 {
    0% {
    background: black;
    }
    50% {
    background: black;
    }
    100% {
    background: green;
    }
}

@-webkit-keyframes signal2 {
    0% {
    background: black;
    }
    50% {
    background: black;
    }
    100% {
    background: yellow;
    }
}

3つのチェックボックスそれぞれに onChangeイベントを登録しておき this.state.checked3trueの場合は正解、それ以外の場合は不正解の画面を表示させてQ2へ戻ります。

第3問:Raspberry Piと連動

3問目が最後の問題となります。

最後はRaspberry Piと連動して、光っているLEDのどれかを消すと悪者をやっつけることができる、という問題にしました。GPIO経由でのLED制御およびWebSocket通信を行うプログラムはGo言語で実装しGobot+WebSocketライブラリを使います。

ピン配置

ピンとパーツの接続は下記としました。

  • GPIO25を赤色LED
  • GPIO17を黄色LED
  • GPIO22を緑色LED
  • GPIO24を赤色LEDをON/OFFにするスイッチ
  • GPIO27を黄色LEDをON/OFFにするスイッチ
  • GPIO23を緑色LEDをON/OFFにするスイッチ

f:id:g-editor:20160610094327j:plain:w650

接続図は Fritzing Fritzing で作成しました。

GobotからGPIOピンへの接続を行う場合GPIO番号ではなくピン番号を指定します。

サーバサイドのプログラムソース

package main

import (
    "github.com/hybridgroup/gobot"
    "github.com/hybridgroup/gobot/platforms/gpio"
    "github.com/hybridgroup/gobot/platforms/raspi"
    "golang.org/x/net/websocket"
    "net/http"
)

var conn *websocket.Conn
var ch = make(chan int)

func main() {
    go func(){
        http.Handle("/quiz", websocket.Handler(wsHandler))
        http.Handle("/", http.FileServer(http.Dir("webroot")))
        if err := http.ListenAndServe(":9999", nil); err != nil {
            panic("ListenAndServe: " + err.Error())
        }
    }()

    gbot := gobot.NewGobot()

    r := raspi.NewRaspiAdaptor("raspi")
    ledY := gpio.NewLedDriver(r, "led", "22")
    ledR := gpio.NewLedDriver(r, "led", "11")
    ledG := gpio.NewLedDriver(r, "led", "15")
    buttonY := gpio.NewButtonDriver(r,"myButtonY","18")
    buttonR := gpio.NewButtonDriver(r,"myButtonR","13")
    buttonG := gpio.NewButtonDriver(r,"myButtonG","16")

    work := func() {
        ledY.On()
        ledR.On()
        ledG.On()
        gobot.On(buttonY.Event("push"),func(data interface{}){ 
            resLed(conn,"Yellow")
            ledY.Toggle()
        })

        gobot.On(buttonR.Event("push"),func(data interface{}){
            resLed(conn,"Red")
            ledR.Toggle()
        })

        gobot.On(buttonG.Event("push"),func(data interface{}){
            resLed(conn,"Green")
            ledG.Toggle()
        })
    }
 
    robot := gobot.NewRobot("blinkBot",
        []gobot.Connection{r},
        []gobot.Device{ledY, ledR, ledG, buttonY, buttonR, buttonG},
        work,
    )
 
    gbot.AddRobot(robot)
    gbot.Start()
}

func wsHandler(ws *websocket.Conn) {
    conn = ws
    <- ch
}

func resLed(c *websocket.Conn, colorType string){
    websocket.Message.Send(c,colorType)
}

クライアント側ソース

    var wsUri = "ws://192.168.1.15:9999/quiz";
    var ws = new WebSocket(wsUri);

    ws.onmessage = function(e) {
    if(e.data == "Yellow"){
        React.render(<Taoshita />, document.body);
    } else {
        nextElm = Q3;
        React.render(<Batsu />,document.body);
    }
    }

    var Taoshita = React.createClass({
    componentDidMount:function(){
        fallAudio.load();
        fallAudio.play();
    },
    gotoSeikai3: function(e){
        React.render(<Seikai3 />,document.body);
    },
    render:function(){
        return (
            <div>
            <div className="container quiestion">
            <div className="dokuro-make" id="dokuro-make">
            <div className="dokuro1">
            <div className="eye1">
            <div className="eye-batsu1">
            </div>
            <div className="eye-batsu2">
            </div>
            </div>
            <div className="eye2">
            <div className="eye-batsu3">
            </div>
            <div className="eye-batsu4">
            </div>
            </div>
            </div>
            <div className="dokuro2">
            <div className="dokuro2-1">
            </div>
            <div className="dokuro2-2">
            </div>
            <div className="dokuro2-3">
            </div>
            </div>
            </div>
            </div>
            <div className="container dokuro-selif">
            <div id="dokuro-selif-down">
            やられた~!
        </div>
            <input type="submit" value="つぎへ" className="btn-primary" onClick={this.gotoSeikai3} />
            </div>
            </div>
        )
    }
    });

サーバサイドでGoのプログラムが起動すると、最初に3つのLED全てを点灯させた後、WebSocket経由でクライアントと接続します。

クライアントはWebsocket経由で通知があったらReact. renderを呼び出してページを切り替えます。間違ったボタンが押された場合は不正解のページを表示させた後に3問目の最初に戻ります。

f:id:hasegawa-ma:20160526231454j:plain:w650

LEDが光っているところ

Gobotでボタンの入力を監視し、ボタンが押されると下のように画面が切り替わります。

f:id:hasegawa-ma:20160601130214g:plain

Raspberry Piのボタンを押したらどくろがやられるところ

子供と遊ぶ

実装が完了したので、早速娘と遊んでみましょう。

WEBサイトはiPhoneで表示させます。どれくらい時間がかかるかわからないのでiPhoneの画面ロックはOFFにしておきましょう。

まずワンをクローゼットに隠し、南京錠で施錠。どくろのサイトのURLをQRコードにし、「ちょうせんじょう」と書き足して娘に渡します。

f:id:hasegawa-ma:20160526231803j:plain:w320

iPhoneでQRコードを読み込むと、どくろからワンはいただいたとメッセージが……

f:id:g-editor:20160704182315j:plain:w320f:id:g-editor:20160704182325p:plain:w320

極力かわいく描いたつもりが、この時点で若干びびり気味です。とはいえ3問答えないとワンは取り戻せません。がんばれ、娘!

第1問

やはり引き算はまだ難しかったようで、なかなか進みません。答えがわからず、しばらく考えていると・・・

f:id:g-editor:20160706164304j:plain:w320

ボールが落ちていきました。

「あっ 2!」

f:id:g-editor:20160704182402p:plain:w320

正解!最初の数字は「5」。

第2問

続けて第2問へ。こちらも同じように答えを聞いてきましたが、

f:id:g-editor:20160706164337j:plain:w320

ちょっと待つと色がつくので気づいたようです。「あか!」

f:id:g-editor:20160704182641p:plain:w320

連続正解!次の数字は「7」です。

第3問

いよいよ最後の問題です。どくろから、パパの部屋へ行けとの指示がありました。

f:id:g-editor:20160704182446p:plain:w320

指示通りパパの部屋へ。急いで階段を駆け上がる娘。

f:id:hasegawa-ma:20160526232826j:plain:w320

どくろの言ったとおり、3つのランプが光っています。

f:id:g-editor:20160706164521j:plain:w320

ここに来てとうとう「わからない」と泣き出してしまいましたが、妻と二人で押すボタンを教えます。

f:id:hasegawa-ma:20160526232954j:plain:w320

正解の黄色のLEDにつながるボタンを押すと・・・

f:id:hasegawa-ma:20160526233024j:plain:w320

やった! どくろをやっつけた!

f:id:g-editor:20160704182538j:plain:w320

最後の番号は「3」でした。

ワン救出

f:id:hasegawa-ma:20160526233122j:plain:w320

番号を入手した娘はクローゼットへ走ります。どくろから聞いた3ケタの番号をセット。

f:id:hasegawa-ma:20160526233255j:plain:w320

するとクローゼットが開き

f:id:g-editor:20160706164619j:plain:w320

無事にワンを助け出すことができました!

反省

ゲーム後に娘から下記の指摘事項をいただきました。

  • 問題が3問しかないのは少ない。
  • ボタン1つ押すんじゃなくて、3つ全部押したら正解のほうがよかった。
  • 隠すのはぬいぐるみじゃなくて、次はいらない棒にしなさい。
  • どくろの色が青だと男の子っぽいので、ピンクにしなさい。
  • 同様にまつ毛をつけなさい。

なるべく怖くないようにどくろを描いたつもりが、それでも結構怖かったみたいでどくろへの指摘が多かったです。

また何かを隠す場合、親が思っている以上に子供はぬいぐるみに愛着があるので、どうでもいいようなものを隠したほうが良いと思いました。 ちなみに娘がぬいぐるみの代わりに隠したらいいと提案してきたのがこちらの棒です。

f:id:hasegawa-ma:20160526233456j:plain:w320

たしかにこれはいらないが・・・

感想

IoTは思った以上に子供と遊ぶのに親和性が高いように思いました。

今回のようなゲームを作る場合、キーボードの特定のキーを押したらクリアのような形でも実装ができますが、あえてボタンを押させることでゲームをやっている感がぐっと増します。

またボタンを押すかわりに3色のケーブルのうちどれか1つを切ったら正解というようなゲームも簡単にできそうです。アクション映画みたいですね。

Gobotは今回はじめて使いましたが直感的でわかりやすく、何よりGo言語の特徴であるchannelやgoroutineが使えるので、センサーを並列処理で扱うことが多いIoTなどのフィジカルコンピューティングに向いている気がします。

子どもと一緒に新しいことをするのは楽しいので、来年は正月にもRaspberry Piと連動した宝探しをやりたいと思います。

お知らせ
ぐるなびでは一緒に働く仲間を募集しています。


長谷川
ぐるなびビッグデータを使った商品開発を行っています。 2児の父親で、プライベートでは子供が喜ぶような何かを作ったりしてますが、どれも反応がいまいちなのが悲しいです。