ebiten で衝突判定

Go製のゲームライブラリ [ebiten](https://github.com/hajimehoshi/ebiten) を使ってSpriteの衝突判定を行ってみます。

## 衝突判定
ebiten には画像を表示したり移動する機能はありますが、画像同士が重なっているかを判定する関数は今のところないようです。
そこで、画像の衝突を判定する機能を実装してみたいと思います。

衝突判定があれば、キャラクター同士がぶつかっているかなど色々便利に使えそうです。

## 概要
作成するプログラムの概要は以下の通りです。

- 登場するキャラクターはプレイヤーとブロックの2種類
	- プレイヤーはユーザーの入力で移動できる
	- ブロックは移動しない
	- プレイヤーはブロックにぶつかる

### ソースコード

- 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) {
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(s.Position.X), float64(s.Position.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/block.go

```go
package sprite

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

type Block struct {
	BaseSprite
}

func NewBlock(images []*ebiten.Image) *Block {
	block := new(Block)
	block.Images = images
	block.ImageNum = len(images)
	return block
}
```

- sprite/player.go

```go
package sprite

import "github.com/hajimehoshi/ebiten"

// 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
}

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

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) {
		dy = -1
		p.count++
	}
	if ebiten.IsKeyPressed(ebiten.KeyDown) {
		dy = 1
		p.count++
	}

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

	p.Position.X += dx
	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()

	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+h1 && y+h+dy >= y1 {
			// 上方向の移動の衝突判定
			// 衝突していたらy軸の移動速度を 0 にする
			dy = 0
		} else if dy > 0 && y+h+dy >= y1 && y+dy <= y1+h1 {
			// 下方向の移動の衝突判定
			// 衝突していたらy軸の移動速度を 0 にする
			dy = 0
		}
	}

	return dx, dy
}
```

- 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 = 10
	g.Player.Position.Y = 10

	// ブロック
	block1 := sprite.NewBlock([]*ebiten.Image{blockImg})
	block1.Position.X = 100
	block1.Position.Y = 50
	block2 := sprite.NewBlock([]*ebiten.Image{blockImg})
	block2.Position.X = 200
	block2.Position.Y = 100

	g.Blocks = []*sprite.Block{
		block1,
		block2,
	}
}

func (g *Game) MainLoop(screen *ebiten.Image) error {
	g.Player.Move([]sprite.Sprite{g.Blocks[0], g.Blocks[1]})

	if ebiten.IsRunningSlowly() {
		return nil
	}

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

	return nil
}

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

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

### 実行結果

![collide.gif](https://qiita-image-store.s3.amazonaws.com/0/4428/f08754a8-0ec3-0ff5-d1df-2e1688a6b512.gif "collide.gif")

## まとめ
`player/player.go` の `IsCollide` 関数が衝突判定部分ですが、プレイヤーの移動を考慮した衝突判定は条件式が煩雑になってしまいました。
もっとシンプルな実装方法がありそうな気がします。