こんにちは!今回はGo言語を使ったゲーム作成の第3回です。第3回では、敵キャラ、文字表示、リトライ機能を追加していきます。
まだ第1回、第2回を読んでいない人は、先に読むことをおすすめします!
-
-
参考【第1回】Go言語(ゲームエンジン:engo)でゲームを作成してみた
こんにちは!近年、多くのプログラミング言語の中で「稼げる言語」として人気が爆発しているのは、Googleが開発したGo言語です。本記事では、2Dゲームエンジンのengoを使用してゲームを作成していきます。プログラミング初心者の方でも分かりやすいように、ソースコードの説明もしっかり行いたいと思います。
続きを見る
-
-
参考【第2回】Go言語(engo)でゲーム作成してみた【実装〜完成まで】
こんにちは!近年、多くのプログラミング言語の中で「稼げる言語」として人気が爆発しているのは、Googleが開発したGo言語です。そんなGo言語を使って、ゲームを作成してみた記事の第2回です。プログラミング初心者の方でも分かりやすいように、ソースコードの説明もしっかり行いたいと思います。
続きを見る
それではいきましょう!
ディレクトリ構成
今回は、新しく「enermySystem」と「hudTextSystem」を追加します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
$GOPATH$ |----bin |----pkg |----src | |----github.com | | |----myfuns | | | |----engo | | | | |----main.go | | | | | | | | | |----assets | | | | | |----Mario | | | | | | |----Characters | | | | | | | |----Mario.png | | | | | | | |----Enemies.png (NEW) | | | | | | | | | | | | | |----Tilesets | | | | | | | |----OverWorld.png | | | | | | | | | |----systems | | | | | |----playerSystem.go | | | | | |----tileSystem.go | | | | | |----enermySystem.go (NEW) | | | | | |----hudTextSystem.go (NEW) |
敵キャラの作成
パックンフラワーの作成
敵キャラは、スーパーマリオブラザーズでお馴染みのパックンフラワーを作成します。
グローバル変数、Entity、System
まずは、グローバル変数、Entity、Systemなど必要な変数を定義します。また、リソースの都合上、画像サイズと実物のサイズがイコールではないので、縦横それぞれ余分サイズを定義します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
package systems import ( "github.com/EngoEngine/ecs" "github.com/EngoEngine/engo" "github.com/EngoEngine/engo/common" ) const ( // EneymyType0 : パックンフラワー EneymyType0 = 0 // Type0Count : Type0Count = 128 // ExtraSizeXType0 : 余分サイズ ExtraSizeXType0 = 6 // ExtraSizeYType0 : 余分サイズ ExtraSizeYType0 = 8 ) var enermyFile = "./Mario/Characters/Enemies.png" // ifTouched : プレイヤーと触れたか var ifTouched bool // EnetmyPositionType0 : 敵キャラの位置 var EnetmyPositionType0 []int // Enermy is struct for the EnermySystem type Enermy struct { ecs.BasicEntity common.RenderComponent common.SpaceComponent count int enermyType int } // EnermySystem creates enemies that disturb the player. type EnermySystem struct { world *ecs.World enermyEntity []*Enermy } |
New()
パックンフラワーは土管上に作成します。パックンフラワーに触れるとゲームオーバーになるように、作成時に位置を記録します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
func (es *EnermySystem) New(w *ecs.World) { // Worldの追加 es.world = w ifTouched = false ifGameOver = false // Enermy配列作成 Enemies := make([]*Enermy, 0) // スプライトシートの作成 Spritesheet32x32 := common.NewSpritesheetWithBorderFromFile(enermyFile, CellWidth32, CellHeight32, 0, 0) //randomNum := rand.Intn(10) for i := 0; i <= TileNum; i++ { if getMakingInfo(PipePoint, i*CellWidth16) { enermy := &Enermy{BasicEntity: ecs.NewBasic()} // SpaceComponent enermy.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: float32(i * CellWidth16), Y: pipePositionY}, } // RenderComponent enermy.RenderComponent = common.RenderComponent{ Drawable: Spritesheet32x32.Cell(7), Scale: engo.Point{X: 1, Y: 1}, } enermy.RenderComponent.SetZIndex(6) // 初期化 enermy.count = 0 // コンポーネントセット Enemies = append(Enemies, enermy) // 敵キャラの位置記録 for j := 0; j < CellWidth32; j++ { if j > ExtraSizeXType0 && j < CellWidth32-ExtraSizeXType0 { EnetmyPositionType0 = append(EnetmyPositionType0, i*CellWidth16+j) } } i++ } } // RenderSystemに追加 for _, system := range es.world.Systems() { switch sys := system.(type) { case *common.RenderSystem: for _, v := range Enemies { es.enermyEntity = append(es.enermyEntity, v) sys.Add(&v.BasicEntity, &v.RenderComponent, &v.SpaceComponent) } } } } |
Update()
パックンフラワーは土管から出てきたり、引っ込んだりを周期的に繰り返します。プレイヤーの左右の足がパックンフラワーに触れたらゲームオーバーです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
func (es *EnermySystem) Update(dt float32) { var playerLeftPositionX float32 var playerRightPositionX float32 var playerBottomPositionY float32 for _, system := range es.world.Systems() { switch sys := system.(type) { case *PlayerSystem: if ifTouched { if ifGameOver { return } ifTouched = false sys.PlayerDie() ifGameOver = true } playerLeftPositionX = sys.playerEntity.LeftPositionX playerRightPositionX = sys.playerEntity.RightPositionX playerBottomPositionY = sys.playerEntity.SpaceComponent.Position.Y + float32(CellHeight32) } } if ifGameOver { return } for _, entity := range es.enermyEntity { if getMakingInfo(EnetmyPositionType0, int(playerLeftPositionX)) || getMakingInfo(EnetmyPositionType0, int(playerRightPositionX)) { if pipePositionY >= playerBottomPositionY && entity.SpaceComponent.Position.Y+ExtraSizeYType0 < playerBottomPositionY { ifTouched = true } } if entity.enermyType == EneymyType0 { if entity.count < Type0Count { entity.SpaceComponent.Position.Y = pipePositionY - float32(entity.count/4) } else if entity.count < Type0Count*2 { // 一時静止 } else if entity.count < Type0Count*3 { entity.SpaceComponent.Position.Y = pipePositionY - CellHeight32 + float32((entity.count-Type0Count*2)/4) } else { entity.count = 0 } entity.count++ } } } |
Update()は毎フレームで呼び出されます。そこで、フレーム毎にカウントをインクリメントすることで、パックンフラワーの動きを制御します。最後にカウントを0にリセットします。
PlayerSystemの編集
プレイヤーとパックンフラワーが触れた時は、「GameOver」の表示と、PlayerSystemの削除を行います。PlayerSystemに新しく「PlayerDie()」を設けます。
1 2 3 4 5 6 7 8 9 10 |
func (ps *PlayerSystem) PlayerDie() { ifGameOver = true for _, system := range ps.world.Systems() { switch sys := system.(type) { case *HUDTextSystem: sys.TextInit(sys.TextEntity, TextEND) } } ps.Remove(ps.playerEntity.BasicEntity) } |
落とし穴に落ちた場合も同じくPlayerDie()を呼び出すように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
func (ps *PlayerSystem) Update(dt float32) { // 死亡したらリターン if ifGameOver { return } // Goal地点に達したら右移動はしない if int(ps.playerEntity.LeftPositionX) >= (TileNum-GoalTileNum+2)*CellWidth16 { for _, system := range ps.world.Systems() { switch sys := system.(type) { case *HUDTextSystem: sys.TextInit(sys.TextEntity, TextGOAL) } } ps.Remove(ps.playerEntity.BasicEntity) } /*省略*/ } |
「ifGameOver」はグローバル変数で定義します。
1 |
var ifGameOver bool |
Worldに追加
EnermySystemをWorldに追加して完了です。
1 2 3 |
func (*myScene) Setup(u engo.Updater) { world.AddSystem(&systems.EnermySystem{}) } |
テキストの作成
次に画面に表示するテキストを作成します。まずはキーボード同様、main.goのSetup()で、事前にフォントをロードします。使用フォントはGo言語独自のフォントです。
フォント読込
1 2 3 4 |
func (*myScene) Setup(u engo.Updater) { // フォント読込 engo.Files.LoadReaderData("go.ttf", bytes.NewReader(gosmallcaps.TTF)) } |
グローバル変数、Entity、System
同じく定義していきます。今回は以下の3つのテキストを表示します。
・GAME START!
・GOAL!!
・GAME OVER
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
package systems import ( "image/color" "github.com/EngoEngine/ecs" "github.com/EngoEngine/engo" "github.com/EngoEngine/engo/common" ) const ( // TextNONE : テキストなし TextNONE = 0 // TextTITLE : タイトル TextTITLE = 1 // TextGOAL : ゴール TextGOAL = 2 // TextEND : 終了 TextEND = 3 ) // Text is an entity containing text printed to the screen type Text struct { ecs.BasicEntity common.SpaceComponent common.RenderComponent textNo int ifMaking bool } // HUDTextSystem prints the text to our HUD based on the current state of the game type HUDTextSystem struct { world *ecs.World TextEntity *Text } |
New()
表示テキストは、場面によって文言を変化させたいので、New()には最低限の処理のみ記述します。ゲーム開始時は「GAME START!」を表示します。
1 2 3 4 5 6 7 |
func (h *HUDTextSystem) New(w *ecs.World) { h.world = w // Entitiy作成 text := &Text{BasicEntity: ecs.NewBasic()} // 初期化 h.TextInit(text, TextTITLE) } |
TextInit()
New()はSystemをSceneに追加した時に呼び出される関数です。いつでも初期化を実行できるように、代わりにTextInit()を設けます。さらに、テキストNoで表示テキストが変更されるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
func (h *HUDTextSystem) TextInit(text *Text, textNo int) { // 初期化 text.textNo = textNo text.ifMaking = true // SpaceComponent TextPositionX := (float32)(0) TextPositionY := engo.WindowHeight() - 220 size := float64(40) // RenderComponent textDisplay := "" // テキストNo毎の処理 switch textNo { case TextTITLE: textDisplay = " GAME START!" case TextGOAL: textDisplay = " GOAL!!" case TextEND: textDisplay = " GAME OVER" } // SpaceComponent text.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: TextPositionX, Y: TextPositionY}, } // RenderComponent fnt := &common.Font{ URL: "go.ttf", FG: color.White, Size: size, } fnt.CreatePreloaded() text.RenderComponent.Drawable = common.Text{ Font: fnt, Text: textDisplay, } text.SetShader(common.TextHUDShader) text.RenderComponent.SetZIndex(10) // Entitiy追加 h.TextEntity = text // SystemにEntity追加 for _, system := range h.world.Systems() { switch sys := system.(type) { case *common.RenderSystem: sys.Add(&text.BasicEntity, &text.RenderComponent, &text.SpaceComponent) } } } |
フォントは、事前にロードした「go.ttf」をURLに指定して、font.CreatePreload()を実行します。最後にロードしたフォントをRenderComponentに設定して完了です。
Worldに追加
同じくWorldにSystemを追加して完了です。
1 2 3 |
func (*myScene) Setup(u engo.Updater) { world.AddSystem(&systems.HUDTextSystem{}) } |
リトライ機能の作成
ゲームオーバーになった時と、ゴールした時にリトライできる機能を追加していきます。
playerSystemの編集
リトライ時はEntityの値を初期化したいので、hudTextSytemと同じようにPlayerInit()を設けます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
func (ps *PlayerSystem) PlayerInit(player *Player) { // XY初期値 PsPositionX := float32(0) PsPositionY := engo.WindowHeight() - CellHeight16*6 // SpaceComponent player.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: PsPositionX, Y: PsPositionY}, Width: 30, Height: 30, } // スプライトシート player.spritesheet = common.NewSpritesheetWithBorderFromFile(playerFile, 32, 32, 0, 0) // RenderComponent player.RenderComponent = common.RenderComponent{ Drawable: player.spritesheet.Cell(PlayerSpriteSheetCell), Scale: engo.Point{X: 1, Y: 1}, } player.RenderComponent.SetZIndex(5) // コンポーネントセット ps.playerEntity = player // 初期化 ps.playerEntity.playerPositionY = PsPositionY ps.playerEntity.LeftPositionX = PsPositionX + float32(ExtraSizeX) ps.playerEntity.RightPositionX = PsPositionX + CellWidth32 - float32(ExtraSizeX) ps.playerEntity.ifFalling = false ps.playerEntity.ifOnPipe = false ps.playerEntity.cameraMoveDistance = 0 ps.playerEntity.topCount = 1 + MaxCount/2 ps.playerEntity.bottomCount = 0 ps.playerEntity.ifStart = false ifGameOver = false // RenderSystemに追加 for _, system := range ps.world.Systems() { switch sys := system.(type) { case *common.RenderSystem: sys.Add(&player.BasicEntity, &player.RenderComponent, &player.SpaceComponent) } } // カメラを移動する engo.Mailbox.Dispatch(common.CameraMessage{ Axis: common.XAxis, Value: engo.WindowWidth() / 2, Incremental: false, }) } |
最後に、カメラ移動の設定を行います。これで、リセット時にカメラの位置が初期位置に戻ります。
New()
New()は以下のように修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func (ps *PlayerSystem) New(w *ecs.World) { // Worldの追加 ps.world = w // Entity生成 player := Player{BasicEntity: ecs.NewBasic()} ps.PlayerInit(&player) // カメラ設定 common.CameraBounds = engo.AABB{ Min: engo.Point{X: 0, Y: 0}, Max: engo.Point{X: 3200, Y: 300}, } } |
hudTextSystemの編集
リトライは「GameOver」または「GOAL!!」が表示されている時に、「Enter」クリックで実行されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
func (h *HUDTextSystem) Update(dt float32) { if engo.Input.Button("Enter").Down() { switch h.TextEntity.textNo { case TextTITLE: for _, system := range h.world.Systems() { switch sys := system.(type) { case *PlayerSystem: sys.playerEntity.ifStart = true } } h.Remove(h.TextEntity.BasicEntity) h.TextEntity.textNo = TextNONE ifGameOver = false case TextGOAL, TextEND: for _, system := range h.world.Systems() { switch sys := system.(type) { case *PlayerSystem: // リトライ sys.PlayerInit(sys.playerEntity) } } h.TextInit(h.TextEntity, TextTITLE) } } } |
スタート時はEnterクリック
また、ゲーム起動時も「Enter」クリックで開始されるようにします。
1 2 3 4 5 |
type Player struct { /*省略*/ // スタートしたか ifStart bool } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func (ps *PlayerSystem) Update(dt float32) { // 死亡したらリターン if ifGameOver { return } // スタートしていなければリターン if !ps.playerEntity.ifStart { return } /*省略*/ } |
完成
これで完成です!段々と一般的なゲームに近づきつつあるので、今後も色々と機能を追加できればなーと思います。
最後まで読んで頂きありがとうございました!
ではまた!