Ebiten: Camera bounds when scaled

Created on 26 Jun 2020  路  5Comments  路  Source: hajimehoshi/ebiten

In the camera example, how to avoid camera moving beyond a certain world coordinate (ie, having bounds) when it's scaled?

question

Most helpful comment

@joaowiciuk, my algorithm hardcoded for my environment. I try to explain with pieces of code. For all transformation I used the following article.

Assumptions:

  1. The game view can zoom in/out and scroll (no rotation or other transformations).
  2. The background image always enough to fill the screen by hight. So, I need to bother about fix zoom only if result hight is not enough.

Algorithm:

  1. The view has 3 geoms:
type View struct {
    viewGeom  ebiten.GeoM // current state
    touchGeom ebiten.GeoM // transformations during current touch hanlding
    fixGeom   ebiten.GeoM // need to bound view to edges

    touches []image.Point // previous touches
}
  1. They are used to calc final geom to draw:
func (v *View) GeoM() ebiten.GeoM {
    geom := v.viewGeom
    geom.Concat(v.touchGeom)
    geom.Concat(v.fixGeom)
    return geom
}
  1. Handling touches contain three parts.
    3.1 If no touches - commits geom chates to current state:
func (v *View) Commit() {
    v.viewGeom = v.GeoM()
    v.touchGeom.Reset()
    v.fixGeom.Reset()
    v.touches = nil
}

3.2 If one touch - it's a scrolling:

func (v *View) handleScrollTouch(touch image.Point) {
    v.touchGeom.Reset()
    v.touchGeom.Translate(float64(touch.X-v.touches[0].X), float64(touch.Y-v.touches[0].Y))
}

3.3 If two touches - it's a zoom and a scrolling:

func (v *View) handleMultTouch(touches []image.Point) {
    m1 := magnitudeSquare(v.touches[0], v.touches[1])
    m2 := magnitudeSquare(touches[0], touches[1])
    scale := math.Sqrt(m2 / m1)

    v.touchGeom.Reset()
    v.touchGeom.Translate(-float64(v.touches[0].X), -float64(v.touches[0].Y))
    v.touchGeom.Scale(scale, scale)
    v.touchGeom.Translate(float64(touches[0].X), float64(touches[0].Y))
}

func magnitudeSquare(p1, p2 image.Point) float64 {
    dx := p2.X - p1.X
    dy := p2.Y - p1.Y
    return float64(dx*dx + dy*dy)
}
  1. After finishing handling touches, the main Update function calls FixGeomToBounds (mentioned earlier).

Hopes, it helps.

All 5 comments

There is no limitation for the camera move so far. I don't plan to add some so far, but do you request it?

I think it's up to the application logic, not the engine.

I do the following algorithm:

  1. Calc final geom.
  2. Apply geom to (0,0) and (0,height). Calc required zoom out coef to calc geom'.
  3. Apply geom' to (0,0). Calc required translate coefs.

I think about to publish that like ebiten extension library. But don't know when it becomes ready for open source :)

It looks like this:

func (v *View) FixGeomToBounds(imageRect, screeRect Rect) {
    v.fixGeom.Reset()
    geom := v.GeoM()

    x0, y0 := geom.Apply(0, 0)
    _, y1 := geom.Apply(0, imageRect.Heigth)

    transformedImgHieght := y1 - y0
    z := math.Sqrt(imageRect.Heigth * imageRect.Heigth /
        transformedImgHieght / transformedImgHieght)
    if z > 1 {
        v.fixGeom.Scale(z, z)
        geom.Concat(v.fixGeom)
        x0, y0 = geom.Apply(0, 0)
    }

    if x0 > 0 {
        v.fixGeom.Translate(-x0, 0)
    }
    if y0 > 0 {
        v.fixGeom.Translate(0, -y0)
    }

    x1, y1 := geom.Apply(imageRect.Width, imageRect.Heigth)
    if x1 < screeRect.Width {
        v.fixGeom.Translate(screeRect.Width-x1, 0)
    }
    if y1 < screeRect.Heigth {
        v.fixGeom.Translate(0, screeRect.Heigth-y1)
    }
}

There is no limitation for the camera move so far. I don't plan to add some so far, but do you request it?

It would be great to have it in the camera example.

I think it's up to the application logic, not the engine.

You're absolutely right, @kulti. Not every game needs it. Your algorithm looks like what I'm trying to implement, but I could not fully understand it. Do you mind sharing the rest of the code?

