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
import (
"image/color"
"errors"
"github.com/hajimehoshi/ebiten"
)
var taskTerminated = errors.New("twenty48: task terminated")
type task func() error
type Board struct {
size int
tiles map[*Tile]struct{}
tasks []task
}
func NewBoard(size int) *Board {
@ -40,66 +45,79 @@ func (b *Board) tileAt(x, y int) *Tile {
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 {
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 err := b.Move(dir); err != nil {
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
}
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
}
if err := addRandomTile(b.tiles, b.size); err != nil {
return err
}
b.tasks = append(b.tasks, func() error {
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
}
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) {
x := b.size*tileSize + (b.size+1)*tileMargin
y := x
return x, y
}
func (b *Board) Draw(screen *ebiten.Image) error {
if err := screen.Fill(frameColor); err != nil {
func (b *Board) Draw(boardImage *ebiten.Image) error {
if err := boardImage.Fill(frameColor); err != nil {
return err
}
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))
r, g, b, a := colorToScale(tileBackgroundColor(v))
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
}
}
}
animatingTiles := map[*Tile]struct{}{}
nonAnimatingTiles := map[*Tile]struct{}{}
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
}
}

View File

@ -32,9 +32,9 @@ type TileData struct {
}
type Tile struct {
current TileData
next TileData
moving bool
current TileData
next TileData
animationCount int
}
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
}
func (t *Tile) isAnimating() bool {
return 0 < t.animationCount
}
func tileAt(tiles map[*Tile]struct{}, x, y int) *Tile {
var result *Tile
for t := range tiles {
@ -91,6 +95,10 @@ func nextTileAt(tiles map[*Tile]struct{}, x, y int) *Tile {
return result
}
const (
maxAnimationCount = 10
)
func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
vx, vy := dir.Vector()
tx := []int{}
@ -113,6 +121,9 @@ func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
if t == nil {
continue
}
if t.animationCount != 0 {
panic("not reach")
}
ii := i
jj := j
for {
@ -140,15 +151,17 @@ func MoveTiles(tiles map[*Tile]struct{}, size int, dir Dir) bool {
moved = true
break
}
next := TileData{}
next.value = t.current.value
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{}
} else {
t.next.value = t.current.value
tt.animationCount = 1
}
t.next.x = ii
t.next.y = jj
t.moving = true
next.x = ii
next.y = jj
t.next = next
t.animationCount = maxAnimationCount
}
}
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 {
cells := make([]bool, size*size)
for t := range tiles {
i := 0
if t.moving {
i = t.next.x + t.next.y*size
} else {
i = t.current.x + t.current.y*size
if t.isAnimating() {
panic("not reach")
}
i := t.current.x + t.current.y*size
cells[i] = true
}
availableCells := []int{}
@ -188,12 +199,17 @@ func addRandomTile(tiles map[*Tile]struct{}, size int) error {
}
func (t *Tile) Update() error {
if !t.moving {
if t.animationCount == 0 {
if t.next.value != 0 {
panic("not reach")
}
return nil
}
t.current = t.next
t.next = TileData{}
t.moving = false
t.animationCount--
if t.animationCount == 0 {
t.current = t.next
t.next = TileData{}
}
return nil
}
@ -212,12 +228,47 @@ func colorToScale(clr color.Color) (float64, float64, float64, float64) {
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 {
i, j := t.current.x, t.current.y
ni, nj := t.next.x, t.next.y
v := t.current.value
if v == 0 {
return nil
}
op := &ebiten.DrawImageOptions{}
x := i*tileSize + (i+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))
r, g, b, a := colorToScale(tileBackgroundColor(v))
op.ColorM.Scale(r, g, b, a)

View File

@ -103,6 +103,21 @@ func TestMoveTiles(t *testing.T) {
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,
Input: []int{