Chapter 26

自作ゲームランチャーを作ろう / Goのパッケージ

eihigh
eihigh
2025.01.31に更新

パッケージをうまく駆使すると、大規模なプログラムを見通しよく整理できます。今回はランチャー(プログラムを起動するためのプログラム)を作りながら、パッケージを作る方法について学びます。

ファイル分割

パッケージを作る前に、ファイル分割について知っておきましょう。

Goはディレクトリの中でファイルを自由に分割できます。試しに main.go の内容を以下だけ残して、

main.go
package main

import "github.com/eihigh/miniten"

func main() {
	p.y = ground - 38 // 初期値=地上
	miniten.Run(draw)
}

game.go というファイルを新しく作り、package main とインポート文を書いた後、残り全てを game.go に移してしまいましょう。

game.go
package main

import "github.com/eihigh/miniten"

type player struct {
	x     float64
	y     float64
	vy    float64
	inAir bool
}

var (
	ground = 300.0
	p      player
)

func draw() {
	// 地面描画
	miniten.DrawRect(0, int(ground)-10, 640, 360)

	// ...以下略...

package main とインポート文だけはファイルを分割するごとにまた必要になりますが、残りのプログラムは main.go にあったものをそのまま移してきただけです。この状態で go run . して問題なく今まで通り動作するのを確かめてみましょう。

ファイル分割は本当に自由です。センスの見せ所ですね。ちなみに今やった分割はセンスが無いので参考にしないでください。

ランチャー

プログラムを起動するためのプログラムをランチャーと呼びます。今回は、今まで作ってきたあなたの力作を集めた至高のゲームランチャーを作ってみます。

まずはHello, World! のときに説明したように、新たなディレクトリ(名前は自由ですが launcher がいいかな)を作り、モジュールを初期化(go mod init launcher)してください。

launcher ディレクトリにはいつも通り main.go を作ります。今回はそれに加えて、flappy ディレクトリを作り、その中にはねるgopherくんゲームの main.go をコピーして持ってきます。rungame ディレクトリも作って同じように走るgopherくんゲームのプログラムをコピーしてきます。全体はこのような構造になります。

launcher
|-- flappy
|   `-- main.go
|-- rungame
|   |-- game.go
|   `-- main.go
|-- go.mod
`-- main.go

launcher直下の main.go はこんな感じで、ターミナルに入力された数字を元に起動するゲームを変えるようにしましょう。

main.go
package main

import (
	"fmt"
)

func main() {
	fmt.Println("起動したいゲームの数字を入力してね")
	fmt.Println("0) はねるgopherくんゲーム")
	fmt.Println("1) 走るgopherくんゲーム")
	input := 0
	fmt.Scan(&input)
	switch input {
	case 0:
		// はねるgopherくんゲームを起動
	case 1:
		// 走るgopherくんゲームを起動
	}
}

さてどうやったら各ゲームを起動できるかがこのページの肝です。

mainパッケージとそれ以外のパッケージ

Goのパッケージはディレクトリごとに存在します。今回だとlauncher自体と、flappy ディレクトリと、rungame ディレクトリがあるので、計3つのパッケージが今あることになります。パッケージをインポートすればそのパッケージが利用できるようになるので、main.go から flappyrungame をインポートして呼び出したいところですが、それには今のままだとダメで、若干改修が必要です。

まず、main パッケージはプログラムの起点となる特殊なパッケージなので、よそのパッケージから利用することはできません。コピーしてきたゲームのパッケージ宣言を main から flappy, rungame に書き換えましょう。

flappy/main.go
-package main
+package flappy
 
 import "github.com/eihigh/miniten"
 
 var (
 	y  = 0.0
 	vy = 0.0 // Velocity of y(速度のy成分)の略

 	// ...以下略...
rungame/main.go
-package main
+package rungame
 
 import "github.com/eihigh/miniten"
 
 func main() {
 	p.y = ground - 38 // 初期値=地上
 	miniten.Run(draw)
 }
rungame/game.go
-package main
+package rungame
 
 import "github.com/eihigh/miniten"
 
 type player struct {
 	x     float64
 	y     float64
 	// ...以下略...

これでランチャーから flappy, rungame パッケージがインポートできるようになりました。ですがもう一手間必要です。

パッケージは大文字から始まる関数や変数、型だけを公開します。小文字のものは非公開であり、インポートしたとて呼び出せません。というわけで、flappy, rungame の main 関数を大文字から始まる Main 関数に書き換えます。これでインポートすれば呼び出せるようになります。

flappy/main.go
 package flappy
 
 import "github.com/eihigh/miniten"
 
 var (
 	y  = 0.0
 	vy = 0.0 // Velocity of y(速度のy成分)の略
 
 	frames = 0       // 経過フレーム数
 	wallXs = []int{} // 壁のX座標
 	wallYs = []int{} // 壁のY座標
 )
 
-func main() {
+func Main() {
 	miniten.Run(draw)
 }
rungame/main.go
 package rungame
 
 import "github.com/eihigh/miniten"
 
-func main() {
+func Main() {
 	p.y = ground - 38 // 初期値=地上
 	miniten.Run(draw)
 }

そういえば、新たなモジュールを作ったので、go mod tidy がまた必要になるかもしれません。適宜実行してください。

$ go mod tidy

そしたら、ランチャーから呼び出してみましょう!インポートパス=パッケージの場所は "モジュール名/flappy" のようになることに注意してください。詳しくは次のページで解説します。

main.go
 package main
 
 import (
 	"fmt"
 
+	"launcher/flappy"
+	"launcher/rungame"
 )
 
 func main() {
 	fmt.Println("起動したいゲームの数字を入力してね")
 	fmt.Println("0) はねるgopherくんゲーム")
 	fmt.Println("1) 走るgopherくんゲーム")
 	input := 0
 	fmt.Scan(&input)
 	switch input {
 	case 0:
 		// はねるgopherくんゲーム
+		flappy.Main()
 	case 1:
 		// 走るgopherくんゲーム
+		rungame.Main()
 	}
 }
$ go run .
起動したいゲームの数字を入力してね
0) はねるgopherくんゲーム
1) 走るgopherくんゲーム
0
(はねるgopherくんゲームが起動する...)

数字を入力して対応するゲームが起動すれば、成功です!🎉

パッケージの使い所

ランチャー以外にも、プログラムの一部分をパッケージとして分けて、必要なものだけ公開することで、大規模なプログラムの見通しをよくすることができます。

が、個人でゲームを作っている間はほぼパッケージを分ける機会は無いかもしれません。むしろ分けることで見るべきところが増えて面倒になるというデメリットも...。

次のページで解説するモジュールを学んで、インターネット上に自分のパッケージを公開して使ってもらう経験を踏めば、ある程度勘所がわかってくるかもしれません。気長にやりましょう!

まとめ

  • 同じディレクトリの中でファイルは自由に分割できる。
  • ディレクトリごとにパッケージが存在する。
  • main パッケージはプログラムの起点となる特殊なパッケージで、よそのパッケージからインポートできない。
  • パッケージをインポートすると、公開された関数・変数・型に限って使えるようになる。
  • パッケージのインポートパス=パッケージの場所は "モジュール名" から始まる文字列になる。

そんなものがあるんだな、程度でサラッと把握したらOKです!

詳しい解説

mainパッケージ

Goで実行できるのはmainパッケージだけです。試しに flappyrungame ディレクトリを開いて go run . するとエラーになるのが確かめられるはずです。

$ go run .
package launcher/flappy is not a main package

mainパッケージの高度な使い方として、モジュールの根っこではなく子ディレクトリにmainパッケージを置いたり、一つのモジュールの中に多数のmainパッケージを作るなどがあります。Ebitengineのexamples配下にはたくさんのmainパッケージがあるのでわかりやすいかと思います。

go run の引数

launcher を開いて、go run ./flappy のように ./ から始まるパスを引数として渡すことで、子ディレクトリを実行できます。

もちろんエラーになりますが
$ go run ./flappy
package launcher/flappy is not a main package

./ を忘れると思ったような挙動にならないので注意してください(詳しく言うと、GOROOT からパッケージを探そうとする)。

init関数

initという名前の関数は特殊で、プログラムの起動時、mainパッケージのmain関数より先に自動で実行されます。主にmain以外のパッケージが変数の初期化を行うときに使います。

rungame/main.go
 package rungame
 
 import "github.com/eihigh/miniten"
 
+func init() {
+	p.y = ground - 38
+}
 
 func Main() {
-	// p.y = ground - 38 // 初期値=地上
 	miniten.Run(draw)
 }

うまく使えば便利なのですが、濫用すると「呼んでないのに実行された(init関数は起動時に自動で実行されるので)」処理が増えて、プログラムの流れを追いづらくなってしまいます。用法用量を守ってお使いください。

循環参照の禁止

パッケージを使い込もうとした人がよくぶつかるのがこの循環参照の禁止ルールです。循環参照とはパッケージAからBをインポートし、BからもAをインポートすることです。AとBの間に別のパッケージがあって間接的に循環していても同じく禁止です。これはむしろメリットと捉えて欲しくて、一つはこのルールのおかげで内部的なビルド(リンク)の仕組みがすごく単純になること、もう一つは強制的に適切な分割ポイントを見極めるよう促してくれていること、これらが循環参照の禁止によってもたらされると考えてください。

結局インターフェースと考え方は同じで、プログラムを分割したところで、やりとりするものの多さ(インターフェースならメソッドの数、パッケージなら公開しているものの数)が多ければ多いほど、分割されたもの同士がお互いをよく知らないといけないことを意味し、そうなると分割した意味はどんどん薄れ、しまいには分割しない方がましだったとなってしまうからです。

循環参照はその最たるもので、パッケージ同士が密接に連携していることを意味し、お互いを完全に把握していないと正しく動作しないので、分割することの意味は非常に薄くなってしまいます。そういうことなのでいっそ最初から禁止してくれた方がありがたいというわけです。

循環参照は「そもそもパッケージを分けるべきではなかった」合図です。迷ったら分けないの精神でパッケージとうまく付き合っていきましょう!