Go でアプリの2回めです

今回から、実際にアプリを作りますが、作るものは既に決まっています。

ロボピッチャー等で有名な弊社のT氏ですが、彼はAndroidでマラカスアプリを作ったことがあるそうです。

なので、偉大な先輩であるT氏に肖って、私もマラカスアプリを作ってみようと思います。

仕様

まず、仕様です。

まずはT氏のマラカスアプリを見てみましょう。

マラカス

素晴らしいマラカスアプリですね。

まず、起動するとマラカスの画像が出ます。

そして、振るとマラカスの音がなります。

つまり、以下の機能をGomobileで実装すれば良いわけですね。

  1. 画像を出す
  2. 音を出す
  3. 振った時のイベントを取る

画像を出す

画像を出してみましょう。

実のところ、Go言語さっぱりなんですが

サンプルのAudioを、見てみます。

001

画像では伝わらないと思いますが、動きつつ、端に移動すると跳ね返り、音がなるようです。

つまり、これから画像と、音を鳴らす方法が分かるわけですね。

とりあえず、サンプルを複製してみます。

cp -r src\golang.org\x\mobile\example\audio\ src\golang.org\x\mobile\maracas

複製して、サンプルのソースを見てみます。

package main

import (
    "image"
    "log"
    "time"

    _ "image/jpeg"

    "golang.org/x/mobile/app"
    "golang.org/x/mobile/asset"
    "golang.org/x/mobile/event/lifecycle"
    "golang.org/x/mobile/event/paint"
    "golang.org/x/mobile/event/size"
    "golang.org/x/mobile/exp/audio"
    "golang.org/x/mobile/exp/f32"
    "golang.org/x/mobile/exp/gl/glutil"
    "golang.org/x/mobile/exp/sprite"
    "golang.org/x/mobile/exp/sprite/clock"
    "golang.org/x/mobile/exp/sprite/glsprite"
    "golang.org/x/mobile/gl"
)

const (
    width  = 72
    height = 60
)

var (
    startTime = time.Now()

    images *glutil.Images
    eng    sprite.Engine
    scene  *sprite.Node

    player *audio.Player

    sz size.Event
)

func main() {
    app.Main(func(a app.App) {
        var glctx gl.Context
        for e := range a.Events() {
            switch e := a.Filter(e).(type) {
            case lifecycle.Event:
                switch e.Crosses(lifecycle.StageVisible) {
                case lifecycle.CrossOn:
                    glctx, _ = e.DrawContext.(gl.Context)
                    onStart(glctx)
                    a.Send(paint.Event{})
                case lifecycle.CrossOff:
                    onStop()
                    glctx = nil
                }
            case size.Event:
                sz = e
            case paint.Event:
                if glctx == nil || e.External {
                    continue
                }
                onPaint(glctx)
                a.Publish()
                a.Send(paint.Event{}) // keep animating
            }
        }
    })
}

func onStart(glctx gl.Context) {
    images = glutil.NewImages(glctx)
    eng = glsprite.Engine(images)
    loadScene()

    rc, err := asset.Open("boing.wav")
    if err != nil {
        log.Fatal(err)
    }
    player, err = audio.NewPlayer(rc, 0, 0)
    if err != nil {
        log.Fatal(err)
    }
}

func onStop() {
    eng.Release()
    images.Release()
    player.Close()
}

func onPaint(glctx gl.Context) {
    glctx.ClearColor(1, 1, 1, 1)
    glctx.Clear(gl.COLOR_BUFFER_BIT)
    now := clock.Time(time.Since(startTime) * 60 / time.Second)
    eng.Render(scene, now, sz)
}

func newNode() *sprite.Node {
    n := &sprite.Node{}
    eng.Register(n)
    scene.AppendChild(n)
    return n
}

func loadScene() {
    gopher := loadGopher()
    scene = &sprite.Node{}
    eng.Register(scene)
    eng.SetTransform(scene, f32.Affine{
        {1, 0, 0},
        {0, 1, 0},
    })

    var x, y float32
    dx, dy := float32(1), float32(1)

    n := newNode()
    // TODO: Shouldn't arranger pass the size.Event?
    n.Arranger = arrangerFunc(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
        eng.SetSubTex(n, gopher)

        if x < 0 {
            dx = 1
            boing()
        }
        if y < 0 {
            dy = 1
            boing()
        }
        if x+width > float32(sz.WidthPt) {
            dx = -1
            boing()
        }
        if y+height > float32(sz.HeightPt) {
            dy = -1
            boing()
        }

        x += dx
        y += dy

        eng.SetTransform(n, f32.Affine{
            {width, 0, x},
            {0, height, y},
        })
    })
}

