Help us understand the problem. What is going on with this article?

Go言語で作るマリオ風2Dゲーム

概要

Go言語で2Dのゲームアプリの作り方を調べたので、簡単なゲームをサンプルとして作りました。
こちらにソースコード一式があります。

作成したもの

以下の画像のように、マリオのようなゲームを作りました。
output.gif

緑の玉がプレーヤーで、青いお化けが敵です。
ステージ上にランダムに配置された落とし穴に落ちたり、ふわふわ動くお化けに当たったりしたら、ゲームオーバーです。

ゲームのステージは2種類あり、上のものの他にも、雪のステージもあります。
snow.gif

使用するライブラリについて

engoというライブラリを用いることで、クロスプラットフォームなデスクトップゲームアプリができます。
このライブラリを使用する上で必要となる基本的な概念を、以下で説明します。

ライブラリの基本的な概念

Entityとは

スクリーンに描画をされて、毎フレームごとに移動や当たり判定などの何らかの処理を行いたいものがある場合は、それらをEntityとして宣言をする必要があります。

私が作成したゲームだと、緑のプレーヤー青いお化けの敵、そして地面や草や木の3種類のエンティティをEntityとして登録しています。

Entityとして登録するには、以下のフィールドを保持する構造体を作ります。

type Sample struct {
    ecs.BasicEntity
    common.RenderComponent
    common.SpaceComponent
}

RenderComponentではEntityの見た目に関する情報を、SpaceComponentでは位置に関する情報を保持します。

Systemとは

上で説明したEntityを、Systemに登録をすることで、画面上に描画処理をしたり毎フレームごとになんらかの処理を行ったりできるようになります。

Systemを宣言するには、以下のフィールドを保持する構造体を作ります。

type SampleSystem struct {
    texture *common.Texture
    sampleEntity *Sample
    world *ecs.World
}

textureは見た目を定義するものであり、sampleEntityは上で説明したEntityを保持するものです。

そして、作成した構造体に以下の3つのメソッドを持たせます。

func (*SampleSystem) New(w *ecs.World){}
func (*SampleSystem) Remove(ecs.BasicEntity) {}
func (*SampleSystem) Update(dt float32) {}

New()Systemが作成された時に、Remove()は削除された時に、Update()は毎フレームに、それぞれ呼び出されるので、必要な処理を中に記述します。

通常New()では見た目の設定など初期設定を、Update()では移動や当たり判定などの処理を、それぞれ行います。

ゲームの作成

詳細なソースコードはGitHubにありますが、ここでは一部をかいつまんで説明します。

背景の作成

地面を描画します。
素材はここからとってきます。
tilesheet_grass.png
tilesheet_snow.png

この素材の一部をタイルのように画面に張り付けていきます。まずはEntitySystemの宣言です。

tileSystem.go
// Entity
type Tile struct {
    ecs.BasicEntity
    common.RenderComponent
    common.SpaceComponent
}

// System
type TileSystem struct {
    world *ecs.World
    // x軸座標
    positionX int
    // y軸座標
    positionY int
    tileEntity []*Tile
    texture *common.Texture
}

続いて、New()関数でこれらを描画をしていきます。

