LoginSignup
4
2

More than 5 years have passed since last update.

ebiten で衝突判定

Last updated at Posted at 2017-05-22

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

実行結果

collide.gif

まとめ

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

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2