example/blocks: Refactoring

This commit is contained in:
Hajime Hoshi 2018-01-22 15:12:17 +09:00
parent 20e12a7ea9
commit 8a7687687f
10 changed files with 273 additions and 253 deletions

View File

@ -22,16 +22,15 @@ import (
const maxFlushCount = 20 const maxFlushCount = 20
// Field represents a game field with block states.
type Field struct { type Field struct {
blocks [fieldBlockNumX][fieldBlockNumY]BlockType blocks [fieldBlockNumX][fieldBlockNumY]BlockType
flushCount int flushCount int
onEndFlushing func(int) onEndFlushAnimating func(int)
}
func NewField() *Field {
return &Field{}
} }
// IsBlocked returns a boolean value indicating whether
// there is a block at position (x, y) on the field.
func (f *Field) IsBlocked(x, y int) bool { func (f *Field) IsBlocked(x, y int) bool {
if x < 0 || fieldBlockNumX <= x { if x < 0 || fieldBlockNumX <= x {
return true return true
@ -45,49 +44,58 @@ func (f *Field) IsBlocked(x, y int) bool {
return f.blocks[x][y] != BlockTypeNone return f.blocks[x][y] != BlockTypeNone
} }
func (f *Field) collides(piece *Piece, x, y int, angle Angle) bool { // MovePieceToLeft tries to move the piece to the left
return piece.collides(f, x, y, angle) // and returns the piece's next x position.
}
func (f *Field) MovePieceToLeft(piece *Piece, x, y int, angle Angle) int { func (f *Field) MovePieceToLeft(piece *Piece, x, y int, angle Angle) int {
if f.collides(piece, x-1, y, angle) { if piece.collides(f, x-1, y, angle) {
return x return x
} }
return x - 1 return x - 1
} }
// MovePieceToRight tries to move the piece to the right
// and returns the piece's next x position.
func (f *Field) MovePieceToRight(piece *Piece, x, y int, angle Angle) int { func (f *Field) MovePieceToRight(piece *Piece, x, y int, angle Angle) int {
if f.collides(piece, x+1, y, angle) { if piece.collides(f, x+1, y, angle) {
return x return x
} }
return x + 1 return x + 1
} }
// PieceDroppable returns a boolean value indicating whether
// the piece at (x, y) with the given angle can drop.
func (f *Field) PieceDroppable(piece *Piece, x, y int, angle Angle) bool { func (f *Field) PieceDroppable(piece *Piece, x, y int, angle Angle) bool {
return !f.collides(piece, x, y+1, angle) return !piece.collides(f, x, y+1, angle)
} }
// DropPiece tries to drop the piece to the right
// and returns the piece's next y position.
func (f *Field) DropPiece(piece *Piece, x, y int, angle Angle) int { func (f *Field) DropPiece(piece *Piece, x, y int, angle Angle) int {
if f.collides(piece, x, y+1, angle) { if piece.collides(f, x, y+1, angle) {
return y return y
} }
return y + 1 return y + 1
} }
// RotatePieceRight tries to rotate the piece to the right
// and returns the piece's next angle.
func (f *Field) RotatePieceRight(piece *Piece, x, y int, angle Angle) Angle { func (f *Field) RotatePieceRight(piece *Piece, x, y int, angle Angle) Angle {
if f.collides(piece, x, y, angle.RotateRight()) { if piece.collides(f, x, y, angle.RotateRight()) {
return angle return angle
} }
return angle.RotateRight() return angle.RotateRight()
} }
// RotatePieceLeft tries to rotate the piece to the left
// and returns the piece's next angle.
func (f *Field) RotatePieceLeft(piece *Piece, x, y int, angle Angle) Angle { func (f *Field) RotatePieceLeft(piece *Piece, x, y int, angle Angle) Angle {
if f.collides(piece, x, y, angle.RotateLeft()) { if piece.collides(f, x, y, angle.RotateLeft()) {
return angle return angle
} }
return angle.RotateLeft() return angle.RotateLeft()
} }
// AbsorbPiece absorbs the piece at (x, y) with the given angle into the field.
func (f *Field) AbsorbPiece(piece *Piece, x, y int, angle Angle) { func (f *Field) AbsorbPiece(piece *Piece, x, y int, angle Angle) {
piece.AbsorbInto(f, x, y, angle) piece.AbsorbInto(f, x, y, angle)
if f.flushable() { if f.flushable() {
@ -95,14 +103,20 @@ func (f *Field) AbsorbPiece(piece *Piece, x, y int, angle Angle) {
} }
} }
func (f *Field) Flushing() bool { // IsFlushAnimating returns a boolean value indicating
// whether there is a flush animation.
func (f *Field) IsFlushAnimating() bool {
return 0 < f.flushCount return 0 < f.flushCount
} }
func (f *Field) SetEndFlushing(fn func(lines int)) { // SetEndFlushAnimating sets a callback fired on the end of flush animation.
f.onEndFlushing = fn // The callback argument is the number of flushed lines.
func (f *Field) SetEndFlushAnimating(fn func(lines int)) {
f.onEndFlushAnimating = fn
} }
// flushable returns a boolean value indicating whether
// there is a flushable line in the field.
func (f *Field) flushable() bool { func (f *Field) flushable() bool {
for j := fieldBlockNumY - 1; 0 <= j; j-- { for j := fieldBlockNumY - 1; 0 <= j; j-- {
if f.flushableLine(j) { if f.flushableLine(j) {
@ -112,6 +126,8 @@ func (f *Field) flushable() bool {
return false return false
} }
// flushableLine returns a boolean value indicating whether
// the line j is flushabled or not.
func (f *Field) flushableLine(j int) bool { func (f *Field) flushableLine(j int) bool {
for i := 0; i < fieldBlockNumX; i++ { for i := 0; i < fieldBlockNumX; i++ {
if f.blocks[i][j] == BlockTypeNone { if f.blocks[i][j] == BlockTypeNone {
@ -125,7 +141,7 @@ func (f *Field) setBlock(x, y int, blockType BlockType) {
f.blocks[x][y] = blockType f.blocks[x][y] = blockType
} }
func (f *Field) endFlushing() int { func (f *Field) endFlushAnimating() int {
flushedLineNum := 0 flushedLineNum := 0
for j := fieldBlockNumY - 1; 0 <= j; j-- { for j := fieldBlockNumY - 1; 0 <= j; j-- {
if f.flushLine(j + flushedLineNum) { if f.flushLine(j + flushedLineNum) {
@ -135,6 +151,11 @@ func (f *Field) endFlushing() int {
return flushedLineNum return flushedLineNum
} }
// flushLine flushes the line j if possible, and if the line is flushed,
// the other lines above the line go down.
//
// flushLine returns a boolean value indicating whether
// the line is actually flushed.
func (f *Field) flushLine(j int) bool { func (f *Field) flushLine(j int) bool {
for i := 0; i < fieldBlockNumX; i++ { for i := 0; i < fieldBlockNumX; i++ {
if f.blocks[i][j] == BlockTypeNone { if f.blocks[i][j] == BlockTypeNone {
@ -153,14 +174,17 @@ func (f *Field) flushLine(j int) bool {
} }
func (f *Field) Update() { func (f *Field) Update() {
if 0 <= f.flushCount {
f.flushCount--
if f.flushCount == 0 { if f.flushCount == 0 {
l := f.endFlushing() return
if f.onEndFlushing != nil {
f.onEndFlushing(l)
} }
f.flushCount--
if f.flushCount > 0 {
return
} }
if f.onEndFlushAnimating != nil {
f.onEndFlushAnimating(f.endFlushAnimating())
} }
} }
@ -171,12 +195,7 @@ func min(a, b float64) float64 {
return a return a
} }
func (f *Field) flushingColor() ebiten.ColorM { func flushingColor(rate float64) ebiten.ColorM {
c := f.flushCount
if c < 0 {
c = 0
}
rate := float64(c) / maxFlushCount
clr := ebiten.ColorM{} clr := ebiten.ColorM{}
alpha := min(1, rate*2) alpha := min(1, rate*2)
clr.Scale(1, 1, 1, alpha) clr.Scale(1, 1, 1, alpha)
@ -186,23 +205,16 @@ func (f *Field) flushingColor() ebiten.ColorM {
} }
func (f *Field) Draw(r *ebiten.Image, x, y int) { func (f *Field) Draw(r *ebiten.Image, x, y int) {
blocks := make([][]BlockType, len(f.blocks)) fc := flushingColor(float64(f.flushCount) / maxFlushCount)
flushingBlocks := make([][]BlockType, len(f.blocks))
for i := 0; i < fieldBlockNumX; i++ {
blocks[i] = make([]BlockType, fieldBlockNumY)
flushingBlocks[i] = make([]BlockType, fieldBlockNumY)
}
for j := 0; j < fieldBlockNumY; j++ { for j := 0; j < fieldBlockNumY; j++ {
if f.flushableLine(j) { if f.flushableLine(j) {
for i := 0; i < fieldBlockNumX; i++ { for i := 0; i < fieldBlockNumX; i++ {
flushingBlocks[i][j] = f.blocks[i][j] drawBlock(r, f.blocks[i][j], i*blockWidth+x, j*blockHeight+y, fc)
} }
} else { } else {
for i := 0; i < fieldBlockNumX; i++ { for i := 0; i < fieldBlockNumX; i++ {
blocks[i][j] = f.blocks[i][j] drawBlock(r, f.blocks[i][j], i*blockWidth+x, j*blockHeight+y, ebiten.ColorM{})
} }
} }
} }
drawBlocks(r, blocks, x, y, ebiten.ColorM{})
drawBlocks(r, flushingBlocks, x, y, f.flushingColor())
} }

View File

@ -17,41 +17,33 @@
package blocks package blocks
import ( import (
"sync"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
) )
const ScreenWidth = 256 const (
const ScreenHeight = 240 ScreenWidth = 256
ScreenHeight = 240
type GameState struct { )
SceneManager *SceneManager
Input *Input
}
type Game struct { type Game struct {
once sync.Once
sceneManager *SceneManager sceneManager *SceneManager
input Input input Input
} }
func NewGame() *Game { func (g *Game) Update(r *ebiten.Image) error {
return &Game{ if g.sceneManager == nil {
sceneManager: NewSceneManager(NewTitleScene()), g.sceneManager = &SceneManager{}
} g.sceneManager.GoTo(&TitleScene{})
} }
func (game *Game) Update(r *ebiten.Image) error { g.input.Update()
game.input.Update() if err := g.sceneManager.Update(&g.input); err != nil {
if err := game.sceneManager.Update(&GameState{
SceneManager: game.sceneManager,
Input: &game.input,
}); err != nil {
return err return err
} }
if !ebiten.IsRunningSlowly() { if ebiten.IsRunningSlowly() {
game.sceneManager.Draw(r) return nil
} }
g.sceneManager.Draw(r)
return nil return nil
} }

View File

@ -22,16 +22,24 @@ import (
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
) )
type abstractButton int type virtualGamepadButton int
const ( const (
abstractButtonLeft abstractButton = iota virtualGamepadButtonLeft virtualGamepadButton = iota
abstractButtonRight virtualGamepadButtonRight
abstractButtonDown virtualGamepadButtonDown
abstractButtonButtonA virtualGamepadButtonButtonA
abstractButtonButtonB virtualGamepadButtonButtonB
) )
var virtualGamepadButtons = []virtualGamepadButton{
virtualGamepadButtonLeft,
virtualGamepadButtonRight,
virtualGamepadButtonDown,
virtualGamepadButtonButtonA,
virtualGamepadButtonButtonB,
}
const threshold = 0.75 const threshold = 0.75
type axis struct { type axis struct {
@ -40,19 +48,19 @@ type axis struct {
} }
type gamepadConfig struct { type gamepadConfig struct {
current abstractButton current virtualGamepadButton
buttons map[abstractButton]ebiten.GamepadButton buttons map[virtualGamepadButton]ebiten.GamepadButton
axes map[abstractButton]axis axes map[virtualGamepadButton]axis
assignedButtons map[ebiten.GamepadButton]struct{} assignedButtons map[ebiten.GamepadButton]struct{}
assignedAxes map[axis]struct{} assignedAxes map[axis]struct{}
} }
func (c *gamepadConfig) initializeIfNeeded() { func (c *gamepadConfig) initializeIfNeeded() {
if c.buttons == nil { if c.buttons == nil {
c.buttons = map[abstractButton]ebiten.GamepadButton{} c.buttons = map[virtualGamepadButton]ebiten.GamepadButton{}
} }
if c.axes == nil { if c.axes == nil {
c.axes = map[abstractButton]axis{} c.axes = map[virtualGamepadButton]axis{}
} }
if c.assignedButtons == nil { if c.assignedButtons == nil {
c.assignedButtons = map[ebiten.GamepadButton]struct{}{} c.assignedButtons = map[ebiten.GamepadButton]struct{}{}
@ -69,27 +77,29 @@ func (c *gamepadConfig) Reset() {
c.assignedAxes = nil c.assignedAxes = nil
} }
func (c *gamepadConfig) Scan(index int, b abstractButton) bool { // Scan scans the current input state and assigns the given virtual gamepad button b
// to the current (pysical) pressed buttons of the gamepad.
func (c *gamepadConfig) Scan(gamepadID int, b virtualGamepadButton) bool {
c.initializeIfNeeded() c.initializeIfNeeded()
delete(c.buttons, b) delete(c.buttons, b)
delete(c.axes, b) delete(c.axes, b)
ebn := ebiten.GamepadButton(ebiten.GamepadButtonNum(index)) ebn := ebiten.GamepadButton(ebiten.GamepadButtonNum(gamepadID))
for eb := ebiten.GamepadButton(0); eb < ebn; eb++ { for eb := ebiten.GamepadButton(0); eb < ebn; eb++ {
if _, ok := c.assignedButtons[eb]; ok { if _, ok := c.assignedButtons[eb]; ok {
continue continue
} }
if ebiten.IsGamepadButtonPressed(index, eb) { if ebiten.IsGamepadButtonPressed(gamepadID, eb) {
c.buttons[b] = eb c.buttons[b] = eb
c.assignedButtons[eb] = struct{}{} c.assignedButtons[eb] = struct{}{}
return true return true
} }
} }
an := ebiten.GamepadAxisNum(index) an := ebiten.GamepadAxisNum(gamepadID)
for a := 0; a < an; a++ { for a := 0; a < an; a++ {
v := ebiten.GamepadAxis(index, a) v := ebiten.GamepadAxis(gamepadID, a)
// Check |v| < 1.0 because // Check |v| < 1.0 because
// 1) there is a bug that a button returns an axis value wrongly // 1) there is a bug that a button returns an axis value wrongly
// and the value may be over 1. // and the value may be over 1.
@ -114,7 +124,7 @@ func (c *gamepadConfig) Scan(index int, b abstractButton) bool {
return false return false
} }
func (c *gamepadConfig) IsButtonPressed(id int, b abstractButton) bool { func (c *gamepadConfig) IsButtonPressed(b virtualGamepadButton) bool {
c.initializeIfNeeded() c.initializeIfNeeded()
bb, ok := c.buttons[b] bb, ok := c.buttons[b]
@ -133,7 +143,8 @@ func (c *gamepadConfig) IsButtonPressed(id int, b abstractButton) bool {
return false return false
} }
func (c *gamepadConfig) Name(b abstractButton) string { // Name returns the button's name.
func (c *gamepadConfig) ButtonName(b virtualGamepadButton) string {
c.initializeIfNeeded() c.initializeIfNeeded()
bb, ok := c.buttons[b] bb, ok := c.buttons[b]

View File

@ -31,25 +31,21 @@ type GamepadScene struct {
buttonStates []string buttonStates []string
} }
func NewGamepadScene() *GamepadScene {
return &GamepadScene{}
}
func (s *GamepadScene) Update(state *GameState) error { func (s *GamepadScene) Update(state *GameState) error {
if s.currentIndex == 0 { if s.currentIndex == 0 {
state.Input.gamepadConfig.Reset() state.Input.gamepadConfig.Reset()
} }
if state.Input.StateForKey(ebiten.KeyEscape) == 1 { if state.Input.StateForKey(ebiten.KeyEscape) == 1 {
state.Input.gamepadConfig.Reset() state.Input.gamepadConfig.Reset()
state.SceneManager.GoTo(NewTitleScene()) state.SceneManager.GoTo(&TitleScene{})
} }
if s.buttonStates == nil { if s.buttonStates == nil {
s.buttonStates = make([]string, len(gamepadAbstractButtons)) s.buttonStates = make([]string, len(virtualGamepadButtons))
} }
for i, b := range gamepadAbstractButtons { for i, b := range virtualGamepadButtons {
if i < s.currentIndex { if i < s.currentIndex {
s.buttonStates[i] = strings.ToUpper(state.Input.gamepadConfig.Name(b)) s.buttonStates[i] = strings.ToUpper(state.Input.gamepadConfig.ButtonName(b))
continue continue
} }
if s.currentIndex == i { if s.currentIndex == i {
@ -62,15 +58,16 @@ func (s *GamepadScene) Update(state *GameState) error {
if 0 < s.countAfterSetting { if 0 < s.countAfterSetting {
s.countAfterSetting-- s.countAfterSetting--
if s.countAfterSetting <= 0 { if s.countAfterSetting <= 0 {
state.SceneManager.GoTo(NewTitleScene()) state.SceneManager.GoTo(&TitleScene{})
} }
return nil return nil
} }
b := gamepadAbstractButtons[s.currentIndex] b := virtualGamepadButtons[s.currentIndex]
if state.Input.gamepadConfig.Scan(0, b) { const gamepadID = 0
if state.Input.gamepadConfig.Scan(gamepadID, b) {
s.currentIndex++ s.currentIndex++
if s.currentIndex == len(gamepadAbstractButtons) { if s.currentIndex == len(virtualGamepadButtons) {
s.countAfterSetting = ebiten.FPS s.countAfterSetting = ebiten.FPS
} }
} }
@ -102,7 +99,7 @@ ROTATE RIGHT: %s
%s` %s`
msg := "" msg := ""
if s.currentIndex == len(gamepadAbstractButtons) { if s.currentIndex == len(virtualGamepadButtons) {
msg = "OK!" msg = "OK!"
} }
str := fmt.Sprintf(f, s.buttonStates[0], s.buttonStates[1], s.buttonStates[2], s.buttonStates[3], s.buttonStates[4], msg) str := fmt.Sprintf(f, s.buttonStates[0], s.buttonStates[1], s.buttonStates[2], s.buttonStates[3], s.buttonStates[4], msg)

View File

@ -78,20 +78,25 @@ func init() {
// Windows // Windows
imageWindows, _ = ebiten.NewImage(ScreenWidth, ScreenHeight, ebiten.FilterNearest) imageWindows, _ = ebiten.NewImage(ScreenWidth, ScreenHeight, ebiten.FilterNearest)
// Windows: Field // Windows: Field
x, y := fieldWindowPosition() x, y := fieldWindowPosition()
drawWindow(imageWindows, x, y, fieldWidth, fieldHeight) drawWindow(imageWindows, x, y, fieldWidth, fieldHeight)
// Windows: Next // Windows: Next
x, y = nextWindowLabelPosition() x, y = nextWindowLabelPosition()
common.ArcadeFont.DrawTextWithShadow(imageWindows, "NEXT", x, y, 1, fontColor) common.ArcadeFont.DrawTextWithShadow(imageWindows, "NEXT", x, y, 1, fontColor)
x, y = nextWindowPosition() x, y = nextWindowPosition()
drawWindow(imageWindows, x, y, 5*blockWidth, 5*blockHeight) drawWindow(imageWindows, x, y, 5*blockWidth, 5*blockHeight)
// Windows: Score // Windows: Score
x, y = scoreTextBoxPosition() x, y = scoreTextBoxPosition()
drawTextBox(imageWindows, "SCORE", x, y, textBoxWidth()) drawTextBox(imageWindows, "SCORE", x, y, textBoxWidth())
// Windows: Level // Windows: Level
x, y = levelTextBoxPosition() x, y = levelTextBoxPosition()
drawTextBox(imageWindows, "LEVEL", x, y, textBoxWidth()) drawTextBox(imageWindows, "LEVEL", x, y, textBoxWidth())
// Windows: Lines // Windows: Lines
x, y = linesTextBoxPosition() x, y = linesTextBoxPosition()
drawTextBox(imageWindows, "LINES", x, y, textBoxWidth()) drawTextBox(imageWindows, "LINES", x, y, textBoxWidth())
@ -138,7 +143,7 @@ type GameScene struct {
func NewGameScene() *GameScene { func NewGameScene() *GameScene {
return &GameScene{ return &GameScene{
field: NewField(), field: &Field{},
rand: rand.New(rand.NewSource(time.Now().UnixNano())), rand: rand.New(rand.NewSource(time.Now().UnixNano())),
} }
} }
@ -168,8 +173,10 @@ func (s *GameScene) drawBackground(r *ebiten.Image) {
r.DrawImage(imageGameBG, op) r.DrawImage(imageGameBG, op)
} }
const fieldWidth = blockWidth * fieldBlockNumX const (
const fieldHeight = blockHeight * fieldBlockNumY fieldWidth = blockWidth * fieldBlockNumX
fieldHeight = blockHeight * fieldBlockNumY
)
func (s *GameScene) choosePiece() *Piece { func (s *GameScene) choosePiece() *Piece {
num := int(BlockTypeMax) num := int(BlockTypeMax)
@ -213,7 +220,7 @@ func (s *GameScene) Update(state *GameState) error {
if s.gameover { if s.gameover {
// TODO: Gamepad key? // TODO: Gamepad key?
if state.Input.StateForKey(ebiten.KeySpace) == 1 { if state.Input.StateForKey(ebiten.KeySpace) == 1 {
state.SceneManager.GoTo(NewTitleScene()) state.SceneManager.GoTo(&TitleScene{})
} }
return nil return nil
} }
@ -233,14 +240,14 @@ func (s *GameScene) Update(state *GameState) error {
angle := s.currentPieceAngle angle := s.currentPieceAngle
// Move piece by user input. // Move piece by user input.
if !s.field.Flushing() { if !s.field.IsFlushAnimating() {
piece := s.currentPiece piece := s.currentPiece
x := s.currentPieceX x := s.currentPieceX
y := s.currentPieceY y := s.currentPieceY
if state.Input.IsRotateRightTrigger() { if state.Input.IsRotateRightJustPressed() {
s.currentPieceAngle = s.field.RotatePieceRight(piece, x, y, angle) s.currentPieceAngle = s.field.RotatePieceRight(piece, x, y, angle)
moved = angle != s.currentPieceAngle moved = angle != s.currentPieceAngle
} else if state.Input.IsRotateLeftTrigger() { } else if state.Input.IsRotateLeftJustPressed() {
s.currentPieceAngle = s.field.RotatePieceLeft(piece, x, y, angle) s.currentPieceAngle = s.field.RotatePieceLeft(piece, x, y, angle)
moved = angle != s.currentPieceAngle moved = angle != s.currentPieceAngle
} else if l := state.Input.StateForLeft(); l == 1 || (10 <= l && l%2 == 0) { } else if l := state.Input.StateForLeft(); l == 1 || (10 <= l && l%2 == 0) {
@ -259,7 +266,7 @@ func (s *GameScene) Update(state *GameState) error {
} }
// Drop the current piece with gravity. // Drop the current piece with gravity.
if !s.field.Flushing() { if !s.field.IsFlushAnimating() {
angle := s.currentPieceAngle angle := s.currentPieceAngle
s.currentPieceYCarry += 2*s.level() + 1 s.currentPieceYCarry += 2*s.level() + 1
const maxCarry = 60 const maxCarry = 60
@ -269,7 +276,7 @@ func (s *GameScene) Update(state *GameState) error {
} }
} }
if !s.field.Flushing() && !s.field.PieceDroppable(piece, s.currentPieceX, s.currentPieceY, angle) { if !s.field.IsFlushAnimating() && !s.field.PieceDroppable(piece, s.currentPieceX, s.currentPieceY, angle) {
if 0 < state.Input.StateForDown() { if 0 < state.Input.StateForDown() {
s.landingCount += 10 s.landingCount += 10
} else { } else {
@ -277,8 +284,8 @@ func (s *GameScene) Update(state *GameState) error {
} }
if maxLandingCount <= s.landingCount { if maxLandingCount <= s.landingCount {
s.field.AbsorbPiece(piece, s.currentPieceX, s.currentPieceY, angle) s.field.AbsorbPiece(piece, s.currentPieceX, s.currentPieceY, angle)
if s.field.Flushing() { if s.field.IsFlushAnimating() {
s.field.SetEndFlushing(func(lines int) { s.field.SetEndFlushAnimating(func(lines int) {
s.lines += lines s.lines += lines
if 0 < lines { if 0 < lines {
s.addScore(lines) s.addScore(lines)
@ -298,7 +305,7 @@ func (s *GameScene) goNextPiece() {
s.initCurrentPiece(s.nextPiece) s.initCurrentPiece(s.nextPiece)
s.nextPiece = s.choosePiece() s.nextPiece = s.choosePiece()
s.landingCount = 0 s.landingCount = 0
if s.currentPiece.Collides(s.field, s.currentPieceX, s.currentPieceY, s.currentPieceAngle) { if s.currentPiece.collides(s.field, s.currentPieceX, s.currentPieceY, s.currentPieceAngle) {
s.gameover = true s.gameover = true
} }
} }
@ -323,7 +330,7 @@ func (s *GameScene) Draw(r *ebiten.Image) {
// Draw blocks // Draw blocks
fieldX, fieldY := fieldWindowPosition() fieldX, fieldY := fieldWindowPosition()
s.field.Draw(r, fieldX, fieldY) s.field.Draw(r, fieldX, fieldY)
if s.currentPiece != nil && !s.field.Flushing() { if s.currentPiece != nil && !s.field.IsFlushAnimating() {
x := fieldX + s.currentPieceX*blockWidth x := fieldX + s.currentPieceX*blockWidth
y := fieldY + s.currentPieceY*blockHeight y := fieldY + s.currentPieceY*blockHeight
s.currentPiece.Draw(r, x, y, s.currentPieceAngle) s.currentPiece.Draw(r, x, y, s.currentPieceAngle)

View File

@ -20,18 +20,10 @@ import (
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
) )
var gamepadAbstractButtons = []abstractButton{
abstractButtonLeft,
abstractButtonRight,
abstractButtonDown,
abstractButtonButtonA,
abstractButtonButtonB,
}
type Input struct { type Input struct {
keyStates map[ebiten.Key]int keyStates map[ebiten.Key]int
gamepadButtonStates map[ebiten.GamepadButton]int gamepadButtonStates map[ebiten.GamepadButton]int
gamepadAbstractButtonStates map[abstractButton]int virtualGamepadButtonStates map[virtualGamepadButton]int
gamepadConfig gamepadConfig gamepadConfig gamepadConfig
} }
@ -49,11 +41,11 @@ func (i *Input) StateForGamepadButton(b ebiten.GamepadButton) int {
return i.gamepadButtonStates[b] return i.gamepadButtonStates[b]
} }
func (i *Input) stateForGamepadAbstractButton(b abstractButton) int { func (i *Input) stateForVirtualGamepadButton(b virtualGamepadButton) int {
if i.gamepadAbstractButtonStates == nil { if i.virtualGamepadButtonStates == nil {
return 0 return 0
} }
return i.gamepadAbstractButtonStates[b] return i.virtualGamepadButtonStates[b]
} }
func (i *Input) Update() { func (i *Input) Update() {
@ -80,30 +72,30 @@ func (i *Input) Update() {
i.gamepadButtonStates[b]++ i.gamepadButtonStates[b]++
} }
if i.gamepadAbstractButtonStates == nil { if i.virtualGamepadButtonStates == nil {
i.gamepadAbstractButtonStates = map[abstractButton]int{} i.virtualGamepadButtonStates = map[virtualGamepadButton]int{}
} }
for _, b := range gamepadAbstractButtons { for _, b := range virtualGamepadButtons {
if !i.gamepadConfig.IsButtonPressed(gamepadID, b) { if !i.gamepadConfig.IsButtonPressed(b) {
i.gamepadAbstractButtonStates[b] = 0 i.virtualGamepadButtonStates[b] = 0
continue continue
} }
i.gamepadAbstractButtonStates[b]++ i.virtualGamepadButtonStates[b]++
} }
} }
func (i *Input) IsRotateRightTrigger() bool { func (i *Input) IsRotateRightJustPressed() bool {
if i.StateForKey(ebiten.KeySpace) == 1 || i.StateForKey(ebiten.KeyX) == 1 { if i.StateForKey(ebiten.KeySpace) == 1 || i.StateForKey(ebiten.KeyX) == 1 {
return true return true
} }
return i.stateForGamepadAbstractButton(abstractButtonButtonB) == 1 return i.stateForVirtualGamepadButton(virtualGamepadButtonButtonB) == 1
} }
func (i *Input) IsRotateLeftTrigger() bool { func (i *Input) IsRotateLeftJustPressed() bool {
if i.StateForKey(ebiten.KeyZ) == 1 { if i.StateForKey(ebiten.KeyZ) == 1 {
return true return true
} }
return i.stateForGamepadAbstractButton(abstractButtonButtonA) == 1 return i.stateForVirtualGamepadButton(virtualGamepadButtonButtonA) == 1
} }
func (i *Input) StateForLeft() int { func (i *Input) StateForLeft() int {
@ -111,7 +103,7 @@ func (i *Input) StateForLeft() int {
if 0 < v { if 0 < v {
return v return v
} }
return i.stateForGamepadAbstractButton(abstractButtonLeft) return i.stateForVirtualGamepadButton(virtualGamepadButtonLeft)
} }
func (i *Input) StateForRight() int { func (i *Input) StateForRight() int {
@ -119,7 +111,7 @@ func (i *Input) StateForRight() int {
if 0 < v { if 0 < v {
return v return v
} }
return i.stateForGamepadAbstractButton(abstractButtonRight) return i.stateForVirtualGamepadButton(virtualGamepadButtonRight)
} }
func (i *Input) StateForDown() int { func (i *Input) StateForDown() int {
@ -127,5 +119,5 @@ func (i *Input) StateForDown() int {
if 0 < v { if 0 < v {
return v return v
} }
return i.stateForGamepadAbstractButton(abstractButtonDown) return i.stateForVirtualGamepadButton(virtualGamepadButtonDown)
} }

View File

@ -75,78 +75,87 @@ type Piece struct {
blocks [][]bool blocks [][]bool
} }
func toBlocks(ints [][]int) [][]bool { func transpose(bs [][]bool) [][]bool {
blocks := make([][]bool, len(ints)) blocks := make([][]bool, len(bs))
for j, row := range ints { for j, row := range bs {
blocks[j] = make([]bool, len(row)) blocks[j] = make([]bool, len(row))
} }
// Tranpose the argument matrix. // Tranpose the argument matrix.
for i, col := range ints { for i, col := range bs {
for j, v := range col { for j, v := range col {
blocks[j][i] = v != 0 blocks[j][i] = v
} }
} }
return blocks return blocks
} }
var Pieces = map[BlockType]*Piece{ // Pieces is the set of all the possible pieces.
var Pieces map[BlockType]*Piece
func init() {
const (
f = false
t = true
)
Pieces = map[BlockType]*Piece{
BlockType1: { BlockType1: {
blockType: BlockType1, blockType: BlockType1,
blocks: toBlocks([][]int{ blocks: transpose([][]bool{
{0, 0, 0, 0}, {f, f, f, f},
{1, 1, 1, 1}, {t, t, t, t},
{0, 0, 0, 0}, {f, f, f, f},
{0, 0, 0, 0}, {f, f, f, f},
}), }),
}, },
BlockType2: { BlockType2: {
blockType: BlockType2, blockType: BlockType2,
blocks: toBlocks([][]int{ blocks: transpose([][]bool{
{1, 0, 0}, {t, f, f},
{1, 1, 1}, {t, t, t},
{0, 0, 0}, {f, f, f},
}), }),
}, },
BlockType3: { BlockType3: {
blockType: BlockType3, blockType: BlockType3,
blocks: toBlocks([][]int{ blocks: transpose([][]bool{
{0, 1, 0}, {f, t, f},
{1, 1, 1}, {t, t, t},
{0, 0, 0}, {f, f, f},
}), }),
}, },
BlockType4: { BlockType4: {
blockType: BlockType4, blockType: BlockType4,
blocks: toBlocks([][]int{ blocks: transpose([][]bool{
{0, 0, 1}, {f, f, t},
{1, 1, 1}, {t, t, t},
{0, 0, 0}, {f, f, f},
}), }),
}, },
BlockType5: { BlockType5: {
blockType: BlockType5, blockType: BlockType5,
blocks: toBlocks([][]int{ blocks: transpose([][]bool{
{1, 1, 0}, {t, t, f},
{0, 1, 1}, {f, t, t},
{0, 0, 0}, {f, f, f},
}), }),
}, },
BlockType6: { BlockType6: {
blockType: BlockType6, blockType: BlockType6,
blocks: toBlocks([][]int{ blocks: transpose([][]bool{
{0, 1, 1}, {f, t, t},
{1, 1, 0}, {t, t, f},
{0, 0, 0}, {f, f, f},
}), }),
}, },
BlockType7: { BlockType7: {
blockType: BlockType7, blockType: BlockType7,
blocks: toBlocks([][]int{ blocks: transpose([][]bool{
{1, 1}, {t, t},
{1, 1}, {t, t},
}), }),
}, },
} }
}
const ( const (
blockWidth = 10 blockWidth = 10
@ -155,24 +164,20 @@ const (
fieldBlockNumY = 20 fieldBlockNumY = 20
) )
func drawBlocks(r *ebiten.Image, blocks [][]BlockType, x, y int, clr ebiten.ColorM) { func drawBlock(r *ebiten.Image, block BlockType, x, y int, clr ebiten.ColorM) {
if block == BlockTypeNone {
return
}
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.ColorM = clr op.ColorM = clr
for j := 0; j < len(blocks[0]); j++ { op.GeoM.Translate(float64(x), float64(y))
for i := 0; i < len(blocks); i++ {
op.GeoM.Reset() srcX := (int(block) - 1) * blockWidth
op.GeoM.Translate(float64(i*blockWidth+x), float64(j*blockHeight+y)) src := image.Rect(srcX, 0, srcX+blockWidth, blockHeight)
block := blocks[i][j]
if block == BlockTypeNone {
continue
}
x := (int(block) - 1) * blockWidth
src := image.Rect(x, 0, x+blockWidth, blockHeight)
op.SourceRect = &src op.SourceRect = &src
r.DrawImage(imageBlocks, op) r.DrawImage(imageBlocks, op)
} }
}
}
func (p *Piece) InitialPosition() (int, int) { func (p *Piece) InitialPosition() (int, int) {
size := len(p.blocks) size := len(p.blocks)
@ -190,6 +195,8 @@ Loop:
return x, y return x, y
} }
// isBlocked returns a boolean value indicating whether
// there is a block at the position (x, y) of the piece with the given angle.
func (p *Piece) isBlocked(i, j int, angle Angle) bool { func (p *Piece) isBlocked(i, j int, angle Angle) bool {
size := len(p.blocks) size := len(p.blocks)
i2, j2 := i, j i2, j2 := i, j
@ -208,6 +215,8 @@ func (p *Piece) isBlocked(i, j int, angle Angle) bool {
return p.blocks[i2][j2] return p.blocks[i2][j2]
} }
// collides returns a boolean value indicating whether
// the piece at (x, y) with the given angle would collide with the field's blocks.
func (p *Piece) collides(field *Field, x, y int, angle Angle) bool { func (p *Piece) collides(field *Field, x, y int, angle Angle) bool {
size := len(p.blocks) size := len(p.blocks)
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
@ -220,10 +229,6 @@ func (p *Piece) collides(field *Field, x, y int, angle Angle) bool {
return false return false
} }
func (p *Piece) Collides(field *Field, x, y int, angle Angle) bool {
return p.collides(field, x, y, angle)
}
func (p *Piece) AbsorbInto(field *Field, x, y int, angle Angle) { func (p *Piece) AbsorbInto(field *Field, x, y int, angle Angle) {
size := len(p.blocks) size := len(p.blocks)
for i := 0; i < size; i++ { for i := 0; i < size; i++ {
@ -242,15 +247,12 @@ func (p *Piece) DrawAtCenter(r *ebiten.Image, x, y, width, height int, angle Ang
} }
func (p *Piece) Draw(r *ebiten.Image, x, y int, angle Angle) { func (p *Piece) Draw(r *ebiten.Image, x, y int, angle Angle) {
size := len(p.blocks)
blocks := make([][]BlockType, size)
for i := range p.blocks { for i := range p.blocks {
blocks[i] = make([]BlockType, size) for j := range p.blocks[i] {
for j := range blocks[i] {
if p.isBlocked(i, j, angle) { if p.isBlocked(i, j, angle) {
blocks[i][j] = p.blockType drawBlock(r, p.blockType, i*blockWidth+x, j*blockHeight+y, ebiten.ColorM{})
} }
} }
} }
drawBlocks(r, blocks, x, y, ebiten.ColorM{})
} }

View File

@ -43,31 +43,35 @@ type SceneManager struct {
transitionCount int transitionCount int
} }
func NewSceneManager(initScene Scene) *SceneManager { type GameState struct {
return &SceneManager{ SceneManager *SceneManager
current: initScene, Input *Input
transitionCount: -1,
}
} }
func (s *SceneManager) Update(state *GameState) error { func (s *SceneManager) Update(input *Input) error {
if s.transitionCount == -1 { if s.transitionCount == 0 {
return s.current.Update(state) return s.current.Update(&GameState{
SceneManager: s,
Input: input,
})
} }
s.transitionCount++
if transitionMaxCount <= s.transitionCount { s.transitionCount--
if s.transitionCount > 0 {
return nil
}
s.current = s.next s.current = s.next
s.next = nil s.next = nil
s.transitionCount = -1
}
return nil return nil
} }
func (s *SceneManager) Draw(r *ebiten.Image) { func (s *SceneManager) Draw(r *ebiten.Image) {
if s.transitionCount == -1 { if s.transitionCount == 0 {
s.current.Draw(r) s.current.Draw(r)
return return
} }
transitionFrom.Clear() transitionFrom.Clear()
s.current.Draw(transitionFrom) s.current.Draw(transitionFrom)
@ -76,13 +80,17 @@ func (s *SceneManager) Draw(r *ebiten.Image) {
r.DrawImage(transitionFrom, nil) r.DrawImage(transitionFrom, nil)
alpha := float64(s.transitionCount) / float64(transitionMaxCount) alpha := 1 - float64(s.transitionCount)/float64(transitionMaxCount)
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.ColorM.Scale(1, 1, 1, alpha) op.ColorM.Scale(1, 1, 1, alpha)
r.DrawImage(transitionTo, op) r.DrawImage(transitionTo, op)
} }
func (s *SceneManager) GoTo(scene Scene) { func (s *SceneManager) GoTo(scene Scene) {
if s.current == nil {
s.current = scene
} else {
s.next = scene s.next = scene
s.transitionCount = 0 s.transitionCount = transitionMaxCount
}
} }

View File

@ -38,13 +38,9 @@ type TitleScene struct {
count int count int
} }
func NewTitleScene() *TitleScene {
return &TitleScene{}
}
func anyGamepadAbstractButtonPressed(i *Input) bool { func anyGamepadAbstractButtonPressed(i *Input) bool {
for _, b := range gamepadAbstractButtons { for _, b := range virtualGamepadButtons {
if i.gamepadConfig.IsButtonPressed(0, b) { if i.gamepadConfig.IsButtonPressed(b) {
return true return true
} }
} }
@ -71,8 +67,11 @@ func (s *TitleScene) Update(state *GameState) error {
state.SceneManager.GoTo(NewGameScene()) state.SceneManager.GoTo(NewGameScene())
return nil return nil
} }
// If 'abstract' gamepad buttons are not set and any gamepad buttons are pressed,
// go to the gamepad configuration scene.
if anyGamepadButtonPressed(state.Input) { if anyGamepadButtonPressed(state.Input) {
state.SceneManager.GoTo(NewGamepadScene()) state.SceneManager.GoTo(&GamepadScene{})
return nil return nil
} }
return nil return nil
@ -93,7 +92,7 @@ func (s *TitleScene) drawTitleBackground(r *ebiten.Image, c int) {
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
for i := 0; i < (ScreenWidth/w+1)*(ScreenHeight/h+2); i++ { for i := 0; i < (ScreenWidth/w+1)*(ScreenHeight/h+2); i++ {
op.GeoM.Reset() op.GeoM.Reset()
dx := (-c / 4) % w dx := -(c / 4) % w
dy := (c / 4) % h dy := (c / 4) % h
dstX := (i%(ScreenWidth/w+1))*w + dx dstX := (i%(ScreenWidth/w+1))*w + dx
dstY := (i/(ScreenWidth/w+1)-1)*h + dy dstY := (i/(ScreenWidth/w+1)-1)*h + dy
@ -103,7 +102,7 @@ func (s *TitleScene) drawTitleBackground(r *ebiten.Image, c int) {
} }
func drawLogo(r *ebiten.Image, str string) { func drawLogo(r *ebiten.Image, str string) {
scale := 4 const scale = 4
textWidth := common.ArcadeFont.TextWidth(str) * scale textWidth := common.ArcadeFont.TextWidth(str) * scale
x := (ScreenWidth - textWidth) / 2 x := (ScreenWidth - textWidth) / 2
y := 32 y := 32

View File

@ -41,7 +41,7 @@ func main() {
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
} }
game := blocks.NewGame() game := &blocks.Game{}
update := game.Update update := game.Update
if err := ebiten.Run(update, blocks.ScreenWidth, blocks.ScreenHeight, 2, "Blocks (Ebiten Demo)"); err != nil { if err := ebiten.Run(update, blocks.ScreenWidth, blocks.ScreenHeight, 2, "Blocks (Ebiten Demo)"); err != nil {
log.Fatal(err) log.Fatal(err)