はじめに
概要
Go言語を用いて、ゲーム用のマップの自動生成のプログラムを書いてみます。
本記事では、マップ生成のロジックに当たる部分をメインに記述するため、使用するライブラリ固有の機能やインターフェース等に関する記述は省くこともあります。
そのため全体的なコードは、GitHubのリポジトリを参照してください。
使用するライブラリ
EngoというGo言語のゲーム作成用ライブラリを用います。
このライブラリは、自分は以前別の記事でも用いたものです。
また、記事中のコードで使用している素材画像は、こちらです。
コード
単純な地面の生成
まずは、単純な草原のマップを作ります。
まず最初に、ゲームのマップの一マスに当たる情報を保持する構造体を定義します。
type tileInfo struct {
spritesheetNum int
}
後にいろいろ情報をつけ足していきますが、現時点ではspritesheetNum
として、マップの描画に用いる画像の、タイルシート上の番号を保持するだけにします。
次に、ステージを表すクラスが持つ、ゲーム開始時に呼び出される関数の中で、stageTiles
という二重配列にマップのデータを入れていきます。
var stageTiles [screenLength][screenLength]tileInfo
for i, s := range stageTiles {
for j, _ := range s {
stageTiles[i][j].spritesheetNum = 0
}
}
spritesheetNum
として設定している値である0
は、何もない草原の画像を表すものです。
上のデータを基に、実際に描画の処理をします。
for i, s := range stageTiles {
for j, y := range s {
tile := &Tile{BasicEntity: ecs.NewBasic()}
tile.SpaceComponent.Position = engo.Point{
X: float32(j * cellLength),
Y: float32(i * cellLength),
}
tile.RenderComponent = common.RenderComponent{
Drawable: Spritesheet.Cell(y.spritesheetNum),
Scale: engo.Point{X: float32(cellLength / 16), Y: float32(cellLength / 16)},
}
tile.RenderComponent.SetZIndex(0)
sys.Add(&tile.BasicEntity, &tile.RenderComponent, &tile.SpaceComponent)
}
}
すると、以下のような何の変哲もない草原のマップが出来上がります。
草の種類を増やす
上のだと寂しいので、せっかく今回用いるタイルマップには複数種類の草の画像があるため、それらをランダムに用います。math/rand
をインポートして、ステージ情報作成の部分を以下のように変更します。
var stageTiles [50][50]tileInfo
for i, s := range stageTiles {
for j, _ := range s {
stageTiles[i][j].spritesheetNum = rand.Intn(4)
}
}
すると、以下の画像のような画面ができます。
少しはマシになりました。
川の生成
次に、マップに川を生成してみます。
まずは欲張らず、直線の川をランダムな地点から生成します。
以下の関数を、ステージ情報を作成した後に呼び出します。
// 川を生成する
func createRiver(w *ecs.World, stageTiles *[screenLength][screenLength]tileInfo) {
rand.Seed(time.Now().UnixNano())
// 川の描画情報
type riverInfo struct {
X int
Y int
tilenum int
}
// 川の始まりの地点の選択
riverStartPoint := rand.Intn(screenLength/2) + screenLength/4
var riverInfoArray []river_info
// 川の始まりの地点の描画
riverInfoArray = append(riverInfoArray, riverInfo{0, riverStartPoint, 60})
riverInfoArray = append(riverInfoArray, riverInfo{0, riverStartPoint+ 1, 61})
riverInfoArray = append(riverInfoArray, riverInfo{0, riverStartPoint+ 2, 62})
// 川の始まり以降の描画
for i := 1; i < screenLength; i++ {
riverInfoArray = append(riverInfoArray, riverInfo{i, riverStartPoint, 60})
riverInfoArray = append(riverInfoArray, riverInfo{i, riverStartPoint+ 1, 61})
riverInfoArray = append(riverInfoArray, riverInfo{i, riverStartPoint+ 2, 62})
}
// 引数として受けとったステージ情報を書き換える
for _, r := range riverInfoArray {
stageTiles[r.X][r.Y].spritesheetNum = r.tilenum
}
}
川の始まりの地点をランダムで選択し描画情報を記録し、その後下流に沿って一マスずつ描画情報を追加します。
そして最後にステージ情報のデータを書き換えています。
川の蛇行
上のコードだと川が直線で味気がないので、川を蛇行させます。
上のコードでは川を一マスずつ下りながら描画をしていましたが、川が蛇行している様子は数マスをかけて描くことになるので、現在描画しているのが蛇行の何マス目であるかをcurveGen
という変数で保持して、その値でswitch
をして描画内容を切り替えます。
func createRiver(w *ecs.World, stageTiles *[screenLength][screenLength]tileInfo) {
rand.Seed(time.Now().UnixNano())
type riverInfo struct {
X int
Y int
tilenum int
}
ifGoingSouth := true
if rand.Intn(2) == 1 {
ifGoingSouth = false
}
// 川の始まりの地点の選択
riverStartPoint := rand.Intn(screenLength/2) + screenLength/4
var riverInfoArray []riverInfo
// 初期位置作成
if ifGoingSouth {
riverInfoArray = append(riverInfoArray, riverInfo{0, riverStartPoint, 60})
riverInfoArray = append(riverInfoArray, riverInfo{0, riverStartPoint + 1, 61})
riverInfoArray = append(riverInfoArray, riverInfo{0, riverStartPoint + 2, 62})
} else {
riverInfoArray = append(riverInfoArray, riverInfo{riverStartPoint, 0, 49})
riverInfoArray = append(riverInfoArray, riverInfo{riverStartPoint + 1, 0, 61})
riverInfoArray = append(riverInfoArray, riverInfo{riverStartPoint + 2, 0, 73})
}
// 初期値以降作成
// 川の描画を終えた座標を持つカーソル
var riverCursorX int
var riverCursorY int
if ifGoingSouth {
riverCursorX = riverStartPoint
riverCursorY = 1
} else {
riverCursorX = 1
riverCursorY = riverStartPoint
}
// 川の蛇行を始めてからの時間
curveGen := 0
shouldContinue := true
var yArray [3]int
var xArray [3]int
var tileNum [3]int
for shouldContinue {
if ifGoingSouth {
yArray[0] = riverCursorY
yArray[1] = riverCursorY
yArray[2] = riverCursorY
xArray[0] = riverCursorX
xArray[1] = riverCursorX + 1
xArray[2] = riverCursorX + 2
switch curveGen {
case 0:
tileNum[0] = 60
tileNum[1] = 61
tileNum[2] = 62
case 1:
tileNum[0] = 49
tileNum[1] = 49
tileNum[2] = 50
curveGen = 2
case 2:
tileNum[0] = 61
tileNum[1] = 61
tileNum[2] = 62
curveGen = 3
case 3:
tileNum[0] = 85
tileNum[1] = 61
tileNum[2] = 62
curveGen = 0
}
riverCursorY++
if riverCursorY >= screenLength || riverCursorX+2 >= screenLength {
shouldContinue = false
}
if curveGen == 0 && rand.Intn(15) == 0 {
ifGoingSouth = !ifGoingSouth
curveGen = 1
}
} else {
yArray[0] = riverCursorY
yArray[1] = riverCursorY + 1
yArray[2] = riverCursorY + 2
xArray[0] = riverCursorX
xArray[1] = riverCursorX
xArray[2] = riverCursorX
switch curveGen {
case 0:
tileNum[0] = 49
tileNum[1] = 61
tileNum[2] = 73
case 1:
tileNum[0] = 60
tileNum[1] = 60
tileNum[2] = 72
curveGen = 2
case 2:
tileNum[0] = 61
tileNum[1] = 61
tileNum[2] = 73
curveGen = 3
case 3:
tileNum[0] = 96
tileNum[1] = 61
tileNum[2] = 73
curveGen = 0
}
riverCursorX++
if riverCursorX >= screenLength || riverCursorY+2 >= screenLength {
shouldContinue = false
}
if curveGen == 0 && rand.Intn(15) == 0 {
ifGoingSouth = !ifGoingSouth
curveGen = 1
}
}
riverInfoArray = append(riverInfoArray, riverInfo{yArray[0], xArray[0], tileNum[0]})
riverInfoArray = append(riverInfoArray, riverInfo{yArray[1], xArray[1], tileNum[1]})
riverInfoArray = append(riverInfoArray, riverInfo{yArray[2], xArray[2], tileNum[2]})
}
// 引数として受けとったステージ情報を書き換える
for _, r := range riverInfoArray {
stageTiles[r.X][r.Y].spritesheetNum = r.tilenum
}
}
森の生成
これまで作成したマップに、散発的に森を植えていきます。森の大きさは9マスに固定して、マップ中のランダムな個所に配置していきます。
func createForest(w *ecs.World, stageTiles *[screenLength][screenLength]tileInfo) {
rand.Seed(time.Now().UnixNano())
type forestInfo struct {
X int
Y int
tilenum int
}
// 森の描画情報を持つ
var forestInfoArray [][]forestInfo
for i := 0; i < rand.Intn(5)+minimumForestNum; i++ {
var tempForestCenter [2]int
shouldContinueSelecting := true
trialGen := 0
for shouldContinueSelecting {
// 森の中心地点をランダムに選択
tempForestCenter[0] = rand.Intn(screenLength)
tempForestCenter[1] = rand.Intn(screenLength)
if stageTiles[tempForestCenter[1]][tempForestCenter[0]].tileType == "grass" || trialGen > createForestMaximumTryCount {
if tempForestCenter[0]+1 < screenLength && tempForestCenter[1]+1 < screenLength && tempForestCenter[0]-1 > 0 && tempForestCenter[1]-1 > 0 {
//各種条件をクリアすれば、実際に森を描画していく
shouldContinueSelecting = false
}
}
trialGen++
}
// 一定回数以上森の中心を選択しようとして失敗した場合
if trialGen > createForestMaximumTryCount {
continue
}
// 森は最低9マスとして、まずその分を描画
var tempForestArray []forestInfo
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0], tempForestCenter[1], 70})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0], tempForestCenter[1] - 1, 58})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0] + 1, tempForestCenter[1] - 1, 59})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0] + 1, tempForestCenter[1], 71})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0] + 1, tempForestCenter[1] + 1, 83})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0], tempForestCenter[1] + 1, 82})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0] - 1, tempForestCenter[1] + 1, 81})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0] - 1, tempForestCenter[1], 69})
tempForestArray = append(tempForestArray, forestInfo{tempForestCenter[0] - 1, tempForestCenter[1] - 1, 57})
forestInfoArray = append(forestInfoArray, tempForestArray)
}
for _, r := range forestInfoArray {
for _, i := range r {
stageTiles[i.Y][i.X].spritesheetNum = i.tilenum
stageTiles[i.Y][i.X].tileType = "forest"
}
}
}
マップ上のランダムな個所を選択し、そこが草地であれば、その地点を中心として9マスの森を作成します。
作成する森のデータがすべてそろったら、マップのタイルのデータにそれを反映させます。
すると以下の画像のようなものが出来上がります。
最後に
今後はより多くの構造体(洞窟、城等)を配置すること、プレーヤーを追加してプレーヤーが土地改造(畑の作成など)を行えるようにすることなどに、注力したいです。