クリックしてコードを展開
tileSystems.go
func (ts *TileSystem) New(w *ecs.World){
    rand.Seed(time.Now().UnixNano())

    ts.world = w
    // 落とし穴作成中の状態を保持(0 => 作成していない、1以上 => 作成中)
    tileMakingState := 0
    // 雲の作成中の状態を保持 (0の場合:作成していない、奇数の場合:{(x+1)/2}番目の雲の前半を作成中、偶数の場合:{x/2}番目の雲の後半を作成中)
    cloudMakingState := 0
    // 雲の高さを保持
    cloudHeight := 0
    // タイルの作成
    tmp := rand.Intn(2)
    var loadTxt string
    // ランダムにステージを選ぶ
    if tmp == 0 {
        loadTxt = "tilemap/tilesheet_grass.png"
    } else {
        loadTxt = "tilemap/tilesheet_snow.png"
    }
    Spritesheet = common.NewSpritesheetWithBorderFromFile(loadTxt, 16, 16, 0, 0)
    Tiles := make([]*Tile, 0)
    for j := 0; j < 2800; j++ {
        // 地表の作成
        if (j > 10){
            if (tileMakingState > 1 && tileMakingState < 4){
                for t:= 0; t < 8; t++ {
                    FallPoint = append(FallPoint,j * 16 - t)
                }
            } else if (tileMakingState == 0){
                // すでに作成中でない場合、たまに落とし穴を作る
                randomNum := rand.Intn(10)
                if (randomNum == 0) {
                    FallStartPoint = append(FallStartPoint,j * 16)
                    tileMakingState = 1
                }
            }
        }
        // 描画するタイルを保持
        var selectedTile int
        // 描画するタイルを選択
        switch tileMakingState {
            case 0: selectedTile = 1
            case 1: selectedTile = 2
            case 2: tileMakingState += 1; continue
            case 3: tileMakingState += 1; continue
            case 4: selectedTile = 0
        }
        // タイルEntityの作成
        tile := &Tile{BasicEntity: ecs.NewBasic()}
        // 位置情報の設定
        tile.SpaceComponent.Position = engo.Point{
            X: float32(j * 16),
            Y: float32(237),
        }
        // 見た目の設定
        tile.RenderComponent.Drawable = Spritesheet.Cell(selectedTile)
        tile.RenderComponent.SetZIndex(0)
        Tiles = append(Tiles, tile)

        if (tileMakingState > 0){
            if (tileMakingState == 4){
                tileMakingState = 0
                continue
            }
            tileMakingState += 1
        }
    }
    for j := 0; j < 2800; j++ {
        // 雲の作成
        if (cloudMakingState == 0){
            randomNum := rand.Intn(6)
            if (randomNum < 7 && randomNum % 2 == 1) {
                cloudMakingState = randomNum
            }
            cloudHeight = rand.Intn(70) + 10
        }
        if (cloudMakingState != 0){
            // 雲Entityの作成
            cloudTile := cloudMakingState + 9
            cloud := &Tile{BasicEntity: ecs.NewBasic()}
            cloud.SpaceComponent.Position = engo.Point{
                X: float32(j * 16),
                Y: float32(cloudHeight),
            }
            cloud.RenderComponent.Drawable = Spritesheet.Cell(cloudTile)
            cloud.RenderComponent.SetZIndex(0)
            Tiles = append(Tiles, cloud)
            // 前半を作成中であれば、次は後半を作成する
            if (cloudMakingState % 2 == 1){
                cloudMakingState += 1
            } else {
                cloudMakingState = 0
            }
        }
        //草の作成
        if (!utils.Contains(FallPoint,j * 16)){
            // 落とし穴の上には作らない
            var grassTile int
            randomNum := rand.Intn(18)
            if (randomNum  < 6) {
                grassTile = 26 + randomNum
                grass := &Tile{BasicEntity: ecs.NewBasic()}
                grass.SpaceComponent.Position = engo.Point{
                    X: float32(j * 16),
                    Y: float32(221),
                }
                grass.RenderComponent.Drawable = Spritesheet.Cell(grassTile)
                grass.RenderComponent.SetZIndex(1)
                Tiles = append(Tiles, grass)

            }
        }

    }
    // 地面の描画
    for i := 0; i < 3; i++ {
        tileMakingState = 0
        for j := 0; j < 2800; j++ {
            if (tileMakingState == 0){
                // 落とし穴を作る場合
                if (utils.Contains(FallStartPoint,j * 16)){
                    tileMakingState = 1
                }
            }
            // 描画するタイルを保持
            var selectedTile int
            // 描画するタイルを選択
            switch tileMakingState {
                case 0: selectedTile = 17
                case 1: selectedTile = 18
                case 2: tileMakingState += 1; continue
                case 3: tileMakingState += 1; continue
                case 4: selectedTile = 16
            }
            tile := &Tile{BasicEntity: ecs.NewBasic()}
            tile.SpaceComponent.Position = engo.Point{
                X: float32(j * 16),
                Y: float32(285 - i * 16),
            }
            tile.RenderComponent.Drawable = Spritesheet.Cell(selectedTile)
            tile.RenderComponent.SetZIndex(0)
            Tiles = append(Tiles, tile)

            if (tileMakingState > 0){
                if (tileMakingState == 4){
                    tileMakingState = 0
                    continue
                }
                tileMakingState += 1
            }
        }
    }
    tileMakingState = 0
    for _, system := range ts.world.Systems() {
        switch sys := system.(type) {
        case *common.RenderSystem:
            for _, v := range Tiles {
                ts.tileEntity = append(ts.tileEntity, v)
                sys.Add(&v.BasicEntity, &v.RenderComponent, &v.SpaceComponent)
            }
        }
    }
}