func boing() {
    player.Seek(0)
    player.Play()
}

func loadGopher() sprite.SubTex {
    a, err := asset.Open("gopher.jpeg")
    if err != nil {
        log.Fatal(err)
    }
    defer a.Close()

    img, _, err := image.Decode(a)
    if err != nil {
        log.Fatal(err)
    }
    t, err := eng.LoadTexture(img)
    if err != nil {
        log.Fatal(err)
    }
    return sprite.SubTex{t, image.Rect(0, 0, 360, 300)}
}

type arrangerFunc func(e sprite.Engine, n *sprite.Node, t clock.Time)

func (a arrangerFunc) Arrange(e sprite.Engine, n *sprite.Node, t clock.Time) { a(e, n, t) }

う~ん、さっぱりわからないですね。

そもそも、Go言語の書式もよく分かっていないんですが。

まあ、とりあえず画像だけが表示されるようにしてみましょう。

毎回Andoroid用にコンパイルすると時間がかかるので、Windowsで確認します。

cd src\golang.org\x\mobile\maracas
go run main.go

main.go:15:2: no buildable Go source files in D:\works\src\golang.org\x\mobile\exp\audio

エラーが出ましたね。

15行目は何かといえば、

    "golang.org/x/mobile/exp/audio"

audioの呼び出しですね。

これは、困りました。
音のテストをする時、Windowsでは試せないってことでしょうか?

まあ、これは後で考えるとして、まずは画像だけを出せるように削っていきます。

では、まずaudio絡みの記述をコメントアウトして実行します。

音はなりませんが、画像が表示され動いています。

次に、ソースのmainを見てみます。

func main() {
    app.Main(func(a app.App) {
        var glctx gl.Context
        for e := range a.Events() {
            switch e := a.Filter(e).(type) {
            case lifecycle.Event:
                switch e.Crosses(lifecycle.StageVisible) {
                case lifecycle.CrossOn:
                    glctx, _ = e.DrawContext.(gl.Context)
                    onStart(glctx)
                    a.Send(paint.Event{})
                case lifecycle.CrossOff:
                    onStop()
                    glctx = nil
                }
            case size.Event:
                sz = e
            case paint.Event:
                if glctx == nil || e.External {
                    continue
                }
                onPaint(glctx)
                a.Publish()
                a.Send(paint.Event{}) // keep animating
            }
        }
    })
}

見る限り、goはmainが呼び出される言語ということですね。

そして、mainの中でeventを受けるようになっています。

            case lifecycle.Event:
            case size.Event:
            case paint.Event:

よく分からないので、それぞれのイベントがどのタイミングで動くかを見てましょう。

何をすればprintデバッグが出来るのかよく分からないのですがサンプルを見ると

        log.Printf("error creating GL program: %v", err)

logで出せそうですね。

Printfはフォーマット付きなので、Printで出してみます。

log.Print("test")

D:\works\src\golang.org\x\mobile\maracas>go run main.go
2016/05/20 19:16:16 test

コンソールに表示されますね。

            case lifecycle.Event:
log.Print("lifecycle")
                switch e.Crosses(lifecycle.StageVisible) {
                case lifecycle.CrossOn:
                    glctx, _ = e.DrawContext.(gl.Context)
                    onStart(glctx)
                    a.Send(paint.Event{})
                case lifecycle.CrossOff:
                    onStop()
                    glctx = nil
                }
            case size.Event:
log.Print("size")
                sz = e
            case paint.Event:
log.Print("paint")
                if glctx == nil || e.External {
                    continue
                }
                onPaint(glctx)
                a.Publish()
                a.Send(paint.Event{}) // keep animating
            }

イベント毎にログを出してみると

2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:47 paint
2016/05/20 19:17:48 paint
2016/05/20 19:17:48 paint
2016/05/20 19:17:48 paint
2016/05/20 19:17:48 paint
2016/05/20 19:17:48 paint

paintばかりが出ます。
時間を見ると、同じ秒内でおよそ60回出ているので、多分フレーム毎に走るものでしょう。

