Go lang コース

Goで簡単なゲームを作成しよう

さっそくGoを使って簡単なゲームを作ってみましょう。ここでの目的は、短いGoプログラムでゲームを作れる実感を持つことですので、作成するプログラムの詳細を理解する必要はありません。コードをコピー&ペーストしていっても良いですが、自分でタイピングしていく方が良いトレーニングになります。

このコースでは、pixelというゲーム制作用のライブラリを使います。pixelのインストールは前のステップで行っているはずですが、まだの人は「Goプログラミングの準備」に戻って実施してください。MacWindowsでページが異なりますので、ご利用のパソコンに合わせて実施してください。

まずは、cdコマンドでご利用のPCの作業ディレクトリに移動してください。特に作業ディレクトリを用意していない場合は、$GOPATH/srcを作業ディレクトリとして使います。

cd $GOPATH/src

次に、このゲームのための新しいディレクトリを作成して、そのディレクトリに移動します。コマンドライン上で以下のコマンドを実行してください。

mkdir flappy
cd flappy

このディレクトリでAtomを開きます。Atomを開くには、アプリケーションの一覧などからAtomを起動しても良いですが、コマンドラインから開くと今いるディレクトリが開かれた状態で起動するので便利です。

atom .

Atomが起動しましたら、Atomのメニューから File > New File を選択して新しいファイルを開きます。

ファイルが開いたら、そこにゲームで使用するウィンドウを表示するプログラムを作成します。下記のコードをファイルに入力してください。

package main

import (
    "github.com/faiface/pixel"
    "github.com/faiface/pixel/pixelgl"
)

func run() {
    cfg := pixelgl.WindowConfig{
        Title:  "Game Screen",
        Bounds: pixel.R(0, 0, 1024, 768),
        VSync:  true,
    }
    win, err := pixelgl.NewWindow(cfg)
    if err != nil {
        panic(err)
    }

    for !win.Closed() {
        win.Update()
    }
}

func main() {
    pixelgl.Run(run)
}

早速、入力したプログラムを実行してみましょう。

Atomのメニューから File > Save を選択するか、command+s(Windowsの場合はctrl+s)を押してファイルを保存してください。ファイル名を聞かれるので「flappy.go」と入力してください。

Goプログラムを実行するには、コマンドラインに「go run (ファイル名)」と入力してリターンキーで実行します。

$ go run flappy.go

黒いウィンドウが開いたら成功です。ゲームウィンドウを閉じるには、ウィンドウの閉じるボタンをクリックするか、コマンドライン上でcontrolを押しながらcを押しましょう。

引き続き、画面にゲームで操作するキャラクターを表示させてみましょう。キャラクターは画像で用意したものを使います。まずは、ブルーのボタンをクリックして、キャラクターの画像ファイルをダウンロードしてください。

https://github.com/egonelbre/gophers

ダウンロードが完了したら、ソースプログラムと同じディレクトリに置いてください。引き続き、プログラムからキャラクター画像を読み込んで表示させるところまでやってみましょう。

package main

import (
    "image"
    "os"
    _ "image/png"
    "github.com/faiface/pixel"
    "github.com/faiface/pixel/pixelgl"
)
func loadPicture(path string) (pixel.Picture, error) {
    file, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    img, _, err := image.Decode(file)
    if err != nil {
        return nil, err
    }
    return pixel.PictureDataFromImage(img), nil
}

func run() {
    cfg := pixelgl.WindowConfig{
        Title:  "Game Screen",
        Bounds: pixel.R(0, 0, 1024, 768),
        VSync:  true,
    }
    win, err := pixelgl.NewWindow(cfg)
    if err != nil {
        panic(err)
    }

    pic, err := loadPicture("surfing.png")
    if err != nil {
        panic(err)
    }
    sprite := pixel.NewSprite(pic, pic.Bounds())

    for !win.Closed() {
        sprite.Draw(win, pixel.IM.Moved(pixel.V(500, 300)))
        win.Update()
    }
}

上手く、キャラクターが表示されましたか?

次に、このキャラクターを動かしてみます。このゲームでは、キャラクターは何もしないと下に落ちていって、spaceキーで上に上昇するようにしてみます。キャラクターの速度や位置は、「Hero」という名前の構造体を使ってまとめて管理します。「hero.○○」という表現が出てきたら、それはキャラクターについてのデータを操作しているんだな、という風に考えるとプログラムが理解できてきます。

