こんにちは!今回はGo言語を使ったゲーム作成の第2回です。
まだ第1回の記事を読んでいない人は、先に読むことをおすすめします!
目次
背景の作成
まずは背景を作成していきます。engoの公式ページによると、Systemは以下の3つの機能を実装しています。
3つの機能
・Update
・Remove
・New
それぞれのテンプレートはこちらです。
1 2 3 4 5 6 7 8 9 10 11 |
// Update is ran every frame, with `dt` being the time // in seconds since the last frame func (*TileSystem) Update(dt float32) {} // Remove is called whenever an Entity is removed from the World, in order to remove it from this sytem as well func (*TileSystem) Remove(ecs.BasicEntity) {} // New is the initialisation of the System func (*TileSystem) New(*ecs.World) { fmt.Println("TileSystem was added to the Scene") } |
Update
Updateは毎フレームで呼び出されます。キャラクターの移動やジャンプをする場合は、Update()の中に処理を記述します。
Remove
Remove()はWorldからEntityを削除する時に呼び出されます。
New
NewはSystemがSceneに追加されたときに呼び出されます。今回作成する背景は特に動気がないので、New()だけソースを記述していきます。
次に、EntityとSystemを作成します。Entityは以下の3要素をメンバーに持ちます。
Entityのメンバー
・BasicEntity
・SpaceComponent
・RenderComponent
また、Systemは以下の2要素をメンバーに持ちます。
Systemのメンバー
・Entity
・World
それぞれを構造体で定義するとこうなります。
1 2 3 4 5 6 7 8 9 10 11 12 |
// Tile is Eintity for the TileSystem type Tile struct { ecs.BasicEntity common.RenderComponent common.SpaceComponent } // TileSystem builds a game background type TileSystem struct { world *ecs.World tileEntity []*Tile } |
次に、必要な定数とグローバル変数を定義します。
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 55 56 57 58 59 |
const ( // TileNum : Tile数 TileNum = 200 // GoalTileNum : 最終地点からゴールまでのタイル数 GoalTileNum = 10 // AroundGoalTileNum : ゴール付近のタイル数 AroundGoalTileNum = 30 // MountTileNum : 山のTile数 MountTileNum = 5 // PipeTileNum : 土管のTile数 PipeTileNum = 1 // CellWidth16 : 1タイル基準幅(16) CellWidth16 = 16 // CellHeight16 : 1タイル基準高さ(16) CellHeight16 = 16 // CellWidth32 : 1タイル基準幅(32) CellWidth32 = 32 // CellHeight32 : 1タイル基準高さ(32) CellHeight32 = 32 // CellHeight64 : 1タイル基準高さ(64) CellHeight64 = 64 // TileDepth :深さ TileDepth = 4 // GroundSpriteSheetCell : スプライトシートで使用する地面のセル番号 GroundSpriteSheetCell = 0 // CloudSpriteSheetCell : スプライトシートで使用する雲のセル番号 CloudSpriteSheetCell = 6 // MountSpriteSheetCell : スプライトシートで使用する山のセル番号 MountSpriteSheetCell = 11 // PipeSpriteSheetCell : スプライトシートで使用する土管のセル番号 PipeSpriteSheetCell = 3 ) var tileFile = "./Mario/Tilesets/OverWorld.png" var castleFile = "./Mario/Tilesets/Castle.png" // FallPoint : 落とし穴の位置 var FallPoint []int // MountPoint : 山の位置 var MountPoint []int // PipePoint : 土管の位置 var PipePoint []int // makingxxxx:作成状態(0:作成中でない 1:作成開始 2:それ以外) var makingFall int var makingCloud int var makingMount int var makingPipe int // addCell:セル追加値 var addCell int // Y値 var mountPositionY float32 var pipePositionY float32 var onPipePositionY float32 var castleositionY float32 |
地面、落とし穴の作成
まずは、地面と落とし穴を作成します。地面は、リソースファイル(スプライトシート)を16px × 16pxで分割した1タイルを、深さ4、長さ200で等間隔で並べていきます。
落とし穴は、ランダムに地面を作成しないことで再現します。ランダム値は、math/randパッケージをimportすると使えるようになります。
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 55 56 57 58 59 60 61 62 63 64 |
import ( "math/rand" "github.com/EngoEngine/ecs" "github.com/EngoEngine/engo" "github.com/EngoEngine/engo/common" } func (ts *TileSystem) New(w *ecs.World) { // Worldの追加 ts.world = w // スプライトシートの作成 Spritesheet16x16 := common.NewSpritesheetWithBorderFromFile(tileFile, CellWidth16, CellHeight16, 0, 0)// 初期化 // 初期化 makingFall = 0 // Tile配列作成 Tiles := make([]*Tile, 0) for i := 0; i <= TileNum; i++ { // ----------------------- // // ------- 地面の作成 ------ // // ----------------------- // // Start付近とGoal付近に落とし穴は作らない if i >= 10 && i < TileNum-AroundGoalTileNum { randomNum := rand.Intn(10) if randomNum == 0 { makingFall = 1 } else { // 最低タイル2つ分は落とし穴を作成する if makingFall == 1 { makingFall = 2 } else { makingFall = 0 } } } if makingFall != 0 { for j := 0; j < CellWidth16; j++ { // 落とし穴の位置記録 FallPoint = append(FallPoint, i*CellWidth16+j) } } else { for j := 0; j < TileDepth; j++ { tile := &Tile{BasicEntity: ecs.NewBasic()} // SpaceComponent tile.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: float32(i * CellWidth16), Y: float32(int(engo.WindowHeight()) - (j+1)*CellHeight16)}, } // RenderComponent tile.RenderComponent = common.RenderComponent{ Drawable: Spritesheet16x16.Cell(GroundSpriteSheetCell), Scale: engo.Point{X: 1, Y: 1}, } tile.RenderComponent.SetZIndex(0) // コンポーネントセット Tiles = append(Tiles, tile) } } } } |
Start地点とGoal地点に落とし穴があると、ゲームとして成立しないので、条件で除外します。
落とし穴の位置は、プレイヤーの落下や、山・土管の作成に必要な情報になるので、全位置を記録します。また、落とし穴は最低2タイルとします。
雲の作成
地面の作成と同時に雲の作成を行います。
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 55 56 57 58 59 60 61 62 63 64 65 66 67 |
func (ts *TileSystem) New(w *ecs.World) { /*省略*/ // スプライトシートの作成 Spritesheet32x32 := common.NewSpritesheetWithBorderFromFile(tileFile, CellWidth32, CellHeight32, 0, 0) // 初期化 addCell = 0 cloudHeight := 0 for i := 0; i <= TileNum; i++ { // ----------------------- // // ------- 地面の作成 ------ // // ----------------------- // /*省略*/ // ----------------------- // // ------- 雲の作成 ------- // // ----------------------- // if makingCloud == 0 { randomNum := rand.Intn(12) if randomNum < 3 { makingCloud = 1 cloudHeight = randomNum } } if makingCloud != 0 { tile := &Tile{BasicEntity: ecs.NewBasic()} j := float32(0) // 2つ目の雲作成中の場合 if makingCloud > 2 { j = float32(i) - 0.5 } else { j = float32(i) } // SpaceComponent tile.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: float32(j * CellWidth32), Y: float32(int(engo.WindowHeight()/3) - cloudHeight*CellHeight16)}, } // RenderComponent tile.RenderComponent = common.RenderComponent{ Drawable: Spritesheet32x32.Cell(CloudSpriteSheetCell + addCell), Scale: engo.Point{X: 1, Y: 1}, } tile.RenderComponent.SetZIndex(float32(makingCloud)) // コンポーネントセット Tiles = append(Tiles, tile) switch makingCloud { case 1: makingCloud++ addCell = 1 break case 2: makingCloud++ addCell = 1 break default: makingCloud = 0 addCell = 0 break } } } } |
ただ雲を配置するだけだと何か物足りなかったので、今回は雲を2つ重ねて、大きな雲にしてみました。
また、リソースの性質上、雲画像はスプライトシートを32px × 32pxで分割します。NewSpritesheetWithBorderFromFile()の引数が変わっていることに注意してください。
山の作成
次に山を作成していきます。山は落とし穴の位置と被らないように、別途で作成します。また、この後に作成する土管と場所が被らないように、山の位置も記録します。
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 |
func (ts *TileSystem) New(w *ecs.World) { /*省略*/ // スプライトシートの作成 Spritesheet16x64 := common.NewSpritesheetWithBorderFromFile(tileFile, CellWidth16, CellHeight64, 0, 0) // 初期化 mountPositionY = engo.WindowHeight() - CellHeight16*7 for i := 0; i <= TileNum; i++ { // ----------------------- // // ------- 山の作成 ------- // // ----------------------- // makingMount = 1 for j := 0; j < MountTileNum+2; j++ { // 山を作成できる十分なスペースがない場合 if getMakingInfo(FallPoint, (i+j)*CellWidth16) { makingMount = 0 } } if makingMount != 0 && i < TileNum-AroundGoalTileNum { for j := 0; j < MountTileNum; j++ { tile := &Tile{BasicEntity: ecs.NewBasic()} // SpaceComponent tile.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: float32((i + j) * CellWidth16), Y: mountPositionY}, } // RenderComponent tile.RenderComponent = common.RenderComponent{ Drawable: Spritesheet16x64.Cell(MountSpriteSheetCell + j), Scale: engo.Point{X: 1, Y: 1}, } tile.RenderComponent.SetZIndex(0) // コンポーネントセット Tiles = append(Tiles, tile) // 山の位置記録 MountPoint = append(MountPoint, (i+j)*CellWidth16) } // ランダムな値をインクリメント i = i + 20 } } } |
山はリソースファイルの都合上、5タイル分の幅が必要なので、落とし穴が一定以上幅で存在しない場所に作成します。落とし穴かどうかの判断は、以下の関数で行います。
1 2 3 4 5 6 7 8 9 |
// getMakingInfo : 対象位置に含まれているか func getMakingInfo(s []int, e int) bool { for _, v := range s { if e == v { return true } } return false } |
土管の作成
山と同様に土管を作成します。落とし穴が山の位置に被らないように、ランダムに作成します。
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 |
// New is the initialisation of the System func (ts *TileSystem) New(w *ecs.World) { /*省略*/ // 初期化 pipePositionY = engo.WindowHeight() - CellHeight16*6 onPipePositionY = engo.WindowHeight() - CellHeight16*8 /*省略*/ for i := 0; i <= TileNum; i++ { // ------------------------ // // ------- 土管の作成 ------- // // ------------------------ // makingPipe = 1 for j := 0; j < PipeTileNum+2; j++ { // 土管を作成できる十分なスペースがない、もしくは山を作成している場合 if getMakingInfo(FallPoint, (i+j)*CellWidth16) || getMakingInfo(MountPoint, (i+j)*CellWidth16) { makingPipe = 0 } } // Start付近とGoal付近は土管は作らない if i >= 10 && i < TileNum-AroundGoalTileNum { if makingPipe != 0 { tile := &Tile{BasicEntity: ecs.NewBasic()} // SpaceComponent tile.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: float32(i * CellWidth16), Y: pipePositionY}, } // RenderComponent tile.RenderComponent = common.RenderComponent{ Drawable: Spritesheet32x32.Cell(PipeSpriteSheetCell), Scale: engo.Point{X: 1, Y: 1}, } tile.RenderComponent.SetZIndex(0) // コンポーネントセット Tiles = append(Tiles, tile) // 土管の位置記録 for j := 0; j < CellWidth32; j++ { PipePoint = append(PipePoint, i*CellWidth16+j) } // ランダムな値をインクリメント i = i + 30 } } } } |
プレイヤーがぶつかったりジャンプすると上に乗れるように、土管の位置も記録します。
ゴールの作成
ステージの最後にゴールを作成します。ゴールに使用するリソースは通常の画像ファイルなので、LoadedSprite()を使用して読み込みます。今回は、城をゴール代わりに使用します。
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 |
func (ts *TileSystem) New(w *ecs.World) { /*省略*/ // 初期化 castleositionY = engo.WindowHeight() - CellHeight16*9 // ----------------------- // // ------- 城の作成 ------- // // ----------------------- // tile := &Tile{BasicEntity: ecs.NewBasic()} // SpaceComponent tile.SpaceComponent = common.SpaceComponent{ Position: engo.Point{X: float32((TileNum - GoalTileNum) * CellWidth16), Y: castleositionY}, } // 画像の読み込み texture, err := common.LoadedSprite(castleFile) if err != nil { fmt.Println("Unable to load texture: " + castleFile + ":" + err.Error()) } // RenderComponent tile.RenderComponent = common.RenderComponent{ Drawable: texture, Scale: engo.Point{X: 1, Y: 1}, } tile.RenderComponent.SetZIndex(0) // コンポーネントセット Tiles = append(Tiles, tile) } |
最後に、作成したEntityの配列をRenderSystemに追加します。これで、背景の完成です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
func (ts *TileSystem) New(w *ecs.World) { /*省略*/ 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) } } } } |
プレイヤーの作成
次にプレイヤーを作成していきます。背景と同様に、まずは定数とグローバル変数、そしてEntityとPlayerSystemを定義します。
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 55 56 57 58 |
import ( "github.com/EngoEngine/ecs" "github.com/EngoEngine/engo" "github.com/EngoEngine/engo/common" ) const ( // MoveDistance : 移動距離 MoveDistance = 4 // JumpHeight : ジャンプの高さ JumpHeight = 4 // MaxCount : MaxCount = 40 // PlayerSpriteSheetCell : スプライトシートで使用する最初のセル番号 PlayerSpriteSheetCell = 8 // ExtraSizeX : プレイヤー画像の余分サイズ ExtraSizeX = 8 ) var playerFile = "./Mario/Characters/Mario.png" // Player is struct for the PlayerSystem type Player struct { ecs.BasicEntity common.RenderComponent common.SpaceComponent // Y初期値 playerPositionY float32 // 左右の足の位置 LeftPositionX float32 RightPositionX float32 // カメラの進んだ距離 cameraMoveDistance int // スプライトシート spritesheet *common.Spritesheet // 使用中のセル番号 useCell int // ジャンプのカウント数 jumpCount int // 2段ジャンプ用のカウント数 jumpCount2Step int // 頂点までのカウント数 topCount int // 着地点までのカウント数 bottomCount int // ジャンプしているか ifJumping bool // 土管上にいるか ifOnPipe bool // 落下しているか ifFalling bool } // PlayerSystem create a Player to operate type PlayerSystem struct { world *ecs.World playerEntity *Player } |
プレイヤーの初期設定
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 |
func (ps *PlayerSystem) New(w *ecs.World) { // Worldの追加 ps.world = w // Entity生成 player := Player{BasicEntity: ecs.NewBasic()} // 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 // RenderSystemに追加 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: 3200, Y: 300}, } } |
プレイヤーが移動して、スクリーン幅(480px)を超えてしまうと、プレイヤーは画面から消えてしまいます。そこで、プレイヤーの移動に合わせてカメラの移動を行います。
カメラの動作は、engo/commonに用意されているCameraSystemを使用します。ここでは、カメラ範囲を設定します。
右移動
main.goで設定した名前のボタン入力を検知したら、プレイヤーを右移動させます。
今回のステージ上には土管があるので、土管用の処理が必要です。地面上を右移動していて土管がある場合は、右移動は無効にします。また、土管上から地面に右移動する場合は、プレイヤーの位置を地面上まで下げます。
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 55 56 57 58 59 60 |
func (ps *PlayerSystem) Update(dt float32) { // プレイヤーを右に移動 if engo.Input.Button("MoveRight").Down() { // 土管位置で土管より下にいる場合 if getMakingInfo(PipePoint, int(ps.playerEntity.RightPositionX)) && int(ps.playerEntity.SpaceComponent.Position.Y) > int(engo.WindowHeight())-CellHeight16*8 { // 右移動できない } else { // 土管上にいる かつ ジャンプ中でない if ps.playerEntity.ifOnPipe && ps.playerEntity.jumpCount == 0 { // 土管位置から外れた場合 if !getMakingInfo(PipePoint, int(ps.playerEntity.LeftPositionX)) && !getMakingInfo(PipePoint, int(ps.playerEntity.RightPositionX)) { ps.playerEntity.ifOnPipe = false ps.playerEntity.SpaceComponent.Position.Y = ps.playerEntity.playerPositionY } } // 画面の真ん中より左に位置していれば、カメラを移動せずプレーヤーを移動する if int(ps.playerEntity.SpaceComponent.Position.X) < ps.playerEntity.cameraMoveDistance+int(engo.WindowWidth())/2 { ps.playerEntity.SpaceComponent.Position.X += MoveDistance ps.playerEntity.LeftPositionX += MoveDistance ps.playerEntity.RightPositionX += MoveDistance } else { // 画面の右端に達していなければプレイヤーを移動する if int(ps.playerEntity.SpaceComponent.Position.X) < int(engo.WindowWidth())-CellWidth32 { ps.playerEntity.SpaceComponent.Position.X += MoveDistance ps.playerEntity.LeftPositionX += MoveDistance ps.playerEntity.RightPositionX += MoveDistance } if int(ps.playerEntity.SpaceComponent.Position.X) < TileNum*CellWidth16-int(engo.WindowWidth()/2) { // カメラを移動する engo.Mailbox.Dispatch(common.CameraMessage{ Axis: common.XAxis, Value: MoveDistance, Incremental: true, }) } ps.playerEntity.cameraMoveDistance += MoveDistance } } // ジャンプ中でない場合 if ps.playerEntity.jumpCount == 0 { switch ps.playerEntity.useCell { case 0: ps.playerEntity.useCell = 1 case 1: ps.playerEntity.useCell = 2 case 2: ps.playerEntity.useCell = 3 case 3: ps.playerEntity.useCell = 4 case 4: ps.playerEntity.useCell = 0 } } else { ps.playerEntity.useCell = 3 } // プレイヤーの動作を変更 ps.playerEntity.RenderComponent.Drawable = ps.playerEntity.spritesheet.Cell(PlayerSpriteSheetCell + ps.playerEntity.useCell) } } |
カメラの移動はMailboxとCameraMessageを使用します。CameraSystemは、1つのWorldに必ず存在し、CameraMessageの送信を常に監視しています。
プレイヤーに動作をつけるため、右移動中はスプライトシートの読込セルをローテーションさせます。また、ジャンプ中は読込セルを固定にします。
ジャンプ
同様にボタン入力を検知したら、ジャンプを行います。ジャンプは2段ジャンプできるようにしました。
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 55 56 57 58 59 60 61 62 63 |
func (ps *PlayerSystem) Update(dt float32) { /*省略*/ // プレイヤーをジャンプ if engo.Input.Button("Jump").JustPressed() { // 2段ジャンプ if ps.playerEntity.ifJumping { if ps.playerEntity.jumpCount < MaxCount/2 { ps.playerEntity.jumpCount2Step = ps.playerEntity.jumpCount - 1 } else { ps.playerEntity.jumpCount2Step = MaxCount - (ps.playerEntity.jumpCount - 1) } ps.playerEntity.jumpCount = 1 ps.playerEntity.ifJumping = false } // 初回ジャンプ if ps.playerEntity.jumpCount == 0 { ps.playerEntity.jumpCount2Step = 0 ps.playerEntity.jumpCount = 1 ps.playerEntity.ifJumping = true } // 土管上からジャンプしていた場合 if ps.playerEntity.ifOnPipe { ps.playerEntity.bottomCount = 1 + MaxCount + ps.playerEntity.jumpCount2Step + 8 } else { // 地面からジャンプしていた場合 ps.playerEntity.bottomCount = 1 + MaxCount + ps.playerEntity.jumpCount2Step } } if ps.playerEntity.jumpCount != 0 { ps.playerEntity.jumpCount++ if ps.playerEntity.jumpCount <= ps.playerEntity.topCount { // Up ps.playerEntity.SpaceComponent.Position.Y -= JumpHeight } else if ps.playerEntity.jumpCount <= ps.playerEntity.bottomCount { if ps.playerEntity.SpaceComponent.Position.Y == onPipePositionY { // 右足もしくは左足が土管上の場合 if getMakingInfo(PipePoint, int(ps.playerEntity.LeftPositionX)) || getMakingInfo(PipePoint, int(ps.playerEntity.RightPositionX)) { ps.playerEntity.jumpCount = 0 ps.playerEntity.ifJumping = false ps.playerEntity.ifOnPipe = true } else { // Down ps.playerEntity.SpaceComponent.Position.Y += JumpHeight } } else { // Down ps.playerEntity.SpaceComponent.Position.Y += JumpHeight } } else { ps.playerEntity.jumpCount = 0 ps.playerEntity.ifJumping = false // 着地点が土管上の場合 if getMakingInfo(PipePoint, int(ps.playerEntity.LeftPositionX)) || getMakingInfo(PipePoint, int(ps.playerEntity.RightPositionX)) { ps.playerEntity.ifOnPipe = true } else { // 着地点が地面の場合 ps.playerEntity.ifOnPipe = false } } } } |
ジャンプを開始する場所と着地する場所によって、プレイヤー位置をどれだけ上げるか、下げるかが変化します。
落下
プレイヤーの左右の足が落とし穴の位置と一致したら、プレイヤーを落下させます。
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 |
func (ps *PlayerSystem) Update(dt float32) { // 落とし穴に落ちる if ps.playerEntity.jumpCount == 0 { if getMakingInfo(FallPoint, int(ps.playerEntity.LeftPositionX)) && getMakingInfo(FallPoint, int(ps.playerEntity.RightPositionX)) { ps.playerEntity.ifFalling = true ps.playerEntity.SpaceComponent.Position.Y += MoveDistance fmt.Println(ps.playerEntity.SpaceComponent.Position.Y) } } if ps.playerEntity.SpaceComponent.Position.Y > engo.WindowHeight() { ps.Remove(ps.playerEntity.BasicEntity) } if ps.playerEntity.ifFalling { return } /*省略*/ } // Remove removes an Entity from the System func (ps *PlayerSystem) Remove(ecs.BasicEntity) { for _, system := range ps.world.Systems() { switch sys := system.(type) { case *common.RenderSystem: sys.Remove(ps.playerEntity.BasicEntity) } } } |
落下後はEntityをRenderSystemから削除し、リターンします。
ゴール
プレイヤーがゴールしたら、EntityをRenderSystemから削除します。
1 2 3 4 5 6 7 8 9 |
func (ps *PlayerSystem) Update(dt float32) { // Goal地点に達したら右移動はしない if int(ps.playerEntity.LeftPositionX) >= (TileNum-GoalTileNum+2)*CellWidth16 { ps.Remove(ps.playerEntity.BasicEntity) } /*省略*/ } |
静止時の動作
最後に静止時の動作を作成して完成です。プレイヤーが地面と土管の上にいるときは、スプライトシートの読込セルを固定にします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
func (ps *PlayerSystem) Update(dt float32) { // 通常時は動作なし if ps.playerEntity.SpaceComponent.Position.Y == ps.playerEntity.playerPositionY { ps.playerEntity.RenderComponent.Drawable = ps.playerEntity.spritesheet.Cell(PlayerSpriteSheetCell) } // 土管上にいる時も動作なし if ps.playerEntity.ifOnPipe { ps.playerEntity.RenderComponent.Drawable = ps.playerEntity.spritesheet.Cell(PlayerSpriteSheetCell) } /*省略*/ } |
完成
これで完成です。お疲れ様でした!
本当に基本の機能しか実装していませんが、意外と落とし穴に落ちてしまい、割と楽しめるゲームができました。
今後は、別ステージ、敵キャラ、リトライ、文字表示など、色々と拡張機能が欲しいので、随時作成していきたいと思います。(第3回更新しました!)