又、リサイズすると「size」が走り、画面フォーカスを当てたり、外したりすると「lifecycle」走るようでした。

さて、試行錯誤しながらですが、画像表示の記述のみに削ってみました。

package main

import (
    "image"
    "log"
    "time"

    _ "image/jpeg"

    "golang.org/x/mobile/app"
    "golang.org/x/mobile/asset"
    "golang.org/x/mobile/event/lifecycle"
    "golang.org/x/mobile/event/paint"
    "golang.org/x/mobile/event/size"
//  "golang.org/x/mobile/exp/audio"
    "golang.org/x/mobile/exp/f32"
    "golang.org/x/mobile/exp/gl/glutil"
    "golang.org/x/mobile/exp/sprite"
    "golang.org/x/mobile/exp/sprite/glsprite"
    "golang.org/x/mobile/gl"
)

const (
    width  = 72
    height = 60
)

var (
    startTime = time.Now()

    images *glutil.Images
    eng    sprite.Engine
    scene  *sprite.Node

//  player *audio.Player

    sz size.Event
)

func main() {
    app.Main(func(a app.App) {

        var glctx gl.Context
        for e := range a.Events() {
            switch e := a.Filter(e).(type) {
            case lifecycle.Event:
                    glctx, _ = e.DrawContext.(gl.Context)
                    onStart(glctx)
            case size.Event:
                sz = e
            case paint.Event:
                if glctx == nil || e.External {
                    continue
                }
                onPaint(glctx)
                a.Publish()
            }
        }
    })
}

func onStart(glctx gl.Context) {
    images = glutil.NewImages(glctx)
    eng = glsprite.Engine(images)
    loadScene()

//  rc, err := asset.Open("boing.wav")
//  if err != nil {
//      log.Fatal(err)
//  }
//  player, err = audio.NewPlayer(rc, 0, 0)
//  if err != nil {
//      log.Fatal(err)
//  }
}

func onPaint(glctx gl.Context) {
    glctx.ClearColor(1, 1, 1, 1)
    glctx.Clear(gl.COLOR_BUFFER_BIT)
    eng.Render(scene, 1, sz)
}

func loadScene() {
    gopher := loadGopher()
    scene = &sprite.Node{}
    eng.Register(scene)
    eng.SetSubTex(scene, gopher)
    eng.SetTransform(scene, f32.Affine{
        {width, 0, 0},
        {0, height, 0},
    })
}

func boing() {
//  player.Seek(0)
//  player.Play()
}

func loadGopher() sprite.SubTex {
    a, err := asset.Open("gopher.jpeg")
    if err != nil {
        log.Fatal(err)
    }
    defer a.Close()

    img, _, err := image.Decode(a)
    if err != nil {
        log.Fatal(err)
    }
    t, err := eng.LoadTexture(img)
    if err != nil {
        log.Fatal(err)
    }
    return sprite.SubTex{t, image.Rect(0, 0, 360, 300)}
}

画像が動かなくなりました。

002

あとは、この画像をマラカスに変更して、音がなるようにすれば良いわけですね。

と、思っていたんですが
画像が左端にあるのは、正直微妙な気がします。

なので、真ん中によるようにしていみます。

画像を真ん中に寄せるには、まず画面のサイズが必要です

ドキュメント

type Event struct {
WidthPx, HeightPx int

WidthPt, HeightPt geom.Pt

PixelsPerPt float32

Orientation Orientation
}

sizeイベントを見ると、WidthPxとHeightPxがありますね。
型もintですのでこれを使ってみましょう。

var poissonX = 0
var poissonY = 0
            case size.Event:
                sz = e
                poissonX = (sz.WidthPx - width) / 2
                poissonY = (sz.HeightPx - height) / 2
    eng.SetTransform(scene, f32.Affine{
        {width, 0, poissonX},
        {0, height, poissonY},
    })

表示位置を画面サイズから計算して、割り当ててみました。
これで良さそうな気がするのですが、実行するとエラーが出ます。

D:\works\src\golang.org\x\mobile\maracas>go run main.go
command-line-arguments
.\main.go:97: cannot use poissonX (type int) as type float32 in array or slice literal
.\main.go:98: cannot use poissonY (type int) as type float32 in array or slice literal

float32で無ければいけないそうです。

なので、型を変更します。

var poissonX float32 = 0
var poissonY float32 = 0