import (
    "time"
    "image"
    "os"
    _ "image/png"
    "github.com/faiface/pixel"
    "github.com/faiface/pixel/pixelgl"
    "golang.org/x/image/colornames"
)
 
    sprite := pixel.NewSprite(pic, pic.Bounds())

    // Set Hero
    type Hero struct {
        velocity pixel.Vec
        rect pixel.Rect
        hp int
    }
    hero := Hero{
        velocity: pixel.V(200, 0),
        rect: pixel.R(0, 0, 100, 100).Moved(pixel.V(win.Bounds().W() / 4, win.Bounds().H() / 2)),
        hp: 100,
    }

    last := time.Now()
    for !win.Closed() {
        dt := time.Since(last).Seconds()
        last = time.Now()
        win.Clear(colornames.Skyblue)

        if win.Pressed(pixelgl.KeySpace) {
            hero.velocity.Y = 500
        }
        hero.rect = hero.rect.Moved(pixel.V(0, hero.velocity.Y * dt))
        hero.velocity.Y -= 900 * dt

        mat := pixel.IM
        mat = mat.ScaledXY(pixel.ZV, pixel.V(hero.rect.W() / sprite.Frame().W(), hero.rect.H() / sprite.Frame().H()))
        mat = mat.Moved(pixel.V(hero.rect.Min.X + hero.rect.W() / 2, hero.rect.Min.Y + hero.rect.H() / 2))
        sprite.Draw(win, mat)
        win.Update()
    }

このゲームでは障害物が登場して、これにぶつからないように進んでいくことにします。障害物はブルーの長方形で表すことにします。

障害物は同時に複数個、登場しますので、wallsというslice(複数のデータを扱うデータ形式)を使って取り扱います。少し分かりにくい表現が多いですが、丁寧にプログラムを書いていきましょう。

