Help us understand the problem. What is going on with this article?

パズルゲームをGoで作ってみる

More than 1 year has passed since last update.

Go言語のMobileは出た位のころに試してはいたけど、ちょっと作りたいものがあったので、現在のはどうかな?ってことで少し調べてみることにした。しかしgoのmobileだと郷ひろみさんのモバイルサイトが出てきて大変ですね。

簡単なパズルゲームでもつくってみます。

開発環境

Windows10 64Bit機。Golandを使用しました。Goは1.10.1です。

インストール

go get golang.org/x/mobile/

を実行。GOPATHに展開されますので、src/golang.org/x/mobile/example/flappyに移動して

go run main.go game.go

を実行してみます
※エラーになる場合、出てきたパッケージ(おそらくshinyなど)のインストール(go get)を行いましょう

実行すると画面が立ち上がり、

無題.png

Flappy Bird風の簡単なGopher君を動かすWindowが現れます。ジャンプして障害物をよけるゲームです。(やられた時のGopher君かわいいのでみなさんもやってみましょう!

画像を利用する部分が参考にできそうです。

実装

flappyを参考にしながら、パズルゲームを作成してみましょう。
「盤面を作って、1つをピックして動かしてみる」
って感じのものを作ってみます。

盤面を作ってみる

このflappyの実装を元に画像を並べてみます。まずはパズルの画像を準備します。

pazzle.png

まぁこんな感じですかね?(絵がきれいだったらモチベ上がりそう)1つにつなげておきます。
これをassetsディレクトリを作成し、置いておきます。

constで各パズルの値を準備しておきます。

const (
    texRed = iota
    texBlue
    texGreen
    texYellow
    texPurple
    texPink
    texBlack
)

ランダムで選択する値としてrandomPiece()を宣言しておきます。

func randomPiece() int {
    return rand.Intn(7)
}

※今後出てくるソースはerror判定などを省いている場合がありますので、実装の参考にされる方は注意してください。

初期処理

アプリが起動して初期処理を行っていくわけですが、
引数「golang.org/x/mobile/gl.Context」はEventから

    ctx, _ = e.DrawContext.(gl.Context)

で取得してきています。
それを利用して描画情報を取得してくるわけですが、
flappyではonStart()で行っています。このまま使ってよいでしょう。

    images = glutil.NewImages(ctx)
    eng = glsprite.Engine(images)

golang.org/x/mobile/exp/gl/glutil.NewImages()」でglutil.Imagesを生成し「golang.org/x/mobile/exp/sprite/glsprite.Engine()」でsprite.Engineを生成します。

そこから今回使用する独自の構造体を生成します。flappyと同じく「Game」と名付けておきましょう。
※ただしpazzleパッケージを作成しています→終わってみたらそんなに大きなものにならなかったんですけどね。

    game = pazzle.NewGame()
    scene = game.Scene(eng)

NewGame()でパズルに必要な値を初期化しておきましょう!
※パズルの盤面の大きさとかかな、、、
その後Scene()でEngineを使って*sprite.Nodeを生成していますが、その部分を見ていきます。

テクスチャーのロード

Textureとして先ほど準備した画像を準備します。

    a, err := asset.Open("pazzle.png")
    defer a.Close()
    m, _, err := image.Decode(a)
    t, err := eng.LoadTexture(m)

assetを利用してOpen()してeng.LoadTexture()で準備しています。
asset.Open()には先ほどassetsに保存した画像ファイル名を渡します。
sprite.SubTexとして、切り取るのですが、
flappyだと一気に切り取ってますが、lintから文句が来たので、なんとなくループで展開しています。

    const n = 36
    rtn := make([]sprite.SubTex,texBlack+1)
    //パズルのピースを切り取る
    for idx := texRed ; idx <= texBlack ; idx++ {
        rect := image.Rect(n*idx, 0, n*(idx+1), n)
        rtn[idx] = sprite.SubTex{
            T:t,
            R:rect,
        }
    }

これで画像の準備は完了です。

盤面の画像を作成

まずは大元のsprite.Nodeを生成してEngineに登録しておきます。

    scene := &sprite.Node{}
    eng.Register(scene)
    eng.SetTransform(scene, f32.Affine{
        {1, 0, 100},
        {0, 1, 100},
    })

Engine.SetTransform()を利用して倍率、表示座標を設定します。
わかりやすく100,100の位置に表示してみましょう!

そしてNodeを構成していくわけですがまずはArrange()関数を持つ型を宣言します。

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) }

NodeにArrangerとしてarrangerFuncを登録(newNode呼び出し)していきます。

    newNode := func(fn arrangerFunc) {
        n := &sprite.Node{Arranger: arrangerFunc(fn)}
        eng.Register(n)
        scene.AppendChild(n)
    }

このnewNode()で設定してあげることで描画処理を行うロジックを作成していきます

描画してみる

newNode()の準備ができたら盤面を描画してみましょう。まずはピースを盤面に設定してあげます。

    for x,_ := range g.board.data {
        for y,_ := range g.board.data[x] {

            x := x
            y := y

            newNode(func(eng sprite.Engine, n *sprite.Node, t clock.Time) {
                a := f32.Affine{
                    {g.imageX, 0, (float32(x) * g.imageX)},
                    {0, g.imageY, (float32(y) * g.imageY)},
                }
                eng.SetSubTex(n, texs[g.board.data[x][y].datum])
                eng.SetTransform(n, a)
            })
        }
    }

