Golang と SDL2 でゲームを作る

Golang で SDL2 を使って作ったゲームを RasPi3 で遊びました。 その方法を紹介します。

本記事は Go Advent Calendar 2016 24 日目の記事です。

導入

皆さんは突然ゲームを書きたくなること、ありませんか? ありますよね? あるでしょ。あると言ってください!

私は最近 ドキュメントスキャナ を入手しまして、 手元にあった古いプログラミング雑誌をまとめてスキャンしてました。 あ、プログラミング雑誌ってのは若い人にはわからないかもしれませんが、 読者が投稿したゲームなどのプログラムのソースコードが掲載され、 それを他の読者が打ち込んで楽しむそういう雑誌です。 だいたいは BASIC で、 今となっては決して長いとはいえないソースコードなんですが、 それでも 超圧縮 されていたりして、 今読んでも「あー楽しいな(楽しかったな)」ってなるわけです。 私が所持・スキャンしていたのは主に プログラムポシェットMSX・FAN といった徳間書店系の雑誌なんですが、 どうしてもスキャン作業中に読んでしまうわけですね。 で、そうして読んでいるうちに突然 「ゲーム作りたい。8bit機と見紛うようなチープなやつを!」 となったわけです。

そんなタイミングで、発売日に買い逃していた ニンテンドークラシックミニ ファミリーコンピューター が偶然買えて、届いてしまいました。 これ、ほんとうによくできていて、 ちょっと遊んだだけなんですが 「うぉーこのサイズの筐体で動くゲームが書きたいぞ!」 ってゲーム書きたい欲が爆発寸前。 そこで横をみると… RaspberryPi 3 (以下 RasPi3) が転がってるじゃありませんか。 RasPi3 は HDMI で TV とつながり、大画面でゲームができるな。 これだ、と。

そこからは逆算と好みの問題でした。 RasPi3 でゲームを書くなら SDL が良いでしょう。 でも今時 C言語 で書くのは嫌ですね。 Pythonとかでも悪くはないのですが、 私の好みで言えば断然 golang ですから、 「golang sdl」あたりのキーワードでぐぐったら veandco/go-sdl2 というバッチリなものがあるじゃないですか。 次に何を作るかですが、 最近リリースされて話題になった ゲーム がありましたよね。 まぁ私は iPhone 持ってないので遊んだことないんですが、 話題性の観点からもコレを パク オマージュして Gopher Run だな!

そうやってできたのが koron/gopherrun です。 以下の動画はそのデモです。

RasPi3 でビルド&起動すると、Gopher 君がのんびり歩いています。 そこでスペースキーを押すとジャンプしてゲームが始まります。 ジャンプの高さはスペースキーを押す長さで調整できます。 谷間に落ちるとゲームオーバーで、 もう一度スペースキーを押すと Gopher 君がのんびり歩いているタイトルへ戻ります。 ちなみにゲーム終了は ESC キー。

開発には XPS13 上の Xubuntu を使ったので、 デスクトップ Linux でも動くでしょう。 Windows は…ビルド環境を整えるのが面倒だったのでパス。 そこをガンバレば動くはず。

実行時にはカレントディレクトリの画像・音源リソースを読み込みます。 そのため単に go get github.com/koron/gopherrun でインストールして、 gopherrun を起動するとエラーになります。 ですから以下のような感じで、取得とビルドを別にして起動してみてください。

go get -v -u -d github.com/koron/gopherrun
cd ~/go/src/github.com/koron/gopherrun
go build
./gopherrun

いやぁー前置き長かったですね。 以下は技術的な解説で、3章に分かれています。 読んでも良いし、読まなくても良いでしょう。 (でも頑張って書いたので読んでくれると嬉しい)

  1. ソースコード解説
  2. RasPi3 のセットアップ
  3. 感想と評価

ソースコード解説

とはいってもシンプルすぎて、解説するところはないんですが… とりあえずは 初期化部分 から見てみましょう。