乱数を発生させて、ランダムで落とし穴や草、雲を作成しています。

敵の作成

敵のお化けを作ります。
お化けの画像はこちらからとってきました。
pipo-halloweenchara2016_19.png

まずはEntityとSystemを宣言します。

enemySystem.go
type Enemy struct {
    ecs.BasicEntity
    common.RenderComponent
    common.SpaceComponent
    // ジャンプの状態(0 => 着地中, 1 => 1ジャンプ中, 2 => 降下中)
    jumpState int
    // ジャンプの残り時間
    jumpDuration int
    // 移動の速度(0 ~ 2, 数値が高いほど早い)
    velocity int
    // 画面から消えているか
    ifDissappearing bool
}

type EnemySystem struct {
    world *ecs.World
    enemyEntity []*Enemy
    texture *common.Texture
}

続いて、New()関数で描画と配置を行います。

クリックしてコードを展開
enemySystem.go
func (es *EnemySystem) New(w *ecs.World){
    es.world = w
    Enemies := make([]*Enemy, 0)
    // ランダムで配置
    for i := 0; i < 44800; i++ {
        randomNum := rand.Intn(400)
        if (randomNum == 0){
            // 敵の作成
            enemy := Enemy{BasicEntity: ecs.NewBasic()}
            enemy.SpaceComponent = common.SpaceComponent{
                Position: engo.Point{X:float32(i),Y:float32(212)},
                Width: 30,
                Height: 30,
            }
            // 画像の読み込み
            texture, err := common.LoadedSprite("pics/ghost.png")
            if err != nil {
                fmt.Println("Unable to load texture: " + err.Error())
            }
            enemy.RenderComponent = common.RenderComponent{
                Drawable: texture,
                Scale: engo.Point{X:1.1, Y:1.1},
            }
            enemy.RenderComponent.SetZIndex(1)
            es.texture = texture
            for _, system := range es.world.Systems() {
                switch sys := system.(type) {
                case *common.RenderSystem:
                    sys.Add(&enemy.BasicEntity, &enemy.RenderComponent, &enemy.SpaceComponent)
                }
            }
            enemy.velocity = rand.Intn(3)
            Enemies = append(Enemies,&enemy)
        }
        es.enemyEntity = Enemies
    }
}

乱数を発生させて、ステージ上のランダムな位置にお化けを発生させます。

そしてUpdate()関数で、作成されたお化けを移動させます。

