LoginSignup
3
2

More than 5 years have passed since last update.

ebiten でフィールドの移動

Posted at

前回の続きです。とりあえずアクションゲームっぽい機能をもつキャラクターを実装できたので、次はフィールドの移動を実装してみます。

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

フィールドの移動

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

ソースコード

  • 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, 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
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
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

まとめ

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

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

その他

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

3
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
3
2