sdl.Init(sdl.INIT_EVERYTHING)
defer sdl.Quit()

w, err := sdl.CreateWindow("Gopher Run!", sdl.WINDOWPOS_UNDEFINED,
	sdl.WINDOWPOS_UNDEFINED, 1280, 720, sdl.WINDOW_SHOWN)
if err != nil {
	return err
}
defer w.Destroy()

r, err := sdl.CreateRenderer(w, -1, renderFlags)
if err != nil {
	return err
}
defer r.Destroy()

ここでは3つのことをしています。まず SDL 自体の初期化、 次に表示用のウィンドウの作成、 最後にウィンドウに描画を行うレンダラーの作成。 それぞれ使い終わったら後片付けが必要なんですが、 もう見慣れたであろう defer でスッキリ書けていますね。

その後には、キャラクター画像などのリソース読み込みが 続きます

// background characters
t1, s1, err := loadTexture(r, "chartable.png")
if err != nil {
	return err
}
defer t1.Destroy()
defer s1.Free()

// sprite characters
t2, s2, err := loadTexture(r, "spritetable.png")
if err != nil {
	return err
}
defer t2.Destroy()
defer s2.Free()

さらにジャンプ時の効果音を鳴らすために、 オーディオをセットアップして音源ファイルを読み込みます (*)。 音源ファイルは 無料効果音で遊ぼう! さまから jump07 を利用させていただきました。

err = mix.OpenAudio(mix.DEFAULT_FREQUENCY, mix.DEFAULT_FORMAT,
	mix.DEFAULT_CHANNELS, mix.DEFAULT_CHUNKSIZE)
if err != nil {
	return err
}

m1, err := mix.LoadMUS("jump07.mp3")
if err != nil {
	return err
}
defer m1.Free()

あとはもう本当に技術的には解説する場所がないんですが、 以下のような メインループ を作って ゴリゴリゴリと実装しただけです。

func (g *Game) Run() error {
	for g.running {
		g.initNewFrame()
		for ev := sdl.PollEvent(); ev != nil; ev = sdl.PollEvent() {
			g.procEvent(ev)
		}
		g.update()
		if err := g.render(); err != nil {
			return err
		}
		g.ren.Present()
	}
	return nil
}

全体的には、やはり C言語 よりスッキリしますし C++ よりも手間要らずで、 ゲーム本体に集中できたので書いてて楽しかったですね。 なんていうか少年の頃の気持ちを思い出しました。

RasPi3 のセットアップ

続いて RasPi3 上の動作環境の構築の話です。 本当は一番最初にコレをやってイケるってことを確認してから始めたので、 時系列と記述順は逆になっています。

SDL2 のインストール

まず最初に断っておきますが、最新の RasPi3 の OS (Raspbian) には SDL2 のコンパイル済みパッケージが提供されています。 ですから sudo apt install libsdl2-dev 一発で SDL2 (go-sdl2) が利用可能になります。 しかし私はこれを使わずに、自分でコンパイル&インストールしました。 というのも、標準のパッケージには X11 のドライバなどの依存関係があり、 それは RasPi3 をコンソール・ターミナルだけで使っている私には、 余計なものだったのです。 コンソールから実行ファイルを起動して、 いきなりゲーム画面がフルスクリーンで起動するところにロマンを感じたのです。

さて SDL2 をビルドする前に RasPi3 を以下のコマンドで最新にしておきましょう。

sudo apt-get update
sudo apt-get upgrade
sudo reboot
sudo apt-get autoclean

その後、以下のビルドに必要な依存パッケージをインストールします。

  • build-essential
  • libfreeimage-dev
  • libopenal-dev
  • libpango1.0-dev
  • libsndfile1-dev
  • libudev-dev
  • libasound2-dev
  • libjpeg-dev
  • libtiff5-dev
  • libwebp-dev
  • automake