@joaowiciuk, my algorithm hardcoded for my environment. I try to explain with pieces of code. For all transformation I used the following article.

Assumptions:

  1. The game view can zoom in/out and scroll (no rotation or other transformations).
  2. The background image always enough to fill the screen by hight. So, I need to bother about fix zoom only if result hight is not enough.

Algorithm:

  1. The view has 3 geoms:
type View struct {
    viewGeom  ebiten.GeoM // current state
    touchGeom ebiten.GeoM // transformations during current touch hanlding
    fixGeom   ebiten.GeoM // need to bound view to edges

    touches []image.Point // previous touches
}
  1. They are used to calc final geom to draw:
func (v *View) GeoM() ebiten.GeoM {
    geom := v.viewGeom
    geom.Concat(v.touchGeom)
    geom.Concat(v.fixGeom)
    return geom
}
  1. Handling touches contain three parts.
    3.1 If no touches - commits geom chates to current state:
func (v *View) Commit() {
    v.viewGeom = v.GeoM()
    v.touchGeom.Reset()
    v.fixGeom.Reset()
    v.touches = nil
}

3.2 If one touch - it's a scrolling:

func (v *View) handleScrollTouch(touch image.Point) {
    v.touchGeom.Reset()
    v.touchGeom.Translate(float64(touch.X-v.touches[0].X), float64(touch.Y-v.touches[0].Y))
}

3.3 If two touches - it's a zoom and a scrolling:

func (v *View) handleMultTouch(touches []image.Point) {
    m1 := magnitudeSquare(v.touches[0], v.touches[1])
    m2 := magnitudeSquare(touches[0], touches[1])
    scale := math.Sqrt(m2 / m1)

    v.touchGeom.Reset()
    v.touchGeom.Translate(-float64(v.touches[0].X), -float64(v.touches[0].Y))
    v.touchGeom.Scale(scale, scale)
    v.touchGeom.Translate(float64(touches[0].X), float64(touches[0].Y))
}

func magnitudeSquare(p1, p2 image.Point) float64 {
    dx := p2.X - p1.X
    dy := p2.Y - p1.Y
    return float64(dx*dx + dy*dy)
}
  1. After finishing handling touches, the main Update function calls FixGeomToBounds (mentioned earlier).

Hopes, it helps.

Hello everyone! Thanks for your answers

I've come with a solution to this problem that I think it's worth sharing here

First, I rewrote the Camera struct in the camera example to

type Camera struct {
    View  image.Rectangle
    Limit image.Rectangle
    Zoom  int
}

Then created the function

func (c *Camera) Move(dx, dy float64) {
    if dx == 0 && dy == 0 {
        return
    }

    x0, y0 := c.ScreenToWorld(0, 0)
    x1, y1 := c.ScreenToWorld(c.View.Dx(), c.View.Dy())
    worldView := image.Rect(int(x0), int(y0), int(x1), int(y1))
    displacement := image.Pt(int(dx), int(dy))

    if worldView.Min.X < c.Limit.Min.X && dx < 0 {
        displacement.X = 0
    }

    if worldView.Min.Y < c.Limit.Min.Y && dy < 0 {
        displacement.Y = 0
    }

    if worldView.Max.X > c.Limit.Max.X && dx > 0 {
        displacement.X = 0
    }
    if worldView.Max.Y > c.Limit.Max.Y && dy > 0 {
        displacement.Y = 0
    }

    newCameraView := c.View.Add(displacement)
    c.View = newCameraView
}

So I could just call it in the Update loop like this:

        dMax := 8.0

    var dx, dy float64

    if ebiten.IsKeyPressed(ebiten.KeyA) {
        dx = -dMax
    }

    if ebiten.IsKeyPressed(ebiten.KeyD) {
        dx = dMax
    }

    if ebiten.IsKeyPressed(ebiten.KeyW) {
        dy = -dMax
    }

    if ebiten.IsKeyPressed(ebiten.KeyS) {
        dy = dMax
    }

The rest of the code is just a matter of adaptation

Was this page helpful?
0 / 5 - 0 ratings

Related issues

QiProject picture QiProject  路  4Comments

hajimehoshi picture hajimehoshi  路  6Comments

hajimehoshi picture hajimehoshi  路  7Comments

nanoslayer picture nanoslayer  路  7Comments

hajimehoshi picture hajimehoshi  路  5Comments