そして実行。

D:\works\src\golang.org\x\mobile\maracas>go run main.go
command-line-arguments
.\main.go:54: cannot use (sz.WidthPx – width) / 2 (type int) as type float32 in assignment
.\main.go:55: cannot use (sz.HeightPx – height) / 2 (type int) as type float32 in assignment

最近、緩い型付けしか触っていないので、この辺に無頓着になっていますね。

キャストして、再度実行します。

poissonX = float32((sz.WidthPx - width) / 2)
poissonY = float32((sz.HeightPx - height) / 2)

だめですね。位置が反映されていません。
一応、logを出してみても、ポジションの値は問題ないようなので、再度変更した値で描画する必要があるようです。

仕方がないので、表示周りの作りを調整します。

func loadScene() {
    scene = &sprite.Node{}
    eng.Register(scene)
}

func adjustScene() {
    eng.SetSubTex(scene, loadGopher())
    eng.SetTransform(scene, f32.Affine{
        {width, 0, poissonX},
        {0, height, poissonY},
    })
}
            case size.Event:
                sz = e
                poissonX = float32((sz.WidthPx - width) / 2)
                poissonY = float32((sz.HeightPx - height) / 2)
                adjustScene()

これで、真ん中によるようになりました。

Windowsで成功したので、Androidで確認すると、出ない、、、、

002

完全に同じ動きをするとは思っていませんが、そこまで複雑なことはやっていないはずなんですが。

さて、画面が黒いところを見るとレンダリングがされていなさそうです。
それらしい、記述をサンプルと比較していくと「a.Send(paint.Event{})」という記述がありました。

イベントを送っている?まあ、それをadjustSceneの後に書いてみます。

003

画面が白くなりました。
画像は相変わらず出てきませんが。。。

ふと、思ったんですがこれ、画像が画面外にあるのではと

Androidでログはどうしたらいいものか

adbでログが見れるようでした。

adb logcat

ただ、このままではログが多すぎて分からないので、grepで絞り込みます。

adb logcat | grep GoLog
  • PC

    sz{949 516 949.00pt 516.00pt 1 0}

  • Android

    I/GoLog (18485): sz{1080 1920 162.00pt 288.00pt 6.6666665 1}
    I/GoLog (18485): sz{1920 1080 288.00pt 162.00pt 6.6666665 2}

pxとptの値が異なりますね。
恐らくこれが原因でしょう。

px以外で計算するよう必要があるようです。

さて、ptの値を数値に変換してとも考えたんですが、よくよくログを見てみると「6.6666665」と見慣れない値があります。

PixelsPerPt float32

これ、計算すればいけそうな気がしてきました。

1080 / 6.6666665 = 162

当たりですね。

poissonX = float32((sz.WidthPx - width) / 2) / sz.PixelsPerPt
poissonY = float32((sz.HeightPx - height) / 2) / sz.PixelsPerPt

そして、サイズを変更すると

004

出ました。
ただ、なんか真ん中じゃないですね。。。

一体何が原因なのかと思ったのですが、よくよく考えると画像サイズが大きいことに気が付きます。
この端末、横幅1000pxぐらいあるはずなんですが、画面の幅が画面の半分ぐらいあります。
ピクセルでの指定であれば4~500ぐらいになるのですが、widthの指定は100程度ということは、この指定もptということでしょう。

なので、width と height は PixelsPerPtでの処理を行わないように修正します。

poissonX = (float32(sz.WidthPx / 2) / sz.PixelsPerPt) - (width / 2)
poissonY = (float32(sz.HeightPx / 2) / sz.PixelsPerPt) - (height / 2)

005

無事中央寄せになりました。

振る

さて、次は振った時のイベントです。

以下が、用意されているイベントなんですが

  • key
  • lifecycle
  • mouse
  • paint
  • size
  • touch

振った時に走りそうなイベントが。。。

まさか、無いのか?

そんなはずはと思い調べていたら、sensorといったものがあったのでこれを使ってみましょう。

まず、ライブラリをダウンロードします。

go get golang.org/x/mobile/exp/sensor/

そして、イベントを拾うようにします。

            case sensor.Event:
                log.Print("sensor.Event")

そして実行してみたのですが、全く反応しません。

イベントであることは確かだと思うのですが、何が原因でしょうか?

sensorを調べていたら、サンプルが見つかりました。

