ebiten でフィールドの移動

[前回](http://qiita.com/zenwerk/items/9759c1a68a0dcef6970b)の続きです。とりあえずアクションゲームっぽい機能をもつキャラクターを実装できたので、次はフィールドの移動を実装してみます。

これまでは、描画されたスクリーン上を絶対座標で動き回るだけでしたが、もう少し広い範囲を動かしたいと思います。

## フィールドの移動
フィールドを移動するにはキャラクターの座標を更新する必要があります。とは言え、キャラクターの座標を更新し続けるといずれ画面範囲を飛び越えて見切れてしまいます。
ということで、ある程度画面を移動したら、キャラクター自身の座標を更新せず周りのオブジェクトの座標を更新することにします。
そうすれば画面上でキャラクターの絶対座標は更新されませんが、周りのオブジェクトが移動するのでマップを動いているように見えます。

## ソースコード

- sprite/sprite.go

```go
package sprite

import (
	"github.com/hajimehoshi/ebiten"
)

type Sprite interface {
	GetCordinates() (int, int, int, int)
}

type position struct {
	X int
	Y int
}

type BaseSprite struct {
	Images     []*ebiten.Image // アニメーションさせる画像の配列
	ImageNum   int             // 総イメージ数
	CurrentNum int             // 現在何枚目の画像が表示されているか
	Position   position        // 現在表示されている位置
	count      int             // フレーム数のカウンター
}

func NewSprite(images []*ebiten.Image) *BaseSprite {
	return &BaseSprite{
		Images:   images,
		ImageNum: len(images),
	}
}

// currentImage は現在表示する画像を選択して返す
func (s *BaseSprite) currentImage() *ebiten.Image {
	// 毎フレーム画像を更新するとアニメーションが早すぎるため
	// 5フレーム毎に画像を更新する
	if s.count > 5 {
		s.count = 0
		s.CurrentNum++
		s.CurrentNum %= s.ImageNum
	}
	return s.Images[s.CurrentNum]
}

func (s *BaseSprite) DrawImage(screen *ebiten.Image, viewPort position) {
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(s.Position.X+viewPort.X), float64(s.Position.Y+viewPort.Y))
	screen.DrawImage(s.currentImage(), op)
}

func (s *BaseSprite) GetCordinates() (int, int, int, int) {
	w, h := s.currentImage().Size()
	return s.Position.X, s.Position.Y, w, h
}
```

- sprite/player.go

```go
package sprite

import (
	"math"

	"github.com/hajimehoshi/ebiten"
)

const (
	xLeftLimit  = 16 * 3         // 左方向移動の画面上の限界
	xRightLimit = 320 - (16 * 3) // 右方向移動の画面上の限界
	yUpperLimit = 16 * 2         // 上方向移動の画面上の限界
	yLowerLimit = 240 - (16 * 2) // 下方向移動の画面上の限界
)

// 四捨五入関数
func round(f float64) int {
	return int(math.Floor(f + .5))
}

// isOverlap は x1-x2 の範囲の整数が x3-x4 の範囲と重なるかを判定する
func isOverlap(x1, x2, x3, x4 int) bool {
	if x1 <= x4 && x2 >= x3 {
		return true
	}
	return false
}

type Player struct {
	BaseSprite
	jumping   bool     // 現在ジャンプ中か
	jumpSpeed float64  // 現在のジャンプ力
	fallSpeed float64  // 落下速度
	ViewPort  position // スクリーン上の相対座標
}

func NewPlayer(images []*ebiten.Image) *Player {
	player := new(Player)
	player.Images = images
	player.ImageNum = len(images)
	player.jumpSpeed = 0
	player.fallSpeed = 0.4
	return player
}

func (p *Player) jump() {
	if !p.jumping {
		p.jumping = true
		p.jumpSpeed = -6
	}
}

func (p *Player) Move(objects []Sprite) {
	// dx, dy はユーザーの移動方向を保存する
	var dx, dy int
	if ebiten.IsKeyPressed(ebiten.KeyLeft) {
		dx = -1
		p.count++
	}
	if ebiten.IsKeyPressed(ebiten.KeyRight) {
		dx = 1
		p.count++
	}
	if ebiten.IsKeyPressed(ebiten.KeyUp) {
		p.jump()
		p.count++
	}

	// 落下速度の計算
	if p.jumpSpeed < 5 {
		p.jumpSpeed += p.fallSpeed
	}
	dy = round(p.jumpSpeed)

	for _, object := range objects {
		dx, dy = p.IsCollide(dx, dy, object)
	}

	// 画面上の左右の移動限界に達しているか確認する
	if p.Position.X+dx < xLeftLimit || p.Position.X+dx > xRightLimit {
		// 移動限界に達しているなら相対座標を更新する
		// 他のオブジェクトがプレイヤーが移動する方向の逆方向に進んで欲しいので反転して(-=)代入する
		p.ViewPort.X -= dx
	} else {
		// 移動限界に達していないなら自身の絶対座標を更新する
		p.Position.X += dx
	}

	if p.Position.Y+dy < yUpperLimit || p.Position.Y+dy > yLowerLimit {
		p.ViewPort.Y -= dy
	} else {
		p.Position.Y += dy
	}
}

// IsCollide はプレイヤーが対象の object と衝突しているか判定する
func (p *Player) IsCollide(dx, dy int, object Sprite) (int, int) {
	// プレイヤーの座標
	x := p.Position.X // x座標の位置
	y := p.Position.Y // y座標の位置
	img := p.currentImage()
	w, h := img.Size() // プレイヤーの幅と高さ

	// 対象のオブジェクトの x,y座標の位置と幅と高さを取得する
	x1, y1, w1, h1 := object.GetCordinates()

	// 対象オブジェクトは相対座標付与して衝突判定を行う
	x1 += p.ViewPort.X
	y1 += p.ViewPort.Y

	overlappedX := isOverlap(x, x+w, x1, x1+w1) // x軸で重なっているか
	overlappedY := isOverlap(y, y+h, y1, y1+h1) // y軸で重なっているか

	if overlappedY {
		if dx < 0 && x+dx <= x1+w1 && x+w+dx >= x1 {
			// 左方向の移動の衝突判定
			// 衝突していたらx軸の移動速度を 0 にする
			dx = 0
		} else if dx > 0 && x+w+dx >= x1 && x+dx <= x1+w1 {
			// 右方向の移動の衝突判定
			// 衝突していたらx軸の移動速度を 0 にする
			dx = 0
		}
	}
	if overlappedX {
		if dy < 0 && y+dy <= y1+w1 && y+h+dy >= y1 {
			// 上方向の移動の衝突判定
			// 衝突していたらy軸の移動速度を 0 にする
			dy = 0
		} else if dy > 0 && y+h+dy >= y1 && y+dy <= y1+h1 {
			// 下方向の移動の衝突判定
			// 衝突していたらy軸の移動速度を 0 にする
			dy = 0

			// ジャンプ中フラグをオフにする
			p.jumping = false
			p.jumpSpeed = 0
		}
	}

	return dx, dy
}

func (p *Player) DrawImage(screen *ebiten.Image) {
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(p.Position.X), float64(p.Position.Y))
	screen.DrawImage(p.currentImage(), op)
}
```

- main.go

```go
package main

import (
	"image"
	"log"
	"strings"

	"github.com/hajimehoshi/ebiten"

	"github.com/zenwerk/go-pixelman3/sprite"
)

var player_anim0 = `------+++++-----
----+++++++++---
---+++++++++++--
--+++++++++++++-
--++++--+++--++-
-+++++--+++--++-
+++++++++++++++-
++++++++++++++++
++++++++++++++++
++++++-+++++-+++
+++++++-----++++
+-++++++++++++++
--++++++++++++-+
---++++++++++---
---++-----++----
--+++++--+++++--`

var player_anim1 = `------+++++-----
----+++++++++---
---+++++++++++--
--+++++++++++++-
--++++--+++--++-
-+++++--+++--++-
+++++++++++++++-
++++++++++++++++
++++++++++++++++
++++++-+++++-+++
+++++++-----++++
+-++++++++++++++
--++++++++++++-+
---++++++++++---
--+++++---++----
---------+++++--`

var player_anim2 = `------+++++-----
----+++++++++---
---+++++++++++--
--+++++++++++++-
--++++--+++--++-
-+++++--+++--++-
+++++++++++++++-
++++++++++++++++
++++++++++++++++
++++++-+++++-+++
+++++++-----++++
+-++++++++++++++
--++++++++++++-+
---++++++++++---
---++----+++++--
--+++++---------`

var block_img = `++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++
++++++++++++++++`

var (
	charWidth   = 16
	charHeight  = 16
	tmpImage    *image.RGBA
	playerAnim0 *ebiten.Image
	playerAnim1 *ebiten.Image
	playerAnim2 *ebiten.Image
	blockImg    *ebiten.Image
)

func createImageFromString(charString string, img *image.RGBA) {
	for indexY, line := range strings.Split(charString, "\n") {
		for indexX, str := range line {
			pos := 4*indexY*charWidth + 4*indexX
			if string(str) == "+" {
				img.Pix[pos] = 0xff   // R
				img.Pix[pos+1] = 0xff // G
				img.Pix[pos+2] = 0xff // B
				img.Pix[pos+3] = 0xff // A
			} else {
				img.Pix[pos] = 0
				img.Pix[pos+1] = 0
				img.Pix[pos+2] = 0
				img.Pix[pos+3] = 0
			}
		}
	}
}

type Game struct {
	Player *sprite.Player
	Blocks []*sprite.Block
}

func (g *Game) Init() {
	tmpImage = image.NewRGBA(image.Rect(0, 0, charWidth, charHeight))

	createImageFromString(player_anim0, tmpImage)
	playerAnim0, _ = ebiten.NewImage(charWidth, charHeight, ebiten.FilterNearest)
	playerAnim0.ReplacePixels(tmpImage.Pix)

	createImageFromString(player_anim1, tmpImage)
	playerAnim1, _ = ebiten.NewImage(charWidth, charHeight, ebiten.FilterNearest)
	playerAnim1.ReplacePixels(tmpImage.Pix)

	createImageFromString(player_anim2, tmpImage)
	playerAnim2, _ = ebiten.NewImage(charWidth, charHeight, ebiten.FilterNearest)
	playerAnim2.ReplacePixels(tmpImage.Pix)

	createImageFromString(block_img, tmpImage)
	blockImg, _ = ebiten.NewImage(charWidth, charHeight, ebiten.FilterNearest)
	blockImg.ReplacePixels(tmpImage.Pix)

	// プレイヤー
	images := []*ebiten.Image{
		playerAnim0,
		playerAnim1,
		playerAnim2,
	}
	g.Player = sprite.NewPlayer(images)
	g.Player.Position.X = 160
	g.Player.Position.Y = 50

	// ブロック
	// 床
	for x := 0; x < 640; x += 17 {
		block := sprite.NewBlock([]*ebiten.Image{blockImg})
		block.Position.X = x
		block.Position.Y = 204
		g.Blocks = append(g.Blocks, block)
	}

	// 左の壁
	for y := 0; y < 200; y += 17 {
		block := sprite.NewBlock([]*ebiten.Image{blockImg})
		block.Position.X = 0
		block.Position.Y = y
		g.Blocks = append(g.Blocks, block)
	}

	// 右の壁
	for y := 0; y < 200; y += 17 {
		block := sprite.NewBlock([]*ebiten.Image{blockImg})
		block.Position.X = 629
		block.Position.Y = y
		g.Blocks = append(g.Blocks, block)
	}

	// 第2床
	for x := 8 * 17; x < 17*13; x += 17 {
		block := sprite.NewBlock([]*ebiten.Image{blockImg})
		block.Position.X = x
		block.Position.Y = 115
		g.Blocks = append(g.Blocks, block)
	}

	block1 := sprite.NewBlock([]*ebiten.Image{blockImg})
	block1.Position.X = 60
	block1.Position.Y = 165
	g.Blocks = append(g.Blocks, block1)

	block2 := sprite.NewBlock([]*ebiten.Image{blockImg})
	block2.Position.X = 95
	block2.Position.Y = 135
	g.Blocks = append(g.Blocks, block2)

}

func (g *Game) MainLoop(screen *ebiten.Image) error {
	sprites := []sprite.Sprite{}
	for _, b := range g.Blocks {
		sprites = append(sprites, b)
	}
	g.Player.Move(sprites)

	if ebiten.IsRunningSlowly() {
		return nil
	}

	g.Player.DrawImage(screen)
	for _, block := range g.Blocks {
		block.DrawImage(screen, g.Player.ViewPort)
	}

	return nil
}

func main() {
	game := Game{}
	game.Init()

	if err := ebiten.Run(game.MainLoop, 320, 240, 2, "go-pixelman3"); err != nil {
		log.Fatal(err)
	}
}
```

- Player構造体に `ViewPort` という座標を追加しています。
	- Player構造体の `Move` 関数内で、ある座標以上の場合は `ViewPort` 内の座標を更新するようにしています
- BaseSprite構造体の `DrawImage` 関数で描画位置を元々の絶対座標+相対座標を合わせたものにしています。
	- 描画時に `block.DrawImage(screen, g.Player.ViewPort)` のように相対座標を渡すことで、プレイヤーの移動に合わせて描画位置が調整されます

## 実行結果

![map_move.gif](https://qiita-image-store.s3.amazonaws.com/0/4428/2eb95e64-6106-7b82-af45-80ee0d3f3a58.gif "map_move.gif")

## まとめ
実装してみると割と単純でしたが、こういった座標系の細かい処理は、考えを整理するのがややこしくて大変でした。
また、これを作るにあたっては ebiten の作者である @hajimehoshi さんが作られた [go-inovation](https://github.com/hajimehoshi/go-inovation) の処理を参考にしました。

ちなみに、画面外のオブジェクトにも描画命令が実行されていますが、その分も重くなるのか気になるところ。

## その他
相対座標という言葉の使い方が合っているか不安。