mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-15 07:27:32 +01:00
353 lines
8.2 KiB
Go
353 lines
8.2 KiB
Go
// Copyright 2019 The Ebiten Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// This demo is inspired by the xscreensaver 'squirals'.
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"log"
|
|
"math/rand/v2"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
|
)
|
|
|
|
const (
|
|
width = 800
|
|
height = 600
|
|
scale = 1
|
|
numOfSquirals = width / 32
|
|
)
|
|
|
|
type palette struct {
|
|
name string
|
|
colors []color.Color
|
|
}
|
|
|
|
var (
|
|
background = color.Black
|
|
|
|
palettes = []palette{
|
|
{
|
|
name: "sand dunes",
|
|
colors: []color.Color{
|
|
color.RGBA{0xF2, 0x74, 0x05, 0xFF}, // #F27405
|
|
color.RGBA{0xD9, 0x52, 0x04, 0xFF}, // #D95204
|
|
color.RGBA{0x40, 0x18, 0x01, 0xFF}, // #401801
|
|
color.RGBA{0xA6, 0x2F, 0x03, 0xFF}, // #A62F03
|
|
color.RGBA{0x73, 0x2A, 0x19, 0xFF}, // #732A19
|
|
},
|
|
},
|
|
{
|
|
name: "mono desert sand",
|
|
colors: []color.Color{
|
|
color.RGBA{0x7F, 0x6C, 0x52, 0xFF}, // #7F6C52
|
|
color.RGBA{0xFF, 0xBA, 0x58, 0xFF}, // #FFBA58
|
|
color.RGBA{0xFF, 0xD9, 0xA5, 0xFF}, // #FFD9A5
|
|
color.RGBA{0x7F, 0x50, 0x0F, 0xFF}, // #7F500F
|
|
color.RGBA{0xCC, 0xAE, 0x84, 0xFF}, // #CCAE84
|
|
},
|
|
},
|
|
{
|
|
name: "land sea gradient",
|
|
colors: []color.Color{
|
|
color.RGBA{0x00, 0xA2, 0xE8, 0xFF}, // #00A2E8
|
|
color.RGBA{0x67, 0xA3, 0xF5, 0xFF}, // #67A3F5
|
|
color.RGBA{0xFF, 0xFF, 0xD5, 0xFF}, // #FFFFD5
|
|
color.RGBA{0xDD, 0xE8, 0x0C, 0xFF}, // #DDE80C
|
|
color.RGBA{0x74, 0x9A, 0x0D, 0xFF}, // #749A0D
|
|
},
|
|
},
|
|
}
|
|
|
|
// blocker is an arbitrary color used to prevent the
|
|
// squirals from leaving the canvas.
|
|
blocker = color.RGBA{0, 0, 0, 254}
|
|
|
|
// dirCycles defines by offset which direction a squiral
|
|
// should try next for the two cases:
|
|
// clockwise:
|
|
// 1. try to turn right (index+1)
|
|
// 2. try to go straight (index+0)
|
|
// 3. try to turn left (index+3)
|
|
// counter-clockwise:
|
|
// 1. try to turn left (index+3)
|
|
// 2. try to go straight (index+0)
|
|
// 3. try to turn right (index+1)
|
|
dirCycles = [2][3]int{
|
|
{1, 0, 3}, // cw
|
|
{3, 0, 1}, // ccw
|
|
}
|
|
|
|
// dirs contains vectors for the directions: east, south, west, north
|
|
// in the specified order.
|
|
dirs = [4]vec2{{1, 0}, {0, 1}, {-1, 0}, {0, -1}}
|
|
|
|
// neighbors defines neighboring cells depending on the moving
|
|
// direction of the squiral:
|
|
// index of 0 -> squiral moves vertically,
|
|
// index of 1 -> squiral moves horizontally.
|
|
// These neighbors are tested for "collisions" during simulation.
|
|
neighbors = [2][2]vec2{
|
|
{{0, 1}, {0, -1}}, // east, west
|
|
{{1, 0}, {-1, 0}}, // south, north
|
|
}
|
|
)
|
|
|
|
type vec2 struct {
|
|
x int
|
|
y int
|
|
}
|
|
|
|
type squiral struct {
|
|
speed int
|
|
pos vec2
|
|
dir int
|
|
rot int
|
|
col color.Color
|
|
dead bool
|
|
}
|
|
|
|
func (s *squiral) spawn(game *Game) {
|
|
s.dead = false
|
|
|
|
rx := rand.IntN(width-4) + 2
|
|
ry := rand.IntN(height-4) + 2
|
|
|
|
for dx := -2; dx <= 2; dx++ {
|
|
for dy := -2; dy <= 2; dy++ {
|
|
tx, ty := rx+dx, ry+dy
|
|
if game.auto.colorMap[tx][ty] != background {
|
|
s.dead = true
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
s.speed = rand.IntN(5) + 1
|
|
s.pos.x = rx
|
|
s.pos.y = ry
|
|
s.dir = rand.IntN(4)
|
|
|
|
game.colorCycle = (game.colorCycle + 1) % len(palettes[game.selectedPalette].colors)
|
|
s.col = palettes[game.selectedPalette].colors[game.colorCycle]
|
|
|
|
s.rot = rand.IntN(2)
|
|
}
|
|
|
|
func (s *squiral) step(game *Game) {
|
|
if s.dead {
|
|
return
|
|
}
|
|
x, y := s.pos.x, s.pos.y // shorthands
|
|
|
|
change := rand.IntN(1000)
|
|
if change < 2 {
|
|
// On 0.2% of iterations, switch rotation direction.
|
|
s.rot = (s.rot + 1) % 2
|
|
}
|
|
|
|
// 1. try to advance the spiral in its rotation
|
|
// direction (clockwise or counter-clockwise).
|
|
for _, next := range dirCycles[s.rot] {
|
|
dir := (s.dir + next) % 4
|
|
off := dirs[dir]
|
|
// Peek all targets by priority.
|
|
target := vec2{
|
|
x: x + off.x,
|
|
y: y + off.y,
|
|
}
|
|
if game.auto.colorMap[target.x][target.y] == background {
|
|
// If the target is free we need to also check the
|
|
// surrounding cells.
|
|
|
|
// a. Test if next cell in direction dir does not have
|
|
// the same color as this squiral.
|
|
ntarg := vec2{
|
|
x: target.x + off.x,
|
|
y: target.y + off.y,
|
|
}
|
|
if game.auto.colorMap[ntarg.x][ntarg.y] == s.col {
|
|
// If this has the same color, we cannot go into this direction,
|
|
// to avoid ugly blocks of equal color.
|
|
continue // try next direction
|
|
}
|
|
|
|
// b. Test all outer fields for the color of the
|
|
// squiral itself.
|
|
horivert := dir % 2
|
|
xtarg := vec2{}
|
|
set := true
|
|
for _, out := range neighbors[horivert] {
|
|
xtarg.x = target.x + out.x
|
|
xtarg.y = target.y + out.y
|
|
|
|
// If one of the outer targets equals the squiral's
|
|
// color, again continue with next direction.
|
|
if game.auto.colorMap[xtarg.x][xtarg.y] == s.col {
|
|
// If this is not free we cannot go into this direction.
|
|
set = false
|
|
break // try next direction
|
|
}
|
|
|
|
xtarg.x = ntarg.x + out.x
|
|
xtarg.y = ntarg.y + out.y
|
|
|
|
// If one of the outer targets equals the squiral's
|
|
// color, again continue with next direction.
|
|
if game.auto.colorMap[xtarg.x][xtarg.y] == s.col {
|
|
// If this is not free we cannot go into this direction.
|
|
set = false
|
|
break // try next direction
|
|
}
|
|
}
|
|
|
|
if set {
|
|
s.pos = target
|
|
s.dir = dir
|
|
// 2. set the color of this squiral to its
|
|
// current position.
|
|
game.setpix(s.pos, s.col)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
s.dead = true
|
|
}
|
|
|
|
type automaton struct {
|
|
squirals [numOfSquirals]squiral
|
|
colorMap [width][height]color.Color
|
|
}
|
|
|
|
func (au *automaton) init(game *Game) {
|
|
// Init the test grid with color (0,0,0,0) and the borders of
|
|
// it with color(0,0,0,254) as a blocker color, so the squirals
|
|
// cannot escape the scene.
|
|
for x := 0; x < width; x++ {
|
|
for y := 0; y < height; y++ {
|
|
if x == 0 || x == width-1 || y == 0 || y == height-1 {
|
|
au.colorMap[x][y] = blocker
|
|
} else {
|
|
au.colorMap[x][y] = background
|
|
}
|
|
}
|
|
}
|
|
|
|
for i := 0; i < numOfSquirals; i++ {
|
|
au.squirals[i].spawn(game)
|
|
}
|
|
}
|
|
|
|
func (a *automaton) step(game *Game) {
|
|
for i := 0; i < numOfSquirals; i++ {
|
|
for s := 0; s < a.squirals[i].speed; s++ {
|
|
a.squirals[i].step(game)
|
|
if a.squirals[i].dead {
|
|
a.squirals[i].spawn(game)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
type Game struct {
|
|
selectedPalette int
|
|
colorCycle int
|
|
canvas *ebiten.Image
|
|
auto automaton
|
|
}
|
|
|
|
func NewGame() *Game {
|
|
g := &Game{
|
|
canvas: ebiten.NewImage(width, height),
|
|
}
|
|
g.canvas.Fill(background)
|
|
g.auto.init(g)
|
|
return g
|
|
}
|
|
|
|
func (g *Game) setpix(xy vec2, col color.Color) {
|
|
g.canvas.Set(xy.x, xy.y, col)
|
|
g.auto.colorMap[xy.x][xy.y] = col
|
|
}
|
|
|
|
func (g *Game) Update() error {
|
|
reset := false
|
|
|
|
if inpututil.IsKeyJustPressed(ebiten.KeyB) {
|
|
if background == color.White {
|
|
background = color.Black
|
|
} else {
|
|
background = color.White
|
|
}
|
|
reset = true
|
|
} else if inpututil.IsKeyJustPressed(ebiten.KeyT) {
|
|
g.selectedPalette = (g.selectedPalette + 1) % len(palettes)
|
|
reset = true
|
|
} else if inpututil.IsKeyJustPressed(ebiten.KeyR) {
|
|
reset = true
|
|
}
|
|
|
|
if reset {
|
|
g.canvas.Fill(background)
|
|
g.auto.init(g)
|
|
}
|
|
|
|
g.auto.step(g)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g *Game) Draw(screen *ebiten.Image) {
|
|
screen.DrawImage(g.canvas, nil)
|
|
ebitenutil.DebugPrintAt(
|
|
screen,
|
|
fmt.Sprintf("TPS: %0.2f, FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()),
|
|
1, 0,
|
|
)
|
|
ebitenutil.DebugPrintAt(
|
|
screen,
|
|
"[r]: respawn",
|
|
1, 16,
|
|
)
|
|
ebitenutil.DebugPrintAt(
|
|
screen,
|
|
"[b]: toggle background (white/black)",
|
|
1, 32,
|
|
)
|
|
ebitenutil.DebugPrintAt(
|
|
screen,
|
|
fmt.Sprintf("[t]: cycle theme (current: %s)", palettes[g.selectedPalette].name),
|
|
1, 48,
|
|
)
|
|
}
|
|
|
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
|
return width, height
|
|
}
|
|
|
|
func main() {
|
|
ebiten.SetTPS(250)
|
|
ebiten.SetWindowSize(width*scale, height*scale)
|
|
ebiten.SetWindowTitle("Squirals (Ebitengine Demo)")
|
|
if err := ebiten.RunGame(NewGame()); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|