インストールのためのコマンドはこうなりました。

sudo apt-get install build-essential libfreeimage-dev libopenal-dev \
  libpango1.0-dev libsndfile1-dev libudev-dev libasound2-dev libjpeg-dev \
  libtiff5-dev libwebp-dev automake

次に SDL2 本体のソースコードをダウンロードして展開します。

mkdir ~/SDL2
cd ~/SDL2
wget https://www.libsdl.org/release/SDL2-2.0.5.tar.gz
tar xzf SDL2-2.0.5.tar.gz

そしてコンフィギュレーションしましょう。 以下の設定には今回こだわった X11 ドライバなどの無効化を含んでいます。 SDL2 にはラズパイのネイティブドライバが含まれているらしく、 それだけが有効になります。

cd SD2-2.0.5
mkdir build && cd build
../configure --host=armv7l-raspberry-linux-gnueabihf --disable-pulseaudio\
  --disable-esd --disable-video-mir --disable-video-wayland \
  --disable-video-x11 --disable-video-opengl

ここで make -j 4 で RasPi3 の速度を活かした並列ビルドをします。 ただコレにはちょっとした罠があり、CC build/SDL_audiotypecvt.loCC build/SDL_blendline.so といったあたりで失敗してしまう可能性があります。 もしそうなったら単に make とだけして、 問題の箇所がコンパイルできた後に Ctrl + C で止めて、 再度 make -j 4 すると良かったりするようです。

最後にコンパイルが終わったら sudo make install しましょう。 それで SDL2 本体のインストールは完了です。

しかし SDL2 には幾つかのサポートライブラリがあります。 今回は Image, Mixer (with MP3 support), TrueTypeFont (TTF) を インストールします。以下はそのコマンドです。

cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_image/release/SDL2_image-2.0.1.tar.gz
tar xzf SDL2_image-2.0.1.tar.gz
cd SDL2_image-2.0.1 && mkdir build && cd build
../configure
make -j 4
sudo make install

次の smpeg は Mixer で MP3 ファイルの読み込みをサポートするための 補助ライブラリです。 かならず Mixer に先立ってインストールしてください。

cd ~/SDL2
wget https://www.libsdl.org/projects/smpeg/release/smpeg2-2.0.0.tar.gz
tar xzf smpeg2-2.0.0.tar.gz
cd smpeg2-2.0.0 && mkdir build && cd build
../configure
make -j 4
sudo make install
cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_mixer/release/SDL2_mixer-2.0.1.tar.gz
tar xzf SDL2_mixer-2.0.1.tar.gz
cd SDL2_mixer-2.0.1 && mkdir build && cd build
../configure
make -j 4
sudo make install

最後の TTF がちょっと曲者で、 RasPi3 は OpenGL ES だけをサポートしていて OpenGL はしていないのですが、 そのような設定を TTF はサポートしていないらしく、 OpenGL を使う設定を強制されコンパイルエラーになってしまいました。 そこで以下のコマンドでは OpenGL を無効化しちゃっています。

cd ~/SDL2
wget https://www.libsdl.org/projects/SDL_ttf/release/SDL2_ttf-2.0.14.tar.gz
tar xzf SDL2_ttf-2.0.14.tar.gz
cd SDL2_ttf-2.0.14 && mkdir build && cd build
sed -i -e 's/have_opengl=yes/have_opengl=no/' ../configure
../configure
make -j 4
sudo make install

以上で SDL2 関連のインストールは完了です。

参考資料

golang のインストール

RasPi3 への golang のインストールは SDL2 に比べればはるかに楽です。 まぁ、何度もやって慣れているからなんですけども。 以下のコマンドで golang 1.8beta2 をインストールしました。

mkdir ~/opt && cd ~/opt
wget https://storage.googleapis.com/golang/go1.8beta2.linux-armv6l.tar.gz
tar xzf go1.8beta2.linux-armv6l.tar.gz
mv go go1.8beta2.linux-armv6l
ln -s go1.8beta2.linux-armv6l golang
export GOROOT=~/opt/golang
export PATH=$GOROOT/bin:$PATH
hash -r
go version

