Goで簡単なゲームを作成しよう
さっそくGoを使って簡単なゲームを作ってみましょう。ここでの目的は、短いGoプログラムでゲームを作れる実感を持つことですので、作成するプログラムの詳細を理解する必要はありません。コードをコピー&ペーストしていっても良いですが、自分でタイピングしていく方が良いトレーニングになります。
準備
このコースでは、pixelというゲーム制作用のライブラリを使います。pixelのインストールは前のステップで行っているはずですが、まだの人は「Goプログラミングの準備」に戻って実施してください。MacとWindowsでページが異なりますので、ご利用のパソコンに合わせて実施してください。
まずは、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)
}
Goプログラムの実行
早速、入力したプログラムを実行してみましょう。
Atomのメニューから File > Save を選択するか、command+s(Windowsの場合はctrl+s)を押してファイルを保存してください。ファイル名を聞かれるので「flappy.go」と入力してください。
Goプログラムを実行するには、コマンドラインに「go run (ファイル名)」と入力してリターンキーで実行します。
$ go run flappy.go
黒いウィンドウが開いたら成功です。ゲームウィンドウを閉じるには、ウィンドウの閉じるボタンをクリックするか、コマンドライン上でcontrolを押しながらcを押しましょう。
キャラクターを表示する
引き続き、画面にゲームで操作するキャラクターを表示させてみましょう。キャラクターは画像で用意したものを使います。まずは、ブルーのボタンをクリックして、キャラクターの画像ファイルをダウンロードしてください。
ダウンロードが完了したら、ソースプログラムと同じディレクトリに置いてください。引き続き、プログラムからキャラクター画像を読み込んで表示させるところまでやってみましょう。
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":