- 投稿日:2019-04-05T16:18:58+09:00
GAE+GO+Echoでgo1.11対応した
GAE+GO+Echoでgo1.11に対応した。
実はほとんど変更する箇所がないけれど最初のサーバー起動のところだけハマったのでメモ的に。
echoのバージョンは v3.3.10 で対応しています。Echo公式のGAEへの対応方法はApp Engine Standard Go 1.9までのもので特に更新されてないっぽかった。
https://echo.labstack.com/cookbook/google-app-engineただ、上記のやり方でも動くしApp Engine APIを使い続ける場合もそのままで良い。
しかし、今回やりたかったのはApp Engine APIからの脱却。
簡単に言うと最後のappengine.Main()を外したい。
で、大したことじゃないので結論。
Go1.11からはgo標準のhttp.ListenAndServeで起動できるようになったので、こいつを使うだけで良い。具体的には公式のコードのappengine.Main() の部分をhttp.ListenAndServe()にするだけ。
package main import ( "fmt" "log" "net/http" "os" "github.com/labstack/echo" ) func createMux() *echo.Echo { e := echo.New() return e } func main() { //この辺は相変わらず必要 e := echo.New() http.Handle("/", e) //ここから追加 port := os.Getenv("PORT") if port == "" { port = "8080" log.Printf("Defaulting to port %s", port) } log.Printf("Listening on port %s", port) //この部分で起動 log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", port), nil)) }仕組みを理解してる人にとっては当然やんって感じかもしれないですね。
余談ですがFirestoreもGAEから使っていたのをFirebaseのものに変更しました。と言っても、FiresotreってGCPからアクセスしてもFirebaseからアクセスしても同じものを使ってるようでFirebaseの画面から連携処理をしたらそのまま同じデータが見れました。コードも少し書き換えるだけでできたので以前書いた記事に加筆するか新しい記事書くかも。
- 投稿日:2019-04-05T13:00:38+09:00
Google App EngineのHTTPレスポンスヘッダにContent-Typeを書き込む順序について
net/http
の ResponseWriter へのレスポンスヘッダへの書き込みは、以下のようにWriteしたあとに変更しても影響はないとされている。Changing the header map after a call to WriteHeader (or Write) has no effect unless the modified headers are trailers.
https://golang.org/src/net/http/server.go#L99またGoogle App Engineのドキュメントには、BodyからContent-Typeを推定すると記載されている。
Content-Type
If you do not explicitly set this header, the http.ResponseWriter class detects the content type from the start of the response body, and sets the Content-Type header accordingly.
このヘッダーを明示的に設定しない場合は、http.ResponseWriter クラスがレスポンス本文の先頭からコンテンツ タイプを検出し、それに応じて Content-Type ヘッダーが設定されます。しかし上記の仕様とは異なる結果となることがあり、
実行環境、Content-Typeを設定するタイミングHTTPメソッドによってResponseとして返ってくるContent-Typeが変わる。ローカルで実行したときの4つの結果はすべて想定通りである。
想定外の挙動は以下の2つ。
- GAE(appdngine.Main)とGAE(http.ListenAndServe)どちらもGETのときはContent-Typeがapplication/jsonにならない。
- GAE(appdngine.Main) | Writeの後 | POST の場合に、Content-Typeがapplication/jsonになっている。
実行環境 Content-Typeを設定するタイミング HTTPメソッド Responseとして返ってくるContent-Type ローカル Writeの前 GET application/json ローカル Writeの前 POST application/json ローカル Writeの後 GET text/plain; charset=utf-8 ローカル Writeの後 POST text/plain; charset=utf-8 GAE(appdngine.Main) Writeの前 GET text/html; charset=UTF-8 GAE(appdngine.Main) Writeの前 POST application/json GAE(appdngine.Main) Writeの後 GET text/html; charset=UTF-8 GAE(appdngine.Main) Writeの後 POST application/json GAE(http.ListenAndServe) Writeの前 GET text/html; charset=UTF-8 GAE(http.ListenAndServe) Writeの前 POST application/json GAE(http.ListenAndServe) Writeの後 GET text/html; charset=UTF-8 GAE(http.ListenAndServe) Writeの後 POST text/plain; charset=utf-8 まだコードは読めていないが予想としては、UTF-8とutf-8と大文字、小文字のパターンが存在している。Go標準は小文字なので、大文字のときはGAEが書き換えている可能性がある。
つまりGAEでGETのときはresponseを書き換えられている。GAE(http.ListenAndServe) | Writeの後 | POST のときだけは、Go標準の挙動としてtext/plain; charset=utf-8が返っている。検証コード
package main import ( "net/http" "google.golang.org/appengine" ) // 正しい実装 func handler1(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{"test": "hello"}`)) } // 正しくない実装 func handler2(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"test": "hello"}`)) w.Header().Set("Content-Type", "application/json") } func main() { http.HandleFunc("/1", handler1) http.HandleFunc("/2", handler2) // ローカル // http.ListenAndServe(":8080", nil) // GAE(appdngine.Main) // appengine.Main() // GAE(http.ListenAndServe) // http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil) }まだ実装の調査できてないけどいったんメモです。
- 投稿日:2019-04-05T11:52:57+09:00
Golandで go mod使う時の注意
- 投稿日:2019-04-05T06:46:10+09:00
Go言語で作るマリオ風2Dゲーム
概要
Go言語で2Dのゲームアプリの作り方を調べたので、簡単なゲームをサンプルとして作りました。
こちらにソースコード一式があります。作成したもの
緑の玉がプレーヤーで、青いお化けが敵です。
ステージ上にランダムに配置された落とし穴に落ちたり、ふわふわ動くお化けに当たったりしたら、ゲームオーバーです。ゲームのステージは2種類あり、上のものの他にも、雪のステージもあります。
使用するライブラリについて
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にありますが、ここでは一部をかいつまんで説明します。
背景の作成
地面と草と雲を描画します。
素材はここからとってきます。
この素材の一部をタイルのように画面に張り付けていきます。まずは
Entity
とSystem
の宣言です。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.gofunc (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) } } } }乱数を発生させて、ランダムで落とし穴や草、雲を作成しています。
敵の作成
敵のお化けを作ります。
お化けの画像はこちらからとってきました。
まずはEntityとSystemを宣言します。
enemySystem.gotype 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.gofunc (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.gofunc (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
です。プレーヤーの作成
プレーヤーの
Entity
とSystem
を宣言します。playerSystem.gotype 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.gofunc (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.gofunc (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.gopackage 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{}) }