少々まどろっこしいことをしていますが、 あとで必要になったら別バージョンの golang をインストールして、 シンボリックリンクを貼り直すだけで手軽に切り替えられるようにするためです。 そういうことをやってくれるツールもありますが、 その導入・管理もそれなりに手間なのです。

動作確認

最後の以下のようなプログラムを実行して動作確認しました。

package main

import (
	"fmt"
	"github.com/veandco/go-sdl2/sdl"
)

func main() {
	sdl.Init(sdl.INIT_EVERYTHING)

	window, err := sdl.CreateWindow("test", sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED,
		800, 600, sdl.WINDOW_SHOWN)
	if err != nil {
		panic(err)
	}
	defer window.Destroy()
	sdl.ShowCursor(0)

	w, h := window.GetSize()
	fmt.Printf("Size: %d, %d\n", w, h)

	surface, err := window.GetSurface()
	if err != nil {
		panic(err)
	}

	rect := sdl.Rect{320, 180, 640, 360}
	surface.FillRect(&rect, 0xffff0000)
	window.UpdateSurface()

	sdl.Delay(10000)
	sdl.Quit()
}

実行前に go get github.com/veandco/go-sdl2/sdl するのを忘れないようにしましょう。(1敗)

感想と評価

最後に完走した感想です(こう書きたかっただけ)。

ゲームそのものは良い感じにチープで、 でも意外と遊べて面白いものができました。 明け方、眠くてしょうがない頃に遊べるようになったのですが、 ついつい意味もなく頑張って遊んでしまう中毒性がありました(寝不足のせい)。

コースはシードを固定した乱数で生成しています。 これがまた良い感じに序盤から鬼畜難易度のコースを生成してくれまして、 デモをみればわかりますが大きな壁として立ちはだかっています。 この難易度、プログラミング雑誌時代のチープさを再現していて良い感じなんですが、 遊ぶという観点からはもっとちゃんと調整したり、 レベルデザインできるようにしたほうが良さそうですね。 ちなみに開発時点ですが、 今よりももっと難易度の高い地形を連発してしまうバグがありました。 直してしまったんですがアレはアレで面白かったなと。 こういうのも自作ゲームの楽しみの1つかもしれません。

足りないものとして背景(遠景)、タイトルロゴなどいろいろありますね。 あとはハイスコアは是非とも欲しかった。 ドコまで到達したかの距離と、連続して最高速で走れた距離という2つの尺度で、 ハイスコアを競えあえたら面白そうです。 本当はそのあたり TTF を使って表示するつもりだったんですが、 現時点では時間切れによりあきらめました。

開発環境については、 golang, SDL2, RasPi3 どれもこういうサクッとゲームプログラミングするには、 最高としか言いようがありません。 ゲームを作るという目的で言えば現在では Unity とか Unreal Engine とか ゲームエンジンを使うのが絶対的に正しいんです。 しかし「プログラミングの楽しさを体験するためにゲームを作る」という目的では、 今回選んだこの組み合わせのほうがずっと良いのではないか、そう思えてなりません。 実にプリミティブな記述がキャラクターの動き、コースの生成、 そういった端々にダイナミックに影響を与えていくのは、 プログラミングの愉悦といって差し支えないと思います。

興味を持ったら golang, SDL2 でゲームプログラミングを始めてみてください。

余談

じつは今回いちばん辛かったのは Gopher 君のビットチューンなドット絵描きでした。 新しいツールを導入するのもアレだったので GIMP2 でポチポチ描いたんですが… キモかわいさはそこそこありますけど、アニメーション的にはいまいちでしたね。

追記

2016/12/24 21:10

Windows でも動いたと まっつんさん からご報告いただきました!