サンプル

サンプルを見ていて、分かったことは

sensor.Notify(a)

で宣言して

                        sensor.Enable(sensor.Accelerometer, 1000*time.Millisecond)

で有効するというのが必要なようでした。

func main() {
    app.Main(func(a app.App) {
        sensor.Notify(a)
        var glctx gl.Context
        for e := range a.Events() {
            switch e := a.Filter(e).(type) {
            case lifecycle.Event:
                glctx, _ = e.DrawContext.(gl.Context)
                onStart(glctx)
                switch e.Crosses(lifecycle.StageVisible) {
                    case lifecycle.CrossOn:
                        sensor.Enable(sensor.Accelerometer, 1000*time.Millisecond)
                    case lifecycle.CrossOff:
                        sensor.Disable(sensor.Accelerometer)
                }
            case size.Event:
                sz = e
                poissonX = (float32(sz.WidthPx / 2) / sz.PixelsPerPt) - (width / 2)
                poissonY = (float32(sz.HeightPx / 2) / sz.PixelsPerPt) - (height / 2)
                adjustScene()
                a.Send(paint.Event{})
            case paint.Event:
                if glctx == nil || e.External {
                    continue
                }
                onPaint(glctx)
                a.Publish()
            case sensor.Event:
                log.Print("sensor.Event : ", e)
            }
        }
    })
}

これで、振るイベントを取ることが出来ました。

さて、次はどこまでを有効な処理とするかです。

実は、机にスマホを置いて触らない状態でも、加速度が動きます。
そこまで、大きい数値ではないのですが。
また、持ち上げただけでも5~9ぐらいの加速度が出てしまいます。

なので、その辺を考えてちょうどいい数値を出す必要があります。
他にも、加速度がxyzと三方向で取れるので、どれを見るかといった問題もあります。

実際に振って試していると、10を少し超えた当たり、12ぐらいが良さそうでした。
それを超えると、振っても反応しないことがあり、下回ると振っていないのに反応したりと望んだ動きではありませんた。

そして、方向はどれか一つに絞る必要もないと思うので、三方向全てで取れるようにします。

            case sensor.Event:
                if e.Data[0] > 12 || e.Data[1] > 12 || e.Data[2] > 12 {
                    log.Print("sensor.Event : ", e.Timestamp, e.Data)
                    // 音を鳴らす
                }

音をならす

次は音です。

まあ、Windowsでテストが出来ると楽なので
少し調べてみます。

D:\works\src\golang.org\x\mobile\example\audio>go run main.go
main.go:47:2: no buildable Go source files in D:\works\src\golang.org\x\mobile\exp\audio

実行すると、エラーが出ます。

D:\works\src\golang.org\x\mobile\example\audio>go get golang.org/x/mobile/exp/audio/
can’t load package: package golang.org/x/mobile/exp/audio: no buildable Go source files in D:\works\src\golang.org\x\mobile\exp\audio

ライブラリを落とそうにも失敗します。

ドキュメントを見ると、libopenalが必要だと書いていますが、今回の環境はWindowsなんですよね。

ドキュメント

apt-get install libopenal-dev

色々と試してみましたが、どうにも解決できなさそうなので一旦保留にします。

さて、肝心の音を鳴らす処理ですが、元々のサンプルにある処理そのままでいけそうですので、コメントアウトしていた記述を戻し、実行します。

                if (e.Data[0] > 12 || e.Data[1] > 12 || e.Data[2] > 12) {
                    boing()
                }

無事、振ると音が鳴るようになりました。

ただ、たまに二重に音がなる事がありました。
恐らく、前の処理が終わる前に、次の処理が走っている為だと思われます。

なので、一度反応したら、暫くの間は新たに反応しないようにします。

    boingTime int64
                if (e.Data[0] > 12 || e.Data[1] > 12 || e.Data[2] > 12) && e.Timestamp > (boingTime + 300000000){
                    boing()
                    boingTime = e.Timestamp
                }

これで、マラカスが鳴るようになりました。

アプリ

まとめ

無事、T氏同様のマラカスアプリを作ることが出来ました。
若干、反応が遅い気もしますが、これは本家同様ということで。

また、反応が過敏であったり、鈍かったりと、加速度の基準値をもう少し調整するともっと良い動きになるのではと思いました。

さて、次もまた別のアプリを作る予定です。

ではまた。