examples/2048: Animate tiles

This commit is contained in:
Hajime Hoshi 2016-07-31 02:49:28 +09:00
parent 0ec07420f4
commit bdf2919ed0
3 changed files with 158 additions and 60 deletions

View File

@ -15,14 +15,19 @@
package twenty48 package twenty48
import ( import (
"image/color" "errors"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
) )
var taskTerminated = errors.New("twenty48: task terminated")
type task func() error
type Board struct { type Board struct {
size int size int
tiles map[*Tile]struct{} tiles map[*Tile]struct{}
tasks []task
} }
func NewBoard(size int) *Board { func NewBoard(size int) *Board {
@ -40,66 +45,79 @@ func (b *Board) tileAt(x, y int) *Tile {
return tileAt(b.tiles, x, y) return tileAt(b.tiles, x, y)
} }
func (b *Board) isAnimating() bool {
for t := range b.tiles {
if t.isAnimating() {
return true
}
}
return false
}
func (b *Board) Update(input *Input) error { func (b *Board) Update(input *Input) error {
if 0 < len(b.tasks) {
t := b.tasks[0]
err := t()
if err == taskTerminated {
b.tasks = b.tasks[1:]
} else if err != nil {
return err
}
return nil
}
if dir, ok := input.Dir(); ok { if dir, ok := input.Dir(); ok {
if err := b.Move(dir); err != nil { if err := b.Move(dir); err != nil {
return err return err
} }
} }
for t := range b.tiles {
if err := t.Update(); err != nil {
return err
}
}
nextTiles := map[*Tile]struct{}{}
for t := range b.tiles {
if t.current.value == 0 {
continue
}
nextTiles[t] = struct{}{}
}
b.tiles = nextTiles
return nil return nil
} }
func (b *Board) Move(dir Dir) error { func (b *Board) Move(dir Dir) error {
if moved := MoveTiles(b.tiles, b.size, dir); !moved { if !MoveTiles(b.tiles, b.size, dir) {
return nil return nil
} }
if err := addRandomTile(b.tiles, b.size); err != nil { b.tasks = append(b.tasks, func() error {
return err for t := range b.tiles {
} if err := t.Update(); err != nil {
return err
}
}
for t := range b.tiles {
if t.isAnimating() {
return nil
}
}
return taskTerminated
})
b.tasks = append(b.tasks, func() error {
nextTiles := map[*Tile]struct{}{}
for t := range b.tiles {
if t.next.value != 0 {
panic("not reach")
}
if t.current.value == 0 {
continue
}
nextTiles[t] = struct{}{}
}
b.tiles = nextTiles
if err := addRandomTile(b.tiles, b.size); err != nil {
return err
}
return taskTerminated
})
return nil return nil
} }
const (
tileSize = 40
tileMargin = 2
)
var (
tileImage *ebiten.Image
)
func init() {
var err error
tileImage, err = ebiten.NewImage(tileSize, tileSize, ebiten.FilterNearest)
if err != nil {
panic(err)
}
if err := tileImage.Fill(color.White); err != nil {
panic(err)
}
}
func (b *Board) Size() (int, int) { func (b *Board) Size() (int, int) {
x := b.size*tileSize + (b.size+1)*tileMargin x := b.size*tileSize + (b.size+1)*tileMargin
y := x y := x
return x, y return x, y
} }
func (b *Board) Draw(screen *ebiten.Image) error { func (b *Board) Draw(boardImage *ebiten.Image) error {
if err := screen.Fill(frameColor); err != nil { if err := boardImage.Fill(frameColor); err != nil {
return err return err
} }
for j := 0; j < b.size; j++ { for j := 0; j < b.size; j++ {
@ -111,13 +129,27 @@ func (b *Board) Draw(screen *ebiten.Image) error {
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
r, g, b, a := colorToScale(tileBackgroundColor(v)) r, g, b, a := colorToScale(tileBackgroundColor(v))
op.ColorM.Scale(r, g, b, a) op.ColorM.Scale(r, g, b, a)
if err := screen.DrawImage(tileImage, op); err != nil { if err := boardImage.DrawImage(tileImage, op); err != nil {
return err return err
} }
} }
} }
animatingTiles := map[*Tile]struct{}{}
nonAnimatingTiles := map[*Tile]struct{}{}
for t := range b.tiles { for t := range b.tiles {
if err := t.Draw(screen); err != nil { if t.isAnimating() {
animatingTiles[t] = struct{}{}
} else {
nonAnimatingTiles[t] = struct{}{}
}
}
for t := range nonAnimatingTiles {
if err := t.Draw(boardImage); err != nil {
return err
}
}
for t := range animatingTiles {
if err := t.Draw(boardImage); err != nil {
return err return err
} }
} }

View File

@ -32,9 +32,9 @@ type TileData struct {
} }
type Tile struct { type Tile struct {
current TileData current TileData
next TileData next TileData
moving bool animationCount int
} }
func NewTile(value int, x, y int) *Tile { func NewTile(value int, x, y int) *Tile {
@ -63,6 +63,10 @@ func (t *Tile) NextPos() (int, int) {
return t.next.x, t.next.y return t.next.x, t.next.y
} }
func (t *Tile) isAnimating() bool {
return 0 < t.animationCount
}
func tileAt(tiles map[*Tile]struct{}, x, y int) *Tile { func tileAt(tiles map[*Tile]struct{}, x, y int) *Tile {
var result *Tile var result *Tile
for t := range tiles { for t := range tiles {
@ -91,6 +95,10 @@ func nextTileAt(tiles map[*Tile]struct{}, x, y int) *Tile {
return result return result
} }
const (
maxAnimationCount = 10
)
func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool { func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
vx, vy := dir.Vector() vx, vy := dir.Vector()
tx := []int{} tx := []int{}
@ -113,6 +121,9 @@ func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
if t == nil { if t == nil {
continue continue
} }
if t.animationCount != 0 {
panic("not reach")
}
ii := i ii := i
jj := j jj := j
for { for {
@ -140,15 +151,17 @@ func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
moved = true moved = true
break break
} }
next := TileData{}
next.value = t.current.value
if tt := nextTileAt(tiles, ii, jj); tt != t && tt != nil { if tt := nextTileAt(tiles, ii, jj); tt != t && tt != nil {
t.next.value = t.current.value + tt.next.value next.value = t.current.value + tt.next.value
tt.next = TileData{} tt.next = TileData{}
} else { tt.animationCount = 1
t.next.value = t.current.value
} }
t.next.x = ii next.x = ii
t.next.y = jj next.y = jj
t.moving = true t.next = next
t.animationCount = maxAnimationCount
} }
} }
return moved return moved
@ -157,12 +170,10 @@ func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
func addRandomTile(tiles map[*Tile]struct{}, size int) error { func addRandomTile(tiles map[*Tile]struct{}, size int) error {
cells := make([]bool, size*size) cells := make([]bool, size*size)
for t := range tiles { for t := range tiles {
i := 0 if t.isAnimating() {
if t.moving { panic("not reach")
i = t.next.x + t.next.y*size
} else {
i = t.current.x + t.current.y*size
} }
i := t.current.x + t.current.y*size
cells[i] = true cells[i] = true
} }
availableCells := []int{} availableCells := []int{}
@ -188,12 +199,17 @@ func addRandomTile(tiles map[*Tile]struct{}, size int) error {
} }
func (t *Tile) Update() error { func (t *Tile) Update() error {
if !t.moving { if t.animationCount == 0 {
if t.next.value != 0 {
panic("not reach")
}
return nil return nil
} }
t.current = t.next t.animationCount--
t.next = TileData{} if t.animationCount == 0 {
t.moving = false t.current = t.next
t.next = TileData{}
}
return nil return nil
} }
@ -212,12 +228,47 @@ func colorToScale(clr color.Color) (float64, float64, float64, float64) {
return rf, gf, bf, af return rf, gf, bf, af
} }
func mean(a, b int, rate float64) int {
return int(float64(a)*rate + float64(b)*(1-rate))
}
const (
tileSize = 40
tileMargin = 2
)
var (
tileImage *ebiten.Image
)
func init() {
var err error
tileImage, err = ebiten.NewImage(tileSize, tileSize, ebiten.FilterNearest)
if err != nil {
panic(err)
}
if err := tileImage.Fill(color.White); err != nil {
panic(err)
}
}
func (t *Tile) Draw(boardImage *ebiten.Image) error { func (t *Tile) Draw(boardImage *ebiten.Image) error {
i, j := t.current.x, t.current.y i, j := t.current.x, t.current.y
ni, nj := t.next.x, t.next.y
v := t.current.value v := t.current.value
if v == 0 {
return nil
}
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
x := i*tileSize + (i+1)*tileMargin x := i*tileSize + (i+1)*tileMargin
y := j*tileSize + (j+1)*tileMargin y := j*tileSize + (j+1)*tileMargin
nx := ni*tileSize + (ni+1)*tileMargin
ny := nj*tileSize + (nj+1)*tileMargin
if 0 < t.animationCount && t.next.value != 0 {
rate := float64(t.animationCount) / maxAnimationCount
x = mean(x, nx, rate)
y = mean(y, ny, rate)
}
op.GeoM.Translate(float64(x), float64(y)) op.GeoM.Translate(float64(x), float64(y))
r, g, b, a := colorToScale(tileBackgroundColor(v)) r, g, b, a := colorToScale(tileBackgroundColor(v))
op.ColorM.Scale(r, g, b, a) op.ColorM.Scale(r, g, b, a)

View File

@ -103,6 +103,21 @@ func TestMoveTiles(t *testing.T) {
0, 0, 0, 0, 0, 0, 0, 0,
}, },
}, },
{
Dir: DirLeft,
Input: []int{
0, 2, 2, 2,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
},
Want: []int{
4, 2, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0,
},
},
{ {
Dir: DirRight, Dir: DirRight,
Input: []int{ Input: []int{