9
7

More than 1 year has passed since last update.

Goでゲーム用マップを自動生成してみる

Last updated at Posted at 2021-08-21

はじめに

概要

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)
    }
}

すると、以下のような何の変哲もない草原のマップが出来上がります。

1.png

草の種類を増やす

上のだと寂しいので、せっかく今回用いるタイルマップには複数種類の草の画像があるため、それらをランダムに用います。math/randをインポートして、ステージ情報作成の部分を以下のように変更します。

var stageTiles [50][50]tileInfo
for i, s := range stageTiles {
    for j, _ := range s {
        stageTiles[i][j].spritesheetNum = rand.Intn(4)
    }
}

すると、以下の画像のような画面ができます。

2.png

少しはマシになりました。

川の生成

次に、マップに川を生成してみます。
まずは欲張らず、直線の川をランダムな地点から生成します。
以下の関数を、ステージ情報を作成した後に呼び出します。

// 川を生成する
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
    }
}

川の始まりの地点をランダムで選択し描画情報を記録し、その後下流に沿って一マスずつ描画情報を追加します。
そして最後にステージ情報のデータを書き換えています。

こんな感じのマップができます。
3.png

川の蛇行

上のコードだと川が直線で味気がないので、川を蛇行させます。
上のコードでは川を一マスずつ下りながら描画をしていましたが、川が蛇行している様子は数マスをかけて描くことになるので、現在描画しているのが蛇行の何マス目であるかを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
    }
}


出来上がりはこんな感じ。
4.png

森の生成

これまで作成したマップに、散発的に森を植えていきます。森の大きさは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マスの森を作成します。
作成する森のデータがすべてそろったら、マップのタイルのデータにそれを反映させます。
すると以下の画像のようなものが出来上がります。

1.png

最後に

今後はより多くの構造体(洞窟、城等)を配置すること、プレーヤーを追加してプレーヤーが土地改造(畑の作成など)を行えるようにすることなどに、注力したいです。

9
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
7