クリックしてコードを展開
enemySystem.go
func (es *EnemySystem) Update(dt float32) {
    // カメラとプレーヤーの位置を取得
    var cameraPosition float32
    var playerPositionX float32
    for _, system := range es.world.Systems() {
        switch sys := system.(type) {
        case *common.CameraSystem:
            cameraPosition = sys.X()
        case *PlayerSystem:
            playerPositionX = sys.playerEntity.SpaceComponent.Position.X
        }
    }
    for _, o := range es.enemyEntity{
        // 画面に描画されていないオブジェクトは移動処理をしない
        if (o.SpaceComponent.Position.X > cameraPosition - 240 && o.SpaceComponent.Position.X < cameraPosition + 200 && !o.ifDissappearing){
            // プレーヤーとの当たり判定
            if (o.SpaceComponent.Position.X == playerPositionX) {
                for _, system := range es.world.Systems() {
                    switch sys := system.(type) {
                    case *PlayerSystem:
                        sys.playerEntity.damage += 1
                    }
                }
            }
            o.SpaceComponent.Position.X -= float32(o.velocity + 1)
            // ジャンプをしていない場合
            if (o.jumpState == 0){
                o.jumpState = rand.Intn(2) + 1
                jumpTemp := rand.Intn(3)
                switch (jumpTemp) {
                    case 0: o.jumpDuration = 15
                    case 1: o.jumpDuration = 25
                    case 2: o.jumpDuration = 35
                }
            }
            // ジャンプ処理
            if (o.jumpState == 1){
                // ジャンプをし終わっていない場合
                if (o.jumpDuration > 0){
                    o.SpaceComponent.Position.Y -= 3
                    o.jumpDuration -= 1
                } else {
                    // ジャンプをし終わった場合
                    o.jumpState = 2
                }
            } else {
                // 降下をし終わっていない場合
                if (o.SpaceComponent.Position.Y < 212){
                    o.SpaceComponent.Position.Y += 3
                } else {
                    // 降下し終わった場合
                    o.jumpState = 0
                }
            }
        }else if (o.ifDissappearing){
            o.SpaceComponent.Position.Y += 3
        }
    }
}

ランダムな高さのジャンプを繰り返しながら、ランダムな速度で移動をさせます。

上にCameraSystemと出てきますが、これはゲーム内の視点を動かすために、ライブラリで最初から用意されているSystemです。

プレーヤーの作成

プレーヤーのEntitySystemを宣言します。

playerSystem.go
type Player struct {
    ecs.BasicEntity
    common.RenderComponent
    common.SpaceComponent
    // ジャンプの時間
    jumpDuration int
    // カメラの進んだ距離
    distance int
    // 落ちているかどうか
    ifFalling bool
    // ダメージ
    damage int
}

type PlayerSystem struct {
    world *ecs.World
    playerEntity *Player
    texture *common.Texture
}

New()関数で描画をします。

クリックしてコードを展開
playerSystem.go
func (ps *PlayerSystem) New(w *ecs.World){
    ps.world = w
    // プレーヤーの作成
    player := Player{BasicEntity: ecs.NewBasic()}

    // 初期の配置
    positionX := int(engo.WindowWidth() / 2)
    positionY := int(engo.WindowHeight() - 88)
    player.SpaceComponent = common.SpaceComponent{
        Position: engo.Point{X:float32(positionX),Y:float32(positionY)},
        Width: 30,
        Height: 30,
    }
    // 画像の読み込み
    texture, err := common.LoadedSprite("pics/greenoctocat.png")
    if err != nil {
        fmt.Println("Unable to load texture: " + err.Error())
    }
    player.RenderComponent = common.RenderComponent{
        Drawable: texture,
        Scale: engo.Point{X:0.1, Y:0.1},
    }
    player.RenderComponent.SetZIndex(1)
    ps.playerEntity = &player
    ps.texture = texture
    for _, system := range ps.world.Systems() {
        switch sys := system.(type) {
        case *common.RenderSystem:
            sys.Add(&player.BasicEntity, &player.RenderComponent, &player.SpaceComponent)
        }
    }
    common.CameraBounds = engo.AABB{
        Min: engo.Point{X: 0, Y: 0},
        Max: engo.Point{X: 40000, Y: 300},
    }
}

Update()関数で、移動をします。