import (
    "time"
    "math/rand"
    "image"
    "os"
    _ "image/png"
    "github.com/faiface/pixel"
    "github.com/faiface/pixel/pixelgl"
    "github.com/faiface/pixel/imdraw"
    "golang.org/x/image/colornames"
)
 

    // Set walls
    type Wall struct {
        X float64
        Y float64
        W float64
        H float64
    }
    var walls []Wall

    distance := 0.0
    last := time.Now()
    for !win.Closed() {
        dt := time.Since(last).Seconds()
        last = time.Now()
        win.Clear(colornames.Skyblue)

        if len(walls) <= 0 || distance - (walls[len(walls)-1].X - win.Bounds().W()) >= 400 {
            new_wall := Wall{}
            new_wall.X = distance + win.Bounds().W()
            new_wall.W = win.Bounds().W() / 10
            new_wall.H = rand.Float64() * win.Bounds().H() * 0.7
            if rand.Intn(2) >= 1 {
                new_wall.Y = new_wall.H
            } else {
                new_wall.Y = win.Bounds().H()
            }
            walls = append(walls, new_wall)
        }
        drawing := imdraw.New(nil)
        for _, wall := range walls {
            drawing.Color = colornames.Blue
            drawing.Push(pixel.V(wall.X - distance, wall.Y))
            drawing.Push(pixel.V(wall.X - distance + wall.W, wall.Y - wall.H))
            drawing.Rectangle(0)

            if wall.X - distance < - wall.W {
                walls = walls[1:]
            }
        }
        drawing.Draw(win)
        distance += hero.velocity.X * dt

        if win.Pressed(pixelgl.KeySpace) {

障害物が右から左に流れてくるようになりましたか?

今の状態では、キャラクターが障害物にぶつかっても何も起こりません。今度は、キャラクターが障害物にぶつかったらHPが減っていき、これが0以下になるとゲームオーバーになるようにしてみましょう。

ゲームの状態を表すstatusという変数を定義して、これが「playing」のときはゲームの実行、「gameover」のときはゲームオーバーとなるように処理を分岐させます。

distance := 0.0
    last := time.Now()
    status := "playing"
    for !win.Closed() {
        dt := time.Since(last).Seconds()
        last = time.Now()
        win.Clear(colornames.Skyblue)
        switch status {
        case "playing":
            if len(walls) <= 0 || distance - (walls[len(walls)-1].X - win.Bounds().W()) >= 400 {
 
            drawing := imdraw.New(nil)
            for _, wall := range walls {
                drawing.Color = colornames.Blue
                drawing.Push(pixel.V(wall.X - distance, wall.Y))
                drawing.Push(pixel.V(wall.X - distance + wall.W, wall.Y - wall.H))
                drawing.Rectangle(0)
                if wall.X - distance <= hero.rect.Max.X && wall.X - distance + wall.W >= hero.rect.Min.X && wall.Y >= hero.rect.Min.Y && wall.Y - wall.H <= hero.rect.Max.Y {
                    drawing.Color = colornames.Red
                    drawing.Push(hero.rect.Min)
                    drawing.Push(hero.rect.Max)
                    drawing.Rectangle(0)
                    hero.hp -= 1
                    if hero.hp <= 0 {
                        status = "gameover"
                    }
                }
                if wall.X - distance < - wall.W {
                    walls = walls[1:]
                }
            }
            drawing.Draw(win)
            distance += hero.velocity.X * dt
            if hero.rect.Max.Y < 0 || hero.rect.Min.Y > win.Bounds().H() {
                status = "gameover"
            }
 
            sprite.Draw(win, mat)
        case "gameover":
            os.Exit(3)
        }
        win.Update()

障害物に何度かぶつかったり、画面の上下に進みすぎるとゲームが終了するようになりましたか?

最後に、ゲームオーバーになったときにスコアを表示して、spaceキーでゲームを再スタートできるようにしてみましょう。

import (
    "time"
    "math/rand"
    "image"
    "os"
    "fmt"
    _ "image/png"
    "github.com/faiface/pixel"
    "github.com/faiface/pixel/pixelgl"
    "github.com/faiface/pixel/imdraw"
    "github.com/faiface/pixel/text"
    "golang.org/x/image/font/basicfont"
    "golang.org/x/image/colornames"
)
 
    var walls []Wall

    // Set text
    basicAtlas := text.NewAtlas(basicfont.Face7x13, text.ASCII)
    basicTxt := text.New(win.Bounds().Center(), basicAtlas)

    distance := 0.0
    last := time.Now()
    status := "playing"
    for !win.Closed() {
 
        case "gameover":
            basicTxt.Clear()
            basicTxt.Color = colornames.Green
            line := fmt.Sprintf("Game Over! Score: %d\n", int(distance))
            basicTxt.Dot.X -= basicTxt.BoundsOf(line).W() / 2
            fmt.Fprintf(basicTxt, line)
            basicTxt.Draw(win, pixel.IM.Scaled(basicTxt.Orig, 4))
            if win.Pressed(pixelgl.KeySpace) {
                hero.hp = 100
                hero.rect = pixel.R(0, 0, 100, 100).Moved(pixel.V(win.Bounds().W() / 4, win.Bounds().H() / 2))
                status = "playing"
                distance = 0.0
                last = time.Now()
                walls = walls[:0]
            }
        }
        win.Update()
    }

これでゲームは完成です。

割と短いプログラムを書くだけで、ちゃんと動作するゲームができたと思います。細かいプログラムの説明はしていませんので、今の段階では、どの箇所が何に関するプログラムなのかが、ぼんやりと想像できればそれで十分です。

引き続きゲームを作りながら、プログラムを理解したり、自分でプログラミングしたりできるように、少しずつトレーニングしていきましょう。

キャラクターの速度によってキャラクター回転させるおまけのプログラムです。以下のコードを追加して、キャラクターの動きがどのように変わるかを見てみましょう。

import (
    "time"
    "math"
    "math/rand"
 
            mat := pixel.IM
            mat = mat.ScaledXY(pixel.ZV, pixel.V(hero.rect.W() / sprite.Frame().W(), hero.rect.H() / sprite.Frame().H()))
            mat = mat.Rotated(pixel.ZV, math.Atan(hero.velocity.Y / (hero.velocity.X * 3)))
            mat = mat.Moved(pixel.V(hero.rect.Min.X + hero.rect.W() / 2, hero.rect.Min.Y + hero.rect.H() / 2))
            sprite.Draw(win, mat)
        case "gameover":