1度ローカルで受け取っていますが、ループ中にnewNode()を呼び出しているだけなので、実際の処理時に補償されない状態になるためです。

ひとまずこれで動作させると

盤面.png

という感じに表示されます。

ピックしてみる

サンプル(flappy)はジャンプするだけですが、今回作成するのはパズルですので、ピックする、移動する、離すという動作を作り込んでいきます。

描画イベント

盤面表示時には割愛しましたが、onStart()を呼び出した後に

a.Send(paint.Event{})

というメソッドを呼んでいます。
これによりappにイベントが伝達され、再描画のイベント処理に入ります。実際の処理はonPaint()を行っています。送り込めるのはinterface{}なので、なんでも送り込めます。

flappyはゲームの性質上、描画をループ(Paint.Event時に再描画)していますが、パズルゲームは自分のタイミングで呼び出していきましょう。

flappyではonPaint()では独自の算出処理とレンダリング処理を行っています。
ゲームの値を更新(床の描画、Gopher君の動き)が描画時に行われるためですが、今回作成するゲームでは指で動かした時に値を更新するので呼んでいません。
※時間制限などをつける場合に使うといいかな?

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

描画データを変更した後に

a.Publish()

で「実際の表示処理」を行ってくれます。

touch.Event

それではタッチイベントを監視して、「ピッキングされたら」みたいな処理を埋め込んで行ってみましょう!

画面を触るとtouch.Eventが飛んできます。
Touchイベントが発生したらまず行いたいのは「どこをピッキングしたか」という情報とピッキング状態にあるというフラグ処理ですね。

仕組みというより泥臭いロジックを書いていきます。
Event.TypeにTypeBegin,TypeEndが存在するのでEndは現状では無視していきましょう。

        touchType := e.Type
    if touchType == touch.TypeBegin{
        //座標からピックしているピースを特定
        return g.pickPiece(e.X,e.Y)
    } else if g.state == statePick || g.state == stateMove {
        //移動処理
        return g.move(e.X,e.Y)
    }

pickPiece()の戻り値は描画するか?って感じにしています。
座標が盤面に入っているか?を算出して、どれをピックしたか?と現在の座標を算出しています。

そして前述したnewNode()で書き込んでいる位置に状態を見て書き出す処理を入れます。

    if g.state == statePick || g.state == stateMove {
        if  x == g.pieceX && y == g.pieceY {
            //座標はオフセットや倍率がかかっているので算出
            startX = (g.pickX - g.boardX) /g.zoom
            startY = (g.pickY - g.boardY) /g.zoom
            //中央分引き込む
            startX -= g.imageX/2
            startY -= g.imageY/2
        }
    }

ピックして移動することができました。
割愛していますが、移動先が決まるとパズルの盤面を更新したりしています。

bandicam-2018-04-13-08-39-49-698.gif

高速に動かすともっさりするのでチューンが必要ですね。

パズルの終了処理など

ピックを終えたら、パズルのルールに従って、パズルを消す?とかの処理も必要でしょうが、まぁ今回は動かすことが目的なので、ルールとかはやめておきましょう。

音を出してみる

せっかく動いたのに何か味気ないなぁと感じるのは、移動した時の音ですよね!
これがパズルゲームの醍醐味と言って問題ないでしょう!

と思ったがaudioのサンプル自体やパッケージがなくなっている。。。
調べてみたら、、、

https://go-review.googlesource.com/c/mobile/+/27671

おお、、、一旦諦め。

debugを使ってみる

mobileを初めて触った時に感動したのが、
golang.org/x/mobile/exp/app/debug」パッケージにあるFPS表示。
※感動したのは文字列をドットで変数にしてあるから

onStart()のところで

images = glutil.NewImages(ctx)
fps = debug.NewFPS(images)

という風にimagesを設定してあげます。

あとはonPaint()で

    fps.Draw(sz)

という風にsize.Eventを渡してあげると

fps.png

画面の左下にFPSが表示されます。
※動かしてないと描画が入らないので、常にしっかりしたFPSが出るわけじゃないですが。

今回触れなかった画面サイズについて調べるにはいいかも。
※例えば「sz.HeightPt」で画面の高さがわかる

実機への導入

で終わろうと思ったのですが、いろいろトラブったので別の記事にしていこうかと。

雑感

作りたいものは全く違うものなんですが、まぁなんとなくパズルゲームっぽくできました。
※ゲームの部分は実装してませんが。

PureGoで作成できるのは現在のところはこういう感じのゲームアプリになると思います。
ゲームじゃない感じのものを作りたければivyなどを参考(実際AndroidiOSに配布されています)にするとよいでしょう。

やる気(+実力)あったらチューンとかやってみるかも。

ソース

ちょっと実機の部分で触る(予定がある)んでブランチにしておきました。

https://github.com/shizuokago/mobile/tree/qiita-2a5bbd35a98153e1b72f

実機導入の記事、、、いつになるかな、、、

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away