クリックしてコードを展開
playerSystem.go
func (ps *PlayerSystem) Update(dt float32) {
    // ダメージが1であればゲームを終了
    if ps.playerEntity.damage > 0 {
        whenDied(ps)
    }
    // 落とし穴
    if (ps.playerEntity.jumpDuration == 0 && utils.Contains(FallPoint,int(ps.playerEntity.SpaceComponent.Position.X)) ){
        ps.playerEntity.ifFalling = true
        ps.playerEntity.SpaceComponent.Position.Y += 5
    }
    // 穴に落ち切ったらライフを0にする
    if ps.playerEntity.SpaceComponent.Position.Y > 300 {
        ps.playerEntity.damage += 1
    }

    if(!ps.playerEntity.ifFalling){
    // 右移動
    if engo.Input.Button("MoveRight").Down()  { 
        // 画面の真ん中より左に位置していれば、カメラを移動せずプレーヤーを移動する
        if (int(ps.playerEntity.SpaceComponent.Position.X) < ps.playerEntity.distance + int(engo.WindowWidth()) / 2){
            ps.playerEntity.SpaceComponent.Position.X += 5
        } else {
            // 画面の右端に達していなければプレーヤーを移動する
            if (int(ps.playerEntity.SpaceComponent.Position.X) < ps.playerEntity.distance + int(engo.WindowWidth()) - 10){
                ps.playerEntity.SpaceComponent.Position.X += 5
            }
            // カメラを移動する
            engo.Mailbox.Dispatch(common.CameraMessage{
                Axis:        common.XAxis,
                Value:       5,
                Incremental: true,
            })
            ps.playerEntity.distance += 5
        }
    }
    // プレーヤーを左に移動
    if engo.Input.Button("MoveLeft").Down()  {
        if int(ps.playerEntity.SpaceComponent.Position.X) > ps.playerEntity.distance + 10{
            ps.playerEntity.SpaceComponent.Position.X -= 5
        }
    }
    // プレーヤーをジャンプ
    if engo.Input.Button("Jump").JustPressed() {
        if ps.playerEntity.jumpDuration == 0 {
            ps.playerEntity.jumpDuration = 1
        }
    }
    if ps.playerEntity.jumpDuration != 0 {
        ps.playerEntity.jumpDuration += 1
        if ps.playerEntity.jumpDuration < 14 {
            ps.playerEntity.SpaceComponent.Position.Y -= 5
        } else if ps.playerEntity.jumpDuration < 26 {
            ps.playerEntity.SpaceComponent.Position.Y += 5
        } else {
            ps.playerEntity.jumpDuration = 0
        }
    }
    }
}

移動をするだけでなく、落とし穴に落ちたらゲームオーバーにする、などの処理も行なっています。

ゲームの開始

上で作成したSystemなどを用いて、ゲームを動かします。

ゲームプログラムのメインの部分は、以下のようになります。

game.go
package main

import (
    "bytes"
    "engo.io/engo"
    "engo.io/engo/common"
    "engo.io/ecs"
    "image/color"
    "golang.org/x/image/font/gofont/gosmallcaps"
    "./systems"
)

type myScene struct {}

func (*myScene) Type() string { return "myGame" }

func (*myScene) Preload() {
    // 必要なファイルを事前に読み込んでおく
    engo.Files.Load("pics/greenoctocat.png", "pics/ghost.png", "tilemap/tilesheet_grass.png", "tilemap/tilesheet_snow.png")
    engo.Files.LoadReaderData("go.ttf", bytes.NewReader(gosmallcaps.TTF))
    common.SetBackground(color.RGBA{255, 250, 220, 0})
}

func (*myScene) Setup(u engo.Updater){
    engo.Input.RegisterButton("MoveRight", engo.KeyD, engo.KeyArrowRight)
    engo.Input.RegisterButton("MoveLeft", engo.KeyA, engo.KeyArrowLeft)
    engo.Input.RegisterButton("Jump", engo.KeySpace)
    world, _ := u.(*ecs.World)
    // Systemの追加
    world.AddSystem(&common.RenderSystem{})
    world.AddSystem(&systems.TileSystem{})
    world.AddSystem(&systems.PlayerSystem{})
    world.AddSystem(&systems.EnemySystem{})
}

func (*myScene) Exit() {
    engo.Exit()
}

func main(){
    opts := engo.RunOptions{
        Title:"myGame",
        Width:400,
        Height:300,
        StandardInputs: true,
        NotResizable:true,
    }
    engo.Run(opts,&myScene{})
}

追記

続編書きました

KMim
文系出身社会人3年目です。
https://github.com/KMimura
fnlp
金融データ処理や自然言語処理に興味のあるメンバーがあつまって情報交換するコミュニティです
https://github.com/fnlp-group
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした