Go製のゲームライブラリ ebiten を使ってSpriteの衝突判定を行ってみます。
衝突判定
ebiten には画像を表示したり移動する機能はありますが、画像同士が重なっているかを判定する関数は今のところないようです。
そこで、画像の衝突を判定する機能を実装してみたいと思います。
衝突判定があれば、キャラクター同士がぶつかっているかなど色々便利に使えそうです。
概要
作成するプログラムの概要は以下の通りです。
- 登場するキャラクターはプレイヤーとブロックの2種類
- プレイヤーはユーザーの入力で移動できる
- ブロックは移動しない
- プレイヤーはブロックにぶつかる
ソースコード
- sprite/sprite.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
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
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
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)
}
}
実行結果
まとめ
player/player.go
の IsCollide
関数が衝突判定部分ですが、プレイヤーの移動を考慮した衝突判定は条件式が煩雑になってしまいました。
もっとシンプルな実装方法がありそうな気がします。